import * as Y from 'yjs'; import { WebrtcProvider } from 'y-webrtc'; // --- Peer ID (stable across reloads) --- function getOrCreatePeerId() { let id = localStorage.getItem('peer-id'); if (!id) { id = crypto.randomUUID(); localStorage.setItem('peer-id', id); } return id; } const peerId = getOrCreatePeerId(); // --- Room name from URL --- function getRoomName() { const params = new URLSearchParams(window.location.search); return params.get('room') || 'default-poll'; } const roomName = getRoomName(); // --- Yjs setup --- const ydoc = new Y.Doc(); const provider = new WebrtcProvider(roomName, ydoc); const yOptions = ydoc.getMap('poll-options'); // --- Data operations --- function addOption(name) { const id = crypto.randomUUID(); const optionMap = new Y.Map(); optionMap.set('name', name); optionMap.set('votes', new Y.Map()); yOptions.set(id, optionMap); } function toggleVote(optionId) { const optionMap = yOptions.get(optionId); if (!optionMap) return; const votes = optionMap.get('votes'); if (votes.has(peerId)) { votes.delete(peerId); } else { votes.set(peerId, true); } } // --- UI rendering --- const container = document.getElementById('poll-options'); const emptyState = document.getElementById('empty-state'); function render() { container.innerHTML = ''; const entries = []; yOptions.forEach((optionMap, id) => { entries.push({ id, name: optionMap.get('name'), votes: optionMap.get('votes').size, voted: optionMap.get('votes').has(peerId), }); }); entries.sort((a, b) => b.votes - a.votes || a.name.localeCompare(b.name)); emptyState.className = entries.length > 0 ? 'hidden' : ''; for (const entry of entries) { const div = document.createElement('div'); div.className = 'poll-option'; const nameSpan = document.createElement('span'); nameSpan.className = 'option-name'; nameSpan.textContent = entry.name; const voteCount = document.createElement('span'); voteCount.className = 'vote-count'; voteCount.textContent = entry.votes; const voteBtn = document.createElement('button'); voteBtn.className = entry.voted ? 'vote-btn voted' : 'vote-btn'; voteBtn.textContent = entry.voted ? 'Unvote' : 'Vote'; voteBtn.addEventListener('click', () => toggleVote(entry.id)); div.append(nameSpan, voteCount, voteBtn); container.appendChild(div); } } // Re-render on any change in the shared state yOptions.observeDeep(() => render()); // --- Add option handlers --- const input = document.getElementById('option-input'); const addBtn = document.getElementById('add-btn'); addBtn.addEventListener('click', () => { const name = input.value.trim(); if (name) { addOption(name); input.value = ''; } }); input.addEventListener('keydown', (e) => { if (e.key === 'Enter') addBtn.click(); }); // --- Connection status & peer count --- const statusEl = document.getElementById('connection-status'); const peerCountEl = document.getElementById('peer-count'); provider.on('synced', ({ synced }) => { statusEl.textContent = synced ? 'Connected' : 'Connecting...'; statusEl.className = synced ? 'connected' : 'connecting'; }); function updatePeerCount() { const count = provider.awareness.getStates().size; peerCountEl.textContent = `${count} peer${count !== 1 ? 's' : ''}`; } provider.awareness.on('change', updatePeerCount); updatePeerCount(); // --- Initial render --- render();