Compare commits
5 Commits
8ad636f825
...
8e82213d1e
| Author | SHA1 | Date | |
|---|---|---|---|
| 8e82213d1e | |||
| cb15143c30 | |||
| 9adf786b8f | |||
| 1bf929280b | |||
| 75c860a62b |
@@ -9,7 +9,7 @@
|
|||||||
hard: number;
|
hard: number;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
let total = $derived(easy + medium + hard);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Labels below the bar -->
|
<!-- Labels below the bar -->
|
||||||
|
|||||||
@@ -28,4 +28,11 @@ describe('EffortBar', () => {
|
|||||||
render(EffortBar, { props: { easy: 3, medium: 3, hard: 1 } });
|
render(EffortBar, { props: { easy: 3, medium: 3, hard: 1 } });
|
||||||
expect(screen.getByTestId('effort-easy').textContent).toContain('×3');
|
expect(screen.getByTestId('effort-easy').textContent).toContain('×3');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders no segments when all counts are zero', () => {
|
||||||
|
render(EffortBar, { props: { easy: 0, medium: 0, hard: 0 } });
|
||||||
|
expect(screen.queryByTestId('effort-easy')).toBeNull();
|
||||||
|
expect(screen.queryByTestId('effort-medium')).toBeNull();
|
||||||
|
expect(screen.queryByTestId('effort-hard')).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -40,4 +40,35 @@ describe('VarietyScoreHero', () => {
|
|||||||
render(VarietyScoreHero, { props: { score: 2.1 } });
|
render(VarietyScoreHero, { props: { score: 2.1 } });
|
||||||
expect(screen.getByTestId('score-description').textContent).toContain('Unzureichend');
|
expect(screen.getByTestId('score-description').textContent).toContain('Unzureichend');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('shows "Unzureichend" for score = 0 (boundary)', () => {
|
||||||
|
render(VarietyScoreHero, { props: { score: 0 } });
|
||||||
|
expect(screen.getByTestId('score-description').textContent).toContain('Unzureichend');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders score 0 in score-value for score = 0', () => {
|
||||||
|
render(VarietyScoreHero, { props: { score: 0 } });
|
||||||
|
expect(screen.getByTestId('score-value').textContent).toContain('0');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders 0-width progress bar for score = 0', () => {
|
||||||
|
render(VarietyScoreHero, { props: { score: 0 } });
|
||||||
|
const bar = screen.getByRole('progressbar');
|
||||||
|
expect(bar.getAttribute('aria-valuenow')).toBe('0');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows "Ausgezeichnet" for score = 10 (boundary)', () => {
|
||||||
|
render(VarietyScoreHero, { props: { score: 10 } });
|
||||||
|
expect(screen.getByTestId('score-description').textContent).toContain('Ausgezeichnet');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows "Verbesserbar" for score = 4 (boundary)', () => {
|
||||||
|
render(VarietyScoreHero, { props: { score: 4 } });
|
||||||
|
expect(screen.getByTestId('score-description').textContent).toContain('Verbesserbar');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows "Gut" for score = 7 (boundary)', () => {
|
||||||
|
render(VarietyScoreHero, { props: { score: 7 } });
|
||||||
|
expect(screen.getByTestId('score-description').textContent).toContain('Gut');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
{#each warnings as warning}
|
{#each warnings as warning}
|
||||||
<div
|
<div
|
||||||
data-testid="warning-card"
|
data-testid="warning-card"
|
||||||
class="rounded-[var(--radius-lg)] bg-[var(--yellow-tint)] px-4 py-3"
|
class="rounded-[var(--radius-lg)] border border-[var(--yellow-light)] bg-[var(--yellow-tint)] px-4 py-3"
|
||||||
>
|
>
|
||||||
<p class="font-[var(--font-sans)] text-[13px] font-medium text-[var(--yellow-text)]">
|
<p class="font-[var(--font-sans)] text-[13px] font-medium text-[var(--yellow-text)]">
|
||||||
{warning.title}
|
{warning.title}
|
||||||
|
|||||||
123
frontend/src/lib/planner/variety.test.ts
Normal file
123
frontend/src/lib/planner/variety.test.ts
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { computeSubScores, computeWarnings } from './variety';
|
||||||
|
|
||||||
|
describe('computeSubScores', () => {
|
||||||
|
it('returns proteinDiversity=10 when no protein repeats', () => {
|
||||||
|
const result = computeSubScores({ tagRepeats: [], ingredientOverlaps: [], easy: 4, medium: 2, hard: 1 });
|
||||||
|
expect(result.proteinDiversity).toBe(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reduces proteinDiversity by 2 per protein repeat', () => {
|
||||||
|
const tagRepeats = [
|
||||||
|
{ tagType: 'protein', tagName: 'Chicken', days: ['MON', 'TUE'] },
|
||||||
|
{ tagType: 'protein', tagName: 'Beef', days: ['WED', 'THU'] }
|
||||||
|
];
|
||||||
|
const result = computeSubScores({ tagRepeats, ingredientOverlaps: [], easy: 0, medium: 0, hard: 0 });
|
||||||
|
// 2 protein repeat entries → 10 - 2*2 = 6
|
||||||
|
expect(result.proteinDiversity).toBe(6);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clamps proteinDiversity to minimum 0', () => {
|
||||||
|
const tagRepeats = Array.from({ length: 6 }, (_, i) => ({
|
||||||
|
tagType: 'protein', tagName: `P${i}`, days: ['MON', 'TUE']
|
||||||
|
}));
|
||||||
|
const result = computeSubScores({ tagRepeats, ingredientOverlaps: [], easy: 0, medium: 0, hard: 0 });
|
||||||
|
expect(result.proteinDiversity).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns ingredientOverlap=10 when no overlaps', () => {
|
||||||
|
const result = computeSubScores({ tagRepeats: [], ingredientOverlaps: [], easy: 0, medium: 0, hard: 0 });
|
||||||
|
expect(result.ingredientOverlap).toBe(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reduces ingredientOverlap by 1.5 per overlap (rounded)', () => {
|
||||||
|
const ingredientOverlaps = [{ ingredientName: 'Rice', days: ['MON', 'TUE'] }];
|
||||||
|
const result = computeSubScores({ tagRepeats: [], ingredientOverlaps, easy: 0, medium: 0, hard: 0 });
|
||||||
|
// 1 overlap → 10 - 1*1.5 = 8.5 → round = 9 (Math.round rounds .5 up)
|
||||||
|
expect(result.ingredientOverlap).toBe(9);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clamps ingredientOverlap to minimum 0', () => {
|
||||||
|
const ingredientOverlaps = Array.from({ length: 8 }, (_, i) => ({
|
||||||
|
ingredientName: `Ing${i}`, days: ['MON', 'TUE']
|
||||||
|
}));
|
||||||
|
const result = computeSubScores({ tagRepeats: [], ingredientOverlaps, easy: 0, medium: 0, hard: 0 });
|
||||||
|
expect(result.ingredientOverlap).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns effortBalance=10 when no meals (total=0)', () => {
|
||||||
|
const result = computeSubScores({ tagRepeats: [], ingredientOverlaps: [], easy: 0, medium: 0, hard: 0 });
|
||||||
|
expect(result.effortBalance).toBe(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns effortBalance=10 when easy and hard are equal', () => {
|
||||||
|
const result = computeSubScores({ tagRepeats: [], ingredientOverlaps: [], easy: 3, medium: 0, hard: 3 });
|
||||||
|
// |3-3| = 0 → 10 - 0 = 10
|
||||||
|
expect(result.effortBalance).toBe(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reduces effortBalance by 1.5 per unit of easy-hard difference', () => {
|
||||||
|
const result = computeSubScores({ tagRepeats: [], ingredientOverlaps: [], easy: 4, medium: 0, hard: 0 });
|
||||||
|
// |4-0| = 4 → 10 - 4*1.5 = 4 → round(4) = 4
|
||||||
|
expect(result.effortBalance).toBe(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clamps effortBalance to minimum 0', () => {
|
||||||
|
const result = computeSubScores({ tagRepeats: [], ingredientOverlaps: [], easy: 10, medium: 0, hard: 0 });
|
||||||
|
// |10-0| = 10 → 10 - 10*1.5 = -5 → clamp to 0
|
||||||
|
expect(result.effortBalance).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores non-protein tag repeats for proteinDiversity', () => {
|
||||||
|
const tagRepeats = [{ tagType: 'category', tagName: 'Pasta', days: ['MON', 'TUE'] }];
|
||||||
|
const result = computeSubScores({ tagRepeats, ingredientOverlaps: [], easy: 0, medium: 0, hard: 0 });
|
||||||
|
expect(result.proteinDiversity).toBe(10);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('computeWarnings', () => {
|
||||||
|
it('returns empty array when no repeats or overlaps', () => {
|
||||||
|
const result = computeWarnings({ tagRepeats: [], ingredientOverlaps: [], duplicatesInPlan: [] });
|
||||||
|
expect(result).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('generates warning for protein appearing on 2+ days', () => {
|
||||||
|
const tagRepeats = [{ tagType: 'protein', tagName: 'Chicken', days: ['MON', 'TUE'] }];
|
||||||
|
const result = computeWarnings({ tagRepeats, ingredientOverlaps: [], duplicatesInPlan: [] });
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result[0].title).toContain('Chicken');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not generate warning for protein appearing on only 1 day', () => {
|
||||||
|
const tagRepeats = [{ tagType: 'protein', tagName: 'Chicken', days: ['MON'] }];
|
||||||
|
const result = computeWarnings({ tagRepeats, ingredientOverlaps: [], duplicatesInPlan: [] });
|
||||||
|
expect(result).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('generates warning for ingredient overlap on 2+ days', () => {
|
||||||
|
const ingredientOverlaps = [{ ingredientName: 'Rice', days: ['MON', 'WED'] }];
|
||||||
|
const result = computeWarnings({ tagRepeats: [], ingredientOverlaps, duplicatesInPlan: [] });
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result[0].title).toContain('Rice');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not generate warning for ingredient appearing on only 1 day', () => {
|
||||||
|
const ingredientOverlaps = [{ ingredientName: 'Rice', days: ['MON'] }];
|
||||||
|
const result = computeWarnings({ tagRepeats: [], ingredientOverlaps, duplicatesInPlan: [] });
|
||||||
|
expect(result).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('generates warning for each duplicate recipe in plan', () => {
|
||||||
|
const result = computeWarnings({ tagRepeats: [], ingredientOverlaps: [], duplicatesInPlan: ['Pasta Bolognese', 'Risotto'] });
|
||||||
|
expect(result).toHaveLength(2);
|
||||||
|
expect(result[0].title).toContain('Pasta Bolognese');
|
||||||
|
expect(result[1].title).toContain('Risotto');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('combines all warning types', () => {
|
||||||
|
const tagRepeats = [{ tagType: 'protein', tagName: 'Chicken', days: ['MON', 'TUE'] }];
|
||||||
|
const ingredientOverlaps = [{ ingredientName: 'Rice', days: ['MON', 'WED'] }];
|
||||||
|
const result = computeWarnings({ tagRepeats, ingredientOverlaps, duplicatesInPlan: ['Pasta'] });
|
||||||
|
expect(result).toHaveLength(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
88
frontend/src/lib/planner/variety.ts
Normal file
88
frontend/src/lib/planner/variety.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
interface TagRepeat {
|
||||||
|
tagType?: string;
|
||||||
|
tagName?: string;
|
||||||
|
days?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IngredientOverlap {
|
||||||
|
ingredientName?: string;
|
||||||
|
days?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SubScoreInput {
|
||||||
|
tagRepeats: TagRepeat[];
|
||||||
|
ingredientOverlaps: IngredientOverlap[];
|
||||||
|
easy: number;
|
||||||
|
medium: number;
|
||||||
|
hard: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubScores {
|
||||||
|
proteinDiversity: number;
|
||||||
|
ingredientOverlap: number;
|
||||||
|
effortBalance: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function computeSubScores(input: SubScoreInput): SubScores {
|
||||||
|
const { tagRepeats, ingredientOverlaps, easy, medium, hard } = input;
|
||||||
|
|
||||||
|
const proteinRepeats = tagRepeats.filter((t) => t.tagType === 'protein').length;
|
||||||
|
const ingredientOverlapCount = ingredientOverlaps.length;
|
||||||
|
const total = easy + medium + hard;
|
||||||
|
|
||||||
|
const effortBalance =
|
||||||
|
total === 0
|
||||||
|
? 10
|
||||||
|
: Math.min(10, Math.round(Math.max(0, 10 - Math.abs(easy - hard) * 1.5)));
|
||||||
|
|
||||||
|
return {
|
||||||
|
proteinDiversity: Math.max(0, Math.round(10 - proteinRepeats * 2)),
|
||||||
|
ingredientOverlap: Math.max(0, Math.round(10 - ingredientOverlapCount * 1.5)),
|
||||||
|
effortBalance
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WarningInput {
|
||||||
|
tagRepeats: TagRepeat[];
|
||||||
|
ingredientOverlaps: IngredientOverlap[];
|
||||||
|
duplicatesInPlan: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Warning {
|
||||||
|
title: string;
|
||||||
|
explanation: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function computeWarnings(input: WarningInput): Warning[] {
|
||||||
|
const { tagRepeats, ingredientOverlaps, duplicatesInPlan } = input;
|
||||||
|
const result: Warning[] = [];
|
||||||
|
|
||||||
|
for (const repeat of tagRepeats) {
|
||||||
|
if ((repeat.days?.length ?? 0) > 1) {
|
||||||
|
const days = (repeat.days ?? []).join(', ');
|
||||||
|
result.push({
|
||||||
|
title: `${repeat.tagName} mehrfach diese Woche`,
|
||||||
|
explanation: `${days} — erwäge einen Tausch für mehr Protein-Abwechslung.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const overlap of ingredientOverlaps) {
|
||||||
|
if ((overlap.days?.length ?? 0) > 1) {
|
||||||
|
const days = (overlap.days ?? []).join(', ');
|
||||||
|
result.push({
|
||||||
|
title: `${overlap.ingredientName} in mehreren Gerichten`,
|
||||||
|
explanation: `${days} — sorge für Zutaten-Abwechslung.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const name of duplicatesInPlan) {
|
||||||
|
result.push({
|
||||||
|
title: `${name} doppelt geplant`,
|
||||||
|
explanation: 'Dasselbe Rezept erscheint mehrfach — tausche eines aus.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
import ScoreBreakdownList from '$lib/planner/ScoreBreakdownList.svelte';
|
import ScoreBreakdownList from '$lib/planner/ScoreBreakdownList.svelte';
|
||||||
import VarietyWarningCards from '$lib/planner/VarietyWarningCards.svelte';
|
import VarietyWarningCards from '$lib/planner/VarietyWarningCards.svelte';
|
||||||
import EffortBar from '$lib/planner/EffortBar.svelte';
|
import EffortBar from '$lib/planner/EffortBar.svelte';
|
||||||
import { formatDayLabel } from '$lib/planner/week';
|
import { computeSubScores, computeWarnings } from '$lib/planner/variety';
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
let score = $derived(varietyScore?.score ?? 0);
|
let score = $derived(varietyScore?.score ?? 0);
|
||||||
|
|
||||||
// Derive effort distribution from week plan slots
|
// Derive effort distribution from week plan slots
|
||||||
let effortCounts = $derived(() => {
|
let effortCounts = $derived.by(() => {
|
||||||
const slots = weekPlan?.slots ?? [];
|
const slots = weekPlan?.slots ?? [];
|
||||||
let easy = 0, medium = 0, hard = 0;
|
let easy = 0, medium = 0, hard = 0;
|
||||||
for (const slot of slots) {
|
for (const slot of slots) {
|
||||||
@@ -28,65 +28,21 @@
|
|||||||
|
|
||||||
// Derive sub-scores from API data
|
// Derive sub-scores from API data
|
||||||
// TODO: replace with API-provided sub-scores once backend supports them.
|
// TODO: replace with API-provided sub-scores once backend supports them.
|
||||||
let subScores = $derived(() => {
|
let subScores = $derived.by(() => computeSubScores({
|
||||||
const proteinRepeats = (varietyScore?.tagRepeats ?? []).filter(
|
tagRepeats: varietyScore?.tagRepeats ?? [],
|
||||||
(t: any) => t.tagType === 'protein'
|
ingredientOverlaps: varietyScore?.ingredientOverlaps ?? [],
|
||||||
).length;
|
...effortCounts
|
||||||
const ingredientOverlapCount = (varietyScore?.ingredientOverlaps ?? []).length;
|
}));
|
||||||
const { easy, medium, hard } = effortCounts();
|
|
||||||
const total = easy + medium + hard;
|
|
||||||
// Effort balance: ideal is roughly equal split; penalise extreme distributions
|
|
||||||
const effortBalance =
|
|
||||||
total === 0
|
|
||||||
? 10
|
|
||||||
: Math.round(Math.max(0, 10 - Math.abs(easy - hard) * 1.5));
|
|
||||||
|
|
||||||
return {
|
|
||||||
proteinDiversity: Math.max(0, Math.round(10 - proteinRepeats * 2)),
|
|
||||||
ingredientOverlap: Math.max(0, Math.round(10 - ingredientOverlapCount * 1.5)),
|
|
||||||
effortBalance: Math.min(10, effortBalance)
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Build warning list from API data
|
// Build warning list from API data
|
||||||
let warnings = $derived(() => {
|
let warnings = $derived.by(() => computeWarnings({
|
||||||
const result: { title: string; explanation: string }[] = [];
|
tagRepeats: varietyScore?.tagRepeats ?? [],
|
||||||
|
ingredientOverlaps: varietyScore?.ingredientOverlaps ?? [],
|
||||||
// Protein repeats
|
duplicatesInPlan: varietyScore?.duplicatesInPlan ?? []
|
||||||
for (const repeat of varietyScore?.tagRepeats ?? []) {
|
}));
|
||||||
if ((repeat.days?.length ?? 0) > 1) {
|
|
||||||
const days = (repeat.days ?? []).map((d: string) => d).join(', ');
|
|
||||||
result.push({
|
|
||||||
title: `${repeat.tagName} mehrfach diese Woche`,
|
|
||||||
explanation: `${days} — erwäge einen Tausch für mehr Protein-Abwechslung.`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ingredient overlaps
|
|
||||||
for (const overlap of varietyScore?.ingredientOverlaps ?? []) {
|
|
||||||
if ((overlap.days?.length ?? 0) > 1) {
|
|
||||||
const days = (overlap.days ?? []).join(', ');
|
|
||||||
result.push({
|
|
||||||
title: `${overlap.ingredientName} in mehreren Gerichten`,
|
|
||||||
explanation: `${days} — sorge für Zutaten-Abwechslung.`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Duplicate recipes in plan
|
|
||||||
for (const name of varietyScore?.duplicatesInPlan ?? []) {
|
|
||||||
result.push({
|
|
||||||
title: `${name} doppelt geplant`,
|
|
||||||
explanation: 'Dasselbe Rezept erscheint mehrfach — tausche eines aus.'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Protein grid: map protein tags to days of the week
|
// Protein grid: map protein tags to days of the week
|
||||||
let proteinByDay = $derived(() => {
|
let proteinByDay = $derived.by(() => {
|
||||||
const map: Record<string, string> = {};
|
const map: Record<string, string> = {};
|
||||||
for (const repeat of varietyScore?.tagRepeats ?? []) {
|
for (const repeat of varietyScore?.tagRepeats ?? []) {
|
||||||
if (repeat.tagType === 'protein') {
|
if (repeat.tagType === 'protein') {
|
||||||
@@ -147,16 +103,16 @@
|
|||||||
<h2 class="mb-3 font-[var(--font-sans)] text-[12px] font-medium uppercase tracking-wide text-[var(--color-text-muted)]">
|
<h2 class="mb-3 font-[var(--font-sans)] text-[12px] font-medium uppercase tracking-wide text-[var(--color-text-muted)]">
|
||||||
Bewertung im Detail
|
Bewertung im Detail
|
||||||
</h2>
|
</h2>
|
||||||
<ScoreBreakdownList subScores={subScores()} />
|
<ScoreBreakdownList {subScores} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Warnings -->
|
<!-- Warnings -->
|
||||||
{#if warnings().length > 0}
|
{#if warnings.length > 0}
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<h2 class="font-[var(--font-sans)] text-[12px] font-medium uppercase tracking-wide text-[var(--color-text-muted)]">
|
<h2 class="font-[var(--font-sans)] text-[12px] font-medium uppercase tracking-wide text-[var(--color-text-muted)]">
|
||||||
Hinweise
|
Hinweise
|
||||||
</h2>
|
</h2>
|
||||||
<VarietyWarningCards warnings={warnings()} />
|
<VarietyWarningCards {warnings} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
@@ -173,7 +129,7 @@
|
|||||||
>
|
>
|
||||||
Planer
|
Planer
|
||||||
</a>
|
</a>
|
||||||
<span class="font-[var(--font-sans)] text-[13px] text-[var(--color-text-muted)]">/</span>
|
<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)]">
|
<h1 class="font-[var(--font-display)] text-[20px] font-[300] text-[var(--color-text)]">
|
||||||
Abwechslungs-Analyse
|
Abwechslungs-Analyse
|
||||||
</h1>
|
</h1>
|
||||||
@@ -208,7 +164,7 @@
|
|||||||
<h2 class="mb-3 font-[var(--font-sans)] text-[12px] font-medium uppercase tracking-wide text-[var(--color-text-muted)]">
|
<h2 class="mb-3 font-[var(--font-sans)] text-[12px] font-medium uppercase tracking-wide text-[var(--color-text-muted)]">
|
||||||
Bewertung im Detail
|
Bewertung im Detail
|
||||||
</h2>
|
</h2>
|
||||||
<ScoreBreakdownList subScores={subScores()} />
|
<ScoreBreakdownList {subScores} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -220,10 +176,10 @@
|
|||||||
Protein-Verteilung
|
Protein-Verteilung
|
||||||
</h2>
|
</h2>
|
||||||
<div class="grid grid-cols-7 gap-[6px]">
|
<div class="grid grid-cols-7 gap-[6px]">
|
||||||
{#each weekDayAbbrs as abbr, i}
|
{#each weekDayAbbrs as abbr, i (weekDayKeys[i])}
|
||||||
{@const key = weekDayKeys[i]}
|
{@const key = weekDayKeys[i]}
|
||||||
{@const protein = proteinByDay()[key]}
|
{@const protein = proteinByDay[key]}
|
||||||
{@const isRepeated = protein && Object.values(proteinByDay()).filter((p) => p === protein).length > 1}
|
{@const isRepeated = protein && Object.values(proteinByDay).filter((p) => p === protein).length > 1}
|
||||||
<div class="flex flex-col items-center gap-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>
|
<span class="font-[var(--font-sans)] text-[10px] text-[var(--color-text-muted)]">{abbr}</span>
|
||||||
<div
|
<div
|
||||||
@@ -235,7 +191,7 @@
|
|||||||
: 'bg-[var(--color-subtle)] text-[var(--color-text-muted)]'}
|
: 'bg-[var(--color-subtle)] text-[var(--color-text-muted)]'}
|
||||||
{isRepeated ? 'ring-2 ring-[var(--yellow)]' : ''}"
|
{isRepeated ? 'ring-2 ring-[var(--yellow)]' : ''}"
|
||||||
>
|
>
|
||||||
{protein ? protein.slice(0, 3) : '—'}
|
{protein ? protein.split(' ')[0].slice(0, 3).toUpperCase() : '—'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -247,11 +203,11 @@
|
|||||||
<h2 class="mb-3 font-[var(--font-sans)] text-[12px] font-medium uppercase tracking-wide text-[var(--color-text-muted)]">
|
<h2 class="mb-3 font-[var(--font-sans)] text-[12px] font-medium uppercase tracking-wide text-[var(--color-text-muted)]">
|
||||||
Aufwandsverteilung
|
Aufwandsverteilung
|
||||||
</h2>
|
</h2>
|
||||||
{#if (effortCounts().easy + effortCounts().medium + effortCounts().hard) > 0}
|
{#if (effortCounts.easy + effortCounts.medium + effortCounts.hard) > 0}
|
||||||
<EffortBar
|
<EffortBar
|
||||||
easy={effortCounts().easy}
|
easy={effortCounts.easy}
|
||||||
medium={effortCounts().medium}
|
medium={effortCounts.medium}
|
||||||
hard={effortCounts().hard}
|
hard={effortCounts.hard}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<p class="font-[var(--font-sans)] text-[12px] text-[var(--color-text-muted)]">
|
<p class="font-[var(--font-sans)] text-[12px] text-[var(--color-text-muted)]">
|
||||||
@@ -263,12 +219,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Bottom: warnings, full width -->
|
<!-- Bottom: warnings, full width -->
|
||||||
{#if warnings().length > 0}
|
{#if warnings.length > 0}
|
||||||
<div class="mt-8 space-y-3">
|
<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)]">
|
<h2 class="font-[var(--font-sans)] text-[12px] font-medium uppercase tracking-wide text-[var(--color-text-muted)]">
|
||||||
Hinweise
|
Hinweise
|
||||||
</h2>
|
</h2>
|
||||||
<VarietyWarningCards warnings={warnings()} />
|
<VarietyWarningCards {warnings} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
Reference in New Issue
Block a user