Add container components

This commit is contained in:
araemer 2025-10-25 20:52:16 +02:00
parent b70554db10
commit f867cd3601
10 changed files with 184 additions and 107 deletions

View file

@ -29,28 +29,16 @@
@apply text-gray-600; @apply text-gray-600;
} }
body {
@apply bg-gray-50 flex items-center;
}
/* errors */ /* errors */
.error-text { .error-text {
@apply text-sm text-red-600; @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 */ /* containers */
.highlight-container-bg { .highlight-container-bg {
@apply bg-gray-200 rounded p-2; @apply bg-gray-200 rounded p-2;

View file

@ -7,6 +7,7 @@ import {getRecipeListUrl} from "../routes";
import {useNavigate} from "react-router-dom"; import {useNavigate} from "react-router-dom";
import PasswordField from "./basics/PasswordField"; import PasswordField from "./basics/PasswordField";
import {ButtonType} from "./basics/BasicButtonDefinitions"; import {ButtonType} from "./basics/BasicButtonDefinitions";
import PageContainer from "./basics/PageContainer.tsx";
export default function LoginPage() { export default function LoginPage() {
const [userName, setUserName] = useState<string>(""); const [userName, setUserName] = useState<string>("");
@ -48,7 +49,7 @@ export default function LoginPage() {
}; };
return ( return (
<div className="app-bg"> <PageContainer>
<div className="flex flex-col gap-3 max-w-sm w-full mx-auto p-6 bg-white rounded-2xl shadow-md"> <div className="flex flex-col gap-3 max-w-sm w-full mx-auto p-6 bg-white rounded-2xl shadow-md">
<h1 className="text-center mb-2">Anmeldung</h1> <h1 className="text-center mb-2">Anmeldung</h1>
@ -75,6 +76,6 @@ export default function LoginPage() {
onClick={executeLogin} onClick={executeLogin}
/> />
</div> </div>
</div> </PageContainer>
); );
} }

View file

@ -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 (
<div
className={clsx(
"bg-gray-100 w-full min-h-screen max-w-6xl shadow-xl p-8",
className
)}
>
{children}
</div>
);
}

View file

@ -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 (
<div
className={clsx(
"p-6 max-w-2xl mx-auto",
className
)}
>
{children}
</div>
);
}

View file

@ -2,7 +2,7 @@ import type {ReactNode} from "react";
import clsx from "clsx"; import clsx from "clsx";
type HorizontalInputGroupLayoutProps = { type HorizontalInputGroupLayoutProps = {
/** Content to render inside the header */ /** Content to render inside the group */
children: ReactNode; children: ReactNode;
/** Optional additional Tailwind classes */ /** Optional additional Tailwind classes */

View file

@ -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 (
<div
className={clsx(
"flex items-center w-screen justify-center min-h-screen ;",
className
)}
>
{children}
</div>
);
}

View file

@ -10,6 +10,9 @@ import {NumberedListItem} from "../basics/NumberedListItem.tsx";
import {ButtonType} from "../basics/BasicButtonDefinitions.ts"; import {ButtonType} from "../basics/BasicButtonDefinitions.ts";
import StickyHeader from "../basics/StickyHeader.tsx"; import StickyHeader from "../basics/StickyHeader.tsx";
import ButtonGroupLayout from "../basics/ButtonGroupLayout.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 ( return (
/*Container spanning entire screen used to center content horizontally */ /*Container spanning entire screen used to center content horizontally */
<div className="app-bg"> <PageContainer>
{/* Container defining the maximum width of the content */} {/* Container defining the maximum width of the content */}
<div className="content-bg"> <ContentBackground>
{/* Header - remains in position when scrolling */} {/* Header - remains in position when scrolling */}
<StickyHeader> <StickyHeader>
<h1>{recipeWorkingCopy.title}</h1> <h1>{recipeWorkingCopy.title}</h1>
</StickyHeader> </StickyHeader>
{/* Content */} {/* Content */}
<div className="content-container"> <ContentBody>
{/* Recipe image */} {/* Recipe image */}
{recipe.imageUrl && ( {recipe.imageUrl && (
<img <img
@ -153,8 +156,8 @@ export default function RecipeDetailPage() {
text="Zurueck" text="Zurueck"
/> />
</ButtonGroupLayout> </ButtonGroupLayout>
</div> </ContentBody>
</div> </ContentBackground>
</div> </PageContainer>
) )
} }

View file

@ -1,15 +1,15 @@
import { useParams, useNavigate } from "react-router-dom" import {useNavigate, useParams} from "react-router-dom"
import { useEffect, useState } from "react" import {useEffect, useState} from "react"
import type { RecipeModel } from "../../models/RecipeModel" import type {RecipeModel} from "../../models/RecipeModel"
import RecipeEditor from "./RecipeEditor" import {RecipeEditor} from "./RecipeEditor"
import { fetchRecipe, createOrUpdateRecipe } from "../../api/points/RecipePoint" import {createOrUpdateRecipe, fetchRecipe} from "../../api/points/RecipePoint"
import { getRecipeDetailUrl, getRecipeListUrl } from "../../routes" import {getRecipeDetailUrl, getRecipeListUrl} from "../../routes"
import { mapRecipeDtoToModel, mapRecipeModelToDto } from "../../mappers/RecipeMapper" import {mapRecipeDtoToModel, mapRecipeModelToDto} from "../../mappers/RecipeMapper"
import type { RecipeDto } from "../../api/dtos/RecipeDto" import type {RecipeDto} from "../../api/dtos/RecipeDto"
export default function RecipeEditPage() { export default function RecipeEditPage() {
// Extract recipe ID from route params // Extract recipe ID from route params
const { id } = useParams<{ id: string }>() const {id} = useParams<{ id: string }>()
const [recipe, setRecipe] = useState<RecipeModel | null>(null) const [recipe, setRecipe] = useState<RecipeModel | null>(null)
const navigate = useNavigate() const navigate = useNavigate()
@ -18,7 +18,7 @@ export default function RecipeEditPage() {
if (id) { if (id) {
try { try {
// Fetch recipe data when editing an existing one // Fetch recipe data when editing an existing one
const data : RecipeDto = await fetchRecipe(id); const data: RecipeDto = await fetchRecipe(id);
setRecipe(mapRecipeDtoToModel(data)); setRecipe(mapRecipeDtoToModel(data));
} catch (err) { } catch (err) {
console.error(err) console.error(err)
@ -28,8 +28,7 @@ export default function RecipeEditPage() {
setRecipe({ setRecipe({
id: "", id: "",
title: "", title: "",
ingredientGroupList: [ ingredientGroupList: [],
],
instructionStepList: [], instructionStepList: [],
servings: { servings: {
amount: 1, amount: 1,
@ -53,14 +52,13 @@ export default function RecipeEditPage() {
} }
const handleCancel = () => { const handleCancel = () => {
console.log("Cancelling edit mode for", recipe ? recipe.id : "no recipe at all") console.log("Cancelling edit mode for", recipe ? recipe.id : "no recipe at all")
navigateBack(); navigateBack();
} }
const navigateBack = () => { const navigateBack = () => {
if(recipe && recipe.id){ if (recipe && recipe.id) {
console.log("navigating to recipe with id", recipe.id) console.log("navigating to recipe with id", recipe.id)
navigate(getRecipeDetailUrl(recipe.id)) // go back to detail view navigate(getRecipeDetailUrl(recipe.id)) // go back to detail view
} else { } else {

View file

@ -7,6 +7,9 @@ import {InstructionStepListEditor} from "./InstructionStepListEditor"
import type {InstructionStepModel} from "../../models/InstructionStepModel" import type {InstructionStepModel} from "../../models/InstructionStepModel"
import {ButtonType} from "../basics/BasicButtonDefinitions" import {ButtonType} from "../basics/BasicButtonDefinitions"
import ButtonGroupLayout from "../basics/ButtonGroupLayout.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";
type RecipeEditorProps = { type RecipeEditorProps = {
recipe: RecipeModel recipe: RecipeModel
@ -14,12 +17,7 @@ type RecipeEditorProps = {
onCancel: () => void onCancel: () => void
} }
/** export function RecipeEditor({recipe, onSave, onCancel}: RecipeEditorProps) {
* 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) {
/** draft of the new recipe */ /** draft of the new recipe */
const [draft, setDraft] = useState<RecipeModel>(recipe) const [draft, setDraft] = useState<RecipeModel>(recipe)
/** Error list */ /** Error list */
@ -86,14 +84,13 @@ export default function RecipeEditor({recipe, onSave, onCancel}: RecipeEditorPro
// @todo add handling of images // @todo add handling of images
return ( return (
/*Container spanning entire screen used to center content horizontally */ /*Container spanning entire screen used to center content horizontally */
<div className="app-bg"> <PageContainer>
{/* Container defining the maximum width of the content */} {/* Container defining the maximum width of the content */}
<div className="content-bg"> <ContentBackground>
<h1 className="border-b-2 border-gray-300 pb-4"> <h1 className="border-b-2 border-gray-300 pb-4">
{recipe.id ? "Rezept bearbeiten" : "Neues Rezept"} {recipe.id ? "Rezept bearbeiten" : "Neues Rezept"}
</h1> </h1>
<div className="content-container"> <ContentBody>
{/* Title */} {/* Title */}
<h2>Titel</h2> <h2>Titel</h2>
<input <input
@ -154,8 +151,8 @@ export default function RecipeEditor({recipe, onSave, onCancel}: RecipeEditorPro
text={"Abbrechen"} text={"Abbrechen"}
/> />
</ButtonGroupLayout> </ButtonGroupLayout>
</div> </ContentBody>
</div> </ContentBackground>
</div> </PageContainer>
) )
} }

View file

@ -6,6 +6,8 @@ import {useNavigate} from "react-router-dom"
import {getRecipeAddUrl, getRecipeDetailUrl, getRecipeListUrl} from "../../routes" import {getRecipeAddUrl, getRecipeDetailUrl, getRecipeListUrl} from "../../routes"
import RecipeListToolbar from "./RecipeListToolbar" import RecipeListToolbar from "./RecipeListToolbar"
import StickyHeader from "../basics/StickyHeader.tsx"; 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. * Displays a list of recipes in a sidebar layout.
@ -42,9 +44,9 @@ export default function RecipeListPage() {
} }
return ( return (
/*Container spanning entire screen used to center content horizontally */ /*Container spanning entire screen used to center content horizontally */
<div className="app-bg"> <PageContainer>
{/* Container defining the maximum width of the content */} {/* Container defining the maximum width of the content */}
<div className="content-bg"> <ContentBackground>
{/* Header - remains in position when scrolling */} {/* Header - remains in position when scrolling */}
<StickyHeader> <StickyHeader>
<h1>Recipes</h1> <h1>Recipes</h1>
@ -67,7 +69,7 @@ export default function RecipeListPage() {
))} ))}
</div> </div>
</div> </div>
</div> </ContentBackground>
</div> </PageContainer>
) )
} }