add calculation of ingredients based on servings

This commit is contained in:
Anika Raemer 2025-09-07 16:28:46 +02:00
parent fee47da55d
commit e467ca7e92
5 changed files with 63 additions and 14 deletions

View file

@ -59,7 +59,7 @@
/* input field */ /* input field */
.input-field { .input-field {
@apply border p-2 w-full mb-2 rounded placeholder-gray-400; @apply border p-2 w-full rounded placeholder-gray-400;
} }
.text-area { .text-area {
@ -72,7 +72,7 @@
} }
/* lists */ /* lists */
.default-list-item { .default-list {
@apply list-disc pl-6 mb-6 @apply list-disc pl-6 mb-6
} }

View file

@ -1,5 +1,8 @@
import { useParams, Link } from "react-router-dom" import { useParams, Link } from "react-router-dom"
import { recipes } from "../mock_data/recipes" import { recipes } from "../mock_data/recipes"
import type { Recipe } from "../types/recipe"
import { useState } from "react"
/** /**
* Displays the full detail of a single recipe, * Displays the full detail of a single recipe,
@ -10,28 +13,66 @@ export default function RecipeDetailView() {
const { id } = useParams<{ id: string }>() const { id } = useParams<{ id: string }>()
const recipe = recipes.find((r) => r.id === id) const recipe = recipes.find((r) => r.id === id)
if (!recipe) { if (!recipe) {
return <p className="p-6">Recipe not found.</p> return <p className="p-6">Recipe not found.</p>
} }
return ( // Working copy for re-calculating ingredients
const [recipeWorkingCopy, updateRecipeWorkingCopy] = useState<Recipe>(recipe)
const recalculateIngredients = (newAmount: number) => {
// Always calculate factor from the *original recipe*, not the working copy
const factor = newAmount / recipe.servings.amount
// Create a new ingredient list with updated amounts
const updatedIngredients = recipe.ingredients.map((ing) => ({
...ing,
amount: ing.amount * factor,
}))
// Update working copy with new servings + recalculated ingredients
updateRecipeWorkingCopy({
...recipeWorkingCopy,
servings: {
...recipeWorkingCopy.servings,
amount: newAmount,
},
ingredients: updatedIngredients,
})
}
// @todo add a feature to recalculate ingredients based on servings
return (
<div className="p-6 max-w-2xl mx-auto"> <div className="p-6 max-w-2xl mx-auto">
<h1 className="content-title">{recipe.title}</h1> <h1 className="content-title">{recipeWorkingCopy.title}</h1>
{/* Recipe image */} {/* Recipe image */}
{recipe.imageUrl && ( {recipeWorkingCopy.imageUrl && (
<img <img
src={recipe.imageUrl} src={recipeWorkingCopy.imageUrl}
alt={recipe.title} alt={recipeWorkingCopy.title}
className="w-full rounded-xl mb-4" className="w-full rounded-xl mb-4 border"
/> />
)} )}
{/* Servings */}
<div className="flex flex-row items-center gap-2 bg-blue-100 columns-2 rounded p-2 mb-4">
<p className="mb-2">For {recipeWorkingCopy.servings.amount} {recipeWorkingCopy.servings.unit}</p>
<input
type="number"
className="input-field w-20 ml-auto"
value={recipeWorkingCopy.servings.amount}
onChange={
e => {
recalculateIngredients(Number(e.target.value))
}
}
/>
</div>
{/* Ingredients */} {/* Ingredients */}
<h2 className="section-heading">Zutaten</h2> <h2 className="section-heading">Zutaten</h2>
<p>For {recipe.servings.amount} {recipe.servings.unit}</p> <ul className="default-list">
<ul className="default-list-item"> {recipeWorkingCopy.ingredients.map((ing, i) => (
{recipe.ingredients.map((ing, i) => (
<li key={i}> <li key={i}>
{ing.amount} {ing.unit ?? ""} {ing.name} {ing.amount} {ing.unit ?? ""} {ing.name}
</li> </li>
@ -40,7 +81,7 @@ export default function RecipeDetailView() {
{/* Instructions */} {/* Instructions */}
<h2 className="section-heading">Zubereitung</h2> <h2 className="section-heading">Zubereitung</h2>
<p className="mb-6">{recipe.instructions}</p> <p className="mb-6">{recipeWorkingCopy.instructions}</p>
{/* Action buttons */} {/* Action buttons */}
<div className="button-group"> <div className="button-group">

View file

@ -20,6 +20,7 @@ export default function RecipeEditor({ recipe, onSave, onCancel }: RecipeEditorP
setDraft({ ...draft, ingredients }) setDraft({ ...draft, ingredients })
} }
if (!recipe) return <div>Oops, there's no recipe in RecipeEditor...</div> if (!recipe) return <div>Oops, there's no recipe in RecipeEditor...</div>
// @todo add handling of images
return ( return (
<div className="p-4 gap-10"> <div className="p-4 gap-10">
<h2 className="content-title"> <h2 className="content-title">

View file

@ -15,7 +15,7 @@ export const recipes: Recipe[] = [
{ name: "Tomato Sauce", amount: 400, unit: "ml" } { 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"
}, },
{ {
id: "2", id: "2",
@ -30,6 +30,6 @@ export const recipes: Recipe[] = [
{ name: "Olives", amount: 100, 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"
}, },
] ]

View file

@ -4,6 +4,13 @@ import type { Servings } from "./servings"
/** /**
* Represents a recipe object in the application. * Represents a recipe object in the application.
*/ */
/*
* @todo ingredient groups! There may be serveral ingredient lists, each with a title.
* e.g. for the dough, for the filling, for the icing,...
* - add type ingredient group with an optional title and a list of ingredients
* - adapt RecipeDetailView
* - add an IngredientGroupListEditor for handling IngredientGroups
*/
export interface Recipe { export interface Recipe {
/** Unique identifier for the recipe */ /** Unique identifier for the recipe */
id: string id: string