TypeScript 97.5%
SQL 1.4%
Python 0.8%
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