enhanced Header component with mobile menu and password change dialog; updated HomePage layout

This commit is contained in:
2025-10-25 22:31:54 +02:00
parent b98e38b38b
commit e9319b49ec
2 changed files with 255 additions and 93 deletions

View File

@@ -8,6 +8,10 @@ import {
CloseButton, CloseButton,
Dialog, Dialog,
Portal, Portal,
HStack,
IconButton,
Menu,
Box,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import { PasswordInput } from "@/components/ui/password-input"; import { PasswordInput } from "@/components/ui/password-input";
import Cookies from "js-cookie"; import Cookies from "js-cookie";
@@ -21,6 +25,7 @@ import {
LifeBuoy, LifeBuoy,
LogOut, LogOut,
CalendarPlus, CalendarPlus,
MoreVertical,
} from "lucide-react"; } from "lucide-react";
import { useUserContext } from "@/states/Context"; import { useUserContext } from "@/states/Context";
import { useState } from "react"; import { useState } from "react";
@@ -33,7 +38,6 @@ const API_BASE =
export const Header = () => { export const Header = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const userData = useUserContext(); const userData = useUserContext();
// Error handling states // Error handling states
@@ -49,6 +53,9 @@ export const Header = () => {
const [, setTriggerLogout] = useAtom(triggerLogoutAtom); const [, setTriggerLogout] = useAtom(triggerLogoutAtom);
const [, setIsLoggedIn] = useAtom(setIsLoggedInAtom); const [, setIsLoggedIn] = useAtom(setIsLoggedInAtom);
// Dialog control
const [isPwOpen, setPwOpen] = useState(false);
const changePassword = async () => { const changePassword = async () => {
if (newPassword !== confirmPassword) { if (newPassword !== confirmPassword) {
setMsgTitle("Passwortänderung fehlgeschlagen"); setMsgTitle("Passwortänderung fehlgeschlagen");
@@ -79,82 +86,262 @@ export const Header = () => {
setMsgDescription("Ihr Passwort wurde erfolgreich geändert"); setMsgDescription("Ihr Passwort wurde erfolgreich geändert");
setMsgStatus("success"); setMsgStatus("success");
setIsMsg(true); setIsMsg(true);
setOldPassword("");
setNewPassword("");
setConfirmPassword("");
};
const username = userData?.username
? userData.username[0].toUpperCase() + userData.username.slice(1)
: "User";
const logout = () => {
Cookies.remove("token");
setIsLoggedIn(false);
setTriggerLogout(true);
}; };
return ( return (
<Stack as="header" gap={3} className="mb-16"> <Stack
as="header"
gap={3}
className="mb-6"
position="relative"
pr={{ base: 10, md: 0 }} // Platz für den Mobile-Button rechts
>
{/* Mobile: Drei-Punkte-Button, vertikal zentriert im Header */}
<Box
display={{ base: "block", md: "none" }}
position="absolute"
top="50%"
right="0"
transform="translateY(-50%)"
zIndex={2}
>
<Menu.Root>
<Menu.Trigger asChild>
<IconButton
aria-label="Aktionen"
variant="solid"
colorScheme="teal"
size="md"
borderRadius="full"
boxShadow="md"
>
<MoreVertical size={20} />
</IconButton>
</Menu.Trigger>
<Menu.Positioner>
<Menu.Content>
<Menu.Item
value="create-loan"
onSelect={() => navigate("/", { replace: true })}
children={
<HStack gap={3}>
<CalendarPlus size={16} />
<Text as="span">Ausleihe erstellen</Text>
</HStack>
}
/>
<Menu.Item
value="my-loans"
onSelect={() => navigate("/my-loans", { replace: true })}
children={
<HStack gap={3}>
<CircleUserRound size={16} />
<Text as="span">Meine Ausleihen</Text>
</HStack>
}
/>
<Menu.Item
value="change-password"
onSelect={() => setPwOpen(true)}
children={
<HStack gap={3}>
<RotateCcwKey size={16} />
<Text as="span">Passwort ändern</Text>
</HStack>
}
/>
<Menu.Item
value="help"
onSelect={() =>
window.open(
"https://git.the1s.de/Matthias-Claudius-Schule/borrow-system/wiki",
"_blank",
"noopener,noreferrer"
)
}
children={
<HStack gap={3}>
<LifeBuoy size={16} />
<Text as="span">Hilfe</Text>
</HStack>
}
/>
<Menu.Item
value="source-code"
onSelect={() =>
window.open(
"https://git.the1s.de/Matthias-Claudius-Schule/borrow-system",
"_blank",
"noopener,noreferrer"
)
}
children={
<HStack gap={3}>
<Code size={16} />
<Text as="span">Source Code</Text>
</HStack>
}
/>
<Menu.Separator />
<Menu.Item
value="logout"
onSelect={logout}
children={
<HStack gap={3} color="red.500">
<LogOut size={16} />
<Text as="span">Logout</Text>
</HStack>
}
/>
</Menu.Content>
</Menu.Positioner>
</Menu.Root>
</Box>
<Flex <Flex
direction={{ base: "column", md: "row" }} direction={{ base: "column", md: "row" }}
align={{ base: "stretch", md: "center" }} align={{ base: "stretch", md: "center" }}
justify="space-between" justify="space-between"
gap={4} gap={4}
> >
<Stack gap={2}> {/* Left: Title + user info */}
<Heading <Stack gap={1}>
size="2xl" {/* Titelzeile ohne Mobile-Menu (wurde nach oben verlegt) */}
className="tracking-tight text-slate-900 dark:text-slate-100" <Flex align="center" justify="space-between" gap={2}>
> <Heading
Home size="2xl"
</Heading> className="tracking-tight text-slate-900 dark:text-slate-100"
<Stack >
direction={{ base: "column", sm: "row" }} Home
gap={2} </Heading>
alignItems="start" </Flex>
>
<HStack gap={3} align="center" flexWrap="wrap">
<Text fontSize="md" className="text-slate-600 dark:text-slate-400"> <Text fontSize="md" className="text-slate-600 dark:text-slate-400">
Willkommen zurück,{" "} Willkommen zurück, {username}!
{userData.username.replace(
/^./,
userData.username[0].toUpperCase()
)}
!
</Text> </Text>
<Badge variant="subtle" px={2} py={1} borderRadius="full"> <Badge variant="subtle" px={2} py={1} borderRadius="full">
Rolle: {userData.role} Rolle: {userData?.role ?? "—"}
</Badge> </Badge>
</Stack> </HStack>
</Stack> </Stack>
<Button onClick={() => navigate("/", { replace: true })}> {/* Right: Actions */}
<CalendarPlus /> Ausleihe erstellen {/* Desktop actions */}
</Button> <HStack
<Button onClick={() => navigate("/my-loans", { replace: true })}> gap={2}
<CircleUserRound /> Meine Ausleihen align="center"
</Button> justify="flex-end"
<Dialog.Root> flexWrap="wrap"
<Dialog.Trigger asChild> display={{ base: "none", md: "flex" }}
<Button> >
<RotateCcwKey /> Passwort ändern <Button
colorScheme="teal"
onClick={() => navigate("/", { replace: true })}
>
<HStack gap={2}>
<CalendarPlus size={18} />
<Text as="span">Ausleihe erstellen</Text>
</HStack>
</Button>
<Button onClick={() => navigate("/my-loans", { replace: true })}>
<HStack gap={2}>
<CircleUserRound size={18} />
<Text as="span">Meine Ausleihen</Text>
</HStack>
</Button>
<Button variant="ghost" onClick={() => setPwOpen(true)}>
<HStack gap={2}>
<RotateCcwKey size={18} />
<Text as="span">Passwort ändern</Text>
</HStack>
</Button>
<a
href="https://git.the1s.de/Matthias-Claudius-Schule/borrow-system/wiki"
target="_blank"
>
<Button variant="ghost">
<HStack gap={2}>
<LifeBuoy size={18} />
<Text as="span">Hilfe</Text>
</HStack>
</Button> </Button>
</Dialog.Trigger> </a>
<Portal>
<Dialog.Backdrop /> <a
<Dialog.Positioner> href="https://git.the1s.de/Matthias-Claudius-Schule/borrow-system"
<Dialog.Content> target="_blank"
<Dialog.Header> >
<Dialog.Title>Passwort ändern</Dialog.Title> <Button variant="ghost">
</Dialog.Header> <HStack gap={2}>
<form <Code size={18} />
onSubmit={(e) => { <Text as="span">Source Code</Text>
e.preventDefault(); </HStack>
changePassword(); </Button>
}} </a>
>
<Dialog.Body> <Button onClick={logout} variant="outline" colorScheme="red">
<HStack gap={2}>
<LogOut size={18} />
<Text as="span">Logout</Text>
</HStack>
</Button>
</HStack>
</Flex>
{/* Passwort-Dialog (kontrolliert) */}
<Dialog.Root open={isPwOpen} onOpenChange={(e: any) => setPwOpen(e.open)}>
<Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content maxW="md">
<Dialog.Header>
<Dialog.Title>Passwort ändern</Dialog.Title>
</Dialog.Header>
<form
onSubmit={(e) => {
e.preventDefault();
changePassword();
}}
>
<Dialog.Body>
<Stack gap={3}>
<PasswordInput <PasswordInput
value={oldPassword}
onChange={(e) => setOldPassword(e.target.value)} onChange={(e) => setOldPassword(e.target.value)}
placeholder="Altes Passwort" placeholder="Altes Passwort"
/> />
<PasswordInput <PasswordInput
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)} onChange={(e) => setNewPassword(e.target.value)}
placeholder="Neues Passwort" placeholder="Neues Passwort"
/> />
<PasswordInput <PasswordInput
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)} onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="Neues Passwort wiederholen" placeholder="Neues Passwort wiederholen"
/> />
</Dialog.Body> </Stack>
<Dialog.Footer> </Dialog.Body>
<Dialog.Footer>
<Stack w="100%" gap={3}>
{isMsg && ( {isMsg && (
<MyAlert <MyAlert
status={msgStatus} status={msgStatus}
@@ -162,49 +349,24 @@ export const Header = () => {
description={msgDescription} description={msgDescription}
/> />
)} )}
<Dialog.ActionTrigger asChild> <HStack justify="flex-end" gap={2}>
<Button variant="outline">Cancel</Button> <Dialog.ActionTrigger asChild>
</Dialog.ActionTrigger> <Button variant="outline">Abbrechen</Button>
<Button type="submit">Save</Button> </Dialog.ActionTrigger>
</Dialog.Footer> <Button type="submit" colorScheme="teal">
</form> Speichern
<Dialog.CloseTrigger asChild> </Button>
<CloseButton size="sm" /> </HStack>
</Dialog.CloseTrigger> </Stack>
</Dialog.Content> </Dialog.Footer>
</Dialog.Positioner> </form>
</Portal> <Dialog.CloseTrigger asChild>
</Dialog.Root> <CloseButton size="sm" />
<a </Dialog.CloseTrigger>
href="https://git.the1s.de/Matthias-Claudius-Schule/borrow-system/wiki" </Dialog.Content>
target="_blank" </Dialog.Positioner>
> </Portal>
<Button> </Dialog.Root>
<LifeBuoy /> Hilfe
</Button>
</a>
<a
href="https://git.the1s.de/Matthias-Claudius-Schule/borrow-system"
target="_blank"
>
<Button>
<Code /> Source Code
</Button>
</a>
<Button
onClick={() => {
Cookies.remove("token");
setIsLoggedIn(false);
setTriggerLogout(true);
}}
variant="solid"
size="sm"
className="self-start md:self-auto"
>
<LogOut /> Logout
</Button>
</Flex>
</Stack> </Stack>
); );
}; };

View File

@@ -110,7 +110,7 @@ export const HomePage = () => {
</VStack> </VStack>
)} )}
{borrowableItems.length > 0 && ( {borrowableItems.length > 0 && (
<Table.ScrollArea borderWidth="1px" rounded="md" height="160px"> <Table.ScrollArea borderWidth="1px" rounded="md">
<Table.Root size="sm" stickyHeader> <Table.Root size="sm" stickyHeader>
<Table.Header> <Table.Header>
<Table.Row bg="bg.subtle"> <Table.Row bg="bg.subtle">