recipe-app/frontend/src/components/recipes/RecipeEditor.tsx
2025-10-25 18:17:32 +02:00

161 lines
6.3 KiB
TypeScript

import {useState} from "react"
import type {RecipeModel} from "../../models/RecipeModel"
import type {IngredientGroupModel} from "../../models/IngredientGroupModel"
import {IngredientGroupListEditor} from "./IngredientGroupListEditor"
import Button from "../basics/Button"
import {InstructionStepListEditor} from "./InstructionStepListEditor"
import type {InstructionStepModel} from "../../models/InstructionStepModel"
import {ButtonType} from "../basics/BasicButtonDefinitions"
import ButtonGroupLayout from "../basics/ButtonGroupLayout.tsx";
type RecipeEditorProps = {
recipe: RecipeModel
onSave: (recipe: RecipeModel) => void
onCancel: () => void
}
/**
* Editor component for managing a recipe, including title,
* ingredients (with amount, unit, name), instructions, and image URL.
* @todo adapt to ingredientGroups!
*/
export default function RecipeEditor({recipe, onSave, onCancel}: RecipeEditorProps) {
/** draft of the new recipe */
const [draft, setDraft] = useState<RecipeModel>(recipe)
/** Error list */
const [errors, setErrors] = useState<{ title?: boolean; ingredients?: boolean }>({})
/**
* Update ingredients
* @param ingredientGroupList updated ingredient groups and ingredients
*/
const updateIngredientGroupList = (ingredientGroupList: IngredientGroupModel[]) => {
setDraft({...draft, ingredientGroupList})
}
/**
* Update instruction steps
* @param instructionStepList updated instructions
*/
const updateInstructionList = (instructionStepList: InstructionStepModel[]) => {
setDraft({...draft, instructionStepList})
}
/**
* Validate recipe
* @returns Information on the errors the validation encountered
*/
const validate = () => {
const newErrors: { title?: boolean; ingredients?: boolean } = {}
// each recipe requires a title
if (!draft.title.trim()) {
newErrors.title = true
}
/* there must be at least one ingredient group
* no group may contain an empty ingredient list
* @todo check whether all ingredients are valid
* @todo enhance visualization of ingredient errors
*/
if (!draft.ingredientGroupList || draft.ingredientGroupList.length === 0) {
newErrors.ingredients = true
} else {
const isAnyIngredientListEmpty = draft.ingredientGroupList.some(
ingGrp => {
return !ingGrp.ingredientList || ingGrp.ingredientList.length === 0
}
)
if (isAnyIngredientListEmpty) {
newErrors.ingredients = true
}
}
setErrors(newErrors)
return Object.keys(newErrors).length === 0
}
/** Handles saving and ensures that the draft is only saved if valid */
const handleSave = (draft: RecipeModel) => {
if (validate()) {
onSave(draft)
}
}
// ensure that there is a recipe and show error otherwise
if (!recipe) return <div>Oops, there's no recipe in RecipeEditor...</div>
// @todo add handling of images
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-bg">
<h1 className="border-b-2 border-gray-300 pb-4">
{recipe.id ? "Rezept bearbeiten" : "Neues Rezept"}
</h1>
<div className="content-container">
{/* Title */}
<h2>Titel</h2>
<input
className={`${errors.title ? "error-text" : ""}`}
placeholder="Titel"
value={draft.title}
onChange={e => setDraft({...draft, title: e.target.value})}
/>
{/* Servings */}
<h2>Portionen</h2>
<div className="columns-3 gap-2 flex items-center">
<label>Für</label>
<input
type="number"
className="w-20"
placeholder="1"
value={draft.servings.amount}
onChange={e => {
const tempServings = draft.servings
tempServings.amount = Number(e.target.value)
setDraft({...draft, servings: tempServings})
}}
/>
<input
placeholder="Personen"
value={draft.servings.unit}
onChange={e => {
const tempServings = draft.servings
tempServings.unit = e.target.value
setDraft({...draft, servings: tempServings})
}}
/>
</div>
{/* Ingredient List - @todo better visualization of errors! */}
<div className={errors.ingredients ? "border error-text rounded p-2" : ""}>
<IngredientGroupListEditor
ingredientGroupList={draft.ingredientGroupList}
onChange={updateIngredientGroupList}
/>
</div>
{/* Instruction List*/}
<InstructionStepListEditor
instructionStepList={draft.instructionStepList}
onChange={updateInstructionList}
/>
<ButtonGroupLayout>
{/* Save Button */}
<Button
onClick={() => handleSave(draft)}
text={"Speichern"}
buttonType={ButtonType.PrimaryButton}
/>
<Button
onClick={() => onCancel()}
text={"Abbrechen"}
/>
</ButtonGroupLayout>
</div>
</div>
</div>
)
}