basic instructions editor

This commit is contained in:
Anika Raemer 2025-10-11 07:32:50 +02:00
parent 47ffe8c74d
commit e94be51f3e
10 changed files with 108 additions and 33 deletions

View file

@ -2,6 +2,7 @@ import { UUID } from "crypto";
import { AbstractDto } from "./AbstractDto.ts";
export class RecipeInstructionStepDto extends AbstractDto{
id?: string;
text!: string;
sortOrder!: number;
recipeId?: UUID;

View file

@ -19,7 +19,7 @@ const RECIPE_URL = `${BASE_URL}/compact-recipe`
* @returns Array of recipe
*/
export async function fetchRecipeList(searchString : string): Promise<RecipeModel[]> {
let url : string = RECIPE_URL;
let url : string = RECIPE_URL; // add an s to the base URL as we want to load a list
// if there's a search string add it as query parameter
if(searchString && searchString !== ""){
url +="?search=" + searchString;

View file

@ -1,6 +1,6 @@
import type { IngredientModel } from "../../models/IngredientModel"
import Button, { ButtonType } from "../basics/Button"
import SvgIcon, { Icon } from "../basics/SvgIcon"
import { Icon } from "../basics/SvgIcon"
/**
* Editor for handling the ingredient list

View file

@ -0,0 +1,56 @@
import type { InstructionStepModel } from "../../models/InstructionStepModel"
import Button, { ButtonType } from "../basics/Button"
import { Icon } from "../basics/SvgIcon"
/**
* Editor for handling the instruction step list
* Ingredients can be edited, added and removed
*/
type InstructionStepListEditorProps = {
instructionStepList: InstructionStepModel[]
onChange: (ingredients: InstructionStepModel[]) => void
}
export function InstructionStepListEditor({ instructionStepList, onChange }: InstructionStepListEditorProps) {
const handleUpdate = (index: number, field: keyof InstructionStepModel, value: string) => {
const updated = instructionStepList.map((ing, i) =>
i === index ? { ...ing, [field]: value } : ing
)
onChange(updated)
}
const handleAdd = () => {
onChange([...instructionStepList, { text: "" }])
}
const handleRemove = (index: number) => {
onChange(instructionStepList.filter((_, i) => i !== index))
}
return (
<div>
<h3 className="subsection-heading">Zubereitung</h3>
{instructionStepList.map((step, index) => (
<div key={index} className="horizontal-input-group">
<input
className="input-field"
placeholder="Description"
value={step.text}
onChange={e => handleUpdate(index, "text", e.target.value)}
/>
<Button
onClick={() => handleRemove(index)}
icon={Icon.X}
buttonType={ButtonType.DarkButton}
/>
</div>
))}
<Button
onClick={handleAdd}
icon={Icon.Plus}
text={"Schritt hinzufügen"}
buttonType={ButtonType.PrimaryButton}
/>
</div>
)
}

View file

@ -122,9 +122,15 @@ export default function RecipeDetailPage() {
))}
</ul>
{/* Instructions */}
{/* Instructions - @todo add reasonable list delegate component*/}
<h2 className="section-heading">Zubereitung</h2>
<p className="mb-6">{recipe.instructions}</p>
<ol className="default-list mb-6">
{recipe.instructionStepList.map((step,j) =>(
<li key={j}>
{step.text}
</li>
))}
</ol>
{/* Action buttons */}
<div className="button-group">

View file

@ -30,7 +30,7 @@ export default function RecipeEditPage() {
title: "",
ingredientGroupList: [
],
instructions: "",
instructionStepList: [],
servings: {
amount: 1,
unit: ""

View file

@ -3,6 +3,8 @@ import type { RecipeModel } from "../../models/RecipeModel"
import type { IngredientGroupModel } from "../../models/IngredientGroupModel"
import { IngredientGroupListEditor } from "./IngredientGroupListEditor"
import Button, { ButtonType } from "../basics/Button"
import { InstructionStepListEditor } from "./InstructionStepListEditor"
import type { InstructionStepModel } from "../../models/InstructionStepModel"
type RecipeEditorProps = {
recipe: RecipeModel
@ -23,11 +25,20 @@ export default function RecipeEditor({ recipe, onSave, onCancel }: RecipeEditorP
/**
* Update ingredients
* @param ingredients new ingredients
* @param ingredientGroupList updated ingredient groups and ingredients
*/
const updateIngredientGroupList = (ingredientGroupList: IngredientGroupModel[]) => {
setDraft({ ...draft, ingredientGroupList })
}
/**
* Update instruction steps
* @param instructionStepList updated instructions
*/
const updateInstructionList = (instructionStepList: InstructionStepModel[]) => {
setDraft({ ...draft, instructionStepList })
}
/**
* Validate recipe
* @returns Information on the errors the validation encountered
@ -119,15 +130,13 @@ export default function RecipeEditor({ recipe, onSave, onCancel }: RecipeEditorP
/>
</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 })}
{/* Instruction List*/ }
<InstructionStepListEditor
instructionStepList={draft.instructionStepList}
onChange={updateInstructionList}
/>
<div className="button-group">
{/* Save Button */}
<Button

View file

@ -1,6 +1,7 @@
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
@ -17,10 +18,13 @@ export function mapRecipeDtoToModel(dto: RecipeDto): RecipeModel {
},
// @todo implement steps in frontend
// join all instruction step texts into a single string for display
instructions: dto.instructions
instructionStepList: dto.instructions
.sort((a, b) => a.sortOrder - b.sortOrder) // ensure correct order
.map(step => step.text)
.join("\n"),
.map(step => ({
text: step.text,
id: step.id
})
),
ingredientGroupList: dto.ingredientGroups
.sort((a, b) => a.sortOrder - b.sortOrder) // ensure groups are ordered
@ -46,25 +50,16 @@ export function mapRecipeDtoToModel(dto: RecipeDto): RecipeModel {
* for sending updates or creations to the backend.
*/
export function mapRecipeModelToDto(model: RecipeModel): RecipeDto {
// Split instructions string back into steps
// @todo implement instructions properly...
/* const instructionLines = model.instructions
.split("\n")
.map(line => line.trim())
.filter(line => line.length > 0)
const instructionDtos: RecipeInstructionStepDto[] = instructionLines.map(
(text, index) => ({
id: crypto.randomUUID(), // or keep existing IDs if you track them in model
text,
// Map instructions
const instructionDtos: RecipeInstructionStepDto[] = model.instructionStepList.map(
(step, index) => ({
id: step.id,
text: step.text,
sortOrder: index + 1,
})
) */
const instructionDtos = [{
text: model.instructions,
sortOrder: 1
}]
)
// Map ingredients
const ingredientGroupDtos: RecipeIngredientGroupDto[] =
model.ingredientGroupList.map((group, groupIndex) => ({
id: group.id,

View file

@ -0,0 +1,7 @@
/**
* Defines the instruction step of a recipe
*/
export interface InstructionStepModel{
id?: string;
text: string;
}

View file

@ -1,4 +1,5 @@
import type { IngredientGroupModel } from "./IngredientGroupModel"
import type { InstructionStepModel } from "./InstructionStepModel"
import type { ServingsModel } from "./ServingsModel"
/**
@ -22,7 +23,7 @@ export interface RecipeModel {
ingredientGroupList: IngredientGroupModel[]
/** Preparation instructions */
instructions: string
instructionStepList: InstructionStepModel[]
/** Number of servings, e.g., for 4 person, 12 cupcakes, 2 glasses */
servings: ServingsModel