- EffortBar: remove unused \`total\` derived variable
- VarietyWarningCards: add border border-[var(--yellow-light)] to cards
- variety page: protein abbreviation uses split(' ')[0].slice(0,3).toUpperCase()
- variety page: breadcrumb separator span gets aria-hidden="true"
Addresses Kai blocker: unused total. Atlas blockers: yellow-light border,
protein abbreviation, breadcrumb aria.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
234 lines
8.1 KiB
Svelte
234 lines
8.1 KiB
Svelte
<script lang="ts">
|
||
import VarietyScoreHero from '$lib/planner/VarietyScoreHero.svelte';
|
||
import ScoreBreakdownList from '$lib/planner/ScoreBreakdownList.svelte';
|
||
import VarietyWarningCards from '$lib/planner/VarietyWarningCards.svelte';
|
||
import EffortBar from '$lib/planner/EffortBar.svelte';
|
||
import { computeSubScores, computeWarnings } from '$lib/planner/variety';
|
||
|
||
let { data } = $props();
|
||
|
||
let weekPlan = $derived(data.weekPlan);
|
||
let varietyScore = $derived(data.varietyScore);
|
||
let weekStart = $derived(data.weekStart);
|
||
|
||
let score = $derived(varietyScore?.score ?? 0);
|
||
|
||
// Derive effort distribution from week plan slots
|
||
let effortCounts = $derived.by(() => {
|
||
const slots = weekPlan?.slots ?? [];
|
||
let easy = 0, medium = 0, hard = 0;
|
||
for (const slot of slots) {
|
||
const effort = slot.recipe?.effort?.toLowerCase() ?? '';
|
||
if (effort === 'easy' || effort === 'einfach') easy++;
|
||
else if (effort === 'medium' || effort === 'mittel') medium++;
|
||
else if (effort === 'hard' || effort === 'aufwändig') hard++;
|
||
}
|
||
return { easy, medium, hard };
|
||
});
|
||
|
||
// Derive sub-scores from API data
|
||
// TODO: replace with API-provided sub-scores once backend supports them.
|
||
let subScores = $derived.by(() => computeSubScores({
|
||
tagRepeats: varietyScore?.tagRepeats ?? [],
|
||
ingredientOverlaps: varietyScore?.ingredientOverlaps ?? [],
|
||
...effortCounts
|
||
}));
|
||
|
||
// Build warning list from API data
|
||
let warnings = $derived.by(() => computeWarnings({
|
||
tagRepeats: varietyScore?.tagRepeats ?? [],
|
||
ingredientOverlaps: varietyScore?.ingredientOverlaps ?? [],
|
||
duplicatesInPlan: varietyScore?.duplicatesInPlan ?? []
|
||
}));
|
||
|
||
// Protein grid: map protein tags to days of the week
|
||
let proteinByDay = $derived.by(() => {
|
||
const map: Record<string, string> = {};
|
||
for (const repeat of varietyScore?.tagRepeats ?? []) {
|
||
if (repeat.tagType === 'protein') {
|
||
for (const day of repeat.days ?? []) {
|
||
map[day] = repeat.tagName ?? '';
|
||
}
|
||
}
|
||
}
|
||
return map;
|
||
});
|
||
|
||
// Days of the week abbreviations for protein grid
|
||
const weekDayAbbrs = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'];
|
||
const weekDayKeys = ['MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT', 'SUN'];
|
||
</script>
|
||
|
||
<svelte:head>
|
||
<title>Abwechslung überprüfen — Mealplan</title>
|
||
</svelte:head>
|
||
|
||
<!-- Mobile layout -->
|
||
<div class="flex h-full flex-col lg:hidden">
|
||
<!-- 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)]">
|
||
Abwechslungs-Analyse
|
||
</h1>
|
||
</header>
|
||
|
||
<div class="flex-1 overflow-y-auto px-4 pb-8 pt-5">
|
||
{#if !varietyScore}
|
||
<div class="flex flex-col items-center justify-center py-16 text-center">
|
||
<p class="font-[var(--font-sans)] text-[14px] text-[var(--color-text-muted)]">
|
||
Noch keine Gerichte geplant. Plane zuerst einige Mahlzeiten.
|
||
</p>
|
||
<a
|
||
href="/planner?week={weekStart}"
|
||
class="mt-3 font-[var(--font-sans)] text-[13px] text-[var(--color-text-muted)] hover:text-[var(--color-text)] hover:underline"
|
||
>
|
||
Zum Wochenplaner →
|
||
</a>
|
||
</div>
|
||
{:else}
|
||
<!-- Big score -->
|
||
<div class="mb-6">
|
||
<VarietyScoreHero {score} />
|
||
</div>
|
||
|
||
<!-- Sub-scores -->
|
||
<div class="mb-6">
|
||
<h2 class="mb-3 font-[var(--font-sans)] text-[12px] font-medium uppercase tracking-wide text-[var(--color-text-muted)]">
|
||
Bewertung im Detail
|
||
</h2>
|
||
<ScoreBreakdownList {subScores} />
|
||
</div>
|
||
|
||
<!-- Warnings -->
|
||
{#if warnings.length > 0}
|
||
<div class="space-y-3">
|
||
<h2 class="font-[var(--font-sans)] text-[12px] font-medium uppercase tracking-wide text-[var(--color-text-muted)]">
|
||
Hinweise
|
||
</h2>
|
||
<VarietyWarningCards {warnings} />
|
||
</div>
|
||
{/if}
|
||
{/if}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Desktop layout -->
|
||
<div class="hidden h-screen lg:flex lg:flex-col">
|
||
<!-- Topbar with breadcrumb -->
|
||
<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}"
|
||
class="font-[var(--font-sans)] text-[13px] text-[var(--color-text-muted)] hover:text-[var(--color-text)]"
|
||
>
|
||
Planer
|
||
</a>
|
||
<span aria-hidden="true" class="font-[var(--font-sans)] text-[13px] text-[var(--color-text-muted)]">/</span>
|
||
<h1 class="font-[var(--font-display)] text-[20px] font-[300] text-[var(--color-text)]">
|
||
Abwechslungs-Analyse
|
||
</h1>
|
||
</header>
|
||
|
||
<div class="flex flex-1 overflow-hidden">
|
||
<!-- Sidebar (224px) — nav placeholder for consistency with C1 -->
|
||
<aside class="hidden w-[224px] flex-shrink-0 border-r border-[var(--color-border)] bg-[var(--color-surface)] xl:block">
|
||
</aside>
|
||
|
||
<!-- Main content -->
|
||
<main class="flex-1 overflow-y-auto bg-[var(--color-page)] px-8 py-6">
|
||
{#if !varietyScore}
|
||
<div class="flex h-full flex-col items-center justify-center">
|
||
<p class="font-[var(--font-sans)] text-[14px] text-[var(--color-text-muted)]">
|
||
Noch keine Gerichte geplant.
|
||
</p>
|
||
<a
|
||
href="/planner?week={weekStart}"
|
||
class="mt-3 font-[var(--font-sans)] text-[13px] text-[var(--color-text-muted)] hover:text-[var(--color-text)] hover:underline"
|
||
>
|
||
Zum Wochenplaner →
|
||
</a>
|
||
</div>
|
||
{:else}
|
||
<!-- Top section: 2 columns -->
|
||
<div class="flex gap-8">
|
||
<!-- Left: score + sub-scores -->
|
||
<div class="flex-1">
|
||
<VarietyScoreHero {score} />
|
||
<div class="mt-6">
|
||
<h2 class="mb-3 font-[var(--font-sans)] text-[12px] font-medium uppercase tracking-wide text-[var(--color-text-muted)]">
|
||
Bewertung im Detail
|
||
</h2>
|
||
<ScoreBreakdownList {subScores} />
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Right (320px): protein grid + effort bar -->
|
||
<div class="w-[320px] flex-shrink-0 space-y-6">
|
||
<!-- Protein grid -->
|
||
<div>
|
||
<h2 class="mb-3 font-[var(--font-sans)] text-[12px] font-medium uppercase tracking-wide text-[var(--color-text-muted)]">
|
||
Protein-Verteilung
|
||
</h2>
|
||
<div class="grid grid-cols-7 gap-[6px]">
|
||
{#each weekDayAbbrs as abbr, i (weekDayKeys[i])}
|
||
{@const key = weekDayKeys[i]}
|
||
{@const protein = proteinByDay[key]}
|
||
{@const isRepeated = protein && Object.values(proteinByDay).filter((p) => p === protein).length > 1}
|
||
<div class="flex flex-col items-center gap-1">
|
||
<span class="font-[var(--font-sans)] text-[10px] text-[var(--color-text-muted)]">{abbr}</span>
|
||
<div
|
||
data-testid="protein-cell"
|
||
data-protein={protein ?? 'none'}
|
||
class="flex h-[44px] w-full items-center justify-center rounded-[var(--radius-sm)] text-[10px] font-medium
|
||
{protein
|
||
? 'bg-[var(--green-tint)] text-[var(--green-dark)]'
|
||
: 'bg-[var(--color-subtle)] text-[var(--color-text-muted)]'}
|
||
{isRepeated ? 'ring-2 ring-[var(--yellow)]' : ''}"
|
||
>
|
||
{protein ? protein.split(' ')[0].slice(0, 3).toUpperCase() : '—'}
|
||
</div>
|
||
</div>
|
||
{/each}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Effort bar -->
|
||
<div>
|
||
<h2 class="mb-3 font-[var(--font-sans)] text-[12px] font-medium uppercase tracking-wide text-[var(--color-text-muted)]">
|
||
Aufwandsverteilung
|
||
</h2>
|
||
{#if (effortCounts.easy + effortCounts.medium + effortCounts.hard) > 0}
|
||
<EffortBar
|
||
easy={effortCounts.easy}
|
||
medium={effortCounts.medium}
|
||
hard={effortCounts.hard}
|
||
/>
|
||
{:else}
|
||
<p class="font-[var(--font-sans)] text-[12px] text-[var(--color-text-muted)]">
|
||
Noch keine Gerichte geplant.
|
||
</p>
|
||
{/if}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Bottom: warnings, full width -->
|
||
{#if warnings.length > 0}
|
||
<div class="mt-8 space-y-3">
|
||
<h2 class="font-[var(--font-sans)] text-[12px] font-medium uppercase tracking-wide text-[var(--color-text-muted)]">
|
||
Hinweise
|
||
</h2>
|
||
<VarietyWarningCards {warnings} />
|
||
</div>
|
||
{/if}
|
||
{/if}
|
||
</main>
|
||
</div>
|
||
</div>
|