Sessions 5-7a: 955 tests, deployment ready
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
|
||||
type Signal = {
|
||||
id: string;
|
||||
ts: string;
|
||||
sport: 'NBA' | 'MLB' | 'WNBA';
|
||||
severity: 'critical' | 'notable' | 'info';
|
||||
type: 'evolution' | 'coaching' | 'cascade' | 'abs' | 'line_movement';
|
||||
title: string;
|
||||
detail: string;
|
||||
players?: string[];
|
||||
};
|
||||
|
||||
const SEVERITY: Record<Signal['severity'], { dot: string; label: string }> = {
|
||||
critical: { dot: '#FF4757', label: 'CRITICAL' },
|
||||
notable: { dot: '#FFB347', label: 'NOTABLE' },
|
||||
info: { dot: '#4A9EFF', label: 'INFO' },
|
||||
};
|
||||
|
||||
const SPORT_COLOR: Record<Signal['sport'], string> = {
|
||||
NBA: '#E94B3C',
|
||||
MLB: '#1E90FF',
|
||||
WNBA: '#FFB347',
|
||||
};
|
||||
|
||||
export default function IntelligencePage() {
|
||||
const router = useRouter();
|
||||
const { user, tier, loading: authLoading } = useAuth();
|
||||
const [signals, setSignals] = useState<Signal[] | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!authLoading && !user) router.replace('/login?next=/intelligence');
|
||||
}, [authLoading, user, router]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/intelligence/feed')
|
||||
.then((r) => r.json())
|
||||
.then((data) => setSignals(Array.isArray(data?.signals) ? data.signals : []))
|
||||
.catch(() => setSignals([]));
|
||||
}, []);
|
||||
|
||||
const locked = tier !== 'desk';
|
||||
|
||||
if (authLoading || !user) {
|
||||
return (
|
||||
<section style={{ minHeight: '60vh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<p className="mono" style={{ color: 'var(--text-tertiary)' }}>Loading intelligence feed…</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section style={{ maxWidth: 760, margin: '0 auto', padding: '24px 16px 120px' }}>
|
||||
<header style={{ marginBottom: 24 }}>
|
||||
<h1 style={{ fontSize: 28, fontWeight: 700, letterSpacing: '-0.02em', marginBottom: 6 }}>
|
||||
Intelligence feed
|
||||
</h1>
|
||||
<p style={{ color: 'var(--text-secondary)', fontSize: 14 }}>
|
||||
Real-time signals the books wish you didn't see. Evolution. Coaching shifts. Cascade effects. Line movement.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
{signals === null ? (
|
||||
<p className="mono" style={{ color: 'var(--text-tertiary)' }}>Loading signals…</p>
|
||||
) : signals.length === 0 ? (
|
||||
<div className="surface" style={{ padding: 32, textAlign: 'center', color: 'var(--text-secondary)' }}>
|
||||
<p className="mono" style={{ fontSize: 11, color: 'var(--text-tertiary)', letterSpacing: '0.08em', marginBottom: 8 }}>
|
||||
QUIET NIGHT
|
||||
</p>
|
||||
<p>No signals yet tonight. The engine is watching. When something moves, you'll see it here first.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ position: 'relative' }}>
|
||||
<ol style={{ position: 'relative', display: 'grid', gap: 12, listStyle: 'none', padding: 0 }} className={locked ? 'tier-locked' : ''}>
|
||||
{signals.map((s, idx) => (
|
||||
<li
|
||||
key={s.id}
|
||||
className={`surface diagonal-cut animate-fade-up stagger-${(idx % 6) + 1}`}
|
||||
style={{ padding: 18, display: 'grid', gridTemplateColumns: 'auto 1fr', gap: 16 }}
|
||||
>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', paddingTop: 4 }}>
|
||||
<span
|
||||
style={{
|
||||
width: 10,
|
||||
height: 10,
|
||||
borderRadius: 999,
|
||||
background: SEVERITY[s.severity].dot,
|
||||
boxShadow: `0 0 0 4px ${SEVERITY[s.severity].dot}33`,
|
||||
}}
|
||||
aria-label={SEVERITY[s.severity].label}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 8, marginBottom: 6 }}>
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
||||
<span
|
||||
className="mono"
|
||||
style={{
|
||||
fontSize: 10,
|
||||
padding: '2px 8px',
|
||||
borderRadius: 999,
|
||||
background: `${SPORT_COLOR[s.sport]}1F`,
|
||||
color: SPORT_COLOR[s.sport],
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
{s.sport}
|
||||
</span>
|
||||
<span className="mono" style={{ fontSize: 10, color: SEVERITY[s.severity].dot, letterSpacing: '0.08em' }}>
|
||||
{s.type.toUpperCase().replace('_', ' ')}
|
||||
</span>
|
||||
</div>
|
||||
<span className="mono" style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>
|
||||
{formatRelative(s.ts)}
|
||||
</span>
|
||||
</div>
|
||||
<h3 style={{ fontSize: 15, fontWeight: 600, marginBottom: 4 }}>{s.title}</h3>
|
||||
<p style={{ fontSize: 13, color: 'var(--text-secondary)', lineHeight: 1.5 }}>{s.detail}</p>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
|
||||
{locked && (
|
||||
<div
|
||||
className="tier-locked-overlay"
|
||||
style={{ minHeight: 240 }}
|
||||
>
|
||||
<p style={{ fontSize: 15, fontWeight: 600 }}>
|
||||
Real-time intelligence is a Desk feature.
|
||||
</p>
|
||||
<p style={{ fontSize: 13, color: 'var(--text-secondary)', maxWidth: 360 }}>
|
||||
Evolution alerts. Coaching shifts. Cascade effects. ABS strike zone intel. Line movement signals across the slate.
|
||||
</p>
|
||||
<a href="/api/checkout?tier=desk" className="btn-primary">
|
||||
Go Desk — $44.99/mo
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function formatRelative(ts: string): string {
|
||||
const diff = Date.now() - new Date(ts).getTime();
|
||||
if (!Number.isFinite(diff)) return '—';
|
||||
const m = Math.floor(diff / 60000);
|
||||
if (m < 1) return 'just now';
|
||||
if (m < 60) return `${m}m ago`;
|
||||
const h = Math.floor(m / 60);
|
||||
if (h < 24) return `${h}h ago`;
|
||||
const d = Math.floor(h / 24);
|
||||
return `${d}d ago`;
|
||||
}
|
||||
Reference in New Issue
Block a user