Smaller refactorings, e.g., margins for title
This commit is contained in:
parent
ef8388be6d
commit
e6ea18bef8
1 changed files with 138 additions and 141 deletions
|
|
@ -1,10 +1,10 @@
|
|||
import { useParams } from "react-router-dom"
|
||||
import type { RecipeModel } from "../../models/RecipeModel"
|
||||
import { useEffect, useState } from "react"
|
||||
import { fetchRecipe } from "../../api/points/RecipePoint"
|
||||
import { getRecipeEditUrl, getRecipeListUrl } from "../../routes"
|
||||
import {useParams} from "react-router-dom"
|
||||
import type {RecipeModel} from "../../models/RecipeModel"
|
||||
import {useEffect, useState} from "react"
|
||||
import {fetchRecipe} from "../../api/points/RecipePoint"
|
||||
import {getRecipeEditUrl, getRecipeListUrl} from "../../routes"
|
||||
import ButtonLink from "../basics/ButtonLink"
|
||||
import { mapRecipeDtoToModel } from "../../mappers/RecipeMapper"
|
||||
import {mapRecipeDtoToModel} from "../../mappers/RecipeMapper"
|
||||
|
||||
|
||||
/**
|
||||
|
|
@ -12,153 +12,150 @@ import { mapRecipeDtoToModel } from "../../mappers/RecipeMapper"
|
|||
* including its ingredients, instructions, and image.
|
||||
*/
|
||||
export default function RecipeDetailPage() {
|
||||
// Extract recipe ID from route params
|
||||
const { id } = useParams<{ id: string }>()
|
||||
// the recipe loaded from the backend, don't change this! it's required for scaling
|
||||
const [recipe, setRecipe] = useState<RecipeModel | null>(null)
|
||||
// Working copy for re-calculating ingredients
|
||||
const [recipeWorkingCopy, setRecipeWorkingCopy] = useState<RecipeModel | null>(null)
|
||||
// load recipe data whenever id changes
|
||||
useEffect(() => {
|
||||
const loadRecipe = async () => {
|
||||
if (id) {
|
||||
try {
|
||||
// Fetch recipe data when editing an existing one
|
||||
console.log("loading recipe with id", id)
|
||||
const data = await fetchRecipe(id)
|
||||
if (data.id != id) {
|
||||
throw new Error("Id mismatch when loading recipes: " + id + " requested and " + data.id + " received!");
|
||||
}
|
||||
setRecipe(mapRecipeDtoToModel(data))
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
// Extract recipe ID from route params
|
||||
const {id} = useParams<{ id: string }>()
|
||||
// the recipe loaded from the backend, don't change this! it's required for scaling
|
||||
const [recipe, setRecipe] = useState<RecipeModel | null>(null)
|
||||
// Working copy for re-calculating ingredients
|
||||
const [recipeWorkingCopy, setRecipeWorkingCopy] = useState<RecipeModel | null>(null)
|
||||
// load recipe data whenever id changes
|
||||
useEffect(() => {
|
||||
const loadRecipe = async () => {
|
||||
if (id) {
|
||||
// Fetch recipe data when editing an existing one
|
||||
console.log("loading recipe with id", id)
|
||||
const data = await fetchRecipe(id)
|
||||
if (data.id != id) {
|
||||
throw new Error("Id mismatch when loading recipes: " + id + " requested and " + data.id + " received!");
|
||||
}
|
||||
setRecipe(mapRecipeDtoToModel(data))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loadRecipe()
|
||||
}, [id])
|
||||
|
||||
// set original recipe data and working copy when recipe changes
|
||||
useEffect(() => {
|
||||
setRecipeWorkingCopy(recipe);
|
||||
}, [recipe])
|
||||
|
||||
|
||||
if (!recipe || !recipeWorkingCopy) {
|
||||
return <p className="p-6">Recipe not found.</p>
|
||||
}
|
||||
|
||||
loadRecipe()
|
||||
}, [id])
|
||||
|
||||
// set original recipe data and working copy when recipe changes
|
||||
useEffect(() => {
|
||||
setRecipeWorkingCopy(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 / recipe.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,
|
||||
}))
|
||||
}))
|
||||
|
||||
if (!recipe || !recipeWorkingCopy) {
|
||||
return <p className="p-6">Recipe not found.</p>
|
||||
}
|
||||
// Update working copy with new servings + recalculated ingredients
|
||||
setRecipeWorkingCopy({
|
||||
...recipeWorkingCopy,
|
||||
servings: {
|
||||
...recipeWorkingCopy.servings,
|
||||
amount: newAmount,
|
||||
},
|
||||
ingredientGroupList: updatedIngredientGroupList,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
/*Container spanning entire screen used to center content horizontally */
|
||||
<div className="app-bg">
|
||||
{/* Container defining the maximum width of the content */}
|
||||
<div className="content-container">
|
||||
{/* Header - remains in position when scrolling */}
|
||||
<div className="sticky bg-gray-100 top-0 left-0 right-0 pb-6 border-b-2 border-gray-300">
|
||||
<h1 className="content-title mb-0">{recipeWorkingCopy.title}</h1>
|
||||
</div>
|
||||
|
||||
/** 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 / recipe.servings.amount
|
||||
{/* Content */}
|
||||
<div className="p-6 max-w-2xl mx-auto">
|
||||
{/* Recipe image */}
|
||||
{recipe.imageUrl && (
|
||||
<img
|
||||
src={recipe.imageUrl}
|
||||
alt={recipe.title}
|
||||
className="w-full rounded-xl mb-4 border"
|
||||
/>
|
||||
)}
|
||||
|
||||
// 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,
|
||||
}))
|
||||
}))
|
||||
{/* 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>
|
||||
|
||||
// Update working copy with new servings + recalculated ingredients
|
||||
setRecipeWorkingCopy({
|
||||
...recipeWorkingCopy,
|
||||
servings: {
|
||||
...recipeWorkingCopy.servings,
|
||||
amount: newAmount,
|
||||
},
|
||||
ingredientGroupList: updatedIngredientGroupList,
|
||||
})
|
||||
}
|
||||
{/* Instructions - @todo add reasonable list delegate component*/}
|
||||
<ol className="space-y-4">
|
||||
{recipe.instructionStepList.map((step, j) => (
|
||||
<li key={j} className="flex items-start gap-4">
|
||||
{/* Step number circle */}
|
||||
<div className="enumeration-indicator">
|
||||
{j + 1}
|
||||
</div>
|
||||
|
||||
return (
|
||||
/*Container spanning entire screen used to center content horizontally */
|
||||
<div className="app-bg">
|
||||
{/* Container defining the maximum width of the content */}
|
||||
<div className="content-container">
|
||||
{/* Header - remains in position when scrolling */}
|
||||
<div className="sticky bg-gray-100 top-0 left-0 right-0 pb-4 border-b-2 border-gray-300">
|
||||
<h1 className="content-title">{recipeWorkingCopy.title}</h1>
|
||||
</div>
|
||||
{/* Step text */}
|
||||
<p className="leading-relaxed">{step.text}</p>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
|
||||
<div className="p-6 max-w-2xl mx-auto">
|
||||
{/* 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>
|
||||
{/* Action buttons */}
|
||||
<div className="button-group">
|
||||
<ButtonLink
|
||||
to={recipe.id !== undefined ? getRecipeEditUrl(recipe.id) : getRecipeListUrl()} // @todo show error instead
|
||||
className="basic-button primary-button-bg primary-button-text"
|
||||
text="Bearbeiten"
|
||||
/>
|
||||
<ButtonLink
|
||||
to={getRecipeListUrl()}
|
||||
className="basic-button default-button-bg default-button-text"
|
||||
text="Zurueck"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{/* Instructions - @todo add reasonable list delegate component*/}
|
||||
<ol className="space-y-4">
|
||||
{recipe.instructionStepList.map((step, j) => (
|
||||
<li key={j} className="flex items-start gap-4">
|
||||
{/* Step number circle */}
|
||||
<div className="enumeration-indicator">
|
||||
{j + 1}
|
||||
</div>
|
||||
|
||||
{/* Step text */}
|
||||
<p className="leading-relaxed">{step.text}</p>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="button-group">
|
||||
<ButtonLink
|
||||
to={recipe.id !== undefined ? getRecipeEditUrl(recipe.id) : getRecipeListUrl()} // @todo show error instead
|
||||
className="basic-button primary-button-bg primary-button-text"
|
||||
text="Bearbeiten"
|
||||
/>
|
||||
<ButtonLink
|
||||
to={getRecipeListUrl()}
|
||||
className="basic-button default-button-bg default-button-text"
|
||||
text="Zurueck"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue