Add container components
This commit is contained in:
parent
b70554db10
commit
f867cd3601
10 changed files with 184 additions and 107 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
30
frontend/src/components/basics/ContentBackground.tsx
Normal file
30
frontend/src/components/basics/ContentBackground.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
29
frontend/src/components/basics/ContentBody.tsx
Normal file
29
frontend/src/components/basics/ContentBody.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 */
|
||||
|
|
|
|||
29
frontend/src/components/basics/PageContainer.tsx
Normal file
29
frontend/src/components/basics/PageContainer.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}/>
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue