added ingredient group editor

This commit is contained in:
Anika Raemer 2025-09-07 20:44:26 +02:00
parent 37057f19f1
commit d4e8a4d09a
7 changed files with 154 additions and 32 deletions

View file

@ -0,0 +1,67 @@
/**
* Editor for ingredient groups
*/
import type { Ingredient } from "../types/ingredient"
import type { IngredientGroup } from "../types/ingredientGroup"
import { IngredientListEditor } from "./IngredientListEditor"
type IngredientGroupListEditorProps = {
ingredientGroupList: IngredientGroup[]
onChange: (ingredientGroupList: IngredientGroup[]) => void
}
export function IngredientGroupListEditor({ ingredientGroupList, onChange }: IngredientGroupListEditorProps) {
const handleUpdate = (index: number, field: keyof IngredientGroup, value: string|Ingredient[] ) => {
const updated = ingredientGroupList.map((ingGrp, i) =>
i === index ? { ...ingGrp, [field]: value} : ingGrp
)
onChange(updated)
}
const updateIngredientList = (index: number, ingredientList: Ingredient[]) => {
handleUpdate(index, "ingredientList", ingredientList)
}
const handleAdd = () => {
onChange([...ingredientGroupList, { title: "", ingredientList: [] }])
}
const handleRemove = (index: number) => {
onChange(ingredientGroupList.filter((_, i) => i !== index))
}
return (
<div>
<h3 className="subsection-heading">Ingredient Groups</h3>
{ingredientGroupList.map((ingGrp, index) => (
<div key={index} className="mb-2 py-4 border-y border-gray-300">
<div className="flex columns-2 gap-2 mb-2 items-center">
<input
className="input-field"
placeholder="Group title (Optional)"
value = {ingGrp.title}
onChange={ e => handleUpdate(index, "title", e.target.value)}
/>
<button
type="button"
className="dark-button whitespace-nowrap"
onClick={() => handleRemove(index)}
>
Remove Group
</button>
</div>
<IngredientListEditor
ingredients={ingGrp.ingredientList}
onChange={list => updateIngredientList(index,list)}
/>
</div>
))}
<button
type="button"
className="primary-button mt-2"
onClick={handleAdd}
>
Add Ingredient Group
</button>
</div>
)
}

View file

@ -27,7 +27,6 @@ export function IngredientListEditor({ ingredients, onChange }: IngredientListEd
return (
<div>
<h3 className="subsection-heading">Ingredients</h3>
{ingredients.map((ing, index) => (
<div key={index} className="flex gap-2 mb-2 items-center">
<input

View file

@ -47,10 +47,10 @@ export default function RecipeDetailView() {
<h1 className="content-title">{recipeWorkingCopy.title}</h1>
{/* Recipe image */}
{recipeWorkingCopy.imageUrl && (
{recipe.imageUrl && (
<img
src={recipeWorkingCopy.imageUrl}
alt={recipeWorkingCopy.title}
src={recipe.imageUrl}
alt={recipe.title}
className="w-full rounded-xl mb-4 border"
/>
)}
@ -71,17 +71,26 @@ export default function RecipeDetailView() {
</div>
{/* Ingredients */}
<h2 className="section-heading">Zutaten</h2>
<ul className="default-list">
{recipeWorkingCopy.ingredients.map((ing, i) => (
<li key={i}>
{ing.amount} {ing.unit ?? ""} {ing.name}
</li>
<ul>
{recipe.ingredientGroupList.map((group,i) => (
<div key={i}>
{group.title && group.title.trim() !== "" && (
<h3 className="subsection-heading">{group.title}</h3>
)}
<ul className="default-list">
{group.ingredientList.map((ing, j) => (
<li key={j}>
{ing.amount} {ing.unit ?? ""} {ing.name}
</li>
))}
</ul>
</div>
))}
</ul>
{/* Instructions */}
<h2 className="section-heading">Zubereitung</h2>
<p className="mb-6">{recipeWorkingCopy.instructions}</p>
<p className="mb-6">{recipe.instructions}</p>
{/* Action buttons */}
<div className="button-group">

View file

@ -1,7 +1,7 @@
import { useState } from "react"
import type { Recipe } from "../types/recipe"
import type { Ingredient } from "../types/ingredient"
import {IngredientListEditor} from "./IngredientListEditor"
import type { IngredientGroup } from "../types/ingredientGroup"
import { IngredientGroupListEditor } from "./IngredientGroupListEditor"
type RecipeEditorProps = {
recipe: Recipe
@ -12,6 +12,7 @@ type RecipeEditorProps = {
/**
* Editor component for managing a recipe, including title,
* ingredients (with amount, unit, name), instructions, and image URL.
* @todo adapt to ingredientGroups!
*/
export default function RecipeEditor({ recipe, onSave, onCancel }: RecipeEditorProps) {
/** draft of the new recipe */
@ -23,8 +24,8 @@ export default function RecipeEditor({ recipe, onSave, onCancel }: RecipeEditorP
* Update ingredients
* @param ingredients new ingredients
*/
const updateIngredients = (ingredients: Ingredient[]) => {
setDraft({ ...draft, ingredients })
const updateIngredientGroupList = (ingredientGroupList: IngredientGroup[]) => {
setDraft({ ...draft, ingredientGroupList })
}
/**
* Validate recipe
@ -40,7 +41,7 @@ export default function RecipeEditor({ recipe, onSave, onCancel }: RecipeEditorP
// the incredient list must not be empty
// @todo enhance validation and visualization of ingredient errors
if (!draft.ingredients || draft.ingredients.length === 0) {
if (!draft.ingredientGroupList || draft.ingredientGroupList.length === 0) {
newErrors.ingredients = true
}
@ -99,9 +100,9 @@ export default function RecipeEditor({ recipe, onSave, onCancel }: RecipeEditorP
</div>
{/* Ingredient List - @todo better visualization of errors! */}
<div className={errors.ingredients ? "border border-red-500 rounded p-2" : ""}>
<IngredientListEditor
ingredients={draft.ingredients}
onChange={updateIngredients}
<IngredientGroupListEditor
ingredientGroupList={draft.ingredientGroupList}
onChange={updateIngredientGroupList}
/>
</div>

View file

@ -9,10 +9,14 @@ export const recipes: Recipe[] = [
id: "1",
title: "Spaghetti Bolognese",
servings: { amount: 1, unit: "Person"},
ingredients: [
{ name: "Spaghetti", amount: 200, unit: "g" },
{ name: "Ground Beef", amount: 300, unit: "g" },
{ name: "Tomato Sauce", amount: 400, unit: "ml" }
ingredientGroupList: [
{
ingredientList: [
{ name: "Spaghetti", amount: 200, unit: "g" },
{ name: "Ground Beef", amount: 300, unit: "g" },
{ name: "Tomato Sauce", amount: 400, unit: "ml" }
]
}
],
instructions: "Cook pasta. Prepare sauce. Mix together. Serve hot.",
//imageUrl: "https://source.unsplash.com/400x300/?spaghetti"
@ -21,15 +25,39 @@ export const recipes: Recipe[] = [
id: "2",
title: "Spaghetti Carbonara",
servings: { amount: 4, unit: "Persons"},
ingredients: [
{ name: "Spaghetti", amount: 500, unit: "g" },
{ name: "Bacon", amount: 150, unit: "g" },
{ name: "Cream", amount: 200, unit: "ml" },
{ name: "Onion", amount: 1},
{ name: "Parmesan cheese", amount: 200, unit: "g"},
{ name: "Olives", amount: 100, unit: "g"}
ingredientGroupList: [
{
ingredientList:[
{ name: "Spaghetti", amount: 500, unit: "g" },
{ name: "Bacon", amount: 150, unit: "g" },
{ name: "Cream", amount: 200, unit: "ml" },
{ name: "Onion", amount: 1},
{ name: "Parmesan cheese", amount: 200, unit: "g"},
{ name: "Olives", amount: 100, unit: "g"}
]
}
],
instructions: "Cook pasta. Prepare sauce. Mix together. Serve hot.",
//imageUrl: "https://source.unsplash.com/400x300/?spaghetti"
},
{ id: "3",
title: "Apfelkuchen Edeltrud",
servings: { amount: 1, unit: "Kuchen"},
ingredientGroupList:[
{
title: "Fuer den Teig",
ingredientList: [
{ name: "Mehl", amount: 400, unit: "g" }
]
},
{
title: "Fuer die Fuellung",
ingredientList:[
{name: "Aepfel", amount: 4},
{name: "Rosinen", amount: 1, unit: "Hand voll"}
]
}
],
instructions: "Einen Muerbteig von 400 g Mehl zubereiten"
}
]

View file

@ -0,0 +1,18 @@
import type { Ingredient } from "./ingredient"
/**
* A group of ingredients
* Consisting of title and ingredient list, this interface is used to group
* the ingredients for a specific part of the dish, e.g., dough, filling and
* icing of a cake
*/
export interface IngredientGroup {
/**
* Title of the group describing its purpose
* The title is optional as recipes consisting of a single ingredient group usually don't
* supply a title
*/
title? : string
/** Ingredients */
ingredientList : Ingredient[]
}

View file

@ -1,4 +1,4 @@
import type { Ingredient } from "./ingredient"
import type { IngredientGroup } from "./ingredientGroup"
import type { Servings } from "./servings"
/**
@ -18,8 +18,8 @@ export interface Recipe {
/** Title of the recipe */
title: string
/** List of ingredients with amount + unit */
ingredients: Ingredient[]
/** List of ingredients groups containing the ingredients of the recipe */
ingredientGroupList: IngredientGroup[]
/** Preparation instructions */
instructions: string