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;
}
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;

View file

@ -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<string>("");
@ -48,7 +49,7 @@ export default function LoginPage() {
};
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">
<h1 className="text-center mb-2">Anmeldung</h1>
@ -75,6 +76,6 @@ export default function LoginPage() {
onClick={executeLogin}
/>
</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";
type HorizontalInputGroupLayoutProps = {
/** Content to render inside the header */
/** Content to render inside the group */
children: ReactNode;
/** 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 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 */
<div className="app-bg">
<PageContainer>
{/* Container defining the maximum width of the content */}
<div className="content-bg">
<ContentBackground>
{/* Header - remains in position when scrolling */}
<StickyHeader>
<h1>{recipeWorkingCopy.title}</h1>
</StickyHeader>
{/* Content */}
<div className="content-container">
<ContentBody>
{/* Recipe image */}
{recipe.imageUrl && (
<img
@ -153,8 +156,8 @@ export default function RecipeDetailPage() {
text="Zurueck"
/>
</ButtonGroupLayout>
</div>
</div>
</div>
</ContentBody>
</ContentBackground>
</PageContainer>
)
}

View file

@ -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<RecipeModel | null>(null)
const navigate = useNavigate()
const {id} = useParams<{ id: string }>()
const [recipe, setRecipe] = useState<RecipeModel | null>(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 <div>Loading...</div>
}
return <RecipeEditor recipe={recipe} onSave={handleSave} onCancel={handleCancel}/>
// error handling -> if there is no recipe, we cannot open edit view
if (!recipe) {
return <div>Loading...</div>
}
return <RecipeEditor recipe={recipe} onSave={handleSave} onCancel={handleCancel}/>
}

View file

@ -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<RecipeModel>(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 */
<div className="app-bg">
<PageContainer>
{/* Container defining the maximum width of the content */}
<div className="content-bg">
<ContentBackground>
<h1 className="border-b-2 border-gray-300 pb-4">
{recipe.id ? "Rezept bearbeiten" : "Neues Rezept"}
</h1>
<div className="content-container">
<ContentBody>
{/* Title */}
<h2>Titel</h2>
<input
@ -154,8 +151,8 @@ export default function RecipeEditor({recipe, onSave, onCancel}: RecipeEditorPro
text={"Abbrechen"}
/>
</ButtonGroupLayout>
</div>
</div>
</div>
</ContentBody>
</ContentBackground>
</PageContainer>
)
}

View file

@ -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 */
<div className="app-bg">
<PageContainer>
{/* Container defining the maximum width of the content */}
<div className="content-bg">
<ContentBackground>
{/* Header - remains in position when scrolling */}
<StickyHeader>
<h1>Recipes</h1>
@ -67,7 +69,7 @@ export default function RecipeListPage() {
))}
</div>
</div>
</div>
</div>
</ContentBackground>
</PageContainer>
)
}