import { useEffect, useState, useMemo } from 'react'; import { NavLink, useLocation, useNavigate } from 'react-router-dom'; import { Activity, BarChart3, Calculator, CalendarDays, ChevronDown, ClipboardCheck, ClipboardList, Database, Info, LayoutGrid, LogOut, Map, Menu, Receipt, Search, Settings, ShieldCheck, Tag, TrendingDown, User, X, Landmark, Repeat, ShoppingCart, type LucideIcon, } from 'lucide-react'; import { api } from '@/api'; import { cn } from '@/lib/utils'; import { BRAND } from '@/lib/brand'; import { useAuth } from '@/hooks/useAuth'; import { useOverdueCount } from '@/hooks/useQueries'; import { ThemeToggle } from '@/components/ui/theme-toggle'; import { Button } from '@/components/ui/button'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { NavPill } from './NavPill'; import { BrandBlock } from './BrandBlock'; interface SidebarNavItem { to: string; icon: LucideIcon; label: string; end?: boolean; simplefinOnly?: boolean; accountToolsOnly?: boolean; } const userNavItems: SidebarNavItem[] = [ { to: '/calendar', icon: CalendarDays, label: 'Calendar' }, { to: '/analytics', icon: BarChart3, label: 'Analytics' }, ]; const adminNavItems: SidebarNavItem[] = [ { to: '/', icon: LayoutGrid, label: 'Tracker', end: true }, { to: '/admin', icon: ShieldCheck, label: 'Admin Panel', end: true }, { to: '/admin/status', icon: Activity, label: 'System Status' }, { to: '/roadmap', icon: Map, label: 'Roadmap' }, ]; const trackerItems: SidebarNavItem[] = [ { to: '/', icon: LayoutGrid, label: 'Overview', end: true }, { to: '/summary', icon: ClipboardList, label: 'Summary' }, { to: '/bills', icon: Receipt, label: 'Bills' }, { to: '/subscriptions', icon: Repeat, label: 'Subscriptions' }, { to: '/categories', icon: Tag, label: 'Categories' }, { to: '/health', icon: ClipboardCheck, label: 'Health' }, { to: '/spending', icon: ShoppingCart, label: 'Spending' }, { to: '/bank-transactions', icon: Landmark, label: 'Banking', simplefinOnly: true }, { to: '/snowball', icon: TrendingDown, label: 'Snowball' }, { to: '/payoff', icon: Calculator, label: 'Payoff' }, { to: '/data', icon: Database, label: 'Data', accountToolsOnly: true }, ]; interface TrackerMenuProps { onNavigate?: () => void; badge?: number; badgeNames?: string[]; items?: SidebarNavItem[]; } function TrackerMenu({ onNavigate, badge = 0, badgeNames = [], items = trackerItems, }: TrackerMenuProps) { const location = useLocation(); const navigate = useNavigate(); const isTrackerActive = useMemo( () => items.some((item) => item.end ? location.pathname === item.to : location.pathname.startsWith(item.to), ), [items, location.pathname], ); return ( Tracker {badge > 0 && ( {badge > 99 ? '99+' : badge} {badge} past due {badgeNames.slice(0, 5).map((name) => ( ยท {name} ))} {badgeNames.length > 5 && ( +{badgeNames.length - 5} more )} )} {items.map((item) => { const Icon = item.icon; return ( { navigate(item.to); onNavigate?.(); }} > {item.label} ); })} ); } function UserMenu({ adminMode = false }: { adminMode?: boolean }) { const { user, logout } = useAuth(); const navigate = useNavigate(); const name = useMemo( () => user?.display_name || user?.displayName || user?.name || user?.username || (adminMode ? 'Admin' : 'Profile'), [user, adminMode], ); const accountToolsAllowed = useMemo(() => !user?.is_default_admin, [user]); const userRole = useMemo(() => user?.role, [user]); const initials = useMemo(() => { const source = name.trim() || (adminMode ? 'Admin' : 'User'); return source .split(/\s+/) .slice(0, 2) .map((part) => part[0]?.toUpperCase()) .join(''); }, [adminMode, name]); const handleLogout = async () => { try { await logout(); } catch { /* ignore */ } navigate('/login', { replace: true }); }; return ( {initials || } {name} {name} {adminMode ? 'Admin workspace' : 'Welcome back'} {userRole === 'admin' && !adminMode && ( <> navigate('/admin')}> Admin Panel navigate('/admin/status')}> System Status > )} {accountToolsAllowed && ( <> navigate('/profile')}> Profile navigate('/settings')}> Settings > )} navigate('/about')}> About {user?.role === 'admin' && ( navigate('/roadmap')}> Roadmap )} Logout ); } export default function Sidebar({ adminMode = false, }: { adminMode?: boolean; mainContentId?: string; }) { const [mobileOpen, setMobileOpen] = useState(false); const { user } = useAuth(); const items = useMemo(() => (adminMode ? adminNavItems : userNavItems), [adminMode]); const [simplefinReady, setSimplefinReady] = useState(false); const { data: overdueData } = useOverdueCount(); const od = overdueData as { count?: number; names?: string[] } | undefined; const overdueCount = !adminMode ? (od?.count ?? 0) : 0; const overdueNames = !adminMode ? (od?.names ?? []) : []; const accountToolsAllowed = !user?.is_default_admin; const trackerMenuItems = useMemo( () => trackerItems.filter( (item) => (!item.simplefinOnly || simplefinReady) && (!item.accountToolsOnly || accountToolsAllowed), ), [simplefinReady, accountToolsAllowed], ); useEffect(() => { if (adminMode) return undefined; let cancelled = false; api .simplefinStatus() .then((status) => { const s = status as { enabled?: boolean; has_connections?: boolean }; if (!cancelled) setSimplefinReady(Boolean(s?.enabled && s?.has_connections)); }) .catch(() => { if (!cancelled) setSimplefinReady(false); }); return () => { cancelled = true; }; }, [adminMode]); return ( {!adminMode && ( )} {items.map((item) => ( ))} {!adminMode && ( window.dispatchEvent(new Event('command-palette:open'))} title="Find a bill" > Search bills Ctrl K )} setMobileOpen((v) => !v)} > {mobileOpen ? : } {mobileOpen && ( {adminMode ? 'Admin workspace' : BRAND.name} {adminMode ? 'System care and access' : 'Choose where you want to go next'} {!adminMode && ( { setMobileOpen(false); window.dispatchEvent(new Event('command-palette:open')); }} > Search )} {!adminMode && trackerMenuItems.map((item) => ( setMobileOpen(false)} badge={item.to === '/' ? overdueCount : undefined} badgeNames={item.to === '/' ? overdueNames : undefined} /> ))} {items.map((item) => ( setMobileOpen(false)} /> ))} )} ); }
{badge} past due
ยท {name}
+{badgeNames.length - 5} more
{adminMode ? 'Admin workspace' : BRAND.name}
{adminMode ? 'System care and access' : 'Choose where you want to go next'}