/*
* 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 (
);
}
function Section(_ref) {
var title = _ref.title, icon = _ref.icon, children = _ref.children, isOpen = _ref.isOpen, onToggle = _ref.onToggle, color = _ref.color || C.accent;
return (
{isOpen &&
{children}
}
);
}
function Label(_ref) {
return {_ref.children}
;
}
function Spacer() { return ; }
/* ═══════════════════════════════════════════════════════════
FORMATTED TEXT EXPORT
═══════════════════════════════════════════════════════════ */
function generateReport(data) {
var ln = function (label, val) { return val ? " " + label + ": " + val : null; };
var section = function (title) { return "\n── " + title + " " + "─".repeat(Math.max(0, 40 - title.length)); };
var tags = function (label, val) {
if (!val) return null;
if (Array.isArray(val)) return val.length ? " " + label + ": " + val.join(", ") : null;
return " " + label + ": " + val;
};
var m = data.match;
var s = data.scouting;
var lines = [];
lines.push("══════════════════════════════════════════");
lines.push(" MATCH ANALYSIS" + (m.opponent ? ": vs " + m.opponent : ""));
if (m.date) lines.push(" " + m.date);
if (m.ourTeam) lines.push(" " + m.ourTeam);
lines.push("══════════════════════════════════════════");
var formLine = [
m.formationIn ? "In Poss: " + m.formationIn : null,
m.formationOut ? "Out Poss: " + m.formationOut : null,
].filter(Boolean).join(" | ");
if (formLine) { lines.push("\nFORMATIONS"); lines.push(" " + formLine); }
if (m.dangerPlayers) { lines.push("\nDANGER PLAYERS"); lines.push(" " + m.dangerPlayers); }
var bu = s.buildUp;
if (bu.gk || bu.focus || bu.fbRole || bu.midfield || bu.patterns || bu.weakness) {
lines.push(section("IN POSSESSION: BUILD-UP"));
var quickTags = [bu.gk ? "GK: " + bu.gk : null, bu.focus ? "Focus: " + bu.focus : null,
bu.fbRole ? "FB: " + bu.fbRole : null, bu.midfield ? "MF: " + bu.midfield : null].filter(Boolean);
if (quickTags.length) lines.push(" " + quickTags.join(" | "));
if (bu.patterns) lines.push(" Patterns: " + bu.patterns);
if (bu.weakness) lines.push(" Under Pressure: " + bu.weakness);
}
var pr = s.progression;
if (pr.entry || pr.fwdRuns || pr.overload || pr.threatZones || (pr.attackerBehavior && pr.attackerBehavior.length) || pr.notes) {
lines.push(section("IN POSSESSION: PROGRESSION"));
var qt = [pr.entry ? "Entry: " + pr.entry : null, pr.fwdRuns ? "Runs Behind: " + pr.fwdRuns : null].filter(Boolean);
if (qt.length) lines.push(" " + qt.join(" | "));
if (pr.attackerBehavior && pr.attackerBehavior.length) lines.push(" Behavior: " + pr.attackerBehavior.join(", "));
if (pr.overload) lines.push(" Overload: " + pr.overload);
if (pr.threatZones) lines.push(" Threat Zones: " + pr.threatZones);
if (pr.notes) lines.push(" Notes: " + pr.notes);
}
var ps = s.pressing;
if (ps.blockHeight || ps.whoLeads || ps.direction || ps.traps) {
lines.push(section("DEFENDING: PRESSING SHAPE"));
var qt2 = [ps.blockHeight ? "Block: " + ps.blockHeight : null,
ps.whoLeads ? "Led by: " + ps.whoLeads : null,
ps.direction ? "Show to: " + ps.direction : null].filter(Boolean);
if (qt2.length) lines.push(" " + qt2.join(" | "));
if (ps.traps) lines.push(" Triggers: " + ps.traps);
}
var bl = s.block;
if (bl.compactness || bl.space || (bl.weakAreas && bl.weakAreas.length) || bl.exploit) {
lines.push(section("DEFENDING: BLOCK"));
var qt3 = [bl.compactness ? "Shape: " + bl.compactness : null,
bl.space ? "Lines: " + bl.space : null].filter(Boolean);
if (qt3.length) lines.push(" " + qt3.join(" | "));
if (bl.weakAreas && bl.weakAreas.length) lines.push(" Weak Areas: " + bl.weakAreas.join(", "));
if (bl.exploit) lines.push(" Exploit: " + bl.exploit);
}
var pt = s.posTransition;
if (pt.style || pt.direction || pt.target || pt.weakness) {
lines.push(section("POSITIVE TRANSITION (THEIR COUNTER)"));
var qt4 = [pt.style, pt.direction ? "Dir: " + pt.direction : null].filter(Boolean);
if (qt4.length) lines.push(" " + qt4.join(" | "));
if (pt.target) lines.push(" Target: " + pt.target);
if (pt.weakness) lines.push(" Exploit: " + pt.weakness);
}
var nt = s.negTransition;
if (nt.speed || nt.response || nt.quality || nt.exposed) {
lines.push(section("NEGATIVE TRANSITION (LOSING BALL)"));
var qt5 = [nt.speed ? "Speed: " + nt.speed : null, nt.response, nt.quality].filter(Boolean);
if (qt5.length) lines.push(" " + qt5.join(" | "));
if (nt.exposed) lines.push(" Exposed: " + nt.exposed);
}
var activeSP = s.setPieces.filter(function (sp) { return sp.type || sp.notes; });
if (activeSP.length) {
lines.push(section("SET PIECES"));
activeSP.forEach(function (sp, i) {
var label = [sp.type, sp.side].filter(Boolean).join(" — ");
lines.push(" " + (i + 1) + ". " + (label || "Set Piece"));
if (sp.notes) lines.push(" " + sp.notes);
});
}
if (data.observations.length) {
lines.push(section("MATCH OBSERVATIONS"));
data.observations.forEach(function (o) {
lines.push(" " + (o.minute ? o.minute + "'" : " ") + " " + o.text);
});
}
var sm = data.summary;
if (sm.problems || sm.opportunities || sm.notes) {
lines.push(section("SUMMARY"));
if (sm.problems) { lines.push(" PROBLEMS:"); lines.push(" " + sm.problems); }
if (sm.opportunities) { lines.push(" OPPORTUNITIES:"); lines.push(" " + sm.opportunities); }
if (sm.notes) { lines.push(" NOTES:"); lines.push(" " + sm.notes); }
}
lines.push("\n══════════════════════════════════════════");
return lines.filter(function (l) { return l !== null; }).join("\n");
}
/* ═══════════════════════════════════════════════════════════
EXPORT MENU
═══════════════════════════════════════════════════════════ */
function ExportMenu(_ref) {
var data = _ref.data;
var _s = useState(false), open = _s[0], setOpen = _s[1];
var _c = useState(false), copied = _c[0], setCopied = _c[1];
var ref = useRef(null);
useEffect(function () {
if (!open) return;
var handler = function (e) { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
document.addEventListener("pointerdown", handler);
return function () { document.removeEventListener("pointerdown", handler); };
}, [open]);
var copyToClipboard = function () {
var text = generateReport(data);
try {
navigator.clipboard.writeText(text).then(function () {
setCopied(true);
setTimeout(function () { setCopied(false); setOpen(false); }, 1200);
});
} catch (e) {
var ta = document.createElement("textarea");
ta.value = text; document.body.appendChild(ta);
ta.select(); document.execCommand("copy");
document.body.removeChild(ta);
setCopied(true);
setTimeout(function () { setCopied(false); setOpen(false); }, 1200);
}
};
var downloadText = function () {
var text = generateReport(data);
var blob = new Blob([text], { type: "text/plain" });
var url = URL.createObjectURL(blob);
var a = document.createElement("a");
a.href = url;
a.download = "analysis-" + (data.match.opponent || "match") + "-" + (data.match.date || new Date().toISOString().slice(0, 10)) + ".txt";
a.click(); URL.revokeObjectURL(url);
setOpen(false);
};
return (
{open && (
)}
);
}
/* ═══════════════════════════════════════════════════════════
MAIN APP
═══════════════════════════════════════════════════════════ */
function App() {
var _d = useState(INITIAL_DATA), data = _d[0], setData = _d[1];
var _t = useState("board"), tab = _t[0], setTab = _t[1];
var _l = useState(false), loaded = _l[0], setLoaded = _l[1];
var clock = useMatchClock();
useEffect(function () {
var saved = loadSaved();
if (saved) setData(function () { return { ...INITIAL_DATA, ...saved }; });
setLoaded(true);
}, []);
useAutoSave(data, loaded);
var updateMatch = function (key, val) { setData(function (d) { return { ...d, match: { ...d.match, [key]: val } }; }); };
var updateBoard = useCallback(function (id, board) {
setData(function (d) { return { ...d, boards: d.boards.map(function (b) { return b.id === id ? board : b; }) }; });
}, []);
var setActiveBoard = function (id) { setData(function (d) { return { ...d, activeBoard: id }; }); };
var addBoard = function () {
var id = uid();
setData(function (d) { return { ...d, boards: d.boards.concat([{ id: id, name: "Board " + (d.boards.length + 1), markers: [], arrows: [] }]), activeBoard: id }; });
};
var renameBoard = function (id, name) { setData(function (d) { return { ...d, boards: d.boards.map(function (b) { return b.id === id ? { ...b, name: name } : b; }) }; }); };
var deleteBoard = function (id) {
setData(function (d) {
var boards = d.boards.filter(function (b) { return b.id !== id; });
return { ...d, boards: boards, activeBoard: boards[0] ? boards[0].id : "" };
});
};
var updateSection = function (section, val) { setData(function (d) { return { ...d, scouting: { ...d.scouting, [section]: val } }; }); };
var updateSetPieces = function (val) { setData(function (d) { return { ...d, scouting: { ...d.scouting, setPieces: val } }; }); };
var updateMomentum = function (val) { setData(function (d) { return { ...d, momentum: val }; }); };
var addObservation = function (obs) { setData(function (d) { return { ...d, observations: d.observations.concat([obs]) }; }); };
var deleteObservation = function (id) { setData(function (d) { return { ...d, observations: d.observations.filter(function (o) { return o.id !== id; }) }; }); };
var updateSummary = function (key, val) { setData(function (d) { return { ...d, summary: { ...d.summary, [key]: val } }; }); };
var resetAll = function () {
if (confirm("Clear all data and start fresh?")) { setData(INITIAL_DATA); clearSaved(); }
};
var tabs = [
{ id: "board", label: "Board", icon: "⚽" },
{ id: "scout", label: "Scout", icon: "📋" },
{ id: "timeline", label: "Timeline", icon: "📊" },
];
return (
{/* Header */}
{data.match.opponent ? "vs " + data.match.opponent : "Match Analysis"}
{clock.minute}'
{clock.half === 1 ? "1H" : "2H"}
{clock.half === 1 && clock.minute >= 45 && (
)}
{/* Content */}
{/* Tab Bar */}
{tabs.map(function (t, i) {
return (
);
})}
);
}