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>
200 lines
6.6 KiB
Svelte
200 lines
6.6 KiB
Svelte
<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>
|