← back to Stayclaim
src/app/admin/layout.tsx
88 lines
import Link from 'next/link';
import { headers } from 'next/headers';
import type { Metadata } from 'next';
// Admin shell must never be statically optimized or edge-cached.
// Auth is enforced by middleware.ts adminGate(); this is defense-in-depth.
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
export const metadata: Metadata = {
robots: { index: false, follow: false, nocache: true, noarchive: true },
title: { default: 'Admin', template: '%s · Admin' },
};
const TABS = [
{ href: '/admin', label: 'Overview' },
{ href: '/admin/sources', label: 'Sources' },
{ href: '/admin/data', label: 'Data browser' },
{ href: '/admin/ingests', label: 'Ingests' },
{ href: '/admin/quality', label: 'Data quality' },
{ href: '/admin/claims', label: 'Claims' },
{ href: '/admin/seeds', label: 'Seed data' },
];
function isActive(pathname: string, href: string): boolean {
if (href === '/admin') return pathname === '/admin' || pathname === '/admin/';
return pathname === href || pathname.startsWith(href + '/');
}
export default function AdminLayout({ children }: { children: React.ReactNode }) {
// Next 14 sets `x-invoke-path` / `x-pathname` / `next-url` on the incoming
// headers. We try a couple of conventional ones; missing → no active tab,
// which is harmless.
const h = headers();
const pathname =
h.get('x-invoke-path') ||
h.get('x-pathname') ||
h.get('next-url') ||
'';
const isDev = process.env.NODE_ENV !== 'production';
return (
<div className="min-h-screen bg-sand">
<header className="border-b border-ink/10 bg-ink text-sand">
<div className="max-w-7xl mx-auto px-6 py-4 flex items-center justify-between gap-6">
<Link href="/admin" className="font-display text-xl text-sand hover:text-coral transition">
◢ Admin Console
</Link>
{isDev ? (
<span className="text-[10px] uppercase tracking-[0.15em] text-coral/80 font-mono">
dev environment
</span>
) : null}
</div>
<nav
aria-label="Admin sections"
className="max-w-7xl mx-auto px-6 flex flex-wrap gap-1 text-xs uppercase tracking-wider"
>
{TABS.map(t => {
const active = isActive(pathname, t.href);
return (
<Link
key={t.href}
href={t.href}
aria-current={active ? 'page' : undefined}
className={
'px-3 py-2 transition border-b ' +
(active
? 'bg-coral text-ink border-ink'
: 'border-transparent hover:bg-coral hover:text-ink')
}
>
{t.label}
</Link>
);
})}
</nav>
</header>
<main className="max-w-7xl mx-auto px-6 py-8">{children}</main>
<footer className="max-w-7xl mx-auto px-6 py-8 mt-12 border-t border-ink/10 text-[10px] uppercase tracking-[0.15em] text-ink/40 flex justify-between">
<span>admin</span>
{isDev ? <span>dev mode</span> : null}
</footer>
</div>
);
}