Superpowered to do lists. No signup required.

Merge pull request #20 from zeucapua/feat/task-timer

✨ feat: implement stopwatch per task

authored by zeu.dev and committed by GitHub 979919ce 58fd42c1

Changed files
+69 -8
src
lib
routes
+18
src/lib/stores.svelte.ts
··· 35 id: string; 36 description: string; 37 is_completed: boolean; 38 } 39 40 export type List = { ··· 59 export function generateId() { 60 return generateRandomString(10, alphabet("a-z", "0-9")); 61 }
··· 35 id: string; 36 description: string; 37 is_completed: boolean; 38 + // optional 39 + duration?: number; 40 + stopwatchInterval?: number; 41 } 42 43 export type List = { ··· 62 export function generateId() { 63 return generateRandomString(10, alphabet("a-z", "0-9")); 64 } 65 + 66 + export function formatSecondsToDuration(seconds: number = 0) { 67 + let hours = Math.floor(seconds / 3600); 68 + let minutes = Math.floor((seconds - (hours * 3600)) / 60); 69 + seconds = seconds - (hours * 3600) - (minutes * 60); 70 + 71 + // string ver. 72 + let hrs, mins, secs; 73 + 74 + if (hours < 10) { hrs = "0" + hours; } else { hrs = hours; } 75 + if (minutes < 10) { mins = "0" + minutes; } else { mins = minutes; } 76 + if (seconds < 10) { secs = "0" + seconds; } else { secs = seconds; } 77 + 78 + return hrs + ':' + mins + ':' + secs ; 79 + }
+51 -8
src/routes/[id]/+page.svelte
··· 1 <script lang="ts"> 2 import { page } from "$app/state"; 3 - import { local_lists, pinned_list, generateId, type List } from "$lib/stores.svelte"; 4 import { goto } from "$app/navigation"; 5 import toast from "svelte-french-toast"; 6 7 let is_menu_open = $state(false); 8 let list : List | undefined = $state(local_lists.value!.find((l) => l.id === page.params.id)); ··· 34 } 35 } 36 37 function createList() { 38 const new_list = { 39 id: generateId(), ··· 65 list = local_lists.value.find((l) => l.id === pinned_list.value); 66 goto(`/${list!.id}`); 67 } 68 </script> 69 70 <main class="flex flex-col w-full px-2 pt-8 pb-28 lg:px-4 lg:pt-4 gap-8 text-xl lg:text-3xl"> ··· 122 </menu> 123 {/if} 124 </section> 125 - <input 126 - type="text" 127 - bind:value={list.title} 128 - placeholder="Untitled" 129 - class="text-5xl font-bold bg-transparent" 130 - /> 131 <ul class="flex flex-col gap-4"> 132 {#each list.tasks as task (task.id)} 133 <li class="group flex justify-between items-center gap-4"> ··· 144 /> 145 </div> 146 147 - <div class="flex gap-4 w-fit"> 148 <button 149 onclick={() => deleteTask(task.id)} 150 class="px-4 py-2 bg-red-500 rounded-xl text-white"
··· 1 <script lang="ts"> 2 import { page } from "$app/state"; 3 + import { local_lists, pinned_list, generateId, type List, type Task, formatSecondsToDuration } from "$lib/stores.svelte"; 4 import { goto } from "$app/navigation"; 5 import toast from "svelte-french-toast"; 6 + import { onMount } from "svelte"; 7 8 let is_menu_open = $state(false); 9 let list : List | undefined = $state(local_lists.value!.find((l) => l.id === page.params.id)); ··· 35 } 36 } 37 38 + function toggleInterval(id: string) { 39 + if (list) { 40 + const task = list.tasks.find((t) => t.id === id) as Task; 41 + if (task.stopwatchInterval) { 42 + clearInterval(task.stopwatchInterval); 43 + task.stopwatchInterval = undefined; 44 + } 45 + else { 46 + if (!task.duration) { task.duration = 0; } 47 + const interval = setInterval(() => { 48 + // @ts-ignore 49 + task.duration += 1; 50 + }, 1000); 51 + task.stopwatchInterval = interval; 52 + } 53 + } 54 + } 55 + 56 function createList() { 57 const new_list = { 58 id: generateId(), ··· 84 list = local_lists.value.find((l) => l.id === pinned_list.value); 85 goto(`/${list!.id}`); 86 } 87 + 88 + onMount(() => { 89 + if (list) { 90 + for (const task of list.tasks) { 91 + // if a task's stopwatch is still running 92 + // remove it so the user can start it again in one click 93 + // instead of two cause the first `toggleInterval` would 94 + // just remove the interval 95 + if (task.stopwatchInterval) { 96 + clearInterval(task.stopwatchInterval); 97 + task.stopwatchInterval = undefined; 98 + local_lists.update(); 99 + } 100 + } 101 + } 102 + }); 103 </script> 104 105 <main class="flex flex-col w-full px-2 pt-8 pb-28 lg:px-4 lg:pt-4 gap-8 text-xl lg:text-3xl"> ··· 157 </menu> 158 {/if} 159 </section> 160 + 161 + <input 162 + type="text" 163 + bind:value={list.title} 164 + placeholder="Untitled" 165 + class="text-5xl font-bold bg-transparent" 166 + /> 167 + 168 <ul class="flex flex-col gap-4"> 169 {#each list.tasks as task (task.id)} 170 <li class="group flex justify-between items-center gap-4"> ··· 181 /> 182 </div> 183 184 + <div class="flex gap-4 w-fit items-center"> 185 + <button 186 + onclick={() => toggleInterval(task.id)} 187 + class="w-fit h-fit tabular-nums text-lg" 188 + > 189 + {formatSecondsToDuration(task.duration!)} 190 + </button> 191 <button 192 onclick={() => deleteTask(task.id)} 193 class="px-4 py-2 bg-red-500 rounded-xl text-white"