Spaces:
Running
Running
File size: 9,849 Bytes
7c367a8 6ff55c7 f7cecf3 2dee593 f5e80db 13fe1f0 f5e80db 3ed93f4 13fe1f0 f5e80db ed83748 2dee593 3ed93f4 13fe1f0 1d9a597 3c8b4ae f5e80db 89e5e47 6ff55c7 f7cecf3 6ff55c7 14b6654 d9f8dc1 6ff55c7 a7d5331 80ac556 a7d5331 6ff55c7 a7edf74 6ff55c7 80ac556 a7d5331 80ac556 a7d5331 6ff55c7 2dee593 6ff55c7 1d9a597 2dee593 6ff55c7 1d9a597 6ce87f4 1d9a597 2dee593 6ff55c7 f7cecf3 6ff55c7 14b6654 6ff55c7 14b6654 6e5220d 14b6654 1d9a597 6ff55c7 1d9a597 f7cecf3 6ff55c7 1d9a597 6ff55c7 1d9a597 2b58a06 2dee593 1d9a597 2b58a06 1d9a597 6ff55c7 2dee593 1d9a597 2dee593 6ff55c7 d9f8dc1 6ff55c7 1d9a597 f7cecf3 6ff55c7 2b58a06 6ff55c7 2b58a06 6ff55c7 1d9a597 6ff55c7 d9f8dc1 6ff55c7 f7cecf3 2b58a06 f7cecf3 f5e80db | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 | // src/components/PitchView.jsx
import React, { useRef, useState, useEffect, useCallback } from "react";
import { DraggablePlayer } from "./DraggablePlayer";
/*
DESIGN DIMENSIONS β single source of truth for the pitch.
All cards are laid out at these fixed pixel dimensions and the whole pitch
is uniformly transform-scaled to fit the actual container. This keeps
proportions identical across every screen size (no element gets relatively
bigger or smaller than another when the viewport changes).
Card: 88Γ106px (larger for better legibility)
Label strip: ~52px
Card slot total β 158px per row
DESIGN_WIDTH = 670: fits widest row (5 mids/defs) = 5Γ88 + 4Γ24 = 536 β€ 670 β
4 starter rows + gaps β 780px total height
Bench β 230px
MAX_SCALE > 1 lets the pitch scale modestly on wide screens without badges
and labels becoming disproportionately large.
*/
const DESIGN_WIDTH = 670;
const STARTERS_H = 780;
const BENCH_H = 230;
// Gap between player card wrappers in px (in design space)
const CARD_GAP = 24;
// Allow the pitch to scale up to 1.1Γ on wide screens so cards don't look
// undersized on desktop. Below DESIGN_WIDTH it scales down naturally to fit.
const MAX_SCALE = 1.0;
export const PitchView = ({
teamData,
activeDragPlayer,
isValidSwap,
captainId,
viceId,
handleCapChange,
playerCardGWs,
fixtures,
activeGW,
setSelectedPlayer,
handleUndoTransfer,
highlightTransferIds,
solverTransferPairs,
resetHighlightedTransfer,
chipsByGw,
}) => {
const containerRef = useRef(null);
const [scale, setScale] = useState(1);
const [isFullscreen, setIsFullscreen] = useState(false);
const toggleFullscreen = useCallback(() => {
if (!containerRef.current) return;
if (!document.fullscreenElement) {
containerRef.current.requestFullscreen?.();
} else {
document.exitFullscreen?.();
}
}, []);
const handlePlayerClick = useCallback((player) => {
setSelectedPlayer(player);
}, [setSelectedPlayer]);
const updateScale = useCallback(() => {
if (!containerRef.current) return;
const next = Math.min(MAX_SCALE, containerRef.current.offsetWidth / DESIGN_WIDTH);
setScale(prev => Math.abs(prev - next) > 0.005 ? next : prev);
}, []);
useEffect(() => {
updateScale();
const ro = new ResizeObserver(updateScale);
if (containerRef.current) ro.observe(containerRef.current);
return () => ro.disconnect();
}, [updateScale]);
useEffect(() => {
const handler = () => {
const fs = !!document.fullscreenElement;
setIsFullscreen(fs);
if (!containerRef.current) return;
const w = fs ? window.innerWidth : containerRef.current.offsetWidth;
const next = Math.min(MAX_SCALE, w / DESIGN_WIDTH);
setScale(prev => Math.abs(prev - next) > 0.005 ? next : prev);
};
document.addEventListener("fullscreenchange", handler);
return () => document.removeEventListener("fullscreenchange", handler);
}, []);
const isBBChip = chipsByGw[activeGW] === "bb";
// The outer "height-reserving" divs must use the SCALED height so the
// document flow collapses correctly β otherwise content below (bench, etc.)
// overlaps the starters area.
const scaledStartersH = STARTERS_H * scale;
const scaledBenchH = BENCH_H * scale;
// Inner transform div: fixed at design size, scaled from top-center
const innerStyle = (designH, extraStyle = {}) => ({
width: DESIGN_WIDTH,
height: designH,
transform: `scale(${scale})`,
transformOrigin: "top center",
position: "absolute",
left: "50%",
marginLeft: -(DESIGN_WIDTH / 2),
top: 0,
willChange: "transform",
...extraStyle,
});
return (
<div
ref={containerRef}
className="fullscreen-pitch w-full bg-[#0a3a2a] rounded-2xl border-4 border-[#072a1e] relative flex flex-col shadow-[0_0_50px_rgba(0,0,0,0.5)] isolate"
>
{/* Fullscreen toggle β mobile only */}
<button
onClick={toggleFullscreen}
className="small-touch-target absolute top-2 right-2 z-50 md:hidden p-1.5 bg-black/40 hover:bg-black/60 rounded-lg border border-white/20 text-white/70 hover:text-white transition-colors"
>
{isFullscreen ? (
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<path d="M8 3v3a2 2 0 0 1-2 2H3"/><path d="M21 8h-3a2 2 0 0 1-2-2V3"/>
<path d="M3 16h3a2 2 0 0 1 2 2v3"/><path d="M16 21v-3a2 2 0 0 1 2-2h3"/>
</svg>
) : (
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<path d="M3 7V5a2 2 0 0 1 2-2h2"/><path d="M17 3h2a2 2 0 0 1 2 2v2"/>
<path d="M21 17v2a2 2 0 0 1-2 2h-2"/><path d="M7 21H5a2 2 0 0 1-2-2v-2"/>
</svg>
)}
</button>
{/* ββ PITCH LINES β fill the full container, never scaled ββ */}
<div className="absolute inset-0 pointer-events-none opacity-30">
<div className="absolute top-0 w-full h-1/2 border-b-2 border-white/40" />
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-48 h-48 border-2 border-white/40 rounded-full" />
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-2 h-2 bg-white/40 rounded-full" />
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-[50%] h-[15%] border-2 border-white/40 border-t-0" />
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-[20%] h-[5%] border-2 border-white/40 border-t-0" />
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-[50%] h-[15%] border-2 border-white/40 border-b-0" />
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-[20%] h-[5%] border-2 border-white/40 border-b-0" />
</div>
{/* ββ STARTERS ββ */}
{/* Outer reserves scaled height in document flow */}
<div style={{ height: scaledStartersH }} className="relative w-full">
{/* Inner is at design dimensions, scaled from top-center */}
<div
style={innerStyle(STARTERS_H)}
className="flex flex-col justify-evenly z-10"
// py-6 = 24px top + 24px bottom padding (48px total)
// justify-evenly distributes remaining 692px across 4 rows of 152px + 5 gaps
>
<div style={{ height: 24 }} /> {/* top padding */}
{["G", "D", "M", "F"].map((pos) => {
const rowPlayers = teamData.slice(0, 11).filter((p) => p.Pos === pos);
if (rowPlayers.length === 0) return null;
return (
<div
key={pos}
className="flex justify-center items-start w-full flex-1"
style={{ gap: CARD_GAP }}
>
{rowPlayers.map((p) => (
<DraggablePlayer
key={p.ID}
player={p}
isBench={false}
isActiveDrag={activeDragPlayer !== null}
isValidTarget={isValidSwap(activeDragPlayer, p)}
captainId={captainId}
viceId={viceId}
handleCapChange={handleCapChange}
playerCardGWs={playerCardGWs}
fixtures={fixtures}
activeGW={activeGW}
onPlayerClick={handlePlayerClick}
onUndo={handleUndoTransfer}
isHighlighted={Array.from(highlightTransferIds[activeGW] || []).includes(p.ID)}
onSolverUndo={(solverTransferPairs[activeGW] || {})[p.ID] ? () => resetHighlightedTransfer(p) : undefined}
activeChipType={chipsByGw[activeGW]}
/>
))}
</div>
);
})}
<div style={{ height: 24 }} /> {/* bottom padding */}
</div>
</div>
{/* ββ BENCH ββ */}
<div
className={`w-full border-t-2 z-10 transition-colors duration-300 relative ${
isBBChip
? "bg-emerald-950/60 border-emerald-500/50 shadow-[0_0_24px_rgba(16,185,129,0.2)]"
: "bg-slate-950/90 border-slate-800"
}`}
style={{ height: scaledBenchH }}
>
<div
style={innerStyle(BENCH_H, {
display: "flex",
justifyContent: "center",
alignItems: "flex-start",
gap: CARD_GAP,
paddingTop: 16,
paddingBottom: 32,
paddingLeft: 24,
paddingRight: 24,
boxSizing: "border-box",
})}
>
{teamData.slice(11, 15).map((p, benchIndex) => (
<DraggablePlayer
key={p.ID}
player={p}
isBench={true}
benchIndex={benchIndex}
isActiveDrag={activeDragPlayer !== null}
isValidTarget={isValidSwap(activeDragPlayer, p)}
captainId={captainId}
viceId={viceId}
handleCapChange={handleCapChange}
playerCardGWs={playerCardGWs}
fixtures={fixtures}
activeGW={activeGW}
onPlayerClick={handlePlayerClick}
onUndo={handleUndoTransfer}
isHighlighted={Array.from(highlightTransferIds[activeGW] || []).includes(p.ID)}
onSolverUndo={(solverTransferPairs[activeGW] || {})[p.ID] ? () => resetHighlightedTransfer(p) : undefined}
activeChipType={chipsByGw[activeGW]}
/>
))}
</div>
</div>
</div>
);
};
|