NBA_PREDICTOR / web /src /pages /HeadToHead.jsx
jashdoshi77's picture
Add analytics, confidence meter, enhanced H2H, daily MVP refresh
3e6f1d3
import { useState, useEffect } from 'react'
import { getTeams, predictGame, getTeamStats } from '../api'
import { TeamLogo, getTeamName } from '../teamLogos'
// Stats Comparison Bar Component
function StatBar({ label, value1, value2, team1, team2, higherIsBetter = true }) {
const max = Math.max(value1, value2);
const width1 = max > 0 ? (value1 / max) * 100 : 50;
const width2 = max > 0 ? (value2 / max) * 100 : 50;
const team1Better = higherIsBetter ? value1 >= value2 : value1 <= value2;
return (
<div className="stat-comparison-row">
<div className="stat-bar-container">
<div className="stat-bar left" style={{ width: `${width1}%`, background: team1Better ? 'var(--accent-success)' : 'var(--bg-tertiary)' }} />
<span className="stat-value-label left" style={{ color: team1Better ? 'var(--accent-success)' : 'var(--text-secondary)' }}>{value1}</span>
</div>
<div className="stat-label-center">{label}</div>
<div className="stat-bar-container right">
<span className="stat-value-label right" style={{ color: !team1Better ? 'var(--accent-success)' : 'var(--text-secondary)' }}>{value2}</span>
<div className="stat-bar right" style={{ width: `${width2}%`, background: !team1Better ? 'var(--accent-success)' : 'var(--bg-tertiary)' }} />
</div>
</div>
);
}
// Recent Form Display
function RecentForm({ form }) {
if (!form || form.length === 0) return null;
return (
<div className="recent-form">
<div className="form-badges">
{form.map((game, i) => (
<span
key={i}
className={`form-badge ${game.result === 'W' ? 'win' : 'loss'}`}
title={`${game.result} vs ${game.opponent} (${game.score})`}
>
{game.result}
</span>
))}
</div>
</div>
);
}
function HeadToHead() {
const [teams, setTeams] = useState([])
const [team1, setTeam1] = useState('LAL')
const [team2, setTeam2] = useState('BOS')
const [comparison, setComparison] = useState(null)
const [teamStats, setTeamStats] = useState({ team1: null, team2: null })
const [loading, setLoading] = useState(false)
const [statsLoading, setStatsLoading] = useState(false)
useEffect(() => {
getTeams().then(data => {
setTeams(data.teams || [])
}).catch(console.error)
}, [])
const handleCompare = async () => {
if (!team1 || !team2 || team1 === team2) return
setLoading(true)
setStatsLoading(true)
try {
// Get predictions for both scenarios
const [homeGame, awayGame] = await Promise.all([
predictGame(team1, team2), // Team1 at home
predictGame(team2, team1), // Team2 at home
])
setComparison({
team1,
team2,
homeGame, // Team1 hosting Team2
awayGame, // Team2 hosting Team1
})
// Fetch detailed team stats in parallel
const [stats1, stats2] = await Promise.all([
getTeamStats(team1),
getTeamStats(team2)
])
setTeamStats({ team1: stats1, team2: stats2 })
} catch (err) {
console.error('Comparison failed:', err)
} finally {
setLoading(false)
setStatsLoading(false)
}
}
const stats1 = teamStats.team1;
const stats2 = teamStats.team2;
return (
<div className="animate-fadeIn">
<div className="page-header">
<h1 className="page-title">Head to Head</h1>
<p className="page-description">Compare two teams across different scenarios</p>
</div>
{/* Team Selector */}
<div className="card" style={{ marginBottom: 'var(--space-6)' }}>
<div style={{ display: 'grid', gridTemplateColumns: '1fr auto 1fr', gap: 'var(--space-6)', alignItems: 'end' }}>
{/* Team 1 */}
<div className="form-group" style={{ marginBottom: 0 }}>
<label className="form-label">Team 1</label>
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-3)' }}>
<TeamLogo abbrev={team1} size="md" />
<select
className="form-select"
value={team1}
onChange={(e) => setTeam1(e.target.value)}
>
{teams.map(team => (
<option key={team.id} value={team.abbrev}>{team.abbrev} - {getTeamName(team.abbrev)}</option>
))}
</select>
</div>
</div>
<div style={{ color: 'var(--text-dim)', fontWeight: '600', paddingBottom: 'var(--space-3)' }}>VS</div>
{/* Team 2 */}
<div className="form-group" style={{ marginBottom: 0 }}>
<label className="form-label">Team 2</label>
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-3)' }}>
<TeamLogo abbrev={team2} size="md" />
<select
className="form-select"
value={team2}
onChange={(e) => setTeam2(e.target.value)}
>
{teams.map(team => (
<option key={team.id} value={team.abbrev}>{team.abbrev} - {getTeamName(team.abbrev)}</option>
))}
</select>
</div>
</div>
</div>
<button
className="btn btn-primary btn-lg btn-block"
onClick={handleCompare}
disabled={loading || team1 === team2}
style={{ marginTop: 'var(--space-6)' }}
>
{loading ? 'Analyzing...' : 'Compare Teams'}
</button>
</div>
{/* Comparison Results */}
{comparison && (
<div className="animate-slideUp">
{/* Team Records Header */}
<div className="stats-grid" style={{ marginBottom: 'var(--space-6)' }}>
<div className="stat-card" style={{ textAlign: 'center' }}>
<TeamLogo abbrev={comparison.team1} size="lg" style={{ margin: '0 auto var(--space-3)' }} />
<div style={{ fontSize: '1.125rem', fontWeight: '600', marginBottom: 'var(--space-2)' }}>
{getTeamName(comparison.team1)}
</div>
{stats1?.record && (
<div style={{ fontSize: '0.875rem', color: 'var(--text-secondary)', marginBottom: 'var(--space-2)' }}>
{stats1.record.wins}-{stats1.record.losses}
</div>
)}
<div className="stat-value accent">
{comparison.homeGame?.home_elo?.toFixed(0) || 'N/A'}
</div>
<div className="stat-label">ELO Rating</div>
{stats1?.recent_form && (
<div style={{ marginTop: 'var(--space-3)' }}>
<div style={{ fontSize: '0.625rem', color: 'var(--text-muted)', marginBottom: 'var(--space-1)' }}>LAST 5</div>
<RecentForm form={stats1.recent_form} />
</div>
)}
</div>
<div className="stat-card" style={{ textAlign: 'center', display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
<div style={{ fontSize: '0.75rem', color: 'var(--text-muted)', marginBottom: 'var(--space-2)' }}>ELO DIFFERENCE</div>
<div style={{ fontSize: '2rem', fontWeight: '700', color: comparison.homeGame?.elo_diff > 0 ? 'var(--accent-success)' : 'var(--accent-danger)' }}>
{comparison.homeGame?.elo_diff > 0 ? '+' : ''}{comparison.homeGame?.elo_diff?.toFixed(0) || 0}
</div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-muted)', marginTop: 'var(--space-2)' }}>
{comparison.homeGame?.elo_diff > 0 ? `${comparison.team1} favored` : `${comparison.team2} favored`}
</div>
</div>
<div className="stat-card" style={{ textAlign: 'center' }}>
<TeamLogo abbrev={comparison.team2} size="lg" style={{ margin: '0 auto var(--space-3)' }} />
<div style={{ fontSize: '1.125rem', fontWeight: '600', marginBottom: 'var(--space-2)' }}>
{getTeamName(comparison.team2)}
</div>
{stats2?.record && (
<div style={{ fontSize: '0.875rem', color: 'var(--text-secondary)', marginBottom: 'var(--space-2)' }}>
{stats2.record.wins}-{stats2.record.losses}
</div>
)}
<div className="stat-value accent">
{comparison.homeGame?.away_elo?.toFixed(0) || 'N/A'}
</div>
<div className="stat-label">ELO Rating</div>
{stats2?.recent_form && (
<div style={{ marginTop: 'var(--space-3)' }}>
<div style={{ fontSize: '0.625rem', color: 'var(--text-muted)', marginBottom: 'var(--space-1)' }}>LAST 5</div>
<RecentForm form={stats2.recent_form} />
</div>
)}
</div>
</div>
{/* Stats Comparison */}
{stats1?.stats && stats2?.stats && (
<div className="card" style={{ marginBottom: 'var(--space-6)' }}>
<h3 style={{ marginBottom: 'var(--space-4)', textAlign: 'center' }}>Season Statistics</h3>
<div className="stats-comparison">
<StatBar label="PPG" value1={stats1.stats.ppg} value2={stats2.stats.ppg} team1={team1} team2={team2} />
<StatBar label="Opp PPG" value1={stats1.stats.opp_ppg} value2={stats2.stats.opp_ppg} team1={team1} team2={team2} higherIsBetter={false} />
<StatBar label="RPG" value1={stats1.stats.rpg} value2={stats2.stats.rpg} team1={team1} team2={team2} />
<StatBar label="APG" value1={stats1.stats.apg} value2={stats2.stats.apg} team1={team1} team2={team2} />
<StatBar label="FG%" value1={stats1.stats.fg_pct} value2={stats2.stats.fg_pct} team1={team1} team2={team2} />
<StatBar label="3P%" value1={stats1.stats.fg3_pct} value2={stats2.stats.fg3_pct} team1={team1} team2={team2} />
</div>
</div>
)}
{/* Key Players */}
{(stats1?.key_players?.length > 0 || stats2?.key_players?.length > 0) && (
<div className="card" style={{ marginBottom: 'var(--space-6)' }}>
<h3 style={{ marginBottom: 'var(--space-4)', textAlign: 'center' }}>Key Players</h3>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 'var(--space-4)' }}>
<div>
<div style={{ textAlign: 'center', marginBottom: 'var(--space-2)' }}>
<TeamLogo abbrev={team1} size="sm" />
<span style={{ marginLeft: 'var(--space-2)', fontWeight: '600' }}>{team1}</span>
</div>
{stats1?.key_players?.map((player, i) => (
<div key={i} style={{ display: 'flex', justifyContent: 'space-between', padding: 'var(--space-2) 0', borderBottom: 'var(--border-subtle)' }}>
<span>{player.name}</span>
<span style={{ color: 'var(--text-muted)' }}>{player.position}</span>
</div>
))}
</div>
<div>
<div style={{ textAlign: 'center', marginBottom: 'var(--space-2)' }}>
<TeamLogo abbrev={team2} size="sm" />
<span style={{ marginLeft: 'var(--space-2)', fontWeight: '600' }}>{team2}</span>
</div>
{stats2?.key_players?.map((player, i) => (
<div key={i} style={{ display: 'flex', justifyContent: 'space-between', padding: 'var(--space-2) 0', borderBottom: 'var(--border-subtle)' }}>
<span>{player.name}</span>
<span style={{ color: 'var(--text-muted)' }}>{player.position}</span>
</div>
))}
</div>
</div>
</div>
)}
{/* Scenario Cards */}
<h3 style={{ marginBottom: 'var(--space-4)' }}>Win Probabilities by Venue</h3>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 'var(--space-4)' }}>
{/* Scenario 1: Team1 at Home */}
<div className="card">
<div className="card-header">
<span className="card-title">{comparison.team1} Home Game</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 'var(--space-4)' }}>
<div style={{ textAlign: 'center' }}>
<TeamLogo abbrev={comparison.team1} size="md" />
<div style={{ fontSize: '1.5rem', fontWeight: '700', color: 'var(--accent-primary)', marginTop: 'var(--space-2)' }}>
{(comparison.homeGame?.home_win_probability * 100).toFixed(1)}%
</div>
</div>
<div style={{ color: 'var(--text-dim)' }}>vs</div>
<div style={{ textAlign: 'center' }}>
<TeamLogo abbrev={comparison.team2} size="md" />
<div style={{ fontSize: '1.5rem', fontWeight: '700', color: 'var(--accent-secondary)', marginTop: 'var(--space-2)' }}>
{(comparison.homeGame?.away_win_probability * 100).toFixed(1)}%
</div>
</div>
</div>
<div style={{ fontSize: '0.875rem', color: 'var(--text-muted)', textAlign: 'center' }}>
Prediction: <span style={{ color: 'var(--accent-primary)', fontWeight: '600' }}>{comparison.homeGame?.predicted_winner}</span>
</div>
</div>
{/* Scenario 2: Team2 at Home */}
<div className="card">
<div className="card-header">
<span className="card-title">{comparison.team2} Home Game</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 'var(--space-4)' }}>
<div style={{ textAlign: 'center' }}>
<TeamLogo abbrev={comparison.team2} size="md" />
<div style={{ fontSize: '1.5rem', fontWeight: '700', color: 'var(--accent-primary)', marginTop: 'var(--space-2)' }}>
{(comparison.awayGame?.home_win_probability * 100).toFixed(1)}%
</div>
</div>
<div style={{ color: 'var(--text-dim)' }}>vs</div>
<div style={{ textAlign: 'center' }}>
<TeamLogo abbrev={comparison.team1} size="md" />
<div style={{ fontSize: '1.5rem', fontWeight: '700', color: 'var(--accent-secondary)', marginTop: 'var(--space-2)' }}>
{(comparison.awayGame?.away_win_probability * 100).toFixed(1)}%
</div>
</div>
</div>
<div style={{ fontSize: '0.875rem', color: 'var(--text-muted)', textAlign: 'center' }}>
Prediction: <span style={{ color: 'var(--accent-primary)', fontWeight: '600' }}>{comparison.awayGame?.predicted_winner}</span>
</div>
</div>
</div>
</div>
)}
</div>
)
}
export default HeadToHead