← back to Grant

components/Sidebar.tsx

323 lines

'use client';

import { useState } from 'react';
import {
  LayoutDashboard,
  Award,
  Newspaper,
  Users,
  Megaphone,
  Settings,
  ChevronLeft,
  ChevronRight,
} from 'lucide-react';

/* ─── Types ──────────────────────────────────────────────────────────────── */
export type TabId =
  | 'dashboard'
  | 'grants'
  | 'news'
  | 'collaborations'
  | 'outreach'
  | '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;
}

/* ─── Navigation config ──────────────────────────────────────────────────── */
const NAV_ITEMS: NavItem[] = [
  { id: 'dashboard',      label: 'Dashboard',      Icon: LayoutDashboard, section: 'main'  },
  { id: 'grants',         label: 'Grants',         Icon: Award,           section: 'main'  },
  { id: 'news',           label: 'News',           Icon: Newspaper,       section: 'main'  },
  { id: 'collaborations', label: 'Collaborations', Icon: Users,           section: 'main'  },
  { id: 'outreach',       label: 'Outreach',       Icon: Megaphone,       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, #059669, #047857)',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            flexShrink: 0,
            boxShadow: '0 2px 8px rgba(5, 150, 105, 0.3)',
          }}
        >
          <span style={{ color: '#fff', fontSize: 14, fontWeight: 900, lineHeight: 1 }}>G</span>
        </div>
        {!collapsed && (
          <div style={{ overflow: 'hidden', whiteSpace: 'nowrap' }}>
            <div style={{ fontSize: 14, fontWeight: 700, color: 'var(--color-text)', lineHeight: 1.2 }}>
              Grant
            </div>
            <div style={{ fontSize: 10, color: 'var(--color-text-muted)', lineHeight: 1.2, letterSpacing: '0.02em' }}>
              Non-Profit Fundraiser
            </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' }}>
            Workspace
          </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>

        {/* 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 }) => (
            <SidebarButton
              key={id}
              id={id}
              label={label}
              Icon={Icon}
              isActive={activeTab === id}
              collapsed={collapsed}
              onClick={() => onTabChange(id)}
            />
          ))}
        </div>

        {/* 2026-05-05 (tick 38): /river demo link — public visualization of */}
        {/* federal grant data as fish swimming toward close-date. */}
        {!collapsed && (
          <a
            href="/river?q=education"
            target="_blank"
            rel="noopener"
            style={{
              display: 'flex', alignItems: 'center', gap: 10,
              padding: '8px 12px', marginTop: 8,
              border: '1px dashed var(--color-border)', borderRadius: 6,
              color: 'var(--color-accent, #10b981)', textDecoration: 'none',
              fontSize: 12, fontWeight: 500,
            }}
            title="Deadline River — federal grants visualized"
          >
            <span style={{ fontSize: 14 }}>~~</span> Deadline River <span style={{ marginLeft: 'auto', fontSize: 9, opacity: 0.6 }}>↗</span>
          </a>
        )}
      </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,
}: {
  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(5, 150, 105, 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 */}
      {isActive && (
        <div
          style={{
            position: 'absolute',
            left: 0,
            top: '20%',
            bottom: '20%',
            width: 3,
            borderRadius: '0 3px 3px 0',
            background: 'linear-gradient(180deg, #34d399, #059669)',
          }}
        />
      )}
      <Icon
        size={18}
        aria-hidden="true"
        style={{
          color: isActive ? '#34d399' : 'currentColor',
          flexShrink: 0,
          transition: 'color 0.15s',
        }}
      />
      {!collapsed && <span>{label}</span>}
      {collapsed && <span className="sr-only">{label}</span>}
    </button>
  );
}