/*
* pitch.jsx — Tactical board: pitch SVG, markers, arrows,
* tool selection, undo/redo, and board tabs.
*
* ── DEPENDS ON (from app.jsx, loaded before this file) ──
* Globals: C, uid
* React hooks: useState, useRef, useCallback, useEffect, useReducer
*
* ── EXPORTS (to global scope) ──
* TacticalBoard — used by App in app.jsx
*/
/* ═══════════════════════════════════════════════════════════
BOARD HISTORY REDUCER
═══════════════════════════════════════════════════════════ */
function boardReducer(state, action) {
switch (action.type) {
case "INIT":
return { current: action.board, history: [], future: [] };
case "DO":
return {
current: action.board,
history: state.history.slice(-30).concat([state.current]),
future: [],
};
case "UNDO": {
if (!state.history.length) return state;
return {
current: state.history[state.history.length - 1],
history: state.history.slice(0, -1),
future: [state.current].concat(state.future),
};
}
case "REDO": {
if (!state.future.length) return state;
return {
current: state.future[0],
history: state.history.concat([state.current]),
future: state.future.slice(1),
};
}
default:
return state;
}
}
/* ═══════════════════════════════════════════════════════════
ACTION MENU
Expands horizontally to the left so it doesn't
drop behind the pitch SVG.
═══════════════════════════════════════════════════════════ */
function ActionMenu(_ref) {
var onUndo = _ref.onUndo, onRedo = _ref.onRedo, onClear = _ref.onClear, canUndo = _ref.canUndo, canRedo = _ref.canRedo;
var _s = useState(false), open = _s[0], setOpen = _s[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 items = [
{ label: "↩ Undo", action: onUndo, disabled: !canUndo },
{ label: "↪ Redo", action: onRedo, disabled: !canRedo },
{ label: "⌫ Clear All", action: onClear, color: C.red },
];
return (
{open && (
{items.map(function (item, i) {
return (
);
})}
)}
);
}
/* ═══════════════════════════════════════════════════════════
PITCH SVG
═══════════════════════════════════════════════════════════ */
function PitchSVG(_ref) {
var children = _ref.children, svgRef = _ref.svgRef, onPointerDown = _ref.onPointerDown, onPointerMove = _ref.onPointerMove, onPointerUp = _ref.onPointerUp;
return (
);
}
/* ═══════════════════════════════════════════════════════════
TACTICAL BOARD
═══════════════════════════════════════════════════════════ */
function TacticalBoard(_ref) {
var boards = _ref.boards, activeBoard = _ref.activeBoard, onUpdate = _ref.onUpdate, onSetActive = _ref.onSetActive, onAddBoard = _ref.onAddBoard, onRenameBoard = _ref.onRenameBoard, onDeleteBoard = _ref.onDeleteBoard;
var svgRef = useRef(null);
var _t = useState("o"), tool = _t[0], setTool = _t[1];
var _a = useState(null), arrowDraft = _a[0], setArrowDraft = _a[1];
var _e = useState(null), editingName = _e[0], setEditingName = _e[1];
var _n = useState(null), newMarkerId = _n[0], setNewMarkerId = _n[1];
var _d = useState(null), dragPos = _d[0], setDragPos = _d[1];
var dragRef = useRef(null);
var boardObj = boards.find(function (b) { return b.id === activeBoard; }) || boards[0];
var _br = useReducer(boardReducer, {
current: { markers: boardObj ? boardObj.markers : [], arrows: boardObj ? boardObj.arrows : [] },
history: [], future: [],
}), boardState = _br[0], dispatch = _br[1];
var prevBoardId = useRef(activeBoard);
useEffect(function () {
if (activeBoard !== prevBoardId.current) {
var b = boards.find(function (bb) { return bb.id === activeBoard; }) || boards[0];
if (b) dispatch({ type: "INIT", board: { markers: b.markers, arrows: b.arrows } });
prevBoardId.current = activeBoard;
}
}, [activeBoard, boards]);
var cur = boardState.current;
var syncToParent = useCallback(function (newBoard) {
if (boardObj) onUpdate(boardObj.id, { ...boardObj, markers: newBoard.markers, arrows: newBoard.arrows });
}, [boardObj, onUpdate]);
var doAction = useCallback(function (newBoard) {
dispatch({ type: "DO", board: newBoard });
syncToParent(newBoard);
}, [syncToParent]);
var handleUndo = useCallback(function () {
if (!boardState.history.length) return;
var prev = boardState.history[boardState.history.length - 1];
dispatch({ type: "UNDO" });
syncToParent(prev);
}, [boardState.history, syncToParent]);
var handleRedo = useCallback(function () {
if (!boardState.future.length) return;
var next = boardState.future[0];
dispatch({ type: "REDO" });
syncToParent(next);
}, [boardState.future, syncToParent]);
var handleClear = useCallback(function () {
var empty = { markers: [], arrows: [] };
doAction(empty);
}, [doAction]);
useEffect(function () {
if (!newMarkerId) return;
var t = setTimeout(function () { setNewMarkerId(null); }, 500);
return function () { clearTimeout(t); };
}, [newMarkerId]);
var getSVGPoint = useCallback(function (e) {
var svg = svgRef.current;
if (!svg) return { x: 0, y: 0 };
var pt = svg.createSVGPoint();
pt.x = e.clientX;
pt.y = e.clientY;
var transformed = pt.matrixTransform(svg.getScreenCTM().inverse());
return { x: transformed.x, y: transformed.y };
}, []);
var hitMarker = useCallback(function (pt, markers, r) {
r = r || 20;
return markers.slice().reverse().find(function (m) { return Math.hypot(m.x - pt.x, m.y - pt.y) < r; });
}, []);
var hitArrow = useCallback(function (pt, arrows) {
return arrows.slice().reverse().find(function (a) {
var mx = (a.x1 + a.x2) / 2, my = (a.y1 + a.y2) / 2;
return Math.hypot(mx - pt.x, my - pt.y) < 25;
});
}, []);
var handlePointerDown = useCallback(function (e) {
e.preventDefault();
if (svgRef.current && svgRef.current.setPointerCapture) svgRef.current.setPointerCapture(e.pointerId);
var pt = getSVGPoint(e);
if (tool === "x" || tool === "o") {
var id = uid();
var newBoard = { ...cur, markers: cur.markers.concat([{ id: id, type: tool, x: pt.x, y: pt.y }]) };
doAction(newBoard);
setNewMarkerId(id);
} else if (tool === "arrow") {
setArrowDraft({ x1: pt.x, y1: pt.y, x2: pt.x, y2: pt.y });
} else if (tool === "select") {
var hit = hitMarker(pt, cur.markers);
if (hit) {
dragRef.current = { id: hit.id, ox: pt.x - hit.x, oy: pt.y - hit.y };
setDragPos({ id: hit.id, x: hit.x, y: hit.y });
}
} else if (tool === "erase") {
var hitM = hitMarker(pt, cur.markers);
if (hitM) { doAction({ ...cur, markers: cur.markers.filter(function (m) { return m.id !== hitM.id; }) }); return; }
var hitA = hitArrow(pt, cur.arrows);
if (hitA) { doAction({ ...cur, arrows: cur.arrows.filter(function (a) { return a.id !== hitA.id; }) }); }
}
}, [tool, cur, doAction, getSVGPoint, hitMarker, hitArrow]);
var handlePointerMove = useCallback(function (e) {
e.preventDefault();
var pt = getSVGPoint(e);
if (dragRef.current && tool === "select") {
setDragPos({ id: dragRef.current.id, x: pt.x - dragRef.current.ox, y: pt.y - dragRef.current.oy });
} else if (arrowDraft && tool === "arrow") {
setArrowDraft(function (a) { return a ? { ...a, x2: pt.x, y2: pt.y } : null; });
}
}, [tool, arrowDraft, getSVGPoint]);
var handlePointerUp = useCallback(function (e) {
if (arrowDraft && tool === "arrow") {
if (Math.hypot(arrowDraft.x2 - arrowDraft.x1, arrowDraft.y2 - arrowDraft.y1) > 15) {
doAction({ ...cur, arrows: cur.arrows.concat([{ id: uid(), x1: arrowDraft.x1, y1: arrowDraft.y1, x2: arrowDraft.x2, y2: arrowDraft.y2 }]) });
}
setArrowDraft(null);
}
if (dragRef.current && dragPos) {
var finalMarkers = cur.markers.map(function (m) {
return m.id === dragPos.id ? { ...m, x: dragPos.x, y: dragPos.y } : m;
});
doAction({ ...cur, markers: finalMarkers });
dragRef.current = null;
setDragPos(null);
}
if (svgRef.current && svgRef.current.releasePointerCapture && e) svgRef.current.releasePointerCapture(e.pointerId);
}, [arrowDraft, tool, cur, dragPos, doAction]);
var tools = [
{ id: "select", label: "Move", icon: "✋" },
{ id: "o", label: "Us", icon: "○", color: C.blue },
{ id: "x", label: "Opp", icon: "✕", color: C.red },
{ id: "arrow", label: "Arrow", icon: "→", color: C.yellow },
{ id: "erase", label: "Erase", icon: "⌫" },
];
var cursorMap = { select: "grab", x: "crosshair", o: "crosshair", arrow: "crosshair", erase: "pointer" };
var displayMarkers = cur.markers.map(function (m) {
if (dragPos && dragPos.id === m.id) return { ...m, x: dragPos.x, y: dragPos.y };
return m;
});
return (
{/* Toolbar */}
{tools.map(function (t) {
return (
);
})}
0} canRedo={boardState.future.length > 0} />
{/* Pitch */}
{/* Arrows */}
{cur.arrows.map(function (a) {
return (
);
})}
{arrowDraft && (
)}
{/* Markers */}
{displayMarkers.map(function (m, idx) {
var oNum = displayMarkers.filter(function (mm, j) { return mm.type === "o" && j <= idx; }).length;
var isNew = m.id === newMarkerId;
var isDragging = dragPos && dragPos.id === m.id;
return (
{isNew && (
)}
{m.type === "o" ? (
<>
{oNum}
>
) : (
<>
>
)}
);
})}
{/* Board tabs */}
{boards.map(function (b) {
return (
);
})}
Double-tap to rename
);
}