feat: enhance Sidebar with toggle functionality and improve layout; update StorageRow and Inventory components for better styling; add new translations for quick product details

This commit is contained in:
2026-06-04 00:28:09 +02:00
parent 2a7f411834
commit 53efd21a2b
8 changed files with 200 additions and 108 deletions
+44 -6
View File
@@ -1,3 +1,4 @@
import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Button, Typography } from "@mui/joy"; import { Button, Typography } from "@mui/joy";
import InventoryIcon from "@mui/icons-material/Inventory"; import InventoryIcon from "@mui/icons-material/Inventory";
@@ -6,6 +7,8 @@ import StorageIcon from "@mui/icons-material/Storage";
import SettingsIcon from "@mui/icons-material/Settings"; import SettingsIcon from "@mui/icons-material/Settings";
import ExitToAppIcon from "@mui/icons-material/ExitToApp"; import ExitToAppIcon from "@mui/icons-material/ExitToApp";
import TranslateIcon from "@mui/icons-material/Translate"; import TranslateIcon from "@mui/icons-material/Translate";
import MenuIcon from "@mui/icons-material/Menu";
import CloseIcon from "@mui/icons-material/Close";
import { useNavigate, useMatchRoute } from "@tanstack/react-router"; import { useNavigate, useMatchRoute } from "@tanstack/react-router";
import Cookies from "js-cookie"; import Cookies from "js-cookie";
import { changeTranslation } from "../utils/uxFncs"; import { changeTranslation } from "../utils/uxFncs";
@@ -14,6 +17,7 @@ export const Sidebar = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const matchRoute = useMatchRoute(); const matchRoute = useMatchRoute();
const [isOpen, setIsOpen] = useState(false);
const btnClass = const btnClass =
"h-11 w-full justify-start! rounded-2xl px-4 text-left text-sm font-semibold text-slate-700 transition hover:bg-white/80 hover:text-[#0b6bcb] [&_.MuiButton-startDecorator]:mr-3! [&_.MuiButton-startDecorator]:ml-0!"; "h-11 w-full justify-start! rounded-2xl px-4 text-left text-sm font-semibold text-slate-700 transition hover:bg-white/80 hover:text-[#0b6bcb] [&_.MuiButton-startDecorator]:mr-3! [&_.MuiButton-startDecorator]:ml-0!";
@@ -21,8 +25,14 @@ export const Sidebar = () => {
const variant = (to: string) => const variant = (to: string) =>
!!matchRoute({ to, fuzzy: false }) ? "soft" : "plain"; !!matchRoute({ to, fuzzy: false }) ? "soft" : "plain";
const handleNavigate = (to: string) => {
navigate({ to });
setIsOpen(false);
};
return ( return (
<aside className="flex h-full min-h-screen w-full max-w-70 flex-col gap-8 border-r border-white/80 bg-linear-to-b from-[#f7fbff] via-[#f2f6fb] to-[#eef3f9] px-6 py-8 shadow-[0_20px_60px_rgba(11,107,203,0.08)]"> <aside className="flex w-full flex-col gap-6 border-b border-white/80 bg-linear-to-b from-[#f7fbff] via-[#f2f6fb] to-[#eef3f9] px-4 py-5 shadow-[0_20px_60px_rgba(11,107,203,0.08)] sm:px-5 lg:h-full lg:min-h-screen lg:max-w-70 lg:border-b-0 lg:border-r lg:px-6 lg:py-8">
<div className="flex items-center justify-between gap-4">
<div className="space-y-2"> <div className="space-y-2">
<Typography <Typography
level="h2" level="h2"
@@ -37,10 +47,36 @@ export const Sidebar = () => {
{Cookies.get("app-name") ? Cookies.get("app-name") : ""} {Cookies.get("app-name") ? Cookies.get("app-name") : ""}
</Typography> </Typography>
</div> </div>
<Button
variant="soft"
size="sm"
className="lg:hidden transition-transform duration-200 ease-out"
onClick={() => setIsOpen((open) => !open)}
startDecorator={
<span
className={`inline-flex transition-transform duration-200 ease-out ${
isOpen ? "rotate-180" : "rotate-0"
}`}
>
{isOpen ? <CloseIcon /> : <MenuIcon />}
</span>
}
sx={{ display: { lg: "none" } }}
>
{isOpen ? t("close") : t("menu")}
</Button>
</div>
<div
className={`flex flex-1 flex-col gap-8 overflow-hidden transition-all duration-200 ease-out lg:overflow-visible lg:transition-none ${
isOpen
? "max-h-120 opacity-100 pointer-events-auto"
: "max-h-0 opacity-0 pointer-events-none"
} lg:max-h-none lg:opacity-100 lg:pointer-events-auto`}
>
<div className="flex flex-1 flex-col gap-2"> <div className="flex flex-1 flex-col gap-2">
<Button <Button
onClick={() => navigate({ to: "/app/inventory" })} onClick={() => handleNavigate("/app/inventory")}
variant={variant("/app/inventory")} variant={variant("/app/inventory")}
startDecorator={<InventoryIcon />} startDecorator={<InventoryIcon />}
className={btnClass} className={btnClass}
@@ -48,7 +84,7 @@ export const Sidebar = () => {
{t("inventory")} {t("inventory")}
</Button> </Button>
<Button <Button
onClick={() => navigate({ to: "/app/add-product" })} onClick={() => handleNavigate("/app/add-product")}
variant={variant("/app/add-product")} variant={variant("/app/add-product")}
startDecorator={<AddBoxIcon />} startDecorator={<AddBoxIcon />}
className={btnClass} className={btnClass}
@@ -56,7 +92,7 @@ export const Sidebar = () => {
{t("add")} {t("add")}
</Button> </Button>
<Button <Button
onClick={() => navigate({ to: "/app/storages" })} onClick={() => handleNavigate("/app/storages")}
variant={variant("/app/storages")} variant={variant("/app/storages")}
startDecorator={<StorageIcon />} startDecorator={<StorageIcon />}
className={btnClass} className={btnClass}
@@ -64,7 +100,7 @@ export const Sidebar = () => {
{t("storages")} {t("storages")}
</Button> </Button>
<Button <Button
onClick={() => navigate({ to: "/app/app-settings" })} onClick={() => handleNavigate("/app/app-settings")}
variant={variant("/app/app-settings")} variant={variant("/app/app-settings")}
startDecorator={<SettingsIcon />} startDecorator={<SettingsIcon />}
className={btnClass} className={btnClass}
@@ -74,7 +110,7 @@ export const Sidebar = () => {
<Button <Button
onClick={() => { onClick={() => {
Cookies.remove("token"); Cookies.remove("token");
navigate({ to: "/login" }); handleNavigate("/login");
}} }}
color="danger" color="danger"
startDecorator={<ExitToAppIcon />} startDecorator={<ExitToAppIcon />}
@@ -85,6 +121,7 @@ export const Sidebar = () => {
<Button <Button
onClick={() => { onClick={() => {
changeTranslation(); changeTranslation();
setIsOpen(false);
}} }}
color="neutral" color="neutral"
startDecorator={<TranslateIcon />} startDecorator={<TranslateIcon />}
@@ -102,6 +139,7 @@ export const Sidebar = () => {
/> />
<span>Stockhome</span> <span>Stockhome</span>
</div> </div>
</div>
</aside> </aside>
); );
}; };
+8 -8
View File
@@ -47,8 +47,8 @@ export const StorageRow = ({ storage }: StorageRowProps) => {
(values.description ?? "") !== (storage.description ?? ""); (values.description ?? "") !== (storage.description ?? "");
return ( return (
<tr key={storage.uuid} className="align-top"> <tr key={storage.uuid} className="border-t border-slate-200 align-top">
<td> <td className="px-6 py-5">
<form.Field name="name"> <form.Field name="name">
{(field) => ( {(field) => (
<Input <Input
@@ -57,12 +57,12 @@ export const StorageRow = ({ storage }: StorageRowProps) => {
onBlur={field.handleBlur} onBlur={field.handleBlur}
size="sm" size="sm"
variant="outlined" variant="outlined"
className="rounded-xl bg-white/90 shadow-[0_8px_18px_rgba(15,23,42,0.06)]" className="rounded-xl bg-slate-50/80 text-slate-700 shadow-none ring-1 ring-inset ring-slate-200 focus-within:ring-2 focus-within:ring-slate-300"
/> />
)} )}
</form.Field> </form.Field>
</td> </td>
<td> <td className="px-6 py-5">
<form.Field name="description"> <form.Field name="description">
{(field) => ( {(field) => (
<Input <Input
@@ -71,18 +71,18 @@ export const StorageRow = ({ storage }: StorageRowProps) => {
onBlur={field.handleBlur} onBlur={field.handleBlur}
size="sm" size="sm"
variant="outlined" variant="outlined"
className="rounded-xl bg-white/90 shadow-[0_8px_18px_rgba(15,23,42,0.06)]" className="rounded-xl bg-slate-50/80 text-slate-700 shadow-none ring-1 ring-inset ring-slate-200 focus-within:ring-2 focus-within:ring-slate-300"
/> />
)} )}
</form.Field> </form.Field>
</td> </td>
<td className="text-sm text-slate-500"> <td className="px-6 py-5 text-sm text-slate-500">
{formatDate(storage.created_at)} {formatDate(storage.created_at)}
</td> </td>
<td className="text-sm text-slate-500"> <td className="px-6 py-5 text-sm text-slate-500">
{formatDate(storage.updated_at)} {formatDate(storage.updated_at)}
</td> </td>
<td> <td className="px-6 py-5 text-right">
<div className="flex flex-wrap justify-end gap-2"> <div className="flex flex-wrap justify-end gap-2">
<Button <Button
color="primary" color="primary"
+1 -1
View File
@@ -97,7 +97,7 @@ export const InventoryPage = () => {
<Sheet <Sheet
variant="outlined" variant="outlined"
className="mt-6 flex h-[calc(100vh-260px)] flex-col overflow-hidden rounded-2xl border border-slate-200 bg-white/80 shadow-sm" className="mt-6 flex min-h-0 flex-col overflow-hidden rounded-2xl border border-slate-200 bg-white/80 shadow-sm sm:h-[calc(100vh-260px)]"
> >
<div className="flex items-center justify-between border-b border-slate-200 px-6 py-4 text-slate-700"> <div className="flex items-center justify-between border-b border-slate-200 px-6 py-4 text-slate-700">
<Typography level="body-lg" fontWeight="bold"> <Typography level="body-lg" fontWeight="bold">
+5 -2
View File
@@ -20,6 +20,7 @@ import { toInputDate } from "../utils/uxFncs";
import type { ProductFormValues } from "../misc/interfaces"; import type { ProductFormValues } from "../misc/interfaces";
import type { productDetailsInterface } from "../misc/interfaces"; import type { productDetailsInterface } from "../misc/interfaces";
import Cookies from "js-cookie"; import Cookies from "js-cookie";
import ElectricBoltIcon from "@mui/icons-material/ElectricBolt";
export const ProductQuickView = () => { export const ProductQuickView = () => {
const uuid = window.location.search.split("uuid=")[1]; const uuid = window.location.search.split("uuid=")[1];
@@ -99,8 +100,10 @@ export const ProductQuickView = () => {
<div className="space-y-6"> <div className="space-y-6">
<div className="flex flex-wrap items-center gap-3"> <div className="flex flex-wrap items-center gap-3">
<div className="space-y-1"> <div className="space-y-1">
<Typography level="h2" className="text-slate-900"> <Typography level="h2" fontWeight="1000" className="text-slate-900">
{t("product-details")} <ElectricBoltIcon color="primary" />
{t("product-details-quick")}
<ElectricBoltIcon color="primary" />
</Typography> </Typography>
</div> </div>
<Chip <Chip
+52 -11
View File
@@ -40,31 +40,72 @@ export const Storages = () => {
</div> </div>
</div> </div>
<Sheet className="mt-6 rounded-3xl border border-slate-200/70 bg-white/90 p-6 shadow-[0_24px_60px_rgba(12,38,78,0.12)] backdrop-blur"> <Sheet
variant="outlined"
className="mt-6 flex min-h-0 flex-col overflow-hidden rounded-2xl border border-slate-200 bg-white/80 shadow-sm sm:h-[calc(100vh-260px)]"
>
<AddStorageModal isOpen={modal} setOpen={setModal} /> <AddStorageModal isOpen={modal} setOpen={setModal} />
{isLoading ? ( {isLoading ? (
<div className="flex items-center justify-center py-16"> <div className="flex items-center justify-center py-16">
<CircularProgress size="lg" /> <CircularProgress size="lg" />
</div> </div>
) : ( ) : (
<div className="overflow-hidden rounded-2xl border border-slate-200/80 bg-white"> <>
<Table hoverRow className="min-w-240 text-slate-700"> <div className="flex items-center justify-between border-b border-slate-200 px-6 py-4 text-slate-700">
<thead className="bg-slate-50/80 text-slate-500"> <Typography level="body-lg" fontWeight="bold">
<tr className="text-sm uppercase tracking-wide"> {t("storages")}
<th className="px-5 py-3 text-left">{t("storage-name")}</th> </Typography>
<th className="px-5 py-3 text-left">{t("description")}</th> </div>
<th className="px-5 py-3 text-left">{t("created-at")}</th> <div className="flex-1 overflow-auto">
<th className="px-5 py-3 text-left">{t("updated-at")}</th> <Table
<th className="px-5 py-3 text-right"></th> stickyHeader
stripe="odd"
variant="plain"
hoverRow
className="min-w-240 text-slate-700"
sx={{
"--TableCell-headBackground":
"var(--joy-palette-background-surface)",
"& thead": {
position: "sticky",
top: 0,
zIndex: 3,
backgroundColor: "rgb(248 250 252)",
},
"& thead tr": {
backgroundColor: "rgb(248 250 252)",
},
"& thead th:nth-child(2)": {
width: "40%",
},
"& thead th:nth-child(3)": {
width: "14%",
},
"& thead th": {
zIndex: 2,
backgroundColor: "rgb(248 250 252)",
backgroundImage: "none",
},
"& tr > *:nth-child(n+4)": { textAlign: "left" },
}}
>
<thead>
<tr className="text-slate-600">
<th className="px-6 py-4">{t("storage-name")}</th>
<th className="px-6 py-4">{t("description")}</th>
<th className="px-6 py-4">{t("created-at")}</th>
<th className="px-6 py-4">{t("updated-at")}</th>
<th className="px-6 py-4 text-right"></th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-slate-200"> <tbody>
{storages?.map((storage: Storage) => ( {storages?.map((storage: Storage) => (
<StorageRow key={storage.uuid} storage={storage} /> <StorageRow key={storage.uuid} storage={storage} />
))} ))}
</tbody> </tbody>
</Table> </Table>
</div> </div>
</>
)} )}
</Sheet> </Sheet>
</> </>
+2 -2
View File
@@ -7,9 +7,9 @@ export const Route = createFileRoute("/app/_hiddenLayout")({
function AppLayout() { function AppLayout() {
return ( return (
<div className="flex min-h-screen w-full bg-[#f7f9fc]"> <div className="flex min-h-screen w-full flex-col bg-[#f7f9fc] lg:flex-row">
<Sidebar /> <Sidebar />
<main className="flex-1 px-8 py-6"> <main className="flex-1 px-4 py-5 sm:px-6 lg:px-8 lg:py-6">
<Outlet /> <Outlet />
</main> </main>
</div> </div>
+6 -1
View File
@@ -42,5 +42,10 @@
"selected": "ausgewählt", "selected": "ausgewählt",
"logout": "Abmelden", "logout": "Abmelden",
"pcs": "Stk.", "pcs": "Stk.",
"change-translation": "English" "change-translation": "English",
"new-storage-title": "Neuer Lagerort",
"new-storage-content": "Geben Sie hier die Daten für einen neuen Lagerort ein.",
"menu": "Menü",
"close": "Schließen",
"product-details-quick": "Schnelle Produktdetails"
} }
+6 -1
View File
@@ -42,5 +42,10 @@
"selected": "selected", "selected": "selected",
"logout": "Logout", "logout": "Logout",
"pcs": "pcs.", "pcs": "pcs.",
"change-translation": "Deutsch" "change-translation": "Deutsch",
"new-storage-title": "New Storage location",
"new-storage-content": "Enter the details for a new storage location here.",
"menu": "Menu",
"close": "Close",
"product-details-quick": "Fast product details"
} }