-
Ingredients
- {ingredients.map((ing, index) => (
-
- )
+ )
}
diff --git a/frontend/src/components/recipes/InstructionStepListDesktopEditor.tsx b/frontend/src/components/recipes/InstructionStepListDesktopEditor.tsx
index a9b09af..da99ab3 100644
--- a/frontend/src/components/recipes/InstructionStepListDesktopEditor.tsx
+++ b/frontend/src/components/recipes/InstructionStepListDesktopEditor.tsx
@@ -2,86 +2,76 @@
* 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 { instructionStepListEditorMethods } from "./InstructionStepListEditor";
+import {closestCenter, DndContext, 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 Button from "../basics/Button";
-import { Plus } from "lucide-react";
-import { ButtonType } from "../basics/BasicButtonDefinitions";
+import {Plus} from "lucide-react";
+import {ButtonType} from "../basics/BasicButtonDefinitions";
+import {instructionStepListEditorMethods} from "./InstructionStepListEditorMethods.ts";
type InstructionStepListDesktopEditorProps = {
- instructionStepList: InstructionStepModel[];
- onChange: (steps: InstructionStepModel[]) => void;
+ instructionStepList: InstructionStepModel[];
+ onChange: (steps: InstructionStepModel[]) => void;
};
export function InstructionStepListDesktopEditor({
- instructionStepList,
- onChange,
-}: InstructionStepListDesktopEditorProps) {
- const { handleUpdate, handleAdd, handleRemove } = instructionStepListEditorMethods(
- instructionStepList,
- onChange
- );
+ instructionStepList,
+ onChange,
+ }: InstructionStepListDesktopEditorProps) {
+ const {handleUpdate, handleAdd, handleRemove} = instructionStepListEditorMethods(
+ instructionStepList,
+ onChange
+ );
- const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 8 } }));
+ 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);
- }
- };
+ 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 (
-
-
Zubereitung
-
- i.internalId)}
- strategy={verticalListSortingStrategy}
- >
-
- {instructionStepList.map((step, index) => (
-
- ))}
-
-
-
-
-
- );
+ return (
+
+
Zubereitung
+
+ i.internalId)}
+ strategy={verticalListSortingStrategy}
+ >
+
+ {instructionStepList.map((step, index) => (
+
+ ))}
+
+
+
+
+
+ );
}
diff --git a/frontend/src/components/recipes/InstructionStepListEditor.tsx b/frontend/src/components/recipes/InstructionStepListEditor.tsx
index 4beb906..f9e96d8 100644
--- a/frontend/src/components/recipes/InstructionStepListEditor.tsx
+++ b/frontend/src/components/recipes/InstructionStepListEditor.tsx
@@ -6,81 +6,42 @@
* - 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";
+import {useEffect, useState} from "react";
+import type {InstructionStepModel} from "../../models/InstructionStepModel";
+import {InstructionStepListDesktopEditor} from "./InstructionStepListDesktopEditor";
+import {InstructionStepListMobileEditor} from "./InstructionStepListMobileEditor";
/**
* Props for the hybrid InstructionStepListEditor
*/
type InstructionStepListEditorProps = {
- instructionStepList: InstructionStepModel[];
- onChange: (steps: 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);
+ instructionStepList,
+ onChange,
+ }: InstructionStepListEditorProps) {
+ const [isTouch, setIsTouch] = useState(false);
- useEffect(() => {
- setIsTouch("ontouchstart" in window || navigator.maxTouchPoints > 0);
- }, []);
+ useEffect(() => {
+ setIsTouch("ontouchstart" in window || navigator.maxTouchPoints > 0);
+ }, []);
- return isTouch ? (
-
- ) : (
-
- );
-}
-
-/**
- * ======= Shared hook for common logic =======
- *
- * Provides add/remove/update functionality used by both mobile and desktop editors.
- */
-export function instructionStepListEditorMethods(
- 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
+ return isTouch ? (
+
+ ) : (
+
);
- onChange(updated);
- };
-
- /**
- * Add a new step at the end
- */
- const handleAdd = () => {
- 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));
- };
-
- return { handleUpdate, handleAdd, handleRemove };
}
+
diff --git a/frontend/src/components/recipes/InstructionStepListEditorMethods.ts b/frontend/src/components/recipes/InstructionStepListEditorMethods.ts
new file mode 100644
index 0000000..7db9757
--- /dev/null
+++ b/frontend/src/components/recipes/InstructionStepListEditorMethods.ts
@@ -0,0 +1,41 @@
+import type {InstructionStepModel} from "../../models/InstructionStepModel.ts";
+
+/**
+ * ======= Shared hook for common logic =======
+ *
+ * Provides add/remove/update functionality used by both mobile and desktop editors.
+ */
+export function instructionStepListEditorMethods(
+ 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: "",
+ internalId: crypto.randomUUID() // add internalId required for drag & drop
+ }]);
+ };
+
+ /**
+ * Remove a step by index
+ */
+ const handleRemove = (index: number) => {
+ onChange(instructionStepList.filter((_, i) => i !== index));
+ };
+
+ return {handleUpdate, handleAdd, handleRemove};
+}
\ No newline at end of file
diff --git a/frontend/src/components/recipes/InstructionStepListMobileEditor.tsx b/frontend/src/components/recipes/InstructionStepListMobileEditor.tsx
index cac7c42..15376d9 100644
--- a/frontend/src/components/recipes/InstructionStepListMobileEditor.tsx
+++ b/frontend/src/components/recipes/InstructionStepListMobileEditor.tsx
@@ -2,86 +2,91 @@
* Mobile editor using Up/Down buttons for reordering
*/
-import { ArrowDown, ArrowUp, Plus, X } from "lucide-react";
-import type { InstructionStepModel } from "../../models/InstructionStepModel";
+import {ArrowDown, ArrowUp, Plus, X} from "lucide-react";
+import type {InstructionStepModel} from "../../models/InstructionStepModel";
import Button from "../basics/Button";
-import { instructionStepListEditorMethods } from "./InstructionStepListEditor";
-import { ButtonType } from "../basics/BasicButtonDefinitions";
+import {ButtonType} from "../basics/BasicButtonDefinitions";
+import {instructionStepListEditorMethods} from "./InstructionStepListEditorMethods.ts";
type InstructionStepListEditorMobileProps = {
- instructionStepList: InstructionStepModel[];
- onChange: (steps: InstructionStepModel[]) => void;
+ instructionStepList: InstructionStepModel[];
+ onChange: (steps: InstructionStepModel[]) => void;
};
export function InstructionStepListMobileEditor({
- instructionStepList,
- onChange,
-}: InstructionStepListEditorMobileProps) {
- const { handleUpdate, handleAdd, handleRemove } = instructionStepListEditorMethods(
- instructionStepList,
- onChange
- );
+ instructionStepList,
+ onChange,
+ }: InstructionStepListEditorMobileProps) {
+ const {handleUpdate, handleAdd, handleRemove} = instructionStepListEditorMethods(
+ 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);
- };
+ 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 (
-
- {instructionStepList.map((step, index) => (
-
-
-
- {index + 1}
+ return (
+
+
Zubereitung
+
+ {instructionStepList.map((step, index) => (
+
+
+
+ {index + 1}
+
+
+ moveStep(index, "up")}
+ buttonType={ButtonType.TransparentButton}
+ disabled={index === 0} // disable if first item
+ />
+ moveStep(index, "down")}
+ buttonType={ButtonType.TransparentButton}
+ disabled={index === instructionStepList.length - 1} // disable if last item
+ />
+
+
+
+
+ ))}
+
-
- moveStep(index, "up")}
- buttonType={ButtonType.TransparentButton}
- />
- moveStep(index, "down")}
- buttonType={ButtonType.TransparentButton}
- />
-
-
-
-
- ))}
-
-
- );
+ );
}
diff --git a/frontend/src/components/recipes/RecipeDetailPage.tsx b/frontend/src/components/recipes/RecipeDetailPage.tsx
index c856c9b..a1c89fd 100644
--- a/frontend/src/components/recipes/RecipeDetailPage.tsx
+++ b/frontend/src/components/recipes/RecipeDetailPage.tsx
@@ -80,7 +80,7 @@ export default function RecipeDetailPage() {
{/* Header - remains in position when scrolling */}
-
{recipeWorkingCopy.title}
+ {recipeWorkingCopy.title}
{/* Content */}
@@ -132,7 +132,7 @@ export default function RecipeDetailPage() {
{/* Instructions - @todo add reasonable list delegate component*/}
{recipe.instructionStepList.map((step, j) => (
-
+
))}
diff --git a/frontend/src/components/recipes/RecipeEditor.tsx b/frontend/src/components/recipes/RecipeEditor.tsx
index c52cdfe..c3d586b 100644
--- a/frontend/src/components/recipes/RecipeEditor.tsx
+++ b/frontend/src/components/recipes/RecipeEditor.tsx
@@ -1,16 +1,16 @@
-import { useState } from "react"
-import type { RecipeModel } from "../../models/RecipeModel"
-import type { IngredientGroupModel } from "../../models/IngredientGroupModel"
-import { IngredientGroupListEditor } from "./IngredientGroupListEditor"
+import {useState} from "react"
+import type {RecipeModel} from "../../models/RecipeModel"
+import type {IngredientGroupModel} from "../../models/IngredientGroupModel"
+import {IngredientGroupListEditor} from "./IngredientGroupListEditor"
import Button from "../basics/Button"
-import { InstructionStepListEditor } from "./InstructionStepListEditor"
-import type { InstructionStepModel } from "../../models/InstructionStepModel"
-import { ButtonType } from "../basics/BasicButtonDefinitions"
+import {InstructionStepListEditor} from "./InstructionStepListEditor"
+import type {InstructionStepModel} from "../../models/InstructionStepModel"
+import {ButtonType} from "../basics/BasicButtonDefinitions"
type RecipeEditorProps = {
- recipe: RecipeModel
- onSave: (recipe: RecipeModel) => void
- onCancel: () => void
+ recipe: RecipeModel
+ onSave: (recipe: RecipeModel) => void
+ onCancel: () => void
}
/**
@@ -18,138 +18,142 @@ type RecipeEditorProps = {
* ingredients (with amount, unit, name), instructions, and image URL.
* @todo adapt to ingredientGroups!
*/
-export default function RecipeEditor({ recipe, onSave, onCancel }: RecipeEditorProps) {
- /** draft of the new recipe */
- const [draft, setDraft] = useState
(recipe)
- /** Error list */
- const [errors, setErrors] = useState<{ title?: boolean; ingredients?: boolean }>({})
+export default function RecipeEditor({recipe, onSave, onCancel}: RecipeEditorProps) {
+ /** draft of the new recipe */
+ const [draft, setDraft] = useState(recipe)
+ /** Error list */
+ const [errors, setErrors] = useState<{ title?: boolean; ingredients?: boolean }>({})
- /**
- * Update 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
- */
- const validate = () => {
- const newErrors: { title?: boolean; ingredients?: boolean } = {}
-
- // each recipe requires a title
- if (!draft.title.trim()) {
- newErrors.title = true
- }
-
- /* there must be at least one ingredient group
- * no group may contain an empty ingredient list
- * @todo check whether all ingredients are valid
- * @todo enhance visualization of ingredient errors
+ /**
+ * Update ingredients
+ * @param ingredientGroupList updated ingredient groups and ingredients
*/
- if (!draft.ingredientGroupList || draft.ingredientGroupList.length === 0) {
- newErrors.ingredients = true
- } else {
- let isAnyIngredientListEmpty = draft.ingredientGroupList.some(
- ingGrp => {
- return !ingGrp.ingredientList || ingGrp.ingredientList.length === 0
+ 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
+ */
+ const validate = () => {
+ const newErrors: { title?: boolean; ingredients?: boolean } = {}
+
+ // each recipe requires a title
+ if (!draft.title.trim()) {
+ newErrors.title = true
}
- )
- if(isAnyIngredientListEmpty){
- newErrors.ingredients = true
- }
+
+ /* there must be at least one ingredient group
+ * no group may contain an empty ingredient list
+ * @todo check whether all ingredients are valid
+ * @todo enhance visualization of ingredient errors
+ */
+ if (!draft.ingredientGroupList || draft.ingredientGroupList.length === 0) {
+ newErrors.ingredients = true
+ } else {
+ const isAnyIngredientListEmpty = draft.ingredientGroupList.some(
+ ingGrp => {
+ return !ingGrp.ingredientList || ingGrp.ingredientList.length === 0
+ }
+ )
+ if (isAnyIngredientListEmpty) {
+ newErrors.ingredients = true
+ }
+ }
+
+ setErrors(newErrors)
+
+ return Object.keys(newErrors).length === 0
}
-
- setErrors(newErrors)
-
- return Object.keys(newErrors).length === 0
- }
- /** Handles saving and ensures that the draft is only saved if valid */
- const handleSave = (draft: RecipeModel) => {
- if (validate()) {
- onSave(draft)
+ /** Handles saving and ensures that the draft is only saved if valid */
+ const handleSave = (draft: RecipeModel) => {
+ if (validate()) {
+ onSave(draft)
+ }
}
- }
- // ensure that there is a recipe and show error otherwise
- if (!recipe) return Oops, there's no recipe in RecipeEditor...
- // @todo add handling of images
- return (
-
-
- {recipe.id ? "Edit Recipe" : "New Recipe"}
-
+ // ensure that there is a recipe and show error otherwise
+ if (!recipe) return
Oops, there's no recipe in RecipeEditor...
+ // @todo add handling of images
+ return (
+ /*Container spanning entire screen used to center content horizontally */
+
+ {/* Container defining the maximum width of the content */}
+
+
+ {recipe.id ? "Rezept bearbeiten" : "Neues Rezept"}
+
- {/* Title */}
-
Title
-
setDraft({ ...draft, title: e.target.value })}
- />
- {/* Servings */}
-
Servings
-
- For
- {
- const tempServings = draft.servings
- tempServings.amount = Number(e.target.value)
- setDraft({...draft, servings: tempServings})
- }}
- />
- {
- const tempServings = draft.servings
- tempServings.unit = e.target.value
- setDraft({...draft, servings: tempServings})
- }}
- />
-
- {/* Ingredient List - @todo better visualization of errors! */}
-
-
-
+ {/* Title */}
+
Titel
+
setDraft({...draft, title: e.target.value})}
+ />
+ {/* Servings */}
+
Portionen
+
+ Für
+ {
+ const tempServings = draft.servings
+ tempServings.amount = Number(e.target.value)
+ setDraft({...draft, servings: tempServings})
+ }}
+ />
+ {
+ const tempServings = draft.servings
+ tempServings.unit = e.target.value
+ setDraft({...draft, servings: tempServings})
+ }}
+ />
+
+ {/* Ingredient List - @todo better visualization of errors! */}
+
+
+
- {/* Instruction List*/ }
-
-
+ {/* Instruction List*/}
+
-
- {/* Save Button */}
- handleSave(draft)}
- text={"Save"}
- buttonType={ButtonType.PrimaryButton}
- />
- onCancel()}
- text={"Cancel"}
- />
-
-
+
+
+ {/* Save Button */}
+ handleSave(draft)}
+ text={"Speichern"}
+ buttonType={ButtonType.PrimaryButton}
+ />
+ onCancel()}
+ text={"Abbrechen"}
+ />
+
+
+
)
}