← back to Ken

kalshi-dash/src/components/TickerTape.jsx

147 lines

import React, { useMemo } from 'react';

/**
 * TickerTape — horizontally scrolling market ticker bar
 *
 * Shows 15-20 fake Kalshi-style contract tickers scrolling left infinitely.
 * Integrated in Layout.jsx above the main content area.
 */

const TICKERS = [
  { sym: 'POTUS-2028', price: '52c', chg: '+3.1%', up: true },
  { sym: 'FED-RATE-CUT', price: '78c', chg: '-1.2%', up: false },
  { sym: 'BTC-100K', price: '34c', chg: '+5.8%', up: true },
  { sym: 'NVIDIA-BEAT', price: '91c', chg: '+0.4%', up: true },
  { sym: 'SP-5500', price: '67c', chg: '-2.3%', up: false },
  { sym: 'INFLATION-4PCT', price: '21c', chg: '+1.7%', up: true },
  { sym: 'RECESSION-2025', price: '18c', chg: '-0.9%', up: false },
  { sym: 'ETH-5K', price: '45c', chg: '+2.2%', up: true },
  { sym: 'APPLE-3T', price: '83c', chg: '+0.6%', up: true },
  { sym: 'FED-HOLD-JUN', price: '62c', chg: '-1.5%', up: false },
  { sym: 'JOBS-200K', price: '55c', chg: '+0.8%', up: true },
  { sym: 'OIL-90', price: '29c', chg: '-3.1%', up: false },
  { sym: 'OPENAI-IPO', price: '41c', chg: '+4.4%', up: true },
  { sym: 'GOLD-3K', price: '71c', chg: '+1.9%', up: true },
  { sym: 'TESLA-SPLIT', price: '37c', chg: '-0.7%', up: false },
  { sym: 'CPI-3PCT', price: '48c', chg: '+2.6%', up: true },
  { sym: 'TRUMP-TARIFF', price: '86c', chg: '+0.3%', up: true },
  { sym: 'VIX-SPIKE', price: '16c', chg: '-4.2%', up: false },
  { sym: 'SPACEX-LAUNCH', price: '94c', chg: '+0.1%', up: true },
  { sym: 'SENATE-FLIP', price: '33c', chg: '+3.7%', up: true },
];

// Dot separator between ticker items
function Dot() {
  return (
    <span
      style={{
        display: 'inline-block',
        width: 3,
        height: 3,
        borderRadius: '50%',
        background: 'var(--cyan)',
        opacity: 0.4,
        margin: '0 14px',
        verticalAlign: 'middle',
        flexShrink: 0,
      }}
    />
  );
}

function TickerItem({ sym, price, chg, up }) {
  return (
    <span
      style={{
        display: 'inline-flex',
        alignItems: 'center',
        gap: 6,
        whiteSpace: 'nowrap',
        fontFamily: 'var(--font-mono)',
        fontSize: '0.68rem',
      }}
    >
      <span style={{ color: 'var(--text-dim)', fontWeight: 600, letterSpacing: '0.04em' }}>{sym}</span>
      <span style={{ color: 'var(--text)', fontWeight: 700 }}>{price}</span>
      <span
        style={{
          color: up ? 'var(--green)' : 'var(--red)',
          fontWeight: 700,
          fontSize: '0.62rem',
        }}
      >
        {chg}
      </span>
    </span>
  );
}

export default function TickerTape() {
  // Double the items so the seamless loop works
  const items = useMemo(() => [...TICKERS, ...TICKERS], []);

  return (
    <div
      style={{
        width: '100%',
        overflow: 'hidden',
        background: 'var(--surface)',
        borderBottom: '1px solid var(--border)',
        borderTop: '1px solid var(--border)',
        height: 32,
        display: 'flex',
        alignItems: 'center',
        position: 'relative',
      }}
      aria-label="Live market ticker"
      role="marquee"
    >
      {/* Left fade mask */}
      <div
        style={{
          position: 'absolute',
          left: 0,
          top: 0,
          bottom: 0,
          width: 48,
          background: 'linear-gradient(90deg, var(--surface), transparent)',
          zIndex: 2,
          pointerEvents: 'none',
        }}
      />

      {/* Right fade mask */}
      <div
        style={{
          position: 'absolute',
          right: 0,
          top: 0,
          bottom: 0,
          width: 48,
          background: 'linear-gradient(270deg, var(--surface), transparent)',
          zIndex: 2,
          pointerEvents: 'none',
        }}
      />

      {/* Scrolling strip */}
      <div
        style={{
          display: 'inline-flex',
          alignItems: 'center',
          animation: 'ticker 60s linear infinite',
          willChange: 'transform',
          paddingLeft: 24,
        }}
      >
        {items.map((t, i) => (
          <React.Fragment key={i}>
            <TickerItem {...t} />
            <Dot />
          </React.Fragment>
        ))}
      </div>
    </div>
  );
}