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() {
{group.ingredientList.map((ing, j) => (
-
- {ing.amount} {ing.unit ?? ""} {ing.name}
+ {ing.amount ?? ""} {ing.unit ?? ""} {ing.name}
))}
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
}