124 lines
3.7 KiB
TypeScript
124 lines
3.7 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import { useRouter, usePathname } from 'next/navigation';
|
|
import Link from 'next/link';
|
|
import {
|
|
AppShell,
|
|
AppShellSkipLink,
|
|
AppShellMobileToggle,
|
|
AppShellOverlay,
|
|
AppShellSidebar,
|
|
AppShellNav,
|
|
AppShellNavItem,
|
|
AppShellMain,
|
|
Button,
|
|
} from '@/components/ui/Primitives';
|
|
import { useAuth } from '@/lib/auth-context';
|
|
import { useTheme } from '@/lib/theme-context';
|
|
import { ProductSwitcher } from '@/components/product-switcher';
|
|
import { SystemBanners } from '@/components/system-banners';
|
|
|
|
const NAV_ITEMS = [
|
|
{ href: '/dashboard', label: 'Overview' },
|
|
{ href: '/dashboard/items', label: 'Items' },
|
|
{ href: '/dashboard/board', label: 'Board' },
|
|
{ href: '/dashboard/fleet', label: 'Fleet' },
|
|
{ href: '/dashboard/settings', label: 'Settings' },
|
|
];
|
|
|
|
/** Open the ⌘K command palette by replaying the global hotkey. */
|
|
function openCommandPalette() {
|
|
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'k', metaKey: true, ctrlKey: true }));
|
|
}
|
|
|
|
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
|
|
const { user, loading, logout } = useAuth();
|
|
const { theme, setTheme } = useTheme();
|
|
const router = useRouter();
|
|
const pathname = usePathname();
|
|
const [navOpen, setNavOpen] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (!loading && !user) {
|
|
router.replace('/login');
|
|
}
|
|
}, [user, loading, router]);
|
|
|
|
if (loading || !user) {
|
|
return (
|
|
<div className="flex min-h-screen items-center justify-center">
|
|
<div className="text-muted-foreground">Loading...</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const go = (href: string) => (e: React.MouseEvent) => {
|
|
e.preventDefault();
|
|
setNavOpen(false);
|
|
router.push(href);
|
|
};
|
|
|
|
return (
|
|
<AppShell>
|
|
<AppShellSkipLink />
|
|
<AppShellMobileToggle open={navOpen} onClick={() => setNavOpen(o => !o)} />
|
|
<AppShellOverlay open={navOpen} onClick={() => setNavOpen(false)} />
|
|
|
|
<AppShellSidebar open={navOpen} label="Primary">
|
|
<div className="flex h-full flex-col gap-6 p-4">
|
|
<Link href="/dashboard" className="px-1 text-lg font-bold tracking-tight">
|
|
Tracker
|
|
</Link>
|
|
|
|
<AppShellNav>
|
|
{NAV_ITEMS.map(item => (
|
|
<AppShellNavItem
|
|
key={item.href}
|
|
href={item.href}
|
|
active={
|
|
item.href === '/dashboard'
|
|
? pathname === item.href
|
|
: pathname.startsWith(item.href)
|
|
}
|
|
onClick={go(item.href)}
|
|
>
|
|
{item.label}
|
|
</AppShellNavItem>
|
|
))}
|
|
</AppShellNav>
|
|
|
|
<div className="mt-auto space-y-3">
|
|
<ProductSwitcher />
|
|
<Button
|
|
variant="secondary"
|
|
size="sm"
|
|
className="w-full justify-start"
|
|
onClick={openCommandPalette}
|
|
>
|
|
Search… ⌘K
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="w-full justify-start"
|
|
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
|
|
>
|
|
{theme === 'dark' ? 'Light mode' : 'Dark mode'}
|
|
</Button>
|
|
<div className="truncate px-1 text-sm text-muted-foreground">{user.email}</div>
|
|
<Button variant="ghost" size="sm" className="w-full justify-start" onClick={logout}>
|
|
Sign out
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</AppShellSidebar>
|
|
|
|
<AppShellMain>
|
|
<SystemBanners />
|
|
{children}
|
|
</AppShellMain>
|
|
</AppShell>
|
|
);
|
|
}
|