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:
199
frontend/src/routes/(app)/planner/suggestions/+page.svelte
Normal file
199
frontend/src/routes/(app)/planner/suggestions/+page.svelte
Normal 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>
|
||||
Reference in New Issue
Block a user