add dragging to step list
This commit is contained in:
parent
646bd573cf
commit
575eecfc69
13 changed files with 406 additions and 60 deletions
62
frontend/package-lock.json
generated
62
frontend/package-lock.json
generated
|
|
@ -8,6 +8,9 @@
|
|||
"name": "frontend",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-router-dom": "^7.8.2"
|
||||
|
|
@ -327,6 +330,59 @@
|
|||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/accessibility": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
|
||||
"integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/core": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
|
||||
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@dnd-kit/accessibility": "^3.1.1",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/sortable": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz",
|
||||
"integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@dnd-kit/core": "^6.3.0",
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/utilities": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
|
||||
"integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz",
|
||||
|
|
@ -3990,6 +4046,12 @@
|
|||
"typescript": ">=4.8.4"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@
|
|||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-router-dom": "^7.8.2"
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@
|
|||
@apply text-gray-600
|
||||
}
|
||||
.primary-button-bg {
|
||||
@apply bg-blue-300 hover:bg-blue-400 text-gray-600
|
||||
@apply bg-blue-300 hover:bg-blue-400
|
||||
}
|
||||
.primary-button-text {
|
||||
@apply text-gray-600
|
||||
|
|
@ -55,6 +55,13 @@
|
|||
@apply text-white
|
||||
}
|
||||
|
||||
.transparent-button-bg {
|
||||
@apply bg-transparent hover:bg-transparent
|
||||
}
|
||||
.transparent-button-text {
|
||||
@apply text-gray-600
|
||||
}
|
||||
|
||||
/* input fields like input and textarea */
|
||||
.input-field {
|
||||
@apply p-2 w-full border rounded-md placeholder-gray-400 border-gray-600 hover:border-blue-600 transition-colors text-gray-600 focus:outline-none focus:border-blue-800;
|
||||
|
|
@ -79,7 +86,7 @@
|
|||
}
|
||||
|
||||
.enumeration-indicator{
|
||||
@apply flex-shrink-0 w-7 h-7 rounded-full bg-blue-300 text-white flex items-center justify-center
|
||||
@apply flex-shrink-0 w-7 h-7 rounded-full bg-blue-300 text-white flex items-center justify-center shadow-sm
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -20,6 +20,10 @@ export const ButtonType = {
|
|||
DefaultButton: {
|
||||
textColor: "default-button-text",
|
||||
backgroundColor: "default-button-bg"
|
||||
},
|
||||
TransparentButton: {
|
||||
textColor: "transparent-button-text",
|
||||
backgroundColor: "transparent-button-bg"
|
||||
}
|
||||
} as const;
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@
|
|||
export const Icon = {
|
||||
LookingGlass: "M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z",
|
||||
X: "M6 18L18 6M6 6l12 12",
|
||||
Plus: "M3 12L21 12M12 3L12 21"
|
||||
Plus: "M3 12L21 12M12 3L12 21",
|
||||
ArrowUp: "M3 18L12 6L21 18",
|
||||
ArrowDown: "M3 6L12 18L21 6",
|
||||
} as const;
|
||||
|
||||
export type Icon = typeof Icon[keyof typeof Icon];
|
||||
|
|
|
|||
|
|
@ -0,0 +1,65 @@
|
|||
/**
|
||||
* Single step component for Desktop editor with drag-and-drop support.
|
||||
*/
|
||||
|
||||
import { useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import type { InstructionStepModel } from "../../models/InstructionStepModel";
|
||||
import Button, { ButtonType } from "../basics/Button";
|
||||
import { Icon } from "../basics/SvgIcon";
|
||||
|
||||
type InstructionStepDesktopListItemProps = {
|
||||
id: string | number;
|
||||
index: number;
|
||||
step: InstructionStepModel;
|
||||
onUpdate: (index: number, value: string) => void;
|
||||
onRemove: (index: number) => void;
|
||||
};
|
||||
|
||||
export function InstructionStepDesktopListItem({
|
||||
id,
|
||||
index,
|
||||
step,
|
||||
onUpdate,
|
||||
onRemove,
|
||||
}: InstructionStepDesktopListItemProps) {
|
||||
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id });
|
||||
|
||||
const style = { transform: CSS.Transform.toString(transform), transition };
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
className="flex items-start gap-3"
|
||||
>
|
||||
<div
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
className="enumeration-indicator cursor-grab"
|
||||
title="Ziehen, um neu zu ordnen"
|
||||
>
|
||||
{index + 1}
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
className="input-field w-full min-h-[60px] resize-none overflow-hidden"
|
||||
placeholder={`Schritt ${index + 1}`}
|
||||
value={step.text}
|
||||
onChange={(e) => onUpdate(index, e.target.value)}
|
||||
onInput={(e) => {
|
||||
const el = e.target as HTMLTextAreaElement;
|
||||
el.style.height = "auto";
|
||||
el.style.height = `${el.scrollHeight}px`;
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
onClick={() => onRemove(index)}
|
||||
icon={Icon.X}
|
||||
buttonType={ButtonType.DarkButton}
|
||||
title="Schritt löschen"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
/**
|
||||
* Desktop editor using drag-and-drop via @dnd-kit
|
||||
*/
|
||||
|
||||
import {
|
||||
DndContext,
|
||||
closestCenter,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from "@dnd-kit/core";
|
||||
import {
|
||||
arrayMove,
|
||||
SortableContext,
|
||||
verticalListSortingStrategy,
|
||||
} from "@dnd-kit/sortable";
|
||||
import type { InstructionStepModel } from "../../models/InstructionStepModel";
|
||||
import { InstructionStepDesktopListItem } from "./InstructionStepDesktopListItem";
|
||||
import { useInstructionStepListEditor } from "./InstructionStepListEditor";
|
||||
import Button, { ButtonType } from "../basics/Button";
|
||||
import { Icon } from "../basics/SvgIcon";
|
||||
|
||||
type InstructionStepListDesktopEditorProps = {
|
||||
instructionStepList: InstructionStepModel[];
|
||||
onChange: (steps: InstructionStepModel[]) => void;
|
||||
};
|
||||
|
||||
export function InstructionStepListDesktopEditor({
|
||||
instructionStepList,
|
||||
onChange,
|
||||
}: InstructionStepListDesktopEditorProps) {
|
||||
const { handleUpdate, handleAdd, handleRemove } = useInstructionStepListEditor(
|
||||
instructionStepList,
|
||||
onChange
|
||||
);
|
||||
|
||||
const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 8 } }));
|
||||
|
||||
const handleDragEnd = (event: any) => {
|
||||
const { active, over } = event;
|
||||
if (active.id !== over.id) {
|
||||
/* find element by internal id
|
||||
* Caution! Due to new elements not having a real ID yet, each list item has an internal ID
|
||||
* in order to give it a unique identifier before saving it to the backend. We have to compare
|
||||
* the id of the drag item to the internalId of the list item here!
|
||||
*/
|
||||
const oldIndex = instructionStepList.findIndex((i) => i.internalId === active.id);
|
||||
const newIndex = instructionStepList.findIndex((i) => i.internalId === over.id);
|
||||
// move element to new position
|
||||
const newOrder = arrayMove(instructionStepList, oldIndex, newIndex);
|
||||
onChange(newOrder);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3 className="subsection-heading mb-3">Zubereitung</h3>
|
||||
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
||||
<SortableContext
|
||||
items={instructionStepList.map((i) => i.internalId)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
<div className="flex flex-col gap-3">
|
||||
{instructionStepList.map((step, index) => (
|
||||
<InstructionStepDesktopListItem
|
||||
key={step.internalId}
|
||||
id={step.internalId}
|
||||
index={index}
|
||||
step={step}
|
||||
onUpdate={handleUpdate}
|
||||
onRemove={handleRemove}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
<Button
|
||||
onClick={handleAdd}
|
||||
icon={Icon.Plus}
|
||||
text="Schritt hinzufügen"
|
||||
buttonType={ButtonType.PrimaryButton}
|
||||
className="mt-4"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,56 +1,86 @@
|
|||
import type { InstructionStepModel } from "../../models/InstructionStepModel"
|
||||
import Button, { ButtonType } from "../basics/Button"
|
||||
import { Icon } from "../basics/SvgIcon"
|
||||
/**
|
||||
* InstructionStepListEditor.tsx
|
||||
*
|
||||
* Hybrid editor for recipe instruction steps:
|
||||
* - Detects device type and renders Desktop or Mobile editor accordingly.
|
||||
* - Common functionality for adding, removing, and updating steps is centralized.
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import type { InstructionStepModel } from "../../models/InstructionStepModel";
|
||||
import { InstructionStepListDesktopEditor } from "./InstructionStepListDesktopEditor";
|
||||
import { InstructionStepListMobileEditor } from "./InstructionStepListMobileEditor";
|
||||
|
||||
/**
|
||||
* Editor for handling the instruction step list
|
||||
* Ingredients can be edited, added and removed
|
||||
* Props for the hybrid InstructionStepListEditor
|
||||
*/
|
||||
type InstructionStepListEditorProps = {
|
||||
instructionStepList: InstructionStepModel[]
|
||||
onChange: (ingredients: InstructionStepModel[]) => void
|
||||
instructionStepList: InstructionStepModel[];
|
||||
onChange: (steps: InstructionStepModel[]) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Hybrid wrapper that decides between desktop (drag-and-drop) or mobile (buttons)
|
||||
*/
|
||||
export function InstructionStepListEditor({
|
||||
instructionStepList,
|
||||
onChange,
|
||||
}: InstructionStepListEditorProps) {
|
||||
const [isTouch, setIsTouch] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsTouch("ontouchstart" in window || navigator.maxTouchPoints > 0);
|
||||
}, []);
|
||||
|
||||
return isTouch ? (
|
||||
<InstructionStepListMobileEditor
|
||||
instructionStepList={instructionStepList}
|
||||
onChange={onChange}
|
||||
/>
|
||||
) : (
|
||||
<InstructionStepListDesktopEditor
|
||||
instructionStepList={instructionStepList}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
/**
|
||||
* ======= Shared hook for common logic =======
|
||||
*
|
||||
* Provides add/remove/update functionality used by both mobile and desktop editors.
|
||||
*/
|
||||
export function useInstructionStepListEditor(
|
||||
instructionStepList: InstructionStepModel[],
|
||||
onChange: (steps: InstructionStepModel[]) => void
|
||||
) {
|
||||
/**
|
||||
* Update the text of a specific step
|
||||
*/
|
||||
const handleUpdate = (index: number, value: string) => {
|
||||
const updated = instructionStepList.map((step, i) =>
|
||||
i === index ? { ...step, text: value } : step
|
||||
);
|
||||
onChange(updated);
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a new step at the end
|
||||
*/
|
||||
const handleAdd = () => {
|
||||
onChange([...instructionStepList, { text: "" }])
|
||||
}
|
||||
onChange([...instructionStepList,
|
||||
{
|
||||
text: "",
|
||||
internalId: crypto.randomUUID() // add internalId required for drag & drop
|
||||
}]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove a step by index
|
||||
*/
|
||||
const handleRemove = (index: number) => {
|
||||
onChange(instructionStepList.filter((_, i) => i !== index))
|
||||
}
|
||||
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>
|
||||
)
|
||||
return { handleUpdate, handleAdd, handleRemove };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,86 @@
|
|||
/**
|
||||
* Mobile editor using Up/Down buttons for reordering
|
||||
*/
|
||||
|
||||
import type { InstructionStepModel } from "../../models/InstructionStepModel";
|
||||
import Button, { ButtonType } from "../basics/Button";
|
||||
import { Icon } from "../basics/SvgIcon";
|
||||
import { useInstructionStepListEditor } from "./InstructionStepListEditor";
|
||||
|
||||
type InstructionStepListEditorMobileProps = {
|
||||
instructionStepList: InstructionStepModel[];
|
||||
onChange: (steps: InstructionStepModel[]) => void;
|
||||
};
|
||||
|
||||
export function InstructionStepListMobileEditor({
|
||||
instructionStepList,
|
||||
onChange,
|
||||
}: InstructionStepListEditorMobileProps) {
|
||||
const { handleUpdate, handleAdd, handleRemove } = useInstructionStepListEditor(
|
||||
instructionStepList,
|
||||
onChange
|
||||
);
|
||||
|
||||
const moveStep = (index: number, direction: "up" | "down") => {
|
||||
const newIndex = direction === "up" ? index - 1 : index + 1;
|
||||
if (newIndex < 0 || newIndex >= instructionStepList.length) return;
|
||||
const newList = [...instructionStepList];
|
||||
const [moved] = newList.splice(index, 1);
|
||||
newList.splice(newIndex, 0, moved);
|
||||
onChange(newList);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
{instructionStepList.map((step, index) => (
|
||||
<div
|
||||
key={step.id}
|
||||
className="flex items-start gap-3 bg-gray-50 rounded-xl p-3 shadow-sm"
|
||||
>
|
||||
<div className="flex flex-col items-center pt-1">
|
||||
<div className="w-8 h-8 rounded-full bg-gray-300 flex items-center justify-center text-sm font-semibold text-gray-800">
|
||||
{index + 1}
|
||||
</div>
|
||||
<div className="flex flex-col mt-2">
|
||||
<Button
|
||||
icon={Icon.ArrowUp}
|
||||
onClick={() => moveStep(index, "up")}
|
||||
buttonType={ButtonType.TransparentButton}
|
||||
/>
|
||||
<Button
|
||||
icon={Icon.ArrowDown}
|
||||
onClick={() => moveStep(index, "down")}
|
||||
buttonType={ButtonType.TransparentButton}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
className="input-field w-full min-h-[60px] resize-none overflow-hidden"
|
||||
placeholder={`Schritt ${index + 1}`}
|
||||
value={step.text}
|
||||
onChange={(e) => handleUpdate(index, e.target.value)}
|
||||
onInput={(e) => {
|
||||
const el = e.target as HTMLTextAreaElement;
|
||||
el.style.height = "auto";
|
||||
el.style.height = `${el.scrollHeight}px`;
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
onClick={() => handleRemove(index)}
|
||||
icon={Icon.X}
|
||||
buttonType={ButtonType.DarkButton}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
onClick={handleAdd}
|
||||
icon={Icon.Plus}
|
||||
text="Schritt hinzufügen"
|
||||
buttonType={ButtonType.PrimaryButton}
|
||||
className="mt-4"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ import { useEffect, useState } from "react"
|
|||
import { fetchRecipe } from "../../api/points/RecipePoint"
|
||||
import { getRecipeEditUrl, getRecipeListUrl } from "../../routes"
|
||||
import ButtonLink from "../basics/ButtonLink"
|
||||
import { mapRecipeDtoToModel } from "../../mappers/recipeMapper"
|
||||
import { mapRecipeDtoToModel } from "../../mappers/RecipeMapper"
|
||||
|
||||
|
||||
/**
|
||||
|
|
@ -123,14 +123,6 @@ export default function RecipeDetailPage() {
|
|||
</ul>
|
||||
|
||||
{/* Instructions - @todo add reasonable list delegate component*/}
|
||||
{/*<h2 className="section-heading">Zubereitung</h2>
|
||||
<ol className="default-list mb-6">
|
||||
{recipe.instructionStepList.map((step,j) =>(
|
||||
<li key={j}>
|
||||
{step.text}
|
||||
</li>
|
||||
))}
|
||||
</ol>*/}
|
||||
<ol className="space-y-4">
|
||||
{recipe.instructionStepList.map((step, j) => (
|
||||
<li key={j} className="flex items-start gap-4">
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import type { RecipeModel } from "../../models/RecipeModel"
|
|||
import RecipeEditor from "./RecipeEditor"
|
||||
import { fetchRecipe, createOrUpdateRecipe } from "../../api/points/RecipePoint"
|
||||
import { getRecipeDetailUrl, getRecipeListUrl } from "../../routes"
|
||||
import { mapRecipeDtoToModel, mapRecipeModelToDto } from "../../mappers/recipeMapper"
|
||||
import { mapRecipeDtoToModel, mapRecipeModelToDto } from "../../mappers/RecipeMapper"
|
||||
import type { RecipeDto } from "../../api/dtos/RecipeDto"
|
||||
|
||||
export default function RecipeEditPage() {
|
||||
|
|
|
|||
|
|
@ -16,13 +16,17 @@ export function mapRecipeDtoToModel(dto: RecipeDto): RecipeModel {
|
|||
amount: dto.amount ?? 1,
|
||||
unit: dto.amountDescription ?? "",
|
||||
},
|
||||
// @todo implement steps in frontend
|
||||
// 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
|
||||
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()
|
||||
})
|
||||
),
|
||||
|
||||
|
|
@ -3,5 +3,10 @@
|
|||
*/
|
||||
export interface InstructionStepModel{
|
||||
id?: string;
|
||||
/**
|
||||
* Unique id required for sorting via drag & drop
|
||||
* Local to the frontend!
|
||||
*/
|
||||
internalId: string;
|
||||
text: string;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue