Clean up css

This commit is contained in:
araemer 2025-10-25 08:00:59 +02:00
parent 5a4e04a47c
commit c866c01dfe
17 changed files with 214 additions and 250 deletions

View file

@ -4,6 +4,31 @@
@tailwind utilities; @tailwind utilities;
/* Custom recipe app styles */ /* Custom recipe app styles */
@layer preflight {
/* headlines */
h1 {
@apply text-3xl font-black text-blue-900;
}
h2 {
@apply text-xl font-bold mb-4 mt-4;
}
h3 {
@apply font-semibold;
}
/* labels */
label {
@apply text-gray-600;
}
/* input fields */
input, textarea {
@apply p-2 w-full border rounded-md bg-white placeholder-gray-400 border-gray-600 hover:border-blue-800 transition-colors text-gray-600 focus:outline-none focus:border-blue-900;
}
}
@layer components { @layer components {
/* background */ /* background */
@ -24,17 +49,6 @@
@apply sticky bg-gray-100 top-0 left-0 right-0 pb-6 border-b-2 border-gray-300; @apply sticky bg-gray-100 top-0 left-0 right-0 pb-6 border-b-2 border-gray-300;
} }
.content-title {
@apply text-3xl font-black pb-6 text-blue-900;
}
.section-heading {
@apply text-xl font-bold pb-4 pt-4;
}
.subsection-heading {
@apply font-semibold pb-2 pt-4;
}
/* icons */ /* icons */
.default-icon { .default-icon {
@ -42,58 +56,11 @@
} }
/* labels */
.label {
@apply text-gray-600;
}
/* errors */ /* errors */
.error-text { .error-text {
@apply text-sm text-red-600; @apply text-sm text-red-600;
} }
/* buttons */
.basic-button {
@apply px-4 py-2 shadow-md rounded-lg whitespace-nowrap disabled:opacity-50 disabled:cursor-not-allowed;
}
.default-button-bg {
@apply bg-gray-300 hover:bg-gray-400;
}
.default-button-text {
@apply text-gray-600;
}
.primary-button-bg {
@apply bg-blue-300 hover:bg-blue-400;
}
.primary-button-text {
@apply text-gray-600;
}
.dark-button-bg {
@apply bg-gray-600 hover:bg-gray-800;
}
.dark-button-text {
@apply text-white;
}
.transparent-button-bg {
@apply bg-transparent hover:bg-transparent;
}
.transparent-button-text {
@apply text-gray-600;
}
/* input fields like input and textarea */
.input-field {
@apply p-2 w-full border rounded-md bg-white placeholder-gray-400 border-gray-600 hover:border-blue-800 transition-colors text-gray-600 focus:outline-none focus:border-blue-900;
}
/* groups */ /* groups */
.button-group { .button-group {
@apply flex gap-4 mt-8; @apply flex gap-4 mt-8;

View file

@ -1,81 +1,80 @@
import { useState } from "react"; import {useState} from "react";
import Button from "./basics/Button"; import Button from "./basics/Button";
import type { LoginRequestDto } from "../api/dtos/LoginRequestDto"; import type {LoginRequestDto} from "../api/dtos/LoginRequestDto";
import type { LoginResponseDto } from "../api/dtos/LoginResponseDto"; import type {LoginResponseDto} from "../api/dtos/LoginResponseDto";
import { login } from "../api/points/AuthPoint"; import {login} from "../api/points/AuthPoint";
import { getRecipeListUrl } from "../routes"; 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";
export default function LoginPage() { export default function LoginPage() {
const [userName, setUserName] = useState<string>(""); const [userName, setUserName] = useState<string>("");
const [password, setPassword] = useState<string>(""); const [password, setPassword] = useState<string>("");
const [errorMessage, setErrorMessage] = useState<string | null>(null); const [errorMessage, setErrorMessage] = useState<string | null>(null);
const navigate = useNavigate(); const navigate = useNavigate();
/** /**
* Login * Login
*/ */
const executeLogin = async () => { const executeLogin = async () => {
const dto: LoginRequestDto = { const dto: LoginRequestDto = {
userName, userName,
password, password,
};
try {
console.log("Trying to log in with " + dto.userName);
const loginResponse: LoginResponseDto = await login(dto);
localStorage.setItem("session", JSON.stringify(loginResponse));
console.log("Successfully logged in as " + loginResponse.userData?.userName);
setErrorMessage(null);
// navigate to recipe list after successful login
navigate(getRecipeListUrl());
} catch (err: any) {
console.error("Login failed:", err);
setErrorMessage("Login fehlgeschlagen! Bitte überprüfe Benutzername und Passwort.");
}
}; };
try { /**
console.log("Trying to log in with " + dto.userName); * Catch key events in order to trigger login on enter
const loginResponse: LoginResponseDto = await login(dto); */
localStorage.setItem("session", JSON.stringify(loginResponse)); const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
console.log("Successfully logged in as " + loginResponse.userData?.userName); if (event.key === "Enter") {
setErrorMessage(null); executeLogin();
// navigate to recipe list after successful login }
navigate(getRecipeListUrl()); };
} catch (err: any) {
console.error("Login failed:", err);
setErrorMessage("Login fehlgeschlagen! Bitte überprüfe Benutzername und Passwort.");
}
};
/** return (
* Catch key events in order to trigger login on enter <div className="app-bg">
*/ <div className="flex flex-col gap-3 max-w-sm w-full mx-auto p-6 bg-white rounded-2xl shadow-md">
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => { <h1 className="text-center mb-2">Anmeldung</h1>
if (event.key === "Enter") {
executeLogin();
}
};
return ( <input
<div className="app-bg"> placeholder="Benutzername"
<div className="flex flex-col gap-3 max-w-sm w-full mx-auto p-6 bg-white rounded-2xl shadow-md"> value={userName}
<h2 className="content-title text-center mb-2">Anmeldung</h2> onChange={(e) => setUserName(e.target.value)}
onKeyDown={handleKeyDown}
/>
<input <PasswordField
className="input-field" onPasswordChanged={setPassword}
placeholder="Benutzername" onKeyDown={handleKeyDown}
value={userName} />
onChange={(e) => setUserName(e.target.value)}
onKeyDown={handleKeyDown}
/>
<PasswordField {/* error message */}
onPasswordChanged = {setPassword} {errorMessage && (
onKeyDown={handleKeyDown} <p className="error-text text-center">{errorMessage}</p>
/> )}
{/* error message */} <Button
{errorMessage && ( buttonType={ButtonType.PrimaryButton}
<p className="error-text text-center">{errorMessage}</p> text="Login"
)} onClick={executeLogin}
/>
<Button </div>
buttonType={ButtonType.PrimaryButton} </div>
text="Login" );
onClick={executeLogin}
/>
</div>
</div>
);
} }

View file

@ -1,38 +1,39 @@
import type { LucideIcon } from "lucide-react"; import type {LucideIcon} from "lucide-react";
/** /**
* Basic definitions used by all Button types, such as Button.tsx and ButtonLink.tsx * Basic definitions used by all Button types, such as Button.tsx and ButtonLink.tsx
*/ */
export type BasicButtonProps = { export type BasicButtonProps = {
/** Optional Lucide icon (e.g. Plus, X, Check) */ /** Optional Lucide icon (e.g. Plus, X, Check) */
icon?: LucideIcon; icon?: LucideIcon;
text?: string; text?: string;
buttonType?: ButtonType; buttonType?: ButtonType;
/** Optional additional style */ /** Optional additional style */
className?: string; className?: string;
} }
export const basicButtonStyle = "px-4 py-2 shadow-md rounded-lg whitespace-nowrap disabled:opacity-50 disabled:cursor-not-allowed"
/** /**
* Define button types here. * Define button types here.
* Export as enum like class. * Export as enum like class.
*/ */
export const ButtonType = { export const ButtonType = {
DarkButton: { DarkButton: {
textColor: "dark-button-text", textColor: "text-white",
backgroundColor: "dark-button-bg", backgroundColor: "bg-gray-600 hover:bg-gray-800",
}, },
PrimaryButton: { PrimaryButton: {
textColor: "primary-button-text", textColor: "text-gray-600",
backgroundColor: "primary-button-bg", backgroundColor: "bg-blue-300 hover:bg-blue-400",
}, },
DefaultButton: { DefaultButton: {
textColor: "default-button-text", textColor: "text-gray-600",
backgroundColor: "default-button-bg", backgroundColor: "bg-gray-300 hover:bg-gray-400",
}, },
TransparentButton: { TransparentButton: {
textColor: "transparent-button-text", textColor: "text-gray-600",
backgroundColor: "transparent-button-bg", backgroundColor: "bg-transparent hover:bg-transparent",
}, },
} as const; } as const;
export type ButtonType = typeof ButtonType[keyof typeof ButtonType]; export type ButtonType = typeof ButtonType[keyof typeof ButtonType];

View file

@ -1,5 +1,5 @@
import {defaultIconSize} from "./SvgIcon"; import {defaultIconSize} from "./SvgIcon";
import {type BasicButtonProps, ButtonType} from "./BasicButtonDefinitions"; import {type BasicButtonProps, basicButtonStyle, ButtonType} from "./BasicButtonDefinitions";
type ButtonProps = BasicButtonProps & { type ButtonProps = BasicButtonProps & {
onClick: () => void; onClick: () => void;
@ -20,7 +20,7 @@ export default function Button({
}: ButtonProps) { }: ButtonProps) {
return ( return (
<button <button
className={`basic-button bg-primary-button-bg ${buttonType.backgroundColor} ${buttonType.textColor} ${className}`} className={`${basicButtonStyle} ${buttonType.backgroundColor} ${buttonType.textColor} ${className}`}
onClick={onClick} onClick={onClick}
disabled={disabled} disabled={disabled}
{...props} {...props}

View file

@ -1,37 +1,37 @@
import { Link, type LinkProps } from "react-router-dom" import {Link, type LinkProps} from "react-router-dom"
import { defaultIconSize } from "./SvgIcon" import {defaultIconSize} from "./SvgIcon"
import { ButtonType, type BasicButtonProps } from "./BasicButtonDefinitions" import {type BasicButtonProps, basicButtonStyle, ButtonType} from "./BasicButtonDefinitions"
type ButtonLinkProps = LinkProps & BasicButtonProps type ButtonLinkProps = LinkProps & BasicButtonProps
& { & {
to: string to: string
} }
/** /**
* Link component having the same stile as the button * Link component having the same stile as the button
*/ */
export default function ButtonLink({ export default function ButtonLink({
to, to,
text, text,
icon: Icon, icon: Icon,
buttonType = ButtonType.DefaultButton, buttonType = ButtonType.DefaultButton,
className = "", className = "",
...props ...props
}: ButtonLinkProps) { }: ButtonLinkProps) {
return ( return (
<Link <Link
to={to} to={to}
className={`basic-button ${buttonType.backgroundColor} ${buttonType.textColor} ${className}`} className={`${basicButtonStyle} ${buttonType.backgroundColor} ${buttonType.textColor} ${className}`}
{...props} {...props}
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{Icon && ( {Icon && (
<Icon <Icon
size={defaultIconSize} size={defaultIconSize}
/> />
)} )}
{text} {text}
</div> </div>
</Link> </Link>
) )
} }

View file

@ -59,7 +59,7 @@ export function NumberStepControl({
pattern="[0-9]*" pattern="[0-9]*"
value={value} value={value}
onChange={handleInputChange} onChange={handleInputChange}
className="w-16 text-center input-field" className="w-16 text-center"
/> />
<button <button

View file

@ -1,41 +1,41 @@
import { Eye, EyeOff } from "lucide-react"; import {Eye, EyeOff} from "lucide-react";
import { useState } from "react"; import {useState} from "react";
import { defaultIconSize } from "./SvgIcon"; import {defaultIconSize} from "./SvgIcon";
type PasswordFieldProps = { type PasswordFieldProps = {
onPasswordChanged: (password : string) => void onPasswordChanged: (password: string) => void
onKeyDown: (event: React.KeyboardEvent<HTMLInputElement>) => void onKeyDown: (event: React.KeyboardEvent<HTMLInputElement>) => void
} }
/** /**
* Password field component * Password field component
*/ */
export default function PasswordField({onPasswordChanged, onKeyDown} : PasswordFieldProps){ export default function PasswordField({onPasswordChanged, onKeyDown}: PasswordFieldProps) {
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
const [password, setPassword] = useState<string>(""); const [password, setPassword] = useState<string>("");
const changePassword = (password : string) => { const changePassword = (password: string) => {
setPassword(password); setPassword(password);
onPasswordChanged(password) onPasswordChanged(password)
} }
return ( return (
<div className="relative"> <div className="relative">
<input <input
className="input-field pr-10" className="pr-10"
type={showPassword ? "text" : "password"} type={showPassword ? "text" : "password"}
placeholder="Passwort" placeholder="Passwort"
value={password} value={password}
onChange={(e) => changePassword(e.target.value)} onChange={(e) => changePassword(e.target.value)}
onKeyDown={onKeyDown} onKeyDown={onKeyDown}
/> />
{/* Add a little eye icon to the right for showing password */} {/* Add a little eye icon to the right for showing password */}
<button <button
type="button" type="button"
onClick={() => setShowPassword(!showPassword)} onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-700" className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-700"
aria-label={showPassword ? "Passwort ausblenden" : "Passwort anzeigen"} aria-label={showPassword ? "Passwort ausblenden" : "Passwort anzeigen"}
> >
{showPassword ? <EyeOff size={defaultIconSize} /> : <Eye size={defaultIconSize} />} {showPassword ? <EyeOff size={defaultIconSize}/> : <Eye size={defaultIconSize}/>}
</button> </button>
</div> </div>
); );

View file

@ -1,21 +1,22 @@
import { useState } from "react"; import {useState} from "react";
import { Search, X} from "lucide-react"; import {Search, X} from "lucide-react";
import { defaultIconSize } from "./SvgIcon"; import {defaultIconSize} from "./SvgIcon";
/** /**
* Custom search field component including a clear search functionality * Custom search field component including a clear search functionality
*/ */
type SearchFieldProps = { type SearchFieldProps = {
onSearchStringChanged: (searchString : string) => void onSearchStringChanged: (searchString: string) => void
} }
/** /**
* @param SearchFieldProps consisting of an initial searchString and an onSearchStringChanged handler * @param SearchFieldProps consisting of an initial searchString and an onSearchStringChanged handler
* @returns Custom search field component * @returns Custom search field component
*/ */
export default function SearchField({onSearchStringChanged} : SearchFieldProps){ export default function SearchField({onSearchStringChanged}: SearchFieldProps) {
const [currentSearchString, setCurrentSearchString] = useState<string>("") const [currentSearchString, setCurrentSearchString] = useState<string>("")
const changeSearchString = (newSearchString : string) => { const changeSearchString = (newSearchString: string) => {
console.log(newSearchString); console.log(newSearchString);
setCurrentSearchString(newSearchString); setCurrentSearchString(newSearchString);
onSearchStringChanged(newSearchString) onSearchStringChanged(newSearchString)
@ -23,34 +24,34 @@ export default function SearchField({onSearchStringChanged} : SearchFieldProps){
return ( return (
<div className="relative"> <div className="relative">
{/* Input of searchfield {/* Input of searchfield
Defines border and behavior. Requires extra padding at both sides to Defines border and behavior. Requires extra padding at both sides to
accommodate the icons accommodate the icons
*/} */}
<input <input
className="input-field pl-10 pr-10" className="pl-10 pr-10"
type="text" type="text"
placeholder="Search" placeholder="Search"
value={currentSearchString} value={currentSearchString}
onChange={ e => changeSearchString(e.target.value) } onChange={e => changeSearchString(e.target.value)}
/> />
{/* Right icon: X {/* Right icon: X
Clears search string on click Clears search string on click
*/} */}
<button <button
className="absolute right-0 inset-y-0 flex items-center -ml-1 mr-3 default-icon" className="absolute right-0 inset-y-0 flex items-center -ml-1 mr-3 default-icon"
onClick = { () => changeSearchString("") } onClick={() => changeSearchString("")}
> >
<X <X
size = {defaultIconSize} size={defaultIconSize}
/> />
</button> </button>
{/* Left icon: Looking glass */} {/* Left icon: Looking glass */}
<div className="absolute left-0 inset-y-0 flex items-center ml-3 default-icon"> <div className="absolute left-0 inset-y-0 flex items-center ml-3 default-icon">
<Search <Search
size = {defaultIconSize} size={defaultIconSize}
/> />
</div>
</div> </div>
</div>
) )
} }

View file

@ -34,13 +34,12 @@ export function IngredientGroupListEditor({ingredientGroupList, onChange}: Ingre
} }
return ( return (
<div> <div>
<h2 className="section-heading">Zutaten</h2> <h2>Zutaten</h2>
<div> <div>
{ingredientGroupList.map((ingGrp, index) => ( {ingredientGroupList.map((ingGrp, index) => (
<div key={index} className="ingredient-group-card mb-4"> <div key={index} className="ingredient-group-card mb-4">
<div className="horizontal-input-group columns-2"> <div className="horizontal-input-group columns-2">
<input <input
className="input-field"
placeholder="Titel (Optional)" placeholder="Titel (Optional)"
value={ingGrp.title} value={ingGrp.title}
onChange={e => handleUpdate(index, "title", e.target.value)} onChange={e => handleUpdate(index, "title", e.target.value)}

View file

@ -57,7 +57,6 @@ export function IngredientListEditor({ingredients, onChange}: IngredientListEdit
<div key={index} className="horizontal-input-group"> <div key={index} className="horizontal-input-group">
<input <input
type="number" type="number"
className="input-field"
placeholder="Menge" placeholder="Menge"
value={ing.amount === undefined || ing.amount === null ? "" : ing.amount} value={ing.amount === undefined || ing.amount === null ? "" : ing.amount}
onChange={e => { onChange={e => {
@ -66,13 +65,12 @@ export function IngredientListEditor({ingredients, onChange}: IngredientListEdit
}} }}
/> />
<input <input
className="input-field w-20" className="w-20"
placeholder="Einheit" placeholder="Einheit"
value={ing.unit ?? ""} value={ing.unit ?? ""}
onChange={e => handleUpdate(index, "unit", e.target.value)} onChange={e => handleUpdate(index, "unit", e.target.value)}
/> />
<input <input
className="input-field"
placeholder="Name" placeholder="Name"
value={ing.name} value={ing.name}
onChange={e => handleUpdate(index, "name", e.target.value)} onChange={e => handleUpdate(index, "name", e.target.value)}

View file

@ -44,7 +44,7 @@ export function InstructionStepDesktopListItem({
</div> </div>
<textarea <textarea
className="input-field w-full min-h-[60px] resize-none overflow-hidden" className="w-full min-h-[60px] resize-none overflow-hidden"
placeholder={`Schritt ${index + 1}`} placeholder={`Schritt ${index + 1}`}
value={step.text} value={step.text}
onChange={(e) => onUpdate(index, e.target.value)} onChange={(e) => onUpdate(index, e.target.value)}

View file

@ -53,7 +53,7 @@ export function InstructionStepListDesktopEditor({
return ( return (
<div> <div>
<h2 className="section-heading mb-2">Zubereitung</h2> <h2 className="mb-2">Zubereitung</h2>
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}> <DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<SortableContext <SortableContext
items={instructionStepList.map((i) => i.internalId)} items={instructionStepList.map((i) => i.internalId)}

View file

@ -43,7 +43,7 @@ export function InstructionStepListMobileEditor({
return ( return (
<div> <div>
<h2 className="section-heading mb-2">Zubereitung</h2> <h2 className="mb-2">Zubereitung</h2>
{/* Instruction list */} {/* Instruction list */}
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">

View file

@ -74,7 +74,7 @@ export function InstructionStepMobileListItem({
{/* Center column: Instruction step */} {/* Center column: Instruction step */}
<textarea <textarea
className="input-field w-full min-h-[120px] resize-none overflow-hidden" className="w-full min-h-[120px] resize-none overflow-hidden"
placeholder={`Schritt ${index + 1}`} placeholder={`Schritt ${index + 1}`}
value={stepModel.text} value={stepModel.text}
onChange={(e) => onUpdate(index, e.target.value)} onChange={(e) => onUpdate(index, e.target.value)}

View file

@ -7,6 +7,7 @@ import ButtonLink from "../basics/ButtonLink"
import {mapRecipeDtoToModel} from "../../mappers/RecipeMapper" import {mapRecipeDtoToModel} from "../../mappers/RecipeMapper"
import {NumberStepControl} from "../basics/NumberStepControl.tsx"; import {NumberStepControl} from "../basics/NumberStepControl.tsx";
import {NumberedListItem} from "../basics/NumberedListItem.tsx"; import {NumberedListItem} from "../basics/NumberedListItem.tsx";
import {ButtonType} from "../basics/BasicButtonDefinitions.ts";
/** /**
@ -81,7 +82,7 @@ export default function RecipeDetailPage() {
<div className="content-bg"> <div className="content-bg">
{/* Header - remains in position when scrolling */} {/* Header - remains in position when scrolling */}
<div className="sticky-header"> <div className="sticky-header">
<h1 className="content-title mb-0 pb-0">{recipeWorkingCopy.title}</h1> <h1>{recipeWorkingCopy.title}</h1>
</div> </div>
{/* Content */} {/* Content */}
@ -111,13 +112,13 @@ export default function RecipeDetailPage() {
</div> </div>
{/* Ingredients */} {/* Ingredients */}
<h2 className="section-heading">Zutaten</h2> <h2>Zutaten</h2>
<ul> <ul>
{recipeWorkingCopy.ingredientGroupList.map((group, i) => ( {recipeWorkingCopy.ingredientGroupList.map((group, i) => (
<div key={i}> <div key={i}>
{/* the title is optional, only print if present */} {/* the title is optional, only print if present */}
{group.title && group.title.trim() !== "" && ( {group.title && group.title.trim() !== "" && (
<h3 className="subsection-heading highlight-container-bg mb-2">{group.title}</h3> <h3 className="highlight-container-bg mb-2">{group.title}</h3>
)} )}
<ul> <ul>
{group.ingredientList.map((ing, j) => ( {group.ingredientList.map((ing, j) => (
@ -131,7 +132,7 @@ export default function RecipeDetailPage() {
</ul> </ul>
{/* Instructions */} {/* Instructions */}
<h2 className="section-heading">Zubereitung</h2> <h2>Zubereitung</h2>
<ol className="space-y-4"> <ol className="space-y-4">
{recipe.instructionStepList.map((step, j) => ( {recipe.instructionStepList.map((step, j) => (
<NumberedListItem key={j} index={j} text={step.text}/> <NumberedListItem key={j} index={j} text={step.text}/>
@ -142,12 +143,11 @@ export default function RecipeDetailPage() {
<div className="button-group"> <div className="button-group">
<ButtonLink <ButtonLink
to={recipe.id !== undefined ? getRecipeEditUrl(recipe.id) : getRecipeListUrl()} // @todo show error instead to={recipe.id !== undefined ? getRecipeEditUrl(recipe.id) : getRecipeListUrl()} // @todo show error instead
className="basic-button primary-button-bg primary-button-text" buttonType={ButtonType.PrimaryButton}
text="Bearbeiten" text="Bearbeiten"
/> />
<ButtonLink <ButtonLink
to={getRecipeListUrl()} to={getRecipeListUrl()}
className="basic-button default-button-bg default-button-text"
text="Zurueck" text="Zurueck"
/> />
</div> </div>

View file

@ -89,25 +89,25 @@ export default function RecipeEditor({recipe, onSave, onCancel}: RecipeEditorPro
{/* Container defining the maximum width of the content */} {/* Container defining the maximum width of the content */}
<div className="content-bg"> <div className="content-bg">
<h1 className="content-title border-b-2 border-gray-300"> <h1 className="border-b-2 border-gray-300">
{recipe.id ? "Rezept bearbeiten" : "Neues Rezept"} {recipe.id ? "Rezept bearbeiten" : "Neues Rezept"}
</h1> </h1>
<div className="content-container"> <div className="content-container">
{/* Title */} {/* Title */}
<h2 className="section-heading">Titel</h2> <h2>Titel</h2>
<input <input
className={`input-field ${errors.title ? "error-text" : ""}`} className={`${errors.title ? "error-text" : ""}`}
placeholder="Titel" placeholder="Titel"
value={draft.title} value={draft.title}
onChange={e => setDraft({...draft, title: e.target.value})} onChange={e => setDraft({...draft, title: e.target.value})}
/> />
{/* Servings */} {/* Servings */}
<h2 className="section-heading">Portionen</h2> <h2>Portionen</h2>
<div className="columns-3 gap-2 flex items-center"> <div className="columns-3 gap-2 flex items-center">
<label>Für</label> <label>Für</label>
<input <input
type="number" type="number"
className="input-field w-20" className="w-20"
placeholder="1" placeholder="1"
value={draft.servings.amount} value={draft.servings.amount}
onChange={e => { onChange={e => {
@ -117,7 +117,6 @@ export default function RecipeEditor({recipe, onSave, onCancel}: RecipeEditorPro
}} }}
/> />
<input <input
className="input-field"
placeholder="Personen" placeholder="Personen"
value={draft.servings.unit} value={draft.servings.unit}
onChange={e => { onChange={e => {

View file

@ -46,7 +46,7 @@ export default function RecipeListPage() {
<div className="content-bg"> <div className="content-bg">
{/* Header - remains in position when scrolling */} {/* Header - remains in position when scrolling */}
<div className="sticky-header"> <div className="sticky-header">
<h1 className="content-title">Recipes</h1> <h1>Recipes</h1>
<RecipeListToolbar <RecipeListToolbar
onAddClicked={handleAdd} onAddClicked={handleAdd}
onSearchStringChanged={setSearchString} onSearchStringChanged={setSearchString}