feat: complete P2P polling app with vote management and UI improvements

- Add peer-to-peer polling with real-time synchronization via PeerJS
- Implement vote changing, unvoting, and proper vote validation
- Add modal interface for adding poll options
- Fix vote inheritance issues for new users joining rooms
- Create sidebar with available polls and connected peers
- Add vote count synchronization across all connected peers
- Implement proper vote state validation per user
- Add localStorage persistence for polls and vote states
- Create responsive design with modern UI components
- Replace emojis with proper text-based icons
- Add comprehensive error handling and user feedback
- Support multiple polls with switching capabilities
- Implement conflict resolution for concurrent voting
This commit is contained in:
2026-03-09 13:37:31 +01:00
parent 4275cbd795
commit fea88b277c
7 changed files with 2145 additions and 1 deletions

210
js/app.js Normal file
View File

@@ -0,0 +1,210 @@
// Main application entry point
class P2PPollApp {
constructor() {
this.peerManager = null;
this.pollManager = null;
this.uiController = null;
this.isInitialized = false;
}
async initialize() {
try {
// Initialize peer manager
this.peerManager = new PeerManager();
this.pollManager = new PollManager(this.peerManager);
this.uiController = new UIController(this.peerManager, this.pollManager);
// Set up event handlers
this.setupEventHandlers();
// Initialize peer connection
await this.peerManager.initialize();
// Load saved data
this.pollManager.loadFromLocalStorage();
// If there are saved polls, update the sidebar
const savedPolls = this.pollManager.getAvailablePolls();
if (savedPolls.length > 0) {
this.uiController.updatePollsList(savedPolls);
}
this.isInitialized = true;
console.log('P2P Poll App initialized successfully');
} catch (error) {
console.error('Failed to initialize app:', error);
this.uiController?.showNotification('Failed to initialize app: ' + error.message, 'error');
}
}
setupEventHandlers() {
// Peer manager events
this.peerManager.onConnectionStatusChange = (status, peerId) => {
this.uiController.updatePeerId(peerId);
if (status === 'connected') {
this.uiController.showNotification('Connected to P2P network', 'success');
} else if (status === 'disconnected') {
this.uiController.showNotification('Disconnected from P2P network', 'error');
}
};
this.peerManager.onPeerConnected = (peerId) => {
this.uiController.showNotification(`Peer ${peerId} connected`, 'success');
// If we're the host and have polls, send them to the new peer
if (this.peerManager.isHost) {
const availablePolls = this.pollManager.getAvailablePolls();
if (availablePolls.length > 0) {
availablePolls.forEach(poll => {
this.peerManager.sendMessage(peerId, {
type: 'poll_update',
poll: poll,
senderId: this.peerManager.getPeerId()
});
});
// Also send current poll if there is one
const currentPoll = this.pollManager.getCurrentPoll();
if (currentPoll) {
this.peerManager.sendMessage(peerId, {
type: 'current_poll',
poll: currentPoll,
senderId: this.peerManager.getPeerId()
});
}
}
} else {
this.peerManager.sendMessage(peerId, {
type: 'sync_request',
senderId: this.peerManager.getPeerId()
});
}
};
this.peerManager.onPeerDisconnected = (peerId) => {
this.uiController.showNotification(`Peer ${peerId} disconnected`, 'info');
};
this.peerManager.onMessageReceived = (message, senderId) => {
this.handleMessage(message, senderId);
};
// Poll manager events
this.pollManager.onPollCreated = (poll) => {
this.uiController.renderPoll(poll);
};
this.pollManager.onPollUpdated = (poll) => {
this.uiController.renderPoll(poll);
};
this.pollManager.onPollSelected = (poll) => {
if (poll) {
this.uiController.renderPoll(poll);
this.uiController.showActivePoll();
} else {
this.uiController.showPollCreation();
}
};
this.pollManager.onPollsListUpdated = (polls) => {
this.uiController.updatePollsList(polls);
};
}
handleMessage(message, senderId) {
console.log('Processing message:', message, 'from:', senderId);
switch (message.type) {
case 'poll_update':
this.pollManager.syncPoll(message.poll);
break;
case 'current_poll':
this.pollManager.syncPoll(message.poll);
if (this.pollManager.getCurrentPoll() && this.pollManager.getCurrentPoll().id === message.poll.id) {
this.uiController.renderPoll(message.poll);
this.uiController.showActivePoll();
}
break;
case 'vote':
this.pollManager.handleVoteMessage(message.optionId, message.voterId);
break;
case 'unvote':
this.pollManager.handleUnvoteMessage(message.voterId);
break;
case 'poll_reset':
this.pollManager.handlePollReset();
break;
case 'sync_request':
const currentPoll = this.pollManager.getCurrentPoll();
if (currentPoll) {
this.peerManager.sendMessage(senderId, {
type: 'current_poll',
poll: currentPoll,
senderId: this.peerManager.getPeerId()
});
}
const availablePolls = this.pollManager.getAvailablePolls();
availablePolls.forEach(poll => {
this.peerManager.sendMessage(senderId, {
type: 'poll_update',
poll: poll,
senderId: this.peerManager.getPeerId()
});
});
break;
default:
console.warn('Unknown message type:', message.type);
}
}
destroy() {
if (this.peerManager) {
this.peerManager.destroy();
}
}
}
document.addEventListener('DOMContentLoaded', async () => {
const app = new P2PPollApp();
try {
await app.initialize();
} catch (error) {
console.error('App initialization failed:', error);
// Show error message to user
const errorDiv = document.createElement('div');
errorDiv.className = 'notification error';
errorDiv.textContent = 'Failed to initialize app. Please refresh the page.';
errorDiv.style.position = 'fixed';
errorDiv.style.top = '20px';
errorDiv.style.right = '20px';
errorDiv.style.zIndex = '1001';
document.body.appendChild(errorDiv);
}
// Cleanup on page unload
window.addEventListener('beforeunload', () => {
app.destroy();
});
});
// Handle page visibility changes
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
console.log('Page hidden - connection may become unstable');
} else {
console.log('Page visible - checking connection status');
}
});