123 lines
3.8 KiB
JavaScript
123 lines
3.8 KiB
JavaScript
import { useState } from "react";
|
|
import { NavLink, useLocation } from "react-router-dom";
|
|
import { useAuth } from "../auth/AuthContext";
|
|
|
|
const navItems = [
|
|
{ to: "/", label: "Dashboard", roles: null },
|
|
{
|
|
label: "Melodies",
|
|
roles: ["superadmin", "melody_editor", "viewer"],
|
|
children: [
|
|
{ to: "/melodies", label: "Editor" },
|
|
{ to: "/melodies/settings", label: "Settings" },
|
|
],
|
|
},
|
|
{ to: "/devices", label: "Devices", roles: ["superadmin", "device_manager", "viewer"] },
|
|
{ to: "/users", label: "Users", roles: ["superadmin", "user_manager", "viewer"] },
|
|
{ to: "/mqtt", label: "MQTT", roles: ["superadmin", "device_manager", "viewer"] },
|
|
];
|
|
|
|
const linkClass = (isActive) =>
|
|
`block px-3 py-2 rounded-md text-sm transition-colors ${
|
|
isActive
|
|
? "bg-[var(--accent)] text-[var(--bg-primary)] font-medium"
|
|
: "text-[var(--text-secondary)] hover:bg-[var(--bg-card-hover)] hover:text-[var(--text-heading)]"
|
|
}`;
|
|
|
|
export default function Sidebar() {
|
|
const { hasRole } = useAuth();
|
|
const location = useLocation();
|
|
|
|
const visibleItems = navItems.filter(
|
|
(item) => item.roles === null || hasRole(...item.roles)
|
|
);
|
|
|
|
return (
|
|
<aside className="w-56 min-h-screen p-4 border-r" style={{ backgroundColor: "var(--bg-sidebar)", borderColor: "var(--border-primary)" }}>
|
|
<div className="mb-8 px-2">
|
|
<img
|
|
src="/logo-dark.png"
|
|
alt="BellSystems"
|
|
className="h-10 w-auto"
|
|
/>
|
|
</div>
|
|
<nav className="space-y-1">
|
|
{visibleItems.map((item) =>
|
|
item.children ? (
|
|
<CollapsibleGroup
|
|
key={item.label}
|
|
label={item.label}
|
|
children={item.children}
|
|
currentPath={location.pathname}
|
|
/>
|
|
) : (
|
|
<NavLink
|
|
key={item.to}
|
|
to={item.to}
|
|
end={item.to === "/"}
|
|
className={({ isActive }) => linkClass(isActive)}
|
|
>
|
|
{item.label}
|
|
</NavLink>
|
|
)
|
|
)}
|
|
</nav>
|
|
</aside>
|
|
);
|
|
}
|
|
|
|
function CollapsibleGroup({ label, children, currentPath }) {
|
|
const isChildActive = children.some(
|
|
(child) =>
|
|
currentPath === child.to ||
|
|
(child.to !== "/" && currentPath.startsWith(child.to + "/"))
|
|
);
|
|
const [open, setOpen] = useState(isChildActive);
|
|
|
|
const shouldBeOpen = open || isChildActive;
|
|
|
|
return (
|
|
<div>
|
|
<button
|
|
type="button"
|
|
onClick={() => setOpen(!shouldBeOpen)}
|
|
className={`w-full flex items-center justify-between px-3 py-2 rounded-md text-sm transition-colors ${
|
|
isChildActive
|
|
? "text-[var(--text-heading)]"
|
|
: "text-[var(--text-secondary)] hover:bg-[var(--bg-card-hover)] hover:text-[var(--text-heading)]"
|
|
}`}
|
|
>
|
|
<span>{label}</span>
|
|
<svg
|
|
className={`w-4 h-4 transition-transform ${shouldBeOpen ? "rotate-90" : ""}`}
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
|
</svg>
|
|
</button>
|
|
{shouldBeOpen && (
|
|
<div className="ml-3 mt-1 space-y-1">
|
|
{children.map((child) => (
|
|
<NavLink
|
|
key={child.to}
|
|
to={child.to}
|
|
end
|
|
className={({ isActive }) =>
|
|
`block pl-4 pr-3 py-1.5 rounded-md text-sm transition-colors ${
|
|
isActive
|
|
? "bg-[var(--accent)] text-[var(--bg-primary)] font-medium"
|
|
: "text-[var(--text-muted)] hover:bg-[var(--bg-card-hover)] hover:text-[var(--text-heading)]"
|
|
}`
|
|
}
|
|
>
|
|
{child.label}
|
|
</NavLink>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|