Spaces:
Running
Running
Commit ·
cbb573e
1
Parent(s): 4c4af79
updates
Browse files- frontend/src/components/PlayerModals.jsx +20 -16
- solver_engine.py +9 -3
frontend/src/components/PlayerModals.jsx
CHANGED
|
@@ -51,9 +51,9 @@ export const PlayerEditModal = ({
|
|
| 51 |
};
|
| 52 |
|
| 53 |
return (
|
| 54 |
-
<div className="fixed inset-0 z-[150] flex items-center justify-center bg-black/80 backdrop-blur-sm p-4">
|
| 55 |
-
<div className="bg-slate-950 border border-slate-800 w-full max-w-2xl rounded-2xl shadow-2xl
|
| 56 |
-
<div className="bg-slate-900 p-5 flex justify-between items-center border-b border-slate-800">
|
| 57 |
<div className="flex flex-col">
|
| 58 |
<h3 className="font-black text-2xl text-slate-100 uppercase tracking-tight">{livePlayer.Name}</h3>
|
| 59 |
<div className="flex gap-3 text-sm font-bold text-slate-500">
|
|
@@ -64,7 +64,7 @@ export const PlayerEditModal = ({
|
|
| 64 |
</div>
|
| 65 |
<button onClick={() => setSelectedPlayer(null)} className="text-slate-500 hover:text-white transition-colors bg-slate-900 p-2 rounded-full border border-slate-800">✕</button>
|
| 66 |
</div>
|
| 67 |
-
<div className="p-6 flex flex-col gap-6">
|
| 68 |
<div className="flex gap-4">
|
| 69 |
{[
|
| 70 |
{ label: `GW${activeGW} xG`, val: livePlayer[`${activeGW}_xG`] ?? livePlayer.xG ?? "-" },
|
|
@@ -206,6 +206,8 @@ export const PlayerSearchModal = ({
|
|
| 206 |
itb,
|
| 207 |
handleAddPlayer,
|
| 208 |
}) => {
|
|
|
|
|
|
|
| 209 |
return (
|
| 210 |
<div className="fixed inset-0 z-[150] flex items-center justify-center bg-black/80 backdrop-blur-sm p-4">
|
| 211 |
<div className="bg-slate-950 border border-slate-800 w-full max-w-lg rounded-2xl shadow-2xl overflow-hidden flex flex-col animate-in zoom-in-95 duration-200">
|
|
@@ -238,10 +240,11 @@ export const PlayerSearchModal = ({
|
|
| 238 |
</button>
|
| 239 |
</div>
|
| 240 |
|
| 241 |
-
|
|
|
|
| 242 |
{globalPlayers
|
| 243 |
-
// THE FIX:
|
| 244 |
-
.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 && p.Name
|
| 245 |
.sort((a, b) => {
|
| 246 |
let valA = sortConfig.key === "ev" ? Number(a[`${activeGW}_Pts`] || 0) : getPlayerPrice(a);
|
| 247 |
let valB = sortConfig.key === "ev" ? Number(b[`${activeGW}_Pts`] || 0) : getPlayerPrice(b);
|
|
@@ -251,7 +254,6 @@ export const PlayerSearchModal = ({
|
|
| 251 |
})
|
| 252 |
.slice(0, 50)
|
| 253 |
.map((p) => {
|
| 254 |
-
// THE FIX: Your true FPL purchasing power includes the money freed up by selling the outgoing player
|
| 255 |
const sellingPrice = getPlayerPrice(selectedPlayer) || 0;
|
| 256 |
const maxBudget = itb + sellingPrice;
|
| 257 |
const cost = getPlayerPrice(p);
|
|
@@ -262,19 +264,21 @@ export const PlayerSearchModal = ({
|
|
| 262 |
key={p.ID}
|
| 263 |
disabled={!isAffordable}
|
| 264 |
onClick={() => handleAddPlayer(p)}
|
| 265 |
-
|
|
|
|
| 266 |
>
|
| 267 |
<div className="flex flex-col items-start text-left">
|
| 268 |
-
|
| 269 |
-
<span className="
|
|
|
|
| 270 |
</div>
|
| 271 |
-
<div className="flex items-center gap-4 text-right">
|
| 272 |
<div className="flex flex-col items-end">
|
| 273 |
-
<span className="text-xs font-mono text-emerald-400 font-bold">EV: {Number(p[`${activeGW}_Pts`] || 0).toFixed(2)}</span>
|
| 274 |
-
<span className="text-[10px] font-mono text-slate-400">{p[`${activeGW}_xMins`] || 0} xMins</span>
|
| 275 |
</div>
|
| 276 |
-
<span className={`text-sm font-mono font-bold ${isAffordable ? "text-slate-300" : "text-red-400"}`}>£{cost.toFixed(1)}m</span>
|
| 277 |
-
<Plus className={`transition-colors ${isAffordable ? "text-slate-600 group-hover:text-luigi-400" : "text-slate-800"}`} size={
|
| 278 |
</div>
|
| 279 |
</button>
|
| 280 |
);
|
|
|
|
| 51 |
};
|
| 52 |
|
| 53 |
return (
|
| 54 |
+
<div className="fixed inset-0 z-[150] flex items-center justify-center bg-black/80 backdrop-blur-sm p-2 sm:p-4">
|
| 55 |
+
<div className="bg-slate-950 border border-slate-800 w-full max-w-2xl max-h-[90vh] sm:max-h-none overflow-y-auto sm:overflow-visible rounded-xl sm:rounded-2xl shadow-2xl animate-in zoom-in-95 duration-200 flex flex-col">
|
| 56 |
+
<div className="bg-slate-900 p-4 sm:p-5 flex justify-between items-center border-b border-slate-800 sticky top-0 z-20">
|
| 57 |
<div className="flex flex-col">
|
| 58 |
<h3 className="font-black text-2xl text-slate-100 uppercase tracking-tight">{livePlayer.Name}</h3>
|
| 59 |
<div className="flex gap-3 text-sm font-bold text-slate-500">
|
|
|
|
| 64 |
</div>
|
| 65 |
<button onClick={() => setSelectedPlayer(null)} className="text-slate-500 hover:text-white transition-colors bg-slate-900 p-2 rounded-full border border-slate-800">✕</button>
|
| 66 |
</div>
|
| 67 |
+
<div className="p-3 sm:p-6 flex flex-col gap-4 sm:gap-6">
|
| 68 |
<div className="flex gap-4">
|
| 69 |
{[
|
| 70 |
{ label: `GW${activeGW} xG`, val: livePlayer[`${activeGW}_xG`] ?? livePlayer.xG ?? "-" },
|
|
|
|
| 206 |
itb,
|
| 207 |
handleAddPlayer,
|
| 208 |
}) => {
|
| 209 |
+
const cleanString = (str) => str ? str.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase() : "";
|
| 210 |
+
const cleanSearch = cleanString(searchQuery);
|
| 211 |
return (
|
| 212 |
<div className="fixed inset-0 z-[150] flex items-center justify-center bg-black/80 backdrop-blur-sm p-4">
|
| 213 |
<div className="bg-slate-950 border border-slate-800 w-full max-w-lg rounded-2xl shadow-2xl overflow-hidden flex flex-col animate-in zoom-in-95 duration-200">
|
|
|
|
| 240 |
</button>
|
| 241 |
</div>
|
| 242 |
|
| 243 |
+
{/* THE FIX: Changed max-h to 50vh on mobile so it doesn't span the whole screen */}
|
| 244 |
+
<div className="max-h-[50vh] sm:max-h-[400px] overflow-y-auto p-1 sm:p-2 custom-scrollbar">
|
| 245 |
{globalPlayers
|
| 246 |
+
// THE FIX: Apply cleanString to both the player name and the search query
|
| 247 |
+
.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))
|
| 248 |
.sort((a, b) => {
|
| 249 |
let valA = sortConfig.key === "ev" ? Number(a[`${activeGW}_Pts`] || 0) : getPlayerPrice(a);
|
| 250 |
let valB = sortConfig.key === "ev" ? Number(b[`${activeGW}_Pts`] || 0) : getPlayerPrice(b);
|
|
|
|
| 254 |
})
|
| 255 |
.slice(0, 50)
|
| 256 |
.map((p) => {
|
|
|
|
| 257 |
const sellingPrice = getPlayerPrice(selectedPlayer) || 0;
|
| 258 |
const maxBudget = itb + sellingPrice;
|
| 259 |
const cost = getPlayerPrice(p);
|
|
|
|
| 264 |
key={p.ID}
|
| 265 |
disabled={!isAffordable}
|
| 266 |
onClick={() => handleAddPlayer(p)}
|
| 267 |
+
// THE FIX: Shrunk padding on mobile (p-2 instead of p-3)
|
| 268 |
+
className={`w-full flex items-center justify-between p-2 sm:p-3 border-b border-slate-800/30 transition-colors group ${isAffordable ? "hover:bg-slate-900 cursor-pointer" : "opacity-40 cursor-not-allowed"}`}
|
| 269 |
>
|
| 270 |
<div className="flex flex-col items-start text-left">
|
| 271 |
+
{/* THE FIX: Shrunk font sizes on mobile */}
|
| 272 |
+
<span className="font-bold text-slate-200 text-xs sm:text-sm">{p.Name}</span>
|
| 273 |
+
<span className="text-[9px] sm:text-[10px] text-slate-500 font-bold uppercase tracking-wider">{p.Team} • {p.Pos}</span>
|
| 274 |
</div>
|
| 275 |
+
<div className="flex items-center gap-2 sm:gap-4 text-right">
|
| 276 |
<div className="flex flex-col items-end">
|
| 277 |
+
<span className="text-[10px] sm:text-xs font-mono text-emerald-400 font-bold">EV: {Number(p[`${activeGW}_Pts`] || 0).toFixed(2)}</span>
|
| 278 |
+
<span className="text-[8px] sm:text-[10px] font-mono text-slate-400">{p[`${activeGW}_xMins`] || 0} xMins</span>
|
| 279 |
</div>
|
| 280 |
+
<span className={`text-xs sm:text-sm font-mono font-bold ${isAffordable ? "text-slate-300" : "text-red-400"}`}>£{cost.toFixed(1)}m</span>
|
| 281 |
+
<Plus className={`transition-colors ${isAffordable ? "text-slate-600 group-hover:text-luigi-400" : "text-slate-800"}`} size={16} />
|
| 282 |
</div>
|
| 283 |
</button>
|
| 284 |
);
|
solver_engine.py
CHANGED
|
@@ -6,8 +6,10 @@ def _norm_id_list(raw) -> list[int]:
|
|
| 6 |
return []
|
| 7 |
out = []
|
| 8 |
for x in raw:
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
| 11 |
else:
|
| 12 |
out.append(int(x))
|
| 13 |
return out
|
|
@@ -110,8 +112,12 @@ def prep_solver_data(payload_data: dict):
|
|
| 110 |
for x in raw:
|
| 111 |
if isinstance(x, list) and len(x) == 2:
|
| 112 |
out.append((int(x[0]), int(x[1])))
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
else:
|
| 114 |
-
out.append((int(x),
|
| 115 |
return out
|
| 116 |
|
| 117 |
settings["banned_next_gw"] = norm_temporal_list(
|
|
|
|
| 6 |
return []
|
| 7 |
out = []
|
| 8 |
for x in raw:
|
| 9 |
+
# THE FIX: Safely extract the ID from React's dictionary payload
|
| 10 |
+
if isinstance(x, dict):
|
| 11 |
+
pid = x.get("ID") or x.get("id", 0)
|
| 12 |
+
out.append(int(pid))
|
| 13 |
else:
|
| 14 |
out.append(int(x))
|
| 15 |
return out
|
|
|
|
| 112 |
for x in raw:
|
| 113 |
if isinstance(x, list) and len(x) == 2:
|
| 114 |
out.append((int(x[0]), int(x[1])))
|
| 115 |
+
elif isinstance(x, dict):
|
| 116 |
+
# THE FIX: Extract the ID and strictly bind it to the FIRST gameweek of the horizon!
|
| 117 |
+
pid = x.get("ID") or x.get("id", 0)
|
| 118 |
+
out.append((int(pid), horizon_gws[0]))
|
| 119 |
else:
|
| 120 |
+
out.append((int(x), horizon_gws[0]))
|
| 121 |
return out
|
| 122 |
|
| 123 |
settings["banned_next_gw"] = norm_temporal_list(
|