# Hauptseite: "Zuletzt angesehen" Sort + Collapsible Sections ## Kontext Die Hauptseite (`src/routes/+page.svelte`) hat heute drei Sektionen — "Deine Favoriten", "Zuletzt hinzugefügt", "Alle Rezepte" — und vier Sort-Optionen für "Alle Rezepte" (Name, Bewertung, Zuletzt gekocht, Hinzugefügt). Der User möchte: 1. Eine fünfte Sort-Option "Zuletzt angesehen" für "Alle Rezepte" 2. "Deine Favoriten" und "Zuletzt hinzugefügt" auf-/zuklappbar machen Beides reduziert visuelle Last und gibt Zugriff auf "kürzlich beschäftigte mich" Rezepte ohne Suche. ## Design-Entscheidungen (durch Brainstorming bestätigt) - **View-Tracking**: zählt sofort beim Laden der Detailseite — kein Threshold - **Storage**: SQLite, pro Profil (konsistent mit Ratings, Cooked, Wishlist) - **Collapsibles**: standardmäßig offen, User-Wahl persistiert pro Device ## Sektion 1 — Schema & View-Tracking ### Migration Neue Datei `src/lib/server/db/migrations/010_recipe_views.sql`: ```sql CREATE TABLE recipe_views ( profile_id INTEGER NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, recipe_id INTEGER NOT NULL REFERENCES recipes(id) ON DELETE CASCADE, last_viewed_at TEXT NOT NULL DEFAULT (datetime('now')), PRIMARY KEY (profile_id, recipe_id) ); CREATE INDEX idx_recipe_views_recent ON recipe_views(profile_id, last_viewed_at DESC); ``` Idempotent über `INSERT OR REPLACE` — mehrfache Visits ein- und desselben Profils auf dasselbe Rezept führen nur zur Aktualisierung des Timestamps, kein Multi-Insert. Cascade auf beide FKs: löscht ein User ein Rezept oder ein Profil, gehen zugehörige Views automatisch mit. ### API Neuer Endpoint `POST /api/recipes/[id]/view`: ``` Request body: { "profile_id": number } Response: 204 No Content Errors: - 400 wenn profile_id fehlt oder kein Number - 404 wenn Recipe nicht existiert (FK-Violation) - 404 wenn Profil nicht existiert (FK-Violation) ``` Implementation: einfache `INSERT OR REPLACE` mit den IDs. `last_viewed_at` nutzt den Default (`datetime('now')`). ### Client-Hook In `src/routes/recipes/[id]/+page.svelte`, in `onMount`: ```ts if (profileStore.active) { void fetch(`/api/recipes/${recipe.id}/view`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ profile_id: profileStore.active.id }) }); } ``` Fire-and-forget, kein UI-Block, kein Error-Handling — wenn der Beacon fehlschlägt, ist es kein User-Visible-Bug, das nächste View korrigiert es. ## Sektion 2 — Sort "Zuletzt angesehen" ### Page In `src/routes/+page.svelte`: ```ts type AllSort = 'name' | 'rating' | 'cooked' | 'created' | 'viewed'; const ALL_SORTS = [ ..., { value: 'viewed', label: 'Zuletzt angesehen' } ]; ``` ### API `GET /api/recipes/all` bekommt einen optionalen `profile_id`-Query-Param. Der Endpoint reicht ihn an `listAllRecipesPaginated` durch. ### DB-Layer `listAllRecipesPaginated` in `src/lib/server/recipes/search-local.ts` bekommt einen optionalen `profileId: number | null`-Parameter. Wenn `sort === 'viewed'` UND `profileId !== null`: ```sql SELECT r.*, ... FROM recipes r LEFT JOIN recipe_views v ON v.recipe_id = r.id AND v.profile_id = :profileId ORDER BY v.last_viewed_at DESC NULLS LAST, r.title COLLATE NOCASE ASC LIMIT :limit OFFSET :offset ``` Bei `sort === 'viewed'` ohne `profileId`: fällt auf alphabetische Sortierung zurück (kein Crash, sinnvolles Default-Verhalten). ### Reactive Refetch bei Profile-Switch Auf Home-Page-Ebene: ein `$effect` der auf `profileStore.activeId` lauscht und — wenn `allSort === 'viewed'` — `setAllSort('viewed')` retriggert (forciert Refetch mit neuem profile_id). Sonst (anderer Sort) keine Aktion, weil andere Sorts nicht profilabhängig sind. ### Snapshot-Kompatibilität Der existierende `rehydrateAll(sort, count, exhausted)` in `+page.svelte` muss `profile_id` mitschicken, sonst zeigt der Back-Nav für sort='viewed' einen anderen Inhalt als vor dem Forward-Klick. Das gleiche gilt für `loadAllMore` und `setAllSort`. ## Sektion 3 — Auf-/Zuklappbare Sektionen ### State In `src/routes/+page.svelte`: ```ts type CollapseKey = 'favorites' | 'recent'; let collapsed = $state>({ favorites: false, recent: false }); const STORAGE_KEY = 'kochwas.collapsed.sections'; function toggle(key: CollapseKey) { collapsed[key] = !collapsed[key]; localStorage.setItem(STORAGE_KEY, JSON.stringify(collapsed)); } ``` In `onMount`: aus localStorage parsen, fehlerhafte JSON ignorieren (default-state behalten). ### Markup Pro Sektion: ```svelte
{#if !collapsed.favorites}
{/if}
``` ### Visual / CSS - Header `