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:
172
src/admin/ui/SidebarNav.tsx
Normal file
172
src/admin/ui/SidebarNav.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user