From d9e2aeff99dcc46db2b8397a5db867daf75719ac Mon Sep 17 00:00:00 2001 From: Till JS Date: Sat, 28 Mar 2026 18:15:26 +0100 Subject: [PATCH] fix(todo): homepage loading state + completed date on task items MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Homepage fixes: - Add loading state: show TaskListSkeleton while liveQuery loads - Fix $derived(() => ...) anti-pattern → $derived.by() - Stabilize date calculations (compute once, not per re-render) - Remove double error check (mutation errors shown via toast) TaskItem improvements: - Show completed-at date (small, 50% opacity) on right side of completed tasks - Click completed date to toggle showing created-at date above it Shared: Add `loading` field to useLiveQueryWithDefault (was missing, prevented proper loading states in consumers) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../web/src/lib/components/TaskItem.svelte | 66 ++++++++++++++++++- .../apps/web/src/routes/(app)/+page.svelte | 50 +++++--------- .../local-store/src/svelte/reactive.svelte.ts | 8 ++- 3 files changed, 89 insertions(+), 35 deletions(-) diff --git a/apps/todo/apps/web/src/lib/components/TaskItem.svelte b/apps/todo/apps/web/src/lib/components/TaskItem.svelte index 45655d8e0..18c692db7 100644 --- a/apps/todo/apps/web/src/lib/components/TaskItem.svelte +++ b/apps/todo/apps/web/src/lib/components/TaskItem.svelte @@ -51,6 +51,9 @@ onSave, }: Props = $props(); + // Toggle for showing created date on completed tasks + let showCreatedDate = $state(false); + // Form state for expanded mode let title = $state(''); let description = $state(''); @@ -481,8 +484,29 @@ {/if} - - {#if dueDateText()} + + {#if task.isCompleted && task.completedAt} + + {:else if dueDateText()} { if (!task.dueDate || task.isCompleted) return false; const taskDate = startOfDay(new Date(task.dueDate)); - return taskDate.getTime() === tomorrowDate.getTime(); + return taskDate.getTime() === tomorrow.getTime(); }) ) ); // Group upcoming tasks by day (starting from day after tomorrow) - let groupedUpcomingTasks = $derived(() => { + let groupedUpcomingTasks = $derived.by(() => { const groups: { date: Date; label: string; tasks: Task[] }[] = []; - const today = startOfDay(new Date()); - // Start from day after tomorrow (day 2) through day 7 for (let i = 2; i <= 7; i++) { const date = addDays(today, i); const dayTasks = applyFilters( @@ -77,7 +79,7 @@ // Total upcoming count (excluding tomorrow) let upcomingCount = $derived( - groupedUpcomingTasks().reduce((sum, group) => sum + group.tasks.length, 0) + groupedUpcomingTasks.reduce((sum, group) => sum + group.tasks.length, 0) ); // Check if all sections are empty @@ -89,7 +91,7 @@ completedTasks.length === 0 ); - // Section visibility logic - show only sections with tasks (except "Today" which is always shown when not all empty) + // Section visibility logic let showTodaySection = $derived(todayTasks.length > 0 || !allEmpty); let showTomorrowSection = $derived(tomorrowTasks.length > 0); let showUpcomingSection = $derived(upcomingCount > 0); @@ -108,12 +110,11 @@ { text: 'Wichtig erledigen !hoch', description: 'Mit Priorität' }, ]; - // Handle clicking a syntax example function handleExampleClick(text: string) { window.dispatchEvent(new CustomEvent('quick-input-set', { detail: { text } })); } - // Drag and drop handler - uses optimistic updates for smooth UX + // Drag and drop handler async function handleTaskDrop(taskId: string, targetDate: Date | 'completed' | 'overdue') { const task = allTasks.value.find((t) => t.id === taskId); if (!task) return; @@ -123,7 +124,7 @@ await tasksStore.updateTaskOptimistic(taskId, { isCompleted: true }); } } else if (targetDate === 'overdue') { - const yesterday = subDays(startOfDay(new Date()), 1); + const yesterday = subDays(today, 1); await tasksStore.updateTaskOptimistic(taskId, { dueDate: yesterday.toISOString(), isCompleted: task.isCompleted ? false : undefined, @@ -142,27 +143,22 @@
- {#if allTasks.error} + {#if allTasks.loading} + + {:else if allTasks.error}
{allTasks.error}
- {:else if tasksStore.error} -
- {tasksStore.error} -
{:else if allEmpty}
-
-

Bereit für einen produktiven Tag

-

Tippe unten um loszulegen...

@@ -170,7 +166,6 @@
-

Schnellstart-Tipps

@@ -192,7 +187,6 @@
- {#if overdueTasks.length > 0} {/if} - {#if showTodaySection} {/if} - {#if showTomorrowSection} {/if} - {#if showUpcomingSection}
- {#each groupedUpcomingTasks() as group} + {#each groupedUpcomingTasks as group}

{group.label} @@ -273,7 +264,6 @@ {/if} - {#if showCompletedSection} {/if} - {#if showOnboardingTip && !tipDismissed}
💡 @@ -327,7 +316,6 @@ padding-bottom: 100px; } - /* Empty state container */ .empty-state-container { display: flex; justify-content: center; @@ -441,7 +429,6 @@ transform: translateY(0); } - /* Onboarding tip */ .onboarding-tip { display: flex; align-items: center; @@ -488,7 +475,6 @@ color: hsl(var(--color-primary)); } - /* Notepad container */ .notepad { max-width: 560px; margin: 0 auto; diff --git a/packages/local-store/src/svelte/reactive.svelte.ts b/packages/local-store/src/svelte/reactive.svelte.ts index 14e9fc467..02cc825c7 100644 --- a/packages/local-store/src/svelte/reactive.svelte.ts +++ b/packages/local-store/src/svelte/reactive.svelte.ts @@ -82,8 +82,9 @@ export function useLiveQuery(querier: () => T | Promise): LiveQueryResult< export function useLiveQueryWithDefault( querier: () => T | Promise, defaultValue: T -): { readonly value: T; readonly error: unknown } { +): { readonly value: T; readonly loading: boolean; readonly error: unknown } { let value = $state(defaultValue); + let loading = $state(true); let error = $state(undefined); const observable: Observable = liveQuery(querier); @@ -91,10 +92,12 @@ export function useLiveQueryWithDefault( const subscription = observable.subscribe({ next: (result) => { value = result; + loading = false; error = undefined; }, error: (err) => { error = err; + loading = false; }, }); @@ -106,6 +109,9 @@ export function useLiveQueryWithDefault( get value() { return value; }, + get loading() { + return loading; + }, get error() { return error; },