Barazo default frontend barazo.forum
at main 160 lines 6.0 kB view raw
1/** 2 * Admin layout with sidebar navigation. 3 * Desktop: persistent sidebar. Mobile (<768px): hamburger + slide-in drawer. 4 * Used by all /admin/* pages. 5 * @see specs/prd-web.md Section 4 (AdminLayout) 6 */ 7 8'use client' 9 10import { useState, useCallback } from 'react' 11import Link from 'next/link' 12import { usePathname } from 'next/navigation' 13import * as Dialog from '@radix-ui/react-dialog' 14import { 15 Article, 16 ChartBar, 17 FolderSimple, 18 ShieldCheck, 19 Gear, 20 PaintBrush, 21 Tag, 22 Users, 23 PuzzlePiece, 24 ClipboardText, 25 ArrowLeft, 26 ShieldWarning, 27 SealCheck, 28 List, 29 ListNumbers, 30 X, 31} from '@phosphor-icons/react' 32import { cn } from '@/lib/utils' 33 34interface AdminLayoutProps { 35 children: React.ReactNode 36} 37 38const NAV_ITEMS = [ 39 { href: '/admin', label: 'Dashboard', icon: ChartBar }, 40 { href: '/admin/categories', label: 'Categories', icon: FolderSimple }, 41 { href: '/admin/pages', label: 'Pages', icon: Article }, 42 { href: '/admin/moderation', label: 'Moderation', icon: ShieldCheck }, 43 { href: '/admin/rules', label: 'Rules', icon: ListNumbers }, 44 { href: '/admin/sybil-detection', label: 'Sybil Detection', icon: ShieldWarning }, 45 { href: '/admin/trust-seeds', label: 'Trust Seeds', icon: SealCheck }, 46 { href: '/admin/settings', label: 'Settings', icon: Gear }, 47 { href: '/admin/design', label: 'Design', icon: PaintBrush }, 48 { href: '/admin/content-ratings', label: 'Content Ratings', icon: Tag }, 49 { href: '/admin/onboarding', label: 'Onboarding', icon: ClipboardText }, 50 { href: '/admin/users', label: 'Users', icon: Users }, 51 { href: '/admin/plugins', label: 'Plugins', icon: PuzzlePiece }, 52] 53 54function AdminNav({ pathname, onLinkClick }: { pathname: string; onLinkClick?: () => void }) { 55 return ( 56 <> 57 <div className="flex h-14 items-center border-b border-border px-4"> 58 <Link 59 href="/" 60 className="flex items-center gap-2 text-sm text-muted-foreground transition-colors hover:text-foreground" 61 aria-label="Back to forum" 62 onClick={onLinkClick} 63 > 64 <ArrowLeft size={16} aria-hidden="true" /> 65 Back to forum 66 </Link> 67 </div> 68 69 <nav aria-label="Admin navigation" className="flex-1 px-3 py-4"> 70 <ul className="space-y-1"> 71 {NAV_ITEMS.map((item) => { 72 const isActive = pathname === item.href 73 const Icon = item.icon 74 return ( 75 <li key={item.href}> 76 <Link 77 href={item.href} 78 aria-current={isActive ? 'page' : undefined} 79 onClick={onLinkClick} 80 className={cn( 81 'flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors', 82 isActive 83 ? 'bg-primary/10 font-medium text-primary' 84 : 'text-muted-foreground hover:bg-muted hover:text-foreground' 85 )} 86 > 87 <Icon size={18} aria-hidden="true" /> 88 {item.label} 89 </Link> 90 </li> 91 ) 92 })} 93 </ul> 94 </nav> 95 </> 96 ) 97} 98 99export function AdminLayout({ children }: AdminLayoutProps) { 100 const pathname = usePathname() 101 const [drawerOpen, setDrawerOpen] = useState(false) 102 103 const closeDrawer = useCallback(() => setDrawerOpen(false), []) 104 105 return ( 106 <div className="flex min-h-screen bg-background"> 107 {/* Mobile top bar */} 108 <div className="fixed inset-x-0 top-0 z-30 flex h-14 items-center border-b border-border bg-card px-4 md:hidden"> 109 <Dialog.Root open={drawerOpen} onOpenChange={setDrawerOpen}> 110 <Dialog.Trigger asChild> 111 <button 112 type="button" 113 aria-label="Open admin menu" 114 className="rounded-md p-2 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" 115 > 116 <List size={20} aria-hidden="true" /> 117 </button> 118 </Dialog.Trigger> 119 120 <Dialog.Portal> 121 <Dialog.Overlay className="fixed inset-0 z-40 bg-black/50 data-[state=closed]:animate-fade-out data-[state=open]:animate-fade-in" /> 122 <Dialog.Content 123 aria-label="Admin menu" 124 aria-describedby={undefined} 125 className="fixed inset-y-0 left-0 z-50 flex w-64 flex-col bg-card shadow-lg data-[state=closed]:animate-slide-out-left data-[state=open]:animate-slide-in-left" 126 > 127 <Dialog.Title className="sr-only">Admin menu</Dialog.Title> 128 <div className="flex h-14 items-center justify-between border-b border-border px-4"> 129 <span aria-hidden="true" className="text-sm font-medium text-foreground"> 130 Admin 131 </span> 132 <Dialog.Close asChild> 133 <button 134 type="button" 135 aria-label="Close admin menu" 136 className="rounded-md p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" 137 > 138 <X size={18} aria-hidden="true" /> 139 </button> 140 </Dialog.Close> 141 </div> 142 143 <AdminNav pathname={pathname} onLinkClick={closeDrawer} /> 144 </Dialog.Content> 145 </Dialog.Portal> 146 </Dialog.Root> 147 148 <span className="ml-3 text-sm font-medium text-foreground">Admin</span> 149 </div> 150 151 {/* Desktop sidebar */} 152 <aside className="hidden w-64 shrink-0 flex-col border-r border-border bg-card md:flex"> 153 <AdminNav pathname={pathname} /> 154 </aside> 155 156 {/* Main content - add top padding on mobile for the fixed bar */} 157 <main className="min-w-0 flex-1 p-6 pt-20 md:pt-6">{children}</main> 158 </div> 159 ) 160}