feat(recipe): Edit-Modus für Zutaten, Schritte und Meta
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m18s

Auf der Rezept-Detail-Seite ein neuer „Bearbeiten"-Button (Pencil-Icon)
in der Action-Bar. Klick schaltet RecipeView auf RecipeEditor um.

Im Editor:
- Titel, Beschreibung, Portionen, Vorbereitungs-/Koch-/Gesamtzeit als
  inline-Inputs.
- Zutaten: pro Zeile Menge, Einheit, Name, Notiz + Trash-Icon zum
  Entfernen. „+ Zutat hinzufügen"-Dashed-Button am Listenende.
- Schritte: nummerierte Textareas, Trash-Icon, „+ Schritt hinzufügen".
- Mengen akzeptieren Komma- oder Punkt-Dezimalen.
- Empty-Items werden beim Speichern automatisch aussortiert.

Backend:
- Neue Repo-Funktionen updateRecipeMeta(id, patch), replaceIngredients,
  replaceSteps — letztere in einer Transaction mit delete+insert und
  FTS-Refresh.
- PATCH /api/recipes/[id] akzeptiert jetzt zusätzlich description,
  servings_default, servings_unit, prep_time_min, cook_time_min,
  total_time_min, cuisine, category, ingredients[], steps[]. Vorher
  nur title/hidden_from_recent; diese beiden bleiben als
  Kurz-Fall erhalten, damit bestehende Aufrufer unverändert laufen.
- Zod-Schema mit expliziten Grenzen (max-Länge, positive Mengen).

Tests: 3 neue Cases für updateRecipeMeta, replaceIngredients (inkl.
FTS-Update), replaceSteps.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-18 12:41:10 +02:00
parent ee783ff50b
commit 9a5c626890
5 changed files with 668 additions and 11 deletions

View File

@@ -14,6 +14,7 @@
LightbulbOff
} from 'lucide-svelte';
import RecipeView from '$lib/components/RecipeView.svelte';
import RecipeEditor from '$lib/components/RecipeEditor.svelte';
import StarRating from '$lib/components/StarRating.svelte';
import { profileStore } from '$lib/client/profile.svelte';
import { wishlistStore } from '$lib/client/wishlist.svelte';
@@ -35,6 +36,46 @@
let titleDraft = $state('');
let titleInput: HTMLInputElement | null = $state(null);
let editMode = $state(false);
let saving = $state(false);
let recipeState = $state(data.recipe);
async function saveRecipe(patch: {
title: string;
description: string | null;
servings_default: number | null;
prep_time_min: number | null;
cook_time_min: number | null;
total_time_min: number | null;
ingredients: typeof data.recipe.ingredients;
steps: typeof data.recipe.steps;
}) {
saving = true;
try {
const res = await fetch(`/api/recipes/${data.recipe.id}`, {
method: 'PATCH',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(patch)
});
if (!res.ok) {
const body = await res.json().catch(() => ({}));
await alertAction({
title: 'Speichern fehlgeschlagen',
message: body.message ?? `HTTP ${res.status}`
});
return;
}
const body = await res.json();
if (body.recipe) {
recipeState = body.recipe;
title = body.recipe.title;
}
editMode = false;
} finally {
saving = false;
}
}
$effect(() => {
ratings = [...data.ratings];
comments = [...data.comments];
@@ -42,6 +83,7 @@
favoriteProfileIds = [...data.favorite_profile_ids];
wishlistProfileIds = [...data.wishlist_profile_ids];
title = data.recipe.title;
recipeState = data.recipe;
});
const myRating = $derived(
@@ -290,7 +332,15 @@
});
</script>
<RecipeView recipe={data.recipe}>
{#if editMode}
<RecipeEditor
recipe={recipeState}
{saving}
onsave={saveRecipe}
oncancel={() => (editMode = false)}
/>
{:else}
<RecipeView recipe={recipeState}>
{#snippet titleSlot()}
<div class="title-row">
{#if editingTitle}
@@ -369,6 +419,10 @@
<span>Bildschirm aus</span>
{/if}
</button>
<button class="btn" onclick={() => (editMode = true)}>
<Pencil size={18} strokeWidth={2} />
<span>Bearbeiten</span>
</button>
<button class="btn danger" onclick={deleteRecipe}>
<Trash2 size={18} strokeWidth={2} />
<span>Löschen</span>
@@ -377,6 +431,7 @@
</div>
{/snippet}
</RecipeView>
{/if}
<section class="comments">
<h2>Kommentare</h2>