200 lines
6.0 KiB
JavaScript
200 lines
6.0 KiB
JavaScript
// 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 = '<p>No options yet. Add one below.</p>';
|
|
return;
|
|
}
|
|
keys.forEach((optId) => {
|
|
const opt = options[optId];
|
|
const el = document.createElement('div');
|
|
el.className = 'option';
|
|
el.innerHTML = `
|
|
<span class="text">${opt.text}</span>
|
|
<button data-optid="${optId}" class="voteBtn">Vote (${opt.votes || 0})</button>
|
|
`;
|
|
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);
|
|
}
|
|
|
|
})(); |