Files
mealprep/frontend/src/lib/planner/DayPicker.svelte
Marcel Raddatz ba41f6984b feat(planner): add DayPicker component (C6)
7-chip week strip with 5 slot states, inline replace warning,
confirm button, and prev/next week navigation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 22:44:29 +02:00

169 lines
5.2 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 { weekDays, prevWeek, nextWeek, formatDayAbbr, formatWeekRange } from './week';
interface Slot {
id: string;
slotDate: string;
recipe: { id: string; name: string } | null;
}
let {
recipeName,
recipeId,
planId,
weekStart,
today,
slots = [],
onconfirm,
onweekchange
}: {
recipeName: string;
recipeId: string;
planId: string;
weekStart: string;
today: string;
slots: Slot[];
onconfirm: (result: { date: string; slotId: string | null }) => void;
onweekchange: (newWeekStart: string) => void;
} = $props();
let selectedDate = $state<string | null>(null);
const slotMap = $derived(
new Map(slots.map((s) => [s.slotDate, s]))
);
const days = $derived(weekDays(weekStart));
function chipState(date: string): string {
const isSelected = selectedDate === date;
const slot = slotMap.get(date);
const hasFilled = slot?.recipe != null;
if (isSelected) {
return hasFilled ? 'sel-filled' : 'sel-empty';
}
if (date === today) return 'today';
return hasFilled ? 'filled' : 'empty';
}
const selectedSlot = $derived(selectedDate ? slotMap.get(selectedDate) : undefined);
const existingRecipeName = $derived(selectedSlot?.recipe?.name ?? null);
const existingSlotId = $derived(selectedSlot?.id ?? null);
function chipStyle(state: string): string {
switch (state) {
case 'empty':
return 'border-style: dashed; border-color: var(--green-light); background: var(--green-tint);';
case 'filled':
return 'border-color: var(--color-border); background: var(--color-surface);';
case 'today':
return 'border-color: var(--yellow); background: var(--yellow-tint);';
case 'sel-empty':
return 'border: 2px solid var(--green-dark); background: var(--green-tint);';
case 'sel-filled':
return 'border: 2px solid var(--orange-dark); background: var(--orange-tint);';
default:
return '';
}
}
function handleChipClick(date: string) {
selectedDate = date;
}
function handleConfirm() {
if (!selectedDate) return;
onconfirm({ date: selectedDate, slotId: existingSlotId });
}
function dayNumber(date: string): string {
return date.slice(-2).replace(/^0/, '');
}
</script>
<div style="background: var(--color-page); font-family: var(--font-sans);">
<!-- Header -->
<div style="padding: 10px 12px 6px; border-bottom: 1px solid var(--color-border);">
<p
style="font-family: var(--font-display); font-size: 14px; font-weight: 500; color: var(--color-text); margin: 0;"
>
Tag wählen
</p>
<p style="font-size: 11px; color: var(--color-text-muted); margin: 2px 0 0;">
{recipeName}
</p>
</div>
<!-- Week navigation -->
<div
style="display: flex; align-items: center; justify-content: space-between; padding: 8px 12px; border-bottom: 1px solid var(--color-border);"
>
<button
type="button"
aria-label="Vorherige Woche"
onclick={() => onweekchange(prevWeek(weekStart))}
style="background: none; border: none; cursor: pointer; padding: 4px 6px; font-size: 14px; color: var(--color-text-muted); border-radius: var(--radius-md);"
>
</button>
<span style="font-size: 12px; font-weight: 500; color: var(--color-text);">
{formatWeekRange(weekStart)}
</span>
<button
type="button"
aria-label="Nächste Woche"
onclick={() => onweekchange(nextWeek(weekStart))}
style="background: none; border: none; cursor: pointer; padding: 4px 6px; font-size: 14px; color: var(--color-text-muted); border-radius: var(--radius-md);"
>
</button>
</div>
<!-- Day chips -->
<div
style="display: flex; gap: 6px; padding: 10px 12px; overflow-x: auto;"
>
{#each days as date (date)}
{@const state = chipState(date)}
<button
type="button"
data-testid="chip-{date}"
data-state={state}
onclick={() => handleChipClick(date)}
style="flex: 1; min-width: 36px; padding: 6px 4px; border-radius: var(--radius-md); border: 1px solid transparent; cursor: pointer; text-align: center; font-family: var(--font-sans); {chipStyle(state)}"
>
<span style="display: block; font-size: 9px; font-weight: 500; color: var(--color-text-muted); text-transform: uppercase; letter-spacing: 0.04em;">
{formatDayAbbr(date, 'narrow')}
</span>
<span style="display: block; font-size: 13px; font-weight: 600; color: var(--color-text); margin-top: 2px;">
{dayNumber(date)}
</span>
</button>
{/each}
</div>
<!-- Replace warning -->
{#if selectedDate && existingRecipeName}
<div
data-testid="replace-warning"
style="margin: 0 12px 10px; padding: 8px 10px; border-radius: var(--radius-md); background: var(--orange-tint); border: 1px solid var(--orange-dark); font-size: 11px; color: var(--color-text);"
>
Ersetzt <strong>{existingRecipeName}</strong> an diesem Tag.
</div>
{/if}
<!-- Confirm button -->
<div style="padding: 0 12px 12px;">
<button
type="button"
data-testid="confirm-btn"
disabled={!selectedDate}
onclick={handleConfirm}
style="width: 100%; padding: 9px 12px; font-family: var(--font-sans); font-size: 13px; font-weight: 600; border-radius: var(--radius-md); border: none; cursor: {selectedDate ? 'pointer' : 'not-allowed'}; background: {selectedDate ? 'var(--green)' : 'var(--color-border)'}; color: {selectedDate ? '#fff' : 'var(--color-text-muted)'};"
>
Einplanen
</button>
</div>
</div>