Files
courtside/server/public/admin.js
T
bkvargyasandClaude Fable 5.1 eb58eb9ad9 Courtside Manager score keeper page per court; Phase below Courts on the desk; estimated finish on court cards
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
2026-09-04 02:10:40 +00:00

38 lines
2.2 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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;
};
})();