layout.tsx85 lines · main
1import Image from 'next/image';
2import Link from 'next/link';
3import { notFound, redirect } from 'next/navigation';
4
5import { UserMenuButton } from '@/components/user-menu-button';
6import { getSessionUser } from '@/lib/session';
7
8import { CockpitMobileNav } from './cockpit-mobile-nav';
9import { CockpitNav } from './cockpit-nav';
10
11export const metadata = {
12 title: 'admin',
13 // Explicit briven favicon on the admin cockpit pages (don't rely on
14 // inherited metadata — these live in a separate route group).
15 icons: {
16 icon: [{ url: '/favicon.svg', type: 'image/svg+xml' }],
17 shortcut: '/favicon.svg',
18 },
19};
20
21/**
22 * Superadmin cockpit shell + gate.
23 *
24 * Gate: reuses the existing Better Auth session via getSessionUser()
25 * (apps/api /v1/me, cookies forwarded by apiFetch). An unauthenticated
26 * visitor is sent to /admin/login; a signed-in non-admin gets notFound()
27 * — the same contract the former /dashboard/admin layout used before the
28 * admin pages were consolidated into this cockpit route group.
29 *
30 * The login page lives in a SEPARATE route group ((admin-auth)) so it is
31 * NOT wrapped by this layout — that is what prevents a redirect loop when
32 * an unauthenticated user lands on /admin/login.
33 */
34export default async function AdminCockpitLayout({
35 children,
36}: {
37 children: React.ReactNode;
38}) {
39 const user = await getSessionUser();
40 if (!user) redirect('/admin/login');
41 if (!user.isAdmin) notFound();
42
43 return (
44 <div className="flex h-dvh flex-col bg-[var(--color-bg)] text-[var(--color-text)]">
45 <header className="shrink-0 border-b border-[var(--color-border-subtle)]">
46 <div className="flex items-center justify-between px-6 py-4">
47 <Link href="/admin" className="flex items-center gap-3" aria-label="briven admin">
48 <Image src="/icon.svg" alt="" width={24} height={24} priority />
49 <span className="font-mono text-sm">briven admin</span>
50 </Link>
51 {/* Same dropdown as the user dashboard — lets the super admin pivot
52 between admin ↔ user dashboard ↔ website ↔ docs, and sign out.
53 variant="admin" swaps the first row to a "dashboard" link;
54 placement="down" opens it below this top header. */}
55 <div className="w-56">
56 <UserMenuButton
57 user={{
58 name: user.name,
59 email: user.email,
60 image: user.image,
61 legalName: user.legalName,
62 }}
63 collapsed={false}
64 isAdmin={user.isAdmin}
65 placement="down"
66 variant="admin"
67 />
68 </div>
69 </div>
70 </header>
71
72 <CockpitMobileNav />
73
74 <div className="flex min-h-0 flex-1">
75 <CockpitNav />
76 {/* Command-deck spacing: generous padding and a wide-but-bounded
77 content column so dashboards breathe on big monitors without
78 stretching into unreadable line lengths. */}
79 <main className="min-w-0 flex-1 overflow-y-auto p-6 md:p-8 lg:p-10">
80 <div className="mx-auto w-full max-w-[1440px]">{children}</div>
81 </main>
82 </div>
83 </div>
84 );
85}