+10
-11
src/lib/stores.svelte.ts
+10
-11
src/lib/stores.svelte.ts
···
3
3
const storage = browser_exists ? localStorage : null;
4
4
5
5
// Generalized Local Storage
6
-
function persisted<T>(key: string, default_value: T) {
6
+
export function persisted<T>(key: string, default_value: T) {
7
7
let value : T = $state(default_value);
8
8
9
9
const initial_local = storage?.getItem(key);
10
10
if (initial_local) {
11
-
try {
12
11
value = JSON.parse(initial_local).value as T;
13
-
if (!value) { update(default_value); }
14
-
}
15
-
catch (e) {
16
-
update(default_value);
17
-
}
12
+
if (!value) { update(); }
13
+
}
14
+
else {
15
+
storage?.setItem(key, JSON.stringify(default_value));
18
16
}
19
17
20
-
function update(new_value : T) {
18
+
function update() {
21
19
if (browser_exists) {
22
-
value = new_value;
23
20
storage?.setItem(key, JSON.stringify({ value }));
24
21
}
25
22
}
26
23
27
24
return {
28
25
get value() { return value; },
26
+
set value(new_value) { value = new_value; update(); },
29
27
update
30
28
}
31
29
}
···
38
36
id: string;
39
37
description: string;
40
38
is_completed: boolean;
39
+
list_id: string;
41
40
}
42
41
43
42
export type List = {
44
43
id: string;
45
44
title: string;
46
-
tasks: Task[]
45
+
tasks: Task[];
47
46
}
48
47
49
-
export const todo_list = persisted<Task[]>("local_todo_list", []);
48
+
export const local_lists = persisted<List[]>("local_lists", [{ id: crypto.randomUUID(), title: "", tasks: [] }]);
+1
-4
src/routes/+layout.svelte
+1
-4
src/routes/+layout.svelte
···
42
42
"nord",
43
43
"sunset",
44
44
];
45
-
46
-
let theme_input = $state(color_theme.value);
47
-
$effect(() => color_theme.update(theme_input));
48
45
</script>
49
46
50
47
<svelte:head>
···
74
71
<input
75
72
type="radio"
76
73
aria-label={theme}
77
-
bind:group={theme_input}
74
+
bind:group={color_theme.value}
78
75
value={theme}
79
76
class="theme-controller btn btn-sm btn-block btn-ghost justify-start"
80
77
/>
+72
-26
src/routes/+page.svelte
+72
-26
src/routes/+page.svelte
···
1
1
<script lang="ts">
2
-
import { todo_list, type Task } from "$lib/stores.svelte";
3
-
4
-
$effect(() => console.log(todo_list));
2
+
import { type Task, local_lists } from "$lib/stores.svelte";
5
3
6
4
// TODO: get better ID management
7
-
let id = $state(0);
8
5
let description = $state("");
9
6
10
-
function randomizeId() {
11
-
let random_id = id;
12
-
while (todo_list.value.find(t => t.id === `${random_id}`)) {
13
-
random_id = Math.floor(Math.random() * 100);
14
-
}
15
-
id = random_id;
16
-
}
7
+
let current_list = $state(local_lists.value[0]);
17
8
18
9
function addTask() {
19
-
randomizeId();
20
-
todo_list.update(
21
-
[...todo_list.value, {
22
-
id: `${id}`,
23
-
description,
24
-
is_completed: false
25
-
}]
26
-
);
27
-
description = "";
10
+
current_list.tasks.push({
11
+
id: crypto.randomUUID(),
12
+
description,
13
+
is_completed: false,
14
+
list_id: current_list.id
15
+
});
28
16
}
29
17
30
18
function removeTask(id: string) {
31
-
todo_list.update(
32
-
todo_list.value.filter((t) => t.id !== id)
33
-
);
19
+
current_list.tasks = current_list.tasks.filter(t => t.id !== id);
34
20
}
35
21
36
-
// update when tasks change (description, is_completed)
37
-
$effect(() => todo_list.update(todo_list.value));
22
+
function createList() {
23
+
current_list = { id: crypto.randomUUID(), title: "", tasks: [] };
24
+
local_lists.value.push(current_list);
25
+
}
26
+
27
+
function removeList(id: string) {
28
+
local_lists.value = local_lists.value.filter(l => l.id !== id);
29
+
if (local_lists.value.length === 0) {
30
+
createList();
31
+
}
32
+
current_list = local_lists.value[0];
33
+
}
34
+
35
+
$effect(() => {
36
+
local_lists.update();
37
+
});
38
38
</script>
39
39
40
40
<ul class="flex flex-col gap-8 overflow-y-scroll h-full max-h-[48rem] p-2">
41
-
{#each todo_list.value as task : Task}
41
+
{#each current_list.tasks as task : Task}
42
42
<li class="group flex gap-4 items-center">
43
43
<input
44
44
type="checkbox"
···
74
74
<button onclick={addTask} class="btn lg:btn-lg btn-primary">Add</button>
75
75
</section>
76
76
77
+
<section class="flex gap-4 justify-evenly">
78
+
<details class="dropdown dropdown-top">
79
+
<summary class="btn btn-secondary">
80
+
<img
81
+
src="/cog.svg"
82
+
alt="Flex Solid 'Cog' by StreamlineHQ"
83
+
class="w-6"
84
+
/>
85
+
</summary>
86
+
<ul class="p-2 shadow menu dropdown-content z-[1] bg-base-100 rounded-box w-52">
87
+
{#each local_lists.value as list : List}
88
+
<li>
89
+
<input
90
+
type="radio"
91
+
aria-label={list.title.length === 0 ? "Untitled" : list.title}
92
+
bind:group={current_list}
93
+
value={list}
94
+
class="btn btn-sm btn-block btn-ghost justify-start"
95
+
/>
96
+
</li>
97
+
{/each}
98
+
<li>
99
+
<button class="" onclick={createList}>
100
+
+ New list
101
+
</button>
102
+
</li>
103
+
</ul>
104
+
</details>
105
+
106
+
<input
107
+
type="text"
108
+
bind:value={current_list.title}
109
+
class="text-center w-fit input"
110
+
placeholder="Untitled List"
111
+
/>
112
+
<button
113
+
onclick={() => removeList(current_list.id)}
114
+
class="btn btn-error"
115
+
>
116
+
<img
117
+
src="/block-2.svg"
118
+
alt="Flex Solid 'Block 2' by StreamlineHQ"
119
+
class="w-4 lg:w-6"
120
+
/>
121
+
</button>
122
+
</section>