diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 22dffc4..5923390 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,47 +1,36 @@ -import {BrowserRouter as Router, Route, Routes} from "react-router-dom" +import { BrowserRouter as Router, Routes, Route } from "react-router-dom" import RecipeDetailPage from "./components/recipes/RecipeDetailPage" import RecipeEditPage from "./components/recipes/RecipeEditPage" import RecipeListPage from "./components/recipes/RecipeListPage" -import { - getLoginUrlDefinition, - getRecipeAddUrlDefinition, - getRecipeDetailsUrlDefinition, - getRecipeEditUrlDefinition, - getRecipeListUrlDefinition, - getUserUrlDefinition -} from "./routes" +import { getLoginUrl, getRecipeAddUrlDefinition, getRecipeDetailsUrlDefinition, getRecipeEditUrlDefinition, getRecipeListUrlDefinition, getRootUrlDefinition } from "./routes" import "./App.css" import LoginPage from "./components/LoginPage" -import UserManagementPage from "./components/users/UserManagementPage.tsx"; /** * Main application component. * Defines routes for the recipe list, detail view, and edit form. */ function App() { - return ( - - - {/* Login page */} - }/> + return ( + + + {/* Login page */} + }/> + {/* Home page: list of recipes */} + } /> - {/* Home page: list of recipes */} - }/> + {/* Detail page: shows one recipe */} + } /> - {/* Detail page: shows one recipe */} - }/> + {/* Edit page: form to edit a recipe */} + } /> - {/* Edit page: form to edit a recipe */} - }/> - - {/* Add page: form to add a recipe */} - }/> - {/*User management */} - }/> - - - ) + {/* Add page: form to add a recipe */} + } /> + + + ) } export default App \ No newline at end of file diff --git a/frontend/src/api/apiClient.ts b/frontend/src/api/apiClient.ts deleted file mode 100644 index a7185f3..0000000 --- a/frontend/src/api/apiClient.ts +++ /dev/null @@ -1,97 +0,0 @@ -/** - * A centralized API client that wraps around the native fetch API. - * It automatically handles authentication headers, JSON parsing, and error handling. - * - * This client is used across the app for interacting with backend endpoints. - * - * @todo modify all endpoints to use api client and remove old utils - */ - -const API_BASE_URL = import.meta.env.VITE_API_BASE - -/** - * Helper: Reads the JWT token from localStorage (if available) - */ -function getAuthToken(): string | null { - try { - const sessionData = localStorage.getItem("session"); - if (!sessionData) return null; - - const parsed = JSON.parse(sessionData); - return parsed.token ?? null; - } catch (err) { - console.error("Failed to parse token from localStorage:", err); - return null; - } -} - -/** - * Custom error class for API responses. - */ -export class ApiError extends Error { - public status: number; - public details?: string; - - constructor(message: string, status: number, details?: string) { - super(message); - this.status = status; - this.details = details; - } -} - -/** - * Wrapper for making API requests. - * Handles JSON encoding/decoding and authorization headers automatically. - * - * @param endpoint - API endpoint (e.g. "user/me") - * @param options - fetch configuration (method, headers, body) - * @returns parsed JSON response - */ -export async function apiRequest( - endpoint: string, - options: RequestInit = {} -): Promise { - const token = getAuthToken(); - - const headers: HeadersInit = { - "Content-Type": "application/json", - ...(token ? {Authorization: `Bearer ${token}`} : {}), - ...options.headers, - }; - - const response = await fetch(`${API_BASE_URL}${endpoint}`, { - ...options, - headers, - }); - - // Handle non-OK responses gracefully - if (!response.ok) { - let details; - try { - details = await response.json(); - } catch { - details = await response.text(); - } - throw new ApiError(`Request to ${endpoint} failed`, response.status, details); - } - - // Handle empty response bodies - if (response.status === 204) { - return {} as T; - } - - return response.json() as Promise; -} - -/** - * Shorthand helpers for convenience. - */ -export const apiClient = { - get: (endpoint: string) => apiRequest(endpoint, {method: "GET"}), - post: (endpoint: string, body: object) => - apiRequest(endpoint, {method: "POST", body: JSON.stringify(body)}), - put: (endpoint: string, body: object) => - apiRequest(endpoint, {method: "PUT", body: JSON.stringify(body)}), - delete: (endpoint: string) => - apiRequest(endpoint, {method: "DELETE"}), -}; diff --git a/frontend/src/api/dtos/ChangeUserPasswordRequest.ts b/frontend/src/api/dtos/ChangeUserPasswordRequest.ts deleted file mode 100644 index e7f822e..0000000 --- a/frontend/src/api/dtos/ChangeUserPasswordRequest.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * DTO for changing user password - */ -export class ChangeUserPasswordRequest { - userId?: string; - password?: string; -} \ No newline at end of file diff --git a/frontend/src/api/dtos/CreateUserRequest.ts b/frontend/src/api/dtos/CreateUserRequest.ts deleted file mode 100644 index 0aa0464..0000000 --- a/frontend/src/api/dtos/CreateUserRequest.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type {UserDto} from "./UserDto.js"; - -/** - * DTO used for user creation - */ -export class CreateUserRequest { - userData?: UserDto; - password?: string; -} \ No newline at end of file diff --git a/frontend/src/api/dtos/CreateUserResponse.ts b/frontend/src/api/dtos/CreateUserResponse.ts deleted file mode 100644 index 09408c1..0000000 --- a/frontend/src/api/dtos/CreateUserResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { UserDto } from "./UserDto.js"; - -export class CreateUserResponse{ - userData?: UserDto; -} \ No newline at end of file diff --git a/frontend/src/api/dtos/LoginRequest.ts b/frontend/src/api/dtos/LoginRequestDto.ts similarity index 71% rename from frontend/src/api/dtos/LoginRequest.ts rename to frontend/src/api/dtos/LoginRequestDto.ts index 70d1e09..a3da6ee 100644 --- a/frontend/src/api/dtos/LoginRequest.ts +++ b/frontend/src/api/dtos/LoginRequestDto.ts @@ -1,7 +1,7 @@ /** * Defines a login request */ -export class LoginRequest { +export class LoginRequestDto { userName?: string; password?: string; } \ No newline at end of file diff --git a/frontend/src/api/dtos/LoginResponse.ts b/frontend/src/api/dtos/LoginResponse.ts deleted file mode 100644 index 1e571ed..0000000 --- a/frontend/src/api/dtos/LoginResponse.ts +++ /dev/null @@ -1,10 +0,0 @@ -import {UserDto} from "./UserDto.js"; - -/** - * Response to a successful login - */ -export class LoginResponse { - userData?: UserDto; - token?: string; - expiryDate?: Date; -} \ No newline at end of file diff --git a/frontend/src/api/dtos/LoginResponseDto.ts b/frontend/src/api/dtos/LoginResponseDto.ts new file mode 100644 index 0000000..fdb5a39 --- /dev/null +++ b/frontend/src/api/dtos/LoginResponseDto.ts @@ -0,0 +1,10 @@ +import { UserDto } from "./UserDto.js"; + +/** + * Response to a successful login + */ +export class LoginResponseDto { + userData?: UserDto; + token?: string; + expiryDate? : Date; +} \ No newline at end of file diff --git a/frontend/src/api/dtos/UserDto.ts b/frontend/src/api/dtos/UserDto.ts index 3aaa4c6..972cad6 100644 --- a/frontend/src/api/dtos/UserDto.ts +++ b/frontend/src/api/dtos/UserDto.ts @@ -1,10 +1,9 @@ -import {AbstractDto} from "./AbstractDto"; -import {UserRole} from "../enums/UserRole"; +import { AbstractDto } from "./AbstractDto.ts"; -export interface UserDto extends AbstractDto { +export class UserDto extends AbstractDto { firstName?: string; lastName?: string; - userName: string; - email: string; - role?: UserRole; + userName!: string; + email!: string; + role?: string; } \ No newline at end of file diff --git a/frontend/src/api/dtos/UserListResponse.ts b/frontend/src/api/dtos/UserListResponse.ts deleted file mode 100644 index ed6b669..0000000 --- a/frontend/src/api/dtos/UserListResponse.ts +++ /dev/null @@ -1,8 +0,0 @@ -import {UserDto} from "./UserDto.js"; - -/** - * API response for delivering a list of users - */ -export class UserListResponse { - valueList: UserDto[] = []; -} \ No newline at end of file diff --git a/frontend/src/api/endpoints/AuthRestResource.ts b/frontend/src/api/endpoints/AuthRestResource.ts deleted file mode 100644 index 603eb1c..0000000 --- a/frontend/src/api/endpoints/AuthRestResource.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type {LoginRequest} from "../dtos/LoginRequest.ts"; -import type {LoginResponse} from "../dtos/LoginResponse.ts"; -import {postJson} from "../utils/requests"; - - -/** - * Util for handling the recipe api - */ -// read base url from .env file -const BASE_URL = import.meta.env.VITE_API_BASE; - -/** - * URL for handling recipes - */ -const AUTH_URL = `${BASE_URL}/auth` - - -/** - * Login to recipe app - * @param loginRequest Login Requets - * @returns LoginResponse - */ -export async function login(loginRequest: LoginRequest): Promise { - const res = await postJson(`${AUTH_URL}/login`, JSON.stringify(loginRequest), false); - return res.json(); -} diff --git a/frontend/src/api/endpoints/UserRestResource.ts b/frontend/src/api/endpoints/UserRestResource.ts deleted file mode 100644 index 02d0695..0000000 --- a/frontend/src/api/endpoints/UserRestResource.ts +++ /dev/null @@ -1,26 +0,0 @@ -import {apiClient} from "../apiClient"; -import type {UserDto} from "../dtos/UserDto"; -import type {CreateUserRequest} from "../dtos/CreateUserRequest.ts"; -import type {ChangeUserPasswordRequest} from "../dtos/ChangeUserPasswordRequest.ts"; -import type {CreateUserResponse} from "../dtos/CreateUserResponse.ts"; -import type {UserListResponse} from "../dtos/UserListResponse.ts"; - -export async function fetchCurrentUser(): Promise { - return apiClient.get("/user/me"); -} - -export async function fetchAllUsers(): Promise { - return apiClient.get("/user/all"); -} - -export async function createUser(dto: CreateUserRequest): Promise { - return apiClient.post("/user/create", dto); -} - -export async function updateUser(dto: UserDto): Promise { - return apiClient.post("/user/update", dto); -} - -export async function changePassword(dto: ChangeUserPasswordRequest) { - return apiClient.post("/user/change-password", dto); -} diff --git a/frontend/src/api/enums/UserRole.ts b/frontend/src/api/enums/UserRole.ts deleted file mode 100644 index d4f5f53..0000000 --- a/frontend/src/api/enums/UserRole.ts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * User roles - Frontend version - * Content should match backend enum exactly - * However, we cannot use a basic enum here as we're using erasableSyntaxOnly. - */ -export const UserRole = { - USER: "user", - ADMIN: "admin", -} as const; - -// Create a type from the values -export type UserRole = typeof UserRole[keyof typeof UserRole]; - -/** - * Helper functions for UserRole in frontend - */ -export const UserRoleHelper = { - /** - * Get display name for role (German) - */ - getDisplayName(role: UserRole): string { - const displayNames: Record = { - [UserRole.USER]: "Benutzer", - [UserRole.ADMIN]: "Administrator", - }; - return displayNames[role]; - }, - - /** - * Get all roles with display names for dropdowns - */ - getRoleOptions(): Array<{ value: UserRole; label: string }> { - return Object.values(UserRole).map((role) => ({ - value: role, - label: this.getDisplayName(role), - })); - }, - - /** - * Check if a string is a valid role - */ - isValidRole(value: string): value is UserRole { - return Object.values(UserRole).includes(value as UserRole); - }, - - /** - * Get all role values - */ - getAllRoles(): UserRole[] { - return Object.values(UserRole); - }, -}; \ No newline at end of file diff --git a/frontend/src/api/points/AuthPoint.ts b/frontend/src/api/points/AuthPoint.ts new file mode 100644 index 0000000..bf34abd --- /dev/null +++ b/frontend/src/api/points/AuthPoint.ts @@ -0,0 +1,26 @@ +import type { LoginRequestDto } from "../dtos/LoginRequestDto"; +import type { LoginResponseDto } from "../dtos/LoginResponseDto"; +import { postJson } from "../utils/requests"; + + +/** + * Util for handling the recipe api + */ +// read base url from .env file +const BASE_URL = import.meta.env.VITE_API_BASE; + +/** + * URL for handling recipes + */ +const AUTH_URL = `${BASE_URL}/auth` + + +/** + * Create new Recipe + * @param recipe Recipe to create + * @returns Saved recipe + */ +export async function login(requestDto: LoginRequestDto): Promise { + const res = await postJson(`${AUTH_URL}/login`, JSON.stringify(requestDto), false); + return res.json(); +} diff --git a/frontend/src/api/endpoints/CompactRecipeRestResource.ts b/frontend/src/api/points/CompactRecipePoint.ts similarity index 100% rename from frontend/src/api/endpoints/CompactRecipeRestResource.ts rename to frontend/src/api/points/CompactRecipePoint.ts diff --git a/frontend/src/api/endpoints/RecipeRestResource.ts b/frontend/src/api/points/RecipePoint.ts similarity index 100% rename from frontend/src/api/endpoints/RecipeRestResource.ts rename to frontend/src/api/points/RecipePoint.ts diff --git a/frontend/src/components/LoginPage.tsx b/frontend/src/components/LoginPage.tsx index d860fbc..9f6f054 100644 --- a/frontend/src/components/LoginPage.tsx +++ b/frontend/src/components/LoginPage.tsx @@ -1,8 +1,8 @@ import {useState} from "react"; import Button from "./basics/Button"; -import type {LoginRequest} from "../api/dtos/LoginRequest.ts"; -import type {LoginResponse} from "../api/dtos/LoginResponse.ts"; -import {login} from "../api/endpoints/AuthRestResource.ts"; +import type {LoginRequestDto} from "../api/dtos/LoginRequestDto"; +import type {LoginResponseDto} from "../api/dtos/LoginResponseDto"; +import {login} from "../api/points/AuthPoint"; import {getRecipeListUrl} from "../routes"; import {useNavigate} from "react-router-dom"; import PasswordField from "./basics/PasswordField"; @@ -20,14 +20,14 @@ export default function LoginPage() { * Login */ const executeLogin = async () => { - const dto: LoginRequest = { + const dto: LoginRequestDto = { userName, password, }; try { console.log("Trying to log in with " + dto.userName); - const loginResponse: LoginResponse = await login(dto); + const loginResponse: LoginResponseDto = await login(dto); localStorage.setItem("session", JSON.stringify(loginResponse)); console.log("Successfully logged in as " + loginResponse.userData?.userName); setErrorMessage(null); diff --git a/frontend/src/components/SettingsMenu.tsx b/frontend/src/components/SettingsMenu.tsx deleted file mode 100644 index 2a9d825..0000000 --- a/frontend/src/components/SettingsMenu.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import {X} from "lucide-react"; -import {useNavigate} from "react-router-dom"; -import type {LoginResponse} from "../api/dtos/LoginResponse.ts"; -import {getUserUrl} from "../routes.ts"; -import Button from "./basics/Button.tsx"; -import {ButtonType} from "./basics/BasicButtonDefinitions.ts"; -import type {UserDto} from "../api/dtos/UserDto.ts"; - -/** - * Overlay settings menu that displays current user info - * and provides navigation to user management. - */ -type SettingsMenuProps = { - onClose: () => void; -}; - -export function SettingsMenu({onClose}: SettingsMenuProps) { - const navigate = useNavigate(); - const storedSession = localStorage.getItem("session"); - const loginData: LoginResponse | null = storedSession - ? JSON.parse(storedSession) - : null; - const user = loginData?.userData; - - const formatUserName = (user: UserDto) => { - const parts = []; - if (user.firstName) parts.push(user.firstName); - if (user.lastName) parts.push(user.lastName); - return parts.length > 0 ? parts.join(" ") : user.userName; - }; - - return ( -
-
- {/* Close button */} - - -

Einstellungen

- - {user ? ( - <> -

- {formatUserName(user)} -

-

- {user.role === "admin" ? "Administrator" : "Benutzer"} -

- -
-
- ); -} diff --git a/frontend/src/components/basics/BasicButtonDefinitions.ts b/frontend/src/components/basics/BasicButtonDefinitions.ts index a5c6895..1c4a678 100644 --- a/frontend/src/components/basics/BasicButtonDefinitions.ts +++ b/frontend/src/components/basics/BasicButtonDefinitions.ts @@ -22,10 +22,6 @@ export const ButtonType = { textColor: "text-white", backgroundColor: "bg-gray-600 hover:bg-gray-800", }, - LightButton: { - textColor: "text-gray-600", - backgroundColor: "bg-white hover:bg-gray-300", - }, PrimaryButton: { textColor: "text-gray-600", backgroundColor: "bg-blue-300 hover:bg-blue-400", diff --git a/frontend/src/components/basics/ButtonGroupLayout.tsx b/frontend/src/components/basics/ButtonGroupLayout.tsx index 0278d1a..30bb688 100644 --- a/frontend/src/components/basics/ButtonGroupLayout.tsx +++ b/frontend/src/components/basics/ButtonGroupLayout.tsx @@ -18,7 +18,7 @@ export default function ButtonGroupLayout({children, className, ...rest}: Button
diff --git a/frontend/src/components/basics/CircularIconButton.tsx b/frontend/src/components/basics/CircularIconButton.tsx index e6ca8c4..3089235 100644 --- a/frontend/src/components/basics/CircularIconButton.tsx +++ b/frontend/src/components/basics/CircularIconButton.tsx @@ -6,7 +6,6 @@ import clsx from "clsx"; type CircularIconButtonProps = { icon: LucideIcon; onClick: () => void; - buttonType?: ButtonType disabled?: boolean; className?: string; }; @@ -18,16 +17,14 @@ export default function CircularIconButton({ onClick, icon: Icon, className = "", - buttonType = ButtonType.PrimaryButton, disabled = false, ...props }: CircularIconButtonProps) { return ( -
- -

{message}

- -
- -
- - - ); -} \ No newline at end of file diff --git a/frontend/src/components/basics/NumberCircle.tsx b/frontend/src/components/basics/NumberCircle.tsx index d3c601b..72cfb6e 100644 --- a/frontend/src/components/basics/NumberCircle.tsx +++ b/frontend/src/components/basics/NumberCircle.tsx @@ -12,18 +12,18 @@ type NumberCircleProps = React.HTMLAttributes & { * Can be used as a standalone indicator or as a drag handle. * Accepts any div attributes (e.g. `className`, `title`, `onClick`, or DnD listeners). */ -export function NumberCircle({number, className, ...rest}: NumberCircleProps) { +export function NumberCircle({ number, className, ...rest }: NumberCircleProps) { return (
- {number} -
- ); + className={clsx( + "flex-shrink-0 w-7 h-7 rounded-full bg-blue-300 text-white", + "flex items-center justify-center shadow-sm", + "cursor-default select-none", + className +)} +> + {number} + +); } diff --git a/frontend/src/components/basics/NumberStepControl.tsx b/frontend/src/components/basics/NumberStepControl.tsx index 71f269e..86cde69 100644 --- a/frontend/src/components/basics/NumberStepControl.tsx +++ b/frontend/src/components/basics/NumberStepControl.tsx @@ -50,7 +50,6 @@ export function NumberStepControl({ className)}> @@ -65,7 +64,6 @@ export function NumberStepControl({ diff --git a/frontend/src/components/basics/PageContentLayout.tsx b/frontend/src/components/basics/PageContentLayout.tsx deleted file mode 100644 index 861a5ac..0000000 --- a/frontend/src/components/basics/PageContentLayout.tsx +++ /dev/null @@ -1,50 +0,0 @@ -// src/components/basics/PageContentLayout.tsx -import React, {type ReactNode, useState} from "react"; -import clsx from "clsx"; -import {Settings} from "lucide-react"; -import {SettingsMenu} from "../SettingsMenu"; -import CircularIconButton from "./CircularIconButton.tsx"; -import {ButtonType} from "./BasicButtonDefinitions.ts"; - -/** - * Layout wrapper for the main content area of a page. - * Includes a settings button (top-right) that opens a global settings menu. - */ -type PageContentLayoutProps = React.HTMLAttributes & { - /** The main page content (e.g. header, body) */ - children: ReactNode; -}; - -export default function PageContentLayout({ - children, - className, - ...props - }: PageContentLayoutProps) { - const [isMenuOpen, setIsMenuOpen] = useState(false); - - return ( -
- {/* Settings Button (top-right) - Sticky with higher z-index than header */} - setIsMenuOpen(true)} - buttonType={ButtonType.LightButton} - className="absolute top-4 right-4 w-10 h-10 z-20" - icon={Settings} - /> - - {/* Page content with top padding to accommodate settings button */} -
- {children} -
- - {/* Overlay Settings Menu */} - {isMenuOpen && setIsMenuOpen(false)}/>} -
- ); -} \ No newline at end of file diff --git a/frontend/src/components/basics/PasswordField.tsx b/frontend/src/components/basics/PasswordField.tsx index 169c145..5d27850 100644 --- a/frontend/src/components/basics/PasswordField.tsx +++ b/frontend/src/components/basics/PasswordField.tsx @@ -7,18 +7,12 @@ type PasswordFieldProps = { onPasswordChanged: (password: string) => void onKeyDown: (event: React.KeyboardEvent) => void className?: string - placeholder?: string } /** * Password field component */ -export default function PasswordField({ - onPasswordChanged, - onKeyDown, - placeholder = "Passwort", - className = "" - }: PasswordFieldProps) { +export default function PasswordField({onPasswordChanged, onKeyDown, className = ""}: PasswordFieldProps) { const [showPassword, setShowPassword] = useState(false); const [password, setPassword] = useState(""); @@ -33,7 +27,7 @@ export default function PasswordField({ changePassword(e.target.value)} onKeyDown={onKeyDown} diff --git a/frontend/src/components/basics/SelectField.tsx b/frontend/src/components/basics/SelectField.tsx deleted file mode 100644 index e8c560a..0000000 --- a/frontend/src/components/basics/SelectField.tsx +++ /dev/null @@ -1,47 +0,0 @@ -type SelectFieldProps = { - value: T; - onChange: (value: T) => void; - options: { value: T; label: string }[]; - className?: string; -}; - -/** - * SelectField - A dropdown styled consistently with input fields - * Generic component that works with any string-based type (including string literals and enums) - * - * @example - * // With UserRole type - * - * value={user.role} - * onChange={(role) => setUser({...user, role})} - * options={UserRoleHelper.getRoleOptions()} - * /> - * - * @example - * // With plain strings - * - * value={status} - * onChange={setStatus} - * options={[{value: "active", label: "Active"}, {value: "inactive", label: "Inactive"}]} - * /> - */ -export default function SelectField({ - value, - onChange, - options, - className = '' - }: SelectFieldProps) { - return ( - - ); -} \ No newline at end of file diff --git a/frontend/src/components/basics/TextLinkButton.tsx b/frontend/src/components/basics/TextLinkButton.tsx deleted file mode 100644 index 733abd6..0000000 --- a/frontend/src/components/basics/TextLinkButton.tsx +++ /dev/null @@ -1,20 +0,0 @@ -type TextLinkButtonProps = { - text: string; - onClick: () => void; - className?: string; -}; - -/** - * TextLinkButton - A button styled as a hyperlink - */ -export default function TextLinkButton({text, onClick, className = ''}: TextLinkButtonProps) { - return ( - - ); -} \ No newline at end of file diff --git a/frontend/src/components/recipes/IngredientGroupListEditor.tsx b/frontend/src/components/recipes/IngredientGroupListEditor.tsx index 6cf42af..ddbfb06 100644 --- a/frontend/src/components/recipes/IngredientGroupListEditor.tsx +++ b/frontend/src/components/recipes/IngredientGroupListEditor.tsx @@ -50,7 +50,6 @@ export function IngredientGroupListEditor({ingredientGroupList, onChange}: Ingre {ingredientGroupList.map((ingGrp, index) => ( {/* Container defining the maximum width of the content */} - + {/* Header - remains in position when scrolling */}

{recipeWorkingCopy.title}

@@ -123,7 +123,6 @@ export default function RecipeDetailPage() {
    {recipeWorkingCopy.ingredientGroupList.map((group, i) => ( @@ -151,7 +150,7 @@ export default function RecipeDetailPage() { /> - + ) } diff --git a/frontend/src/components/recipes/RecipeEditor.tsx b/frontend/src/components/recipes/RecipeEditor.tsx index 154bf6c..08f9118 100644 --- a/frontend/src/components/recipes/RecipeEditor.tsx +++ b/frontend/src/components/recipes/RecipeEditor.tsx @@ -7,9 +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"; -import PageContentLayout from "../basics/PageContentLayout.tsx"; type RecipeEditorProps = { recipe: RecipeModel @@ -86,7 +86,7 @@ export function RecipeEditor({recipe, onSave, onCancel}: RecipeEditorProps) { /*Container spanning entire screen used to center content horizontally */ {/* Container defining the maximum width of the content */} - +

    {recipe.id ? "Rezept bearbeiten" : "Neues Rezept"}

    @@ -152,7 +152,7 @@ export function RecipeEditor({recipe, onSave, onCancel}: RecipeEditorProps) { /> -
    +
    ) } diff --git a/frontend/src/components/recipes/RecipeListPage.tsx b/frontend/src/components/recipes/RecipeListPage.tsx index 2dab8dd..765d7d5 100644 --- a/frontend/src/components/recipes/RecipeListPage.tsx +++ b/frontend/src/components/recipes/RecipeListPage.tsx @@ -6,8 +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"; -import PageContentLayout from "../basics/PageContentLayout.tsx"; /** * Displays a list of recipes in a sidebar layout. @@ -46,7 +46,7 @@ export default function RecipeListPage() { /*Container spanning entire screen used to center content horizontally */ {/* Container defining the maximum width of the content */} - + {/* Header - remains in position when scrolling */}

    Recipes

    @@ -69,7 +69,7 @@ export default function RecipeListPage() { ))} -
    +
    ) } diff --git a/frontend/src/components/users/ChangePasswordModal.tsx b/frontend/src/components/users/ChangePasswordModal.tsx deleted file mode 100644 index b480f10..0000000 --- a/frontend/src/components/users/ChangePasswordModal.tsx +++ /dev/null @@ -1,65 +0,0 @@ -// src/components/basics/ChangePasswordModal.tsx -import {useState} from "react"; -import {changePassword} from "../../api/endpoints/UserRestResource.ts"; -import PasswordField from "../basics/PasswordField.tsx"; -import ButtonGroupLayout from "../basics/ButtonGroupLayout.tsx"; -import Button from "../basics/Button.tsx"; -import {ButtonType} from "../basics/BasicButtonDefinitions.ts"; - -type ChangePasswordModalProps = { - userId?: string; - onClose: () => void; -}; - -export function ChangePasswordModal({userId, onClose}: ChangePasswordModalProps) { - const [password, setPassword] = useState(""); - const [confirm, setConfirm] = useState(""); - const [error, setError] = useState(""); - - const handleSave = async () => { - if (password !== confirm) { - setError("Passwörter stimmen nicht überein."); - return; - } - try { - await changePassword({userId, password}); - onClose(); - } catch { - setError("Fehler beim Ändern des Passworts"); - } - }; - - return ( -
    -
    -

    Passwort ändern

    - - - - {error &&

    {error}

    } - - -
    -
    - ); -} diff --git a/frontend/src/components/users/UserEditForm.tsx b/frontend/src/components/users/UserEditForm.tsx deleted file mode 100644 index 06b0c4f..0000000 --- a/frontend/src/components/users/UserEditForm.tsx +++ /dev/null @@ -1,175 +0,0 @@ -import {useEffect, useState} from "react"; -import type {UserDto} from "../../api/dtos/UserDto"; -import Button from "../basics/Button"; -import {ButtonType} from "../basics/BasicButtonDefinitions"; -import PasswordField from "../basics/PasswordField"; -import ButtonGroupLayout from "../basics/ButtonGroupLayout"; -import TextLinkButton from "../basics/TextLinkButton"; -import SelectField from "../basics/SelectField"; -import {UserRole, UserRoleHelper} from "../../api/enums/UserRole.ts"; - -type UserEditFormProps = { - user: UserDto; - isAdmin: boolean; - isSaving: boolean; - onSave: (user: UserDto, password?: string) => Promise; - onCancel: () => void; - onOpenPasswordModal: () => void; -}; - -/** - * UserEditForm - Form for creating/editing a user - */ -export default function UserEditForm({ - user, - isAdmin, - isSaving, - onSave, - onCancel, - onOpenPasswordModal, - }: UserEditFormProps) { - const [editedUser, setEditedUser] = useState({...user}); - const [password, setPassword] = useState(""); - const [confirmPassword, setConfirmPassword] = useState(""); - const [passwordError, setPasswordError] = useState(""); - - /** - * React to changes in selected user. - * - * When a new user is selected, the edit user must be updated and password fields as well as error messege - * must be reset. - */ - useEffect(() => { - setEditedUser({...user}); - setPassword(""); - setConfirmPassword(""); - setPasswordError(""); - }, [user]); - /** - * Calls on save after validating passwords for new user. - */ - const handleSave = async () => { - if (!editedUser.id) { - // New user - validate passwords - if (confirmPassword !== password) { - setPasswordError("Passwörter stimmen nicht überein"); - return; - } - if (!password || password.length === 0) { - setPasswordError("Passwort ist erforderlich"); - return; - } - setPasswordError(""); - await onSave(editedUser, password); - } else { - // Existing user - await onSave(editedUser); - } - }; - - /** - * All available roles - */ - const roleOptions = UserRoleHelper.getRoleOptions(); - - return ( -
    -

    {editedUser.id ? "Benutzer bearbeiten" : "Neuen Benutzer anlegen"}

    -
    - - - setEditedUser({...editedUser, userName: e.target.value}) - } - /> - - - - setEditedUser({...editedUser, firstName: e.target.value}) - } - /> - - - - setEditedUser({...editedUser, lastName: e.target.value}) - } - /> - - - - setEditedUser({...editedUser, email: e.target.value}) - } - /> - - {isAdmin && ( - <> - - - value={editedUser.role ?? UserRole.USER} - onChange={(value) => - setEditedUser({...editedUser, role: value}) - } - options={roleOptions} - /> - - )} - - {/* Show password field only when creating new user */} - {!editedUser.id && ( - <> - - { - }} - /> - - - { - }} - /> - {passwordError &&

    {passwordError}

    } - - )} - - {/* Change password link for existing user */} - {editedUser.id && ( -
    - -
    - )} - - -
    -
    - ); -} \ No newline at end of file diff --git a/frontend/src/components/users/UserList.tsx b/frontend/src/components/users/UserList.tsx deleted file mode 100644 index 3e6b8df..0000000 --- a/frontend/src/components/users/UserList.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import clsx from "clsx"; -import type {UserDto} from "../../api/dtos/UserDto"; - -type UserListProps = { - users: UserDto[]; - selectedUser: UserDto | null; - onSelectUser: (user: UserDto) => void; -}; - -/** - * UserList - Displays a list of users with selection - */ -export default function UserList({users, selectedUser, onSelectUser}: UserListProps) { - - const formatUserName = (user: UserDto) => { - const parts = []; - if (user.lastName) parts.push(user.lastName); - if (user.firstName) parts.push(user.firstName); - return parts.length > 0 ? parts.join(", ") : ""; - }; - - return ( -
    -

    Benutzer

    -
      - {users.map((user) => ( -
    • { - onSelectUser(user) - }} - > -
      - {formatUserName(user)} -
      -
      - {user.userName} -
      -
    • - ))} -
    -
    - ); -} \ No newline at end of file diff --git a/frontend/src/components/users/UserManagementPage.tsx b/frontend/src/components/users/UserManagementPage.tsx deleted file mode 100644 index 82690df..0000000 --- a/frontend/src/components/users/UserManagementPage.tsx +++ /dev/null @@ -1,217 +0,0 @@ -import {useEffect, useState} from "react"; -import {createUser, fetchAllUsers, fetchCurrentUser, updateUser,} from "../../api/endpoints/UserRestResource.ts"; -import type {UserDto} from "../../api/dtos/UserDto"; -import ContentBody from "../basics/ContentBody"; -import StickyHeader from "../basics/StickyHeader"; -import Button from "../basics/Button"; -import {ButtonType} from "../basics/BasicButtonDefinitions"; -import PageContainer from "../basics/PageContainer"; -import {ChangePasswordModal} from "./ChangePasswordModal"; -import ButtonGroupLayout from "../basics/ButtonGroupLayout"; -import {Plus, X} from "lucide-react"; -import ButtonLink from "../basics/ButtonLink"; -import {getRecipeListUrl} from "../../routes"; -import type {CreateUserResponse} from "../../api/dtos/CreateUserResponse"; -import type {UserListResponse} from "../../api/dtos/UserListResponse"; -import UserList from "./UserList"; -import UserEditForm from "./UserEditForm"; -import ErrorPopup from "../basics/ErrorPopup"; -import {UserRole} from "../../api/enums/UserRole.ts"; -import PageContentLayout from "../basics/PageContentLayout.tsx"; - -/** - * UserManagementPage - * ------------------- - * Displays a two-column layout: - * - Left: list of all users - * - Right: edit form for selected or new user - * - * Allows: - * - Admins to manage all users (add/edit) - * - Regular users to edit their own profile - * - Password changes via modal - */ -export default function UserManagementPage() { - const [users, setUsers] = useState([]); - const [selectedUser, setSelectedUser] = useState(null); - const [currentUser, setCurrentUser] = useState(null); - const [isPasswordModalOpen, setIsPasswordModalOpen] = useState(false); - const [isSaving, setIsSaving] = useState(false); - const [error, setError] = useState(null); - - const isAdmin = currentUser?.role === UserRole.ADMIN; - - // Load current user and user list (if admin) - useEffect(() => { - loadData(); - }, []); - - /** - * Load data for user management page - * - * An admin can see all users while a normal user can only see his own user data. - * Initially, the current user is selected. - */ - const loadData = async () => { - try { - const me = await fetchCurrentUser(); - setCurrentUser(me); - - if (me.role === UserRole.ADMIN) { - const userResponse: UserListResponse = await fetchAllUsers(); - - // Sort users alphabetically by last name, then first name - const sortedUsers = userResponse.valueList.sort((a, b) => { - const lastNameCompare = (a.lastName || "").localeCompare( - b.lastName || "" - ); - if (lastNameCompare !== 0) { - return lastNameCompare; - } - return (a.firstName || "").localeCompare(b.firstName || ""); - }); - - setUsers(sortedUsers); - setSelectedUser(me); - } else { - setUsers([me]); - setSelectedUser(me); - } - } catch (err) { - setError( - err instanceof Error - ? err.message - : "Fehler beim Laden der Benutzerdaten" - ); - } - }; - - /** Handles selecting a user from the list */ - const handleSelectUser = (user: UserDto) => { - setSelectedUser({...user}); - }; - - /** Handles saving user (create or update) */ - const handleSave = async (user: UserDto, password?: string) => { - setIsSaving(true); - setError(null); - try { - if (!user.id) { - // New user - const response: CreateUserResponse = await createUser({ - userData: user, - password: password || "", - }); - const userDto = response.userData; - if (userDto) { - const newUsers = [...users, userDto].sort((a, b) => { - const lastNameCompare = (a.lastName || "").localeCompare( - b.lastName || "" - ); - if (lastNameCompare !== 0) { - return lastNameCompare; - } - return (a.firstName || "").localeCompare(b.firstName || ""); - }); - setUsers(newUsers); - setSelectedUser(userDto); - } - } else { - // Existing user - const updated = await updateUser(user); - setUsers(users.map((u) => (u.id === updated.id ? updated : u))); - setSelectedUser(updated); - } - } catch (err) { - setError( - err instanceof Error - ? err.message - : "Fehler beim Speichern der Benutzerdaten" - ); - } finally { - setIsSaving(false); - } - }; - - /** Opens password modal */ - const openPasswordModal = () => { - setIsPasswordModalOpen(true); - }; - - /** Add new empty user */ - const handleAddUser = () => { - setSelectedUser({ - userName: "", - firstName: "", - lastName: "", - email: "", - role: UserRole.USER, - }); - }; - - return ( - - - -

    Benutzerverwaltung

    - - {isAdmin && ( -