124 lines
4.3 KiB
TypeScript
124 lines
4.3 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import { getHeadshotUrl, PLAYER_SILHOUETTE } from '@/lib/playerHeadshot';
|
|
import { getVisibleCount, getHiddenCount, type Tier } from '@/lib/tierGate';
|
|
|
|
/**
|
|
* HotListPanel (Session 23).
|
|
*
|
|
* Rolling recent-window leaders — ranked by who's TRENDING, not who has
|
|
* the biggest raw number. A 20-PPG player erupting for 28/31/25 is hot;
|
|
* a 30-PPG star who dropped 28 is not. Free users see the top 3.
|
|
*
|
|
* Self-hides when empty so the landing page never renders a dead box.
|
|
*/
|
|
|
|
interface HotPlayer {
|
|
rank: number;
|
|
name: string;
|
|
playerId: string | number | null;
|
|
team: string | null;
|
|
stat: string;
|
|
recentAvg: number;
|
|
statLine: string;
|
|
trendDescription: string;
|
|
}
|
|
|
|
export interface HotListPanelProps {
|
|
sport: string;
|
|
tier?: Tier;
|
|
stat?: string;
|
|
limit?: number;
|
|
}
|
|
|
|
export default function HotListPanel({ sport, tier = 'free', stat = 'all', limit }: HotListPanelProps) {
|
|
const [players, setPlayers] = useState<HotPlayer[] | null>(null);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
async function load() {
|
|
try {
|
|
const res = await fetch(`/api/hotlist/${sport}?stat=${encodeURIComponent(stat)}`);
|
|
if (!res.ok) { if (!cancelled) setPlayers([]); return; }
|
|
const data = await res.json();
|
|
if (!cancelled) setPlayers(Array.isArray(data?.players) ? data.players : []);
|
|
} catch {
|
|
if (!cancelled) setPlayers([]);
|
|
}
|
|
}
|
|
load();
|
|
return () => { cancelled = true; };
|
|
}, [sport, stat]);
|
|
|
|
if (!players || players.length === 0) return null;
|
|
|
|
const tierCount = getVisibleCount(tier, players.length);
|
|
const cap = limit && limit > 0 ? Math.min(limit, tierCount) : tierCount;
|
|
const visible = players.slice(0, cap);
|
|
const hidden = limit ? players.length - visible.length : getHiddenCount(tier, players.length);
|
|
|
|
return (
|
|
<section className="hot-list-panel" style={{ margin: '16px 0' }}>
|
|
<h3 style={panelHeading}>📈 HOT RIGHT NOW</h3>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
{visible.map((p) => (
|
|
<div key={`${p.name}-${p.stat}`} style={rowStyle}>
|
|
<span style={rankStyle}>#{p.rank}</span>
|
|
<img
|
|
src={getHeadshotUrl({ sport, playerId: p.playerId })}
|
|
alt={p.name}
|
|
width={36}
|
|
height={36}
|
|
style={avatarStyle}
|
|
onError={(e) => { (e.target as HTMLImageElement).src = PLAYER_SILHOUETTE; }}
|
|
/>
|
|
<div style={{ flex: 1, minWidth: 0 }}>
|
|
<div style={playerName}>{p.name}{p.team ? ` · ${p.team}` : ''}</div>
|
|
<div style={statLineStyle}>{p.statLine}</div>
|
|
</div>
|
|
<span style={trendStyle}>{p.trendDescription}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
{hidden > 0 && (
|
|
<a href="/pricing" style={upsellStyle}>
|
|
{hidden} more — upgrade to see the full board →
|
|
</a>
|
|
)}
|
|
</section>
|
|
);
|
|
}
|
|
|
|
const panelHeading: React.CSSProperties = {
|
|
fontSize: 12, letterSpacing: '0.12em', textTransform: 'uppercase',
|
|
color: 'var(--text-tertiary, #6A6A78)', margin: '0 0 10px',
|
|
};
|
|
const rowStyle: React.CSSProperties = {
|
|
display: 'flex', alignItems: 'center', gap: 10,
|
|
padding: '8px 10px', borderRadius: 10,
|
|
background: 'var(--surface, #12121A)', border: '1px solid var(--border, #2A2A36)',
|
|
};
|
|
const rankStyle: React.CSSProperties = {
|
|
flex: '0 0 auto', fontSize: 13, fontWeight: 800, width: 28,
|
|
color: 'var(--text-tertiary, #6A6A78)',
|
|
};
|
|
const avatarStyle: React.CSSProperties = {
|
|
borderRadius: '50%', objectFit: 'cover', background: '#1A1A24', flex: '0 0 auto',
|
|
};
|
|
const playerName: React.CSSProperties = {
|
|
fontSize: 14, fontWeight: 700, color: 'var(--text-primary, #F0F0F4)',
|
|
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
|
|
};
|
|
const statLineStyle: React.CSSProperties = {
|
|
fontSize: 12, color: 'var(--text-secondary, #9A9AA8)',
|
|
};
|
|
const trendStyle: React.CSSProperties = {
|
|
flex: '0 0 auto', fontSize: 11, fontWeight: 700, padding: '3px 8px',
|
|
borderRadius: 6, background: 'rgba(46,160,67,0.15)', color: '#3FB950',
|
|
};
|
|
const upsellStyle: React.CSSProperties = {
|
|
display: 'inline-block', marginTop: 10, fontSize: 12, fontWeight: 600,
|
|
color: 'var(--accent, #E94B3C)', textDecoration: 'none',
|
|
};
|