A per-court signed link from the desk opens a full-screen scoring page with big +/- buttons, undo, best-of sets, and a confirmed Finalize step; it follows whatever match is on the court and can never edit a finished result. Public court cards show the estimated finish time. Release 0.4.0. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01MB7nCCAscYsb3zzkT6LHZi
38 lines
2.2 KiB
JavaScript
38 lines
2.2 KiB
JavaScript
/* 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));
|
||
});
|
||
});
|
||
|
||
document.querySelectorAll('[data-copy]').forEach(b => b.addEventListener('click', () => navigator.clipboard?.writeText(b.dataset.copy).then(() => { b.textContent = 'Copied'; })));
|
||
|
||
// 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;
|
||
};
|
||
})();
|