feat(suggestions): C2 — Meal suggestions (variety-aware) (#40)

feat(suggestions): implement C2 meal suggestion screen (Issue #27)

Co-authored-by: Marcel Raddatz <marcel@raddatz.cloud>
Co-committed-by: Marcel Raddatz <marcel@raddatz.cloud>
This commit was merged in pull request #40.
This commit is contained in:
2026-04-03 11:18:45 +02:00
committed by marcel
parent 05e47c3dac
commit 7c07bc443b
7 changed files with 768 additions and 0 deletions

View File

@@ -0,0 +1,199 @@
<script lang="ts">
import SuggestionCard from '$lib/planner/SuggestionCard.svelte';
import SuggestionContextBanner from '$lib/planner/SuggestionContextBanner.svelte';
import { formatDayLabel } from '$lib/planner/week';
let { data } = $props();
let weekPlan = $derived(data.weekPlan);
let suggestions = $derived(data.suggestions ?? []);
let selectedDay = $derived(data.selectedDay);
let weekStart = $derived(data.weekStart);
// Add rank and derive reasoning from simulatedScore for display.
// TODO: replace hardcoded threshold (7.5) with API-provided reasoning once backend supports it.
let rankedSuggestions = $derived(
suggestions.map((s: any, i: number) => ({
...s,
reasoningType: (s.simulatedScore ?? 0) >= 7.5 ? 'good' : 'warning',
reasoningLabel:
(s.simulatedScore ?? 0) >= 7.5
? 'Passt gut zur Woche'
: 'Wiederholung möglich'
}))
);
</script>
<svelte:head>
<title>Gerichtsvorschläge — Mealplan</title>
</svelte:head>
<!-- Mobile layout: full-width list with context banner -->
<div class="flex h-full flex-col lg:hidden">
<!-- Mobile topbar -->
<header class="sticky top-0 z-10 flex items-center gap-3 border-b border-[var(--color-border)] bg-[var(--color-page)] px-4 py-3">
<a
href="/planner?week={weekStart}"
aria-label="Zurück zum Planer"
class="font-[var(--font-sans)] text-[20px] text-[var(--color-text-muted)] hover:text-[var(--color-text)]"
>
</a>
<h1 class="font-[var(--font-display)] text-[18px] font-[300] text-[var(--color-text)]">
Vorschläge für {formatDayLabel(selectedDay)}
</h1>
</header>
<!-- Context banner -->
<div class="px-4 pt-3">
<SuggestionContextBanner {selectedDay} {weekPlan} />
</div>
<!-- Suggestion list -->
<div class="flex-1 overflow-y-auto px-4 pt-4 pb-6">
{#if rankedSuggestions.length === 0}
<div class="flex flex-col items-center justify-center py-12 text-center">
<p class="font-[var(--font-sans)] text-[14px] text-[var(--color-text-muted)]">
Keine Vorschläge verfügbar.
</p>
<a
href="/recipes?selectFor={selectedDay}&week={weekStart}"
class="mt-3 font-[var(--font-sans)] text-[13px] text-[var(--color-text-muted)] hover:text-[var(--color-text)] hover:underline"
>
Gesamte Rezeptbibliothek durchsuchen →
</a>
</div>
{:else}
<div class="space-y-3">
{#each rankedSuggestions as suggestion, i}
<SuggestionCard
{suggestion}
rank={i + 1}
planId={weekPlan?.id ?? ''}
slotDate={selectedDay}
{weekStart}
/>
{/each}
</div>
<!-- Browse full library fallback -->
<div class="mt-6 text-center">
<a
href="/recipes?selectFor={selectedDay}&week={weekStart}"
class="font-[var(--font-sans)] text-[13px] text-[var(--color-text-muted)] hover:text-[var(--color-text)] hover:underline"
>
Gesamte Rezeptbibliothek durchsuchen →
</a>
</div>
{/if}
</div>
</div>
<!-- Desktop: 2-panel layout -->
<div class="hidden h-screen lg:flex lg:flex-col">
<!-- Topbar -->
<header class="flex items-center gap-4 border-b border-[var(--color-border)] bg-[var(--color-page)] px-6 py-4">
<a
href="/planner?week={weekStart}"
aria-label="Zurück zum Planer"
class="font-[var(--font-sans)] text-[20px] text-[var(--color-text-muted)] hover:text-[var(--color-text)]"
>
</a>
<h1 class="font-[var(--font-display)] text-[20px] font-[300] text-[var(--color-text)]">
Vorschläge für {formatDayLabel(selectedDay)}
</h1>
</header>
<div class="flex flex-1 overflow-hidden">
<!-- Left context panel (280px) -->
<aside class="flex w-[280px] flex-shrink-0 flex-col border-r border-[var(--color-border)] bg-[var(--color-surface)] p-5 overflow-y-auto">
<h2 class="mb-3 font-[var(--font-sans)] text-[12px] font-medium uppercase tracking-wide text-[var(--color-text-muted)]">
Diese Woche bisher
</h2>
{#if weekPlan?.slots?.length}
<ul class="space-y-2">
{#each (weekPlan.slots ?? []).filter((s: any) => s.slotDate !== selectedDay) as slot}
<li class="flex items-baseline gap-2">
<span class="min-w-[28px] font-[var(--font-sans)] text-[11px] text-[var(--color-text-muted)]">
{formatDayLabel(slot.slotDate ?? '').split(',')[0]}
</span>
{#if slot.recipe}
<span class="font-[var(--font-sans)] text-[13px] text-[var(--color-text)] truncate">
{slot.recipe.name}
</span>
{:else}
<span class="font-[var(--font-sans)] text-[13px] text-[var(--color-text-muted)]">— Nicht geplant</span>
{/if}
</li>
{/each}
</ul>
{:else}
<p class="font-[var(--font-sans)] text-[13px] text-[var(--color-text-muted)]">
Noch keine Gerichte diese Woche geplant.
</p>
{/if}
<!-- Filter reasons -->
<div class="mt-6">
<h3 class="mb-2 font-[var(--font-sans)] text-[12px] font-medium uppercase tracking-wide text-[var(--color-text-muted)]">
Filterkriterien
</h3>
<ul class="space-y-1">
<li class="font-[var(--font-sans)] text-[12px] text-[var(--color-text-muted)]">
· Keine Zutatenwiederholungen (3 Tage)
</li>
<li class="font-[var(--font-sans)] text-[12px] text-[var(--color-text-muted)]">
· Protein-Abwechslung beachten
</li>
<li class="font-[var(--font-sans)] text-[12px] text-[var(--color-text-muted)]">
· Aufwandsbalance
</li>
</ul>
</div>
<!-- Browse library link in desktop panel footer -->
<div class="mt-auto pt-6">
<a
href="/recipes?selectFor={selectedDay}&week={weekStart}"
class="font-[var(--font-sans)] text-[12px] text-[var(--color-text-muted)] hover:text-[var(--color-text)] hover:underline"
>
Gesamte Bibliothek →
</a>
</div>
</aside>
<!-- Right suggestions panel -->
<main class="flex-1 overflow-y-auto bg-[var(--color-page)] px-6 py-5">
{#if rankedSuggestions.length === 0}
<div class="flex h-full flex-col items-center justify-center">
<p class="font-[var(--font-sans)] text-[14px] text-[var(--color-text-muted)]">
Keine Vorschläge verfügbar.
</p>
</div>
{:else}
<div class="space-y-3">
{#each rankedSuggestions as suggestion, i}
<SuggestionCard
{suggestion}
rank={i + 1}
planId={weekPlan?.id ?? ''}
slotDate={selectedDay}
{weekStart}
/>
{/each}
</div>
<div class="mt-6 text-center">
<a
href="/recipes?selectFor={selectedDay}&week={weekStart}"
class="font-[var(--font-sans)] text-[13px] text-[var(--color-text-muted)] hover:text-[var(--color-text)] hover:underline"
>
Gesamte Rezeptbibliothek durchsuchen →
</a>
</div>
{/if}
</main>
</div>
</div>