mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 00:41:09 +02:00
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) <noreply@anthropic.com>
This commit is contained in:
parent
f5a9edcfb6
commit
96ff16b7a7
2 changed files with 82 additions and 29 deletions
|
|
@ -16,6 +16,8 @@
|
|||
|
||||
let lists = $state<QuoteList[]>([]);
|
||||
let loading = $state(true);
|
||||
let saving = $state(false);
|
||||
let deletingId = $state<string | null>(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"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
{#if deletingId === list.id}
|
||||
<div
|
||||
class="w-5 h-5 border-2 border-red-400 border-t-transparent rounded-full animate-spin"
|
||||
></div>
|
||||
{:else}
|
||||
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</a>
|
||||
|
|
@ -285,9 +300,14 @@
|
|||
</button>
|
||||
<button
|
||||
onclick={createList}
|
||||
disabled={!newListName.trim()}
|
||||
class="px-6 py-2 bg-primary text-white rounded-lg font-medium hover:bg-primary-hover transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
disabled={!newListName.trim() || saving}
|
||||
class="px-6 py-2 bg-primary text-white rounded-lg font-medium hover:bg-primary-hover transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
||||
>
|
||||
{#if saving}
|
||||
<div
|
||||
class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"
|
||||
></div>
|
||||
{/if}
|
||||
{$_('lists.createModal.submit')}
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@
|
|||
|
||||
let list = $state<QuoteList | null>(null);
|
||||
let isLoading = $state(true);
|
||||
let isSaving = $state(false);
|
||||
let isAdding = $state(false);
|
||||
let removingQuoteId = $state<string | null>(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 @@
|
|||
<button
|
||||
class="remove-btn"
|
||||
onclick={() => handleRemoveQuote(quote.id)}
|
||||
disabled={removingQuoteId === quote.id}
|
||||
aria-label={$_('lists.detail.remove')}
|
||||
>
|
||||
<svg width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
{#if removingQuoteId === quote.id}
|
||||
<div
|
||||
class="w-4 h-4 border-2 border-red-400 border-t-transparent rounded-full animate-spin"
|
||||
></div>
|
||||
{:else}
|
||||
<svg width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
{$_('lists.detail.remove')}
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -422,7 +441,16 @@
|
|||
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" onclick={closeEditModal}>{$_('common.cancel')}</button>
|
||||
<button class="btn btn-primary" onclick={handleUpdateList} disabled={!editName.trim()}>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
onclick={handleUpdateList}
|
||||
disabled={!editName.trim() || isSaving}
|
||||
>
|
||||
{#if isSaving}
|
||||
<div
|
||||
class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin inline-block mr-1"
|
||||
></div>
|
||||
{/if}
|
||||
{$_('common.save')}
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -480,8 +508,13 @@
|
|||
<button
|
||||
class="btn btn-primary"
|
||||
onclick={handleAddQuotes}
|
||||
disabled={selectedQuoteIds.size === 0}
|
||||
disabled={selectedQuoteIds.size === 0 || isAdding}
|
||||
>
|
||||
{#if isAdding}
|
||||
<div
|
||||
class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin inline-block mr-1"
|
||||
></div>
|
||||
{/if}
|
||||
{$_('lists.detail.addModal.submit', { values: { count: selectedQuoteIds.size } })}
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue