Add permanent list delete; move stores out of main nav
- Add DELETE /api/lists/{id}/permanent endpoint for hard-delete alongside existing soft-delete (archive)
- Add Delete button with confirmation on list detail page next to Archive
- Handle ListDeleted SignalR event on lists overview for real-time removal
- Remove Stores from bottom nav; add Manage stores link at bottom of Lists page
- Add back-to-lists navigation on the Stores page
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -185,6 +185,20 @@ public static class ShoppingListEndpoints
|
|||||||
return Results.NoContent();
|
return Results.NoContent();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group.MapDelete("/{id:int}/permanent", async (int id, YesChefDb db, HttpContext http, IHubContext<ShoppingListHub> hub) =>
|
||||||
|
{
|
||||||
|
var familyId = http.User.GetFamilyId();
|
||||||
|
var list = await db.ShoppingLists.FirstOrDefaultAsync(l => l.Id == id && l.FamilyId == familyId);
|
||||||
|
if (list is null) return Results.NotFound();
|
||||||
|
|
||||||
|
db.ShoppingLists.Remove(list);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
await hub.Clients.Group(OverviewGroup(familyId)).SendAsync("ListDeleted", new { list.Id });
|
||||||
|
|
||||||
|
return Results.NoContent();
|
||||||
|
});
|
||||||
|
|
||||||
group.MapPost("/{listId:int}/items", async (int listId, AddItemRequest request, YesChefDb db, HttpContext http, IHubContext<ShoppingListHub> hub) =>
|
group.MapPost("/{listId:int}/items", async (int listId, AddItemRequest request, YesChefDb db, HttpContext http, IHubContext<ShoppingListHub> hub) =>
|
||||||
{
|
{
|
||||||
var familyId = http.User.GetFamilyId();
|
var familyId = http.User.GetFamilyId();
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
const navItems = [
|
const navItems = [
|
||||||
{ href: '/lists', label: 'Lists', icon: '📋' },
|
{ href: '/lists', label: 'Lists', icon: '📋' },
|
||||||
{ href: '/recipes', label: 'Recipes', icon: '📖' },
|
{ href: '/recipes', label: 'Recipes', icon: '📖' },
|
||||||
{ href: '/stores', label: 'Stores', icon: '🏪' },
|
|
||||||
{ href: '/family', label: 'Family', icon: '👪' }
|
{ href: '/family', label: 'Family', icon: '👪' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,10 @@
|
|||||||
lists = lists.filter((l) => l.id !== data.id);
|
lists = lists.filter((l) => l.id !== data.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
connection.on('ListDeleted', (data: { id: number }) => {
|
||||||
|
lists = lists.filter((l) => l.id !== data.id);
|
||||||
|
});
|
||||||
|
|
||||||
connection.on('ListSummaryUpdated', (data: ListSummary) => {
|
connection.on('ListSummaryUpdated', (data: ListSummary) => {
|
||||||
lists = lists.map((l) =>
|
lists = lists.map((l) =>
|
||||||
l.id === data.id ? { ...l, itemCount: data.itemCount, checkedCount: data.checkedCount, updatedAt: data.updatedAt } : l
|
l.id === data.id ? { ...l, itemCount: data.itemCount, checkedCount: data.checkedCount, updatedAt: data.updatedAt } : l
|
||||||
@@ -148,4 +152,11 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<div class="mt-6 border-t border-gray-100 pt-4">
|
||||||
|
<a href="/stores" class="flex items-center justify-between text-sm text-gray-500">
|
||||||
|
<span>Manage stores</span>
|
||||||
|
<span>›</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -253,6 +253,12 @@
|
|||||||
await api(`/api/lists/${listId}`, { method: 'DELETE' });
|
await api(`/api/lists/${listId}`, { method: 'DELETE' });
|
||||||
goto('/lists');
|
goto('/lists');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function deleteList() {
|
||||||
|
if (!confirm(`Permanently delete "${list!.name}"? This cannot be undone.`)) return;
|
||||||
|
await api(`/api/lists/${listId}/permanent`, { method: 'DELETE' });
|
||||||
|
goto('/lists');
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if loading}
|
{#if loading}
|
||||||
@@ -265,12 +271,20 @@
|
|||||||
<h2 class="text-2xl font-bold">{list.name}</h2>
|
<h2 class="text-2xl font-bold">{list.name}</h2>
|
||||||
<p class="text-sm text-gray-500">{list.store.name}</p>
|
<p class="text-sm text-gray-500">{list.store.name}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
<button
|
<button
|
||||||
onclick={archiveList}
|
onclick={archiveList}
|
||||||
class="rounded-lg border border-gray-300 px-3 py-1.5 text-sm text-gray-500"
|
class="rounded-lg border border-gray-300 px-3 py-1.5 text-sm text-gray-500"
|
||||||
>
|
>
|
||||||
Archive
|
Archive
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onclick={deleteList}
|
||||||
|
class="btn-danger-link text-sm"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onsubmit={e => { e.preventDefault(); addItem(); }} class="mb-4 space-y-2">
|
<form onsubmit={e => { e.preventDefault(); addItem(); }} class="mb-4 space-y-2">
|
||||||
|
|||||||
@@ -152,7 +152,10 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h2 class="mb-4 text-2xl font-bold">Stores</h2>
|
<div class="mb-4">
|
||||||
|
<a href="/lists" class="text-sm text-gray-500">← Back to lists</a>
|
||||||
|
<h2 class="mt-1 text-2xl font-bold">Stores</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
<form onsubmit={e => { e.preventDefault(); addStore(); }} class="mb-6 flex gap-2">
|
<form onsubmit={e => { e.preventDefault(); addStore(); }} class="mb-6 flex gap-2">
|
||||||
<input
|
<input
|
||||||
|
|||||||
Reference in New Issue
Block a user