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

@@ -161,7 +161,7 @@ class WeekPlanControllerTest {
@Test
void getSuggestionsShouldReturn200() throws Exception {
var recipe = new SlotResponse.SlotRecipe(UUID.randomUUID(), "Stir Fry", "easy", (short) 15, null);
var recipe = new SuggestionResponse.SuggestionRecipe(UUID.randomUUID(), "Stir Fry", "easy", (short) 15);
var item = new SuggestionResponse.SuggestionItem(recipe, 1.5, false);
var response = new SuggestionResponse(List.of(item));

View File

@@ -165,7 +165,7 @@ class RecipeControllerTest {
private RecipeCreateRequest sampleCreateRequest() {
var ingredientId = UUID.randomUUID();
return new RecipeCreateRequest(
"Spaghetti Bolognese", (short) 4, (short) 45, "medium", true, null,
"Spaghetti Bolognese", 4, 45, "medium", null,
List.of(new RecipeCreateRequest.IngredientEntry(
ingredientId, null, new BigDecimal("400"), "g", (short) 1)),
List.of(new RecipeCreateRequest.StepEntry((short) 1, "Boil water.")),

View File

@@ -126,7 +126,7 @@ class RecipeServiceTest {
});
var request = new RecipeCreateRequest(
"Spaghetti Bolognese", (short) 4, (short) 45, "medium", true, null,
"Spaghetti Bolognese", 4, 45, "medium", null,
List.of(new RecipeCreateRequest.IngredientEntry(
ingredient.getId(), null, new BigDecimal("400"), "g", (short) 1)),
List.of(new RecipeCreateRequest.StepEntry((short) 1, "Boil water.")),
@@ -166,7 +166,7 @@ class RecipeServiceTest {
});
var request = new RecipeCreateRequest(
"Carbonara", (short) 2, (short) 30, "medium", false, null,
"Carbonara", 2, 30, "medium", null,
List.of(new RecipeCreateRequest.IngredientEntry(
null, "pancetta", new BigDecimal("100"), "g", (short) 1)),
List.of(),
@@ -192,7 +192,7 @@ class RecipeServiceTest {
when(recipeRepository.save(any(Recipe.class))).thenAnswer(i -> i.getArgument(0));
var request = new RecipeCreateRequest(
"Chicken Rice", (short) 3, (short) 25, "easy", true, null,
"Chicken Rice", 3, 25, "easy", null,
List.of(new RecipeCreateRequest.IngredientEntry(
ingredient.getId(), null, new BigDecimal("300"), "g", (short) 1)),
List.of(new RecipeCreateRequest.StepEntry((short) 1, "Cook rice.")),
@@ -450,7 +450,7 @@ class RecipeServiceTest {
when(householdRepository.findById(HOUSEHOLD_ID)).thenReturn(Optional.empty());
var request = new RecipeCreateRequest(
"Test", (short) 2, (short) 15, "easy", false, null,
"Test", 2, 15, "easy", null,
List.of(), List.of(), List.of());
assertThatThrownBy(() -> recipeService.createRecipe(HOUSEHOLD_ID, request))
@@ -466,7 +466,7 @@ class RecipeServiceTest {
when(ingredientRepository.findById(ingredientId)).thenReturn(Optional.empty());
var request = new RecipeCreateRequest(
"Test", (short) 2, (short) 15, "easy", false, null,
"Test", 2, 15, "easy", null,
List.of(new RecipeCreateRequest.IngredientEntry(
ingredientId, null, new BigDecimal("100"), "g", (short) 1)),
List.of(), List.of());
@@ -491,7 +491,7 @@ class RecipeServiceTest {
});
var request = new RecipeCreateRequest(
"Simple", (short) 1, (short) 5, "easy", false, null,
"Simple", 1, 5, "easy", null,
null, null, null);
RecipeDetailResponse result = recipeService.createRecipe(HOUSEHOLD_ID, request);
@@ -518,7 +518,7 @@ class RecipeServiceTest {
.thenReturn(Optional.empty());
var request = new RecipeCreateRequest(
"Updated", (short) 2, (short) 20, "easy", false, null,
"Updated", 2, 20, "easy", null,
List.of(), List.of(), List.of());
assertThatThrownBy(() -> recipeService.updateRecipe(HOUSEHOLD_ID, id, request))