← 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>
  );
}