type Snapshot = { recipes: { recipe_id: number }[]; uncheckedCount: number; }; export class ShoppingCartStore { uncheckedCount = $state(0); recipeIds = $state>(new Set()); loaded = $state(false); private readonly fetchImpl: typeof fetch; constructor(fetchImpl?: typeof fetch) { this.fetchImpl = fetchImpl ?? ((...a) => fetch(...a)); } async refresh(): Promise { 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 { 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 { 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();