feat(shopping): ShoppingCartStore (Client)
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 2m16s

Svelte-5-Runes-Store mit uncheckedCount, recipeIds und loaded.
refresh() holt Snapshot via GET /api/shopping-list, addRecipe/
removeRecipe posten bzw. loeschen und refreshen anschliessend.
Bei Netzwerkfehler bleibt der letzte bekannte State erhalten.
This commit is contained in:
hsiegeln
2026-04-21 23:31:29 +02:00
parent dc15cf04a9
commit 1bd5dd106f
2 changed files with 125 additions and 0 deletions

View File

@@ -0,0 +1,52 @@
type Snapshot = {
recipes: { recipe_id: number }[];
uncheckedCount: number;
};
export class ShoppingCartStore {
uncheckedCount = $state(0);
recipeIds = $state<Set<number>>(new Set());
loaded = $state(false);
private readonly fetchImpl: typeof fetch;
constructor(fetchImpl?: typeof fetch) {
this.fetchImpl = fetchImpl ?? ((...a) => fetch(...a));
}
async refresh(): Promise<void> {
try {
const res = await this.fetchImpl('/api/shopping-list');
if (!res.ok) return;
const body = (await res.json()) as Snapshot;
this.recipeIds = new Set(body.recipes.map((r) => r.recipe_id));
this.uncheckedCount = body.uncheckedCount;
this.loaded = true;
} catch {
// keep last known state on network error
}
}
async addRecipe(recipeId: number): Promise<void> {
const res = await this.fetchImpl('/api/shopping-list/recipe', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ recipe_id: recipeId })
});
// Consume body to avoid leaking response, even if we ignore the payload.
await res.json().catch(() => null);
await this.refresh();
}
async removeRecipe(recipeId: number): Promise<void> {
const res = await this.fetchImpl(`/api/shopping-list/recipe/${recipeId}`, { method: 'DELETE' });
await res.json().catch(() => null);
await this.refresh();
}
isInCart(recipeId: number): boolean {
return this.recipeIds.has(recipeId);
}
}
export const shoppingCartStore = new ShoppingCartStore();