'use client'; import { useEffect, useState } from 'react'; import { GradePill } from '@/components/GradeCard'; import PlayerAvatar from '@/components/vyndr/PlayerAvatar'; import BookWordmark from '@/components/vyndr/BookWordmark'; /** * PublicProfile (A1 Session 10) — the public ledger record for one handle. * * DATA SEMANTICS: every row here is a REAL settled ledger entry — outcome, * actual value, closing-line value. Nothing curated: publishing shows the * ENTIRE settled record, misses included. The header NEVER renders a * percentage under min_sample (20) settles — it shows RECORD BUILDING. * * Unknown and unpublished handles arrive as the same 404 — the page renders * one indistinguishable not-found state for both. */ interface ProfileRow { id: string; player_name: string; sport: string; stat: string; line: number; side: 'over' | 'under'; locked_odds?: string | null; book?: string | null; grade: string; model_value?: number | null; game_date: string; closing_line?: number | null; clv?: number | null; clv_result?: 'beat' | 'faded' | 'flat' | null; outcome?: 'hit' | 'miss' | 'push' | null; actual_value?: number | null; revised_from_grade?: string | null; } interface ProfileAggregate { settled: number; hits: number; misses: number; pushes: number; hit_pct: number | null; clv_sample: number; clv_beat: number; beat_close_pct: number | null; pending: number; } interface ProfilePayload { handle: string; aggregate: ProfileAggregate | null; entries: ProfileRow[]; min_sample?: number; } const SPORT_COLOR: Record = { nba: '#E94B3C', mlb: '#1E90FF', wnba: '#FFB347', soccer: '#7BC96F', }; export default function PublicProfile({ handle }: { handle: string }) { const [data, setData] = useState(null); const [state, setState] = useState<'loading' | 'ready' | 'notfound'>('loading'); useEffect(() => { let active = true; setState('loading'); fetch(`/api/profiles/${encodeURIComponent(handle)}`) .then(async (r) => { if (!active) return; if (!r.ok) { setState('notfound'); return; } const d = await r.json(); if (!active) return; setData(d); setState('ready'); }) .catch(() => { if (active) setState('notfound'); }); return () => { active = false; }; }, [handle]); if (state === 'loading') { return (

Loading record…

); } if (state === 'notfound' || !data) { // Same state for unknown AND unpublished — the API never tells us which. return (

NO RECORD HERE

This profile does not exist or is not published.

VYNDR ledgers are private by default. A record only appears here when its owner publishes it.

); } const agg = data.aggregate; const minSample = Number(data.min_sample) > 0 ? Number(data.min_sample) : 20; return (

PUBLIC LEDGER

@{data.handle}

Every settled read. Wins and misses. Nothing curated.

{/* Record header — never a percentage under min_sample settles. */} {data.entries.length === 0 ? (

NO SETTLED READS YET

Reads land here as they settle against real results.

) : (
{data.entries.map((row, i) => ( ))}
)}
); } /** * The record HERO (DS4 · billboard — DESIGN-SPEC Part 3 + Part 6 #4). This is * the shareable surface Kev pitches partners with. One bold figure among muted * context: the HIT RATE as a large mono tabular number, beat-close as the * secondary bold figure, the raw record demoted. CLV-VERIFIED badge only when * closing-line value is actually tracked (honest — never a decorative badge). */ function RecordHeader({ agg, minSample }: { agg: ProfileAggregate | null; minSample: number }) { const ready = Boolean(agg && agg.settled >= minSample && agg.hit_pct != null); const clvVerified = Boolean(ready && agg && agg.beat_close_pct != null); return (
{ready && agg ? ( <>
RECORD · LAST 30D {clvVerified && ( ✓ CLV-VERIFIED )}
{/* HERO figures — hit rate is the largest thing on the page. */}
{agg.hit_pct}%
HIT RATE · {agg.hits}-{agg.misses}
{agg.beat_close_pct != null && (
{agg.beat_close_pct}%
BEAT CLOSE
)}
{agg.pending} pending
) : (

RECORD BUILDING

Percentages render at {minSample} settled reads. {agg && ( {' '}{agg.settled} settled · {agg.pending} pending )}

)}
); } function OutcomeChip({ row }: { row: ProfileRow }) { if (!row.outcome) { return PENDING; } const color = row.outcome === 'hit' ? 'var(--g-a, #00D4A0)' : row.outcome === 'miss' ? 'var(--miss, #FF6B6B)' : 'var(--text-secondary)'; const mark = row.outcome === 'hit' ? '✓ HIT' : row.outcome === 'miss' ? '✕ MISS' : '– PUSH'; return ( {mark}{row.actual_value != null ? ` (${row.actual_value})` : ''} ); } function ClvChip({ row }: { row: ProfileRow }) { if (!row.clv_result || row.clv == null) return null; const color = row.clv_result === 'beat' ? 'var(--g-a, #00D4A0)' : row.clv_result === 'faded' ? 'var(--miss, #FF6B6B)' : 'var(--text-tertiary)'; return ( CLV {row.clv > 0 ? '+' : ''}{row.clv} · {row.clv_result.toUpperCase()} ); } function ProfileCard({ row, index }: { row: ProfileRow; index: number }) { const sportColor = SPORT_COLOR[row.sport] || 'var(--text-secondary)'; return (
{row.sport.toUpperCase()} {row.revised_from_grade && ( {row.revised_from_grade} )}
{/* Real entity identity (DS0) — headshot / team-colored monogram. */}

{row.player_name}

{row.side} {row.line} {row.stat.replace(/_/g, ' ')}

{row.book ? : '—'}{row.locked_odds ? ` · ${row.locked_odds}` : ''} · {row.game_date} {row.model_value != null && · MODEL {row.model_value}}

); }