From 96ff16b7a777e42ad66cd63752886932ccdeb062 Mon Sep 17 00:00:00 2001 From: Till JS Date: Thu, 26 Mar 2026 19:57:26 +0100 Subject: [PATCH] feat(zitare): add loading states to list operations Add spinner indicators and disable buttons during create, delete, update, add quotes, and remove quote operations to prevent double clicks and give visual feedback. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../web/src/routes/(app)/lists/+page.svelte | 46 +++++++++---- .../src/routes/(app)/lists/[id]/+page.svelte | 65 ++++++++++++++----- 2 files changed, 82 insertions(+), 29 deletions(-) diff --git a/apps/zitare/apps/web/src/routes/(app)/lists/+page.svelte b/apps/zitare/apps/web/src/routes/(app)/lists/+page.svelte index 05ccbd0e2..4127b8a8a 100644 --- a/apps/zitare/apps/web/src/routes/(app)/lists/+page.svelte +++ b/apps/zitare/apps/web/src/routes/(app)/lists/+page.svelte @@ -16,6 +16,8 @@ let lists = $state([]); let loading = $state(true); + let saving = $state(false); + let deletingId = $state(null); let showCreateModal = $state(false); let newListName = $state(''); let newListDescription = $state(''); @@ -58,11 +60,12 @@ } async function createList() { - if (!newListName.trim()) return; + if (!newListName.trim() || saving) return; const token = await authStore.getValidToken(); if (!token) return; + saving = true; try { const response = await fetch(`${getBackendUrl()}/api/lists`, { method: 'POST', @@ -86,15 +89,18 @@ } } catch (error) { console.error('Failed to create list:', error); + } finally { + saving = false; } } async function deleteList(listId: string) { - if (!confirm($_('lists.confirmDelete'))) return; + if (deletingId || !confirm($_('lists.confirmDelete'))) return; const token = await authStore.getValidToken(); if (!token) return; + deletingId = listId; try { const response = await fetch(`${getBackendUrl()}/api/lists/${listId}`, { method: 'DELETE', @@ -107,6 +113,8 @@ } } catch (error) { console.error('Failed to delete list:', error); + } finally { + deletingId = null; } } @@ -214,16 +222,23 @@ e.stopPropagation(); deleteList(list.id); }} - class="p-2 text-foreground-muted hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg transition-colors" + disabled={deletingId === list.id} + class="p-2 text-foreground-muted hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg transition-colors disabled:opacity-50" > - - - + {#if deletingId === list.id} +
+ {:else} + + + + {/if} @@ -285,9 +300,14 @@ diff --git a/apps/zitare/apps/web/src/routes/(app)/lists/[id]/+page.svelte b/apps/zitare/apps/web/src/routes/(app)/lists/[id]/+page.svelte index 3d172c151..edbd261de 100644 --- a/apps/zitare/apps/web/src/routes/(app)/lists/[id]/+page.svelte +++ b/apps/zitare/apps/web/src/routes/(app)/lists/[id]/+page.svelte @@ -13,6 +13,9 @@ let list = $state(null); let isLoading = $state(true); + let isSaving = $state(false); + let isAdding = $state(false); + let removingQuoteId = $state(null); let searchTerm = $state(''); let isSearchOpen = $state(false); let showEditModal = $state(false); @@ -83,7 +86,9 @@ } async function handleUpdateList() { - if (list && editName.trim()) { + if (!list || !editName.trim() || isSaving) return; + isSaving = true; + try { const updated = await listsStore.updateList(list.id, { name: editName.trim(), description: editDescription.trim() || undefined, @@ -95,6 +100,8 @@ } else { toast.error($_('lists.detail.toast.updateError')); } + } finally { + isSaving = false; } } @@ -130,32 +137,37 @@ } async function handleAddQuotes() { - if (list) { - const count = selectedQuoteIds.size; + if (!list || isAdding) return; + isAdding = true; + try { let successCount = 0; for (const quoteId of selectedQuoteIds) { const success = await listsStore.addQuoteToList(list.id, quoteId); if (success) successCount++; } if (successCount > 0) { - // Reload list to get updated quote IDs list = await listsStore.getList(list.id); toast.success($_('lists.detail.toast.quotesAdded', { values: { count: successCount } })); } closeAddQuotesModal(); + } finally { + isAdding = false; } } async function handleRemoveQuote(quoteId: string) { - if (list && confirm($_('lists.detail.removeConfirm'))) { + if (!list || removingQuoteId || !confirm($_('lists.detail.removeConfirm'))) return; + removingQuoteId = quoteId; + try { const success = await listsStore.removeQuoteFromList(list.id, quoteId); if (success) { - // Reload list to get updated quote IDs list = await listsStore.getList(list.id); toast.info($_('lists.detail.toast.quoteRemoved')); } else { toast.error($_('lists.detail.toast.removeError')); } + } finally { + removingQuoteId = null; } } @@ -339,16 +351,23 @@ @@ -422,7 +441,16 @@ @@ -480,8 +508,13 @@