310 lines
13 KiB
TypeScript
310 lines
13 KiB
TypeScript
"use client";
|
|
import { useState, ReactNode } from 'react';
|
|
import { usePathname } from 'next/navigation';
|
|
import { useSession } from 'next-auth/react';
|
|
import {
|
|
Dialog,
|
|
DialogBackdrop,
|
|
DialogPanel,
|
|
Menu,
|
|
MenuButton,
|
|
MenuItem,
|
|
MenuItems,
|
|
TransitionChild,
|
|
} from '@headlessui/react';
|
|
import {
|
|
Bars3Icon,
|
|
BellIcon,
|
|
CalendarIcon,
|
|
ChartPieIcon,
|
|
Cog6ToothIcon,
|
|
DocumentDuplicateIcon,
|
|
FolderIcon,
|
|
HomeIcon,
|
|
UsersIcon,
|
|
XMarkIcon,
|
|
CubeIcon,
|
|
} from '@heroicons/react/24/outline';
|
|
import { ChevronDownIcon, MagnifyingGlassIcon } from '@heroicons/react/20/solid';
|
|
|
|
const navigation = [
|
|
{ name: 'Dashboard', href: '/admin', icon: HomeIcon },
|
|
{ name: 'Users', href: '/admin/users', icon: UsersIcon },
|
|
{ name: 'Category Mapping', href: '/admin/category-mapping', icon: ChartPieIcon },
|
|
{ name: 'Products', href: '/admin/products', icon: CubeIcon },
|
|
// { name: 'Settings', href: '/admin/settings', icon: Cog6ToothIcon }, // optional/future
|
|
];
|
|
const userNavigation = [
|
|
{ name: 'Your profile', href: '/account/profile' },
|
|
{ name: 'Sign out', href: '/api/auth/signout' },
|
|
];
|
|
|
|
function classNames(...classes: string[]) {
|
|
return classes.filter(Boolean).join(' ');
|
|
}
|
|
|
|
export default function AdminNavbar({ children }: { children: ReactNode }) {
|
|
const [sidebarOpen, setSidebarOpen] = useState(false);
|
|
const pathname = usePathname();
|
|
const { data: session } = useSession();
|
|
|
|
// Get user display name
|
|
const getUserDisplayName = () => {
|
|
if (!session?.user) return 'Admin User';
|
|
|
|
const user = session.user as any;
|
|
if (user.first_name && user.last_name) {
|
|
return `${user.first_name} ${user.last_name}`;
|
|
}
|
|
if (user.name) {
|
|
return user.name;
|
|
}
|
|
if (user.email) {
|
|
return user.email.split('@')[0]; // Use email prefix as fallback
|
|
}
|
|
return 'Admin User';
|
|
};
|
|
|
|
const getUserInitials = () => {
|
|
if (!session?.user) return 'A';
|
|
|
|
const user = session.user as any;
|
|
if (user.first_name && user.last_name) {
|
|
return `${user.first_name[0]}${user.last_name[0]}`.toUpperCase();
|
|
}
|
|
if (user.name) {
|
|
return user.name.split(' ').map((n: string) => n[0]).join('').toUpperCase().slice(0, 2);
|
|
}
|
|
if (user.email) {
|
|
return user.email[0].toUpperCase();
|
|
}
|
|
return 'A';
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<div>
|
|
<Dialog open={sidebarOpen} onClose={setSidebarOpen} className="relative z-50 lg:hidden">
|
|
<DialogBackdrop
|
|
transition
|
|
className="fixed inset-0 bg-gray-900/80 transition-opacity duration-300 ease-linear data-[closed]:opacity-0"
|
|
/>
|
|
|
|
<div className="fixed inset-0 flex">
|
|
<DialogPanel
|
|
transition
|
|
className="relative mr-16 flex w-full max-w-xs flex-1 transform transition duration-300 ease-in-out data-[closed]:-translate-x-full"
|
|
>
|
|
<TransitionChild>
|
|
<div className="absolute left-full top-0 flex w-16 justify-center pt-5 duration-300 ease-in-out data-[closed]:opacity-0">
|
|
<button type="button" onClick={() => setSidebarOpen(false)} className="-m-2.5 p-2.5">
|
|
<span className="sr-only">Close sidebar</span>
|
|
<XMarkIcon aria-hidden="true" className="size-6 text-white" />
|
|
</button>
|
|
</div>
|
|
</TransitionChild>
|
|
|
|
{/* Sidebar component */}
|
|
<div className="flex grow flex-col gap-y-5 overflow-y-auto bg-white px-6 pb-4">
|
|
<div className="flex h-16 shrink-0 items-center">
|
|
<img
|
|
alt="Your Company"
|
|
src="https://tailwindcss.com/plus-assets/img/logos/mark.svg?color=indigo&shade=600"
|
|
className="h-8 w-auto"
|
|
/>
|
|
</div>
|
|
<nav className="flex flex-1 flex-col">
|
|
<ul role="list" className="flex flex-1 flex-col gap-y-7">
|
|
<li>
|
|
<ul role="list" className="-mx-2 space-y-1">
|
|
{navigation.map((item) => (
|
|
<li key={item.name}>
|
|
<a
|
|
href={item.href}
|
|
className={classNames(
|
|
pathname === item.href
|
|
? 'bg-gray-50 text-indigo-600'
|
|
: 'text-gray-700 hover:bg-gray-50 hover:text-indigo-600',
|
|
'group flex gap-x-3 rounded-md p-2 text-sm/6 font-semibold',
|
|
)}
|
|
>
|
|
<item.icon
|
|
aria-hidden="true"
|
|
className={classNames(
|
|
pathname === item.href ? 'text-indigo-600' : 'text-gray-400 group-hover:text-indigo-600',
|
|
'size-6 shrink-0',
|
|
)}
|
|
/>
|
|
{item.name}
|
|
</a>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</li>
|
|
<li className="mt-auto">
|
|
<a
|
|
href="#"
|
|
className="group -mx-2 flex gap-x-3 rounded-md p-2 text-sm/6 font-semibold text-gray-700 hover:bg-gray-50 hover:text-indigo-600"
|
|
>
|
|
<Cog6ToothIcon
|
|
aria-hidden="true"
|
|
className="size-6 shrink-0 text-gray-400 group-hover:text-indigo-600"
|
|
/>
|
|
Settings
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</nav>
|
|
</div>
|
|
</DialogPanel>
|
|
</div>
|
|
</Dialog>
|
|
|
|
{/* Static sidebar for desktop */}
|
|
<div className="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-72 lg:flex-col">
|
|
{/* Sidebar component */}
|
|
<div className="flex grow flex-col gap-y-5 overflow-y-auto border-r border-gray-200 bg-white px-6 pb-4">
|
|
<div className="flex h-16 shrink-0 items-center">
|
|
<img
|
|
alt="Your Company"
|
|
src="https://tailwindcss.com/plus-assets/img/logos/mark.svg?color=indigo&shade=600"
|
|
className="h-8 w-auto"
|
|
/>
|
|
</div>
|
|
<nav className="flex flex-1 flex-col">
|
|
<ul role="list" className="flex flex-1 flex-col gap-y-7">
|
|
<li>
|
|
<ul role="list" className="-mx-2 space-y-1">
|
|
{navigation.map((item) => (
|
|
<li key={item.name}>
|
|
<a
|
|
href={item.href}
|
|
className={classNames(
|
|
pathname === item.href
|
|
? 'bg-gray-50 text-indigo-600'
|
|
: 'text-gray-700 hover:bg-gray-50 hover:text-indigo-600',
|
|
'group flex gap-x-3 rounded-md p-2 text-sm/6 font-semibold',
|
|
)}
|
|
>
|
|
<item.icon
|
|
aria-hidden="true"
|
|
className={classNames(
|
|
pathname === item.href ? 'text-indigo-600' : 'text-gray-400 group-hover:text-indigo-600',
|
|
'size-6 shrink-0',
|
|
)}
|
|
/>
|
|
{item.name}
|
|
</a>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</li>
|
|
<li className="mt-auto">
|
|
<a
|
|
href="#"
|
|
className="group -mx-2 flex gap-x-3 rounded-md p-2 text-sm/6 font-semibold text-gray-700 hover:bg-gray-50 hover:text-indigo-600"
|
|
>
|
|
<Cog6ToothIcon
|
|
aria-hidden="true"
|
|
className="size-6 shrink-0 text-gray-400 group-hover:text-indigo-600"
|
|
/>
|
|
Settings
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="lg:pl-72">
|
|
<div className="sticky top-0 z-40 flex h-16 shrink-0 items-center gap-x-4 border-b border-gray-200 bg-white px-4 shadow-sm sm:gap-x-6 sm:px-6 lg:px-8">
|
|
<button type="button" onClick={() => setSidebarOpen(true)} className="-m-2.5 p-2.5 text-gray-700 lg:hidden">
|
|
<span className="sr-only">Open sidebar</span>
|
|
<Bars3Icon aria-hidden="true" className="size-6" />
|
|
</button>
|
|
|
|
{/* Separator */}
|
|
<div aria-hidden="true" className="h-6 w-px bg-gray-200 lg:hidden" />
|
|
|
|
<div className="flex flex-1 gap-x-4 self-stretch lg:gap-x-6">
|
|
<form action="#" method="GET" className="grid flex-1 grid-cols-1">
|
|
<input
|
|
name="search"
|
|
type="search"
|
|
placeholder="Search"
|
|
aria-label="Search"
|
|
className="col-start-1 row-start-1 block size-full bg-white pl-8 text-base text-gray-900 outline-none placeholder:text-gray-400 sm:text-sm/6"
|
|
/>
|
|
<MagnifyingGlassIcon
|
|
aria-hidden="true"
|
|
className="pointer-events-none col-start-1 row-start-1 size-5 self-center text-gray-400"
|
|
/>
|
|
</form>
|
|
|
|
{/* Back to Site Button */}
|
|
<a
|
|
href="/"
|
|
className="inline-flex items-center px-3 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 transition-colors"
|
|
>
|
|
<HomeIcon className="w-4 h-4 mr-2" />
|
|
Back to Site
|
|
</a>
|
|
<div className="flex items-center gap-x-4 lg:gap-x-6">
|
|
<button type="button" className="-m-2.5 p-2.5 text-gray-400 hover:text-gray-500">
|
|
<span className="sr-only">View notifications</span>
|
|
<BellIcon aria-hidden="true" className="size-6" />
|
|
</button>
|
|
|
|
{/* Separator */}
|
|
<div aria-hidden="true" className="hidden lg:block lg:h-6 lg:w-px lg:bg-gray-200" />
|
|
|
|
{/* Profile dropdown */}
|
|
<Menu as="div" className="relative">
|
|
<MenuButton className="relative flex items-center">
|
|
<span className="absolute -inset-1.5" />
|
|
<span className="sr-only">Open user menu</span>
|
|
<div className="size-8 rounded-full bg-indigo-600 flex items-center justify-center">
|
|
<span className="text-sm font-medium text-white">
|
|
{getUserInitials()}
|
|
</span>
|
|
</div>
|
|
<span className="hidden lg:flex lg:items-center">
|
|
<span aria-hidden="true" className="ml-4 text-sm/6 font-semibold text-gray-900">
|
|
{getUserDisplayName()}
|
|
</span>
|
|
<ChevronDownIcon aria-hidden="true" className="ml-2 size-5 text-gray-400" />
|
|
</span>
|
|
</MenuButton>
|
|
<MenuItems
|
|
transition
|
|
className="absolute right-0 z-10 mt-2.5 w-48 origin-top-right rounded-md bg-white py-2 shadow-lg ring-1 ring-gray-900/5 transition focus:outline-none data-[closed]:scale-95 data-[closed]:transform data-[closed]:opacity-0 data-[enter]:duration-100 data-[leave]:duration-75 data-[enter]:ease-out data-[leave]:ease-in"
|
|
>
|
|
{session?.user && (
|
|
<div className="px-3 py-2 text-xs text-gray-500 border-b border-gray-100">
|
|
{session.user.email}
|
|
</div>
|
|
)}
|
|
{userNavigation.map((item) => (
|
|
<MenuItem key={item.name}>
|
|
<a
|
|
href={item.href}
|
|
className="block px-3 py-2 text-sm text-gray-900 data-[focus]:bg-gray-50 data-[focus]:outline-none hover:bg-gray-50"
|
|
>
|
|
{item.name}
|
|
</a>
|
|
</MenuItem>
|
|
))}
|
|
</MenuItems>
|
|
</Menu>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<main className="py-10">
|
|
{children}
|
|
</main>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|