Make amount for an ingredient optional.

This commit is contained in:
araemer 2025-10-21 15:32:48 +02:00
parent 3c9c94957f
commit 0dc2eb2e3c
4 changed files with 122 additions and 96 deletions

View file

@ -3,25 +3,48 @@ import type {IngredientModel} from "../../models/IngredientModel"
import Button from "../basics/Button"
import {ButtonType} from "../basics/BasicButtonDefinitions"
/**
* Editor for handling the ingredient list
* Ingredients can be edited, added and removed
*/
type IngredientListEditorProps = {
ingredients: IngredientModel[]
onChange: (ingredients: IngredientModel[]) => void
}
/**
* Editor for handling the ingredient list
*
* Ingredients can be edited, added and removed
* @param ingredient List of IngredientModels describing the ingredient
* @param onChange Method to call on changing an ingredient.
*/
export function IngredientListEditor({ingredients, onChange}: IngredientListEditorProps) {
const handleUpdate = (index: number, field: keyof IngredientModel, value: string | number) => {
const updated = ingredients.map((ing, i) =>
i === index ? {...ing, [field]: field === "amount" ? Number(value) : value} : ing
)
onChange(updated)
}
const handleUpdate = (
index: number,
field: keyof IngredientModel,
value: string | number | undefined
) => {
const updated = ingredients.map((ing, i) => {
if (i !== index) return ing;
if (field === "amount") {
// Handle optional numeric value
if (value === undefined || value === "") {
const {amount, ...rest} = ing; // remove the field
return rest as IngredientModel;
}
// valid number
return {...ing, amount: Number(value)};
}
// handle other fields normally
return {...ing, [field]: value};
});
onChange(updated);
};
const handleAdd = () => {
onChange([...ingredients, {name: "", amount: 0, unit: ""}])
onChange([...ingredients, {name: "", unit: ""}])
}
const handleRemove = (index: number) => {
@ -36,8 +59,11 @@ export function IngredientListEditor({ingredients, onChange}: IngredientListEdit
type="number"
className="input-field"
placeholder="Menge"
value={ing.amount}
onChange={e => handleUpdate(index, "amount", e.target.value)}
value={ing.amount === undefined || ing.amount === null ? "" : ing.amount}
onChange={e => {
const value = e.target.value;
handleUpdate(index, "amount", value === "" ? undefined : Number(value));
}}
/>
<input
className="input-field w-20"

View file

@ -58,7 +58,7 @@ export default function RecipeDetailPage() {
...ingGrp,
ingredientList: ingGrp.ingredientList.map((ing) => ({
...ing,
amount: ing.amount * factor,
amount: ing.amount !== undefined ? ing.amount * factor : ing.amount,
}))
}))
@ -121,7 +121,7 @@ export default function RecipeDetailPage() {
<ul className="default-list">
{group.ingredientList.map((ing, j) => (
<li key={j}>
{ing.amount} {ing.unit ?? ""} {ing.name}
{ing.amount ?? ""} {ing.unit ?? ""} {ing.name}
</li>
))}
</ul>

View file

@ -1,52 +1,52 @@
import type { RecipeModel } from "../models/RecipeModel"
import type { RecipeDto } from "../api/dtos/RecipeDto"
import type { RecipeIngredientGroupDto } from "../api/dtos/RecipeIngredientGroupDto"
import type { RecipeInstructionStepDto } from "../api/dtos/RecipeInstructionStepDto"
import type {RecipeModel} from "../models/RecipeModel"
import type {RecipeDto} from "../api/dtos/RecipeDto"
import type {RecipeIngredientGroupDto} from "../api/dtos/RecipeIngredientGroupDto"
import type {RecipeInstructionStepDto} from "../api/dtos/RecipeInstructionStepDto"
/**
* Maps a RecipeDto (as returned by the backend) to the Recipe model
* used in the frontend application.
*/
export function mapRecipeDtoToModel(dto: RecipeDto): RecipeModel {
return {
id: dto.id,
title: dto.title,
return {
id: dto.id,
title: dto.title,
servings: {
amount: dto.amount ?? 1,
unit: dto.amountDescription ?? "",
},
// join all instruction step texts into a single string for display
instructionStepList: dto.instructions
.sort((a, b) => a.sortOrder - b.sortOrder) // ensure correct order
.map(step => ({
text: step.text,
id: step.id,
/* When mapping a stepDTO, it should already contain a UUID. If
* If, however, for some reason, it does not, add a UUID as sorting
* steps in teh GUI requires a unique identifier for each item.
*/
internalId: step.id !== undefined? step.id : crypto.randomUUID()
})
),
servings: {
amount: dto.amount ?? 1,
unit: dto.amountDescription ?? "",
},
// join all instruction step texts into a single string for display
instructionStepList: dto.instructions
.sort((a, b) => a.sortOrder - b.sortOrder) // ensure correct order
.map(step => ({
text: step.text,
id: step.id,
/* When mapping a stepDTO, it should already contain a UUID. If
* If, however, for some reason, it does not, add a UUID as sorting
* steps in teh GUI requires a unique identifier for each item.
*/
internalId: step.id !== undefined ? step.id : crypto.randomUUID()
})
),
ingredientGroupList: dto.ingredientGroups
.sort((a, b) => a.sortOrder - b.sortOrder) // ensure groups are ordered
.map(group => ({
id: group.id,
title: group.title,
ingredientList: group.ingredients
.sort((a, b) => a.sortOrder - b.sortOrder) // ensure ingredients are ordered
.map(ing => ({
id: ing.id,
name: ing.name ?? "", // @todo ensure that name and amount are indeed present
amount: ing.amount ?? 0,
unit: ing.unit,
//subtext: ing.subtext ?? undefined,
})),
})),
imageUrl: undefined, // not part of DTO yet, placeholder
}
ingredientGroupList: dto.ingredientGroups
.sort((a, b) => a.sortOrder - b.sortOrder) // ensure groups are ordered
.map(group => ({
id: group.id,
title: group.title,
ingredientList: group.ingredients
.sort((a, b) => a.sortOrder - b.sortOrder) // ensure ingredients are ordered
.map(ing => ({
id: ing.id,
name: ing.name ?? "", // @todo ensure that name and amount are indeed present
amount: ing.amount,
unit: ing.unit,
//subtext: ing.subtext ?? undefined,
})),
})),
imageUrl: undefined, // not part of DTO yet, placeholder
}
}
/**
@ -54,37 +54,37 @@ export function mapRecipeDtoToModel(dto: RecipeDto): RecipeModel {
* for sending updates or creations to the backend.
*/
export function mapRecipeModelToDto(model: RecipeModel): RecipeDto {
// Map instructions
const instructionDtos: RecipeInstructionStepDto[] = model.instructionStepList.map(
(step, index) => ({
id: step.id,
text: step.text,
sortOrder: index + 1,
})
)
// Map instructions
const instructionDtos: RecipeInstructionStepDto[] = model.instructionStepList.map(
(step, index) => ({
id: step.id,
text: step.text,
sortOrder: index + 1,
})
)
// Map ingredients
const ingredientGroupDtos: RecipeIngredientGroupDto[] =
model.ingredientGroupList.map((group, groupIndex) => ({
id: group.id,
title: group.title,
sortOrder: groupIndex + 1, // sortOrder from list index
ingredients: group.ingredientList.map((ing, ingIndex) => ({
id: ing.id,
name: ing.name,
amount: ing.amount,
unit: ing.unit,
sortOrder: ingIndex + 1, // sortOrder from index
//subtext: ing.subtext ?? null,
})),
}))
// Map ingredients
const ingredientGroupDtos: RecipeIngredientGroupDto[] =
model.ingredientGroupList.map((group, groupIndex) => ({
id: group.id,
title: group.title,
sortOrder: groupIndex + 1, // sortOrder from list index
ingredients: group.ingredientList.map((ing, ingIndex) => ({
id: ing.id,
name: ing.name,
amount: ing.amount,
unit: ing.unit,
sortOrder: ingIndex + 1, // sortOrder from index
//subtext: ing.subtext ?? null,
})),
}))
return {
id: model.id,
title: model.title,
amount: model.servings.amount,
amountDescription: model.servings.unit,
instructions: instructionDtos,
ingredientGroups: ingredientGroupDtos,
}
return {
id: model.id,
title: model.title,
amount: model.servings.amount,
amountDescription: model.servings.unit,
instructions: instructionDtos,
ingredientGroups: ingredientGroupDtos,
}
}

View file

@ -2,15 +2,15 @@
* Represents a single ingredient in a recipe.
*/
export interface IngredientModel {
id?: string
/** Name of the ingredient (e.g. "Spaghetti") */
name: string
id?: string
/** Name of the ingredient (e.g. "Spaghetti") */
name: string
/** Quantity required (e.g. 200, 1.5) */
amount: number
/** Quantity required (e.g. 200, 1.5) */
amount?: number
/** Unit of measurement (e.g. "g", "tbsp", "cups").
* Optional for cases like "1 egg".
*/
unit?: string
/** Unit of measurement (e.g. "g", "tbsp", "cups").
* Optional for cases like "1 egg".
*/
unit?: string
}