← back to Hub

components/AppShell.tsx

241 lines

'use client';

import { useState } 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 CommandCenterTab from './CommandCenterTab';
import SDCCTab from './SDCCTab';
import GrantTab from './GrantTab';
import PattyTab from './PattyTab';
import FreddyTab from './FreddyTab';
import ActivityTab from './ActivityTab';
import AnalyticsTab from './AnalyticsTab';
import SettingsTab from './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 = 7480;

/* ---- 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>('command');
  const [sidebarOpen, setSidebarOpen] = useState(false);
  const [showAppSwitcher, setShowAppSwitcher] = useState(false);

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

  function renderPanel() {
    switch (activeTab) {
      case 'command':   return <CommandCenterTab />;
      case 'sdcc':      return <SDCCTab />;
      case 'grant':     return <GrantTab />;
      case 'patty':     return <PattyTab />;
      case 'freddy':    return <FreddyTab />;
      case 'activity':  return <ActivityTab />;
      case 'analytics': return <AnalyticsTab />;
      case 'settings':  return <SettingsTab />;
    }
  }

  const TAB_LABELS: Record<TabId, string> = {
    command: 'Command Center',
    sdcc: 'SDCC',
    grant: 'Grant',
    patty: 'Patty',
    freddy: 'Freddy',
    activity: 'Activity Feed',
    analytics: 'Analytics',
    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>
  );
}

/* ---- Public export -- wraps Shell in AuthProvider ------------------------- */
export default function AppShell() {
  return (
    <AuthProvider>
      <ToastProvider>
        <Shell />
      </ToastProvider>
    </AuthProvider>
  );
}