renaming, restructuring, adding an api util to the frontend (currently editPage only and a mock backend

This commit is contained in:
Anika Raemer 2025-09-10 20:04:26 +02:00
parent 1bd1952ecb
commit 38a5707622
16 changed files with 247 additions and 117 deletions

View file

@ -0,0 +1,121 @@
import { useParams, Link } from "react-router-dom"
import { recipes } from "../../mock_data/recipes"
import type { Recipe } from "../../types/recipe"
import { useState } from "react"
/**
* Displays the full detail of a single recipe,
* including its ingredients, instructions, and image.
*/
export default function RecipeDetailPage() {
// Extract recipe ID from route params
const { id } = useParams<{ id: string }>()
const recipe = recipes.find((r) => r.id === id)
if (!recipe) {
return <p className="p-6">Recipe not found.</p>
}
// Working copy for re-calculating ingredients
const [recipeWorkingCopy, updateRecipeWorkingCopy] = useState<Recipe>(recipe)
// Keep original immutable for scaling
const [originalRecipe] = useState<Recipe>(recipe)
/** recalculate ingredients based on the amount of servings */
const recalculateIngredients = (newAmount: number) => {
// Always calculate factor from the *original recipe*, not the working copy
const factor = newAmount / originalRecipe.servings.amount
// Create a new ingredient list with updated amounts
const updatedIngredientGroupList = recipe.ingredientGroupList.map((ingGrp) => ({
...ingGrp,
ingredientList: ingGrp.ingredientList.map((ing) => ({
...ing,
amount: ing.amount * factor,
}))
}))
// Update working copy with new servings + recalculated ingredients
updateRecipeWorkingCopy({
...recipeWorkingCopy,
servings: {
...recipeWorkingCopy.servings,
amount: newAmount,
},
ingredientGroupList: updatedIngredientGroupList,
})
}
return (
<div className="p-6 max-w-2xl mx-auto">
<h1 className="content-title">{recipeWorkingCopy.title}</h1>
{/* Recipe image */}
{recipe.imageUrl && (
<img
src={recipe.imageUrl}
alt={recipe.title}
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 */}
<h2 className="section-heading">Zutaten</h2>
<ul>
{recipeWorkingCopy.ingredientGroupList.map((group,i) => (
<div key={i}>
{/* the title is optional, only print if present */}
{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">{recipe.instructions}</p>
{/* Action buttons */}
<div className="button-group">
<Link
to={`/recipe/${recipe.id}/edit`}
className="primary-button"
>
Bearbeiten
</Link>
<Link
to="/"
className="default-button"
>
Zurueck
</Link>
</div>
</div>
)
}