" : "") + "" +
sections.filter(Boolean).join("") +
"";
}
function printPDF(data) {
var html = generateReportHTML(data);
var iframe = document.createElement("iframe");
iframe.setAttribute("aria-hidden", "true");
iframe.style.position = "fixed";
iframe.style.right = "0";
iframe.style.bottom = "0";
iframe.style.width = "0";
iframe.style.height = "0";
iframe.style.border = "0";
iframe.style.visibility = "hidden";
document.body.appendChild(iframe);
var cleanup = function () {
setTimeout(function () {
if (iframe.parentNode) iframe.parentNode.removeChild(iframe);
}, 1000);
};
var doPrint = function () {
try {
var win = iframe.contentWindow;
win.focus();
win.print();
} catch (e) { /* ignore */ }
var onAfter = function () { cleanup(); win.removeEventListener("afterprint", onAfter); };
try { iframe.contentWindow.addEventListener("afterprint", onAfter); } catch (e) { cleanup(); }
};
iframe.onload = function () { setTimeout(doPrint, 100); };
var doc = iframe.contentWindow.document;
doc.open();
doc.write(html);
doc.close();
}
/* ═══════════════════════════════════════════════════════════
EXPORT MENU
═══════════════════════════════════════════════════════════ */
function ExportMenu(_ref) {
var data = _ref.data, onAction = _ref.onAction;
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);
var onSuccess = function () {
setCopied(true);
setTimeout(function () { setCopied(false); setOpen(false); if (onAction) onAction(); }, 1200);
};
var fallback = function () {
var ta = document.createElement("textarea");
ta.value = text; document.body.appendChild(ta);
ta.select(); document.execCommand("copy");
document.body.removeChild(ta);
onSuccess();
};
try {
navigator.clipboard.writeText(text).then(onSuccess).catch(fallback);
} catch (e) {
fallback();
}
};
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);
if (onAction) onAction();
};
return (
{open && (
)}
);
}
/* ═══════════════════════════════════════════════════════════
SAVE BUTTON & LOAD MENU
═══════════════════════════════════════════════════════════ */
function SaveButton(_ref) {
var data = _ref.data, onAction = _ref.onAction, confirm = _ref.confirm;
var _s = useState(false), saved = _s[0], setSaved = _s[1];
var commit = function () {
saveToLibrary(data);
setSaved(true);
setTimeout(function () { setSaved(false); }, 1400);
if (onAction) onAction();
};
var onClick = function () {
var dupLabel = findDuplicateLabel(data);
if (!dupLabel) { commit(); return; }
confirm({
title: "Overwrite saved analysis",
message: "An analysis for " + dupLabel + " already exists. Overwrite it?",
confirmLabel: "Overwrite",
}).then(function (ok) { if (ok) commit(); });
};
return (
);
}
function LoadMenu(_ref) {
var onLoad = _ref.onLoad, currentData = _ref.currentData, onAction = _ref.onAction, confirm = _ref.confirm;
var _o = useState(false), open = _o[0], setOpen = _o[1];
var _l = useState([]), lib = _l[0], setLib = _l[1];
var ref = useRef(null);
useEffect(function () {
if (!open) return;
setLib(loadLibrary());
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 hasContent = function (d) {
return !!(d && (d.match.opponent || d.match.ourTeam || d.match.date ||
(d.observations && d.observations.length) ||
(d.summary && (d.summary.problems || d.summary.opportunities || d.summary.notes))));
};
var doLoad = function (entry) {
onLoad(entry.data);
setOpen(false);
if (onAction) onAction();
};
var load = function (entry) {
if (!hasContent(currentData)) { doLoad(entry); return; }
confirm({
title: "Replace current analysis",
message: "Replace the current analysis with " + (entry.ourTeam || "Our Team") + " vs " + (entry.opponent || "Opponent") + "?",
confirmLabel: "Replace",
}).then(function (ok) { if (ok) doLoad(entry); });
};
var remove = function (e, entry) {
e.stopPropagation();
var label = (entry.ourTeam || "Our Team") + " vs " + (entry.opponent || "Opponent");
confirm({
title: "Delete saved analysis",
message: "Delete saved analysis: " + label + "?",
confirmLabel: "Delete",
danger: true,
}).then(function (ok) {
if (!ok) return;
deleteFromLibrary(entry.id);
setLib(loadLibrary());
});
};
var fmtSavedAt = function (iso) {
try { return new Date(iso).toLocaleDateString(); } catch (e) { return ""; }
};
return (
{open && (
{lib.length === 0 && (
No saved analyses yet.
)}
{lib.map(function (entry) {
var title = (entry.ourTeam || "Our Team") + " vs " + (entry.opponent || "Opponent");
return (
{title}
{entry.date && (
Match date: {entry.date}
)}
Save date: {fmtSavedAt(entry.savedAt)}
);
})}
)}
);
}
/* ═══════════════════════════════════════════════════════════
ACCORDION PILL
Mobile-only: collapses a header button group into a single
pill. Tapping it floats the full group upward as an overlay.
═══════════════════════════════════════════════════════════ */
function AccordionPill(_ref) {
var label = _ref.label, open = _ref.open, onToggle = _ref.onToggle,
align = _ref.align, children = _ref.children;
var ref = useRef(null);
var _p = useState(null), pos = _p[0], setPos = _p[1];
useEffect(function () {
if (!open) { setPos(null); return; }
// The overlay floats upward from the pill. It uses fixed positioning
// so it can extend past the app's clipped (overflow:hidden) bounds.
var measure = function () {
if (!ref.current) return;
var r = ref.current.getBoundingClientRect();
var next = { bottom: Math.round(window.innerHeight - r.bottom) };
if (align === "right") next.right = Math.round(window.innerWidth - r.right);
else next.left = Math.round(r.left);
setPos(next);
};
measure();
var handler = function (e) {
if (ref.current && !ref.current.contains(e.target)) onToggle(false);
};
document.addEventListener("pointerdown", handler);
window.addEventListener("resize", measure);
return function () {
document.removeEventListener("pointerdown", handler);
window.removeEventListener("resize", measure);
};
}, [open, align]);
return (
{open && pos && (
{children}
)}
);
}
/* ═══════════════════════════════════════════════════════════
ERROR BOUNDARY
A class component because that's the only way React lets us
catch render errors. Wraps each tab panel so one section
crashing doesn't blank the whole app — the scout's notes live
in localStorage and survive a render error.
═══════════════════════════════════════════════════════════ */
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { error: null };
this.handleRetry = this.handleRetry.bind(this);
}
static getDerivedStateFromError(error) {
return { error: error };
}
componentDidCatch(error, info) {
console.error("ErrorBoundary caught error in " + (this.props.section || "section") + ":", error, info);
}
handleRetry() {
this.setState({ error: null });
}
render() {
if (!this.state.error) return this.props.children;
var section = this.props.section || "this section";
return (
Something went wrong
The {section} view hit an error and stopped rendering.
Your saved work is safe — it's stored locally and untouched by this crash.
);
}
}
/* ═══════════════════════════════════════════════════════════
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();
var isMobile = useIsMobile();
var _om = useState(null), openMenu = _om[0], setOpenMenu = _om[1];
var _cf = useConfirm(), confirm = _cf.confirm, confirmModal = _cf.modal;
useEffect(function () {
var saved = loadSaved();
if (saved) setData(function () { return mergeWithDefaults(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 () {
confirm({
title: "Reset all data",
message: "Clear all data and start fresh?",
confirmLabel: "Reset",
danger: true,
}).then(function (ok) {
if (!ok) return;
setData(INITIAL_DATA);
clearSaved();
});
};
var tabs = [
{ id: "board", label: "Board" },
{ id: "timeline", label: "Timeline" },
{ id: "scout", label: "Scout" }
];
var activeViewLabel = (tabs.find(function (t) { return t.id === tab; }) || tabs[0]).label;
var closeMenus = function () { setOpenMenu(null); };
var tabGroup = (
{tabs.map(function (t) {
var active = tab === t.id;
return (
);
})}
{/* Left zone — view tabs (stacks vertically on mobile).
Bottom-aligned on mobile so the tabs sit close to the
header's lower border, even though the action group is
one Reset row taller. */}
{isMobile ? (
{tabGroup}
) : tabGroup}
{/* Center zone — match clock. Bottom-aligned on mobile so
it sits near the lower border like the button groups. */}
{clock.minute}'{clock.half === 1 ? "1H" : "2H"}
{/* Right zone — actions. Reset is the final segment of the
Save/Load/Export group; the group stacks on mobile. */}