const USERNAME_KEY = 'scrumPoker.username'; const SCALE_PRESETS = { fibonacci: ['0', '1', '2', '3', '5', '8', '13', '21', '?'], tshirt: ['XS', 'S', 'M', 'L', 'XL', '?'], 'powers-of-two': ['1', '2', '4', '8', '16', '32', '?'], }; const themeToggleBtn = document.getElementById('theme-toggle'); const roomConfigForm = document.getElementById('room-config-form'); const statusLine = document.getElementById('config-status'); const scaleSelect = document.getElementById('estimation-scale'); const maxPeopleInput = document.getElementById('max-people'); const previewScale = document.getElementById('preview-scale'); const previewMaxPeople = document.getElementById('preview-max-people'); const previewCards = document.getElementById('preview-cards'); const customCardInput = document.getElementById('custom-card'); const addCardButton = document.getElementById('add-card'); const usernameInput = document.getElementById('username'); let isDarkMode = false; let nextCardID = 1; let currentCards = []; let draggingCardID = ''; const savedUsername = localStorage.getItem(USERNAME_KEY); if (savedUsername && !usernameInput.value) { usernameInput.value = savedUsername; } themeToggleBtn.addEventListener('click', () => { isDarkMode = !isDarkMode; if (isDarkMode) { document.documentElement.setAttribute('data-theme', 'dark'); themeToggleBtn.textContent = 'Light Mode'; return; } document.documentElement.removeAttribute('data-theme'); themeToggleBtn.textContent = 'Dark Mode'; }); function createCard(value) { return { id: String(nextCardID++), value: value.toString() }; } function getCardsForScale(scale) { return (SCALE_PRESETS[scale] || SCALE_PRESETS.fibonacci).map(createCard); } function captureCardPositions() { const positions = new Map(); previewCards.querySelectorAll('.preview-card').forEach((el) => { positions.set(el.dataset.cardId, el.getBoundingClientRect()); }); return positions; } function animateReflow(previousPositions) { previewCards.querySelectorAll('.preview-card').forEach((el) => { const previousRect = previousPositions.get(el.dataset.cardId); if (!previousRect) { return; } const nextRect = el.getBoundingClientRect(); const deltaX = previousRect.left - nextRect.left; const deltaY = previousRect.top - nextRect.top; if (!deltaX && !deltaY) { return; } el.style.transform = `translate(${deltaX}px, ${deltaY}px)`; requestAnimationFrame(() => { el.style.transform = 'translate(0, 0)'; }); }); } function removeCard(cardID) { const cardEl = previewCards.querySelector(`[data-card-id="${cardID}"]`); if (!cardEl) { return; } cardEl.classList.add('is-removing'); cardEl.addEventListener('animationend', () => { const previousPositions = captureCardPositions(); currentCards = currentCards.filter((card) => card.id !== cardID); renderCards(previousPositions); }, { once: true }); } function reorderCard(fromID, toID) { if (fromID === toID) { return; } const fromIndex = currentCards.findIndex((card) => card.id === fromID); const toIndex = currentCards.findIndex((card) => card.id === toID); if (fromIndex < 0 || toIndex < 0) { return; } const previousPositions = captureCardPositions(); const [moved] = currentCards.splice(fromIndex, 1); currentCards.splice(toIndex, 0, moved); renderCards(previousPositions); } function buildCardElement(card) { const cardEl = document.createElement('div'); cardEl.className = 'preview-card'; cardEl.dataset.cardId = card.id; cardEl.textContent = card.value; cardEl.draggable = true; const removeBtn = document.createElement('button'); removeBtn.type = 'button'; removeBtn.className = 'preview-card-remove'; removeBtn.textContent = 'X'; removeBtn.setAttribute('aria-label', `Remove ${card.value}`); removeBtn.addEventListener('click', (event) => { event.stopPropagation(); removeCard(card.id); }); cardEl.addEventListener('dragstart', () => { draggingCardID = card.id; cardEl.classList.add('dragging'); }); cardEl.addEventListener('dragend', () => { draggingCardID = ''; cardEl.classList.remove('dragging'); }); cardEl.addEventListener('dragover', (event) => { event.preventDefault(); }); cardEl.addEventListener('drop', (event) => { event.preventDefault(); if (!draggingCardID) { return; } reorderCard(draggingCardID, card.id); }); cardEl.appendChild(removeBtn); return cardEl; } function renderCards(previousPositions = new Map()) { previewCards.innerHTML = ''; currentCards.forEach((card) => { previewCards.appendChild(buildCardElement(card)); }); animateReflow(previousPositions); } function resetCardsForScale() { const previousPositions = captureCardPositions(); currentCards = getCardsForScale(scaleSelect.value); renderCards(previousPositions); } function updatePreviewMeta() { previewScale.textContent = `Scale: ${scaleSelect.value}`; previewMaxPeople.textContent = `Max: ${maxPeopleInput.value || 0}`; } addCardButton.addEventListener('click', () => { const value = customCardInput.value.trim(); if (!value) { return; } const previousPositions = captureCardPositions(); currentCards.push(createCard(value.slice(0, 8))); renderCards(previousPositions); customCardInput.value = ''; customCardInput.focus(); }); customCardInput.addEventListener('keydown', (event) => { if (event.key === 'Enter') { event.preventDefault(); addCardButton.click(); } }); scaleSelect.addEventListener('change', () => { resetCardsForScale(); updatePreviewMeta(); statusLine.textContent = 'Card deck reset to selected estimation scale.'; }); maxPeopleInput.addEventListener('input', updatePreviewMeta); roomConfigForm.addEventListener('submit', async (event) => { event.preventDefault(); const formData = new FormData(roomConfigForm); const username = (formData.get('username') || '').toString().trim(); const roomName = (formData.get('roomName') || '').toString().trim(); if (!username || !roomName) { statusLine.textContent = 'Room name and username are required.'; return; } localStorage.setItem(USERNAME_KEY, username); const payload = { roomName, creatorUsername: username, maxPeople: Number(formData.get('maxPeople') || 50), cards: currentCards.map((card) => card.value), allowSpectators: Boolean(formData.get('allowSpectators')), anonymousVoting: Boolean(formData.get('anonymousVoting')), autoReset: Boolean(formData.get('autoReset')), revealMode: (formData.get('revealMode') || 'manual').toString(), votingTimeoutSec: Number(formData.get('votingTimeoutSec') || 0), password: (formData.get('password') || '').toString(), }; statusLine.textContent = 'Creating room...'; try { const response = await fetch('/api/rooms', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); const data = await response.json(); if (!response.ok) { statusLine.textContent = data.error || 'Failed to create room.'; return; } const target = `/room/${encodeURIComponent(data.roomId)}?participantId=${encodeURIComponent(data.creatorParticipantId)}&adminToken=${encodeURIComponent(data.adminToken)}`; window.location.assign(target); } catch (err) { statusLine.textContent = 'Network error while creating room.'; } }); roomConfigForm.addEventListener('reset', () => { window.setTimeout(() => { updatePreviewMeta(); resetCardsForScale(); statusLine.textContent = 'Room settings reset to defaults.'; }, 0); }); updatePreviewMeta(); resetCardsForScale();