/* * app.jsx — Core module: shared constants, utilities, hooks, * small reusable components, and the main App shell. * * ── LOAD ORDER ── * This file is loaded FIRST. It defines everything the other * three files (pitch.jsx, scouting-form.jsx, timeline.jsx) * depend on: theme colors, uid(), Field, ToggleGroup, etc. * * The App component defined here references TacticalBoard, * ScoutingForm, and MatchTimeline — which don't exist yet * when this file executes. That's fine: JavaScript doesn't * evaluate a function body until the function is called. * By the time the mount script calls App(), all four files * have executed and those globals exist. * * Data persistence uses localStorage (the browser's built-in * key-value store). */ /* eslint-disable no-unused-vars */ var { useState, useRef, useCallback, useEffect, useReducer } = React; /* ═══════════════════════════════════════════════════════════ THEME ═══════════════════════════════════════════════════════════ */ var C = { bg: "#0f1410", bgCard: "#161e14", bgInput: "#1a2316", bgHover: "#1e2a1a", border: "#2a3824", borderActive: "#3a4a30", accent: "#b8e636", accentDim: "rgba(184,230,54,0.15)", blue: "#4a9eff", blueDim: "rgba(74,158,255,0.15)", red: "#ff6b5a", redDim: "rgba(255,107,90,0.15)", yellow: "#ffd44a", text: "#d0dbc4", textMuted: "#7a8c6c", textDim: "#4a5c3e", pitch: "#1e2e18", pitchLine: "#3a5a2e", font: "'Saira', sans-serif", fontCond: "'Saira Condensed', sans-serif", }; var FONT_LINK = "https://fonts.googleapis.com/css2?family=Saira:wght@300;400;500;600;700;800&family=Saira+Condensed:wght@400;500;600;700;800&display=swap"; var INITIAL_DATA = { match: { opponent: "", ourTeam: "", date: "", formationIn: "", formationOut: "", dangerPlayers: "" }, boards: [{ id: "b1", name: "Formation", markers: [], arrows: [] }], activeBoard: "b1", scouting: { buildUp: {}, progression: {}, pressing: {}, block: {}, posTransition: {}, negTransition: {}, setPieces: [{ type: null, side: null, notes: "", minute: "" }], }, momentum: {}, observations: [], summary: { problems: "", opportunities: "", notes: "" }, }; var _id = Date.now(); var uid = function () { return "" + (++_id); }; var STORAGE_KEY = "match-analysis-v3"; /* ═══════════════════════════════════════════════════════════ AUTO-SAVE using localStorage ═══════════════════════════════════════════════════════════ */ function useAutoSave(data, loaded) { var dataRef = useRef(data); dataRef.current = data; var tick = useRef(0); useEffect(function () { if (!loaded) return; tick.current++; var snap = tick.current; var t = setTimeout(function () { if (snap !== tick.current) return; try { localStorage.setItem(STORAGE_KEY, JSON.stringify(dataRef.current)); } catch (e) { console.warn("Auto-save failed", e); } }, 600); return function () { clearTimeout(t); }; }, [data, loaded]); } function loadSaved() { try { var raw = localStorage.getItem(STORAGE_KEY); return raw ? JSON.parse(raw) : null; } catch (e) { return null; } } function clearSaved() { try { localStorage.removeItem(STORAGE_KEY); } catch (e) { /* ignore */ } } /* ═══════════════════════════════════════════════════════════ MATCH CLOCK ═══════════════════════════════════════════════════════════ */ function useMatchClock() { var _s = useState(0), seconds = _s[0], setSeconds = _s[1]; var _r = useState(false), running = _r[0], setRunning = _r[1]; var _h = useState(1), half = _h[0], setHalf = _h[1]; var interval = useRef(null); useEffect(function () { if (running) { interval.current = setInterval(function () { setSeconds(function (s) { return s + 1; }); }, 1000); } else { clearInterval(interval.current); } return function () { clearInterval(interval.current); }; }, [running]); var minute = Math.floor(seconds / 60) + (half === 2 ? 45 : 0); var toggle = function () { setRunning(function (r) { return !r; }); }; var reset = function () { setSeconds(0); setRunning(false); }; var startSecondHalf = function () { setHalf(2); setSeconds(0); setRunning(true); }; return { minute: minute, seconds: seconds % 60, running: running, half: half, toggle: toggle, reset: reset, startSecondHalf: startSecondHalf }; } /* ═══════════════════════════════════════════════════════════ SMALL REUSABLE COMPONENTS ═══════════════════════════════════════════════════════════ */ function ToggleGroup(_ref) { var options = _ref.options, value = _ref.value, onChange = _ref.onChange, multi = _ref.multi || false; var handle = function (opt) { if (multi) { var a = value || []; onChange(a.includes(opt) ? a.filter(function (v) { return v !== opt; }) : a.concat([opt])); } else { onChange(value === opt ? null : opt); } }; return (
{options.map(function (opt) { var on = multi ? (value || []).includes(opt) : value === opt; return ( ); })}
); } function Field(_ref) { var label = _ref.label, value = _ref.value, onChange = _ref.onChange, placeholder = _ref.placeholder, multiline = _ref.multiline, minute = _ref.minute, onMinuteChange = _ref.onMinuteChange; var shared = { value: value || "", onChange: function (e) { onChange(e.target.value); }, placeholder: placeholder || "", style: { width: "100%", padding: "8px 11px", fontSize: 14, background: C.bgInput, border: "1px solid " + C.border, borderRadius: 4, color: C.text, fontFamily: C.font, resize: multiline ? "vertical" : "none", minHeight: multiline ? 60 : "auto", outline: "none", boxSizing: "border-box", }, }; return (
{label && (
{onMinuteChange !== undefined && ( )}
)} {multiline ?