← back to Norma

components/NikkiShell.tsx

392 lines

'use client';

import { useState, useEffect } from 'react';
import { LogOut, Menu, LayoutDashboard, Newspaper, Megaphone, Award, MoreHorizontal } from 'lucide-react';
import GlobalSearch from './GlobalSearch';
import { useAuth } from './AuthProvider';
import { useOrg } from './OrgProvider';
import Sidebar, { type TabId } from './Sidebar';
import NikkiDashboard from './nikki/NikkiDashboard';
import DraftsTab from './drafts/DraftsTab';
import PetitionsTab from './petitions/PetitionsTab';
import GrantsTab from './grants/GrantsTab';
import PipelineTab from './pipeline/PipelineTab';
import StatementsTab from './statements/StatementsTab';
import DonationsTab from './donations/DonationsTab';
import NewsTab from './NewsTab';
import CommunityTab from './community/CommunityTab';
import TopicRadar from './radar/TopicRadar';
import PetitionCreator from './petitions/PetitionCreator';
import TrendingPetitions from './petitions/TrendingPetitions';
import LiveNews from './news/LiveNews';
import EmailSendsTab from './email-sends/EmailSendsTab';
import EmailAnalyzer from './email-analyzer/EmailAnalyzer';
import SavedAnalyses from './email-analyzer/SavedAnalyses';
import GmailCRMTab from './gmail/GmailCRMTab';
import ContactsTab from './contacts/ContactsTab';
import JournalistHub from './journalists/JournalistHub';
import CongressTab from './congress/CongressTab';
import PowerIntelligenceTab from './intelligence/PowerIntelligenceTab';
import InTheNewsTab from './intelligence/InTheNewsTab';
import ChatPanel from './ChatPanel';
import SettingsTab from './settings/SettingsTab';

/* ─── NikkiShell — org-locked staff shell ────────────────────────────────── */
export default function NikkiShell() {
  const { user, logout } = useAuth();
  const { org } = useOrg();
  const [activeTab, setActiveTab] = useState<TabId>('email-sends');
  const [sidebarOpen, setSidebarOpen] = useState(false);
  const [journalistNavId, setJournalistNavId] = useState<string | null>(null);

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

  /* ── Keyboard shortcuts ──────────────────────────────────────────────── */
  useEffect(() => {
    const KBD_NAV: Record<string, TabId> = {
      h: 'dashboard',
      d: 'drafts',
      p: 'petitions',
      r: 'topic-radar',
      l: 'pipeline',
      n: 'contacts',
    };

    function handleKeyDown(e: KeyboardEvent) {
      if (e.metaKey || e.ctrlKey || e.altKey) return;
      const tag = (e.target as HTMLElement)?.tagName;
      if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return;
      if ((e.target as HTMLElement)?.isContentEditable) return;

      const tab = KBD_NAV[e.key.toLowerCase()];
      if (tab) {
        e.preventDefault();
        handleTabChange(tab);
      }
    }

    window.addEventListener('keydown', handleKeyDown);
    return () => window.removeEventListener('keydown', handleKeyDown);
  }, []);

  function renderPanel() {
    if (!org) {
      return (
        <div className="flex flex-col items-center justify-center py-20 gap-6 max-w-lg mx-auto">
          <div className="spinner" />
          <p className="text-sm" style={{ color: 'var(--color-text-muted)' }}>
            Loading organization...
          </p>
        </div>
      );
    }

    switch (activeTab) {
      case 'dashboard':           return <NikkiDashboard onNavigate={(tab) => handleTabChange(tab as TabId)} />;
      case 'drafts':              return <DraftsTab onNavigateToSettings={() => {}} />;
      case 'news':                return <NewsTab onNavigate={(tab) => handleTabChange(tab as TabId)} onNavigateToJournalist={(name) => { setJournalistNavId(`name:${name}`); setActiveTab('journalists'); }} />;
      case 'petitions':           return <PetitionsTab onNavigate={(tab) => handleTabChange(tab as TabId)} />;
      case 'grants':              return <GrantsTab onNavigate={(tab) => handleTabChange(tab as TabId)} />;
      case 'pipeline':            return <PipelineTab />;
      case 'statements':          return <StatementsTab />;
      case 'donations':           return <DonationsTab />;
      case 'email-sends':         return <EmailSendsTab />;
      case 'email-analyzer':      return <EmailAnalyzer />;
      case 'email-analyzer-saved':
        return (
          <SavedAnalyses
            onOpen={(sid) => {
              try { sessionStorage.setItem('norma-analyzer-load-session', sid); } catch { /* noop */ }
              handleTabChange('email-analyzer');
            }}
          />
        );
      case 'gmail-crm':           return <GmailCRMTab />;
      case 'contacts':            return <ContactsTab />;
      case 'journalists':         return <JournalistHub initialJournalistId={journalistNavId} />;
      case 'community':           return <CommunityTab />;
      case 'topic-radar':         return <TopicRadar onNavigate={(tab) => handleTabChange(tab as TabId)} />;
      case 'petition-creator':    return <PetitionCreator />;
      case 'trending-petitions':  return <TrendingPetitions />;
      case 'live-news':           return <LiveNews />;
      case 'congress':            return <CongressTab onNavigate={(tab) => handleTabChange(tab as TabId)} />;
      case 'power-intelligence':  return <PowerIntelligenceTab />;
      case 'in-the-news':         return <InTheNewsTab />;
      case 'settings':            return <SettingsTab />;
      default:
        return (
          <div className="flex flex-col items-center justify-center py-20 gap-3" style={{ color: 'var(--color-text-muted)' }}>
            <p className="text-lg font-semibold">Tab not found</p>
            <p className="text-sm">The selected view is not available for staff accounts.</p>
          </div>
        );
    }
  }

  const TAB_LABELS: Partial<Record<TabId, string>> = {
    dashboard: 'Dashboard',
    drafts: 'Drafts',
    news: 'News Feed',
    petitions: 'Petitions',
    grants: 'Grants',
    pipeline: 'Pipeline',
    statements: 'Statements',
    donations: 'Donations',
    'email-sends': 'Email Center',
    'email-analyzer': 'Email Analyzer',
    'email-analyzer-saved': 'Saved Analyses',
    'gmail-crm': 'Gmail CRM',
    'power-intelligence': 'Power Intel',
    'in-the-news': 'In the News',
    contacts: 'Network',
    journalists: 'Journalists',
    community: 'Community',
    'topic-radar': 'Topic Radar',
    'petition-creator': 'Create Petition',
    'trending-petitions': 'Trending Petitions',
    'live-news': 'Live News',
    congress: 'Congress',
  };

  const brandColor = org?.brand_color || '#10b981';

  return (
    <div
      className="flex"
      style={{ backgroundColor: 'var(--color-bg)', minHeight: '100vh' }}
    >
      {/* ── Skip Navigation ───────────────────────────────────────────────── */}
      <a
        href="#main-content"
        className="sr-only focus:not-sr-only"
        style={{
          position: 'absolute',
          top: 8,
          left: 8,
          zIndex: 9999,
          padding: '8px 16px',
          backgroundColor: 'var(--color-primary)',
          color: '#fff',
          borderRadius: 'var(--radius-md)',
          fontSize: '0.875rem',
          fontWeight: 600,
          textDecoration: 'none',
        }}
      >
        Skip to main content
      </a>

      {/* ── Left Sidebar ─────────────────────────────────────────────────── */}
      <Sidebar
        activeTab={activeTab}
        onTabChange={handleTabChange}
        isOpen={sidebarOpen}
        onToggle={() => setSidebarOpen(p => !p)}
        role="staff"
      />

      {/* ── Right: Header + Content ──────────────────────────────────────── */}
      <div className="flex flex-col flex-1 min-w-0">
        {/* ── Top Header Bar ─────────────────────────────────────────────── */}
        <header
          className="flex items-center justify-between px-5 h-14 shrink-0"
          style={{
            backgroundColor: 'var(--color-surface)',
            borderBottom: '1px solid var(--color-border)',
          }}
        >
          {/* Left: hamburger + section name */}
          <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] || activeTab}
            </h1>
          </div>

          {/* Center: global search */}
          <GlobalSearch onNavigate={(tab, id) => {
            if (tab === 'journalists' && id) {
              setJournalistNavId(id);
            }
            setActiveTab(tab as TabId);
            setSidebarOpen(false);
          }} />

          {/* Right: org name + user + logout */}
          <div className="flex items-center gap-3">
            {/* Org badge — fixed, not switchable */}
            {org && (
              <div className="flex items-center gap-2">
                <div
                  style={{
                    width: 24,
                    height: 24,
                    borderRadius: 6,
                    background: `linear-gradient(135deg, ${brandColor}, ${brandColor}cc)`,
                    display: 'flex',
                    alignItems: 'center',
                    justifyContent: 'center',
                    flexShrink: 0,
                  }}
                >
                  <span style={{ color: '#fff', fontSize: 10, fontWeight: 800 }}>
                    {(org.short_name || org.org_name.slice(0, 2)).slice(0, 3)}
                  </span>
                </div>
                <span
                  className="text-xs hidden sm:inline font-medium"
                  style={{ color: 'var(--color-text-secondary)' }}
                >
                  {org.org_name}
                </span>
              </div>
            )}
            {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 Content ───────────────────────────────────────────────── */}
        <main id="main-content" className="flex-1 overflow-auto mobile-main-content">
          {renderPanel()}
        </main>
      </div>
      <ChatPanel />

      {/* ── Mobile Bottom Navigation ─────────────────────────────────────── */}
      <nav
        className="mobile-bottom-nav"
        aria-label="Mobile bottom navigation"
        style={{
          display: 'none',
          position: 'fixed',
          bottom: 0,
          left: 0,
          right: 0,
          height: 56,
          zIndex: 50,
          backgroundColor: 'var(--color-surface)',
          borderTop: '1px solid var(--color-border)',
          boxShadow: '0 -4px 16px rgba(0,0,0,0.08)',
        }}
      >
        <div
          style={{
            display: 'flex',
            alignItems: 'center',
            height: '100%',
            paddingBottom: 'env(safe-area-inset-bottom, 0px)',
          }}
        >
          {([
            { id: 'dashboard' as TabId,  label: 'Dashboard', Icon: LayoutDashboard },
            { id: 'news'      as TabId,  label: 'News',      Icon: Newspaper       },
            { id: 'petitions' as TabId,  label: 'Petitions', Icon: Megaphone       },
            { id: 'grants'    as TabId,  label: 'Grants',    Icon: Award           },
          ] as const).map(({ id, label, Icon }) => {
            const isActive = activeTab === id;
            return (
              <button
                key={id}
                type="button"
                onClick={() => handleTabChange(id)}
                aria-current={isActive ? 'page' : undefined}
                aria-label={label}
                style={{
                  flex: 1,
                  display: 'flex',
                  flexDirection: 'column',
                  alignItems: 'center',
                  justifyContent: 'center',
                  gap: 3,
                  minHeight: 44,
                  height: '100%',
                  border: 'none',
                  background: 'none',
                  cursor: 'pointer',
                  color: isActive ? 'var(--color-primary)' : 'var(--color-text-muted)',
                  transition: 'color 0.15s',
                  position: 'relative',
                }}
              >
                {isActive && (
                  <div
                    aria-hidden="true"
                    style={{
                      position: 'absolute',
                      top: 0,
                      left: '20%',
                      right: '20%',
                      height: 2,
                      borderRadius: '0 0 2px 2px',
                      background: 'linear-gradient(90deg, #34d399, #10b981)',
                      boxShadow: '0 0 6px rgba(52,211,153,0.6)',
                    }}
                  />
                )}
                <Icon size={20} aria-hidden="true" />
                <span style={{ fontSize: 10, fontWeight: isActive ? 700 : 500, lineHeight: 1 }}>
                  {label}
                </span>
              </button>
            );
          })}
          <button
            type="button"
            onClick={() => setSidebarOpen(true)}
            aria-label="Open navigation menu"
            style={{
              flex: 1,
              display: 'flex',
              flexDirection: 'column',
              alignItems: 'center',
              justifyContent: 'center',
              gap: 3,
              minHeight: 44,
              height: '100%',
              border: 'none',
              background: 'none',
              cursor: 'pointer',
              color: 'var(--color-text-muted)',
              transition: 'color 0.15s',
            }}
          >
            <MoreHorizontal size={20} aria-hidden="true" />
            <span style={{ fontSize: 10, fontWeight: 500, lineHeight: 1 }}>More</span>
          </button>
        </div>
      </nav>
    </div>
  );
}