'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)}
)}
>
)
}