Courtside MVP: engine, web app, Docker + Cloudflare Tunnel deploy, docs
Tournament engine (pools, single/double elimination, standings with proper tiebreaks, withdrawals, two-court queue with ETAs and alerts), a single-process Node server with SQLite via node:sqlite and a WebSocket live board, organizer desk, QR landing page that follows the tournament phase, Dockerfile and compose with cloudflared, and documentation for deploying and running a day. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01MB7nCCAscYsb3zzkT6LHZi
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
/* Organizer desk: live score pad. Each tap posts the running score so the public board
|
||||
shows it; "Mark final" submits the set as the result. Plain fetch, no framework. */
|
||||
(() => {
|
||||
const slug = location.pathname.split('/')[3];
|
||||
const post = (path, body) => fetch(`/admin/t/${slug}/${path}`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) }).then(async r => { const j = await r.json().catch(() => ({})); if (!r.ok) throw new Error(j.error || r.statusText); return j; });
|
||||
|
||||
document.querySelectorAll('.scorepad').forEach(pad => {
|
||||
const id = pad.dataset.match;
|
||||
const a = pad.querySelector('[data-side=a]'), b = pad.querySelector('[data-side=b]');
|
||||
let timer = null;
|
||||
const push = () => { clearTimeout(timer); timer = setTimeout(() => post('live', { match: id, a: +a.textContent, b: +b.textContent }).catch(e => alert(e.message)), 150); };
|
||||
pad.querySelectorAll('[data-op]').forEach(btn => btn.addEventListener('click', () => {
|
||||
const [side, op] = btn.dataset.op;
|
||||
const el = side === 'a' ? a : b;
|
||||
el.textContent = Math.max(0, +el.textContent + (op === '+' ? 1 : -1));
|
||||
push();
|
||||
}));
|
||||
const finalBtn = pad.nextElementSibling.querySelector('[data-final]');
|
||||
finalBtn.addEventListener('click', () => {
|
||||
const sa = +a.textContent, sb = +b.textContent;
|
||||
if (!confirm(`Final: ${sa}–${sb}?`)) return;
|
||||
post('score', { match: id, sets: `${sa}-${sb}` }).then(() => location.reload()).catch(e => alert(e.message));
|
||||
});
|
||||
});
|
||||
|
||||
// keep the desk fresh when another organizer enters a score
|
||||
const ws = new WebSocket(`${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/ws/${slug}`);
|
||||
let last = null;
|
||||
ws.onmessage = ev => {
|
||||
const s = JSON.parse(ev.data);
|
||||
const sig = JSON.stringify([s.phase, s.courts.map(c => [c.match?.id, c.match?.status, c.status]), s.upNext.map(u => u.id)]);
|
||||
if (last && sig !== last && !document.querySelector('.scorepad:hover')) location.reload();
|
||||
last = sig;
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user