diff --git a/frontend/src/App.css b/frontend/src/App.css index a2f3294..db6d28d 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -29,28 +29,16 @@ @apply text-gray-600; } + body { + @apply bg-gray-50 flex items-center; + } + /* errors */ .error-text { @apply text-sm text-red-600; } - /* @todo replace by components! - -/* background */ - .app-bg { - @apply flex items-center w-screen justify-center min-h-screen bg-gray-50; - } - - .content-bg { - @apply bg-gray-100 w-full min-h-screen max-w-6xl shadow-xl p-8; - } - - .content-container { - @apply p-6 max-w-2xl mx-auto; - } - - /* containers */ .highlight-container-bg { @apply bg-gray-200 rounded p-2; diff --git a/frontend/src/components/LoginPage.tsx b/frontend/src/components/LoginPage.tsx index 22b4d1f..9f6f054 100644 --- a/frontend/src/components/LoginPage.tsx +++ b/frontend/src/components/LoginPage.tsx @@ -7,6 +7,7 @@ import {getRecipeListUrl} from "../routes"; import {useNavigate} from "react-router-dom"; import PasswordField from "./basics/PasswordField"; import {ButtonType} from "./basics/BasicButtonDefinitions"; +import PageContainer from "./basics/PageContainer.tsx"; export default function LoginPage() { const [userName, setUserName] = useState(""); @@ -48,7 +49,7 @@ export default function LoginPage() { }; return ( -
+

Anmeldung

@@ -75,6 +76,6 @@ export default function LoginPage() { onClick={executeLogin} />
-
+ ); } diff --git a/frontend/src/components/basics/ContentBackground.tsx b/frontend/src/components/basics/ContentBackground.tsx new file mode 100644 index 0000000..898307e --- /dev/null +++ b/frontend/src/components/basics/ContentBackground.tsx @@ -0,0 +1,30 @@ +import type {ReactNode} from "react"; +import clsx from "clsx"; + +type ContentBackgroundProps = { + /** Content to render inside the container */ + children: ReactNode; + + /** Optional additional Tailwind classes */ + className?: string; +}; + +/** + * Background for the content area of a page + * @param children Children the page, i.e., header and body + * @param className Optional additional styles + * @see ContentBody.tsx + * @constructor + */ +export default function ContentBackground({children, className = ""}: ContentBackgroundProps) { + return ( +
+ {children} +
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/basics/ContentBody.tsx b/frontend/src/components/basics/ContentBody.tsx new file mode 100644 index 0000000..3e19c16 --- /dev/null +++ b/frontend/src/components/basics/ContentBody.tsx @@ -0,0 +1,29 @@ +import {type ReactNode} from "react"; +import clsx from "clsx"; + +type ContentBodyProps = { + /** Content to render inside the container */ + children: ReactNode; + + /** Optional additional Tailwind classes */ + className?: string; +}; + +/** + * Body for the content area of a page + * @param children Children the page, i.e., header and body + * @param className Optional additional styles + * @constructor + */ +export default function ContentBody({children, className = ""}: ContentBodyProps) { + return ( +
+ {children} +
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/basics/HorizontalInputGroupLayout.tsx b/frontend/src/components/basics/HorizontalInputGroupLayout.tsx index a32cce6..732af60 100644 --- a/frontend/src/components/basics/HorizontalInputGroupLayout.tsx +++ b/frontend/src/components/basics/HorizontalInputGroupLayout.tsx @@ -2,7 +2,7 @@ import type {ReactNode} from "react"; import clsx from "clsx"; type HorizontalInputGroupLayoutProps = { - /** Content to render inside the header */ + /** Content to render inside the group */ children: ReactNode; /** Optional additional Tailwind classes */ diff --git a/frontend/src/components/basics/PageContainer.tsx b/frontend/src/components/basics/PageContainer.tsx new file mode 100644 index 0000000..f22f397 --- /dev/null +++ b/frontend/src/components/basics/PageContainer.tsx @@ -0,0 +1,29 @@ +import {type ReactNode} from "react"; +import clsx from "clsx"; + +type PageContainerProps = { + /** Content to render inside the container */ + children: ReactNode; + + /** Optional additional Tailwind classes */ + className?: string; +}; + +/** + * Container for a page providing the correct dimensions to its children + * @param children Children the page, i.e., header and body + * @param className Optional additional styles + * @constructor + */ +export default function PageContainer({children, className = ""}: PageContainerProps): ReactNode { + return ( +
+ {children} +
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/recipes/RecipeDetailPage.tsx b/frontend/src/components/recipes/RecipeDetailPage.tsx index 54426bb..bb053ef 100644 --- a/frontend/src/components/recipes/RecipeDetailPage.tsx +++ b/frontend/src/components/recipes/RecipeDetailPage.tsx @@ -10,6 +10,9 @@ import {NumberedListItem} from "../basics/NumberedListItem.tsx"; import {ButtonType} from "../basics/BasicButtonDefinitions.ts"; import StickyHeader from "../basics/StickyHeader.tsx"; import ButtonGroupLayout from "../basics/ButtonGroupLayout.tsx"; +import ContentBackground from "../basics/ContentBackground.tsx"; +import ContentBody from "../basics/ContentBody.tsx"; +import PageContainer from "../basics/PageContainer.tsx"; /** @@ -79,16 +82,16 @@ export default function RecipeDetailPage() { return ( /*Container spanning entire screen used to center content horizontally */ -
+ {/* Container defining the maximum width of the content */} -
+ {/* Header - remains in position when scrolling */}

{recipeWorkingCopy.title}

{/* Content */} -
+ {/* Recipe image */} {recipe.imageUrl && ( -
-
-
+ + + ) } diff --git a/frontend/src/components/recipes/RecipeEditPage.tsx b/frontend/src/components/recipes/RecipeEditPage.tsx index 594b42e..6a9a5d8 100644 --- a/frontend/src/components/recipes/RecipeEditPage.tsx +++ b/frontend/src/components/recipes/RecipeEditPage.tsx @@ -1,77 +1,75 @@ -import { useParams, useNavigate } from "react-router-dom" -import { useEffect, useState } from "react" -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 type { RecipeDto } from "../../api/dtos/RecipeDto" +import {useNavigate, useParams} from "react-router-dom" +import {useEffect, useState} from "react" +import type {RecipeModel} from "../../models/RecipeModel" +import {RecipeEditor} from "./RecipeEditor" +import {createOrUpdateRecipe, fetchRecipe} from "../../api/points/RecipePoint" +import {getRecipeDetailUrl, getRecipeListUrl} from "../../routes" +import {mapRecipeDtoToModel, mapRecipeModelToDto} from "../../mappers/RecipeMapper" +import type {RecipeDto} from "../../api/dtos/RecipeDto" export default function RecipeEditPage() { // Extract recipe ID from route params - const { id } = useParams<{ id: string }>() - const [recipe, setRecipe] = useState(null) - const navigate = useNavigate() + const {id} = useParams<{ id: string }>() + const [recipe, setRecipe] = useState(null) + const navigate = useNavigate() - useEffect(() => { - const loadRecipe = async () => { - if (id) { - try { - // Fetch recipe data when editing an existing one - const data : RecipeDto = await fetchRecipe(id); - setRecipe(mapRecipeDtoToModel(data)); - } catch (err) { - console.error(err) + useEffect(() => { + const loadRecipe = async () => { + if (id) { + try { + // Fetch recipe data when editing an existing one + const data: RecipeDto = await fetchRecipe(id); + setRecipe(mapRecipeDtoToModel(data)); + } catch (err) { + console.error(err) + } + } else { + // New recipe case + setRecipe({ + id: "", + title: "", + ingredientGroupList: [], + instructionStepList: [], + servings: { + amount: 1, + unit: "" + } + }) + } + } + + loadRecipe() + }, [id]) + + const handleSave = async (updated: RecipeModel) => { + try { + const dto = mapRecipeModelToDto(updated); + await createOrUpdateRecipe(dto); + navigateBack(); + } catch (err) { + console.error(err) } - } else { - // New recipe case - setRecipe({ - id: "", - title: "", - ingredientGroupList: [ - ], - instructionStepList: [], - servings: { - amount: 1, - unit: "" - } - }) - } } - loadRecipe() - }, [id]) - const handleSave = async (updated: RecipeModel) => { - try { - const dto = mapRecipeModelToDto(updated); - await createOrUpdateRecipe(dto); - navigateBack(); - } catch (err) { - console.error(err) + const handleCancel = () => { + console.log("Cancelling edit mode for", recipe ? recipe.id : "no recipe at all") + navigateBack(); } - } - - - const handleCancel = () => { - console.log("Cancelling edit mode for", recipe ? recipe.id : "no recipe at all") - navigateBack(); - } - - const navigateBack = () => { - if(recipe && recipe.id){ - console.log("navigating to recipe with id", recipe.id) - navigate(getRecipeDetailUrl(recipe.id)) // go back to detail view - } else { - console.log("navigating back to list as no recipe was selected") - navigate(getRecipeListUrl()) // no recipe -> go back to list + const navigateBack = () => { + if (recipe && recipe.id) { + console.log("navigating to recipe with id", recipe.id) + navigate(getRecipeDetailUrl(recipe.id)) // go back to detail view + } else { + console.log("navigating back to list as no recipe was selected") + navigate(getRecipeListUrl()) // no recipe -> go back to list + } } - } - // error handling -> if there is no recipe, we cannot open edit view - if (!recipe) { - return
Loading...
- } - - return + // error handling -> if there is no recipe, we cannot open edit view + if (!recipe) { + return
Loading...
+ } + + return } \ No newline at end of file diff --git a/frontend/src/components/recipes/RecipeEditor.tsx b/frontend/src/components/recipes/RecipeEditor.tsx index 6620c01..08f9118 100644 --- a/frontend/src/components/recipes/RecipeEditor.tsx +++ b/frontend/src/components/recipes/RecipeEditor.tsx @@ -7,6 +7,9 @@ import {InstructionStepListEditor} from "./InstructionStepListEditor" import type {InstructionStepModel} from "../../models/InstructionStepModel" import {ButtonType} from "../basics/BasicButtonDefinitions" import ButtonGroupLayout from "../basics/ButtonGroupLayout.tsx"; +import ContentBackground from "../basics/ContentBackground.tsx"; +import ContentBody from "../basics/ContentBody.tsx"; +import PageContainer from "../basics/PageContainer.tsx"; type RecipeEditorProps = { recipe: RecipeModel @@ -14,12 +17,7 @@ type RecipeEditorProps = { onCancel: () => void } -/** - * Editor component for managing a recipe, including title, - * ingredients (with amount, unit, name), instructions, and image URL. - * @todo adapt to ingredientGroups! - */ -export default function RecipeEditor({recipe, onSave, onCancel}: RecipeEditorProps) { +export function RecipeEditor({recipe, onSave, onCancel}: RecipeEditorProps) { /** draft of the new recipe */ const [draft, setDraft] = useState(recipe) /** Error list */ @@ -86,14 +84,13 @@ export default function RecipeEditor({recipe, onSave, onCancel}: RecipeEditorPro // @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 */}

Titel

-
-
-
+ + + ) } diff --git a/frontend/src/components/recipes/RecipeListPage.tsx b/frontend/src/components/recipes/RecipeListPage.tsx index 79bfc72..765d7d5 100644 --- a/frontend/src/components/recipes/RecipeListPage.tsx +++ b/frontend/src/components/recipes/RecipeListPage.tsx @@ -6,6 +6,8 @@ import {useNavigate} from "react-router-dom" import {getRecipeAddUrl, getRecipeDetailUrl, getRecipeListUrl} from "../../routes" import RecipeListToolbar from "./RecipeListToolbar" import StickyHeader from "../basics/StickyHeader.tsx"; +import ContentBackground from "../basics/ContentBackground.tsx"; +import PageContainer from "../basics/PageContainer.tsx"; /** * Displays a list of recipes in a sidebar layout. @@ -42,9 +44,9 @@ export default function RecipeListPage() { } return ( /*Container spanning entire screen used to center content horizontally */ -
+ {/* Container defining the maximum width of the content */} -
+ {/* Header - remains in position when scrolling */}

Recipes

@@ -67,7 +69,7 @@ export default function RecipeListPage() { ))}
- - + + ) }