diff --git a/bun.lock b/bun.lock
index f4cddbb..df860b1 100644
--- a/bun.lock
+++ b/bun.lock
@@ -5,30 +5,11 @@
"": {
"name": "buzzer",
"dependencies": {
- "@hono/node-server": "^1.19.11",
- "hono": "^4.12.8",
- },
- "devDependencies": {
- "@types/bun": "latest",
- },
- "peerDependencies": {
- "typescript": "^5",
+ "hono": "^4.7.7",
},
},
},
"packages": {
- "@hono/node-server": ["@hono/node-server@1.19.11", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g=="],
-
- "@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="],
-
- "@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
-
- "bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="],
-
"hono": ["hono@4.12.8", "", {}, "sha512-VJCEvtrezO1IAR+kqEYnxUOoStaQPGrCmX3j4wDTNOcD1uRPFpGlwQUIW8niPuvHXaTUxeOUl5MMDGrl+tmO9A=="],
-
- "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
-
- "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
}
}
diff --git a/src/public/index.html b/src/public/index.html
index f69a957..fc58a38 100644
--- a/src/public/index.html
+++ b/src/public/index.html
@@ -1,953 +1,2168 @@
+
-
-
-BUZZER//SYS
-
-
-
-
-
-
-
BUZZER
-
// QUIZ CONTROL SYS
-
-
-
-
-
-
-
-
BUZZER
-
REAL-TIME QUIZ CONTROL SYSTEM
-
-
-
-
// HOST SESSION
-
Create a room and act as moderator. Control the buzzer, manage teams, and track scores.
-
-
-
-
// JOIN SESSION
-
Join an existing room with a room code. Enter your name and start buzzing.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
BUZZER
-
PLAYERS
-
SETTINGS
-
-
-
-
-
-
-
// BUZZ ORDER
-
No buzzes yet — open a round to begin.
-
-
-
-
-
-
-
-
-
-
-
// GAME SETTINGS
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
WAITING FOR ROUND TO OPEN
-
-
-
-
-
-
-
-
-
-
// REJOIN SESSION
-
You have a saved moderator session. Rejoin it?
-
-
-
-
-
-
-
-
-
-
-
// SET SCORE
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
// END ROOM
-
This will disconnect all players and delete the room. Are you sure?
-
-
-
-
-
-
-
-
-
-
+ function updateLiveTeamName(idx, val) {
+ if (!room) return;
+ const names = [...room.settings.teamNames];
+ names[idx] = val.trim() || 'Team ' + (idx + 1);
+ ws_send({ type: 'update_settings', settings: { teamNames: names } });
+ }
+
+ function pushSetting(key, val) {
+ ws_send({ type: 'update_settings', settings: { [key]: val } });
+ }
+
+ // ══════════════════════════════════════════════════════
+ // MOD TIMER
+ // ══════════════════════════════════════════════════════
+ function modTimerLoad() {
+ modTimerRemaining = parseInt(document.getElementById('mod-timer-set').value) || 30;
+ modTimerRunning = false;
+ clearInterval(modTimerInterval);
+ document.getElementById('btn-timer-startstop').textContent = 'START';
+ renderModTimerDisplay();
+ }
+ function modTimerReset() {
+ clearInterval(modTimerInterval);
+ modTimerRunning = false;
+ modTimerRemaining = parseInt(document.getElementById('mod-timer-set').value) || 30;
+ document.getElementById('btn-timer-startstop').textContent = 'START';
+ renderModTimerDisplay();
+ // also reset player timers via broadcast happens via room_update on open_round
+ }
+ function modTimerToggle() {
+ if (modTimerRunning) {
+ clearInterval(modTimerInterval); modTimerRunning = false;
+ document.getElementById('btn-timer-startstop').textContent = 'START';
+ } else {
+ if (modTimerRemaining <= 0) modTimerLoad();
+ modTimerRunning = true;
+ document.getElementById('btn-timer-startstop').textContent = 'PAUSE';
+ modTimerInterval = setInterval(() => {
+ modTimerRemaining--;
+ renderModTimerDisplay();
+ broadcastTimer(modTimerRemaining);
+ if (modTimerRemaining <= 0) {
+ clearInterval(modTimerInterval); modTimerRunning = false;
+ document.getElementById('btn-timer-startstop').textContent = 'START';
+ ws_send({ type: 'close_round' });
+ toast('TIME UP — round closed', 'warn');
+ }
+ }, 1000);
+ }
+ }
+ function renderModTimerDisplay() {
+ const el = document.getElementById('mod-timer-disp');
+ const s = modTimerRemaining;
+ el.textContent = Math.floor(s / 60) + ':' + (s % 60 < 10 ? '0' : '') + (s % 60);
+ el.className = 'timer-display' + (s <= 5 ? 'danger' : s <= 10 ? 'warn' : '');
+ }
+ // We broadcast timer state via a simple ws message for player display
+ function broadcastTimer(s) {
+ ws_send({ type: '__timer__', seconds: s }); // mod echoes to server — but we handle client-side only for now
+ }
+
+ // ══════════════════════════════════════════════════════
+ // PLAYER RENDER
+ // ══════════════════════════════════════════════════════
+ function renderPlayer() {
+ if (!room) return;
+ document.getElementById('p-code').textContent = room.id;
+ const me = room.players.find(p => p.id === myId);
+ document.getElementById('p-namelbl').textContent = me?.name ?? '';
+ renderTeamPicker();
+ renderPlayerBuzzer();
+ renderRoster();
+ }
+
+ function renderTeamPicker() {
+ if (!room) return;
+ const s = room.settings;
+ const picker = document.getElementById('p-team-picker');
+ if (s.mode !== 'teams' || !s.playerPickTeam || room.teamLocked) {
+ picker.style.display = 'none'; return;
+ }
+ picker.style.display = 'block';
+ const me = room.players.find(p => p.id === myId);
+ const grid = document.getElementById('p-team-grid');
+ grid.innerHTML = '';
+ for (let i = 0; i < s.numTeams; i++) {
+ const members = room.players.filter(p => p.teamIndex === i);
+ const color = COLORS[i];
+ const isMine = me?.teamIndex === i;
+ const btn = document.createElement('button');
+ btn.className = 'team-btn' + (isMine ? ' mine' : '');
+ btn.style.borderColor = isMine ? color : 'var(--border)';
+ btn.style.color = isMine ? color : 'var(--text)';
+ btn.innerHTML = `${esc(s.teamNames[i] ?? 'Team ' + (i + 1))}
${members.length} player${members.length !== 1 ? 's' : ''}
`;
+ btn.onclick = () => ws_send({ type: 'pick_team', teamIndex: i });
+ grid.appendChild(btn);
+ }
+ }
+
+ function renderPlayerBuzzer() {
+ if (!room) return;
+ const btn = document.getElementById('buzz-btn');
+ const sts = document.getElementById('buzz-status');
+ const bz = room.buzzerState;
+ const already = bz.buzzOrder.includes(myId);
+ const isFirst = bz.buzzOrder[0] === myId;
+ if (!bz.roundOpen) {
+ btn.className = 's-closed'; btn.disabled = true; sts.textContent = 'WAITING FOR ROUND';
+ } else if (isFirst) {
+ btn.className = 's-first'; btn.disabled = false; sts.textContent = '⚡ YOU BUZZED FIRST!';
+ } else if (already) {
+ const pos = bz.buzzOrder.indexOf(myId) + 1;
+ btn.className = 's-buzzed'; btn.disabled = true; sts.textContent = 'BUZZED — #' + pos + ' IN ORDER';
+ } else if (room.settings.buzzerLockout && bz.buzzOrder.length > 0) {
+ btn.className = 's-locked'; btn.disabled = true; sts.textContent = 'BUZZER LOCKED OUT';
+ } else {
+ btn.className = 's-open'; btn.disabled = false; sts.textContent = 'ROUND OPEN — BUZZ!';
+ }
+ }
+
+ function renderRoster() {
+ if (!room) return;
+ const el = document.getElementById('p-roster');
+ if (room.players.length === 0) { el.innerHTML = 'No players yet.
'; return; }
+ el.innerHTML = '';
+ room.players.forEach(p => {
+ const isMe = p.id === myId;
+ const color = p.teamIndex !== null ? COLORS[p.teamIndex] : 'var(--g)';
+ const teamName = (room.settings.mode === 'teams' && p.teamIndex !== null) ? (room.settings.teamNames[p.teamIndex] ?? '') : '';
+ const row = document.createElement('div');
+ row.className = 'roster-row' + (isMe ? ' roster-me' : '');
+ row.innerHTML = `
+
+ ${esc(p.name)}${isMe ? ' (YOU)' : ''}
+ ${teamName ? `${esc(teamName)}
` : ''}
+ `;
+ el.appendChild(row);
+ });
+ }
+
+ function addFeed(evt) {
+ const feed = document.getElementById('p-feed');
+ const isFirst = evt.buzzOrder?.[0] === evt.playerId;
+ const color = evt.teamIndex !== null ? COLORS[evt.teamIndex] : 'var(--g)';
+ const teamStr = (room?.settings.mode === 'teams' && evt.teamIndex !== null) ? ` [${esc(room.settings.teamNames[evt.teamIndex] ?? '')}]` : '';
+ const div = document.createElement('div');
+ div.className = 'feed-entry' + (isFirst ? ' first' : '');
+ div.style.borderColor = isFirst ? 'var(--yellow)' : color;
+ div.innerHTML = `${esc(evt.playerName)}${teamStr} buzzed${isFirst ? ' — FIRST!' : ''}`;
+ feed.prepend(div);
+ while (feed.children.length > 20) feed.removeChild(feed.lastChild);
+ }
+
+ function doBuzz() { ws_send({ type: 'buzz' }); }
+
+ // ══════════════════════════════════════════════════════
+ // TABS
+ // ══════════════════════════════════════════════════════
+ function setTab(name, el) {
+ document.querySelectorAll('.tab').forEach(t => t.classList.remove('on'));
+ el.classList.add('on');
+ ['buzzer', 'players', 'teams', 'settings'].forEach(t => {
+ const te = document.getElementById('tab-' + t);
+ if (te) te.style.display = t === name ? 'block' : 'none';
+ });
+ if (name === 'players') renderModPlayerList();
+ if (name === 'teams') renderModTeams();
+ }
+
+ // ══════════════════════════════════════════════════════
+ // MODALS / TOAST / UTIL
+ // ══════════════════════════════════════════════════════
+ function openModal(id) { document.getElementById(id).classList.add('on'); }
+ function closeModal(id) { document.getElementById(id).classList.remove('on'); }
+ function copyCode() {
+ if (!room) return;
+ navigator.clipboard.writeText(room.id).then(() => toast('Code copied', 'ok'));
+ }
+ function toast(msg, type = '') {
+ const el = document.createElement('div');
+ el.className = 'toast ' + type; el.textContent = msg;
+ document.getElementById('toasts').appendChild(el);
+ setTimeout(() => { el.style.transition = 'opacity .3s'; el.style.opacity = '0'; setTimeout(() => el.remove(), 300); }, 2700);
+ }
+ function esc(s) {
+ return String(s ?? '').replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"');
+ }
+
+ // ══════════════════════════════════════════════════════
+ // KEYBOARD — SPACE TO BUZZ
+ // ══════════════════════════════════════════════════════
+ document.addEventListener('keydown', e => {
+ if (role !== 'player') return;
+ if (e.code === 'Space' || e.key === ' ') {
+ const tag = document.activeElement?.tagName;
+ if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return;
+ e.preventDefault();
+ const btn = document.getElementById('buzz-btn');
+ if (!btn.disabled) { btn.click(); btn.style.transform = 'scale(.93)'; setTimeout(() => btn.style.transform = '', 120); }
+ }
+ });
+
+ // close modals on backdrop click
+ document.querySelectorAll('.modal-bg').forEach(bg => {
+ bg.addEventListener('click', e => { if (e.target === bg) bg.classList.remove('on'); });
+ });
+
+ // ══════════════════════════════════════════════════════
+ // INIT
+ // ══════════════════════════════════════════════════════
+ window.addEventListener('DOMContentLoaded', () => {
+ const m = loadMod();
+ if (m) { document.getElementById('rejoin-bar').style.display = 'block'; document.getElementById('m-rejoin-code').textContent = m.id; }
+ showScr('s-land');
+ connect(null);
+ // init seg-mode display
+ segSelect('seg-mode', document.querySelector('#seg-mode .seg-opt.active'));
+ });
+
-
+
+