Files
courtside/server/public/admin.js
T
bkvargyasandClaude Fable 5.1 a4bee22d96 Fix double elimination with byes never finishing; hand-built brackets
Byes now cascade through the whole bracket (a slot nobody can fill is a bye or
a void), re-run on server start so stuck tournaments recover. New manual
bracket builder on the desk: pick round-1 pairings per match. Tests at 5-23
teams for both bracket types. Release 0.5.0.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MB7nCCAscYsb3zzkT6LHZi
2026-09-05 15:33:36 +00:00

49 lines
2.8 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'; })));
// manual bracket builder: highlight a team picked twice before the form is submitted
const pairsForm = document.querySelector('.manualform');
if (pairsForm) {
const check = () => {
const seen = new Map();
pairsForm.querySelectorAll('select[name^=a],select[name^=b]').forEach(sel => { if (sel.value) seen.set(sel.value, (seen.get(sel.value) ?? 0) + 1); });
pairsForm.querySelectorAll('tr').forEach(tr => tr.classList.toggle('dup', [...tr.querySelectorAll('select')].some(sel => sel.value && seen.get(sel.value) > 1)));
};
pairsForm.addEventListener('change', check); check();
}
// 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;
};
})();