Add components for SelectField and TextLinkButton

This commit is contained in:
araemer 2025-11-13 20:37:26 +01:00
parent e539f9201b
commit c8b8435b69
3 changed files with 101 additions and 51 deletions

View file

@ -0,0 +1,25 @@
type SelectFieldProps = {
value: string;
onChange: (value: string) => void;
options: { value: string; label: string }[];
className?: string;
};
/**
* SelectField - A dropdown styled consistently with input fields
*/
export default function SelectField({value, onChange, options, className = ''}: SelectFieldProps) {
return (
<select
value={value}
onChange={(e) => onChange(e.target.value)}
className={`p-2 w-full border rounded-md bg-white border-gray-600 hover:border-blue-800 transition-colors text-gray-600 focus:outline-none focus:border-blue-900 cursor-pointer ${className}`}
>
{options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
);
}

View file

@ -0,0 +1,20 @@
type TextLinkButtonProps = {
text: string;
onClick: () => void;
className?: string;
};
/**
* TextLinkButton - A button styled as a hyperlink
*/
export default function TextLinkButton({text, onClick, className = ''}: TextLinkButtonProps) {
return (
<button
type="button"
onClick={onClick}
className={`text-blue-600 hover:text-blue-800 hover:underline transition-colors bg-transparent border-none p-0 cursor-pointer ${className}`}
>
{text}
</button>
);
}

View file

@ -1,6 +1,6 @@
import {useEffect, useState} from "react"; import {useEffect, useState} from "react";
import {createUser, fetchCurrentUser, updateUser} from "../../api/points/UserPoint"; import {createUser, fetchCurrentUser, updateUser} from "../../api/points/UserPoint";
import type {UserDto} from "../../api/dtos/UserDto.ts"; // @todo add model and mapper! import type {UserDto} from "../../api/dtos/UserDto.ts";
import ContentBackground from "../basics/ContentBackground"; import ContentBackground from "../basics/ContentBackground";
import ContentBody from "../basics/ContentBody"; import ContentBody from "../basics/ContentBody";
import StickyHeader from "../basics/StickyHeader"; import StickyHeader from "../basics/StickyHeader";
@ -14,12 +14,14 @@ import ButtonGroupLayout from "../basics/ButtonGroupLayout.tsx";
import {Plus, X} from "lucide-react"; import {Plus, X} from "lucide-react";
import ButtonLink from "../basics/ButtonLink.tsx"; import ButtonLink from "../basics/ButtonLink.tsx";
import {getRecipeListUrl} from "../../routes.ts"; import {getRecipeListUrl} from "../../routes.ts";
import TextLinkButton from "../basics/TextLinkButton.tsx";
import SelectField from "../basics/SelectField.tsx";
/** /**
* UserManagementPage * UserManagementPage
* ------------------- * -------------------
* Displays a two-column layout: * Displays a two-column layout:
* - Left: list of all users * - Left: list of all users (wider on desktop)
* - Right: edit form for selected or new user * - Right: edit form for selected or new user
* *
* Allows: * Allows:
@ -55,6 +57,7 @@ export default function UserManagementPage() {
setSelectedUser(me); setSelectedUser(me);
//} //}
}; };
/** Handles selecting a user from the list */ /** Handles selecting a user from the list */
const handleSelectUser = (user: UserDto) => { const handleSelectUser = (user: UserDto) => {
setSelectedUser({...user}); setSelectedUser({...user});
@ -98,6 +101,11 @@ export default function UserManagementPage() {
}); });
}; };
const roleOptions = [
{value: "user", label: "Benutzer"},
{value: "admin", label: "Administrator"}
];
return ( return (
<PageContainer> <PageContainer>
<ContentBackground> <ContentBackground>
@ -120,17 +128,17 @@ export default function UserManagementPage() {
</StickyHeader> </StickyHeader>
<ContentBody> <ContentBody>
<div className="flex flex-col md:flex-row gap-6"> <div className="flex flex-col md:flex-row gap-0">
{/* User List @todo selector component!*/} {/* User List - Wider on desktop, attached to left */}
<div className="md:w-1/3 border-r border-gray-300"> <div className="md:w-1/2 lg:w-2/5 border-r border-gray-300 pr-6">
<h2>Benutzer</h2> <h2>Benutzer</h2>
<ul> <ul className="space-y-1">
{users.map((user) => ( {users.map((user) => (
<li <li
key={user.id ?? user.userName} key={user.id ?? user.userName}
className={clsx( className={clsx(
"p-2 hover:bg-gray-200", "p-3 rounded cursor-pointer hover:bg-gray-200 transition-colors",
selectedUser?.id === user.id && "bg-gray-300 font-semibold" selectedUser?.id === user.id && "bg-blue-100 hover:bg-blue-200 font-semibold"
)} )}
onClick={() => handleSelectUser(user)} onClick={() => handleSelectUser(user)}
> >
@ -141,20 +149,13 @@ export default function UserManagementPage() {
</div> </div>
{/* Edit Form */} {/* Edit Form */}
<div className="md:w-2/3"> <div className="md:w-1/2 lg:w-3/5 pl-6">
{selectedUser ? ( {selectedUser ? (
<div> <div>
<h2> <h2>
{selectedUser.id ? "Benutzer bearbeiten" : "Neuen Benutzer anlegen"} {selectedUser.id ? "Benutzer bearbeiten" : "Neuen Benutzer anlegen"}
</h2> </h2>
<form <div className="flex flex-col gap-3 max-w-md">
onSubmit={(e) => {
e.preventDefault();
handleSave();
}}
className="flex flex-col gap-3 max-w-md"
>
{/* @todo create component for laben and input field combination */}
<label>Benutzername</label> <label>Benutzername</label>
<input <input
type="text" type="text"
@ -164,6 +165,7 @@ export default function UserManagementPage() {
setSelectedUser({...selectedUser, userName: e.target.value}) setSelectedUser({...selectedUser, userName: e.target.value})
} }
/> />
<label>Vorname</label> <label>Vorname</label>
<input <input
type="text" type="text"
@ -173,6 +175,7 @@ export default function UserManagementPage() {
setSelectedUser({...selectedUser, firstName: e.target.value}) setSelectedUser({...selectedUser, firstName: e.target.value})
} }
/> />
<label>Nachname</label> <label>Nachname</label>
<input <input
type="text" type="text"
@ -182,6 +185,7 @@ export default function UserManagementPage() {
setSelectedUser({...selectedUser, lastName: e.target.value}) setSelectedUser({...selectedUser, lastName: e.target.value})
} }
/> />
<label>E-Mail</label> <label>E-Mail</label>
<input <input
type="email" type="email"
@ -192,46 +196,47 @@ export default function UserManagementPage() {
} }
/> />
{isAdmin && (<label>Benutzergruppe</label>)}
{isAdmin && ( {isAdmin && (
// @todo style <>
<select <label>Benutzergruppe</label>
className="input-field" <SelectField
value={selectedUser.role ?? "user"} value={selectedUser.role ?? "user"}
onChange={(e) => onChange={(value) =>
setSelectedUser({...selectedUser, role: e.target.value}) setSelectedUser({...selectedUser, role: value})
} }
> options={roleOptions}
<option value="user">Benutzer</option> />
<option value="admin">Administrator</option> </>
</select>
)} )}
{/* Show password field only when creating new user */} {/* Show password field only when creating new user */}
{!selectedUser.id && (<label>Passwort</label>)}
{!selectedUser.id && ( {!selectedUser.id && (
<PasswordField <>
onPasswordChanged={setPassword} <label>Passwort</label>
onKeyDown={() => { <PasswordField
}} onPasswordChanged={setPassword}
/> onKeyDown={() => {
)} }}
{!selectedUser.id && (<label>Passwort bestätigen</label>)} />
{!selectedUser.id && (
<PasswordField <label>Passwort bestätigen</label>
placeholder="Passwort bestätigen" <PasswordField
onPasswordChanged={setConfirmPassword} placeholder="Passwort bestätigen"
onKeyDown={() => { onPasswordChanged={setConfirmPassword}
}} onKeyDown={() => {
/> }}
/>
</>
)} )}
{/* Change password button for existing user */} {/* Change password link for existing user */}
{selectedUser.id && ( {selectedUser.id && (
<Button <div className="mt-2 text-right">
text="Passwort ändern" <TextLinkButton
onClick={openPasswordModal} text="Passwort ändern"
/> onClick={openPasswordModal}
/>
</div>
)} )}
<ButtonGroupLayout> <ButtonGroupLayout>
@ -245,7 +250,7 @@ export default function UserManagementPage() {
onClick={loadData} onClick={loadData}
/> />
</ButtonGroupLayout> </ButtonGroupLayout>
</form> </div>
</div> </div>
) : ( ) : (
<p className="text-gray-600">Bitte einen Benutzer auswählen.</p> <p className="text-gray-600">Bitte einen Benutzer auswählen.</p>