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;
|
@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;
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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";
|
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 */
|
||||||
|
|
|
||||||
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 {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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,77 +1,75 @@
|
||||||
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()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadRecipe = async () => {
|
const loadRecipe = async () => {
|
||||||
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)
|
||||||
|
}
|
||||||
|
} 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) => {
|
const handleCancel = () => {
|
||||||
try {
|
console.log("Cancelling edit mode for", recipe ? recipe.id : "no recipe at all")
|
||||||
const dto = mapRecipeModelToDto(updated);
|
navigateBack();
|
||||||
await createOrUpdateRecipe(dto);
|
|
||||||
navigateBack();
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
const navigateBack = () => {
|
||||||
|
if (recipe && recipe.id) {
|
||||||
const handleCancel = () => {
|
console.log("navigating to recipe with id", recipe.id)
|
||||||
console.log("Cancelling edit mode for", recipe ? recipe.id : "no recipe at all")
|
navigate(getRecipeDetailUrl(recipe.id)) // go back to detail view
|
||||||
navigateBack();
|
} 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)
|
// error handling -> if there is no recipe, we cannot open edit view
|
||||||
navigate(getRecipeDetailUrl(recipe.id)) // go back to detail view
|
if (!recipe) {
|
||||||
} else {
|
return <div>Loading...</div>
|
||||||
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}/>
|
return <RecipeEditor recipe={recipe} onSave={handleSave} onCancel={handleCancel}/>
|
||||||
}
|
}
|
||||||
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue