Boston Advantage 2014 Select 1 — Game Tracker
https://unpkg.com/react@18/umd/react.production.min.js
https://unpkg.com/react-dom@18/umd/react-dom.production.min.js
https://unpkg.com/@babel/standalone/babel.min.js
https://cdn.tailwindcss.com

function App() {
const TEAM_STATS = [
“Goal Against”,
“BA Shot on Goal”,
“Opponent Shot on Goal”,
“Positive/Aggressive Play”,
“Selfish Play/Turnover”,
“BA Blocked Shot”,
“Opponent Blocked Shot”,
“Smart Pass/Headman Play”,
“Overhandle/Late Decision”
];

const PENALTY_SEVERITIES = [
{ key: “Dumb”, label: “Dumb Penalty”, color: “red” },
{ key: “Mistake”, label: “Mistake Penalty”, color: “yellow” },
{ key: “Standard”, label: “Standard Penalty”, color: “green” }
];

const defaultRoster = [
{ number: “1”, name: “Jameson Dzialo”, active: true },
{ number: “8”, name: “Jackson Stevens”, active: true },
{ number: “9”, name: “Rocco Sorrentino”, active: true },
{ number: “11”, name: “Cam McNamara”, active: true },
{ number: “16”, name: “Ethan Hanson”, active: true },
{ number: “17”, name: “Griffin Shearing”, active: true },
{ number: “21”, name: “Cam Thompson”, active: true },
{ number: “22”, name: “Kyle Pavlu”, active: true },
{ number: “33”, name: “Cole McIntosh”, active: true },
{ number: “35”, name: “Nolan Cox”, active: true },
{ number: “37”, name: “Chris Murphy”, active: true },
{ number: “63”, name: “Zac Bosse”, active: true },
{ number: “82”, name: “Liam Larkin”, active: true },
{ number: “86”, name: “Shane Savastano”, active: true },
{ number: “88”, name: “Ryan Matheny”, active: true },
{ number: “93”, name: “Xavier Garrett”, active: true },
{ number: “97”, name: “Joey Pericolo”, active: true }
];

const [screen, setScreen] = React.useState(“tracker”);
const [roster, setRoster] = React.useState(defaultRoster);
const [period, setPeriod] = React.useState(“1”);
const [log, setLog] = React.useState([]);
const [flashMsg, setFlashMsg] = React.useState(“”);
const [playerModal, setPlayerModal] = React.useState(null);
const [syncUrl, setSyncUrl] = React.useState(“”);
const [syncStatus, setSyncStatus] = React.useState(“Not connected”);
const [showNewGameConfirm, setShowNewGameConfirm] = React.useState(false);
const [newGameStep, setNewGameStep] = React.useState(“roster”);
const [gaModalOpen, setGaModalOpen] = React.useState(false);
const [gaOnIce, setGaOnIce] = React.useState([]);
const [gaGoalie, setGaGoalie] = React.useState(null);
const [gameInfo, setGameInfo] = React.useState({ date: new Date().toISOString().slice(0, 10), opponent: “” });
const [newGameDate, setNewGameDate] = React.useState(new Date().toISOString().slice(0, 10));
const [newGameOpponent, setNewGameOpponent] = React.useState(“”);
const [newGameError, setNewGameError] = React.useState(“”);
const [notes, setNotes] = React.useState([]);
const [noteText, setNoteText] = React.useState(“”);
const [noteTimeRemaining, setNoteTimeRemaining] = React.useState(“”);
const [penaltyModalOpen, setPenaltyModalOpen] = React.useState(false);
const [penaltyPlayer, setPenaltyPlayer] = React.useState(null);
const [penaltySeverity, setPenaltySeverity] = React.useState(null);
const [gameHistory, setGameHistory] = React.useState(() => {
try {
const stored = window.localStorage.getItem(“ba_game_history”);
return stored ? JSON.parse(stored) : [];
} catch (e) {
return [];
}
});
const [gameOverModalOpen, setGameOverModalOpen] = React.useState(false);
const [finalScoreBA, setFinalScoreBA] = React.useState(“”);
const [finalScoreOpp, setFinalScoreOpp] = React.useState(“”);
const [gameOverError, setGameOverError] = React.useState(“”);
const [expandedGameId, setExpandedGameId] = React.useState(null);
const [editingGameId, setEditingGameId] = React.useState(null);
const [deleteConfirmGameId, setDeleteConfirmGameId] = React.useState(null);

React.useEffect(() => {
try {
window.localStorage.setItem(“ba_game_history”, JSON.stringify(gameHistory));
} catch (e) {}
}, [gameHistory]);

const GOALIES = [“Nolan Cox”, “Jameson Dzialo”];

const flashRef = React.useRef(null);

function showFlash(msg) {
setFlashMsg(msg);
if (flashRef.current) clearTimeout(flashRef.current);
flashRef.current = setTimeout(() => setFlashMsg(“”), 1200);
}

function logEvent(player, stat, groupId) {
const gid = groupId || `${Date.now()}-${Math.random()}`;
const entry = {
id: Date.now() + Math.random(),
groupId: gid,
time: new Date().toLocaleTimeString(),
timestamp: new Date().toISOString(),
gameDate: gameInfo.date,
opponent: gameInfo.opponent,
period,
player: player || “TEAM”,
stat
};
setLog(prev => [entry, …prev]);
showFlash(`${stat}${player ? ” — ” + player : “”} (P${period})`);

if (syncUrl.trim()) {
fetch(syncUrl.trim(), {
method: “POST”,
mode: “no-cors”,
headers: { “Content-Type”: “text/plain;charset=utf-8” },
body: JSON.stringify(entry)
})
.then(() => setSyncStatus(“Last event sent to Google Sheets”))
.catch(() => setSyncStatus(“Could not send — check the sync URL”));
}
return gid;
}

function undoLast() {
if (log.length === 0) {
showFlash(“Nothing to undo”);
return;
}
const lastGroupId = log[0].groupId;
const undone = log.filter(e => e.groupId === lastGroupId);
setLog(prev => prev.filter(e => e.groupId !== lastGroupId));
showFlash(`Undid: ${undone[0].stat}${undone[0].player !== “TEAM” ? ” — ” + undone[0].player : “”}`);

if (syncUrl.trim()) {
fetch(syncUrl.trim(), {
method: “POST”,
mode: “no-cors”,
headers: { “Content-Type”: “text/plain;charset=utf-8” },
body: JSON.stringify({ action: “undo”, groupId: lastGroupId, entries: undone })
}).catch(() => {});
}
}

function handleTeamStatTap(stat) {
if (stat === “Goal Against”) {
setGaOnIce([]);
setGaGoalie(null);
setGaModalOpen(true);
return;
}
logEvent(null, stat);
}

function toggleOnIce(name) {
setGaOnIce(prev =>
prev.includes(name) ? prev.filter(n => n !== name) : […prev, name]
);
}

function confirmGoalAgainst() {
const gid = `${Date.now()}-${Math.random()}`;
logEvent(null, “Goal Against”, gid);
gaOnIce.forEach(name => logEvent(name, “On Ice For GA”, gid));
if (gaGoalie) {
logEvent(gaGoalie, “Goal Against – Goalie”, gid);
}
setGaModalOpen(false);
setGaOnIce([]);
setGaGoalie(null);
}

function handleGoalAssistTap(stat) {
setPlayerModal(stat);
}

function openPenaltyModal() {
setPenaltyPlayer(null);
setPenaltySeverity(null);
setPenaltyModalOpen(true);
}

function confirmPenalty() {
if (!penaltyPlayer || !penaltySeverity) {
showFlash(“Pick a player and a severity”);
return;
}
logEvent(penaltyPlayer, `Penalty (${penaltySeverity})`);
setPenaltyModalOpen(false);
setPenaltyPlayer(null);
setPenaltySeverity(null);
}

function choosePlayer(playerName) {
logEvent(playerName, playerModal);
setPlayerModal(null);
}

function deleteLogEntry(id) {
setLog(prev => prev.filter(e => e.id !== id));
}

function addNote() {
if (!noteText.trim()) {
showFlash(“Enter a note first”);
return;
}
const note = {
id: Date.now() + Math.random(),
time: new Date().toLocaleTimeString(),
timestamp: new Date().toISOString(),
gameDate: gameInfo.date,
opponent: gameInfo.opponent,
period,
timeRemaining: noteTimeRemaining.trim(),
text: noteText.trim()
};
setNotes(prev => [note, …prev]);
setNoteText(“”);
setNoteTimeRemaining(“”);
showFlash(“Note added”);

if (syncUrl.trim()) {
fetch(syncUrl.trim(), {
method: “POST”,
mode: “no-cors”,
headers: { “Content-Type”: “text/plain;charset=utf-8” },
body: JSON.stringify({ action: “note”, …note })
})
.then(() => setSyncStatus(“Note sent to Google Sheets”))
.catch(() => setSyncStatus(“Could not send — check the sync URL”));
}
}

function deleteNote(id) {
setNotes(prev => prev.filter(n => n.id !== id));
}

function startNewGame() {
if (!newGameDate.trim() || !newGameOpponent.trim()) {
setNewGameError(“Please enter both a date and an opponent.”);
return;
}

const completedGame = {
action: “new_game”,
completedAt: new Date().toISOString(),
gameDate: gameInfo.date,
opponent: gameInfo.opponent,
roster: activeRoster,
events: log,
notes: notes
};

if (syncUrl.trim()) {
fetch(syncUrl.trim(), {
method: “POST”,
mode: “no-cors”,
headers: { “Content-Type”: “text/plain;charset=utf-8” },
body: JSON.stringify(completedGame)
})
.then(() => setSyncStatus(“Game summary sent to Google Sheets”))
.catch(() => setSyncStatus(“Could not send summary — check the sync URL”));
}

if (log.length > 0) {
setGameHistory(prev => [
…prev,
{
id: Date.now() + Math.random(),
gameDate: gameInfo.date,
opponent: gameInfo.opponent,
events: log,
notes: notes,
roster: activeRoster,
finalized: false,
baScore: null,
oppScore: null
}
]);
}

setLog([]);
setNotes([]);
setPeriod(“1”);
setFlashMsg(“”);
setShowNewGameConfirm(false);
setNewGameError(“”);
setGameInfo({ date: newGameDate.trim(), opponent: newGameOpponent.trim() });
showFlash(syncUrl.trim() ? “New game started — summary sent” : “New game started”);
}

function openGameOverModal() {
setFinalScoreBA(“”);
setFinalScoreOpp(“”);
setGameOverError(“”);
setGameOverModalOpen(true);
}

function confirmGameOver() {
if (finalScoreBA.trim() === “” || finalScoreOpp.trim() === “”) {
setGameOverError(“Please enter both final scores.”);
return;
}
const baScore = parseInt(finalScoreBA, 10);
const oppScore = parseInt(finalScoreOpp, 10);
if (isNaN(baScore) || isNaN(oppScore) || baScore < 0 || oppScore […prev, finishedGame]);

if (syncUrl.trim()) {
fetch(syncUrl.trim(), {
method: “POST”,
mode: “no-cors”,
headers: { “Content-Type”: “text/plain;charset=utf-8” },
body: JSON.stringify({ action: “game_over”, …finishedGame })
})
.then(() => setSyncStatus(“Final game summary sent to Google Sheets”))
.catch(() => setSyncStatus(“Could not send summary — check the sync URL”));
}

setLog([]);
setNotes([]);
setPeriod(“1”);
setFlashMsg(“”);
setGameOverModalOpen(false);
setGameOverError(“”);
setGameInfo({ date: new Date().toISOString().slice(0, 10), opponent: “” });
showFlash(`Game Over — Final ${baScore}-${oppScore} saved to Game Log`);
}

function deleteGameFromHistory(id) {
setGameHistory(prev => prev.filter(g => g.id !== id));
if (expandedGameId === id) setExpandedGameId(null);
if (editingGameId === id) setEditingGameId(null);
if (deleteConfirmGameId === id) setDeleteConfirmGameId(null);
showFlash(“Game deleted”);
}

function updateGameScoreField(id, field, value) {
setGameHistory(prev =>
prev.map(g => (g.id === id ? { …g, [field]: value === “” ? “” : parseInt(value, 10) || 0 } : g))
);
}

function deleteEventFromGame(gameId, eventId) {
setGameHistory(prev =>
prev.map(g =>
g.id === gameId ? { …g, events: g.events.filter(e => e.id !== eventId) } : g
)
);
}

function toggleExpandGame(id) {
setExpandedGameId(prev => (prev === id ? null : id));
}

function toggleEditGame(id) {
setEditingGameId(prev => (prev === id ? null : id));
}

function computeGameStats(events, roster) {
const teamStatCounts = {};
TEAM_STATS.forEach(stat => {
teamStatCounts[stat] = events.filter(e => e.stat === stat).length;
});
const playerStats = (roster || [])
.map(p => {
const goals = events.filter(e => e.stat === “Goal” && e.player === p.name).length;
const assists = events.filter(e => e.stat === “Assist” && e.player === p.name).length;
const onIceGA = events.filter(e => e.stat === “On Ice For GA” && e.player === p.name).length;
const dumb = events.filter(e => e.stat === “Penalty (Dumb)” && e.player === p.name).length;
const mistake = events.filter(e => e.stat === “Penalty (Mistake)” && e.player === p.name).length;
const standard = events.filter(e => e.stat === “Penalty (Standard)” && e.player === p.name).length;
return {
name: p.name,
number: p.number,
goals,
assists,
onIceGA,
plusMinus: goals + assists – onIceGA,
dumb,
mistake,
standard,
penaltyTotal: dumb + mistake + standard
};
})
.filter(p => p.goals + p.assists + p.onIceGA + p.penaltyTotal > 0);
return { teamStatCounts, playerStats };
}

function updateRosterField(index, field, value) {
setRoster(prev => {
const copy = […prev];
copy[index] = { …copy[index], [field]: value };
return copy;
});
}

function toggleRosterActive(index) {
setRoster(prev => {
const copy = […prev];
copy[index] = { …copy[index], active: copy[index].active === false ? true : false };
return copy;
});
}

const activeRoster = roster.filter(p => p.name && p.name.trim() !== “”);
const availableRoster = activeRoster.filter(p => p.active !== false);

const periods = [“1”, “2”, “3”, “OT”];
const allStats = […TEAM_STATS];

function plusMinusFor(playerName) {
const goals = log.filter(e => e.stat === “Goal” && e.player === playerName).length;
const assists = log.filter(e => e.stat === “Assist” && e.player === playerName).length;
const onIceGA = log.filter(e => e.stat === “On Ice For GA” && e.player === playerName).length;
return { goals, assists, onIceGA, plusMinus: goals + assists – onIceGA };
}

function countFor(stat, per) {
return log.filter(e => e.stat === stat && (per === “Total” || e.period === per)).length;
}

function penaltyCountFor(severityKey, per) {
return countFor(`Penalty (${severityKey})`, per);
}

function penaltiesByPlayer() {
return activeRoster
.map(p => ({
name: p.name,
number: p.number,
dumb: log.filter(e => e.stat === “Penalty (Dumb)” && e.player === p.name).length,
mistake: log.filter(e => e.stat === “Penalty (Mistake)” && e.player === p.name).length,
standard: log.filter(e => e.stat === “Penalty (Standard)” && e.player === p.name).length
}))
.filter(p => p.dumb + p.mistake + p.standard > 0);
}

function seasonTotalsFor(playerName) {
let goals = 0, assists = 0, onIceGA = 0;
gameHistory.forEach(g => {
g.events.forEach(e => {
if (e.player !== playerName) return;
if (e.stat === “Goal”) goals += 1;
else if (e.stat === “Assist”) assists += 1;
else if (e.stat === “On Ice For GA”) onIceGA += 1;
});
});
goals += log.filter(e => e.stat === “Goal” && e.player === playerName).length;
assists += log.filter(e => e.stat === “Assist” && e.player === playerName).length;
onIceGA += log.filter(e => e.stat === “On Ice For GA” && e.player === playerName).length;
return { goals, assists, onIceGA, plusMinus: goals + assists – onIceGA };
}

function seasonPenaltyCountFor(severityKey) {
const statName = `Penalty (${severityKey})`;
let count = 0;
gameHistory.forEach(g => {
count += g.events.filter(e => e.stat === statName).length;
});
count += log.filter(e => e.stat === statName).length;
return count;
}

const seasonGamesPlayedCount = gameHistory.length + (log.length > 0 ? 1 : 0);
const finalizedGames = gameHistory.filter(g => g.finalized).sort((a, b) => {
if (a.gameDate === b.gameDate) return 0;
return a.gameDate `${e.time},${e.period},${e.player},${e.stat}`)
.join(“\n”);
const csv = header + rows;
const blob = new Blob([csv], { type: “text/csv” });
const url = URL.createObjectURL(blob);
const a = document.createElement(“a”);
a.href = url;
a.download = “game_log.csv”;
a.click();
URL.revokeObjectURL(url);
}

const navBtn = (key, label) => (

);

return (

Boston Advantage 2014 Select 1 logo

BOSTON ADVANTAGE

{gameInfo.opponent
? `vs ${gameInfo.opponent} · ${gameInfo.date}`
: “2014 SELECT 1 · GAME TRACKER”}


{navBtn(“tracker”, “Tracker”)}
{navBtn(“log”, “Game Notes”)}
{navBtn(“summary”, “Stats Summary”)}
{navBtn(“gamelog”, “Game Log”)}
{navBtn(“sync”, “Sync”)}

{flashMsg && (

✓ Logged: {flashMsg}

)}

{screen === “tracker” && (

Current Period
{periods.map(p => (

))}

Player Stats

Team Stats





)}

{screen === “log” && (

Current Game
{gameInfo.opponent ? `vs ${gameInfo.opponent}` : “No opponent set”} · {gameInfo.date}

Add a Note — P{period}

setNoteTimeRemaining(e.target.value)}
className=”w-full border rounded-lg p-3 text-sm mb-2″
/>
setNoteText(e.target.value)}
rows={3}
className=”w-full border rounded-lg p-3 text-sm mb-2″
/>

Notes ({notes.length})
{notes.length === 0 && (

No notes yet for this game.

)}
{notes.map(n => (

{n.opponent ? `vs ${n.opponent}` : “No opponent set”} · {n.gameDate} · P{n.period}
{n.timeRemaining ? ` · ${n.timeRemaining} remaining` : “”}
{n.text}
Logged {n.time}

))}

Stat Log ({log.length} events)

{log.length === 0 && (

No events logged yet. Tap stats on the Tracker tab.

)}
{log.map(entry => (

P{entry.period} — {entry.stat}
{entry.player} · {entry.time}

))}

)}

{screen === “summary” && (

Game Summary
Current Game
{gameInfo.opponent ? `vs ${gameInfo.opponent}` : “No opponent set”} · {gameInfo.date}

{allStats.map((stat, idx) => (

))}

Stat P1 P2 P3 OT Total
{stat} {countFor(stat, “1”)} {countFor(stat, “2”)} {countFor(stat, “3”)} {countFor(stat, “OT”)} {countFor(stat, “Total”)}
Player Plus / Minus (This Game)
{activeRoster.map((p, idx) => {
const { goals, assists, onIceGA, plusMinus } = plusMinusFor(p.name);
const pmColor =
plusMinus > 0
? “text-green-700”
: plusMinus < 0
? "text-red-600"
: "text-gray-700";
return (

);
})}
{activeRoster.length === 0 && (

)}

Player G A GA On-Ice +/-
{p.number ? `#${p.number} ` : “”}
{p.name}
{goals} {assists} {onIceGA} {plusMinus > 0 ? `+${plusMinus}` : plusMinus}
Add players on the Roster tab to see plus/minus.
Penalty Summary (This Game)
{PENALTY_SEVERITIES.map((sev, idx) => {
const dotColor =
sev.color === “red”
? “bg-red-600”
: sev.color === “yellow”
? “bg-yellow-400”
: “bg-green-600”;
return (

);
})}

Penalty Type P1 P2 P3 OT Total

{sev.label}
{penaltyCountFor(sev.key, “1”)} {penaltyCountFor(sev.key, “2”)} {penaltyCountFor(sev.key, “3”)} {penaltyCountFor(sev.key, “OT”)} {penaltyCountFor(sev.key, “Total”)}
Total Penalties {PENALTY_SEVERITIES.reduce((sum, sev) => sum + penaltyCountFor(sev.key, “1”), 0)} {PENALTY_SEVERITIES.reduce((sum, sev) => sum + penaltyCountFor(sev.key, “2”), 0)} {PENALTY_SEVERITIES.reduce((sum, sev) => sum + penaltyCountFor(sev.key, “3”), 0)} {PENALTY_SEVERITIES.reduce((sum, sev) => sum + penaltyCountFor(sev.key, “OT”), 0)} {PENALTY_SEVERITIES.reduce((sum, sev) => sum + penaltyCountFor(sev.key, “Total”), 0)}
Penalties By Player (This Game)
{penaltiesByPlayer().map((p, idx) => (

))}
{penaltiesByPlayer().length === 0 && (

)}

Player Dumb Mistake Standard Total
{p.number ? `#${p.number} ` : “”}
{p.name}
{p.dumb} {p.mistake} {p.standard} {p.dumb + p.mistake + p.standard}
No penalties logged yet this game.
Season Totals — All Games ({seasonGamesPlayedCount} games logged)
{activeRoster.map((p, idx) => {
const { goals, assists, onIceGA, plusMinus } = seasonTotalsFor(p.name);
const pmColor =
plusMinus > 0
? “text-green-700”
: plusMinus < 0
? "text-red-600"
: "text-gray-700";
return (

);
})}
{activeRoster.length === 0 && (

)}

Player G A GA On-Ice +/-
{p.number ? `#${p.number} ` : “”}
{p.name}
{goals} {assists} {onIceGA} {plusMinus > 0 ? `+${plusMinus}` : plusMinus}
Add players on the Roster tab to see season totals.
Team Total {activeRoster.reduce((sum, p) => sum + seasonTotalsFor(p.name).goals, 0)} {activeRoster.reduce((sum, p) => sum + seasonTotalsFor(p.name).assists, 0)} {activeRoster.reduce((sum, p) => sum + seasonTotalsFor(p.name).onIceGA, 0)} {activeRoster.reduce((sum, p) => sum + seasonTotalsFor(p.name).plusMinus, 0)}
Season Total Penalties — All Games
{PENALTY_SEVERITIES.map((sev, idx) => {
const dotColor =
sev.color === “red”
? “bg-red-600”
: sev.color === “yellow”
? “bg-yellow-400”
: “bg-green-600”;
return (

);
})}

Penalty Type Total

{sev.label}
{seasonPenaltyCountFor(sev.key)}
Total Penalties {PENALTY_SEVERITIES.reduce((sum, sev) => sum + seasonPenaltyCountFor(sev.key), 0)}

)}

{screen === “gamelog” && (

Game Log — Completed Games ({finalizedGames.length})

{finalizedGames.length === 0 && (

No completed games yet. Tap “GAME OVER — Enter Final Score” on the Tracker tab to save a game here.

)}

{finalizedGames.map(g => {
const isExpanded = expandedGameId === g.id;
const isEditing = editingGameId === g.id;
const { teamStatCounts, playerStats } = computeGameStats(g.events, g.roster || activeRoster);
return (

toggleExpandGame(g.id)}
className=”p-3 flex justify-between items-center cursor-pointer active:bg-gray-50″
>

vs {g.opponent || “Unknown”} · {g.gameDate}
Final: BA {g.baScore} — {g.oppScore} Opponent

{isExpanded ? “▲” : “▼”}

{isExpanded && (


{deleteConfirmGameId === g.id && (

Delete this game permanently? This cannot be undone.

)}

{isEditing ? (

BA Score
updateGameScoreField(g.id, “baScore”, e.target.value)}
className=”w-full border rounded-lg p-2 text-sm”
/>
Opponent Score
updateGameScoreField(g.id, “oppScore”, e.target.value)}
className=”w-full border rounded-lg p-2 text-sm”
/>

) : null}

Player Goals / Assists / Penalties
{playerStats.map((p, idx) => (

))}
{playerStats.length === 0 && (

)}

Player G A +/- Pen (D/M/S)
{p.number ? `#${p.number} ` : “”}
{p.name}
{p.goals} {p.assists} {p.plusMinus > 0 ? `+${p.plusMinus}` : p.plusMinus} {p.dumb}/{p.mistake}/{p.standard}
No player stats recorded for this game.
Team Stats — Boston Advantage vs Opponent
{TEAM_STATS.map((stat, idx) => (

))}

Stat Total
{stat} {teamStatCounts[stat]}

{isEditing && (

Logged Events ({g.events.length}) — tap ✕ to remove a mistaken entry
{g.events.map(entry => (

P{entry.period} — {entry.stat}
{entry.player}

))}
{g.events.length === 0 && (

No events left for this game.

)}

)}

{g.notes && g.notes.length > 0 && (

Notes
{g.notes.map(n => (

P{n.period}{n.timeRemaining ? ` · ${n.timeRemaining} remaining` : “”}: {n.text}

))}

)}

)}

);
})}

)}

{screen === “sync” && (

Google Sheets Sync

Paste the Google Apps Script web-app URL here. Every new stat tap will be added to the Game Log sheet.

{
setSyncUrl(e.target.value);
setSyncStatus(e.target.value.trim() ? “Ready to sync new events” : “Not connected”);
}}
placeholder=”https://script.google.com/macros/s/…/exec&#8221;
className=”w-full border rounded-lg p-3 text-sm”
/>

{syncStatus}

This app keeps its live display on your phone and sends each newly logged event to Google Sheets. Existing events are not re-sent.

)}

{showNewGameConfirm && newGameStep === “roster” && (

Set Roster for New Game

Edit names/numbers if needed, and mark each player In or Out for this game. Players marked Out won’t show up as options on the Tracker.

{roster.map((p, i) => {
const isActive = p.active !== false;
return (

updateRosterField(i, “number”, e.target.value)}
className=”w-12 text-center border rounded p-2 text-sm”
/>
updateRosterField(i, “name”, e.target.value)}
className=”flex-1 border rounded p-2 text-sm”
/>

);
})}


)}

{showNewGameConfirm && newGameStep === “details” && (

Start a new game?

The current game will be summarized in Google Sheets, then the tracker will reset to Period 1.

Game Date
setNewGameDate(e.target.value)}
className=”w-full border rounded-lg p-3 text-sm”
/>
Opponent
setNewGameOpponent(e.target.value)}
className=”w-full border rounded-lg p-3 text-sm”
/>

{newGameError && (

{newGameError}

)}


)}

{gameOverModalOpen && (

🏁 Game Over

Enter the final score. This game will be saved to the Game Log and statistics collection will end for {gameInfo.opponent ? `vs ${gameInfo.opponent}` : “this game”}.

Boston Advantage
setFinalScoreBA(e.target.value)}
className=”w-full border rounded-lg p-3 text-sm”
placeholder=”0″
/>
Opponent
setFinalScoreOpp(e.target.value)}
className=”w-full border rounded-lg p-3 text-sm”
placeholder=”0″
/>

{gameOverError && (

{gameOverError}

)}


)}

{gaModalOpen && (

Goal Against — who was on the ice?

Goalie in Net
{GOALIES.map(name => (

))}
Skaters On Ice (tap all that apply)
{availableRoster
.filter(p => !GOALIES.includes(p.name))
.map(p => {
const selected = gaOnIce.includes(p.name);
return (

);
})}

)}

{penaltyModalOpen && (

Penalty — who took it?

Player
{availableRoster.map(p => (

))}
Penalty Type
{PENALTY_SEVERITIES.map(sev => {
const selected = penaltySeverity === sev.key;
const baseClasses =
sev.color === “red”
? selected
? “bg-red-600 text-white border-red-600”
: “bg-red-50 text-red-700 border-red-600”
: sev.color === “yellow”
? selected
? “bg-yellow-400 text-blue-900 border-yellow-400”
: “bg-yellow-50 text-yellow-700 border-yellow-400”
: selected
? “bg-green-600 text-white border-green-600”
: “bg-green-50 text-green-700 border-green-600”;
return (

);
})}

)}

{playerModal && (

Who scored the {playerModal === “Goal” ? “goal” : “assist”}?

{availableRoster
.map(p => (

))}

)}

);
}

ReactDOM.createRoot(document.getElementById(“root”)).render();

Boston Advantage 2014 Select 1 — Game Tracker
https://unpkg.com/react@18/umd/react.production.min.js
https://unpkg.com/react-dom@18/umd/react-dom.production.min.js
https://unpkg.com/@babel/standalone/babel.min.js
https://cdn.tailwindcss.com

function App() {
const TEAM_STATS = [
“Goal Against”,
“BA Shot on Goal”,
“Opponent Shot on Goal”,
“Positive/Aggressive Play”,
“Selfish Play/Turnover”,
“BA Blocked Shot”,
“Opponent Blocked Shot”,
“Smart Pass/Headman Play”,
“Overhandle/Late Decision”
];

const PENALTY_SEVERITIES = [
{ key: “Dumb”, label: “Dumb Penalty”, color: “red” },
{ key: “Mistake”, label: “Mistake Penalty”, color: “yellow” },
{ key: “Standard”, label: “Standard Penalty”, color: “green” }
];

const defaultRoster = [
{ number: “1”, name: “Jameson Dzialo”, active: true },
{ number: “8”, name: “Jackson Stevens”, active: true },
{ number: “9”, name: “Rocco Sorrentino”, active: true },
{ number: “11”, name: “Cam McNamara”, active: true },
{ number: “16”, name: “Ethan Hanson”, active: true },
{ number: “17”, name: “Griffin Shearing”, active: true },
{ number: “21”, name: “Cam Thompson”, active: true },
{ number: “22”, name: “Kyle Pavlu”, active: true },
{ number: “33”, name: “Cole McIntosh”, active: true },
{ number: “35”, name: “Nolan Cox”, active: true },
{ number: “37”, name: “Chris Murphy”, active: true },
{ number: “63”, name: “Zac Bosse”, active: true },
{ number: “82”, name: “Liam Larkin”, active: true },
{ number: “86”, name: “Shane Savastano”, active: true },
{ number: “88”, name: “Ryan Matheny”, active: true },
{ number: “93”, name: “Xavier Garrett”, active: true },
{ number: “97”, name: “Joey Pericolo”, active: true }
];

const [screen, setScreen] = React.useState(“tracker”);
const [roster, setRoster] = React.useState(defaultRoster);
const [period, setPeriod] = React.useState(“1”);
const [log, setLog] = React.useState([]);
const [flashMsg, setFlashMsg] = React.useState(“”);
const [playerModal, setPlayerModal] = React.useState(null);
const [syncUrl, setSyncUrl] = React.useState(“”);
const [syncStatus, setSyncStatus] = React.useState(“Not connected”);
const [showNewGameConfirm, setShowNewGameConfirm] = React.useState(false);
const [newGameStep, setNewGameStep] = React.useState(“roster”);
const [gaModalOpen, setGaModalOpen] = React.useState(false);
const [gaOnIce, setGaOnIce] = React.useState([]);
const [gaGoalie, setGaGoalie] = React.useState(null);
const [gameInfo, setGameInfo] = React.useState({ date: new Date().toISOString().slice(0, 10), opponent: “” });
const [newGameDate, setNewGameDate] = React.useState(new Date().toISOString().slice(0, 10));
const [newGameOpponent, setNewGameOpponent] = React.useState(“”);
const [newGameError, setNewGameError] = React.useState(“”);
const [notes, setNotes] = React.useState([]);
const [noteText, setNoteText] = React.useState(“”);
const [noteTimeRemaining, setNoteTimeRemaining] = React.useState(“”);
const [penaltyModalOpen, setPenaltyModalOpen] = React.useState(false);
const [penaltyPlayer, setPenaltyPlayer] = React.useState(null);
const [penaltySeverity, setPenaltySeverity] = React.useState(null);
const [gameHistory, setGameHistory] = React.useState(() => {
try {
const stored = window.localStorage.getItem(“ba_game_history”);
return stored ? JSON.parse(stored) : [];
} catch (e) {
return [];
}
});
const [gameOverModalOpen, setGameOverModalOpen] = React.useState(false);
const [finalScoreBA, setFinalScoreBA] = React.useState(“”);
const [finalScoreOpp, setFinalScoreOpp] = React.useState(“”);
const [gameOverError, setGameOverError] = React.useState(“”);
const [expandedGameId, setExpandedGameId] = React.useState(null);
const [editingGameId, setEditingGameId] = React.useState(null);
const [deleteConfirmGameId, setDeleteConfirmGameId] = React.useState(null);

React.useEffect(() => {
try {
window.localStorage.setItem(“ba_game_history”, JSON.stringify(gameHistory));
} catch (e) {}
}, [gameHistory]);

const GOALIES = [“Nolan Cox”, “Jameson Dzialo”];

const flashRef = React.useRef(null);

function showFlash(msg) {
setFlashMsg(msg);
if (flashRef.current) clearTimeout(flashRef.current);
flashRef.current = setTimeout(() => setFlashMsg(“”), 1200);
}

function logEvent(player, stat, groupId) {
const gid = groupId || `${Date.now()}-${Math.random()}`;
const entry = {
id: Date.now() + Math.random(),
groupId: gid,
time: new Date().toLocaleTimeString(),
timestamp: new Date().toISOString(),
gameDate: gameInfo.date,
opponent: gameInfo.opponent,
period,
player: player || “TEAM”,
stat
};
setLog(prev => [entry, …prev]);
showFlash(`${stat}${player ? ” — ” + player : “”} (P${period})`);

if (syncUrl.trim()) {
fetch(syncUrl.trim(), {
method: “POST”,
mode: “no-cors”,
headers: { “Content-Type”: “text/plain;charset=utf-8” },
body: JSON.stringify(entry)
})
.then(() => setSyncStatus(“Last event sent to Google Sheets”))
.catch(() => setSyncStatus(“Could not send — check the sync URL”));
}
return gid;
}

function undoLast() {
if (log.length === 0) {
showFlash(“Nothing to undo”);
return;
}
const lastGroupId = log[0].groupId;
const undone = log.filter(e => e.groupId === lastGroupId);
setLog(prev => prev.filter(e => e.groupId !== lastGroupId));
showFlash(`Undid: ${undone[0].stat}${undone[0].player !== “TEAM” ? ” — ” + undone[0].player : “”}`);

if (syncUrl.trim()) {
fetch(syncUrl.trim(), {
method: “POST”,
mode: “no-cors”,
headers: { “Content-Type”: “text/plain;charset=utf-8” },
body: JSON.stringify({ action: “undo”, groupId: lastGroupId, entries: undone })
}).catch(() => {});
}
}

function handleTeamStatTap(stat) {
if (stat === “Goal Against”) {
setGaOnIce([]);
setGaGoalie(null);
setGaModalOpen(true);
return;
}
logEvent(null, stat);
}

function toggleOnIce(name) {
setGaOnIce(prev =>
prev.includes(name) ? prev.filter(n => n !== name) : […prev, name]
);
}

function confirmGoalAgainst() {
const gid = `${Date.now()}-${Math.random()}`;
logEvent(null, “Goal Against”, gid);
gaOnIce.forEach(name => logEvent(name, “On Ice For GA”, gid));
if (gaGoalie) {
logEvent(gaGoalie, “Goal Against – Goalie”, gid);
}
setGaModalOpen(false);
setGaOnIce([]);
setGaGoalie(null);
}

function handleGoalAssistTap(stat) {
setPlayerModal(stat);
}

function openPenaltyModal() {
setPenaltyPlayer(null);
setPenaltySeverity(null);
setPenaltyModalOpen(true);
}

function confirmPenalty() {
if (!penaltyPlayer || !penaltySeverity) {
showFlash(“Pick a player and a severity”);
return;
}
logEvent(penaltyPlayer, `Penalty (${penaltySeverity})`);
setPenaltyModalOpen(false);
setPenaltyPlayer(null);
setPenaltySeverity(null);
}

function choosePlayer(playerName) {
logEvent(playerName, playerModal);
setPlayerModal(null);
}

function deleteLogEntry(id) {
setLog(prev => prev.filter(e => e.id !== id));
}

function addNote() {
if (!noteText.trim()) {
showFlash(“Enter a note first”);
return;
}
const note = {
id: Date.now() + Math.random(),
time: new Date().toLocaleTimeString(),
timestamp: new Date().toISOString(),
gameDate: gameInfo.date,
opponent: gameInfo.opponent,
period,
timeRemaining: noteTimeRemaining.trim(),
text: noteText.trim()
};
setNotes(prev => [note, …prev]);
setNoteText(“”);
setNoteTimeRemaining(“”);
showFlash(“Note added”);

if (syncUrl.trim()) {
fetch(syncUrl.trim(), {
method: “POST”,
mode: “no-cors”,
headers: { “Content-Type”: “text/plain;charset=utf-8” },
body: JSON.stringify({ action: “note”, …note })
})
.then(() => setSyncStatus(“Note sent to Google Sheets”))
.catch(() => setSyncStatus(“Could not send — check the sync URL”));
}
}

function deleteNote(id) {
setNotes(prev => prev.filter(n => n.id !== id));
}

function startNewGame() {
if (!newGameDate.trim() || !newGameOpponent.trim()) {
setNewGameError(“Please enter both a date and an opponent.”);
return;
}

const completedGame = {
action: “new_game”,
completedAt: new Date().toISOString(),
gameDate: gameInfo.date,
opponent: gameInfo.opponent,
roster: activeRoster,
events: log,
notes: notes
};

if (syncUrl.trim()) {
fetch(syncUrl.trim(), {
method: “POST”,
mode: “no-cors”,
headers: { “Content-Type”: “text/plain;charset=utf-8” },
body: JSON.stringify(completedGame)
})
.then(() => setSyncStatus(“Game summary sent to Google Sheets”))
.catch(() => setSyncStatus(“Could not send summary — check the sync URL”));
}

if (log.length > 0) {
setGameHistory(prev => [
…prev,
{
id: Date.now() + Math.random(),
gameDate: gameInfo.date,
opponent: gameInfo.opponent,
events: log,
notes: notes,
roster: activeRoster,
finalized: false,
baScore: null,
oppScore: null
}
]);
}

setLog([]);
setNotes([]);
setPeriod(“1”);
setFlashMsg(“”);
setShowNewGameConfirm(false);
setNewGameError(“”);
setGameInfo({ date: newGameDate.trim(), opponent: newGameOpponent.trim() });
showFlash(syncUrl.trim() ? “New game started — summary sent” : “New game started”);
}

function openGameOverModal() {
setFinalScoreBA(“”);
setFinalScoreOpp(“”);
setGameOverError(“”);
setGameOverModalOpen(true);
}

function confirmGameOver() {
if (finalScoreBA.trim() === “” || finalScoreOpp.trim() === “”) {
setGameOverError(“Please enter both final scores.”);
return;
}
const baScore = parseInt(finalScoreBA, 10);
const oppScore = parseInt(finalScoreOpp, 10);
if (isNaN(baScore) || isNaN(oppScore) || baScore < 0 || oppScore […prev, finishedGame]);

if (syncUrl.trim()) {
fetch(syncUrl.trim(), {
method: “POST”,
mode: “no-cors”,
headers: { “Content-Type”: “text/plain;charset=utf-8” },
body: JSON.stringify({ action: “game_over”, …finishedGame })
})
.then(() => setSyncStatus(“Final game summary sent to Google Sheets”))
.catch(() => setSyncStatus(“Could not send summary — check the sync URL”));
}

setLog([]);
setNotes([]);
setPeriod(“1”);
setFlashMsg(“”);
setGameOverModalOpen(false);
setGameOverError(“”);
setGameInfo({ date: new Date().toISOString().slice(0, 10), opponent: “” });
showFlash(`Game Over — Final ${baScore}-${oppScore} saved to Game Log`);
}

function deleteGameFromHistory(id) {
setGameHistory(prev => prev.filter(g => g.id !== id));
if (expandedGameId === id) setExpandedGameId(null);
if (editingGameId === id) setEditingGameId(null);
if (deleteConfirmGameId === id) setDeleteConfirmGameId(null);
showFlash(“Game deleted”);
}

function updateGameScoreField(id, field, value) {
setGameHistory(prev =>
prev.map(g => (g.id === id ? { …g, [field]: value === “” ? “” : parseInt(value, 10) || 0 } : g))
);
}

function deleteEventFromGame(gameId, eventId) {
setGameHistory(prev =>
prev.map(g =>
g.id === gameId ? { …g, events: g.events.filter(e => e.id !== eventId) } : g
)
);
}

function toggleExpandGame(id) {
setExpandedGameId(prev => (prev === id ? null : id));
}

function toggleEditGame(id) {
setEditingGameId(prev => (prev === id ? null : id));
}

function computeGameStats(events, roster) {
const teamStatCounts = {};
TEAM_STATS.forEach(stat => {
teamStatCounts[stat] = events.filter(e => e.stat === stat).length;
});
const playerStats = (roster || [])
.map(p => {
const goals = events.filter(e => e.stat === “Goal” && e.player === p.name).length;
const assists = events.filter(e => e.stat === “Assist” && e.player === p.name).length;
const onIceGA = events.filter(e => e.stat === “On Ice For GA” && e.player === p.name).length;
const dumb = events.filter(e => e.stat === “Penalty (Dumb)” && e.player === p.name).length;
const mistake = events.filter(e => e.stat === “Penalty (Mistake)” && e.player === p.name).length;
const standard = events.filter(e => e.stat === “Penalty (Standard)” && e.player === p.name).length;
return {
name: p.name,
number: p.number,
goals,
assists,
onIceGA,
plusMinus: goals + assists – onIceGA,
dumb,
mistake,
standard,
penaltyTotal: dumb + mistake + standard
};
})
.filter(p => p.goals + p.assists + p.onIceGA + p.penaltyTotal > 0);
return { teamStatCounts, playerStats };
}

function updateRosterField(index, field, value) {
setRoster(prev => {
const copy = […prev];
copy[index] = { …copy[index], [field]: value };
return copy;
});
}

function toggleRosterActive(index) {
setRoster(prev => {
const copy = […prev];
copy[index] = { …copy[index], active: copy[index].active === false ? true : false };
return copy;
});
}

const activeRoster = roster.filter(p => p.name && p.name.trim() !== “”);
const availableRoster = activeRoster.filter(p => p.active !== false);

const periods = [“1”, “2”, “3”, “OT”];
const allStats = […TEAM_STATS];

function plusMinusFor(playerName) {
const goals = log.filter(e => e.stat === “Goal” && e.player === playerName).length;
const assists = log.filter(e => e.stat === “Assist” && e.player === playerName).length;
const onIceGA = log.filter(e => e.stat === “On Ice For GA” && e.player === playerName).length;
return { goals, assists, onIceGA, plusMinus: goals + assists – onIceGA };
}

function countFor(stat, per) {
return log.filter(e => e.stat === stat && (per === “Total” || e.period === per)).length;
}

function penaltyCountFor(severityKey, per) {
return countFor(`Penalty (${severityKey})`, per);
}

function penaltiesByPlayer() {
return activeRoster
.map(p => ({
name: p.name,
number: p.number,
dumb: log.filter(e => e.stat === “Penalty (Dumb)” && e.player === p.name).length,
mistake: log.filter(e => e.stat === “Penalty (Mistake)” && e.player === p.name).length,
standard: log.filter(e => e.stat === “Penalty (Standard)” && e.player === p.name).length
}))
.filter(p => p.dumb + p.mistake + p.standard > 0);
}

function seasonTotalsFor(playerName) {
let goals = 0, assists = 0, onIceGA = 0;
gameHistory.forEach(g => {
g.events.forEach(e => {
if (e.player !== playerName) return;
if (e.stat === “Goal”) goals += 1;
else if (e.stat === “Assist”) assists += 1;
else if (e.stat === “On Ice For GA”) onIceGA += 1;
});
});
goals += log.filter(e => e.stat === “Goal” && e.player === playerName).length;
assists += log.filter(e => e.stat === “Assist” && e.player === playerName).length;
onIceGA += log.filter(e => e.stat === “On Ice For GA” && e.player === playerName).length;
return { goals, assists, onIceGA, plusMinus: goals + assists – onIceGA };
}

function seasonPenaltyCountFor(severityKey) {
const statName = `Penalty (${severityKey})`;
let count = 0;
gameHistory.forEach(g => {
count += g.events.filter(e => e.stat === statName).length;
});
count += log.filter(e => e.stat === statName).length;
return count;
}

const seasonGamesPlayedCount = gameHistory.length + (log.length > 0 ? 1 : 0);
const finalizedGames = gameHistory.filter(g => g.finalized).sort((a, b) => {
if (a.gameDate === b.gameDate) return 0;
return a.gameDate `${e.time},${e.period},${e.player},${e.stat}`)
.join(“\n”);
const csv = header + rows;
const blob = new Blob([csv], { type: “text/csv” });
const url = URL.createObjectURL(blob);
const a = document.createElement(“a”);
a.href = url;
a.download = “game_log.csv”;
a.click();
URL.revokeObjectURL(url);
}

const navBtn = (key, label) => (

);

return (

Boston Advantage 2014 Select 1 logo

BOSTON ADVANTAGE

{gameInfo.opponent
? `vs ${gameInfo.opponent} · ${gameInfo.date}`
: “2014 SELECT 1 · GAME TRACKER”}


{navBtn(“tracker”, “Tracker”)}
{navBtn(“log”, “Game Notes”)}
{navBtn(“summary”, “Stats Summary”)}
{navBtn(“gamelog”, “Game Log”)}
{navBtn(“sync”, “Sync”)}

{flashMsg && (

✓ Logged: {flashMsg}

)}

{screen === “tracker” && (

Current Period
{periods.map(p => (

))}

Player Stats

Team Stats





)}

{screen === “log” && (

Current Game
{gameInfo.opponent ? `vs ${gameInfo.opponent}` : “No opponent set”} · {gameInfo.date}

Add a Note — P{period}

setNoteTimeRemaining(e.target.value)}
className=”w-full border rounded-lg p-3 text-sm mb-2″
/>
setNoteText(e.target.value)}
rows={3}
className=”w-full border rounded-lg p-3 text-sm mb-2″
/>

Notes ({notes.length})
{notes.length === 0 && (

No notes yet for this game.

)}
{notes.map(n => (

{n.opponent ? `vs ${n.opponent}` : “No opponent set”} · {n.gameDate} · P{n.period}
{n.timeRemaining ? ` · ${n.timeRemaining} remaining` : “”}
{n.text}
Logged {n.time}

))}

Stat Log ({log.length} events)

{log.length === 0 && (

No events logged yet. Tap stats on the Tracker tab.

)}
{log.map(entry => (

P{entry.period} — {entry.stat}
{entry.player} · {entry.time}

))}

)}

{screen === “summary” && (

Game Summary
Current Game
{gameInfo.opponent ? `vs ${gameInfo.opponent}` : “No opponent set”} · {gameInfo.date}

{allStats.map((stat, idx) => (

))}

Stat P1 P2 P3 OT Total
{stat} {countFor(stat, “1”)} {countFor(stat, “2”)} {countFor(stat, “3”)} {countFor(stat, “OT”)} {countFor(stat, “Total”)}
Player Plus / Minus (This Game)
{activeRoster.map((p, idx) => {
const { goals, assists, onIceGA, plusMinus } = plusMinusFor(p.name);
const pmColor =
plusMinus > 0
? “text-green-700”
: plusMinus < 0
? "text-red-600"
: "text-gray-700";
return (

);
})}
{activeRoster.length === 0 && (

)}

Player G A GA On-Ice +/-
{p.number ? `#${p.number} ` : “”}
{p.name}
{goals} {assists} {onIceGA} {plusMinus > 0 ? `+${plusMinus}` : plusMinus}
Add players on the Roster tab to see plus/minus.
Penalty Summary (This Game)
{PENALTY_SEVERITIES.map((sev, idx) => {
const dotColor =
sev.color === “red”
? “bg-red-600”
: sev.color === “yellow”
? “bg-yellow-400”
: “bg-green-600”;
return (

);
})}

Penalty Type P1 P2 P3 OT Total

{sev.label}
{penaltyCountFor(sev.key, “1”)} {penaltyCountFor(sev.key, “2”)} {penaltyCountFor(sev.key, “3”)} {penaltyCountFor(sev.key, “OT”)} {penaltyCountFor(sev.key, “Total”)}
Total Penalties {PENALTY_SEVERITIES.reduce((sum, sev) => sum + penaltyCountFor(sev.key, “1”), 0)} {PENALTY_SEVERITIES.reduce((sum, sev) => sum + penaltyCountFor(sev.key, “2”), 0)} {PENALTY_SEVERITIES.reduce((sum, sev) => sum + penaltyCountFor(sev.key, “3”), 0)} {PENALTY_SEVERITIES.reduce((sum, sev) => sum + penaltyCountFor(sev.key, “OT”), 0)} {PENALTY_SEVERITIES.reduce((sum, sev) => sum + penaltyCountFor(sev.key, “Total”), 0)}
Penalties By Player (This Game)
{penaltiesByPlayer().map((p, idx) => (

))}
{penaltiesByPlayer().length === 0 && (

)}

Player Dumb Mistake Standard Total
{p.number ? `#${p.number} ` : “”}
{p.name}
{p.dumb} {p.mistake} {p.standard} {p.dumb + p.mistake + p.standard}
No penalties logged yet this game.
Season Totals — All Games ({seasonGamesPlayedCount} games logged)
{activeRoster.map((p, idx) => {
const { goals, assists, onIceGA, plusMinus } = seasonTotalsFor(p.name);
const pmColor =
plusMinus > 0
? “text-green-700”
: plusMinus < 0
? "text-red-600"
: "text-gray-700";
return (

);
})}
{activeRoster.length === 0 && (

)}

Player G A GA On-Ice +/-
{p.number ? `#${p.number} ` : “”}
{p.name}
{goals} {assists} {onIceGA} {plusMinus > 0 ? `+${plusMinus}` : plusMinus}
Add players on the Roster tab to see season totals.
Team Total {activeRoster.reduce((sum, p) => sum + seasonTotalsFor(p.name).goals, 0)} {activeRoster.reduce((sum, p) => sum + seasonTotalsFor(p.name).assists, 0)} {activeRoster.reduce((sum, p) => sum + seasonTotalsFor(p.name).onIceGA, 0)} {activeRoster.reduce((sum, p) => sum + seasonTotalsFor(p.name).plusMinus, 0)}
Season Total Penalties — All Games
{PENALTY_SEVERITIES.map((sev, idx) => {
const dotColor =
sev.color === “red”
? “bg-red-600”
: sev.color === “yellow”
? “bg-yellow-400”
: “bg-green-600”;
return (

);
})}

Penalty Type Total

{sev.label}
{seasonPenaltyCountFor(sev.key)}
Total Penalties {PENALTY_SEVERITIES.reduce((sum, sev) => sum + seasonPenaltyCountFor(sev.key), 0)}

)}

{screen === “gamelog” && (

Game Log — Completed Games ({finalizedGames.length})

{finalizedGames.length === 0 && (

No completed games yet. Tap “GAME OVER — Enter Final Score” on the Tracker tab to save a game here.

)}

{finalizedGames.map(g => {
const isExpanded = expandedGameId === g.id;
const isEditing = editingGameId === g.id;
const { teamStatCounts, playerStats } = computeGameStats(g.events, g.roster || activeRoster);
return (

toggleExpandGame(g.id)}
className=”p-3 flex justify-between items-center cursor-pointer active:bg-gray-50″
>

vs {g.opponent || “Unknown”} · {g.gameDate}
Final: BA {g.baScore} — {g.oppScore} Opponent

{isExpanded ? “▲” : “▼”}

{isExpanded && (


{deleteConfirmGameId === g.id && (

Delete this game permanently? This cannot be undone.

)}

{isEditing ? (

BA Score
updateGameScoreField(g.id, “baScore”, e.target.value)}
className=”w-full border rounded-lg p-2 text-sm”
/>
Opponent Score
updateGameScoreField(g.id, “oppScore”, e.target.value)}
className=”w-full border rounded-lg p-2 text-sm”
/>

) : null}

Player Goals / Assists / Penalties
{playerStats.map((p, idx) => (

))}
{playerStats.length === 0 && (

)}

Player G A +/- Pen (D/M/S)
{p.number ? `#${p.number} ` : “”}
{p.name}
{p.goals} {p.assists} {p.plusMinus > 0 ? `+${p.plusMinus}` : p.plusMinus} {p.dumb}/{p.mistake}/{p.standard}
No player stats recorded for this game.
Team Stats — Boston Advantage vs Opponent
{TEAM_STATS.map((stat, idx) => (

))}

Stat Total
{stat} {teamStatCounts[stat]}

{isEditing && (

Logged Events ({g.events.length}) — tap ✕ to remove a mistaken entry
{g.events.map(entry => (

P{entry.period} — {entry.stat}
{entry.player}

))}
{g.events.length === 0 && (

No events left for this game.

)}

)}

{g.notes && g.notes.length > 0 && (

Notes
{g.notes.map(n => (

P{n.period}{n.timeRemaining ? ` · ${n.timeRemaining} remaining` : “”}: {n.text}

))}

)}

)}

);
})}

)}

{screen === “sync” && (

Google Sheets Sync

Paste the Google Apps Script web-app URL here. Every new stat tap will be added to the Game Log sheet.

{
setSyncUrl(e.target.value);
setSyncStatus(e.target.value.trim() ? “Ready to sync new events” : “Not connected”);
}}
placeholder=”https://script.google.com/macros/s/…/exec&#8221;
className=”w-full border rounded-lg p-3 text-sm”
/>

{syncStatus}

This app keeps its live display on your phone and sends each newly logged event to Google Sheets. Existing events are not re-sent.

)}

{showNewGameConfirm && newGameStep === “roster” && (

Set Roster for New Game

Edit names/numbers if needed, and mark each player In or Out for this game. Players marked Out won’t show up as options on the Tracker.

{roster.map((p, i) => {
const isActive = p.active !== false;
return (

updateRosterField(i, “number”, e.target.value)}
className=”w-12 text-center border rounded p-2 text-sm”
/>
updateRosterField(i, “name”, e.target.value)}
className=”flex-1 border rounded p-2 text-sm”
/>

);
})}


)}

{showNewGameConfirm && newGameStep === “details” && (

Start a new game?

The current game will be summarized in Google Sheets, then the tracker will reset to Period 1.

Game Date
setNewGameDate(e.target.value)}
className=”w-full border rounded-lg p-3 text-sm”
/>
Opponent
setNewGameOpponent(e.target.value)}
className=”w-full border rounded-lg p-3 text-sm”
/>

{newGameError && (

{newGameError}

)}


)}

{gameOverModalOpen && (

🏁 Game Over

Enter the final score. This game will be saved to the Game Log and statistics collection will end for {gameInfo.opponent ? `vs ${gameInfo.opponent}` : “this game”}.

Boston Advantage
setFinalScoreBA(e.target.value)}
className=”w-full border rounded-lg p-3 text-sm”
placeholder=”0″
/>
Opponent
setFinalScoreOpp(e.target.value)}
className=”w-full border rounded-lg p-3 text-sm”
placeholder=”0″
/>

{gameOverError && (

{gameOverError}

)}


)}

{gaModalOpen && (

Goal Against — who was on the ice?

Goalie in Net
{GOALIES.map(name => (

))}
Skaters On Ice (tap all that apply)
{availableRoster
.filter(p => !GOALIES.includes(p.name))
.map(p => {
const selected = gaOnIce.includes(p.name);
return (

);
})}

)}

{penaltyModalOpen && (

Penalty — who took it?

Player
{availableRoster.map(p => (

))}
Penalty Type
{PENALTY_SEVERITIES.map(sev => {
const selected = penaltySeverity === sev.key;
const baseClasses =
sev.color === “red”
? selected
? “bg-red-600 text-white border-red-600”
: “bg-red-50 text-red-700 border-red-600”
: sev.color === “yellow”
? selected
? “bg-yellow-400 text-blue-900 border-yellow-400”
: “bg-yellow-50 text-yellow-700 border-yellow-400”
: selected
? “bg-green-600 text-white border-green-600”
: “bg-green-50 text-green-700 border-green-600”;
return (

);
})}

)}

{playerModal && (

Who scored the {playerModal === “Goal” ? “goal” : “assist”}?

{availableRoster
.map(p => (

))}

)}

);
}

ReactDOM.createRoot(document.getElementById(“root”)).render();