new admin ui
This commit is contained in:
@@ -1,109 +1,199 @@
|
||||
"use client";
|
||||
|
||||
import type React from "react";
|
||||
import * as React from "react";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
LayoutDashboard,
|
||||
Download,
|
||||
Layers,
|
||||
Boxes,
|
||||
Store,
|
||||
Users,
|
||||
Settings,
|
||||
LucideMail,
|
||||
Wand2,
|
||||
} from "lucide-react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Disclosure, DisclosureButton, DisclosurePanel } from "@headlessui/react";
|
||||
import { ChevronRight, PanelLeftClose, PanelLeftOpen } from "lucide-react";
|
||||
|
||||
export type AdminNavItem = {
|
||||
label: string;
|
||||
href: string;
|
||||
icon: React.ReactNode;
|
||||
icon?: React.ReactNode;
|
||||
};
|
||||
|
||||
type AdminLeftNavigationProps = {
|
||||
collapsed: boolean;
|
||||
onToggleCollapsed: () => void;
|
||||
items?: AdminNavItem[]; // ✅ NEW
|
||||
export type AdminNavGroup = {
|
||||
label: string;
|
||||
icon?: React.ReactNode;
|
||||
items: AdminNavItem[];
|
||||
};
|
||||
|
||||
const defaultNavItems: AdminNavItem[] = [
|
||||
{ label: "Dashboard", href: "/admin", icon: <LayoutDashboard className="h-4 w-4" /> },
|
||||
{ label: "Imports", href: "/admin/import-status", icon: <Download className="h-4 w-4" /> },
|
||||
{ label: "Mappings", href: "/admin/mapping", icon: <Layers className="h-4 w-4" /> },
|
||||
{ label: "Products", href: "/admin/products", icon: <Boxes className="h-4 w-4" /> },
|
||||
{ label: "Merchants", href: "/admin/merchants", icon: <Store className="h-4 w-4" /> },
|
||||
{ label: "Platforms", href: "/admin/platforms", icon: <Layers className="h-4 w-4" /> },
|
||||
{ label: "Enrichment", href: "/admin/enrichment", icon: <Wand2 className="h-4 w-4" /> },
|
||||
{ label: "Users", href: "/admin/users", icon: <Users className="h-4 w-4" /> },
|
||||
{ label: "Settings", href: "/admin/settings", icon: <Settings className="h-4 w-4" /> },
|
||||
{ label: "Manage Emails", href: "/admin/email/manage", icon: <LucideMail className="h-4 w-4" /> },
|
||||
{ label: "Send an Email", href: "/admin/email/send", icon: <LucideMail className="h-4 w-4" /> },
|
||||
{ label: "Beta Invites", href: "/admin/beta-invites", icon: <LucideMail className="h-4 w-4" />,
|
||||
},
|
||||
];
|
||||
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,
|
||||
items = defaultNavItems, // ✅ NEW default
|
||||
}: AdminLeftNavigationProps) {
|
||||
groups,
|
||||
}: {
|
||||
collapsed: boolean;
|
||||
onToggleCollapsed: () => void;
|
||||
groups: AdminNavGroup[];
|
||||
}) {
|
||||
const pathname = usePathname() || "/admin";
|
||||
|
||||
return (
|
||||
<aside
|
||||
className={`hidden border-r border-zinc-900 bg-zinc-950/80 px-3 py-6 md:flex md:flex-col transition-all duration-200 ${
|
||||
collapsed ? "w-16" : "w-60"
|
||||
}`}
|
||||
className={cx(
|
||||
"sticky top-0 flex h-screen shrink-0 flex-col border-r border-zinc-900 bg-zinc-950/60 backdrop-blur",
|
||||
collapsed ? "w-[68px]" : "w-[280px]"
|
||||
)}
|
||||
>
|
||||
<div className="mb-6 flex items-center gap-2">
|
||||
{/* 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="inline-flex h-8 w-8 items-center justify-center rounded-md border border-zinc-800 bg-zinc-950 text-zinc-400 transition hover:border-amber-400/60 hover:text-amber-300"
|
||||
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"}
|
||||
>
|
||||
<span className="sr-only">Toggle sidebar</span>
|
||||
<div className="space-y-0.5">
|
||||
<span className="block h-[1px] w-3 bg-current" />
|
||||
<span className="block h-[1px] w-3 bg-current" />
|
||||
<span className="block h-[1px] w-3 bg-current" />
|
||||
</div>
|
||||
{collapsed ? (
|
||||
<PanelLeftOpen className="h-4 w-4" />
|
||||
) : (
|
||||
<PanelLeftClose className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{!collapsed && (
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-zinc-500">
|
||||
<Link href="/" title="Back to Battl Dashboard">
|
||||
Battl Builders
|
||||
</Link>
|
||||
</p>
|
||||
<p className="text-[10px] text-zinc-600">Admin Command</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 space-y-1 text-sm">
|
||||
{items.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className="flex items-center justify-between rounded-md px-2 py-1.5 text-zinc-300 transition hover:bg-zinc-900 hover:text-amber-300"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{item.icon}
|
||||
{!collapsed && <span>{item.label}</span>}
|
||||
</div>
|
||||
{!collapsed && <span className="text-[10px] text-zinc-600">›</span>}
|
||||
</Link>
|
||||
))}
|
||||
{/* 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>
|
||||
|
||||
{!collapsed && (
|
||||
<div className="mt-6 border-t border-zinc-900 pt-4 text-[11px] text-zinc-600">
|
||||
<p className="text-[10px] uppercase tracking-[0.16em] text-zinc-500">Status</p>
|
||||
<p className="mt-1">
|
||||
Import engine: <span className="text-emerald-400">online</span>
|
||||
</p>
|
||||
<p className="text-zinc-500">Builder UI: in progress</p>
|
||||
{/* Footer */}
|
||||
<div className={cx("border-t border-zinc-900 p-3", collapsed && "px-2")}>
|
||||
<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="Admin user"
|
||||
>
|
||||
<div className="h-8 w-8 rounded-full bg-zinc-900 ring-1 ring-zinc-800" />
|
||||
{!collapsed && (
|
||||
<div className="min-w-0">
|
||||
<div className="text-xs uppercase tracking-[0.18em] text-zinc-500">
|
||||
Internal
|
||||
</div>
|
||||
<div className="truncate text-sm text-zinc-200">ADMIN</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user