// src/components/PlayerModals.jsx import React,{ useState, useEffect, useContext } from "react"; import { Search, Plus } from "lucide-react"; import { getPlayerPrice } from "../utils/fplLogic"; import { getShortName } from "../utils/teams"; import { PlayerContext } from "../PlayerContext"; const SafeMinsInput = ({ initialValue, onSave, isChild = false, disabled = false }) => { const [val, setVal] = useState(initialValue); useEffect(() => setVal(initialValue), [initialValue]); return ( { setVal(e.target.value); onSave(e.target.value); }} className={isChild ? "w-12 bg-slate-950 text-center font-mono text-xs font-bold text-indigo-400 rounded py-1 outline-none focus:ring-1 ring-indigo-500 border border-slate-800" : `w-16 text-center font-mono text-sm font-bold rounded py-1 outline-none ${disabled ? 'bg-transparent text-slate-500' : 'bg-slate-900 text-emerald-400 focus:bg-slate-800 focus:ring-1 ring-emerald-500'}` } /> ); }; export const PlayerEditModal = ({ selectedPlayer, setSelectedPlayer, activeGW, horizonGWs, updatePlayerStat, handleTransferOut, fixtures, fixtureOverrides, sessionEdits, globalPlayers }) => { const { effectiveFixtures, globalXmins } = useContext(PlayerContext); // THE FIX: Grab the live updating player, not the frozen snapshot! const livePlayer = globalPlayers?.find(p => p.ID === selectedPlayer.ID) || selectedPlayer; const TEAM_SHORTS = { 1: "ARS", 2: "AVL", 3: "BUR", 4: "BOU", 5: "BRE", 6: "BHA", 7: "CHE", 8: "CRY", 9: "EVE", 10: "FUL", 11: "LEE", 12: "LIV", 13: "MCI", 14: "MUN", 15: "NEW", 16: "NFO", 17: "SUN", 18: "TOT", 19: "WHU", 20: "WOL" }; return (

{livePlayer.Name}

{livePlayer.Team} | £{getPlayerPrice(livePlayer).toFixed(1)}m
{[ { label: `GW${activeGW} xG`, val: livePlayer[`${activeGW}_xG`] ?? livePlayer.xG ?? "-" }, { label: `GW${activeGW} xA`, val: livePlayer[`${activeGW}_xA`] ?? livePlayer.xA ?? "-" }, { label: `GW${activeGW} CS%`, val: livePlayer[`${activeGW}_CS_Pct`] ?? livePlayer.CS_Pct ?? "-" }, ] .filter(stat => !(stat.label.includes('CS%') && livePlayer.Pos === 'F')) .map((stat) => (
{stat.label} {typeof stat.val === "number" ? stat.val.toFixed(2) : stat.val}
))}
{horizonGWs.map((gw) => { const matches = []; if (livePlayer.match_projections) { Object.entries(livePlayer.match_projections).forEach(([mId, mData]) => { // THE FIX 2: Look at the merged globals instead of the empty prop! const override = effectiveFixtures?.[mId]; // THE FIX 3: Force Number() to prevent API float bugs if (override && Number(override[gw]) > 0) matches.push({ ...mData, id: mId, prob: Number(override[gw]) }); else if (!override && String(mData.default_gw) === String(gw)) matches.push({ ...mData, id: mId, prob: 1.0 }); }); } const hasMultiple = matches.length > 1; const isBlank = matches.length === 0; return ( {hasMultiple && matches.map(m => { const oppName = TEAM_SHORTS[m.opponent_team_id] || m.opponent_team_id; const fixLabel = m.is_home ? `${oppName} (H)` : `${oppName} (A)`; const globalMatchMins = globalXmins?.[livePlayer.ID]?.[m.id]; const sessionVal = sessionEdits?.[livePlayer.ID]?.[`${m.id}_xMins`]; const currentMins = Math.round(sessionVal !== undefined ? Number(sessionVal) : (globalMatchMins !== undefined ? Number(globalMatchMins) : m.xMins)); const scaledEV = (currentMins > 0 && m.xMins > 0) ? (m.Pts / m.xMins) * currentMins : 0; return ( ); })} ); })}
GW Fixture xMins Proj. EV
GW{gw} {isBlank ? ( "BLANK" ) : hasMultiple ? ( "MULTIPLE" ) : (
{matches[0]?.is_home ? `${TEAM_SHORTS[matches[0].opponent_team_id]} (H)` : `${TEAM_SHORTS[matches[0]?.opponent_team_id]} (A)`} {matches[0]?.prob < 1.0 && ( {Math.round(matches[0].prob * 100)}% )}
)}
{ if (hasMultiple) { matches.forEach(m => updatePlayerStat(livePlayer.ID, m.id, "xMins", newVal)); } else { updatePlayerStat(livePlayer.ID, gw, "xMins", newVal); } }} />
{Number(livePlayer[`${gw}_Pts`] || 0).toFixed(2)}
{fixLabel} ({Math.round(m.prob * 100)}%) updatePlayerStat(livePlayer.ID, m.id, "xMins", newVal)} /> {(scaledEV * m.prob).toFixed(2)}
); }; export const PlayerSearchModal = ({ selectedPlayer, setSelectedPlayer, searchQuery, setSearchQuery, sortConfig, setSortConfig, globalPlayers, ownedPlayerIds, activeGW, itb, handleAddPlayer, }) => { const squadTeamCounts = {}; const currentSquad = globalPlayers.filter(p => ownedPlayerIds.has(p.ID) || ownedPlayerIds.has(String(p.ID)) || ownedPlayerIds.has(Number(p.ID))); currentSquad.forEach(p => { squadTeamCounts[p.Team] = (squadTeamCounts[p.Team] || 0) + 1; }); // Free up a slot if we are actively transferring out a player from a team! if (selectedPlayer && selectedPlayer.Team) { squadTeamCounts[selectedPlayer.Team] = Math.max(0, (squadTeamCounts[selectedPlayer.Team] || 0) - 1); } const cleanString = (str) => str ? str.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase() : ""; const cleanSearch = cleanString(searchQuery); return (
setSearchQuery(e.target.value)} className="flex-1 bg-transparent border-none outline-none text-slate-200 font-bold" autoFocus />
SORT BY:
{/* THE FIX: Changed max-h to 50vh on mobile so it doesn't span the whole screen */}
{globalPlayers // THE FIX: Apply cleanString to both the player name and the search query .filter((p) => !ownedPlayerIds.has(p.ID) && !ownedPlayerIds.has(String(p.ID)) && !ownedPlayerIds.has(Number(p.ID)) && String(p.ID) !== String(selectedPlayer.replacedPlayer?.ID) && p.Pos === selectedPlayer.Pos && cleanString(p.Name).includes(cleanSearch)) .sort((a, b) => { let valA = sortConfig.key === "ev" ? Number(a[`${activeGW}_Pts`] || 0) : getPlayerPrice(a); let valB = sortConfig.key === "ev" ? Number(b[`${activeGW}_Pts`] || 0) : getPlayerPrice(b); if (valA < valB) return sortConfig.direction === "desc" ? 1 : -1; if (valA > valB) return sortConfig.direction === "desc" ? -1 : 1; return 0; }) .slice(0, 50) .map((p) => { const sellingPrice = getPlayerPrice(selectedPlayer) || 0; const maxBudget = itb + sellingPrice; const cost = getPlayerPrice(p); const isAffordable = cost <= maxBudget; const isAtTeamLimit = (squadTeamCounts[p.Team] || 0) >= 3; const isSelectable = isAffordable && !isAtTeamLimit; return ( ); })}
); };