Optional age on signup and the desk. Auto-generated brackets pair neighbours by age with byes spread down the sheet; teams without an age are placed at random, fully random when no ages exist. Release 0.6.0. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01MB7nCCAscYsb3zzkT6LHZi
63 lines
4.8 KiB
JavaScript
63 lines
4.8 KiB
JavaScript
import { esc } from '../http.js';
|
|
import { layout, flash } from './layout.js';
|
|
|
|
export function homePage(list) {
|
|
const rows = list.length
|
|
? list.map(x => `<a class="row" href="/t/${esc(x.slug)}"><span class="pill ${esc(x.phase)}">${esc(phaseLabel(x.phase))}</span><b>${esc(x.name)}</b><span class="muted">${esc(x.date ?? '')} · ${x.teams} teams</span></a>`).join('')
|
|
: `<p class="muted">No tournaments yet. An organizer creates one from the <a href="/admin">desk</a>.</p>`;
|
|
return layout({ title: 'Tournaments', body: `<h1>Tournaments</h1><div class="list">${rows}</div>` });
|
|
}
|
|
|
|
export const phaseLabel = p => ({ checkin: 'Registration open', closed: 'Registration closed', live: 'Live', final: 'Final' }[p] ?? p);
|
|
|
|
/** Registration form, shown while phase = checkin. */
|
|
export function registerPage(state, { error = null, values = {} } = {}) {
|
|
const r = state.rules;
|
|
const body = `
|
|
<section class="hero">
|
|
<div class="eyebrow">${esc(state.date ?? '')}</div>
|
|
<h1>${esc(state.name)}</h1>
|
|
<p class="lede">${esc(r.teamSize)}s · ${esc(r.pointsTo)} points, win by ${esc(r.winBy)}${r.cap ? `, cap ${esc(r.cap)}` : ''} · ${state.teams.length} team${state.teams.length === 1 ? '' : 's'} registered</p>
|
|
${state.notes ? `<p class="notes">${esc(state.notes)}</p>` : ''}
|
|
</section>
|
|
${flash(error, 'error')}
|
|
<form class="card form" method="post" action="/t/${esc(state.slug)}/register">
|
|
<h2>Register your team</h2>
|
|
<label>Team name <input name="name" required maxlength="40" autocomplete="off" value="${esc(values.name ?? '')}" placeholder="Net Gains"></label>
|
|
<label>Captain's name <input name="captain" required maxlength="60" value="${esc(values.captain ?? '')}"></label>
|
|
<label>Captain's mobile <input name="phone" type="tel" inputmode="tel" maxlength="30" value="${esc(values.phone ?? '')}" placeholder="optional, for organizer contact"></label>
|
|
<label>Number of players <input name="players" type="number" inputmode="numeric" min="${esc(r.teamSize)}" max="20" required value="${esc(values.players ?? r.teamSize)}"></label>
|
|
<label>Average age of your players <input name="avgAge" type="number" inputmode="numeric" min="5" max="99" value="${esc(values.avgAge ?? '')}" placeholder="e.g. 34"><span class="hint">Optional. We use this to match teams of similar age groups against each other in the first round, for the best play. Teams that skip it are placed at random. Not shown publicly.</span></label>
|
|
<label>Player names <textarea name="playerNames" rows="3" placeholder="one per line (optional)">${esc(values.playerNames ?? '')}</textarea></label>
|
|
<button class="primary" type="submit">Register</button>
|
|
<p class="muted small">After you register you'll get a private team link. Keep this page's QR code handy: once play starts it becomes the live board.</p>
|
|
</form>
|
|
<section class="card">
|
|
<h2>Registered so far</h2>
|
|
${state.teams.length ? `<ol class="plain">${state.teams.map(t => `<li>${esc(t.name)} <span class="muted">(${t.players})</span></li>`).join('')}</ol>` : '<p class="muted">Be the first.</p>'}
|
|
</section>`;
|
|
return layout({ title: state.name, body });
|
|
}
|
|
|
|
export function registeredPage(state, team, origin) {
|
|
const link = `${origin}/t/${state.slug}/team/${team.code}`;
|
|
const body = `
|
|
<section class="hero"><div class="eyebrow">You're in</div><h1>${esc(team.name)}</h1><p class="lede">${team.players} players · captain ${esc(team.captain ?? '')}</p></section>
|
|
<section class="card">
|
|
<h2>Your team link</h2>
|
|
<p>This page will show your next match, which court, and your record. Bookmark it or add it to your home screen.</p>
|
|
<p class="linkbox"><a href="${esc(link)}">${esc(link)}</a></p>
|
|
<p><button class="primary" type="button" data-copy="${esc(link)}">Copy link</button> <a class="button" href="sms:?&body=${encodeURIComponent(`${team.name} team page: ${link}`)}">Text it to myself</a></p>
|
|
<p class="muted small">Team code: <b class="mono">${esc(team.code)}</b>. Anyone with the code can see your team page; nobody can change scores from it.</p>
|
|
</section>
|
|
<p><a href="/t/${esc(state.slug)}">Back to the tournament</a></p>
|
|
<script>document.querySelector('[data-copy]')?.addEventListener('click',e=>{navigator.clipboard?.writeText(e.target.dataset.copy).then(()=>{e.target.textContent='Copied'})});</script>`;
|
|
return layout({ title: `${team.name} registered`, body });
|
|
}
|
|
|
|
/** Everything after check-in: closed, live and final are all rendered client-side from the state JSON. */
|
|
export function boardPage(state, { teamId = null, display = false } = {}) {
|
|
const body = `<div id="app" class="app" data-team="${esc(teamId ?? '')}" data-display="${display ? '1' : ''}"><noscript>This page needs JavaScript to show live scores.</noscript><div class="loading">Loading ${esc(state.name)}…</div></div>`;
|
|
return layout({ title: state.name, body, state, script: '/static/board.js', bodyClass: display ? 'display' : '' });
|
|
}
|