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 ( return (
<div> <div>
<h3 className="subsection-heading">Ingredients</h3>
{ingredients.map((ing, index) => ( {ingredients.map((ing, index) => (
<div key={index} className="flex gap-2 mb-2 items-center"> <div key={index} className="flex gap-2 mb-2 items-center">
<input <input

View file

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

View file

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

View file

@ -9,10 +9,14 @@ export const recipes: Recipe[] = [
id: "1", id: "1",
title: "Spaghetti Bolognese", title: "Spaghetti Bolognese",
servings: { amount: 1, unit: "Person"}, servings: { amount: 1, unit: "Person"},
ingredients: [ ingredientGroupList: [
{ name: "Spaghetti", amount: 200, unit: "g" }, {
{ name: "Ground Beef", amount: 300, unit: "g" }, ingredientList: [
{ name: "Tomato Sauce", amount: 400, unit: "ml" } { 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.", instructions: "Cook pasta. Prepare sauce. Mix together. Serve hot.",
//imageUrl: "https://source.unsplash.com/400x300/?spaghetti" //imageUrl: "https://source.unsplash.com/400x300/?spaghetti"
@ -21,15 +25,39 @@ export const recipes: Recipe[] = [
id: "2", id: "2",
title: "Spaghetti Carbonara", title: "Spaghetti Carbonara",
servings: { amount: 4, unit: "Persons"}, servings: { amount: 4, unit: "Persons"},
ingredients: [ ingredientGroupList: [
{ name: "Spaghetti", amount: 500, unit: "g" }, {
{ name: "Bacon", amount: 150, unit: "g" }, ingredientList:[
{ name: "Cream", amount: 200, unit: "ml" }, { name: "Spaghetti", amount: 500, unit: "g" },
{ name: "Onion", amount: 1}, { name: "Bacon", amount: 150, unit: "g" },
{ name: "Parmesan cheese", amount: 200, unit: "g"}, { name: "Cream", amount: 200, unit: "ml" },
{ name: "Olives", amount: 100, unit: "g"} { 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.", instructions: "Cook pasta. Prepare sauce. Mix together. Serve hot.",
//imageUrl: "https://source.unsplash.com/400x300/?spaghetti" //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" import type { Servings } from "./servings"
/** /**
@ -18,8 +18,8 @@ export interface Recipe {
/** Title of the recipe */ /** Title of the recipe */
title: string title: string
/** List of ingredients with amount + unit */ /** List of ingredients groups containing the ingredients of the recipe */
ingredients: Ingredient[] ingredientGroupList: IngredientGroup[]
/** Preparation instructions */ /** Preparation instructions */
instructions: string instructions: string