← 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>
);
}