Spaces:
Running
Running
| 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> | |
| ); | |
| } |