← back to Hub
components/Sidebar.tsx
316 lines
'use client';
import { useState } from 'react';
import {
LayoutDashboard,
Mail,
Award,
Megaphone,
DollarSign,
Activity,
BarChart3,
Settings,
ChevronLeft,
ChevronRight,
} from 'lucide-react';
/* ---- Types --------------------------------------------------------------- */
export type TabId =
| 'command'
| 'sdcc'
| 'grant'
| 'patty'
| 'freddy'
| 'activity'
| 'analytics'
| 'settings';
interface NavItem {
id: TabId;
label: string;
Icon: React.ElementType;
section: 'main' | 'tools';
color?: string;
}
interface SidebarProps {
activeTab: TabId;
onTabChange: (tab: TabId) => void;
isOpen?: boolean;
onToggle?: () => void;
}
/* ---- Navigation config --------------------------------------------------- */
const NAV_ITEMS: NavItem[] = [
{ id: 'command', label: 'Command Center', Icon: LayoutDashboard, section: 'main' },
{ id: 'sdcc', label: 'SDCC', Icon: Mail, section: 'main', color: '#3b82f6' },
{ id: 'grant', label: 'Grant', Icon: Award, section: 'main', color: '#059669' },
{ id: 'patty', label: 'Patty', Icon: Megaphone, section: 'main', color: '#7c3aed' },
{ id: 'freddy', label: 'Freddy', Icon: DollarSign, section: 'main', color: '#d97706' },
{ id: 'activity', label: 'Activity', Icon: Activity, section: 'tools' },
{ id: 'analytics', label: 'Analytics', Icon: BarChart3, section: 'tools' },
{ id: 'settings', label: 'Settings', Icon: Settings, section: 'tools' },
];
/* ---- Sidebar ------------------------------------------------------------- */
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, #0891b2, #0e7490)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
boxShadow: '0 2px 8px rgba(8, 145, 178, 0.3)',
}}
>
<span style={{ color: '#fff', fontSize: 14, fontWeight: 900, lineHeight: 1 }}>H</span>
</div>
{!collapsed && (
<div style={{ overflow: 'hidden', whiteSpace: 'nowrap' }}>
<div style={{ fontSize: 14, fontWeight: 700, color: 'var(--color-text)', lineHeight: 1.2 }}>
Hub
</div>
<div style={{ fontSize: 10, color: 'var(--color-text-muted)', lineHeight: 1.2, letterSpacing: '0.02em' }}>
SDCC Command Center
</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' }}>
Dashboard
</div>
)}
<div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{mainItems.map(({ id, label, Icon, color }) => (
<SidebarButton
key={id}
id={id}
label={label}
Icon={Icon}
isActive={activeTab === id}
collapsed={collapsed}
onClick={() => onTabChange(id)}
accentColor={color}
/>
))}
</div>
{/* Divider */}
<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' }}>
Tools
</div>
)}
<div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{toolItems.map(({ id, label, Icon, color }) => (
<SidebarButton
key={id}
id={id}
label={label}
Icon={Icon}
isActive={activeTab === id}
collapsed={collapsed}
onClick={() => onTabChange(id)}
accentColor={color}
/>
))}
</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>
</>
);
}
/* ---- SidebarButton ------------------------------------------------------- */
function SidebarButton({
id,
label,
Icon,
isActive,
collapsed,
onClick,
accentColor,
}: {
id: string;
label: string;
Icon: React.ElementType;
isActive: boolean;
collapsed: boolean;
onClick: () => void;
accentColor?: string;
}) {
const activeColor = accentColor || '#22d3ee';
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 ? `${activeColor}18` : '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 */}
{isActive && (
<div
style={{
position: 'absolute',
left: 0,
top: '20%',
bottom: '20%',
width: 3,
borderRadius: '0 3px 3px 0',
backgroundColor: activeColor,
}}
/>
)}
<Icon
size={18}
aria-hidden="true"
style={{
color: isActive ? activeColor : 'currentColor',
flexShrink: 0,
transition: 'color 0.15s',
}}
/>
{!collapsed && <span>{label}</span>}
{collapsed && <span className="sr-only">{label}</span>}
</button>
);
}