SPB Git forge

spb/fetcha

Public
11commits 1branches 0releases
1.5 MBsize
maindefault branch
16 days agolast push
TypeScript 97.5% SQL 1.4% Python 0.8%
6.1 KB · 139 lines tsx
Raw Blame History
1"use client";2import * as React from "react";3import Link from "next/link";4import { usePathname } from "next/navigation";5import {6  Activity,7  ArrowLeft,8  Building2,9  ChevronDown,10  CreditCard,11  Flag,12  FolderKanban,13  Globe,14  KeyRound,15  LayoutDashboard,16  ListTree,17  Menu,18  Server,19  ShieldAlert,20  Users,21  Waypoints,22  Wrench,23  X,24} from "lucide-react";25import { cn } from "@/lib/utils";26import { LogoMark } from "@/components/ui/logo";27import { ThemeToggle } from "@/components/ui/theme";28import { Badge } from "@/components/ui/badge";29import { Button } from "@/components/ui/button";3031export const ADMIN_NAV = [32  { href: "/admin", label: "Overview", icon: LayoutDashboard, exact: true },33  { href: "/admin/users", label: "Users", icon: Users },34  { href: "/admin/access", label: "Access", icon: KeyRound },35  { href: "/admin/organizations", label: "Organizations", icon: Building2 },36  { href: "/admin/projects", label: "Projects", icon: FolderKanban },37  { href: "/admin/requests", label: "Requests", icon: ListTree },38  { href: "/admin/usage", label: "Usage", icon: Activity },39  { href: "/admin/providers", label: "Providers", icon: Server },40  { href: "/admin/routing", label: "Routing", icon: Waypoints },41  { href: "/admin/domains", label: "Domains", icon: Globe },42  { href: "/admin/billing", label: "Billing", icon: CreditCard },43  { href: "/admin/abuse", label: "Abuse", icon: ShieldAlert },44  { href: "/admin/system", label: "System", icon: Wrench },45  { href: "/admin/flags", label: "Flags", icon: Flag },46] as const;4748function AdminNav({ onNavigate, className }: { onNavigate?: () => void; className?: string }) {49  const pathname = usePathname();50  return (51    <nav className={cn("flex flex-col gap-0.5", className)} aria-label="Admin">52      {ADMIN_NAV.map((item) => {53        const active = "exact" in item && item.exact ? pathname === item.href : pathname === item.href || pathname.startsWith(item.href + "/");54        const Icon = item.icon;55        return (56          <Link57            key={item.href}58            href={item.href}59            onClick={onNavigate}60            aria-current={active ? "page" : undefined}61            className={cn(62              "group flex h-8 items-center gap-2.5 rounded-md px-2.5 text-[13.5px] font-medium transition-colors",63              active ? "bg-bg-muted text-fg" : "text-fg-muted hover:bg-bg-subtle hover:text-fg",64            )}65          >66            <Icon className={cn("size-4 shrink-0", active ? "text-fg" : "text-fg-subtle group-hover:text-fg-muted")} aria-hidden />67            {item.label}68          </Link>69        );70      })}71    </nav>72  );73}7475export function AdminShell({ children, user }: { children: React.ReactNode; user: { email: string; name: string } }) {76  const pathname = usePathname();77  // The menu is "open for" a given pathname, so it closes itself on navigation without an effect.78  const [openPath, setOpenPath] = React.useState<string | null>(null);79  const open = openPath === pathname;80  const setOpen = (v: boolean | ((prev: boolean) => boolean)) => setOpenPath((prev) => ((typeof v === "function" ? v(prev === pathname) : v) ? pathname : null));81  const current = ADMIN_NAV.find((i) => ("exact" in i && i.exact ? pathname === i.href : pathname === i.href || pathname.startsWith(i.href + "/")));8283  return (84    <div className="flex min-h-dvh flex-col bg-bg">85      <header className="sticky top-0 z-30 border-b border-white/10 bg-[#0a0d14] text-[#eef1f6]">86        <div className="flex h-12 items-center gap-3 px-4 sm:px-6">87          <Link href="/admin" className="flex items-center gap-2 rounded-md font-semibold tracking-[-0.03em] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/40">88            <LogoMark size={20} className="[&_rect]:fill-white [&_path]:stroke-[#0a0d14]" />89            <span className="text-[15px]">Fetcha Admin</span>90          </Link>91          <Badge className="border-transparent bg-[#c93b3b]/25 text-[#ffb4b4] uppercase tracking-wide" dot>92            Internal93          </Badge>94          <span className="hidden text-[12px] text-white/50 md:inline">Upstream provider names and costs are visible here. Never share screenshots externally.</span>95          <div className="ml-auto flex items-center gap-1">96            <Button asChild variant="ghost" size="sm" className="text-white/70 hover:bg-white/10 hover:text-white">97              <Link href="/dashboard">98                <ArrowLeft className="size-3.5" /> Dashboard99              </Link>100            </Button>101            <ThemeToggle className="text-white/70 hover:bg-white/10 hover:text-white" />102            <span className="ml-2 hidden max-w-[220px] truncate text-[12.5px] text-white/60 sm:inline" title={user.email}>103              {user.email}104            </span>105          </div>106        </div>107      </header>108109      <div className="flex flex-1">110        <aside className="hidden w-[220px] shrink-0 flex-col border-r border-border bg-bg-subtle/60 lg:flex">111          <div className="sticky top-12 max-h-[calc(100dvh-3rem)] overflow-y-auto px-3 py-4 scrollbar-thin">112            <AdminNav />113          </div>114        </aside>115116        <div className="flex min-w-0 flex-1 flex-col">117          <div className="border-b border-border bg-bg-subtle/60 px-4 py-2 lg:hidden">118            <Button variant="outline" size="sm" className="w-full justify-between" onClick={() => setOpen((v) => !v)} aria-expanded={open} aria-controls="admin-mobile-nav">119              <span className="flex items-center gap-2">120                {open ? <X className="size-4" /> : <Menu className="size-4" />}121                {current?.label ?? "Admin"}122              </span>123              <ChevronDown className={cn("size-4 text-fg-subtle transition-transform", open && "rotate-180")} />124            </Button>125            {open ? (126              <div id="admin-mobile-nav" className="mt-2 rounded-md border border-border bg-bg p-2 shadow-sm">127                <AdminNav onNavigate={() => setOpen(false)} />128              </div>129            ) : null}130          </div>131          <main className="flex-1 px-4 py-6 sm:px-6 lg:px-8">132            <div className="mx-auto w-full max-w-[1320px] animate-fade-in">{children}</div>133          </main>134        </div>135      </div>136    </div>137  );138}139