spb/datacenterindex
Public
HTML 53.9%
TypeScript 44.5%
JavaScript 0.6%
SQL 0.5%
1"use client";23import { Activity, ArrowUpRight, Database, FileText, Gauge, GitMerge, Hammer, LogOut, Plug, ShieldCheck, Wrench, Zap } from "lucide-react";4import Link from "next/link";5import { usePathname } from "next/navigation";6import type { ReactNode } from "react";7import { LogoMark } from "@/components/brand/logo";8import { ThemeToggle } from "@/components/theme";9import { cn } from "@/components/ui/cn";10import { ToastProvider } from "./toast";1112export const ADMIN_NAV = [13 { label: "Overview", href: "/admin", icon: Activity, match: (p: string) => p === "/admin" },14 { label: "Connectors", href: "/admin/connectors", icon: Plug, match: (p: string) => p.startsWith("/admin/connectors") },15 { label: "Documents", href: "/admin/documents", icon: FileText, match: (p: string) => p.startsWith("/admin/documents") },16 { label: "Matches", href: "/admin/matches", icon: GitMerge, match: (p: string) => p.startsWith("/admin/matches") },17 { label: "Quality", href: "/admin/quality", icon: ShieldCheck, match: (p: string) => p.startsWith("/admin/quality") },18 { label: "Projects", href: "/admin/projects", icon: Hammer, match: (p: string) => p.startsWith("/admin/projects") },19 { label: "Data gaps", href: "/admin/data-gaps", icon: Gauge, match: (p: string) => p.startsWith("/admin/data-gaps") },20 { label: "Events", href: "/admin/events", icon: Zap, match: (p: string) => p.startsWith("/admin/events") },21 { label: "Dev tool", href: "/admin/devtool", icon: Wrench, match: (p: string) => p.startsWith("/admin/devtool") },22 { label: "Sources", href: "/admin/sources", icon: Database, match: (p: string) => p.startsWith("/admin/sources") },23] as const;2425/** Compact operational shell: left rail ≥ md, horizontal tabs below (sticky) on phones. */26export function AdminShell({ children }: { children: ReactNode }) {27 const path = usePathname();28 return (29 <ToastProvider>30 <div className="flex min-h-dvh">31 <aside className="sticky top-0 hidden h-dvh w-[200px] shrink-0 flex-col border-r border-hair bg-surface md:flex">32 <div className="flex h-12 items-center gap-2 border-b border-hair px-3">33 <Link href="/admin" className="flex items-center gap-2 text-ink">34 <LogoMark size={20} />35 <span className="text-[13px] font-semibold tracking-tight">DCI Admin</span>36 </Link>37 </div>38 <nav aria-label="Admin" className="flex-1 overflow-y-auto py-2">39 <ul className="space-y-px px-2">40 {ADMIN_NAV.map((n) => {41 const active = n.match(path);42 const Icon = n.icon;43 return (44 <li key={n.href}>45 <Link href={n.href} aria-current={active ? "page" : undefined} className={cn("flex h-9 items-center gap-2.5 rounded-sm px-2 text-[13px]", active ? "bg-surface-2 font-medium text-ink" : "text-ink-2 hover:bg-surface-2 hover:text-ink")}>46 <Icon className={cn("size-4", active ? "text-accent" : "text-ink-3")} strokeWidth={1.75} />47 {n.label}48 </Link>49 </li>50 );51 })}52 </ul>53 </nav>54 <div className="space-y-1 border-t border-hair p-2">55 <a href="/" className="flex h-9 items-center gap-2 rounded-sm px-2 text-[12.5px] text-ink-2 hover:bg-surface-2 hover:text-ink" target="_blank" rel="noopener">56 <ArrowUpRight className="size-4 text-ink-3" strokeWidth={1.75} /> View site57 </a>58 <div className="flex items-center justify-between px-1">59 <ThemeToggle />60 <form action="/admin/logout" method="post">61 <button type="submit" className="inline-flex h-9 items-center gap-1.5 rounded-sm px-2 text-[12.5px] text-ink-2 hover:bg-surface-2 hover:text-ink">62 <LogOut className="size-4 text-ink-3" strokeWidth={1.75} /> Log out63 </button>64 </form>65 </div>66 </div>67 </aside>6869 <div className="flex min-w-0 flex-1 flex-col">70 <header className="sticky top-0 z-[60] border-b border-hair bg-paper/95 backdrop-blur md:hidden">71 <div className="flex h-12 items-center gap-2 px-3">72 <Link href="/admin" className="flex items-center gap-2 text-ink">73 <LogoMark size={20} />74 <span className="text-[13px] font-semibold tracking-tight">DCI Admin</span>75 </Link>76 <div className="ml-auto flex items-center gap-0.5">77 <ThemeToggle />78 <form action="/admin/logout" method="post">79 <button type="submit" aria-label="Log out" className="inline-flex size-9 items-center justify-center rounded-sm text-ink-2 hover:bg-surface-2">80 <LogOut className="size-4" strokeWidth={1.75} />81 </button>82 </form>83 </div>84 </div>85 <nav aria-label="Admin" className="flex overflow-x-auto border-t border-hair px-2 no-scrollbar">86 {ADMIN_NAV.map((n) => {87 const active = n.match(path);88 return (89 <Link key={n.href} href={n.href} aria-current={active ? "page" : undefined} className={cn("relative inline-flex h-10 shrink-0 items-center whitespace-nowrap px-2.5 text-[12.5px] font-medium", active ? "text-ink after:absolute after:inset-x-2 after:bottom-0 after:h-[2px] after:bg-ink" : "text-ink-3")}>90 {n.label}91 </Link>92 );93 })}94 </nav>95 </header>96 <main id="main" className="min-w-0 flex-1 px-3 pb-10 pt-3 sm:px-5 sm:pt-4 lg:px-6">97 {children}98 </main>99 </div>100 </div>101 </ToastProvider>102 );103}104105/** Page title row for admin pages (denser than the public PageHeader). */106export function AdminPageHeader({ title, eyebrow, description, aside, className }: { title: ReactNode; eyebrow?: ReactNode; description?: ReactNode; aside?: ReactNode; className?: string }) {107 return (108 <header className={cn("mb-3 flex flex-col gap-2 sm:mb-4 sm:flex-row sm:items-end sm:justify-between", className)}>109 <div className="min-w-0">110 {eyebrow && <div className="label-caps mb-1">{eyebrow}</div>}111 <h1 className="truncate text-[18px] font-semibold leading-tight tracking-[-0.01em] text-ink sm:text-[20px]">{title}</h1>112 {description && <p className="mt-0.5 text-[12.5px] text-ink-3">{description}</p>}113 </div>114 {aside && <div className="flex shrink-0 flex-wrap items-center gap-2">{aside}</div>}115 </header>116 );117}118