// Minimal GUN-based P2P Poll scaffold (vanilla JS) (function() { // Initialize GUN (no server needed; can optionally connect to a relay server) // If you want a local-only node for prototyping: Gun() // If you want to use a signaling server for discovery, pass a list of peers to Gun().opt({peers: [...]}) const gun = Gun(); // DOM helpers const pollSection = document.getElementById('poll'); const pollIdInput = document.getElementById('pollId'); const loadPollBtn = document.getElementById('loadPollBtn'); const createPollBtn = document.getElementById('createPollBtn'); const pollTitleInput = document.getElementById('pollTitle'); const setTitleBtn = document.getElementById('setTitleBtn'); const optionsDiv = document.getElementById('options'); const newOptionInput = document.getElementById('newOption'); const addOptionBtn = document.getElementById('addOptionBtn'); const totalVotesSpan = document.getElementById('totalVotes'); const pollIdDisplay = document.getElementById('pollIdDisplay'); let currentPollId = null; let pollsRef = null; // Gun node for /polls/{pollId} let pollData = null; // local cache // Helpers function renderOptionList(options) { optionsDiv.innerHTML = ''; const keys = Object.keys(options || {}); if (keys.length === 0) { optionsDiv.innerHTML = '
No options yet. Add one below.
'; return; } keys.forEach((optId) => { const opt = options[optId]; const el = document.createElement('div'); el.className = 'option'; el.innerHTML = ` ${opt.text} `; optionsDiv.appendChild(el); }); } function updateTotalVotes() { const opts = pollData && pollData.options ? pollData.options : {}; const total = Object.values(opts).reduce((sum, o) => sum + (o.votes || 0), 0); totalVotesSpan.textContent = total; } // Load existing poll and subscribe to changes function loadPoll(pollId) { if (!pollId) return; currentPollId = pollId; pollIdDisplay.textContent = pollId; pollsRef = gun.get('polls').get(pollId); // Initialize structure if new poll pollsRef.get('title').put('Untitled Poll'); pollsRef.get('options').put({}); // ensure map exists // Real-time updates: listen to changes pollsRef.get('title').on((title) => { pollTitleInput.value = title || ''; }); pollsRef.on((data) => { // data is the entire poll node if subscribed; but we explore explicit fields }); // For options map pollsRef.get('options').on((opts) => { // opts is the entire options map; convert to plain object const optionsObj = toPlainObject(opts); pollData = { title: null, options: optionsObj }; renderOptionList(optionsObj); updateTotalVotes(); }); // Also listen for title changes separately (in case some clients push via title path) pollsRef.get('title').on((title) => { pollTitleInput.value = title || ''; }); // Show UI document.getElementById('setup').style.display = 'none'; pollSection.style.display = 'block'; } // Create a new poll function createPoll() { const pollId = pollIdInput.value.trim(); if (!pollId) { alert('Please enter a poll ID to create.'); return; } currentPollId = pollId; // Initialize poll pollsRef = gun.get('polls').get(pollId); pollsRef.put({ title: 'New Poll', }); pollsRef.get('options').put({}); // ensure map exists // reflect in UI pollIdDisplay.textContent = pollId; pollTitleInput.value = 'New Poll'; pollSection.style.display = 'block'; document.getElementById('setup').style.display = 'none'; } // Add option function addOption() { if (!pollsRef) { alert('Load or create a poll first.'); return; } const text = newOptionInput.value.trim(); if (!text) return; const optionId = 'opt_' + Date.now(); pollsRef.get('options').get(optionId).put({ text, votes: 0 }); newOptionInput.value = ''; } // Set title function setTitle() { if (!pollsRef) return; const title = pollTitleInput.value.trim() || 'Untitled'; pollsRef.get('title').put(title); } // Vote handler (delegated) function handleVoteClick(e) { if (e.target.classList.contains('voteBtn')) { const optId = e.target.getAttribute('data-optid'); if (!optId || !pollsRef) return; const optRef = pollsRef.get('options').get(optId); // Increment votes locally optRef.get('votes').once((v) => { const current = v || 0; optRef.put({ votes: current + 1 }); }); // UI will update via the on() listener on options map } } // Utilities function toPlainObject(obj) { // Gun emits special proxy-like objects; convert to plain object if (!obj) return {}; // If it's already a plain object, return if (typeof obj !== 'object') return {}; // Gun events deliver object-like; attempt shallow copy try { const copy = JSON.parse(JSON.stringify(obj)); return copy; } catch (e) { // Fallback: return as-is if cannot stringify return obj; } } // Event bindings loadPollBtn.addEventListener('click', () => { const id = pollIdInput.value.trim(); if (!id) { alert('Enter a Poll ID to load.'); return; } loadPoll(id); }); createPollBtn.addEventListener('click', () => { createPoll(); }); addOptionBtn.addEventListener('click', addOption); newOptionInput.addEventListener('keydown', (ev) => { if (ev.key === 'Enter') addOption(); }); setTitleBtn.addEventListener('click', setTitle); // Delegate vote clicks optionsDiv.addEventListener('click', handleVoteClick); // Initialize: optionally auto-create a poll if URL param present // Example: ?pollId=my-poll-123 const urlParams = new URLSearchParams(window.location.search); const autoPoll = urlParams.get('pollId'); if (autoPoll) { pollIdInput.value = autoPoll; loadPoll(autoPoll); } })();