← back to Freddy
components/Sidebar.tsx
304 lines
'use client';
import { useState } from 'react';
import {
BarChart3,
Flame,
Building2,
Landmark,
Sparkles,
Send,
DollarSign,
TrendingUp,
Settings,
ChevronLeft,
ChevronRight,
} from 'lucide-react';
export type TabId =
| 'pulse'
| 'causes'
| 'organizations'
| 'granters'
| 'matches'
| 'solicitor'
| 'revenue'
| 'impact'
| 'settings';
interface NavItem {
id: TabId;
label: string;
Icon: React.ElementType;
section: 'main' | 'tools';
}
interface SidebarProps {
activeTab: TabId;
onTabChange: (tab: TabId) => void;
isOpen?: boolean;
onToggle?: () => void;
}
const NAV_ITEMS: NavItem[] = [
{ id: 'pulse', label: 'Pulse Board', Icon: BarChart3, section: 'main' },
{ id: 'causes', label: 'Causes', Icon: Flame, section: 'main' },
{ id: 'organizations', label: 'Organizations', Icon: Building2, section: 'main' },
{ id: 'granters', label: 'Granters', Icon: Landmark, section: 'main' },
{ id: 'matches', label: 'Matches', Icon: Sparkles, section: 'main' },
{ id: 'solicitor', label: 'Solicitor', Icon: Send, section: 'main' },
{ id: 'revenue', label: 'Revenue', Icon: DollarSign, section: 'main' },
{ id: 'impact', label: 'Impact', Icon: TrendingUp, section: 'main' },
{ id: 'settings', label: 'Settings', Icon: Settings, section: 'tools' },
];
export default function Sidebar({ activeTab, onTabChange, isOpen, onToggle }: SidebarProps) {
const [collapsed, setCollapsed] = useState(false);
const mainItems = NAV_ITEMS.filter((i) => i.section === 'main');
const toolItems = NAV_ITEMS.filter((i) => i.section === 'tools');
return (
<>
{isOpen && onToggle && (
<div
onClick={onToggle}
className="sidebar-backdrop"
style={{
position: 'fixed',
inset: 0,
backgroundColor: 'rgba(0,0,0,0.5)',
zIndex: 39,
}}
/>
)}
<aside
className={`sidebar-root${isOpen ? ' sidebar-open' : ''}`}
style={{
width: collapsed ? 64 : 220,
minWidth: collapsed ? 64 : 220,
transition: 'width 0.2s cubic-bezier(0.4, 0, 0.2, 1), min-width 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
height: '100vh',
display: 'flex',
flexDirection: 'column',
backgroundColor: 'var(--color-surface)',
borderRight: '1px solid var(--color-border)',
position: 'relative',
overflow: 'hidden',
flexShrink: 0,
}}
aria-label="Main navigation"
>
{/* Brand */}
<div
style={{
padding: collapsed ? '16px 0' : '16px',
display: 'flex',
alignItems: 'center',
gap: 10,
justifyContent: collapsed ? 'center' : 'flex-start',
borderBottom: '1px solid var(--color-border)',
height: 56,
flexShrink: 0,
}}
>
<div
style={{
width: 32,
height: 32,
borderRadius: 10,
background: 'linear-gradient(135deg, #d97706, #b45309)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
boxShadow: '0 2px 8px rgba(217, 119, 6, 0.3)',
fontWeight: 800,
fontSize: 16,
color: '#fff',
fontFamily: 'Georgia, serif',
}}
>
F
</div>
{!collapsed && (
<div style={{ overflow: 'hidden', whiteSpace: 'nowrap' }}>
<div style={{ fontSize: 14, fontWeight: 700, color: 'var(--color-text)', lineHeight: 1.2 }}>
Freddy
</div>
<div style={{ fontSize: 10, color: 'var(--color-text-muted)', lineHeight: 1.2, letterSpacing: '0.02em' }}>
Fundraising Coordinator
</div>
</div>
)}
</div>
{/* Main Nav */}
<nav style={{ flex: 1, overflowY: 'auto', overflowX: 'hidden', padding: '8px 8px 0' }}>
{!collapsed && (
<div style={{ padding: '4px 8px 6px', fontSize: 10, fontWeight: 600, color: 'var(--color-text-muted)', letterSpacing: '0.06em', textTransform: 'uppercase' }}>
Platform
</div>
)}
<div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{mainItems.map(({ id, label, Icon }) => (
<SidebarButton
key={id}
id={id}
label={label}
Icon={Icon}
isActive={activeTab === id}
collapsed={collapsed}
onClick={() => onTabChange(id)}
/>
))}
</div>
<div
style={{
height: 1,
backgroundColor: 'var(--color-border)',
margin: collapsed ? '12px 8px' : '12px 4px',
opacity: 0.6,
}}
/>
{!collapsed && (
<div style={{ padding: '4px 8px 6px', fontSize: 10, fontWeight: 600, color: 'var(--color-text-muted)', letterSpacing: '0.06em', textTransform: 'uppercase' }}>
System
</div>
)}
<div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{toolItems.map(({ id, label, Icon }) => (
<SidebarButton
key={id}
id={id}
label={label}
Icon={Icon}
isActive={activeTab === id}
collapsed={collapsed}
onClick={() => onTabChange(id)}
/>
))}
</div>
</nav>
{/* Collapse toggle */}
<div style={{ padding: '8px', borderTop: '1px solid var(--color-border)', flexShrink: 0 }}>
<button
onClick={() => setCollapsed((c) => !c)}
title={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
style={{
width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: collapsed ? 'center' : 'flex-start',
gap: 10,
padding: collapsed ? '8px 0' : '8px 12px',
borderRadius: 8,
border: 'none',
backgroundColor: 'transparent',
color: 'var(--color-text-muted)',
cursor: 'pointer',
fontSize: 12,
transition: 'background-color 0.15s, color 0.15s',
}}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = 'var(--color-surface-el)';
e.currentTarget.style.color = 'var(--color-text-secondary)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = 'transparent';
e.currentTarget.style.color = 'var(--color-text-muted)';
}}
>
{collapsed ? <ChevronRight size={16} /> : <ChevronLeft size={16} />}
{!collapsed && <span>Collapse</span>}
</button>
</div>
</aside>
</>
);
}
function SidebarButton({
label,
Icon,
isActive,
collapsed,
onClick,
}: {
id: string;
label: string;
Icon: React.ElementType;
isActive: boolean;
collapsed: boolean;
onClick: () => void;
}) {
return (
<button
onClick={onClick}
aria-current={isActive ? 'page' : undefined}
title={collapsed ? label : undefined}
style={{
display: 'flex',
alignItems: 'center',
gap: 10,
padding: collapsed ? '10px 0' : '8px 12px',
justifyContent: collapsed ? 'center' : 'flex-start',
borderRadius: 8,
border: 'none',
cursor: 'pointer',
position: 'relative',
backgroundColor: isActive ? 'rgba(217, 119, 6, 0.12)' : 'transparent',
color: isActive ? 'var(--color-text)' : 'var(--color-text-secondary)',
fontSize: 13,
fontWeight: isActive ? 600 : 500,
transition: 'background-color 0.15s, color 0.15s',
width: '100%',
overflow: 'hidden',
whiteSpace: 'nowrap',
}}
onMouseEnter={(e) => {
if (!isActive) {
e.currentTarget.style.backgroundColor = 'var(--color-surface-el)';
e.currentTarget.style.color = 'var(--color-text)';
}
}}
onMouseLeave={(e) => {
if (!isActive) {
e.currentTarget.style.backgroundColor = 'transparent';
e.currentTarget.style.color = 'var(--color-text-secondary)';
}
}}
>
{/* Active indicator bar -- amber */}
{isActive && (
<div
style={{
position: 'absolute',
left: 0,
top: '20%',
bottom: '20%',
width: 3,
borderRadius: '0 3px 3px 0',
background: 'linear-gradient(180deg, #fbbf24, #d97706)',
}}
/>
)}
<Icon
size={18}
aria-hidden="true"
style={{
color: isActive ? '#fbbf24' : 'currentColor',
flexShrink: 0,
transition: 'color 0.15s',
}}
/>
{!collapsed && <span>{label}</span>}
{collapsed && <span className="sr-only">{label}</span>}
</button>
);
}