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";
|
import { AbstractDto } from "./AbstractDto.ts";
|
||||||
|
|
||||||
export class RecipeInstructionStepDto extends AbstractDto{
|
export class RecipeInstructionStepDto extends AbstractDto{
|
||||||
|
id?: string;
|
||||||
text!: string;
|
text!: string;
|
||||||
sortOrder!: number;
|
sortOrder!: number;
|
||||||
recipeId?: UUID;
|
recipeId?: UUID;
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ const RECIPE_URL = `${BASE_URL}/compact-recipe`
|
||||||
* @returns Array of recipe
|
* @returns Array of recipe
|
||||||
*/
|
*/
|
||||||
export async function fetchRecipeList(searchString : string): Promise<RecipeModel[]> {
|
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 there's a search string add it as query parameter
|
||||||
if(searchString && searchString !== ""){
|
if(searchString && searchString !== ""){
|
||||||
url +="?search=" + searchString;
|
url +="?search=" + searchString;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import type { IngredientModel } from "../../models/IngredientModel"
|
import type { IngredientModel } from "../../models/IngredientModel"
|
||||||
import Button, { ButtonType } from "../basics/Button"
|
import Button, { ButtonType } from "../basics/Button"
|
||||||
import SvgIcon, { Icon } from "../basics/SvgIcon"
|
import { Icon } from "../basics/SvgIcon"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Editor for handling the ingredient list
|
* 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>
|
</ul>
|
||||||
|
|
||||||
{/* Instructions */}
|
{/* Instructions - @todo add reasonable list delegate component*/}
|
||||||
<h2 className="section-heading">Zubereitung</h2>
|
<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 */}
|
{/* Action buttons */}
|
||||||
<div className="button-group">
|
<div className="button-group">
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ export default function RecipeEditPage() {
|
||||||
title: "",
|
title: "",
|
||||||
ingredientGroupList: [
|
ingredientGroupList: [
|
||||||
],
|
],
|
||||||
instructions: "",
|
instructionStepList: [],
|
||||||
servings: {
|
servings: {
|
||||||
amount: 1,
|
amount: 1,
|
||||||
unit: ""
|
unit: ""
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ import type { RecipeModel } from "../../models/RecipeModel"
|
||||||
import type { IngredientGroupModel } from "../../models/IngredientGroupModel"
|
import type { IngredientGroupModel } from "../../models/IngredientGroupModel"
|
||||||
import { IngredientGroupListEditor } from "./IngredientGroupListEditor"
|
import { IngredientGroupListEditor } from "./IngredientGroupListEditor"
|
||||||
import Button, { ButtonType } from "../basics/Button"
|
import Button, { ButtonType } from "../basics/Button"
|
||||||
|
import { InstructionStepListEditor } from "./InstructionStepListEditor"
|
||||||
|
import type { InstructionStepModel } from "../../models/InstructionStepModel"
|
||||||
|
|
||||||
type RecipeEditorProps = {
|
type RecipeEditorProps = {
|
||||||
recipe: RecipeModel
|
recipe: RecipeModel
|
||||||
|
|
@ -23,11 +25,20 @@ export default function RecipeEditor({ recipe, onSave, onCancel }: RecipeEditorP
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update ingredients
|
* Update ingredients
|
||||||
* @param ingredients new ingredients
|
* @param ingredientGroupList updated ingredient groups and ingredients
|
||||||
*/
|
*/
|
||||||
const updateIngredientGroupList = (ingredientGroupList: IngredientGroupModel[]) => {
|
const updateIngredientGroupList = (ingredientGroupList: IngredientGroupModel[]) => {
|
||||||
setDraft({ ...draft, ingredientGroupList })
|
setDraft({ ...draft, ingredientGroupList })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update instruction steps
|
||||||
|
* @param instructionStepList updated instructions
|
||||||
|
*/
|
||||||
|
const updateInstructionList = (instructionStepList: InstructionStepModel[]) => {
|
||||||
|
setDraft({ ...draft, instructionStepList })
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate recipe
|
* Validate recipe
|
||||||
* @returns Information on the errors the validation encountered
|
* @returns Information on the errors the validation encountered
|
||||||
|
|
@ -119,14 +130,12 @@ export default function RecipeEditor({ recipe, onSave, onCancel }: RecipeEditorP
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 className="subsection-heading">Instructions</h3>
|
{/* Instruction List*/ }
|
||||||
{/* Instructions */}
|
<InstructionStepListEditor
|
||||||
<textarea
|
instructionStepList={draft.instructionStepList}
|
||||||
className="input-field"
|
onChange={updateInstructionList}
|
||||||
placeholder="Instructions"
|
|
||||||
value={draft.instructions}
|
|
||||||
onChange={e => setDraft({ ...draft, instructions: e.target.value })}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
||||||
<div className="button-group">
|
<div className="button-group">
|
||||||
{/* Save Button */}
|
{/* Save Button */}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import type { RecipeModel } from "../models/RecipeModel"
|
import type { RecipeModel } from "../models/RecipeModel"
|
||||||
import type { RecipeDto } from "../api/dtos/RecipeDto"
|
import type { RecipeDto } from "../api/dtos/RecipeDto"
|
||||||
import type { RecipeIngredientGroupDto } from "../api/dtos/RecipeIngredientGroupDto"
|
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
|
* 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
|
// @todo implement steps in frontend
|
||||||
// join all instruction step texts into a single string for display
|
// 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
|
.sort((a, b) => a.sortOrder - b.sortOrder) // ensure correct order
|
||||||
.map(step => step.text)
|
.map(step => ({
|
||||||
.join("\n"),
|
text: step.text,
|
||||||
|
id: step.id
|
||||||
|
})
|
||||||
|
),
|
||||||
|
|
||||||
ingredientGroupList: dto.ingredientGroups
|
ingredientGroupList: dto.ingredientGroups
|
||||||
.sort((a, b) => a.sortOrder - b.sortOrder) // ensure groups are ordered
|
.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.
|
* for sending updates or creations to the backend.
|
||||||
*/
|
*/
|
||||||
export function mapRecipeModelToDto(model: RecipeModel): RecipeDto {
|
export function mapRecipeModelToDto(model: RecipeModel): RecipeDto {
|
||||||
// Split instructions string back into steps
|
// Map instructions
|
||||||
// @todo implement instructions properly...
|
const instructionDtos: RecipeInstructionStepDto[] = model.instructionStepList.map(
|
||||||
/* const instructionLines = model.instructions
|
(step, index) => ({
|
||||||
.split("\n")
|
id: step.id,
|
||||||
.map(line => line.trim())
|
text: step.text,
|
||||||
.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,
|
|
||||||
sortOrder: index + 1,
|
sortOrder: index + 1,
|
||||||
})
|
})
|
||||||
) */
|
)
|
||||||
const instructionDtos = [{
|
|
||||||
text: model.instructions,
|
|
||||||
sortOrder: 1
|
|
||||||
}]
|
|
||||||
|
|
||||||
|
// Map ingredients
|
||||||
const ingredientGroupDtos: RecipeIngredientGroupDto[] =
|
const ingredientGroupDtos: RecipeIngredientGroupDto[] =
|
||||||
model.ingredientGroupList.map((group, groupIndex) => ({
|
model.ingredientGroupList.map((group, groupIndex) => ({
|
||||||
id: group.id,
|
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 { IngredientGroupModel } from "./IngredientGroupModel"
|
||||||
|
import type { InstructionStepModel } from "./InstructionStepModel"
|
||||||
import type { ServingsModel } from "./ServingsModel"
|
import type { ServingsModel } from "./ServingsModel"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -22,7 +23,7 @@ export interface RecipeModel {
|
||||||
ingredientGroupList: IngredientGroupModel[]
|
ingredientGroupList: IngredientGroupModel[]
|
||||||
|
|
||||||
/** Preparation instructions */
|
/** Preparation instructions */
|
||||||
instructions: string
|
instructionStepList: InstructionStepModel[]
|
||||||
|
|
||||||
/** Number of servings, e.g., for 4 person, 12 cupcakes, 2 glasses */
|
/** Number of servings, e.g., for 4 person, 12 cupcakes, 2 glasses */
|
||||||
servings: ServingsModel
|
servings: ServingsModel
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue