basic instructions editor
This commit is contained in:
parent
47ffe8c74d
commit
e94be51f3e
10 changed files with 108 additions and 33 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export default function RecipeEditPage() {
|
|||
title: "",
|
||||
ingredientGroupList: [
|
||||
],
|
||||
instructions: "",
|
||||
instructionStepList: [],
|
||||
servings: {
|
||||
amount: 1,
|
||||
unit: ""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
7
frontend/src/models/InstructionStepModel.ts
Normal file
7
frontend/src/models/InstructionStepModel.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
/**
|
||||
* Defines the instruction step of a recipe
|
||||
*/
|
||||
export interface InstructionStepModel{
|
||||
id?: string;
|
||||
text: string;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue