Barazo default frontend
barazo.forum
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}