Fix routing and update translations: change landing page route to '/landingpage', update user info dialog, and enhance localization strings.

This commit is contained in:
2025-11-21 23:31:41 +01:00
parent 238cd9254a
commit c6571033b0
6 changed files with 170 additions and 87 deletions

View File

@@ -79,7 +79,7 @@ function App() {
<Route element={<ProtectedRoutes />}> <Route element={<ProtectedRoutes />}>
<Route path="/" element={<HomePage />} /> <Route path="/" element={<HomePage />} />
<Route path="/my-loans" element={<MyLoansPage />} /> <Route path="/my-loans" element={<MyLoansPage />} />
<Route path="/landing" element={<Landingpage />} /> <Route path="/landingpage" element={<Landingpage />} />
</Route> </Route>
<Route path="/login" element={<LoginPage />} /> <Route path="/login" element={<LoginPage />} />

View File

@@ -1,5 +1,4 @@
import { import {
Badge,
Button, Button,
Flex, Flex,
Heading, Heading,
@@ -14,6 +13,7 @@ import {
Box, Box,
Avatar, Avatar,
Card, Card,
Grid,
} 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";
@@ -28,7 +28,8 @@ import {
LogOut, LogOut,
CalendarPlus, CalendarPlus,
MoreVertical, MoreVertical,
Flag, Languages,
Table,
} from "lucide-react"; } from "lucide-react";
import { useUserContext } from "@/states/Context"; import { useUserContext } from "@/states/Context";
import { useState } from "react"; import { useState } from "react";
@@ -97,6 +98,18 @@ export const Header = () => {
const username = userData.first_name ? userData.first_name : "N/A"; const username = userData.first_name ? userData.first_name : "N/A";
const fullname = userData.first_name + " " + userData.last_name; const fullname = userData.first_name + " " + userData.last_name;
const randomColor = [
"gray",
"red",
"orange",
"yellow",
"green",
"teal",
"blue",
"cyan",
"purple",
"pink",
];
const logout = () => { const logout = () => {
Cookies.remove("token"); Cookies.remove("token");
@@ -158,12 +171,12 @@ export const Header = () => {
} }
/> />
<Menu.Item <Menu.Item
value="change-password" value="landingpage"
onSelect={() => setPwOpen(true)} onSelect={() => navigate("/landingpage", { replace: true })}
children={ children={
<HStack gap={3}> <HStack gap={3}>
<RotateCcwKey size={16} /> <Table size={16} />
<Text as="span">{t("change-password")}</Text> <Text as="span">{t("landingpage")}</Text>
</HStack> </HStack>
} }
/> />
@@ -177,7 +190,7 @@ export const Header = () => {
}} }}
children={ children={
<HStack gap={3}> <HStack gap={3}>
<LifeBuoy size={16} /> <Languages size={16} />
<Text as="span">{t("change-language")}</Text> <Text as="span">{t("change-language")}</Text>
</HStack> </HStack>
} }
@@ -244,7 +257,7 @@ export const Header = () => {
size="2xl" size="2xl"
className="tracking-tight text-slate-900 dark:text-slate-100" className="tracking-tight text-slate-900 dark:text-slate-100"
> >
Home {t("app-title")}
</Heading> </Heading>
</Flex> </Flex>
@@ -253,13 +266,11 @@ export const Header = () => {
{t("greeting")} {t("greeting")}
<strong>{username}</strong>! <strong>{username}</strong>!
</Text> </Text>
<Badge variant="subtle" px={2} py={1} borderRadius="full">
Rolle: {userData?.role ?? "—"}
</Badge>
</HStack> </HStack>
</Stack> </Stack>
<HStack> {/* Avatar: visible on mobile, hidden on desktop (desktop version is in the actions bar) */}
<HStack display={{ base: "flex", md: "none" }}>
<Avatar.Root> <Avatar.Root>
<button <button
onClick={() => setUserDialog(true)} onClick={() => setUserDialog(true)}
@@ -279,6 +290,18 @@ export const Header = () => {
flexWrap="wrap" flexWrap="wrap"
display={{ base: "none", md: "flex" }} display={{ base: "none", md: "flex" }}
> >
{/* Desktop avatar, aligned with action buttons */}
<Avatar.Root
colorPalette={randomColor[Math.floor(Math.random() * 10)]}
>
<button
onClick={() => setUserDialog(true)}
style={{ cursor: "pointer" }}
>
<Avatar.Fallback name={fullname} />
</button>
</Avatar.Root>
<Button <Button
colorScheme="teal" colorScheme="teal"
onClick={() => navigate("/", { replace: true })} onClick={() => navigate("/", { replace: true })}
@@ -296,10 +319,10 @@ export const Header = () => {
</HStack> </HStack>
</Button> </Button>
<Button variant="ghost" onClick={() => setPwOpen(true)}> <Button onClick={() => navigate("/landingpage", { replace: true })}>
<HStack gap={2}> <HStack gap={2}>
<RotateCcwKey size={18} /> <Table size={18} />
<Text as="span">{t("change-password")}</Text> <Text as="span">{t("landingpage")}</Text>
</HStack> </HStack>
</Button> </Button>
@@ -313,7 +336,7 @@ export const Header = () => {
}} }}
> >
<HStack gap={2}> <HStack gap={2}>
<Flag size={18} /> <Languages size={18} />
<Text as="span">{t("change-language")}</Text> <Text as="span">{t("change-language")}</Text>
</HStack> </HStack>
</Button> </Button>
@@ -351,48 +374,84 @@ export const Header = () => {
</HStack> </HStack>
</Flex> </Flex>
{/* User Info Dialoge */}
{userDialog && ( {userDialog && (
<Card.Root maxW="sm"> <Flex
<Card.Header> position="fixed"
<Card.Title> inset={0}
{" "} zIndex={1000}
<Avatar.Root> align="center"
<button justify="center"
onClick={() => setUserDialog(true)} bg="blackAlpha.400"
style={{ cursor: "pointer" }} backdropFilter="blur(6px)"
> >
<Avatar.Fallback name={fullname} /> <Card.Root maxW="sm" w="full" mx={4}>
</button> <Card.Header>
</Avatar.Root> <Card.Title>
</Card.Title> <Flex justify="center" align="center" w="100%">
<Card.Description>{t("user-info-desc")}</Card.Description> <Avatar.Root
</Card.Header> size={"2xl"}
<Card.Body> colorPalette={randomColor[Math.floor(Math.random() * 10)]}
<Stack gap="4" w="full"> >
<Text> <Avatar.Fallback name={fullname} />
<strong>{t("first-name")}:</strong> {userData.first_name} </Avatar.Root>
</Text> </Flex>
<Text> </Card.Title>
<strong>{t("last-name")}:</strong> {userData.last_name} <Card.Description>{t("user-info-desc")}</Card.Description>
</Text> </Card.Header>
<Text> <Card.Body>
<strong>{t("username")}:</strong> {userData.username} <Stack gap="4" w="full">
</Text> <Box as="dl">
<Text> <Grid
<strong>{t("role")}:</strong> {userData.role} templateColumns="auto 1fr"
</Text> rowGap={2}
<Text> columnGap={4}
<strong>{t("admin-status")}:</strong>{" "} alignItems="start"
{userData.is_admin ? t("yes") : t("no")} >
</Text> <Text as="dt" fontWeight="bold" textAlign="left">
</Stack> {t("first-name")}:
</Card.Body> </Text>
<Card.Footer justifyContent="flex-end"> <Text as="dd">{userData.first_name}</Text>
<Button variant="outline" onClick={() => setUserDialog(false)}>
{t("cancel")} <Text as="dt" fontWeight="bold" textAlign="left">
</Button> {t("last-name")}:
</Card.Footer> </Text>
</Card.Root> <Text as="dd">{userData.last_name}</Text>
<Text as="dt" fontWeight="bold" textAlign="left">
{t("username")}:
</Text>
<Text as="dd">{userData.username}</Text>
<Text as="dt" fontWeight="bold" textAlign="left">
{t("role")}:
</Text>
<Text as="dd">{userData.role}</Text>
<Text as="dt" fontWeight="bold" textAlign="left">
{t("admin-status")}:
</Text>
<Text as="dd">
{userData.is_admin ? t("yes") : t("no")}
</Text>
</Grid>
</Box>
<Button variant="solid" onClick={() => setPwOpen(true)}>
<HStack gap={2}>
<RotateCcwKey size={18} />
<Text as="span">{t("change-password")}</Text>
</HStack>
</Button>
</Stack>
</Card.Body>
<Card.Footer justifyContent="flex-end">
<Button variant="outline" onClick={() => setUserDialog(false)}>
{t("cancel")}
</Button>
</Card.Footer>
</Card.Root>
</Flex>
)} )}
{/* Passwort-Dialog (kontrolliert) */} {/* Passwort-Dialog (kontrolliert) */}

View File

@@ -10,11 +10,11 @@ import {
SimpleGrid, SimpleGrid,
Button, Button,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import { Lock, LockOpen } from "lucide-react";
import MyAlert from "@/components/myChakra/MyAlert"; import MyAlert from "@/components/myChakra/MyAlert";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { API_BASE } from "@/config/api.config"; import { API_BASE } from "@/config/api.config";
import Cookies from "js-cookie"; import Cookies from "js-cookie";
import { useNavigate } from "react-router-dom";
export const formatDateTime = (value: string | null | undefined) => { export const formatDateTime = (value: string | null | undefined) => {
if (!value) return "N/A"; if (!value) return "N/A";
@@ -38,7 +38,7 @@ type Device = {
id: number; id: number;
item_name: string; item_name: string;
can_borrow_role: string; can_borrow_role: string;
inSafe: number; in_safe: number;
entry_created_at: string; entry_created_at: string;
last_borrowed_person: string | null; last_borrowed_person: string | null;
currently_borrowing: string | null; currently_borrowing: string | null;
@@ -46,6 +46,7 @@ type Device = {
const Landingpage: React.FC = () => { const Landingpage: React.FC = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [loans, setLoans] = useState<Loan[]>([]); const [loans, setLoans] = useState<Loan[]>([]);
@@ -119,6 +120,10 @@ const Landingpage: React.FC = () => {
Matthias-Claudius-Schule Technik Matthias-Claudius-Schule Technik
</Heading> </Heading>
<Button onClick={() => navigate("/", { replace: true })}>
{t("back")}
</Button>
<Heading as="h2" size="md" mb={4}> <Heading as="h2" size="md" mb={4}>
{t("all-loans")} {t("all-loans")}
</Heading> </Heading>
@@ -158,10 +163,10 @@ const Landingpage: React.FC = () => {
<strong>{t("rented-items")}</strong> <strong>{t("rented-items")}</strong>
</Table.ColumnHeader> </Table.ColumnHeader>
<Table.ColumnHeader> <Table.ColumnHeader>
<strong>{t("return-date")}</strong> <strong>{t("take-date")}</strong>
</Table.ColumnHeader> </Table.ColumnHeader>
<Table.ColumnHeader> <Table.ColumnHeader>
<strong>{t("take-date")}</strong> <strong>{t("return-date")}</strong>
</Table.ColumnHeader> </Table.ColumnHeader>
</Table.Row> </Table.Row>
</Table.Header> </Table.Header>
@@ -177,8 +182,8 @@ const Landingpage: React.FC = () => {
? loan.loaned_items_name.join(", ") ? loan.loaned_items_name.join(", ")
: loan.loaned_items_name} : loan.loaned_items_name}
</Table.Cell> </Table.Cell>
<Table.Cell>{formatDateTime(loan.returned_date)}</Table.Cell>
<Table.Cell>{formatDateTime(loan.take_date)}</Table.Cell> <Table.Cell>{formatDateTime(loan.take_date)}</Table.Cell>
<Table.Cell>{formatDateTime(loan.returned_date)}</Table.Cell>
</Table.Row> </Table.Row>
))} ))}
</Table.Body> </Table.Body>
@@ -201,24 +206,25 @@ const Landingpage: React.FC = () => {
<Card.Root <Card.Root
key={device.id} key={device.id}
size="sm" size="sm"
bg={device.inSafe ? "green" : "red"} bg={device.in_safe ? "green" : "red"}
h="full" h="full"
minH="100px" minH="100px"
> >
<Card.Header> <Card.Header>
{device.inSafe ? <LockOpen size={16} /> : <Lock size={16} />} <Heading size="md">
<Heading size="md">{device.item_name}</Heading> <strong>{device.item_name}</strong>
</Heading>
</Card.Header> </Card.Header>
<Card.Body color="fg.muted"> <Card.Body>
<Text> <Text>
{t("rent-role")}: {device.can_borrow_role} <strong>{t("role")}</strong>: {device.can_borrow_role}
</Text> </Text>
<Text> <Text>
{t("last-borrowed-person")}:{" "} <strong>{t("last-borrowed-person")}</strong>:{" "}
{device.last_borrowed_person || "N/A"} {device.last_borrowed_person || "N/A"}
</Text> </Text>
<Text> <Text>
{t("currently-borrowed-by")}:{" "} <strong>{t("currently-borrowed-by")}</strong>:{" "}
{device.currently_borrowing || "N/A"} {device.currently_borrowing || "N/A"}
</Text> </Text>
</Card.Body> </Card.Body>
@@ -238,7 +244,6 @@ const Landingpage: React.FC = () => {
borderRadius="full" borderRadius="full"
> >
<HStack gap={2}> <HStack gap={2}>
<LockOpen size={16} />
<Text>{t("in-locker")}</Text> <Text>{t("in-locker")}</Text>
</HStack> </HStack>
</Button> </Button>
@@ -251,7 +256,6 @@ const Landingpage: React.FC = () => {
borderRadius="full" borderRadius="full"
> >
<HStack gap={2}> <HStack gap={2}>
<Lock size={16} />
<Text>{t("not-in-locker")}</Text> <Text>{t("not-in-locker")}</Text>
</HStack> </HStack>
</Button> </Button>

View File

@@ -62,5 +62,15 @@
"change-language": "Sprache ändern", "change-language": "Sprache ändern",
"timezone-info": "Die angezeigten Daten und Uhrzeiten werden in deutscher Zeitzone dargestellt und müssen auch so eingegeben werden.", "timezone-info": "Die angezeigten Daten und Uhrzeiten werden in deutscher Zeitzone dargestellt und müssen auch so eingegeben werden.",
"optional-note": "Optionale Notiz", "optional-note": "Optionale Notiz",
"note": "Notiz" "note": "Notiz",
"user-info-desc": "Hier können Sie Ihre persönlichen Informationen einsehen und ändern.",
"role": "Rolle",
"admin-status": "Admin-Status",
"first-name": "Vorname",
"last-name": "Nachname",
"app-title": "Ausleihsystem",
"last-borrowed-person": "Zuletzt ausgeliehen von",
"currently-borrowed-by": "Derzeit ausgeliehen von",
"back": "Zurückgehen",
"landingpage": "Übersichtsseite"
} }

View File

@@ -62,5 +62,15 @@
"change-language": "Change language", "change-language": "Change language",
"timezone-info": "The displayed dates and times are shown in Berlin timezone and must also be entered as such.", "timezone-info": "The displayed dates and times are shown in Berlin timezone and must also be entered as such.",
"optional-note": "Optional note", "optional-note": "Optional note",
"note": "Note" "note": "Note",
"user-info-desc": "Here you can view and edit your personal information.",
"role": "Role",
"admin-status": "Admin status",
"first-name": "First name",
"last-name": "Last name",
"app-title": "Borrow System",
"last-borrowed-person": "Last borrowed by",
"currently-borrowed-by": "Currently borrowed by",
"back": "Go back",
"landingpage": "Overview page"
} }

View File

@@ -48,19 +48,19 @@ services:
- mysql_v2 - mysql_v2
restart: unless-stopped restart: unless-stopped
mysql: # mysql:
container_name: borrow_system-mysql # container_name: borrow_system-mysql
image: mysql:8.0 # image: mysql:8.0
restart: unless-stopped # restart: unless-stopped
environment: # environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} # MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
MYSQL_DATABASE: borrow_system # MYSQL_DATABASE: borrow_system
TZ: Europe/Berlin # TZ: Europe/Berlin
volumes: # volumes:
- mysql-data:/var/lib/mysql # - mysql-data:/var/lib/mysql
- ./mysql-timezone.cnf:/etc/mysql/conf.d/timezone.cnf:ro # - ./mysql-timezone.cnf:/etc/mysql/conf.d/timezone.cnf:ro
ports: # ports:
- "3309:3306" # - "3309:3306"
mysql_v2: mysql_v2:
container_name: borrow_system-mysql-v2 container_name: borrow_system-mysql-v2