Files
427e7578-d7bf-49c8-aee9-2dd…/PollingApp.html
2026-03-18 17:41:37 +01:00

201 lines
5.6 KiB
HTML
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>PeertoPeer Poll up to 14 users (no vote loops)</title>
<style>
body {font-family:Arial,Helvetica,sans-serif; margin:20px;}
#options li {margin:5px 0;}
button {margin-left:5px;}
</style>
</head>
<body>
<h2>PeertoPeer Poll (max14 participants)</h2>
<div id="setup">
<label>Your Peer ID: <input id="myId" readonly size="30"></label><br><br>
<label>Room host ID (leave empty if you are the first client):
<input id="hostId" placeholder="host peer id">
</label>
<button id="joinBtn">Join / Create Room</button>
<p id="status"></p>
</div>
<hr>
<h3>Poll</h3>
<ul id="options"></ul>
<input id="newOption" placeholder="New option text">
<button id="addOptionBtn">Add option</button>
<script src="https://unpkg.com/peerjs@1.5.4/dist/peerjs.min.js"></script>
<script>
/* ---------- 1. Initialise PeerJS ---------- */
const peer = new Peer(); // public signalling server
const myIdInput = document.getElementById('myId');
const statusEl = document.getElementById('status');
let isHost = false; // true for the first client
let connections = []; // all open DataConnections
let knownPeers = new Set(); // IDs of every peer we know (including host)
peer.on('open', id => {
myIdInput.value = id;
statusEl.textContent = 'Enter a host ID (or leave empty) and click Join.';
});
/* ---------- 2. Accept incoming connections (host side) ---------- */
peer.on('connection', incoming => {
registerConnection(incoming);
});
/* ---------- 3. Join / create a room ---------- */
document.getElementById('joinBtn').onclick = () => {
const hostId = document.getElementById('hostId').value.trim();
if (hostId) {
isHost = false;
connectToPeer(hostId);
statusEl.textContent = `Connecting to host ${hostId}`;
} else {
isHost = true;
knownPeers.add(peer.id);
statusEl.textContent = 'Room created share this Peer ID with others.';
}
};
/* ---------- 4. Helper: connect to a remote peer ---------- */
function connectToPeer(peerId) {
if (knownPeers.has(peerId)) return;
const conn = peer.connect(peerId);
registerConnection(conn);
}
/* ---------- 5. Register a DataConnection ---------- */
function registerConnection(conn) {
if (!conn) return;
connections.push(conn);
knownPeers.add(conn.peer);
conn.on('open', () => {
// 1⃣ Send full poll state
conn.send({type: 'full', payload: poll, msgId: crypto.randomUUID()});
// 2⃣ If we are the host, tell the newcomer about other peers
if (isHost) {
const others = Array.from(knownPeers).filter(id => id !== conn.peer && id !== peer.id);
if (others.length) conn.send({type: 'peer-list', payload: others, msgId: crypto.randomUUID()});
}
});
conn.on('data', data => handleMessage(data, conn.peer));
conn.on('close', () => {
connections = connections.filter(c => c !== conn);
knownPeers.delete(conn.peer);
});
}
/* ---------- 6. Shared poll state ---------- */
let poll = {options: []}; // each option: {id, text, votes}
/* ---------- 7. Track my own votes ---------- */
const myVotes = new Set(); // option.id values
/* ---------- 8. Remember which messages we already processed ---------- */
const seenMsgIds = new Set();
/* ---------- 9. Broadcast helper (skip the sender) ---------- */
function broadcast(msg, except = null) {
connections.forEach(c => {
if (c.open && c.peer !== except) c.send(msg);
});
}
/* ---------- 10. Message handling ---------- */
function handleMessage(msg, senderId) {
// Ignore duplicates
if (seenMsgIds.has(msg.msgId)) return;
seenMsgIds.add(msg.msgId);
switch (msg.type) {
case 'full':
poll = msg.payload;
render();
break;
case 'add':
poll.options.push(msg.payload);
render();
broadcast(msg, senderId); // forward once
break;
case 'vote':
const opt = poll.options.find(o => o.id === msg.payload.id);
if (opt) opt.votes++;
render();
broadcast(msg, senderId);
break;
case 'peer-list':
msg.payload.forEach(id => {
if (id !== peer.id && !knownPeers.has(id)) connectToPeer(id);
});
break;
}
}
/* ---------- 11. UI rendering ---------- */
function render() {
const ul = document.getElementById('options');
ul.innerHTML = '';
poll.options.forEach(opt => {
const li = document.createElement('li');
li.textContent = `${opt.text} ${opt.votes} vote(s)`;
const btn = document.createElement('button');
btn.textContent = myVotes.has(opt.id) ? 'Voted' : 'Vote';
btn.disabled = myVotes.has(opt.id);
btn.onclick = () => {
// Record locally prevents doubleclick on the same client
myVotes.add(opt.id);
btn.textContent = 'Voted';
btn.disabled = true;
const voteMsg = {
type: 'vote',
payload: {id: opt.id},
msgId: crypto.randomUUID()
};
// Apply locally first
opt.votes++;
render();
// Send to all peers
broadcast(voteMsg);
};
li.appendChild(btn);
ul.appendChild(li);
});
}
/* ---------- 12. Add new option ---------- */
document.getElementById('addOptionBtn').onclick = () => {
const txt = document.getElementById('newOption').value.trim();
if (!txt) return;
const option = {id: crypto.randomUUID(), text: txt, votes: 0};
poll.options.push(option);
render();
const addMsg = {
type: 'add',
payload: option,
msgId: crypto.randomUUID()
};
broadcast(addMsg);
document.getElementById('newOption').value = '';
};
</script>
</body>
</html>