Make amount for an ingredient optional.
This commit is contained in:
parent
3c9c94957f
commit
0dc2eb2e3c
4 changed files with 122 additions and 96 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue