← back to Bertha

kalshi-dash/src/Layout.jsx

356 lines

import React, { useState, useEffect } from 'react';
import { Outlet, Link, useLocation, useNavigate } from 'react-router-dom';
import { api, checkSession, logout } from './api';
import TickerTape from './components/TickerTape';

const NAV = [
  { to: '/', label: 'Dashboard', d: 'M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6' },
  { to: '/markets', label: 'Markets', d: 'M13 7h8m0 0v8m0-8l-8 8-4-4-6 6' },
  { to: '/signals', label: 'Signals', d: 'M13 10V3L4 14h7v7l9-11h-7z' },
  { to: '/intelligence', label: 'Intel Feed', d: 'M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z' },
  { to: '/trading', label: 'Trading', d: 'M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z' },
  { to: '/positions', label: 'Positions', d: 'M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z' },
  { to: '/orders', label: 'Orders', d: 'M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4' },
  { to: '/heatmap', label: 'Heatmap', d: 'M4 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1V5zm10 0a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1V5zM4 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1v-4zm10 0a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z' },
  { to: '/orbit', label: 'Orbit', d: 'M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9' },
  { to: '/pulse', label: 'Pulse', d: 'M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z' },
  { to: '/weather', label: 'Weather', d: 'M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z' },
  { to: '/risk', label: 'Portfauxlios', d: 'M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4' },
  { to: '/settings', label: 'Settings', d: 'M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z M15 12a3 3 0 11-6 0 3 3 0 016 0z' },
];

function StatusDot({ active, warn }) {
  const color = warn
    ? 'var(--orange)'
    : active
    ? 'var(--green)'
    : 'var(--red)';
  return (
    <span
      style={{
        display: 'inline-block',
        width: 6,
        height: 6,
        borderRadius: '50%',
        background: color,
        flexShrink: 0,
        boxShadow: `0 0 4px ${color}`,
      }}
    />
  );
}

export default function Layout() {
  const loc = useLocation();
  const nav = useNavigate();
  const [h, setH] = useState(null);
  const [col, setCol] = useState(false);
  const [authChecked, setAuthChecked] = useState(false);

  useEffect(() => {
    checkSession().then(ok => {
      if (!ok) { nav('/login'); return; }
      setAuthChecked(true);
      const load = () => api('/health').then(setH).catch(() => {});
      load();
      const i = setInterval(load, 15000);
      return () => clearInterval(i);
    });
  }, []);

  if (!authChecked) {
    return (
      <div className="flex items-center justify-center min-h-screen" style={{ background: 'var(--bg)' }}>
        <div
          className="w-8 h-8 rounded-full border-2 border-t-transparent animate-spin"
          style={{ borderColor: 'var(--cyan)', borderTopColor: 'transparent' }}
        />
      </div>
    );
  }

  const sidebarW = col ? 64 : 220;

  return (
    <div className="flex min-h-screen" style={{ background: 'var(--bg)' }}>
      {/* ── Sidebar ── */}
      {/* ── CRT Scan Line Overlay ── */}
      <div className="scan-overlay" aria-hidden="true" />

      <aside
        className="sidebar-mesh"
        style={{
          width: sidebarW,
          minWidth: sidebarW,
          borderRight: '1px solid var(--border)',
          display: 'flex',
          flexDirection: 'column',
          transition: 'width 200ms ease, min-width 200ms ease',
          overflow: 'hidden',
          position: 'sticky',
          top: 0,
          height: '100vh',
        }}
      >
        {/* Brand */}
        <div
          style={{
            padding: col ? '16px 12px' : '16px 18px',
            borderBottom: '1px solid var(--border)',
            display: 'flex',
            alignItems: 'center',
            gap: 12,
            minHeight: 64,
            flexShrink: 0,
          }}
        >
          {/* Orange gradient K badge */}
          <div
            className="logo-animated"
            style={{
              width: 34,
              height: 34,
              borderRadius: 8,
              background: 'linear-gradient(135deg, var(--orange), #E5690D)',
              display: 'flex',
              alignItems: 'center',
              justifyContent: 'center',
              flexShrink: 0,
              boxShadow: '0 2px 10px rgba(247,147,26,0.3)',
            }}
          >
            <span style={{ color: '#0E0E10', fontWeight: 900, fontSize: 16, fontFamily: 'var(--font-heading)' }}>K</span>
          </div>

          {!col && (
            <span
              className="kalshi-gradient heading"
              style={{ fontSize: 13, fontWeight: 700, letterSpacing: '0.06em', whiteSpace: 'nowrap', filter: 'drop-shadow(0 0 8px rgba(0,240,255,0.3))' }}
            >
              KEN TRADER
            </span>
          )}
        </div>

        {/* Nav items */}
        <nav style={{ flex: 1, padding: '10px 8px', overflowY: 'auto', overflowX: 'hidden' }}>
          {NAV.map((n, idx) => {
            const active = loc.pathname === n.to || (n.to !== '/' && loc.pathname.startsWith(n.to));
            const isSettings = n.to === '/settings';
            return (
              <React.Fragment key={n.to}>
                {isSettings && (
                  <div style={{ margin: '8px 4px', height: 1, background: 'linear-gradient(90deg, transparent, var(--border), transparent)' }} />
                )}
              <Link
                to={n.to}
                title={col ? n.label : undefined}
                style={{
                  display: 'flex',
                  alignItems: 'center',
                  gap: col ? 0 : 10,
                  padding: col ? '9px 0' : '9px 10px',
                  justifyContent: col ? 'center' : 'flex-start',
                  borderRadius: 8,
                  marginBottom: 2,
                  textDecoration: 'none',
                  fontSize: 13,
                  fontWeight: active ? 600 : 400,
                  color: active ? 'var(--cyan)' : 'var(--text-dim)',
                  background: active ? 'rgba(0,240,255,0.08)' : 'transparent',
                  borderLeft: active ? '3px solid var(--cyan)' : '3px solid transparent',
                  boxShadow: active ? 'inset 0 0 16px rgba(0,240,255,0.06), inset 3px 0 8px rgba(0,240,255,0.1)' : undefined,
                  textShadow: active ? '0 0 8px rgba(0,240,255,0.4)' : undefined,
                  transition: 'all 150ms ease',
                  whiteSpace: 'nowrap',
                  overflow: 'hidden',
                }}
                onMouseEnter={e => {
                  if (!active) {
                    e.currentTarget.style.color = 'var(--text)';
                    e.currentTarget.style.background = 'rgba(0,240,255,0.04)';
                    e.currentTarget.style.borderLeftColor = 'rgba(0,240,255,0.3)';
                  }
                }}
                onMouseLeave={e => {
                  if (!active) {
                    e.currentTarget.style.color = 'var(--text-dim)';
                    e.currentTarget.style.background = 'transparent';
                    e.currentTarget.style.borderLeftColor = 'transparent';
                  }
                }}
              >
                <svg
                  style={{ width: 17, height: 17, flexShrink: 0, opacity: active ? 1 : 0.7 }}
                  fill="none"
                  stroke="currentColor"
                  strokeWidth="1.6"
                  viewBox="0 0 24 24"
                >
                  <path strokeLinecap="round" strokeLinejoin="round" d={n.d} />
                </svg>
                {!col && (
                  <span style={{ overflow: 'hidden', textOverflow: 'ellipsis' }}>{n.label}</span>
                )}
              </Link>
              </React.Fragment>
            );
          })}
        </nav>

        {/* Bottom status + controls */}
        <div
          style={{
            borderTop: '1px solid var(--border)',
            padding: col ? '12px 8px' : '12px 14px',
            flexShrink: 0,
          }}
        >
          {/* Status indicators — hidden when collapsed */}
          {!col && h && (
            <div style={{ marginBottom: 10, display: 'flex', flexDirection: 'column', gap: 6 }}>
              {[
                { label: 'Env', value: (h.env || 'demo').toUpperCase(), warn: h.env === 'demo', active: h.env === 'prod' },
                { label: 'API', value: h.kalshi_connected ? 'CONNECTED' : 'OFF', active: h.kalshi_connected },
                { label: 'DB', value: h.db_connected ? 'OK' : 'DOWN', active: h.db_connected },
                { label: 'Safe Mode', value: h.safe_mode ? 'ON' : 'OFF', warn: h.safe_mode, active: !h.safe_mode },
              ].map(s => (
                <div key={s.label} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
                  <span style={{ fontSize: 10, color: 'var(--text-muted)', fontFamily: 'var(--font-body)' }}>{s.label}</span>
                  <div style={{ display: 'flex', alignItems: 'center', gap: 5 }}>
                    <StatusDot active={s.active} warn={s.warn} />
                    <span
                      style={{
                        fontSize: 10,
                        fontFamily: 'var(--font-mono)',
                        fontWeight: 600,
                        color: s.warn ? 'var(--orange)' : s.active ? 'var(--green)' : 'var(--red)',
                      }}
                    >
                      {s.value}
                    </span>
                  </div>
                </div>
              ))}
            </div>
          )}

          {/* Collapse + Logout */}
          <div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
            <button
              onClick={() => setCol(!col)}
              title={col ? 'Expand sidebar' : 'Collapse sidebar'}
              style={{
                flex: 1,
                display: 'flex',
                alignItems: 'center',
                justifyContent: 'center',
                padding: '7px 0',
                background: 'transparent',
                border: '1px solid var(--border)',
                borderRadius: 7,
                cursor: 'pointer',
                color: 'var(--text-muted)',
                transition: 'all 150ms ease',
              }}
              onMouseEnter={e => { e.currentTarget.style.borderColor = 'var(--cyan)'; e.currentTarget.style.color = 'var(--cyan)'; }}
              onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border)'; e.currentTarget.style.color = 'var(--text-muted)'; }}
            >
              <svg
                style={{ width: 14, height: 14, transition: 'transform 200ms ease', transform: col ? 'rotate(180deg)' : 'none' }}
                fill="none"
                stroke="currentColor"
                viewBox="0 0 24 24"
              >
                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M11 19l-7-7 7-7m8 14l-7-7 7-7" />
              </svg>
            </button>

            {!col && (
              <button
                onClick={logout}
                style={{
                  padding: '7px 10px',
                  background: 'transparent',
                  border: '1px solid var(--border)',
                  borderRadius: 7,
                  cursor: 'pointer',
                  color: 'var(--text-muted)',
                  fontSize: 11,
                  fontFamily: 'var(--font-body)',
                  fontWeight: 500,
                  transition: 'all 150ms ease',
                  whiteSpace: 'nowrap',
                }}
                onMouseEnter={e => { e.currentTarget.style.borderColor = 'var(--red)'; e.currentTarget.style.color = 'var(--red)'; }}
                onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border)'; e.currentTarget.style.color = 'var(--text-muted)'; }}
              >
                Logout
              </button>
            )}
          </div>
        </div>
      </aside>

      {/* ── Main content ── */}
      <main style={{ flex: 1, overflowY: 'auto', overflowX: 'hidden', display: 'flex', flexDirection: 'column' }}>
        {/* Ticker Tape */}
        <TickerTape />

        {/* Market Status Bar */}
        <div className="market-status-bar">
          <span style={{ color: 'var(--cyan)', fontWeight: 700, letterSpacing: '0.06em' }}>KEN TRADER</span>
          <span style={{ color: 'var(--border-light)' }}>|</span>
          <span>
            KALSHI{' '}
            <span style={{ color: h?.kalshi_connected ? 'var(--green)' : 'var(--red)', fontWeight: 700 }}>
              {h?.kalshi_connected ? 'LIVE' : 'OFFLINE'}
            </span>
          </span>
          <span style={{ color: 'var(--border-light)' }}>|</span>
          <span>
            DB{' '}
            <span style={{ color: h?.db_connected ? 'var(--green)' : 'var(--red)', fontWeight: 700 }}>
              {h?.db_connected ? 'OK' : 'DOWN'}
            </span>
          </span>
          <span style={{ color: 'var(--border-light)' }}>|</span>
          <span>
            ENV{' '}
            <span style={{ color: h?.env === 'prod' ? 'var(--green)' : 'var(--orange)', fontWeight: 700 }}>
              {(h?.env || 'DEMO').toUpperCase()}
            </span>
          </span>
          {h?.safe_mode && (
            <>
              <span style={{ color: 'var(--border-light)' }}>|</span>
              <span style={{ color: 'var(--orange)', fontWeight: 700 }}>SAFE MODE ON</span>
            </>
          )}
          <span style={{ marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: 12 }}>
            <span style={{ color: 'var(--border-light)' }}>
              {new Date().toLocaleTimeString('en-US', { timeZone: 'America/Los_Angeles', hour: '2-digit', minute: '2-digit', second: '2-digit' })} PT
            </span>
            {/* Notification Bell */}
            <button
              className="notification-bell"
              aria-label="Alerts (7 unread)"
              title="7 unread alerts"
              style={{ background: 'none', border: 'none', display: 'flex', alignItems: 'center' }}
            >
              <svg fill="none" stroke="currentColor" strokeWidth="1.8" viewBox="0 0 24 24" aria-hidden="true">
                <path strokeLinecap="round" strokeLinejoin="round" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
              </svg>
              <span className="notification-count" aria-label="7 alerts">7</span>
            </button>
          </span>
        </div>

        <div style={{ maxWidth: 1600, margin: '0 auto', padding: '24px 28px', flex: 1, width: '100%' }}>
          <Outlet />
        </div>
      </main>
    </div>
  );
}