From 6de8a475d1bf75f3b41e93e59d29ec66a18fd529 Mon Sep 17 00:00:00 2001 From: Patrick Britton Date: Sun, 8 Feb 2026 18:19:56 -0600 Subject: [PATCH] feat: reorganize admin sidebar into collapsible grouped hierarchy (Users & Content, Moderation & Safety, Platform) --- admin/src/components/Sidebar.tsx | 165 +++++++++++++++++++++++++++---- 1 file changed, 145 insertions(+), 20 deletions(-) diff --git a/admin/src/components/Sidebar.tsx b/admin/src/components/Sidebar.tsx index c766b27..3df8b58 100644 --- a/admin/src/components/Sidebar.tsx +++ b/admin/src/components/Sidebar.tsx @@ -6,34 +6,146 @@ import { useAuth } from '@/lib/auth'; import { cn } from '@/lib/utils'; import { LayoutDashboard, Users, FileText, Shield, ShieldCheck, Scale, Flag, - Settings, Activity, LogOut, ChevronLeft, ChevronRight, Sliders, FolderTree, HardDrive, AtSign, Brain, ScrollText, Wrench, Bot, + Settings, Activity, LogOut, ChevronLeft, ChevronRight, ChevronDown, + Sliders, FolderTree, HardDrive, AtSign, Brain, ScrollText, Wrench, Bot, + UserCog, ShieldAlert, Cog, } from 'lucide-react'; import { useState } from 'react'; -const navItems = [ +type NavItem = { href: string; label: string; icon: any }; +type NavGroup = { label: string; icon: any; items: NavItem[] }; +type NavEntry = NavItem | NavGroup; + +function isGroup(entry: NavEntry): entry is NavGroup { + return 'items' in entry; +} + +const navigation: NavEntry[] = [ { href: '/', label: 'Dashboard', icon: LayoutDashboard }, - { href: '/users', label: 'Users', icon: Users }, - { href: '/posts', label: 'Posts', icon: FileText }, - { href: '/moderation', label: 'Moderation', icon: Shield }, - { href: '/appeals', label: 'Appeals', icon: Scale }, - { href: '/reports', label: 'Reports', icon: Flag }, - { href: '/algorithm', label: 'Algorithm', icon: Sliders }, - { href: '/categories', label: 'Categories', icon: FolderTree }, - { href: '/usernames', label: 'Usernames', icon: AtSign }, - { href: '/ai-moderation', label: 'AI Moderation', icon: Brain }, - { href: '/ai-audit-log', label: 'AI Audit Log', icon: ScrollText }, - { href: '/content-tools', label: 'Content Tools', icon: Wrench }, - { href: '/official-accounts', label: 'Official Accounts', icon: Bot }, - { href: '/safe-links', label: 'Safe Links', icon: ShieldCheck }, - { href: '/storage', label: 'Storage', icon: HardDrive }, - { href: '/system', label: 'System Health', icon: Activity }, - { href: '/settings', label: 'Settings', icon: Settings }, + { + label: 'Users & Content', + icon: UserCog, + items: [ + { href: '/users', label: 'Users', icon: Users }, + { href: '/posts', label: 'Posts', icon: FileText }, + { href: '/categories', label: 'Categories', icon: FolderTree }, + { href: '/official-accounts', label: 'Official Accounts', icon: Bot }, + ], + }, + { + label: 'Moderation & Safety', + icon: ShieldAlert, + items: [ + { href: '/moderation', label: 'Moderation Queue', icon: Shield }, + { href: '/ai-moderation', label: 'AI Moderation', icon: Brain }, + { href: '/ai-audit-log', label: 'AI Audit Log', icon: ScrollText }, + { href: '/appeals', label: 'Appeals', icon: Scale }, + { href: '/reports', label: 'Reports', icon: Flag }, + { href: '/safe-links', label: 'Safe Links', icon: ShieldCheck }, + { href: '/content-tools', label: 'Content Tools', icon: Wrench }, + ], + }, + { + label: 'Platform', + icon: Cog, + items: [ + { href: '/algorithm', label: 'Algorithm', icon: Sliders }, + { href: '/usernames', label: 'Usernames', icon: AtSign }, + { href: '/storage', label: 'Storage', icon: HardDrive }, + { href: '/system', label: 'System Health', icon: Activity }, + { href: '/settings', label: 'Settings', icon: Settings }, + ], + }, ]; +function NavGroupSection({ + group, + pathname, + collapsed, + open, + onToggle, +}: { + group: NavGroup; + pathname: string; + collapsed: boolean; + open: boolean; + onToggle: () => void; +}) { + const Icon = group.icon; + const hasActive = group.items.some( + (item) => pathname === item.href || pathname.startsWith(item.href) + ); + + return ( +
+ + {(open || collapsed) && ( +
+ {group.items.map((item) => { + const isActive = + pathname === item.href || pathname.startsWith(item.href); + const ItemIcon = item.icon; + return ( + + + {!collapsed && {item.label}} + + ); + })} +
+ )} +
+ ); +} + export default function Sidebar() { const pathname = usePathname(); const { logout } = useAuth(); const [collapsed, setCollapsed] = useState(false); + const [openGroups, setOpenGroups] = useState>(() => { + // All groups open by default + const defaults: Record = {}; + navigation.forEach((entry) => { + if (isGroup(entry)) defaults[entry.label] = true; + }); + return defaults; + }); + + const toggleGroup = (label: string) => { + setOpenGroups((prev) => ({ ...prev, [label]: !prev[label] })); + }; return (