'use client' import Link from 'next/link' import { usePathname } from 'next/navigation' import { useEffect, useRef, useState } from 'react' import { cn } from '@/lib/utils' import { signOut } from '@/lib/auth-actions' import { BrandMark } from '@/components/brand-mark' import type { Tables } from '@/types/database' type SpaceMember = Tables<'space_members'> type Space = Tables<'spaces'> import { Menu, X, LayoutDashboard, ListChecks, FolderKanban, Settings2, Wrench, MessageSquare, Users, CreditCard, BookUser, Download, LogOut, Vote, ShieldAlert, ScrollText, LineChart, UserSearch, MessagesSquare, SlidersHorizontal, ClipboardList, FileText, Award, BadgeCheck, GraduationCap, Hammer, ChevronDown, DoorClosed, CalendarCheck, Zap, Home, Compass, } from 'lucide-react' interface NavLinkProps { href: string label: string icon: React.ElementType badge?: number active: boolean external?: boolean onClick?: () => void } function NavLink({ href, label, icon: Icon, badge, active, external, onClick }: NavLinkProps) { const className = cn( 'flex items-center justify-between mx-2 px-3 py-2 rounded-md text-sm transition-colors', active ? 'bg-[var(--sidebar-primary)]/15 text-[var(--sidebar-primary)]' : 'text-[var(--sidebar-foreground)]/70 hover:bg-[var(--sidebar-accent)] hover:text-[var(--sidebar-foreground)]', ) const inner = ( <> {label} {badge && badge > 0 ? ( {badge} ) : null} ) if (external) { return ( {inner} ) } return ( {inner} ) } // Collapsible category. Open/closed is remembered per category in // localStorage so it survives navigation; defaults to open so nothing is // hidden on first visit. function NavSection({ id, title, children, }: { id: string title: string children: React.ReactNode }) { const storageKey = `sidebar:section:${id}` const [open, setOpen] = useState(true) useEffect(() => { try { if (window.localStorage.getItem(storageKey) === 'closed') setOpen(false) } catch { /* localStorage unavailable; keep default */ } }, [storageKey]) const toggle = () => { setOpen(prev => { const next = !prev try { window.localStorage.setItem(storageKey, next ? 'open' : 'closed') } catch { /* ignore */ } return next }) } return (
{open &&
{children}
}
) } interface AppSidebarProps { member: SpaceMember & { spaces: Space | null } roleName?: string taskBadge?: number commsBadge?: number paymentBadge?: number navPerms?: Record } export function AppSidebar({ member, roleName, taskBadge = 0, commsBadge = 0, paymentBadge = 0, navPerms = {} }: AppSidebarProps) { const pathname = usePathname() const space = member.spaces const initials = (member.display_name || 'U').split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2) const isAdmin = member.role === 'admin' || member.role === 'board' // Feature-management links: admin/board see all; a member with a delegated // *.manage permission sees just that one (matches the page guards). const can = (perm: string) => isAdmin || !!navPerms[perm] const canForms = can('forms.manage') const canCerts = can('certifications.manage') const canClasses = can('classes.manage') const canEquip = can('equipment.manage') const canDoor = can('door.manage') const showAdminSection = isAdmin || canForms || canCerts || canClasses || canEquip || canDoor const [drawerOpen, setDrawerOpen] = useState(false) const menuButtonRef = useRef(null) const drawerCloseRef = useRef(null) // The mobile drawer is a modal surface: lock background scroll, close on // Escape, and move focus into and back out of it for keyboard/AT users. useEffect(() => { if (!drawerOpen) return const onKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') setDrawerOpen(false) } document.addEventListener('keydown', onKeyDown) const prevOverflow = document.body.style.overflow document.body.style.overflow = 'hidden' drawerCloseRef.current?.focus() return () => { document.removeEventListener('keydown', onKeyDown) document.body.style.overflow = prevOverflow menuButtonRef.current?.focus() } }, [drawerOpen]) const isActive = (href: string) => pathname === href || pathname.startsWith(href + '/') const handleSignOut = async () => { await signOut() } const closeDrawer = () => setDrawerOpen(false) const renderNav = (onNav?: () => void) => ( <> {/* Logo */}

{space?.name || 'My Space'}

{/* Nav */} {/* User */}
) return ( <> {/* Desktop sidebar */} {/* Mobile: top bar */}
{space?.name}
{/* Mobile: drawer overlay */} {drawerOpen && (
Menu
{renderNav(closeDrawer)}
)} ) }