Superpowered to do lists. No signup required.

✨ working multiple lists

Changed files
+83 -41
src
+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
··· 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
··· 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>