feat(recipes): add image upload, fix save 500, seed HelloFresh data

- Store hero image as base64 data URI in text column (V023 migration)
- Add file upload UI to RecipeForm with FileReader preview
- Remove isChildFriendly from RecipeCreateRequest (no form field)
- Fix 500 on save: effort values now lowercase, serves/cookTimeMin changed
  from primitive short to nullable Integer to survive omitted fields
- Fix empty categories panel: removed stale tagType=category filter
- Group category tags by type with German headings in recipe form
- Split SuggestionResponse.SuggestionRecipe (no image) from SlotRecipe
- Seed 11 HelloFresh recipes with ingredients, steps and tags (V101)
- Add frontend e2e scaffold, specs and dev yml

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-09 20:23:28 +02:00
parent 116e400a91
commit 520dae5adf
34 changed files with 9862 additions and 84 deletions

View File

@@ -2,7 +2,7 @@ import { error, redirect, fail } from '@sveltejs/kit';
import type { PageServerLoad, Actions } from './$types';
import { apiClient } from '$lib/server/api';
const VALID_EFFORTS = ['Easy', 'Medium', 'Hard'];
const VALID_EFFORTS = ['easy', 'medium', 'hard'];
export const load: PageServerLoad = async ({ fetch, params }) => {
const api = apiClient(fetch);
@@ -17,9 +17,7 @@ export const load: PageServerLoad = async ({ fetch, params }) => {
const recipe = recipeResult.data;
const allTags = tagsResult.data ?? [];
const categories = allTags
.filter((t) => t.tagType === 'category')
.map((t) => ({ id: t.id!, name: t.name!, tagType: t.tagType }));
const categories = allTags.map((t) => ({ id: t.id!, name: t.name!, tagType: t.tagType }));
return {
recipe: {
@@ -50,6 +48,7 @@ export const actions: Actions = {
const serves = formData.get('serves');
const cookTimeMin = formData.get('cookTimeMin');
const effort = formData.get('effort') as string;
const heroImageUrl = (formData.get('heroImageUrl') as string) || null;
const ingredientsJson = formData.get('ingredientsJson') as string;
const stepsJson = formData.get('stepsJson') as string;
const tagIds = formData.getAll('tagIds') as string[];
@@ -76,6 +75,7 @@ export const actions: Actions = {
serves: serves ? Number(serves) || undefined : undefined,
cookTimeMin: cookTimeMin ? Number(cookTimeMin) || undefined : undefined,
effort,
heroImageUrl,
ingredients: (parsedIngredients as { name: string; quantity: string; unit: string }[])
.filter((ing) => ing.name?.trim())
.map((ing, i) => ({