← back to Patty

components/AppShell.tsx

255 lines

'use client';

import { useState, useEffect } from 'react';
import { LogOut, Menu, LayoutGrid } from 'lucide-react';
import { AuthProvider, useAuth } from './AuthProvider';
import { ToastProvider } from './ToastProvider';
import Sidebar, { type TabId } from './Sidebar';
import DashboardTab from './dashboard/DashboardTab';
import NewsFeedTab from './news/NewsFeedTab';
import TopicsTab from './topics/TopicsTab';
import PetitionsTab from './petitions/PetitionsTab';
import GrantsTab from './grants/GrantsTab';
import HeatMapTab from './heatmap/HeatMapTab';
import CampaignsTab from './campaigns/CampaignsTab';
import SourcesTab from './sources/SourcesTab';
import SettingsTab from './settings/SettingsTab';

const APP_LINKS = [
  { name: 'Hub', port: 7480, color: '#0891b2', icon: 'H' },
  { name: 'SDCC', port: 7400, color: '#3b82f6', icon: 'S' },
  { name: 'Grant', port: 7450, color: '#059669', icon: 'G' },
  { name: 'Patty', port: 7460, color: '#7c3aed', icon: 'P' },
  { name: 'Freddy', port: 7470, color: '#d97706', icon: 'F' },
];
const CURRENT_PORT = 7460;

/* ─── Inner shell (needs auth context) ──────────────────────────────────── */
function Shell() {
  const { user, logout, isAdmin, role, canUseAI } = useAuth();

  // Tier badge for non-admin (guest) accounts so the limited mode is obvious.
  const accountBadge = isAdmin
    ? null
    : role === 'member'
      ? { label: 'Guest', color: '#3b82f6' }
      : canUseAI
        ? { label: 'Demo · read-only', color: '#7c3aed' }
        : { label: 'Read-only', color: '#6b7280' };
  const [activeTab, setActiveTab] = useState<TabId>('dashboard');
  const [sidebarOpen, setSidebarOpen] = useState(false);
  const [showAppSwitcher, setShowAppSwitcher] = useState(false);
  const [selectedPetitionId, setSelectedPetitionId] = useState<number | null>(null);
  const [selectedGrantId, setSelectedGrantId] = useState<string | null>(null);

  function handleTabChange(tab: TabId) {
    setActiveTab(tab);
    setSidebarOpen(false);
  }

  function handleViewPetition(id: number) {
    setSelectedPetitionId(id);
    setActiveTab('petitions');
  }

  function handleViewGrant(id: string) {
    setSelectedGrantId(id);
    setActiveTab('grants');
  }

  function renderPanel() {
    switch (activeTab) {
      case 'dashboard':   return <DashboardTab />;
      case 'newsfeed':    return <NewsFeedTab />;
      case 'topics':      return <TopicsTab onViewPetition={handleViewPetition} onViewGrant={handleViewGrant} />;
      case 'petitions':   return <PetitionsTab selectedId={selectedPetitionId} onSelectedConsumed={() => setSelectedPetitionId(null)} onViewGrant={handleViewGrant} />;
      case 'grants':      return <GrantsTab selectedId={selectedGrantId} onSelectedConsumed={() => setSelectedGrantId(null)} />;
      case 'heatmap':     return <HeatMapTab />;
      case 'campaigns':   return <CampaignsTab />;
      case 'sources':     return <SourcesTab />;
      case 'settings':    return <SettingsTab />;
    }
  }

  const TAB_LABELS: Record<TabId, string> = {
    dashboard:  'Dashboard',
    newsfeed:   'News Feed',
    topics:     'Topics',
    petitions:  'Petitions',
    grants:     'Grants',
    heatmap:    'Heat Map',
    campaigns:  'Campaigns',
    sources:    'Sources',
    settings:   'Settings',
  };

  return (
    <div
      className="flex"
      style={{ backgroundColor: 'var(--color-bg)', minHeight: '100vh' }}
    >
      <Sidebar activeTab={activeTab} onTabChange={handleTabChange} isOpen={sidebarOpen} onToggle={() => setSidebarOpen(p => !p)} />

      <div className="flex flex-col flex-1 min-w-0">
        <header
          className="flex items-center justify-between px-5 h-14 shrink-0"
          style={{
            backgroundColor: 'var(--color-surface)',
            borderBottom: '1px solid var(--color-border)',
          }}
        >
          <div className="flex items-center gap-2">
            <button
              className="btn btn-ghost btn-sm"
              onClick={() => setSidebarOpen(p => !p)}
              style={{ display: 'none' }}
              aria-label="Toggle menu"
              id="mobile-menu-btn"
            >
              <Menu size={18} />
            </button>
            <h1
              className="text-sm font-semibold"
              style={{ color: 'var(--color-text)' }}
            >
              {TAB_LABELS[activeTab]}
            </h1>
          </div>

          <div className="flex items-center gap-3">
            <div style={{ position: 'relative' }}>
              <button
                className="btn btn-ghost btn-sm"
                onClick={() => setShowAppSwitcher(p => !p)}
                title="Switch app"
                aria-label="Switch app"
              >
                <LayoutGrid size={16} />
              </button>
              {showAppSwitcher && (
                <>
                  <div
                    style={{ position: 'fixed', inset: 0, zIndex: 49 }}
                    onClick={() => setShowAppSwitcher(false)}
                  />
                  <div
                    style={{
                      position: 'absolute',
                      top: '100%',
                      right: 0,
                      marginTop: 8,
                      padding: 8,
                      borderRadius: 12,
                      backgroundColor: 'var(--color-surface)',
                      border: '1px solid var(--color-border)',
                      boxShadow: '0 8px 24px rgba(0,0,0,0.4)',
                      zIndex: 50,
                      minWidth: 200,
                    }}
                  >
                    <div style={{ padding: '4px 8px 8px', fontSize: 10, fontWeight: 600, color: 'var(--color-text-muted)', letterSpacing: '0.06em', textTransform: 'uppercase' }}>
                      SDCC Ecosystem
                    </div>
                    {APP_LINKS.map((app) => (
                      <a
                        key={app.name}
                        href={`http://45.61.58.125:${app.port}`}
                        target="_blank"
                        rel="noopener noreferrer"
                        style={{
                          display: 'flex',
                          alignItems: 'center',
                          gap: 10,
                          padding: '8px 10px',
                          borderRadius: 8,
                          textDecoration: 'none',
                          color: 'var(--color-text)',
                          fontSize: '0.8125rem',
                          fontWeight: 500,
                          transition: 'background-color 0.15s',
                          borderLeft: app.port === CURRENT_PORT ? `3px solid ${app.color}` : '3px solid transparent',
                        }}
                        onMouseEnter={(e) => { e.currentTarget.style.backgroundColor = 'var(--color-surface-el)'; }}
                        onMouseLeave={(e) => { e.currentTarget.style.backgroundColor = 'transparent'; }}
                        onClick={() => setShowAppSwitcher(false)}
                      >
                        <div
                          style={{
                            width: 28,
                            height: 28,
                            borderRadius: 8,
                            background: `linear-gradient(135deg, ${app.color}, ${app.color}dd)`,
                            display: 'flex',
                            alignItems: 'center',
                            justifyContent: 'center',
                            color: '#fff',
                            fontSize: 12,
                            fontWeight: 800,
                            flexShrink: 0,
                          }}
                        >
                          {app.icon}
                        </div>
                        <div>
                          <div style={{ lineHeight: 1.2 }}>{app.name}</div>
                          <div style={{ fontSize: 10, color: 'var(--color-text-muted)', lineHeight: 1.2 }}>
                            Port {app.port}
                          </div>
                        </div>
                      </a>
                    ))}
                  </div>
                </>
              )}
            </div>
            {accountBadge && (
              <span
                title={`Signed in as a ${accountBadge.label} account`}
                style={{
                  display: 'inline-flex', alignItems: 'center', padding: '3px 9px',
                  borderRadius: 9999, fontSize: '0.7rem', fontWeight: 700,
                  letterSpacing: '0.02em', textTransform: 'uppercase', color: '#fff',
                  backgroundColor: accountBadge.color,
                }}
              >
                {accountBadge.label}
              </span>
            )}
            {user && (
              <span
                className="text-xs hidden sm:inline"
                style={{ color: 'var(--color-text-muted)' }}
              >
                {user}
              </span>
            )}
            <button
              onClick={logout}
              className="btn btn-ghost btn-sm"
              aria-label="Sign out"
              title="Sign out"
            >
              <LogOut size={15} aria-hidden="true" />
              <span className="hidden sm:inline">Sign out</span>
            </button>
          </div>
        </header>

        <main className="flex-1 overflow-auto">
          {renderPanel()}
        </main>
      </div>
    </div>
  );
}

export default function AppShell() {
  return (
    <AuthProvider>
      <ToastProvider>
        <Shell />
      </ToastProvider>
    </AuthProvider>
  );
}