Files
mealprep/frontend/src/routes/(app)/recipes/+page.svelte

225 lines
7.1 KiB
Svelte

<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, form = null }: { data: { recipes: RecipeSummary[]; activePlan: any }; form?: any } =
$props();
// ── Search / filter ──────────────────────────────────────────────────────
let searchQuery = $state('');
let activeFilter = $state('Alle');
const effortMap: Record<string, string> = {
Leicht: 'easy',
Mittel: 'medium',
Schwer: 'hard'
};
let filteredRecipes = $derived(
data.recipes
.filter((r) => {
if (activeFilter === 'Alle') return true;
return r.effort === effortMap[activeFilter];
})
.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>
<title>Rezepte — Mealplan</title>
</svelte:head>
<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>
</div>
<input type="search" placeholder="Suchen…" class="input" bind:value={searchQuery} />
<FilterChipRow {activeFilter} onFilter={(f) => (activeFilter = f)} />
<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>