feat(recipes): add C6 day-picker flow — week plan load + slot actions + DayPicker sheet
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import { invalidateAll } from '$app/navigation';
|
||||
import { tick } from 'svelte';
|
||||
import FilterChipRow from '$lib/recipes/FilterChipRow.svelte';
|
||||
import RecipeGrid from '$lib/recipes/RecipeGrid.svelte';
|
||||
import type { RecipeSummary } from '$lib/recipes/types';
|
||||
import DayPicker from '$lib/planner/DayPicker.svelte';
|
||||
import BottomSheet from '$lib/components/BottomSheet.svelte';
|
||||
import UndoBar from '$lib/planner/UndoBar.svelte';
|
||||
|
||||
let { data }: { data: { recipes: RecipeSummary[] } } = $props();
|
||||
let { data, form = null }: { data: { recipes: RecipeSummary[]; activePlan: any }; form?: any } =
|
||||
$props();
|
||||
|
||||
// ── Search / filter ──────────────────────────────────────────────────────
|
||||
let searchQuery = $state('');
|
||||
let activeFilter = $state('Alle');
|
||||
|
||||
@@ -22,6 +30,77 @@
|
||||
})
|
||||
.filter((r) => r.name.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||
);
|
||||
|
||||
// ── Today (computed once at module level) ─────────────────────────────────
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
|
||||
// ── DayPicker / BottomSheet state ─────────────────────────────────────────
|
||||
let pickerOpen = $state(false);
|
||||
let pickerRecipeId = $state('');
|
||||
let pickerRecipeName = $state('');
|
||||
let pickerPlan = $state<any>(null);
|
||||
let pickerWeekStart = $state('');
|
||||
|
||||
// ── Undo bar state ────────────────────────────────────────────────────────
|
||||
let undoVisible = $state(false);
|
||||
let undoMessage = $state('');
|
||||
let undoPlanId = $state('');
|
||||
let undoSlotId = $state('');
|
||||
|
||||
// ── Hidden form field state ───────────────────────────────────────────────
|
||||
let addPlanId = $state('');
|
||||
let addSlotDate = $state('');
|
||||
let addRecipeId = $state('');
|
||||
let updPlanId = $state('');
|
||||
let updSlotId = $state('');
|
||||
let updRecipeId = $state('');
|
||||
|
||||
// ── Form element refs ─────────────────────────────────────────────────────
|
||||
let addSlotFormEl: HTMLFormElement;
|
||||
let updateSlotFormEl: HTMLFormElement;
|
||||
let deleteSlotFormEl: HTMLFormElement;
|
||||
|
||||
// ── Handlers ──────────────────────────────────────────────────────────────
|
||||
function openDayPicker(recipeId: string, recipeName: string) {
|
||||
if (!data.activePlan) return;
|
||||
pickerRecipeId = recipeId;
|
||||
pickerRecipeName = recipeName;
|
||||
pickerPlan = data.activePlan;
|
||||
pickerWeekStart = data.activePlan.weekStart;
|
||||
pickerOpen = true;
|
||||
}
|
||||
|
||||
async function handleWeekChange(newWeekStart: string) {
|
||||
const res = await fetch(`/recipes?week=${newWeekStart}`);
|
||||
const { plan } = await res.json();
|
||||
pickerPlan = plan;
|
||||
pickerWeekStart = newWeekStart;
|
||||
}
|
||||
|
||||
async function handleDayPickerConfirm({ date, slotId }: { date: string; slotId: string | null }) {
|
||||
pickerOpen = false;
|
||||
|
||||
if (slotId) {
|
||||
// Replace existing slot
|
||||
updPlanId = pickerPlan?.id ?? '';
|
||||
updSlotId = slotId;
|
||||
updRecipeId = pickerRecipeId;
|
||||
await tick();
|
||||
updateSlotFormEl.requestSubmit();
|
||||
} else {
|
||||
// Add to empty slot
|
||||
addPlanId = pickerPlan?.id ?? '';
|
||||
addSlotDate = date;
|
||||
addRecipeId = pickerRecipeId;
|
||||
await tick();
|
||||
addSlotFormEl.requestSubmit();
|
||||
}
|
||||
}
|
||||
|
||||
function handleUndo() {
|
||||
undoVisible = false;
|
||||
deleteSlotFormEl.requestSubmit();
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -30,18 +109,116 @@
|
||||
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="font-[var(--font-display)] text-[28px] font-medium text-[var(--color-text)]">Rezepte</h1>
|
||||
<a href="/recipes/new" class="font-sans text-[13px] font-medium tracking-[0.04em] rounded-[var(--radius-md)] bg-[var(--green-dark)] px-[24px] py-[12px] text-white">Rezept hinzufügen</a>
|
||||
<h1 class="font-[var(--font-display)] text-[28px] font-medium text-[var(--color-text)]">
|
||||
Rezepte
|
||||
</h1>
|
||||
<a
|
||||
href="/recipes/new"
|
||||
class="font-sans text-[13px] font-medium tracking-[0.04em] rounded-[var(--radius-md)] bg-[var(--green-dark)] px-[24px] py-[12px] text-white"
|
||||
>
|
||||
Rezept hinzufügen
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="search"
|
||||
placeholder="Suchen…"
|
||||
class="input"
|
||||
bind:value={searchQuery}
|
||||
/>
|
||||
<input type="search" placeholder="Suchen…" class="input" bind:value={searchQuery} />
|
||||
|
||||
<FilterChipRow {activeFilter} onFilter={(f) => (activeFilter = f)} />
|
||||
|
||||
<RecipeGrid recipes={filteredRecipes} />
|
||||
<RecipeGrid
|
||||
recipes={filteredRecipes}
|
||||
onplan={data.activePlan ? openDayPicker : undefined}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<BottomSheet open={pickerOpen} onclose={() => (pickerOpen = false)} height="55vh">
|
||||
{#if pickerPlan}
|
||||
<DayPicker
|
||||
recipeName={pickerRecipeName}
|
||||
recipeId={pickerRecipeId}
|
||||
planId={pickerPlan?.id ?? ''}
|
||||
weekStart={pickerWeekStart}
|
||||
{today}
|
||||
slots={pickerPlan?.slots ?? []}
|
||||
onconfirm={handleDayPickerConfirm}
|
||||
onweekchange={handleWeekChange}
|
||||
/>
|
||||
{/if}
|
||||
</BottomSheet>
|
||||
|
||||
<UndoBar
|
||||
visible={undoVisible}
|
||||
message={undoMessage}
|
||||
onundo={handleUndo}
|
||||
ondismiss={() => (undoVisible = false)}
|
||||
/>
|
||||
|
||||
<!-- Hidden forms for slot mutations -->
|
||||
<form
|
||||
bind:this={addSlotFormEl}
|
||||
method="POST"
|
||||
action="?/addSlot"
|
||||
class="hidden"
|
||||
use:enhance={({ formData }) => {
|
||||
formData.set('planId', addPlanId);
|
||||
formData.set('slotDate', addSlotDate);
|
||||
formData.set('recipeId', addRecipeId);
|
||||
return async ({ result, update }) => {
|
||||
if (result.type === 'success') {
|
||||
undoPlanId = addPlanId;
|
||||
undoSlotId = (result.data as any)?.slot?.id ?? '';
|
||||
undoMessage = `${pickerRecipeName} hinzugefügt`;
|
||||
undoVisible = true;
|
||||
await invalidateAll();
|
||||
}
|
||||
await update({ reset: false });
|
||||
};
|
||||
}}
|
||||
>
|
||||
<input type="hidden" name="planId" value={addPlanId} />
|
||||
<input type="hidden" name="slotDate" value={addSlotDate} />
|
||||
<input type="hidden" name="recipeId" value={addRecipeId} />
|
||||
</form>
|
||||
|
||||
<form
|
||||
bind:this={updateSlotFormEl}
|
||||
method="POST"
|
||||
action="?/updateSlot"
|
||||
class="hidden"
|
||||
use:enhance={({ formData }) => {
|
||||
formData.set('planId', updPlanId);
|
||||
formData.set('slotId', updSlotId);
|
||||
formData.set('recipeId', updRecipeId);
|
||||
return async ({ result, update }) => {
|
||||
if (result.type === 'success') {
|
||||
undoPlanId = updPlanId;
|
||||
undoSlotId = (result.data as any)?.slot?.id ?? '';
|
||||
undoMessage = `${pickerRecipeName} hinzugefügt`;
|
||||
undoVisible = true;
|
||||
await invalidateAll();
|
||||
}
|
||||
await update({ reset: false });
|
||||
};
|
||||
}}
|
||||
>
|
||||
<input type="hidden" name="planId" value={updPlanId} />
|
||||
<input type="hidden" name="slotId" value={updSlotId} />
|
||||
<input type="hidden" name="recipeId" value={updRecipeId} />
|
||||
</form>
|
||||
|
||||
<form
|
||||
bind:this={deleteSlotFormEl}
|
||||
method="POST"
|
||||
action="?/deleteSlot"
|
||||
class="hidden"
|
||||
use:enhance={({ formData }) => {
|
||||
formData.set('planId', undoPlanId);
|
||||
formData.set('slotId', undoSlotId);
|
||||
return async ({ update }) => {
|
||||
await invalidateAll();
|
||||
await update({ reset: false });
|
||||
};
|
||||
}}
|
||||
>
|
||||
<input type="hidden" name="planId" value={undoPlanId} />
|
||||
<input type="hidden" name="slotId" value={undoSlotId} />
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user