diff --git a/frontend/src/components/recipes/IngredientListEditor.tsx b/frontend/src/components/recipes/IngredientListEditor.tsx index 3ddaf34..e289099 100644 --- a/frontend/src/components/recipes/IngredientListEditor.tsx +++ b/frontend/src/components/recipes/IngredientListEditor.tsx @@ -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)); + }} /> ({ ...ing, - amount: ing.amount * factor, + amount: ing.amount !== undefined ? ing.amount * factor : ing.amount, })) })) @@ -121,7 +121,7 @@ export default function RecipeDetailPage() { diff --git a/frontend/src/mappers/RecipeMapper.ts b/frontend/src/mappers/RecipeMapper.ts index fe72204..397a880 100644 --- a/frontend/src/mappers/RecipeMapper.ts +++ b/frontend/src/mappers/RecipeMapper.ts @@ -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, + } } diff --git a/frontend/src/models/IngredientModel.ts b/frontend/src/models/IngredientModel.ts index 4eea16e..ead9477 100644 --- a/frontend/src/models/IngredientModel.ts +++ b/frontend/src/models/IngredientModel.ts @@ -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 }