"use client"; /** * `` — sidebar + main column shell for the v1.5 admin split. * * Mirrors `` (`src/components/settings/settings-shell.tsx`): * each section lives at its own route under `/admin/[section]/page.tsx` and * mounts inside this shell. The shell only owns navigation; the page itself * renders the section heading and content. * * Mobile-first per `docs/ui-guidelines.md`: * - =md: sticky 220px sidebar + content column with `max-w-screen-xl`. */ import * as React from "react"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { AlertTriangle, Bell, Database, KeyRound, Inbox, Plug, ScrollText, Server, Settings, ShieldAlert, Users, type LucideIcon, } from "lucide-react"; import { cn } from "@/lib/utils"; import { useTranslations } from "@/lib/i18n/context"; import { isAdminSectionSlug, type AdminSectionSlug } from "./section-slugs"; interface AdminSection { slug: AdminSectionSlug; /** i18n key under `admin.section..title`. */ titleKey: string; icon: LucideIcon; } /** * Source of truth for the admin section list. Order matches the in-app * navigation order so the sidebar, mobile strip, and `generateStaticParams()` * all line up. Don't reorder without updating tests. */ export const ADMIN_SECTIONS: readonly AdminSection[] = [ { slug: "system-status", titleKey: "admin.section.system-status.title", icon: Server, }, { slug: "general", titleKey: "admin.section.general.title", icon: Settings, }, { slug: "services", titleKey: "admin.section.services.title", icon: Plug, }, { slug: "integrations", titleKey: "admin.section.integrations.title", icon: AlertTriangle, }, { slug: "feedback", titleKey: "admin.section.feedback.title", icon: Inbox, }, { slug: "reminders", titleKey: "admin.section.reminders.title", icon: Bell, }, { slug: "users", titleKey: "admin.section.users.title", icon: Users, }, { slug: "api-tokens", titleKey: "admin.section.api-tokens.title", icon: KeyRound, }, { slug: "login-overview", titleKey: "admin.section.login-overview.title", icon: ScrollText, }, { slug: "backups", titleKey: "admin.section.backups.title", icon: Database, }, { slug: "danger-zone", titleKey: "admin.section.danger-zone.title", icon: ShieldAlert, }, ] as const; export interface AdminShellProps { /** * Optional override for the active section. When omitted the shell reads * the active slug from `usePathname()` (the production behaviour). Tests * pass an explicit value so they don't need to mock the router. */ active?: AdminSectionSlug; children: React.ReactNode; } function deriveActiveSlug( pathname: string | null, override?: AdminSectionSlug, ): AdminSectionSlug | null { if (override) return override; if (!pathname) return null; // Match `/admin/` and ignore any trailing segments. The // `/admin` overview page falls through to `null` so no entry is // marked active. const match = pathname.match(/^\/admin\/([^/]+)/); const candidate = match?.[1] ?? ""; return isAdminSectionSlug(candidate) ? candidate : null; } export function AdminShell({ active, children }: AdminShellProps) { const pathname = usePathname(); const { t } = useTranslations(); const activeSlug = deriveActiveSlug(pathname, active); return (
{/* Mobile section strip — horizontal scroll, hidden on md+. */}
{/* Desktop sticky sidebar */} {/* Main column — page renders its own h1 + subtitle */}
{children}
); }