SPB Git forge
38commits 1branches 0releases
338.7 MBsize
maindefault branch
4 h agolast push
HTML 53.9% TypeScript 44.5% JavaScript 0.6% SQL 0.5%
6.5 KB · 118 lines tsx
Raw Blame History
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