docs and background for details page

This commit is contained in:
Anika Raemer 2025-10-12 20:09:00 +02:00
parent 3f075d509b
commit ef8388be6d
5 changed files with 123 additions and 106 deletions

View file

@ -10,6 +10,10 @@
@apply flex items-center w-screen justify-center min-h-screen bg-gray-50; @apply flex items-center w-screen justify-center min-h-screen bg-gray-50;
} }
.content-container {
@apply bg-gray-100 w-full min-h-screen max-w-6xl shadow-xl p-8
}
/* headings */ /* headings */
.content-title{ .content-title{
@apply text-3xl font-black mb-8 text-blue-900; @apply text-3xl font-black mb-8 text-blue-900;
@ -61,7 +65,6 @@
.dark-button-text{ .dark-button-text{
@apply text-white; @apply text-white;
} }
.transparent-button-bg { .transparent-button-bg {
@apply bg-transparent hover:bg-transparent; @apply bg-transparent hover:bg-transparent;
} }
@ -71,7 +74,7 @@
/* input fields like input and textarea */ /* input fields like input and textarea */
.input-field { .input-field {
@apply p-2 w-full border rounded-md placeholder-gray-400 border-gray-600 hover:border-blue-800 transition-colors text-gray-600 focus:outline-none focus:border-blue-900; @apply p-2 w-full border rounded-md bg-white placeholder-gray-400 border-gray-600 hover:border-blue-800 transition-colors text-gray-600 focus:outline-none focus:border-blue-900;
} }
/* groups */ /* groups */

View file

@ -1,5 +1,8 @@
import type { LucideIcon } from "lucide-react"; import type { LucideIcon } from "lucide-react";
/**
* Basic definitions used by all Button types, such as Button.tsx and ButtonLink.tsx
*/
export type BasicButtonProps = { export type BasicButtonProps = {
/** Optional Lucide icon (e.g. Plus, X, Check) */ /** Optional Lucide icon (e.g. Plus, X, Check) */
icon?: LucideIcon; icon?: LucideIcon;
@ -15,20 +18,20 @@ export type BasicButtonProps = {
*/ */
export const ButtonType = { export const ButtonType = {
DarkButton: { DarkButton: {
textColor: "text-dark-button-text", textColor: "dark-button-text",
backgroundColor: "bg-dark-button-bg", backgroundColor: "dark-button-bg",
}, },
PrimaryButton: { PrimaryButton: {
textColor: "text-primary-button-text", textColor: "primary-button-text",
backgroundColor: "bg-primary-button-bg", backgroundColor: "primary-button-bg",
}, },
DefaultButton: { DefaultButton: {
textColor: "text-default-button-text", textColor: "default-button-text",
backgroundColor: "bg-default-button-bg", backgroundColor: "default-button-bg",
}, },
TransparentButton: { TransparentButton: {
textColor: "text-transparent-button-text", textColor: "transparent-button-text",
backgroundColor: "bg-transparent-button-bg", backgroundColor: "transparent-button-bg",
}, },
} as const; } as const;

View file

@ -5,6 +5,9 @@ type ButtonProps = BasicButtonProps & {
onClick: () => void; onClick: () => void;
}; };
/**
* Button component. Styles are defined in BasicButtonDefinitions.ts
*/
export default function Button({ export default function Button({
onClick, onClick,
icon: Icon, icon: Icon,
@ -15,7 +18,7 @@ export default function Button({
}: ButtonProps) { }: ButtonProps) {
return ( return (
<button <button
className={`basic-button ${buttonType.backgroundColor} ${buttonType.textColor} ${className}`} className={`basic-button bg-primary-button-bg ${buttonType.backgroundColor} ${buttonType.textColor} ${className}`}
onClick={onClick} onClick={onClick}
{...props} {...props}
> >

View file

@ -17,47 +17,47 @@ export default function RecipeDetailPage() {
// the recipe loaded from the backend, don't change this! it's required for scaling // the recipe loaded from the backend, don't change this! it's required for scaling
const [recipe, setRecipe] = useState<RecipeModel | null>(null) const [recipe, setRecipe] = useState<RecipeModel | null>(null)
// Working copy for re-calculating ingredients // Working copy for re-calculating ingredients
const [recipeWorkingCopy, setRecipeWorkingCopy] = useState<RecipeModel|null>(null) const [recipeWorkingCopy, setRecipeWorkingCopy] = useState<RecipeModel | null>(null)
// load recipe data whenever id changes // load recipe data whenever id changes
useEffect(() => { useEffect(() => {
const loadRecipe = async () => { const loadRecipe = async () => {
if (id) { if (id) {
try { try {
// Fetch recipe data when editing an existing one // Fetch recipe data when editing an existing one
console.log("loading recipe with id", id) console.log("loading recipe with id", id)
const data = await fetchRecipe(id) const data = await fetchRecipe(id)
if(data.id != id){ if (data.id != id) {
throw new Error("Id mismatch when loading recipes: " + id + " requested and " + data.id + " received!"); throw new Error("Id mismatch when loading recipes: " + id + " requested and " + data.id + " received!");
}
setRecipe(mapRecipeDtoToModel(data))
} catch (err) {
console.error(err)
} }
setRecipe(mapRecipeDtoToModel(data))
} catch (err) {
console.error(err)
} }
} }
}
loadRecipe()
}, [id])
// set original recipe data and working copy when recipe changes loadRecipe()
useEffect( ()=> { }, [id])
setRecipeWorkingCopy(recipe);
}, [recipe]) // set original recipe data and working copy when recipe changes
useEffect(() => {
setRecipeWorkingCopy(recipe);
}, [recipe])
if (!recipe || !recipeWorkingCopy) { if (!recipe || !recipeWorkingCopy) {
return <p className="p-6">Recipe not found.</p> return <p className="p-6">Recipe not found.</p>
} }
/** recalculate ingredients based on the amount of servings */
/** recalculate ingredients based on the amount of servings */
const recalculateIngredients = (newAmount: number) => { const recalculateIngredients = (newAmount: number) => {
// Always calculate factor from the *original recipe*, not the working copy // Always calculate factor from the *original recipe*, not the working copy
const factor = newAmount / recipe.servings.amount const factor = newAmount / recipe.servings.amount
// Create a new ingredient list with updated amounts // Create a new ingredient list with updated amounts
const updatedIngredientGroupList = recipe.ingredientGroupList.map((ingGrp) => ({ const updatedIngredientGroupList = recipe.ingredientGroupList.map((ingGrp) => ({
...ingGrp, ...ingGrp,
ingredientList: ingGrp.ingredientList.map((ing) => ({ ingredientList: ingGrp.ingredientList.map((ing) => ({
...ing, ...ing,
amount: ing.amount * factor, amount: ing.amount * factor,
@ -75,82 +75,90 @@ export default function RecipeDetailPage() {
}) })
} }
return ( return (
<div className="p-6 max-w-2xl mx-auto"> /*Container spanning entire screen used to center content horizontally */
<h1 className="content-title">{recipeWorkingCopy.title}</h1> <div className="app-bg">
{/* Container defining the maximum width of the content */}
{/* Recipe image */} <div className="content-container">
{recipe.imageUrl && ( {/* Header - remains in position when scrolling */}
<img <div className="sticky bg-gray-100 top-0 left-0 right-0 pb-4 border-b-2 border-gray-300">
src={recipe.imageUrl} <h1 className="content-title">{recipeWorkingCopy.title}</h1>
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> </div>
{/* Ingredients */}
<h2 className="section-heading">Zutaten</h2> <div className="p-6 max-w-2xl mx-auto">
<ul> {/* Recipe image */}
{recipeWorkingCopy.ingredientGroupList.map((group,i) => ( {recipe.imageUrl && (
<div key={i}> <img
{/* the title is optional, only print if present */} src={recipe.imageUrl}
{group.title && group.title.trim() !== "" && ( alt={recipe.title}
<h3 className="subsection-heading">{group.title}</h3> className="w-full rounded-xl mb-4 border"
)} />
<ul className="default-list"> )}
{group.ingredientList.map((ing, j) => (
<li key={j}> {/* Servings */}
{ing.amount} {ing.unit ?? ""} {ing.name} <div className="flex flex-row items-center gap-2 bg-blue-100 columns-2 rounded p-2 mb-4">
</li> <p className="mb-2">For {recipeWorkingCopy.servings.amount} {recipeWorkingCopy.servings.unit}</p>
))} <input
</ul> type="number"
className="input-field w-20 ml-auto"
value={recipeWorkingCopy.servings.amount}
onChange={
e => {
recalculateIngredients(Number(e.target.value))
}
}
/>
</div> </div>
))} {/* Ingredients */}
</ul> <h2 className="section-heading">Zutaten</h2>
<ul>
{/* Instructions - @todo add reasonable list delegate component*/} {recipeWorkingCopy.ingredientGroupList.map((group, i) => (
<ol className="space-y-4"> <div key={i}>
{recipe.instructionStepList.map((step, j) => ( {/* the title is optional, only print if present */}
<li key={j} className="flex items-start gap-4"> {group.title && group.title.trim() !== "" && (
{/* Step number circle */} <h3 className="subsection-heading">{group.title}</h3>
<div className="enumeration-indicator"> )}
{j + 1} <ul className="default-list">
{group.ingredientList.map((ing, j) => (
<li key={j}>
{ing.amount} {ing.unit ?? ""} {ing.name}
</li>
))}
</ul>
</div> </div>
))}
</ul>
{/* Step text */} {/* Instructions - @todo add reasonable list delegate component*/}
<p className="leading-relaxed">{step.text}</p> <ol className="space-y-4">
</li> {recipe.instructionStepList.map((step, j) => (
))} <li key={j} className="flex items-start gap-4">
</ol> {/* Step number circle */}
<div className="enumeration-indicator">
{j + 1}
</div>
{/* Action buttons */} {/* Step text */}
<div className="button-group"> <p className="leading-relaxed">{step.text}</p>
<ButtonLink </li>
to={recipe.id !== undefined ? getRecipeEditUrl(recipe.id) : getRecipeListUrl()} // @todo show error instead ))}
className="basic-button primary-button-bg primary-button-text" </ol>
text="Bearbeiten"
/> {/* Action buttons */}
<ButtonLink <div className="button-group">
to={getRecipeListUrl()} <ButtonLink
className="basic-button default-button-bg default-button-text" to={recipe.id !== undefined ? getRecipeEditUrl(recipe.id) : getRecipeListUrl()} // @todo show error instead
text="Zurueck" 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> </div>
</div>
) )
} }

View file

@ -41,7 +41,7 @@ export default function RecipeListPage() {
/*Container spanning entire screen used to center content horizontally */ /*Container spanning entire screen used to center content horizontally */
<div className="app-bg"> <div className="app-bg">
{/* Container defining the maximum width of the content */} {/* Container defining the maximum width of the content */}
<div className="bg-gray-100 w-full min-h-screen max-w-6xl shadow-xl p-8"> <div className="content-container">
{/* Header - remains in position when scrolling */} {/* 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"> <div className="sticky bg-gray-100 top-0 left-0 right-0 pb-4 border-b-2 border-gray-300">
<h1 className="content-title">Recipes</h1> <h1 className="content-title">Recipes</h1>