import React from 'react'; import { Link, NavLink, useLocation } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { LayoutDashboard, CalendarDays, Settings } from 'lucide-react'; import toast from 'react-hot-toast'; import { cn } from '@/lib/utils'; import { Badge } from '@/components/ui/badge'; import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger, } from '@/components/ui/sheet'; import { ADMIN_HOME_PATH, ADMIN_EVENTS_PATH, ADMIN_EVENT_VIEW_PATH, ADMIN_EVENT_PHOTOS_PATH, ADMIN_SETTINGS_PATH, ADMIN_BILLING_PATH, } from '../constants'; import { registerApiErrorListener } from '../lib/apiError'; import { getDashboardSummary, getEvents, getTenantPackagesOverview } from '../api'; import { NotificationCenter } from './NotificationCenter'; import { UserMenu } from './UserMenu'; import { useEventContext } from '../context/EventContext'; import { EventSwitcher, EventMenuBar } from './EventNav'; import { useAuth } from '../auth/context'; import { CommandShelf } from './CommandShelf'; type NavItem = { key: string; to: string; label: string; icon: React.ComponentType>; end?: boolean; highlight?: boolean; prefetchKey?: string; }; type PageTab = { key: string; label: string; href: string; badge?: React.ReactNode; }; interface AdminLayoutProps { title: string; subtitle?: string; actions?: React.ReactNode; children: React.ReactNode; disableCommandShelf?: boolean; tabs?: PageTab[]; currentTabKey?: string; } export function AdminLayout({ title, subtitle, actions, children, disableCommandShelf, tabs, currentTabKey }: AdminLayoutProps) { const { t } = useTranslation('common'); const prefetchedPathsRef = React.useRef>(new Set()); const { events } = useEventContext(); const singleEvent = events.length === 1 ? events[0] : null; const eventsPath = singleEvent?.slug ? ADMIN_EVENT_VIEW_PATH(singleEvent.slug) : ADMIN_EVENTS_PATH; const eventsLabel = events.length === 1 ? t('navigation.event', { defaultValue: 'Event' }) : t('navigation.events'); const photosPath = singleEvent?.slug ? ADMIN_EVENT_PHOTOS_PATH(singleEvent.slug) : ADMIN_EVENTS_PATH; const billingLabel = t('navigation.billing', { defaultValue: 'Paket' }); const baseNavItems = React.useMemo(() => [ { key: 'dashboard', to: ADMIN_HOME_PATH, label: t('navigation.dashboard'), icon: LayoutDashboard, end: true, prefetchKey: ADMIN_HOME_PATH, }, { key: 'events', to: eventsPath, label: eventsLabel, icon: CalendarDays, end: Boolean(singleEvent?.slug), highlight: events.length === 1, prefetchKey: ADMIN_EVENTS_PATH, }, { key: 'billing', to: ADMIN_BILLING_PATH, label: billingLabel, icon: Settings, prefetchKey: ADMIN_BILLING_PATH, }, ], [eventsLabel, eventsPath, billingLabel, singleEvent, events.length, t]); const { user } = useAuth(); const isMember = user?.role === 'member'; const navItems = React.useMemo( () => baseNavItems.filter((item) => { if (!isMember) { return true; } return !['dashboard', 'billing'].includes(item.key); }), [baseNavItems, isMember], ); const prefetchers = React.useMemo(() => ({ [ADMIN_HOME_PATH]: () => Promise.all([ getDashboardSummary(), getEvents(), getTenantPackagesOverview(), ]).then(() => undefined), [ADMIN_EVENTS_PATH]: () => getEvents().then(() => undefined), [ADMIN_BILLING_PATH]: () => getTenantPackagesOverview().then(() => undefined), [ADMIN_SETTINGS_PATH]: () => Promise.resolve(), }), []); const triggerPrefetch = React.useCallback( (path: string) => { if (prefetchedPathsRef.current.has(path)) { return; } const runner = prefetchers[path as keyof typeof prefetchers]; if (!runner) { return; } prefetchedPathsRef.current.add(path); Promise.resolve(runner()).catch(() => { prefetchedPathsRef.current.delete(path); }); }, [prefetchers], ); React.useEffect(() => { document.body.classList.add('tenant-admin-theme'); return () => { document.body.classList.remove('tenant-admin-theme'); }; }, []); React.useEffect(() => { const unsubscribe = registerApiErrorListener((detail) => { const fallback = t('errors.generic'); const message = detail?.message?.trim() ? detail.message : fallback; toast.error(message, { id: detail?.code ? `api-error-${detail.code}` : undefined, }); }); return unsubscribe; }, [t]); return (

{t('app.brand')}

{title}

{subtitle ?

{subtitle}

: null}
{disableCommandShelf ? : null} {actions}
{disableCommandShelf ? : } {tabs && tabs.length ? : null}
{children}
); } function PageTabsNav({ tabs, currentKey }: { tabs: PageTab[]; currentKey?: string }) { const location = useLocation(); const { t } = useTranslation('common'); const [mobileOpen, setMobileOpen] = React.useState(false); const isActive = (tab: PageTab): boolean => { if (currentKey) { return tab.key === currentKey; } return location.pathname === tab.href || location.pathname.startsWith(tab.href); }; const activeTab = React.useMemo(() => tabs.find((tab) => isActive(tab)), [tabs, location.pathname, currentKey]); const handleTabClick = React.useCallback( (tab: PageTab) => { setMobileOpen(false); const [path, hash] = tab.href.split('#'); if (location.pathname === path && hash) { window.location.hash = `#${hash}`; } }, [location.pathname], ); return (
{tabs.map((tab) => { const active = isActive(tab); return ( handleTabClick(tab)} className={cn( 'flex items-center gap-2 rounded-2xl px-4 py-2 text-sm font-semibold transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-rose-400/60', active ? 'bg-rose-600 text-white shadow shadow-rose-300/40' : 'bg-white text-slate-600 hover:text-slate-900 dark:bg-white/5 dark:text-slate-300 dark:hover:text-white' )} > {tab.label} {tab.badge !== undefined ? ( {tab.badge} ) : null} ); })}
{t('navigation.tabs.title')} {t('navigation.tabs.subtitle')}
{tabs.map((tab) => { const active = isActive(tab); return ( { handleTabClick(tab); setMobileOpen(false); }} className={cn( 'flex items-center justify-between rounded-2xl border px-4 py-3 text-sm font-medium shadow-sm transition', active ? 'border-rose-200 bg-rose-50 text-rose-700' : 'border-slate-200 bg-white text-slate-700 dark:border-white/10 dark:bg-white/5 dark:text-slate-200' )} > {tab.label} {tab.badge !== undefined ? ( {tab.badge} ) : null} ); })}
); } function TenantMobileNav({ items, onPrefetch, }: { items: NavItem[]; onPrefetch: (path: string) => void; }) { const { t } = useTranslation('common'); return ( ); }