fpl-solver / frontend /src /components /Fixtures.jsx
AnayShukla's picture
updates
052f3f2
import React, { useState, useEffect, useContext } from 'react';
import { getShortName } from '../utils/teams';
import { PlayerContext } from '../PlayerContext';
export default function Fixtures() {
const [fixtures, setFixtures] = useState([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetch('https://anayshukla-fpl-solver.hf.space/api/fixtures')
.then(res => res.json())
.then(data => {
setFixtures(data);
setIsLoading(false);
});
}, []);
if (isLoading) return <div className="text-luigi-400 animate-pulse p-8">Loading fixtures...</div>;
const { effectiveFixtures } = useContext(PlayerContext);
const TEAM_MAP = {
"Arsenal": 1, "Aston Villa": 2, "Burnley": 3, "AFC Bournemouth": 4, "Brentford": 5,
"Brighton": 6, "Brighton and Hove Albion": 6, "Chelsea": 7, "Crystal Palace": 8, "Everton": 9, "Fulham": 10,
"Leeds United": 11, "Liverpool": 12, "Man City": 13, "Manchester City": 13,
"Man Utd": 14, "Manchester United": 14, "Newcastle": 15, "Newcastle United": 15,
"Nott'm Forest": 16, "Nottingham Forest": 16, "Sunderland": 17,
"Spurs": 18, "Tottenham": 18, "Tottenham Hotspur": 18,
"West Ham": 19, "West Ham United": 19, "Wolves": 20, "Wolverhampton Wanderers": 20
};
const expandedFixtures = [];
fixtures.forEach(match => {
const hId = match.home_team_id || TEAM_MAP[match.home_team] || match.home_team;
const aId = match.away_team_id || TEAM_MAP[match.away_team] || match.away_team;
const matchId = `${hId}_vs_${aId}`;
const override = effectiveFixtures?.[matchId];
if (override) {
Object.entries(override).forEach(([gw, prob]) => {
// THE FIX: Prevent floating point ghost fixtures (must be >= 0.5%)
if (Number(prob) >= 0.005) {
expandedFixtures.push({ ...match, GW: Number(gw), shiftProb: Number(prob) });
}
});
} else {
expandedFixtures.push({ ...match, shiftProb: 1.0 });
}
});
const groupedFixtures = expandedFixtures.reduce((acc, match) => {
(acc[match.GW] = acc[match.GW] || []).push(match);
return acc;
}, {});
return (
<div className="space-y-8">
{Object.entries(groupedFixtures).map(([gw, matches]) => (
<div key={gw} className="bg-slate-900/40 p-6 rounded-xl border border-slate-800 backdrop-blur-sm shadow-xl">
<h3 className="text-xl font-bold text-luigi-400 mb-4 border-b border-slate-800 pb-2">Gameweek {gw}</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 sm:gap-4">
{matches.map((match, idx) => {
const hw = match.home_win_prob;
const aw = match.away_win_prob;
const isGhost = match.shiftProb < 0.995;
const homeBox = hw > aw ? 'text-emerald-400 bg-emerald-900/30' : (hw < aw ? 'text-rose-400 bg-rose-900/30' : 'text-slate-300 bg-slate-800/50');
const awayBox = aw > hw ? 'text-emerald-400 bg-emerald-900/30' : (aw < hw ? 'text-rose-400 bg-rose-900/30' : 'text-slate-300 bg-slate-800/50');
const ghostStyles = isGhost
? "border-dashed border-indigo-500/50 opacity-80 bg-[repeating-linear-gradient(45deg,transparent,transparent_10px,rgba(99,102,241,0.05)_10px,rgba(99,102,241,0.05)_20px)]"
: "border-slate-800/80 hover:border-slate-600";
return (
<div
key={`${idx}-${match.shiftProb}`}
title={isGhost ? `${Math.round(match.shiftProb * 100)}% chance of playing in GW${gw}` : `Confirmed Fixture`}
className={`relative bg-slate-950 rounded-lg border overflow-hidden shadow-lg transition-colors ${ghostStyles}`}
>
{isGhost && (
<div className="absolute top-2 right-2 bg-indigo-900/80 text-indigo-300 text-[10px] font-black px-2 py-0.5 rounded border border-indigo-500/50 backdrop-blur-md z-tooltip shadow-lg">
{Math.round(match.shiftProb * 100)}% Chance
</div>
)}
{/* Header: Teams & xG */}
<div className="bg-slate-900/80 px-4 py-3 flex justify-between items-center border-b border-slate-800">
<div className="flex flex-col items-center w-1/3">
<span className="text-lg font-bold text-slate-100">{getShortName(match.home_team)}</span>
{/* UPDATED: Larger, bolder xG text */}
<span className="text-base font-mono font-bold text-slate-300 mt-1">
{match.expected_home_goals.toFixed(2)} xG
</span>
</div>
<span className="text-slate-600 text-xs font-bold uppercase tracking-widest bg-slate-950 px-2 py-1 rounded-full border border-slate-800">vs</span>
<div className="flex flex-col items-center w-1/3">
<span className="text-lg font-bold text-slate-100">{getShortName(match.away_team)}</span>
{/* UPDATED: Larger, bolder xG text */}
<span className="text-base font-mono font-bold text-slate-300 mt-1">
{match.expected_away_goals.toFixed(2)} xG
</span>
</div>
</div>
{/* Body: Probabilities & Clean Sheets */}
<div className="p-3">
<div className="flex justify-between text-[10px] text-slate-400 font-mono text-center mb-3">
<div className="flex flex-col w-[30%]">
<span className="mb-1">HOME WIN</span>
<span className={`text-sm py-1 rounded font-bold ${homeBox}`}>{(match.home_win_prob * 100).toFixed(1)}%</span>
</div>
<div className="flex flex-col w-[30%]">
<span className="mb-1">DRAW</span>
<span className="text-slate-300 text-sm bg-slate-800/30 py-1 rounded font-bold">{(match.draw_prob * 100).toFixed(1)}%</span>
</div>
<div className="flex flex-col w-[30%]">
<span className="mb-1">AWAY WIN</span>
<span className={`text-sm py-1 rounded font-bold ${awayBox}`}>{(match.away_win_prob * 100).toFixed(1)}%</span>
</div>
</div>
{/* Clean Sheet Odds */}
<div className="flex justify-between border-t border-slate-800/50 pt-2 text-xs">
<div className="text-slate-400">Home CS: <span className="text-luigi-400 font-mono font-bold">{(match.home_clean_sheet_odds * 100).toFixed(1)}%</span></div>
<div className="text-slate-400">Away CS: <span className="text-luigi-400 font-mono font-bold">{(match.away_clean_sheet_odds * 100).toFixed(1)}%</span></div>
</div>
</div>
</div>
);
})}
</div>
</div>
))}
</div>
);
}