feat(recipes): add RecipeForm component — add/edit two-state form

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-03 10:17:17 +02:00
parent fcf0f297bb
commit 2cef8a1169
2 changed files with 277 additions and 0 deletions

View File

@@ -0,0 +1,134 @@
<script lang="ts">
type Category = { id: string; name: string; tagType?: string };
type EditRecipe = {
id: string;
name: string;
serves?: number;
cookTimeMin?: number;
effort?: string;
heroImageUrl?: string;
ingredients: { name: string; quantity: number; unit: string }[];
steps: { instruction: string }[];
tagIds: string[];
} | null;
const { recipe, categories, action }: {
recipe: EditRecipe;
categories: Category[];
action: string;
} = $props();
const effortOptions = [
{ label: 'Leicht', value: 'Easy' },
{ label: 'Mittel', value: 'Medium' },
{ label: 'Schwer', value: 'Hard' }
];
// Take a plain snapshot of the initial prop value so local state is
// initialised once and not tied to ongoing prop reactivity.
const initial = (() => $state.snapshot(recipe))();
let name = $state(initial?.name ?? '');
let serves = $state<number | ''>(initial?.serves ?? '');
let cookTimeMin = $state<number | ''>(initial?.cookTimeMin ?? '');
let effort = $state(initial?.effort ?? '');
let selectedTagIds = $state<string[]>(initial?.tagIds ? [...initial.tagIds] : []);
let ingredients = $state(
initial?.ingredients.map((ing) => ({
name: ing.name,
quantity: ing.quantity as number | '',
unit: ing.unit
})) ?? [{ name: '', quantity: '' as number | '', unit: '' }]
);
let steps = $state(initial?.steps.map((s) => s.instruction) ?? ['']);
</script>
<form method="POST" {action}>
<!-- Basic info -->
<label for="name">Name</label>
<input id="name" name="name" type="text" bind:value={name} required />
<label for="serves">Portionen</label>
<input id="serves" name="serves" type="number" bind:value={serves} />
<label for="cookTimeMin">Kochzeit</label>
<input id="cookTimeMin" name="cookTimeMin" type="number" bind:value={cookTimeMin} />
<!-- Effort chips -->
<fieldset>
<legend>Schwierigkeitsgrad</legend>
{#each effortOptions as opt (opt.value)}
<label>
<input type="radio" name="effort" value={opt.value} bind:group={effort} />
{opt.label}
</label>
{/each}
</fieldset>
<!-- Category chips -->
<fieldset>
<legend>Kategorien</legend>
{#each categories as cat (cat.id)}
<label>
<input
type="checkbox"
name="tagIds"
value={cat.id}
checked={selectedTagIds.includes(cat.id)}
onchange={(e) => {
if (e.currentTarget.checked) {
selectedTagIds = [...selectedTagIds, cat.id];
} else {
selectedTagIds = selectedTagIds.filter((id) => id !== cat.id);
}
}}
/>
{cat.name}
</label>
{/each}
</fieldset>
<!-- Ingredients -->
<fieldset>
<legend>Zutaten</legend>
{#each ingredients as ing, i (i)}
<div>
<input type="number" bind:value={ing.quantity} placeholder="Menge" />
<input type="text" bind:value={ing.unit} placeholder="Einheit" />
<input type="text" bind:value={ing.name} placeholder="Zutat" />
<button type="button" onclick={() => (ingredients = ingredients.filter((_, j) => j !== i))}>
Entfernen
</button>
</div>
{/each}
<button
type="button"
onclick={() => (ingredients = [...ingredients, { name: '', quantity: '', unit: '' }])}
>
Zutat hinzufügen
</button>
</fieldset>
<!-- Steps -->
<fieldset>
<legend>Schritte</legend>
{#each steps as _, i (i)}
<div>
<textarea bind:value={steps[i]} placeholder="Schritt beschreiben…"></textarea>
<button type="button" onclick={() => (steps = steps.filter((_, j) => j !== i))}>
Entfernen
</button>
</div>
{/each}
<button type="button" onclick={() => (steps = [...steps, ''])}>Schritt hinzufügen</button>
</fieldset>
<!-- Hidden inputs for form submission -->
<input type="hidden" name="ingredientsJson" value={JSON.stringify(ingredients)} />
<input type="hidden" name="stepsJson" value={JSON.stringify(steps)} />
<!-- Footer -->
<a href="/recipes">Abbrechen</a>
<button type="submit">Speichern</button>
</form>