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