Add initial implementation of P2P Poll app with HTML, CSS, and JavaScript
This commit is contained in:
200
app.js
Normal file
200
app.js
Normal file
@@ -0,0 +1,200 @@
|
||||
// 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);
|
||||
}
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user