class PollManager { constructor(peerManager) { this.peerManager = peerManager; this.currentPoll = null; this.availablePolls = new Map(); // pollId -> poll this.myVotes = new Set(); this.onPollUpdated = null; this.onPollCreated = null; this.onPollSelected = null; this.onPollsListUpdated = null; } createPoll(question, options) { const poll = { id: this.generatePollId(), question: question.trim(), options: options.map((text, index) => ({ id: `opt-${index}`, text: text.trim(), votes: 0, voters: [] })), createdBy: this.peerManager.getPeerId(), createdAt: Date.now() }; // Clear any existing votes when creating a new poll this.myVotes.clear(); this.currentPoll = poll; this.availablePolls.set(poll.id, poll); this.saveToLocalStorage(); if (this.onPollCreated) { this.onPollCreated(poll); } if (this.onPollsListUpdated) { this.onPollsListUpdated(Array.from(this.availablePolls.values())); } // Broadcast new poll to all peers this.broadcastPollUpdate(); return poll; } addOption(text) { if (!this.currentPoll) { throw new Error('No active poll'); } const newOption = { id: `opt-${Date.now()}`, text: text.trim(), votes: 0, voters: [] }; this.currentPoll.options.push(newOption); this.saveToLocalStorage(); this.notifyPollUpdated(); this.broadcastPollUpdate(); return newOption; } vote(optionId) { if (!this.currentPoll) { throw new Error('No active poll'); } const myPeerId = this.peerManager.getPeerId(); // Check if already voted for this option if (this.myVotes.has(optionId)) { return false; } this.removePreviousVote(myPeerId); // Add new vote const option = this.currentPoll.options.find(opt => opt.id === optionId); if (!option) { throw new Error('Option not found'); } option.votes++; option.voters.push(myPeerId); this.myVotes.add(optionId); this.saveToLocalStorage(); this.notifyPollUpdated(); this.broadcastVote(optionId, myPeerId); return true; } removePreviousVote(peerId) { if (!this.currentPoll) return; this.currentPoll.options.forEach(option => { const voterIndex = option.voters.indexOf(peerId); if (voterIndex !== -1) { option.votes--; option.voters.splice(voterIndex, 1); // Remove from my votes if it's my vote if (peerId === this.peerManager.getPeerId()) { this.myVotes.delete(option.id); } } }); } resetPoll() { if (!this.currentPoll) { throw new Error('No active poll'); } this.currentPoll.options.forEach(option => { option.votes = 0; option.voters = []; }); this.myVotes.clear(); this.saveToLocalStorage(); this.notifyPollUpdated(); this.broadcastPollReset(); } syncPoll(pollData) { if (!this.availablePolls.has(pollData.id)) { this.availablePolls.set(pollData.id, pollData); this.saveToLocalStorage(); if (this.onPollsListUpdated) { this.onPollsListUpdated(Array.from(this.availablePolls.values())); } } if (!this.currentPoll || this.currentPoll.id === pollData.id) { this.currentPoll = pollData; this.availablePolls.set(pollData.id, pollData); this.validateMyVotes(pollData); this.saveToLocalStorage(); if (this.onPollCreated) { this.onPollCreated(pollData); } } else if (this.currentPoll.id === pollData.id) { this.mergePollData(pollData); this.availablePolls.set(pollData.id, this.currentPoll); this.validateMyVotes(this.currentPoll); this.saveToLocalStorage(); this.notifyPollUpdated(); } } mergePollData(remotePoll) { const localOptions = new Map(this.currentPoll.options.map(opt => [opt.id, opt])); const remoteOptions = new Map(remotePoll.options.map(opt => [opt.id, opt])); remoteOptions.forEach((remoteOpt, id) => { if (!localOptions.has(id)) { this.currentPoll.options.push(remoteOpt); localOptions.set(id, remoteOpt); } }); this.currentPoll.options.forEach(localOpt => { const remoteOpt = remoteOptions.get(localOpt.id); if (remoteOpt) { const allVoters = new Set([...localOpt.voters, ...remoteOpt.voters]); localOpt.voters = Array.from(allVoters); localOpt.votes = localOpt.voters.length; } }); } handleVoteMessage(optionId, voterId) { if (!this.currentPoll) return; this.removePreviousVote(voterId); const option = this.currentPoll.options.find(opt => opt.id === optionId); if (option && !option.voters.includes(voterId)) { option.votes++; option.voters.push(voterId); // Update my votes if this is my vote if (voterId === this.peerManager.getPeerId()) { this.myVotes.add(optionId); } this.saveToLocalStorage(); this.notifyPollUpdated(); } } handleUnvoteMessage(voterId) { if (!this.currentPoll) return; // Remove vote from this voter this.removePreviousVote(voterId); // Update my votes if this is my vote if (voterId === this.peerManager.getPeerId()) { this.myVotes.clear(); } this.saveToLocalStorage(); this.notifyPollUpdated(); } handlePollReset() { if (!this.currentPoll) return; this.currentPoll.options.forEach(option => { option.votes = 0; option.voters = []; }); this.myVotes.clear(); this.saveToLocalStorage(); this.notifyPollUpdated(); } broadcastPollUpdate() { if (!this.currentPoll) return; this.peerManager.broadcastMessage({ type: 'poll_update', poll: this.currentPoll, senderId: this.peerManager.getPeerId() }); } broadcastVote(optionId, voterId) { this.peerManager.broadcastMessage({ type: 'vote', optionId: optionId, voterId: voterId, senderId: this.peerManager.getPeerId() }); } broadcastPollReset() { this.peerManager.broadcastMessage({ type: 'poll_reset', senderId: this.peerManager.getPeerId() }); } selectPoll(pollId) { const poll = this.availablePolls.get(pollId); if (poll) { this.currentPoll = poll; // Clear votes when switching to a different poll // Only clear if this is a different poll than what we had voted in const currentVotedOption = this.getMyVotedOption(); if (currentVotedOption && !poll.options.some(opt => opt.id === currentVotedOption)) { this.myVotes.clear(); } this.saveToLocalStorage(); if (this.onPollSelected) { this.onPollSelected(poll); } if (this.onPollUpdated) { this.onPollUpdated(poll); } return true; } return false; } createNewPoll() { this.currentPoll = null; // Clear current poll inputs but keep available polls if (this.onPollSelected) { this.onPollSelected(null); } } getAvailablePolls() { return Array.from(this.availablePolls.values()); } unvote() { if (!this.currentPoll) { throw new Error('No active poll'); } const myPeerId = this.peerManager.getPeerId(); const myVotedOption = this.getMyVotedOption(); if (!myVotedOption) { return false; // No vote to remove } // Remove vote from current option this.removePreviousVote(myPeerId); this.saveToLocalStorage(); this.notifyPollUpdated(); this.broadcastUnvote(myPeerId); return true; } validateMyVotes(poll) { const myPeerId = this.peerManager.getPeerId(); const validVotes = new Set(); console.log('Validating votes for peer:', myPeerId); console.log('Current myVotes:', Array.from(this.myVotes)); console.log('Poll voters:', poll.options.map(opt => ({ id: opt.id, voters: opt.voters }))); // Check each vote in myVotes to see if it's actually mine this.myVotes.forEach(optionId => { const option = poll.options.find(opt => opt.id === optionId); if (option && option.voters.includes(myPeerId)) { // This vote is actually mine validVotes.add(optionId); console.log(`Vote ${optionId} is valid for peer ${myPeerId}`); } else { console.log(`Vote ${optionId} is NOT valid for peer ${myPeerId}`); } }); // Replace myVotes with only the valid votes this.myVotes = validVotes; console.log('Final valid votes:', Array.from(this.myVotes)); } getCurrentPoll() { return this.currentPoll; } broadcastUnvote(peerId) { this.peerManager.broadcastMessage({ type: 'unvote', voterId: peerId, senderId: this.peerManager.getPeerId() }); } hasVoted(optionId) { return this.myVotes.has(optionId); } getMyVotedOption() { return Array.from(this.myVotes)[0] || null; } generatePollId() { return `poll-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } saveToLocalStorage() { // Save all available polls const pollsData = Array.from(this.availablePolls.values()); localStorage.setItem('p2p-polls-list', JSON.stringify(pollsData)); // Save current poll separately if (this.currentPoll) { localStorage.setItem('p2p-poll-current', JSON.stringify(this.currentPoll)); } else { localStorage.removeItem('p2p-poll-current'); } localStorage.setItem('p2p-poll-my-votes', JSON.stringify(Array.from(this.myVotes))); } loadFromLocalStorage() { try { const savedPollsList = localStorage.getItem('p2p-polls-list'); const savedVotes = localStorage.getItem('p2p-poll-my-votes'); if (savedPollsList) { const polls = JSON.parse(savedPollsList); this.availablePolls.clear(); polls.forEach(poll => { this.availablePolls.set(poll.id, poll); }); if (this.onPollsListUpdated) { this.onPollsListUpdated(polls); } } if (savedVotes) { this.myVotes = new Set(JSON.parse(savedVotes)); // Validate votes against current poll const currentPoll = this.currentPoll || this.availablePolls.values().next().value; if (currentPoll) { this.validateMyVotes(currentPoll); } } } catch (error) { console.error('Failed to load from localStorage:', error); } } clearData() { this.currentPoll = null; this.availablePolls.clear(); this.myVotes.clear(); localStorage.removeItem('p2p-poll-current'); localStorage.removeItem('p2p-polls-list'); localStorage.removeItem('p2p-poll-my-votes'); } notifyPollUpdated() { if (this.onPollUpdated) { this.onPollUpdated(this.currentPoll); } } }