feat(mui): SidebarNav — MUI List nav with white-pill active state

Add src/admin/ui/SidebarNav.tsx: drawer content component using MUI
List/ListItemButton with NavLink polymorphism. Active item uses the
`selected` prop for the white "pill" styling (not NavLink active class).
Renders logo header, permission-filtered nav sections, and user/logout
footer. Not yet wired — next task mounts it in a Drawer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-06-06 20:06:25 +02:00
parent a353178c2d
commit 7a21c85cce

172
src/admin/ui/SidebarNav.tsx Normal file
View File

@@ -0,0 +1,172 @@
import { NavLink, useLocation } from "react-router-dom";
import Box from "@mui/material/Box";
import List from "@mui/material/List";
import ListItemButton from "@mui/material/ListItemButton";
import ListItemIcon from "@mui/material/ListItemIcon";
import ListItemText from "@mui/material/ListItemText";
import Typography from "@mui/material/Typography";
import Avatar from "@mui/material/Avatar";
import Button from "@mui/material/Button";
import { useAuth } from "../context/AuthContext";
import { useTheme as useAppTheme } from "../../context/ThemeContext";
import { menuSections, isItemActive, hasItemPermission } from "./navData";
interface SidebarNavProps {
/** Closes the mobile drawer on item click (optional — desktop omits it). */
onNavigate?: () => void;
/** Logout handler, owned by the parent. */
onLogout: () => void;
}
export default function SidebarNav({ onNavigate, onLogout }: SidebarNavProps) {
const { user, hasPermission } = useAuth();
const { theme } = useAppTheme();
const location = useLocation();
const visibleSections = menuSections
.map((section) => ({
...section,
items: section.items.filter((item) =>
hasItemPermission(item, hasPermission),
),
}))
.filter((section) => section.items.length > 0);
return (
<Box
sx={{ display: "flex", flexDirection: "column", height: "100%", p: 1.5 }}
>
{/* Logo header */}
<Box sx={{ px: 1, py: 1 }}>
<Box
component="img"
src={`/api/admin/company-settings/logo?variant=${theme === "dark" ? "dark" : "light"}`}
alt="Logo"
onError={(e) => {
e.currentTarget.src =
theme === "dark"
? "/images/logo-dark.png"
: "/images/logo-light.png";
}}
sx={{
maxHeight: 40,
maxWidth: "100%",
objectFit: "contain",
display: "block",
}}
/>
</Box>
{/* Nav */}
<Box sx={{ flex: 1, overflowY: "auto", mt: 1 }}>
{visibleSections.map((section) => (
<Box key={section.label}>
<Typography
variant="caption"
sx={{
display: "block",
px: 1.5,
py: 0.5,
textTransform: "uppercase",
letterSpacing: ".06em",
fontWeight: 700,
color: "text.secondary",
fontSize: ".64rem",
}}
>
{section.label}
</Typography>
<List disablePadding>
{section.items.map((item) => (
<ListItemButton
key={item.path}
component={NavLink}
to={item.path}
end={item.end}
onClick={onNavigate}
selected={isItemActive(item, location.pathname)}
sx={{
borderRadius: 2,
mb: 0.25,
py: 0.6,
color: "text.secondary",
"& svg": { width: 18, height: 18 },
"&.Mui-selected, &.Mui-selected:hover": {
bgcolor: "background.paper",
color: "primary.main",
fontWeight: 700,
boxShadow: "0 2px 8px rgba(20,20,40,0.07)",
},
}}
>
<ListItemIcon sx={{ minWidth: 32, color: "inherit" }}>
{item.icon}
</ListItemIcon>
<ListItemText
primary={item.label}
primaryTypographyProps={{
fontSize: ".82rem",
fontWeight: "inherit",
}}
/>
</ListItemButton>
))}
</List>
</Box>
))}
</Box>
{/* Footer */}
<Box sx={{ pt: 1, mt: 1, borderTop: 1, borderColor: "divider" }}>
<Box
sx={{ display: "flex", alignItems: "center", gap: 1, px: 0.5, mb: 1 }}
>
<Avatar
sx={{
width: 34,
height: 34,
bgcolor: "primary.main",
fontSize: ".8rem",
fontWeight: 700,
fontFamily: "'Urbanist', sans-serif",
}}
>
{user?.fullName?.charAt(0) || user?.username?.charAt(0) || "U"}
</Avatar>
<Box sx={{ minWidth: 0 }}>
<Typography noWrap sx={{ fontSize: ".82rem", fontWeight: 600 }}>
{user?.fullName || user?.username}
</Typography>
<Typography
noWrap
sx={{ fontSize: ".7rem", color: "text.secondary" }}
>
{user?.roleDisplay}
</Typography>
</Box>
</Box>
<Button
fullWidth
color="inherit"
onClick={onLogout}
startIcon={
<svg
viewBox="0 0 24 24"
width="18"
height="18"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
<polyline points="16 17 21 12 16 7" />
<line x1="21" y1="12" x2="9" y2="12" />
</svg>
}
>
Odhlásit se
</Button>
</Box>
</Box>
);
}