Files
mealprep/frontend/src/routes/(app)/planner/suggestions/+page.svelte
Marcel Raddatz 7c07bc443b 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>
2026-04-03 11:18:45 +02:00

200 lines
6.6 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>