116 lines
3.7 KiB
TypeScript
116 lines
3.7 KiB
TypeScript
import { Link, useLocation, useParams } from 'react-router';
|
|
import {
|
|
Server,
|
|
LayoutDashboard,
|
|
Network,
|
|
Settings,
|
|
Users,
|
|
Shield,
|
|
Gamepad2,
|
|
ScrollText,
|
|
ChevronLeft,
|
|
} from 'lucide-react';
|
|
import { cn } from '@source/ui';
|
|
import { Button } from '@/components/ui/button';
|
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
import { Separator } from '@/components/ui/separator';
|
|
import { useAuthStore } from '@/stores/auth';
|
|
|
|
interface NavItem {
|
|
label: string;
|
|
href: string;
|
|
icon: React.ComponentType<{ className?: string }>;
|
|
}
|
|
|
|
export function Sidebar() {
|
|
const location = useLocation();
|
|
const { orgId } = useParams();
|
|
const user = useAuthStore((s) => s.user);
|
|
|
|
const orgNav: NavItem[] = orgId
|
|
? [
|
|
{ label: 'Dashboard', href: `/org/${orgId}/dashboard`, icon: LayoutDashboard },
|
|
{ label: 'Servers', href: `/org/${orgId}/servers`, icon: Server },
|
|
{ label: 'Nodes', href: `/org/${orgId}/nodes`, icon: Network },
|
|
{ label: 'Members', href: `/org/${orgId}/settings/members`, icon: Users },
|
|
{ label: 'Settings', href: `/org/${orgId}/settings`, icon: Settings },
|
|
]
|
|
: [];
|
|
|
|
const adminNav: NavItem[] = user?.isSuperAdmin
|
|
? [
|
|
{ label: 'Users', href: '/admin/users', icon: Users },
|
|
{ label: 'Games', href: '/admin/games', icon: Gamepad2 },
|
|
{ label: 'Nodes', href: '/admin/nodes', icon: Network },
|
|
{ label: 'Audit Logs', href: '/admin/audit-logs', icon: ScrollText },
|
|
]
|
|
: [];
|
|
|
|
return (
|
|
<div className="flex h-full w-64 flex-col border-r bg-card">
|
|
<div className="flex h-14 items-center gap-2 border-b px-4">
|
|
<Shield className="h-6 w-6 text-primary" />
|
|
<span className="text-lg font-bold">GamePanel</span>
|
|
</div>
|
|
|
|
<ScrollArea className="flex-1 py-2">
|
|
{orgId && (
|
|
<div className="px-3 py-2">
|
|
<div className="mb-1 flex items-center gap-1 px-2">
|
|
<Link to="/" className="text-xs text-muted-foreground hover:text-foreground">
|
|
<ChevronLeft className="inline h-3 w-3" /> Organizations
|
|
</Link>
|
|
</div>
|
|
<NavSection items={orgNav} currentPath={location.pathname} />
|
|
</div>
|
|
)}
|
|
|
|
{!orgId && (
|
|
<div className="px-3 py-2">
|
|
<p className="mb-2 px-2 text-xs font-medium text-muted-foreground">ORGANIZATIONS</p>
|
|
<Link to="/">
|
|
<Button variant="ghost" className="w-full justify-start gap-2">
|
|
<LayoutDashboard className="h-4 w-4" />
|
|
All Organizations
|
|
</Button>
|
|
</Link>
|
|
</div>
|
|
)}
|
|
|
|
{adminNav.length > 0 && (
|
|
<>
|
|
<Separator className="mx-3 my-2" />
|
|
<div className="px-3 py-2">
|
|
<p className="mb-2 px-2 text-xs font-medium text-muted-foreground">ADMIN</p>
|
|
<NavSection items={adminNav} currentPath={location.pathname} />
|
|
</div>
|
|
</>
|
|
)}
|
|
</ScrollArea>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function NavSection({ items, currentPath }: { items: NavItem[]; currentPath: string }) {
|
|
return (
|
|
<nav className="flex flex-col gap-1">
|
|
{items.map((item) => {
|
|
const isActive =
|
|
currentPath === item.href || currentPath.startsWith(item.href + '/');
|
|
return (
|
|
<Link key={item.href} to={item.href}>
|
|
<Button
|
|
variant={isActive ? 'secondary' : 'ghost'}
|
|
className="w-full justify-start gap-2"
|
|
size="sm"
|
|
>
|
|
<item.icon className={cn('h-4 w-4', isActive && 'text-primary')} />
|
|
{item.label}
|
|
</Button>
|
|
</Link>
|
|
);
|
|
})}
|
|
</nav>
|
|
);
|
|
}
|