147 lines
4.5 KiB
TypeScript
147 lines
4.5 KiB
TypeScript
import { useState } from "react"
|
|
import type { Recipe } from "../../types/recipe"
|
|
import type { IngredientGroup } from "../../types/ingredientGroup"
|
|
import { IngredientGroupListEditor } from "./IngredientGroupListEditor"
|
|
|
|
type RecipeEditorProps = {
|
|
recipe: Recipe
|
|
onSave: (recipe: Recipe) => 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<Recipe>(recipe)
|
|
/** Error list */
|
|
const [errors, setErrors] = useState<{ title?: boolean; ingredients?: boolean }>({})
|
|
|
|
/**
|
|
* Update ingredients
|
|
* @param ingredients new ingredients
|
|
*/
|
|
const updateIngredientGroupList = (ingredientGroupList: IngredientGroup[]) => {
|
|
setDraft({ ...draft, ingredientGroupList })
|
|
}
|
|
/**
|
|
* 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 {
|
|
let 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: Recipe) => {
|
|
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 (
|
|
<div className="p-4 gap-10">
|
|
<h2 className="content-title">
|
|
{recipe.id ? "Edit Recipe" : "New Recipe"}
|
|
</h2>
|
|
|
|
{/* Title */}
|
|
<h3 className="subsection-heading">Title</h3>
|
|
<input
|
|
className={`input-field ${errors.title ? "border-red-500" : ""}`}
|
|
placeholder="Title"
|
|
value={draft.title}
|
|
onChange={e => setDraft({ ...draft, title: e.target.value })}
|
|
/>
|
|
{/* Servings */}
|
|
<h3 className="subsection-heading">Servings</h3>
|
|
<div className="columns-3 gap-2 flex items-center">
|
|
<label>For</label>
|
|
<input
|
|
type="number"
|
|
className="input-field 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
|
|
className="input-field"
|
|
placeholder="Persons"
|
|
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 border-red-500 rounded p-2" : ""}>
|
|
<IngredientGroupListEditor
|
|
ingredientGroupList={draft.ingredientGroupList}
|
|
onChange={updateIngredientGroupList}
|
|
/>
|
|
</div>
|
|
|
|
<h3 className="subsection-heading">Instructions</h3>
|
|
{/* Instructions */}
|
|
<textarea
|
|
className="input-field"
|
|
placeholder="Instructions"
|
|
value={draft.instructions}
|
|
onChange={e => setDraft({ ...draft, instructions: e.target.value })}
|
|
/>
|
|
|
|
<div className="button-group">
|
|
{/* Save Button */}
|
|
<button
|
|
className="primary-button"
|
|
onClick={() => handleSave(draft)}
|
|
>
|
|
Save
|
|
</button>
|
|
<button
|
|
className="default-button"
|
|
onClick={() => onCancel()}
|
|
>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|