feat(wishlist): "für alle löschen" + Badge refresht auf jede Navigation
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m14s
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m14s
1) Trash-Button auf Wunschliste wieder da. Im Gegensatz zum Heart
entfernt er den Eintrag NICHT nur für das aktive Profil, sondern
löscht alle Memberships auf diesem Rezept. Bestätigungsdialog macht
das explizit ("wird für alle Profile aus der Wunschliste gestrichen").
- repository.ts: neue Funktion removeFromWishlistForAll(recipeId)
- DELETE /api/wishlist/:id?all=true → family-wide
DELETE /api/wishlist/:id?profile_id=X → nur mein Eintrag
- UI: zwei Action-Buttons untereinander (Heart, Trash)
2) wishlistStore.refresh() läuft jetzt in afterNavigate des Root-Layouts.
Vorher wurde der Badge nur aktualisiert, wenn derselbe Tab die Aktion
ausgelöst hat. Wenn ein anderer Tab / anderes Gerät etwas ändert,
bleibt der Badge stale bis zum nächsten Full-Reload. Mit afterNavigate
reicht eine Client-Navigation, um ihn zu aktualisieren — was deutlich
näher an dem liegt, was der User erwartet.
This commit is contained in:
@@ -94,6 +94,13 @@ export function removeFromWishlist(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function removeFromWishlistForAll(
|
||||||
|
db: Database.Database,
|
||||||
|
recipeId: number
|
||||||
|
): void {
|
||||||
|
db.prepare('DELETE FROM wishlist WHERE recipe_id = ?').run(recipeId);
|
||||||
|
}
|
||||||
|
|
||||||
export function isOnMyWishlist(
|
export function isOnMyWishlist(
|
||||||
db: Database.Database,
|
db: Database.Database,
|
||||||
recipeId: number,
|
recipeId: number,
|
||||||
|
|||||||
@@ -102,6 +102,11 @@
|
|||||||
navHits = [];
|
navHits = [];
|
||||||
navWebHits = [];
|
navWebHits = [];
|
||||||
navOpen = false;
|
navOpen = false;
|
||||||
|
// Badge nach jeder Client-Navigation frisch halten — sonst kann er
|
||||||
|
// hinter den tatsächlichen Wunschliste-Einträgen herlaufen, wenn
|
||||||
|
// auf einem anderen Gerät oder in einem anderen Tab etwas geändert
|
||||||
|
// wurde.
|
||||||
|
void wishlistStore.refresh();
|
||||||
});
|
});
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import type { RequestHandler } from './$types';
|
import type { RequestHandler } from './$types';
|
||||||
import { json, error } from '@sveltejs/kit';
|
import { json, error } from '@sveltejs/kit';
|
||||||
import { getDb } from '$lib/server/db';
|
import { getDb } from '$lib/server/db';
|
||||||
import { removeFromWishlist } from '$lib/server/wishlist/repository';
|
import {
|
||||||
|
removeFromWishlist,
|
||||||
|
removeFromWishlistForAll
|
||||||
|
} from '$lib/server/wishlist/repository';
|
||||||
|
|
||||||
function parsePositiveInt(raw: string | null, field: string): number {
|
function parsePositiveInt(raw: string | null, field: string): number {
|
||||||
const n = raw === null ? NaN : Number(raw);
|
const n = raw === null ? NaN : Number(raw);
|
||||||
@@ -9,9 +12,16 @@ function parsePositiveInt(raw: string | null, field: string): number {
|
|||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DELETE /api/wishlist/:id?profile_id=X → entfernt nur den eigenen Wunsch
|
||||||
|
// DELETE /api/wishlist/:id?all=true → entfernt für ALLE Profile
|
||||||
export const DELETE: RequestHandler = async ({ params, url }) => {
|
export const DELETE: RequestHandler = async ({ params, url }) => {
|
||||||
const id = parsePositiveInt(params.recipe_id!, 'recipe_id');
|
const id = parsePositiveInt(params.recipe_id!, 'recipe_id');
|
||||||
const profileId = parsePositiveInt(url.searchParams.get('profile_id'), 'profile_id');
|
const db = getDb();
|
||||||
removeFromWishlist(getDb(), id, profileId);
|
if (url.searchParams.get('all') === 'true') {
|
||||||
|
removeFromWishlistForAll(db, id);
|
||||||
|
} else {
|
||||||
|
const profileId = parsePositiveInt(url.searchParams.get('profile_id'), 'profile_id');
|
||||||
|
removeFromWishlist(db, id, profileId);
|
||||||
|
}
|
||||||
return json({ ok: true });
|
return json({ ok: true });
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { Heart, CookingPot } from 'lucide-svelte';
|
import { Heart, Trash2, CookingPot } from 'lucide-svelte';
|
||||||
import { profileStore } from '$lib/client/profile.svelte';
|
import { profileStore } from '$lib/client/profile.svelte';
|
||||||
import { wishlistStore } from '$lib/client/wishlist.svelte';
|
import { wishlistStore } from '$lib/client/wishlist.svelte';
|
||||||
import { alertAction } from '$lib/client/confirm.svelte';
|
import { alertAction, confirmAction } from '$lib/client/confirm.svelte';
|
||||||
import type { WishlistEntry, SortKey } from '$lib/server/wishlist/repository';
|
import type { WishlistEntry, SortKey } from '$lib/server/wishlist/repository';
|
||||||
|
|
||||||
let entries = $state<WishlistEntry[]>([]);
|
let entries = $state<WishlistEntry[]>([]);
|
||||||
@@ -51,6 +51,19 @@
|
|||||||
void wishlistStore.refresh();
|
void wishlistStore.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function removeForAll(entry: WishlistEntry) {
|
||||||
|
const ok = await confirmAction({
|
||||||
|
title: 'Von der Wunschliste entfernen?',
|
||||||
|
message: `„${entry.title}" wird für alle Profile aus der Wunschliste gestrichen. Das Rezept selbst bleibt erhalten.`,
|
||||||
|
confirmLabel: 'Entfernen',
|
||||||
|
destructive: true
|
||||||
|
});
|
||||||
|
if (!ok) return;
|
||||||
|
await fetch(`/api/wishlist/${entry.recipe_id}?all=true`, { method: 'DELETE' });
|
||||||
|
await load();
|
||||||
|
void wishlistStore.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
void load();
|
void load();
|
||||||
void wishlistStore.refresh();
|
void wishlistStore.refresh();
|
||||||
@@ -123,6 +136,13 @@
|
|||||||
<span class="count">{e.wanted_by_count}</span>
|
<span class="count">{e.wanted_by_count}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
class="del"
|
||||||
|
aria-label="Für alle entfernen"
|
||||||
|
onclick={() => removeForAll(e)}
|
||||||
|
>
|
||||||
|
<Trash2 size={18} strokeWidth={2} />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -242,12 +262,16 @@
|
|||||||
}
|
}
|
||||||
.actions {
|
.actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
flex-direction: column;
|
||||||
|
gap: 0.4rem;
|
||||||
|
align-items: stretch;
|
||||||
|
justify-content: center;
|
||||||
padding: 0.5rem 0.6rem 0.5rem 0;
|
padding: 0.5rem 0.6rem 0.5rem 0;
|
||||||
}
|
}
|
||||||
.like {
|
.like,
|
||||||
|
.del {
|
||||||
min-width: 48px;
|
min-width: 48px;
|
||||||
min-height: 44px;
|
min-height: 40px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
border: 1px solid #e4eae7;
|
border: 1px solid #e4eae7;
|
||||||
background: white;
|
background: white;
|
||||||
@@ -264,6 +288,11 @@
|
|||||||
background: #fdf3f3;
|
background: #fdf3f3;
|
||||||
border-color: #f1b4b4;
|
border-color: #f1b4b4;
|
||||||
}
|
}
|
||||||
|
.del:hover {
|
||||||
|
color: #c53030;
|
||||||
|
border-color: #f1b4b4;
|
||||||
|
background: #fdf3f3;
|
||||||
|
}
|
||||||
.count {
|
.count {
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|||||||
Reference in New Issue
Block a user