Files
shadow-gunbuilder-ai-proto/components/AdminLeftNavigation.tsx
T

219 lines
7.9 KiB
TypeScript

"use client";
import * as React from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { Disclosure, DisclosureButton, DisclosurePanel } from "@headlessui/react";
import { ChevronRight, PanelLeftClose, PanelLeftOpen } from "lucide-react";
import { useAuth } from "@/context/AuthContext";
export type AdminNavItem = {
label: string;
href: string;
icon?: React.ReactNode;
};
export type AdminNavGroup = {
label: string;
icon?: React.ReactNode;
items: AdminNavItem[];
};
function cx(...classes: Array<string | false | undefined | null>) {
return classes.filter(Boolean).join(" ");
}
function isActiveRoute(pathname: string, href: string) {
// Exact match for /admin, prefix match for nested pages
if (href === "/admin") return pathname === "/admin";
return pathname === href || pathname.startsWith(`${href}/`);
}
function groupHasActiveItem(pathname: string, group: AdminNavGroup) {
return group.items.some((i) => isActiveRoute(pathname, i.href));
}
export default function AdminLeftNavigation({
collapsed,
onToggleCollapsed,
groups,
}: {
collapsed: boolean;
onToggleCollapsed: () => void;
groups: AdminNavGroup[];
}) {
const pathname = usePathname() || "/admin";
const { user } = useAuth();
const identity =
user?.displayName?.trim() ||
user?.username?.trim() ||
user?.email?.trim() ||
user?.uuid ||
"Admin";
const secondary = user?.email && user.email !== identity ? user.email : null;
return (
<aside
className={cx(
"fixed inset-y-0 left-0 flex shrink-0 flex-col border-r border-zinc-900 bg-zinc-950/60 backdrop-blur z-40",
collapsed ? "w-[68px]" : "w-[280px]"
)}
>
{/* Header / Brand */}
<div className={cx("flex items-center justify-between px-3 py-3", collapsed && "justify-center")}>
<div className={cx("flex items-center gap-2", collapsed && "hidden")}>
<div className="h-8 w-8 rounded-lg bg-zinc-900/70 ring-1 ring-zinc-800" />
<div className="leading-tight">
<div className="text-xs uppercase tracking-[0.22em] text-zinc-500">
Battl
</div>
<div className="text-sm font-medium text-zinc-100">
Admin
</div>
</div>
</div>
<button
type="button"
onClick={onToggleCollapsed}
className={cx(
"inline-flex h-9 w-9 items-center justify-center rounded-md border border-zinc-800 bg-zinc-900/40 text-zinc-200 hover:bg-zinc-900/70",
collapsed && "mx-auto"
)}
aria-label={collapsed ? "Expand sidebar" : "Collapse sidebar"}
title={collapsed ? "Expand" : "Collapse"}
>
{collapsed ? (
<PanelLeftOpen className="h-4 w-4" />
) : (
<PanelLeftClose className="h-4 w-4" />
)}
</button>
</div>
{/* Nav */}
<nav className="flex-1 overflow-y-auto px-2 pb-3">
<ul className="space-y-1">
{groups.map((group) => {
const defaultOpen = groupHasActiveItem(pathname, group);
// In collapsed mode, we render items as a flat icon list (no accordion)
if (collapsed) {
return (
<li key={group.label} className="pt-1">
<div className="my-2 h-px bg-zinc-900" />
<div className="flex flex-col gap-1">
{group.items.map((item) => {
const active = isActiveRoute(pathname, item.href);
return (
<Link
key={item.href}
href={item.href}
title={`${group.label}${item.label}`}
className={cx(
"flex h-10 items-center justify-center rounded-md border border-transparent",
active
? "bg-white/5 text-white ring-1 ring-zinc-800"
: "text-zinc-400 hover:bg-white/5 hover:text-zinc-200"
)}
>
<span className="inline-flex">{item.icon}</span>
</Link>
);
})}
</div>
</li>
);
}
return (
<li key={group.label}>
<Disclosure defaultOpen={defaultOpen}>
{({ open }) => (
<>
<DisclosureButton
className={cx(
"group flex w-full items-center justify-between rounded-md px-2 py-2 text-left text-xs font-semibold uppercase tracking-[0.18em]",
open ? "bg-white/5 text-zinc-100" : "text-zinc-400 hover:bg-white/5 hover:text-zinc-200"
)}
>
<span className="flex items-center gap-2">
<span className="inline-flex text-zinc-400 group-hover:text-zinc-200">
{group.icon}
</span>
{group.label}
</span>
<ChevronRight
className={cx(
"h-4 w-4 text-zinc-500 transition-transform",
open && "rotate-90"
)}
/>
</DisclosureButton>
<DisclosurePanel className="mt-1 space-y-1 pl-1">
{group.items.map((item) => {
const active = isActiveRoute(pathname, item.href);
return (
<Link
key={item.href}
href={item.href}
className={cx(
"flex items-center gap-2 rounded-md px-2 py-2 text-sm",
active
? "bg-white/5 text-white ring-1 ring-zinc-800"
: "text-zinc-300 hover:bg-white/5 hover:text-white"
)}
>
<span className="inline-flex h-5 w-5 items-center justify-center text-zinc-400">
{item.icon}
</span>
<span className="truncate">{item.label}</span>
</Link>
);
})}
</DisclosurePanel>
</>
)}
</Disclosure>
</li>
);
})}
</ul>
</nav>
{/* Footer */}
<div className={cx("border-t border-zinc-900 p-3", collapsed && "px-2")}>
<Link
href="/builder"
className={cx(
"mb-3 inline-flex w-full items-center justify-center rounded-md border border-zinc-800 bg-zinc-900/40 px-3 py-2 text-xs font-medium text-zinc-200 hover:bg-zinc-900/70",
collapsed && "px-2"
)}
title="Back to Builder"
>
{collapsed ? "↩" : "Back to Builder"}
</Link>
<div
className={cx(
"flex items-center gap-3 rounded-md bg-zinc-900/30 px-3 py-2 ring-1 ring-zinc-800",
collapsed && "justify-center px-2"
)}
title={secondary ?? identity}
>
<div className="h-8 w-8 rounded-full bg-zinc-900 ring-1 ring-zinc-800" />
{!collapsed && (
<div className="min-w-0">
<div className="truncate text-sm text-zinc-200">{identity}</div>
<div className="truncate text-xs text-zinc-500">
{secondary ?? "Authenticated user"}
</div>
</div>
)}
</div>
</div>
</aside>
);
}