const USERNAME_KEY = 'scrumPoker.username'; const roomID = document.body.dataset.roomId; const params = new URLSearchParams(window.location.search); const roomSkeleton = document.getElementById('room-skeleton'); const roomGrid = document.getElementById('room-grid'); const roomTitle = document.getElementById('room-title'); const revealModeLabel = document.getElementById('reveal-mode-label'); const roundStateLabel = document.getElementById('round-state-label'); const votingBoard = document.getElementById('voting-board'); const summaryBody = document.getElementById('summary-body'); const summaryAverage = document.getElementById('summary-average'); const summaryRecommended = document.getElementById('summary-recommended'); const participantList = document.getElementById('participant-list'); const adminControls = document.getElementById('admin-controls'); const revealBtn = document.getElementById('reveal-btn'); const resetBtn = document.getElementById('reset-btn'); const participantLinkInput = document.getElementById('participant-link'); const adminLinkInput = document.getElementById('admin-link'); const roomStatus = document.getElementById('room-status'); const joinPanel = document.getElementById('join-panel'); const joinForm = document.getElementById('join-form'); const joinUsernameInput = document.getElementById('join-username'); const joinRoleInput = document.getElementById('join-role'); const joinPasswordInput = document.getElementById('join-password'); const joinAdminTokenInput = document.getElementById('join-admin-token'); const joinError = document.getElementById('join-error'); let participantID = params.get('participantId') || ''; let adminToken = params.get('adminToken') || ''; let eventSource = null; const savedUsername = localStorage.getItem(USERNAME_KEY) || ''; joinUsernameInput.value = savedUsername; joinAdminTokenInput.value = adminToken; function setJoinError(message) { if (!message) { joinError.classList.add('hidden'); joinError.textContent = ''; return; } joinError.classList.remove('hidden'); joinError.textContent = message; } function updateURL() { const next = new URL(window.location.href); if (participantID) { next.searchParams.set('participantId', participantID); } else { next.searchParams.delete('participantId'); } if (adminToken) { next.searchParams.set('adminToken', adminToken); } else { next.searchParams.delete('adminToken'); } window.history.replaceState({}, '', next.toString()); } function activateRoomView() { document.body.classList.remove('prejoin'); roomSkeleton.classList.add('hidden'); roomGrid.classList.remove('hidden'); joinPanel.classList.add('hidden'); } async function joinRoom({ username, role, password, participantIdOverride }) { const response = await fetch(`/api/rooms/${encodeURIComponent(roomID)}/join`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ participantId: participantIdOverride || participantID, username, role, password, adminToken, }), }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || 'Unable to join room.'); } participantID = data.participantId; localStorage.setItem(USERNAME_KEY, data.username); updateURL(); setJoinError(''); } function renderParticipants(participants, isRevealed) { participantList.innerHTML = ''; participants.forEach((participant) => { const item = document.createElement('li'); item.className = 'participant-item'; const name = document.createElement('span'); let label = participant.username; if (participant.id === participantID) { label += ' (You)'; } if (participant.isAdmin) { label += ' [Admin]'; } name.textContent = label; const status = document.createElement('span'); if (participant.role === 'viewer') { status.textContent = 'Viewer'; } else if (!participant.hasVoted) { status.textContent = 'Voting...'; } else if (isRevealed) { status.textContent = participant.voteValue || '-'; } else { status.textContent = participant.voteValue ? `Voted (${participant.voteValue})` : 'Voted'; } item.appendChild(name); item.appendChild(status); participantList.appendChild(item); }); } function parseNumericVote(value) { if (!/^-?\d+(\.\d+)?$/.test(value)) { return null; } return Number(value); } function calculateSummary(state) { const rows = new Map(); const numericVotes = []; state.participants.forEach((participant) => { if (participant.role !== 'participant' || !participant.hasVoted || !participant.voteValue) { return; } if (!rows.has(participant.voteValue)) { rows.set(participant.voteValue, []); } rows.get(participant.voteValue).push(participant.username); const numeric = parseNumericVote(participant.voteValue); if (numeric !== null) { numericVotes.push(numeric); } }); let average = null; if (numericVotes.length > 0) { average = numericVotes.reduce((acc, value) => acc + value, 0) / numericVotes.length; } const deckNumeric = state.cards .map(parseNumericVote) .filter((value) => value !== null) .sort((a, b) => a - b); let recommended = null; if (average !== null && deckNumeric.length > 0) { recommended = deckNumeric.find((value) => value >= average) ?? deckNumeric[deckNumeric.length - 1]; } return { rows, average, recommended }; } function renderSummary(state) { if (!state.revealed) { summaryBody.innerHTML = '