diff --git a/README.md b/README.md
index 0217c70..e77e049 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,57 @@
-# P2P Poll App
\ No newline at end of file
+# P2P Poll App
+
+A peer-to-peer polling application where users can create polls, add options, and vote in real-time without a central server.
+
+## Features
+
+- **Real-time P2P voting** using WebRTC via PeerJS
+- **Dynamic option management** - add/remove options during polling
+- **Duplicate vote prevention** - one vote per user
+- **Automatic data synchronization** across all connected peers
+- **Local storage persistence** for poll recovery
+- **Responsive design** works on desktop and mobile
+- **No server required** - uses PeerJS free signaling service
+
+## How to Use
+
+1. **Open the app** in your browser (open `index.html`)
+2. **First user (Host)**: Leave the Peer ID field empty and click "Connect to Host"
+3. **Copy your Peer ID** using the "Copy Your Peer ID" button
+4. **Share your Peer ID** with other users (via chat, email, etc.)
+5. **Other users**: Paste the host's Peer ID and click "Connect to Host"
+6. **Create a poll** with question and options
+7. **Vote** by clicking on options (one vote per person)
+8. **Watch results** update in real-time across all devices
+
+## Technical Details
+
+- **P2P Library**: PeerJS (WebRTC-based)
+- **Frontend**: Vanilla JavaScript with modern CSS
+- **Data Sync**: Custom conflict resolution for concurrent operations
+- **Storage**: localStorage for basic persistence
+- **Network**: Full mesh topology where each peer connects to all others
+
+## File Structure
+
+```
+├── index.html # Main application
+├── css/
+│ └── styles.css # Application styling
+└── js/
+ ├── app.js # Main application logic
+ ├── peer-manager.js # P2P connection handling
+ ├── poll-manager.js # Poll data and sync logic
+ └── ui-controller.js # UI interactions
+```
+
+## Browser Support
+
+Requires modern browsers with WebRTC support:
+- Chrome 23+
+- Firefox 22+
+- Safari 11+
+- Edge 79+
+
+## Development
+
+Simply open `index.html` in a browser - no build process required. For testing with multiple peers, open the app in multiple browser tabs or windows.
\ No newline at end of file
diff --git a/css/styles.css b/css/styles.css
new file mode 100644
index 0000000..ef078d9
--- /dev/null
+++ b/css/styles.css
@@ -0,0 +1,665 @@
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ min-height: 100vh;
+ color: #333;
+}
+
+.container {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 20px;
+}
+
+.main-content {
+ display: grid;
+ grid-template-columns: 280px 1fr;
+ gap: 20px;
+ margin-top: 20px;
+}
+
+.sidebar {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.sidebar-section {
+ background: white;
+ border-radius: 12px;
+ padding: 20px;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+}
+
+.sidebar-section h3 {
+ margin: 0 0 15px 0;
+ color: #333;
+ font-size: 1.1rem;
+}
+
+.sidebar-hint {
+ margin-bottom: 10px;
+}
+
+.hint-text {
+ font-size: 0.8rem;
+ color: #6b7280;
+ font-style: italic;
+ text-align: center;
+ padding: 8px;
+ background: #f9fafb;
+ border-radius: 6px;
+ border: 1px solid #e5e7eb;
+}
+
+.polls-list {
+ max-height: 300px;
+ overflow-y: auto;
+}
+
+.poll-item {
+ padding: 12px;
+ border: 1px solid #e5e7eb;
+ border-radius: 8px;
+ margin-bottom: 8px;
+ cursor: pointer;
+ transition: all 0.3s;
+ position: relative;
+}
+
+.poll-item:hover {
+ border-color: #667eea;
+ background: #f9fafb;
+ transform: translateY(-1px);
+}
+
+.poll-item.active {
+ border-color: #667eea;
+ background: #ede9fe;
+}
+
+.poll-item-title {
+ font-weight: 500;
+ color: #333;
+ margin-bottom: 4px;
+}
+
+.poll-item-meta {
+ font-size: 0.8rem;
+ color: #6b7280;
+}
+
+.poll-item::after {
+ content: ">";
+ position: absolute;
+ right: 12px;
+ top: 50%;
+ transform: translateY(-50%);
+ color: #9ca3af;
+ font-size: 0.8rem;
+ opacity: 0;
+ transition: opacity 0.3s;
+}
+
+.poll-item:hover::after {
+ opacity: 1;
+}
+
+.no-polls {
+ color: #9ca3af;
+ font-style: italic;
+ text-align: center;
+ padding: 20px 0;
+}
+
+.peers-info {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.peers-status {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ font-size: 0.9rem;
+}
+
+.status-text {
+ padding: 2px 8px;
+ border-radius: 12px;
+ font-size: 0.8rem;
+ font-weight: 500;
+}
+
+.status-text.connected {
+ background: #dcfce7;
+ color: #16a34a;
+}
+
+.status-text.disconnected {
+ background: #fef2f2;
+ color: #dc2626;
+}
+
+.status-text.connecting {
+ background: #fef3c7;
+ color: #d97706;
+}
+
+.peers-sidebar {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ max-height: 150px;
+ overflow-y: auto;
+}
+
+.peers-sidebar li {
+ padding: 6px 0;
+ border-bottom: 1px solid #f3f4f6;
+ font-size: 0.9rem;
+ color: #6b7280;
+}
+
+.peers-sidebar li:last-child {
+ border-bottom: none;
+}
+
+header {
+ background: white;
+ border-radius: 12px;
+ padding: 20px;
+ margin-bottom: 20px;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+h1 {
+ color: #667eea;
+ font-size: 2rem;
+}
+
+.connection-status {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ font-size: 0.9rem;
+}
+
+#peer-id {
+ background: #f0f0f0;
+ padding: 5px 10px;
+ border-radius: 6px;
+ font-family: monospace;
+ max-width: 150px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.status-connected {
+ color: #10b981;
+}
+
+.status-disconnected {
+ color: #ef4444;
+}
+
+.status-connecting {
+ color: #f59e0b;
+}
+
+.section {
+ background: white;
+ border-radius: 12px;
+ padding: 20px;
+ margin-bottom: 20px;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+}
+
+.section.hidden {
+ display: none;
+}
+
+h2 {
+ color: #333;
+ margin-bottom: 20px;
+ font-size: 1.5rem;
+}
+
+h3 {
+ color: #666;
+ margin-bottom: 15px;
+ font-size: 1.1rem;
+}
+
+.connection-info {
+ background: #f0f9ff;
+ padding: 15px;
+ border-radius: 8px;
+ margin-bottom: 20px;
+ border-left: 4px solid #3b82f6;
+}
+
+.connection-info p {
+ margin: 5px 0;
+ font-size: 0.9rem;
+ color: #1e40af;
+}
+
+.connection-controls {
+ display: flex;
+ gap: 10px;
+ margin-bottom: 20px;
+ flex-wrap: wrap;
+}
+
+input[type="text"] {
+ flex: 1;
+ min-width: 200px;
+ padding: 12px;
+ border: 2px solid #e5e7eb;
+ border-radius: 8px;
+ font-size: 1rem;
+ transition: border-color 0.3s;
+}
+
+input[type="text"]:focus {
+ outline: none;
+ border-color: #667eea;
+}
+
+button {
+ background: #667eea;
+ color: white;
+ border: none;
+ padding: 12px 20px;
+ border-radius: 8px;
+ font-size: 1rem;
+ cursor: pointer;
+ transition: all 0.3s;
+}
+
+button:hover {
+ background: #5a67d8;
+ transform: translateY(-1px);
+}
+
+button:disabled {
+ background: #9ca3af;
+ cursor: not-allowed;
+ transform: none;
+}
+
+button.small-btn {
+ padding: 8px 12px;
+ font-size: 0.9rem;
+}
+
+button.danger {
+ background: #ef4444;
+}
+
+button.danger:hover {
+ background: #dc2626;
+}
+
+
+.poll-form {
+ display: flex;
+ flex-direction: column;
+ gap: 15px;
+}
+
+#options-container {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.option-input {
+ display: flex;
+ gap: 10px;
+ align-items: center;
+}
+
+.option-text {
+ flex: 1;
+}
+
+.remove-option-btn {
+ background: #ef4444;
+ color: white;
+ border: none;
+ padding: 4px 8px;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 0.8rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 60px;
+}
+
+.remove-option-btn.hidden {
+ display: none;
+}
+
+.poll-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+}
+
+.vote-hint {
+ margin-bottom: 20px;
+}
+
+.vote-hint .hint-text {
+ font-size: 0.9rem;
+ color: #6b7280;
+ text-align: center;
+ padding: 10px;
+ background: #f0f9ff;
+ border-radius: 8px;
+ border: 1px solid #bfdbfe;
+}
+
+.options-list {
+ display: flex;
+ flex-direction: column;
+ gap: 15px;
+}
+
+.poll-option {
+ background: #f9fafb;
+ padding: 15px;
+ border-radius: 8px;
+ border: 2px solid transparent;
+ transition: all 0.3s;
+ cursor: pointer;
+ position: relative;
+}
+
+.poll-option:hover {
+ border-color: #667eea;
+ transform: translateY(-1px);
+}
+
+.poll-option.voted {
+ border-color: #10b981;
+ background: #ecfdf5;
+}
+
+.poll-option.voted::before {
+ content: "✓";
+ position: absolute;
+ top: 10px;
+ right: 10px;
+ background: #10b981;
+ color: white;
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 12px;
+ font-weight: bold;
+}
+
+.poll-option.change-vote {
+ border-color: #f59e0b;
+ background: #fffbeb;
+}
+
+.poll-option.change-vote:hover {
+ border-color: #d97706;
+}
+
+.option-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 10px;
+}
+
+.option-text {
+ font-weight: 500;
+ color: #333;
+}
+
+.vote-count {
+ background: #667eea;
+ color: white;
+ padding: 4px 8px;
+ border-radius: 12px;
+ font-size: 0.8rem;
+}
+
+.vote-bar {
+ background: #e5e7eb;
+ height: 8px;
+ border-radius: 4px;
+ overflow: hidden;
+}
+
+.vote-fill {
+ background: #667eea;
+ height: 100%;
+ transition: width 0.3s ease;
+}
+
+.poll-actions {
+ display: flex;
+ gap: 10px;
+ margin-top: 20px;
+}
+
+.overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.5);
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ z-index: 1000;
+}
+
+.overlay.hidden {
+ display: none;
+}
+
+.loading-spinner {
+ width: 40px;
+ height: 40px;
+ border: 4px solid #f3f3f3;
+ border-top: 4px solid #667eea;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+ margin-bottom: 20px;
+}
+
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
+
+.notification {
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ background: #333;
+ color: white;
+ padding: 15px 20px;
+ border-radius: 8px;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+ z-index: 1001;
+ max-width: 300px;
+}
+
+.notification.success {
+ background: #10b981;
+}
+
+.notification.error {
+ background: #ef4444;
+}
+
+.notification.hidden {
+ display: none;
+}
+
+/* Modal Styles */
+.modal {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.5);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ z-index: 1002;
+}
+
+.modal.hidden {
+ display: none;
+}
+
+.modal-content {
+ background: white;
+ border-radius: 12px;
+ width: 90%;
+ max-width: 400px;
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
+ overflow: hidden;
+}
+
+.modal-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 20px;
+ border-bottom: 1px solid #e5e7eb;
+}
+
+.modal-header h3 {
+ margin: 0;
+ color: #333;
+ font-size: 1.2rem;
+}
+
+.modal-close {
+ background: none;
+ border: none;
+ font-size: 1.5rem;
+ color: #6b7280;
+ cursor: pointer;
+ padding: 0;
+ width: 30px;
+ height: 30px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 50%;
+ transition: all 0.3s;
+}
+
+.modal-close:hover {
+ background: #f3f4f6;
+ color: #374151;
+}
+
+.modal-body {
+ padding: 20px;
+}
+
+.modal-body input {
+ width: 100%;
+ padding: 12px;
+ border: 2px solid #e5e7eb;
+ border-radius: 8px;
+ font-size: 1rem;
+ transition: border-color 0.3s;
+}
+
+.modal-body input:focus {
+ outline: none;
+ border-color: #667eea;
+}
+
+.modal-footer {
+ padding: 20px;
+ border-top: 1px solid #e5e7eb;
+ display: flex;
+ gap: 10px;
+ justify-content: flex-end;
+}
+
+.btn-secondary {
+ background: #6b7280;
+ color: white;
+}
+
+.btn-secondary:hover {
+ background: #4b5563;
+}
+
+.btn-primary {
+ background: #667eea;
+ color: white;
+}
+
+.btn-primary:hover {
+ background: #5a67d8;
+}
+
+@media (max-width: 768px) {
+ .container {
+ padding: 10px;
+ }
+
+ .main-content {
+ grid-template-columns: 1fr;
+ gap: 15px;
+ }
+
+ .sidebar {
+ order: 2;
+ }
+
+ main {
+ order: 1;
+ }
+
+ header {
+ flex-direction: column;
+ gap: 15px;
+ text-align: center;
+ }
+
+ .connection-controls {
+ flex-direction: column;
+ }
+
+ .poll-header {
+ flex-direction: column;
+ gap: 10px;
+ align-items: flex-start;
+ }
+}
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..5b9cdd0
--- /dev/null
+++ b/index.html
@@ -0,0 +1,133 @@
+
+
+
+
+
+ P2P Polling App
+
+
+
+
+
+
+ P2P Polling App
+
+ Loading...
+ ●
+
+
+
+
+
+
+
+
+
+
+ Connect to Peer
+
+
Host: Share your Peer ID below with others
+
Joiner: Enter the host's Peer ID to connect
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Click any option to vote. Click your voted option again to remove your vote.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/js/app.js b/js/app.js
new file mode 100644
index 0000000..dbea66b
--- /dev/null
+++ b/js/app.js
@@ -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');
+ }
+});
diff --git a/js/peer-manager.js b/js/peer-manager.js
new file mode 100644
index 0000000..89e6b9e
--- /dev/null
+++ b/js/peer-manager.js
@@ -0,0 +1,233 @@
+class PeerManager {
+ constructor() {
+ this.peer = null;
+ this.connections = new Map();
+ this.roomId = null;
+ this.isHost = false;
+ this.onPeerConnected = null;
+ this.onPeerDisconnected = null;
+ this.onMessageReceived = null;
+ this.onConnectionStatusChange = null;
+ }
+
+ async initialize() {
+ try {
+ this.peer = new Peer({
+ debug: 2,
+ config: {
+ iceServers: [
+ { urls: 'stun:stun.l.google.com:19302' },
+ { urls: 'stun:stun1.l.google.com:19302' }
+ ]
+ }
+ });
+
+ this.peer.on('open', (id) => {
+ console.log('My peer ID is:', id);
+ this.updateConnectionStatus('connected');
+ this.updatePeersList();
+ if (this.onConnectionStatusChange) {
+ this.onConnectionStatusChange('connected', id);
+ }
+ });
+
+ this.peer.on('connection', (conn) => {
+ this.handleIncomingConnection(conn);
+ });
+
+ this.peer.on('disconnected', () => {
+ this.updateConnectionStatus('disconnected');
+ this.updatePeersList();
+ });
+
+ this.peer.on('error', (err) => {
+ console.error('Peer error:', err);
+ this.updateConnectionStatus('error');
+ });
+
+ } catch (error) {
+ console.error('Failed to initialize peer:', error);
+ throw error;
+ }
+ }
+
+ async createRoom() {
+ if (!this.peer) {
+ throw new Error('Peer not initialized');
+ }
+
+ this.roomId = this.generateRoomId();
+ this.isHost = true;
+
+ console.log('Created room:', this.roomId);
+ return this.roomId;
+ }
+
+ async joinRoom(roomId) {
+ if (!this.peer) {
+ throw new Error('Peer not initialized');
+ }
+
+ this.roomId = roomId;
+ this.isHost = false;
+
+ console.log('Attempting to join room:', roomId);
+
+
+ try {
+ const conn = this.peer.connect(roomId);
+ await this.setupConnection(conn);
+ console.log('Successfully joined room');
+ return true;
+ } catch (error) {
+ console.error('Failed to join room:', error);
+ throw new Error('Could not connect to host. Make sure the host is online and you have the correct peer ID.');
+ }
+ }
+
+ handleIncomingConnection(conn) {
+ this.setupConnection(conn);
+ }
+
+ async setupConnection(conn) {
+ return new Promise((resolve, reject) => {
+ conn.on('open', () => {
+ console.log('Connected to:', conn.peer);
+ this.connections.set(conn.peer, conn);
+ this.updatePeersList();
+
+ if (this.isHost) {
+ if (this.onPeerConnected) {
+ this.onPeerConnected(conn.peer);
+ }
+ } else {
+ if (this.onPeerConnected) {
+ this.onPeerConnected(conn.peer);
+ }
+ }
+
+ resolve(conn);
+ });
+
+ conn.on('data', (data) => {
+ this.handleMessage(data, conn.peer);
+ });
+
+ conn.on('close', () => {
+ console.log('Connection closed:', conn.peer);
+ this.connections.delete(conn.peer);
+ this.updatePeersList();
+
+ if (this.onPeerDisconnected) {
+ this.onPeerDisconnected(conn.peer);
+ }
+ });
+
+ conn.on('error', (err) => {
+ console.error('Connection error:', err);
+ reject(err);
+ });
+ });
+ }
+
+ handleMessage(data, senderId) {
+ console.log('Received message:', data, 'from:', senderId);
+
+ if (this.onMessageReceived) {
+ this.onMessageReceived(data, senderId);
+ }
+ }
+
+ sendMessage(peerId, message) {
+ const conn = this.connections.get(peerId);
+ if (conn && conn.open) {
+ conn.send(message);
+ return true;
+ }
+ return false;
+ }
+
+ broadcastMessage(message) {
+ let sentCount = 0;
+ this.connections.forEach((conn, peerId) => {
+ if (conn.open) {
+ conn.send(message);
+ sentCount++;
+ }
+ });
+ return sentCount;
+ }
+
+ getPeerId() {
+ return this.peer ? this.peer.id : null;
+ }
+
+ getRoomId() {
+ return this.roomId;
+ }
+
+ getConnectedPeers() {
+ return Array.from(this.connections.keys());
+ }
+
+ getPeerCount() {
+ return this.connections.size;
+ }
+
+ isConnected() {
+ return this.peer && this.peer.disconnected !== true;
+ }
+
+ generateRoomId() {
+ return Math.random().toString(36).substr(2, 9).toUpperCase();
+ }
+
+ updateConnectionStatus(status) {
+ const indicator = document.getElementById('connection-indicator');
+ const statusText = document.getElementById('connection-status-text');
+
+ if (indicator) {
+ indicator.className = `status-${status}`;
+ }
+
+ if (statusText) {
+ statusText.textContent = status.charAt(0).toUpperCase() + status.slice(1);
+ statusText.className = `status-text ${status}`;
+ }
+ }
+
+ updatePeersList() {
+ const peersList = document.getElementById('peers');
+ const peerCount = document.getElementById('peer-count');
+
+ if (peersList && peerCount) {
+ peerCount.textContent = this.getPeerCount();
+ peersList.innerHTML = '';
+
+ if (this.getPeerCount() === 0) {
+ const li = document.createElement('li');
+ li.textContent = 'No connected peers';
+ li.style.fontStyle = 'italic';
+ li.style.color = '#9ca3af';
+ peersList.appendChild(li);
+ } else {
+ this.getConnectedPeers().forEach(peerId => {
+ const li = document.createElement('li');
+ li.textContent = `${peerId} ${peerId === this.peer.id ? '(You)' : ''}`;
+ li.style.fontWeight = peerId === this.peer.id ? 'bold' : 'normal';
+ li.style.color = peerId === this.peer.id ? '#667eea' : '#6b7280';
+ peersList.appendChild(li);
+ });
+ }
+ }
+ }
+
+ destroy() {
+ if (this.peer) {
+ this.connections.forEach(conn => conn.close());
+ this.peer.destroy();
+ this.peer = null;
+ this.connections.clear();
+ }
+ }
+}
diff --git a/js/poll-manager.js b/js/poll-manager.js
new file mode 100644
index 0000000..5bda570
--- /dev/null
+++ b/js/poll-manager.js
@@ -0,0 +1,428 @@
+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);
+ }
+ }
+}
diff --git a/js/ui-controller.js b/js/ui-controller.js
new file mode 100644
index 0000000..ca7606f
--- /dev/null
+++ b/js/ui-controller.js
@@ -0,0 +1,419 @@
+class UIController {
+ constructor(peerManager, pollManager) {
+ this.peerManager = peerManager;
+ this.pollManager = pollManager;
+ this.initializeEventListeners();
+ }
+
+ initializeEventListeners() {
+ // Connection controls
+ document.getElementById('join-btn').addEventListener('click', () => this.handleJoinRoom());
+ document.getElementById('copy-id-btn').addEventListener('click', () => this.copyPeerId());
+
+ // Poll creation
+ document.getElementById('create-poll-btn').addEventListener('click', () => this.handleCreatePoll());
+ document.getElementById('add-option-btn').addEventListener('click', () => this.addOptionInput());
+
+ // Active poll
+ document.getElementById('add-poll-option-btn').addEventListener('click', () => this.showAddOptionModal());
+ document.getElementById('reset-poll-btn').addEventListener('click', () => this.handleResetPoll());
+ document.getElementById('new-poll-btn').addEventListener('click', () => this.handleNewPoll());
+
+ // Modal controls
+ document.getElementById('close-modal-btn').addEventListener('click', () => this.hideAddOptionModal());
+ document.getElementById('cancel-modal-btn').addEventListener('click', () => this.hideAddOptionModal());
+ document.getElementById('save-option-btn').addEventListener('click', () => this.handleAddOptionFromModal());
+
+ // Enter key handlers
+ document.getElementById('room-id').addEventListener('keypress', (e) => {
+ if (e.key === 'Enter') this.handleJoinRoom();
+ });
+
+ document.getElementById('poll-question').addEventListener('keypress', (e) => {
+ if (e.key === 'Enter') this.handleCreatePoll();
+ });
+
+ document.getElementById('new-option-input').addEventListener('keypress', (e) => {
+ if (e.key === 'Enter') this.handleAddOptionFromModal();
+ });
+
+ // Close modal on background click
+ document.getElementById('add-option-modal').addEventListener('click', (e) => {
+ if (e.target.id === 'add-option-modal') {
+ this.hideAddOptionModal();
+ }
+ });
+ }
+
+ handleJoinRoom() {
+ const roomIdInput = document.getElementById('room-id');
+ const peerId = roomIdInput.value.trim();
+
+ this.showLoading(true);
+
+ if (peerId) {
+ // Connect to existing peer (host)
+ this.peerManager.joinRoom(peerId)
+ .then(() => {
+ this.showNotification('Connected to host successfully!', 'success');
+ this.showPollCreation();
+ })
+ .catch(error => {
+ this.showNotification('Failed to connect: ' + error.message, 'error');
+ console.error('Connect error:', error);
+ })
+ .finally(() => {
+ this.showLoading(false);
+ });
+ } else {
+ // Act as host - just show poll creation
+ this.peerManager.createRoom();
+ this.showNotification('You are the host. Share your Peer ID with others to connect.', 'success');
+ this.showPollCreation();
+ this.showLoading(false);
+ }
+ }
+
+ async copyPeerId() {
+ const peerId = this.peerManager.getPeerId();
+ if (peerId) {
+ try {
+ await navigator.clipboard.writeText(peerId);
+ this.showNotification('Peer ID copied to clipboard!', 'success');
+ } catch (error) {
+ // Fallback for older browsers
+ const textArea = document.createElement('textarea');
+ textArea.value = peerId;
+ document.body.appendChild(textArea);
+ textArea.select();
+ document.execCommand('copy');
+ document.body.removeChild(textArea);
+ this.showNotification('Peer ID copied to clipboard!', 'success');
+ }
+ }
+ }
+
+ handleCreatePoll() {
+ const questionInput = document.getElementById('poll-question');
+ const optionInputs = document.querySelectorAll('.option-text');
+ const options = [];
+
+ console.log('Question input:', questionInput);
+ console.log('Found option inputs:', optionInputs.length);
+
+ // Check if question input exists
+ if (!questionInput) {
+ console.error('Question input not found!');
+ this.showNotification('Error: Question input not found', 'error');
+ return;
+ }
+
+ const question = questionInput.value ? questionInput.value.trim() : '';
+ console.log('Question:', question);
+
+ optionInputs.forEach((input, index) => {
+ console.log(`Input ${index}:`, input, 'value:', input?.value);
+ if (input && typeof input.value !== 'undefined') {
+ const text = input.value.trim();
+ if (text) {
+ options.push(text);
+ }
+ } else {
+ console.warn(`Input ${index} is invalid or has no value property`);
+ }
+ });
+
+ console.log('Final options:', options);
+
+ if (!question) {
+ this.showNotification('Please enter a poll question', 'error');
+ return;
+ }
+
+ if (options.length < 2) {
+ this.showNotification('Please enter at least 2 options', 'error');
+ return;
+ }
+
+ try {
+ this.pollManager.createPoll(question, options);
+ this.showNotification('Poll created successfully!', 'success');
+ } catch (error) {
+ console.error('Create poll error:', error);
+ this.showNotification('Failed to create poll: ' + error.message, 'error');
+ }
+ }
+
+ showAddOptionModal() {
+ const modal = document.getElementById('add-option-modal');
+ const input = document.getElementById('new-option-input');
+ modal.classList.remove('hidden');
+ input.value = '';
+ input.focus();
+ }
+
+ hideAddOptionModal() {
+ const modal = document.getElementById('add-option-modal');
+ const input = document.getElementById('new-option-input');
+ modal.classList.add('hidden');
+ input.value = '';
+ }
+
+ handleAddOptionFromModal() {
+ const input = document.getElementById('new-option-input');
+ const optionText = input.value.trim();
+
+ if (!optionText) {
+ this.showNotification('Please enter an option text', 'error');
+ return;
+ }
+
+ try {
+ this.pollManager.addOption(optionText);
+ this.hideAddOptionModal();
+ this.showNotification('Option added successfully!', 'success');
+ } catch (error) {
+ this.showNotification('Failed to add option: ' + error.message, 'error');
+ }
+ }
+
+ handleAddPollOption() {
+ this.showAddOptionModal();
+ }
+
+ handleResetPoll() {
+ if (confirm('Are you sure you want to reset all votes?')) {
+ try {
+ this.pollManager.resetPoll();
+ this.showNotification('Poll reset successfully!', 'success');
+ } catch (error) {
+ this.showNotification('Failed to reset poll: ' + error.message, 'error');
+ }
+ }
+ }
+
+ handleNewPoll() {
+ this.pollManager.createNewPoll();
+ this.showPollCreation();
+ this.clearPollForm();
+ }
+
+ addOptionInput() {
+ const container = document.getElementById('options-container');
+ const optionCount = container.children.length;
+
+ const optionDiv = document.createElement('div');
+ optionDiv.className = 'option-input';
+ optionDiv.innerHTML = `
+
+
+ `;
+
+ container.appendChild(optionDiv);
+ this.updateOptionRemoveButtons();
+
+ // Add event listener to new input
+ const newInput = optionDiv.querySelector('.option-text');
+ newInput.addEventListener('keypress', (e) => {
+ if (e.key === 'Enter') this.handleCreatePoll();
+ });
+
+ // Add event listener to remove button
+ const removeBtn = optionDiv.querySelector('.remove-option-btn');
+ removeBtn.addEventListener('click', () => {
+ optionDiv.remove();
+ this.updateOptionRemoveButtons();
+ this.updateOptionPlaceholders();
+ });
+
+ // Focus on new input
+ newInput.focus();
+ }
+
+ updateOptionRemoveButtons() {
+ const optionInputs = document.querySelectorAll('.option-input');
+ const removeButtons = document.querySelectorAll('.remove-option-btn');
+
+ removeButtons.forEach((btn, index) => {
+ if (optionInputs.length <= 2) {
+ btn.classList.add('hidden');
+ } else {
+ btn.classList.remove('hidden');
+ }
+ });
+ }
+
+ updateOptionPlaceholders() {
+ const optionInputs = document.querySelectorAll('.option-text');
+ optionInputs.forEach((input, index) => {
+ input.placeholder = `Option ${index + 1}`;
+ });
+ }
+
+ clearPollForm() {
+ document.getElementById('poll-question').value = '';
+ const container = document.getElementById('options-container');
+ container.innerHTML = `
+
+
+
+
+
+
+
+
+ `;
+ this.updateOptionRemoveButtons();
+ }
+
+ showPollCreation() {
+ document.getElementById('connection-section').classList.add('hidden');
+ document.getElementById('poll-creation').classList.remove('hidden');
+ document.getElementById('active-poll').classList.add('hidden');
+ }
+
+ showActivePoll() {
+ document.getElementById('connection-section').classList.add('hidden');
+ document.getElementById('poll-creation').classList.add('hidden');
+ document.getElementById('active-poll').classList.remove('hidden');
+ }
+
+ updatePeerId(peerId) {
+ const element = document.getElementById('peer-id');
+ if (element) {
+ element.textContent = peerId || 'Loading...';
+ }
+ }
+
+ renderPoll(poll) {
+ if (!poll) return;
+
+ this.showActivePoll();
+
+ // Update poll question
+ document.getElementById('poll-question-display').textContent = poll.question;
+
+ // Render options
+ const optionsContainer = document.getElementById('poll-options');
+ optionsContainer.innerHTML = '';
+
+ const totalVotes = poll.options.reduce((sum, opt) => sum + opt.votes, 0);
+ const myVotedOption = this.pollManager.getMyVotedOption();
+
+ poll.options.forEach(option => {
+ const optionDiv = document.createElement('div');
+ optionDiv.className = 'poll-option';
+
+ const hasVoted = this.pollManager.hasVoted(option.id);
+ const myVotedOption = this.pollManager.getMyVotedOption();
+
+ console.log(`Option ${option.id}: hasVoted=${hasVoted}, myVotedOption=${myVotedOption}`);
+
+ if (hasVoted) {
+ optionDiv.classList.add('voted');
+ } else if (myVotedOption) {
+ // User has voted but not for this option - this is a change vote option
+ optionDiv.classList.add('change-vote');
+ }
+
+ const percentage = totalVotes > 0 ? (option.votes / totalVotes * 100).toFixed(1) : 0;
+
+ optionDiv.innerHTML = `
+
+
+ `;
+
+ optionDiv.addEventListener('click', () => {
+ const myVotedOption = this.pollManager.getMyVotedOption();
+
+ if (this.pollManager.hasVoted(option.id)) {
+ // Clicking your current vote - unvote
+ if (confirm('Remove your vote?')) {
+ const success = this.pollManager.unvote();
+ if (success) {
+ this.showNotification('Vote removed!', 'success');
+ }
+ }
+ } else {
+ // Either first vote or changing vote
+ const success = this.pollManager.vote(option.id);
+ if (success) {
+ if (myVotedOption) {
+ this.showNotification('Vote changed successfully!', 'success');
+ } else {
+ this.showNotification('Vote recorded!', 'success');
+ }
+ }
+ }
+ });
+
+ optionsContainer.appendChild(optionDiv);
+ });
+ }
+
+ showNotification(message, type = 'info') {
+ const notification = document.getElementById('notification');
+ notification.textContent = message;
+ notification.className = `notification ${type}`;
+ notification.classList.remove('hidden');
+
+ setTimeout(() => {
+ notification.classList.add('hidden');
+ }, 3000);
+ }
+
+ showLoading(show) {
+ const overlay = document.getElementById('loading-overlay');
+ if (show) {
+ overlay.classList.remove('hidden');
+ } else {
+ overlay.classList.add('hidden');
+ }
+ }
+
+ updatePeersList() {
+ // Use the peer manager's updatePeersList method
+ this.peerManager.updatePeersList();
+ }
+
+ updatePollsList(polls) {
+ const pollsList = document.getElementById('polls-list');
+
+ if (!pollsList) return;
+
+ if (polls.length === 0) {
+ pollsList.innerHTML = 'No polls created yet
';
+ return;
+ }
+
+ pollsList.innerHTML = '';
+
+ polls.forEach(poll => {
+ const pollDiv = document.createElement('div');
+ pollDiv.className = 'poll-item';
+
+ if (this.pollManager.getCurrentPoll() && this.pollManager.getCurrentPoll().id === poll.id) {
+ pollDiv.classList.add('active');
+ }
+
+ const totalVotes = poll.options.reduce((sum, opt) => sum + opt.votes, 0);
+ const createdDate = new Date(poll.createdAt).toLocaleString();
+
+ pollDiv.innerHTML = `
+ ${poll.question}
+ ${totalVotes} votes • ${createdDate}
+ `;
+
+ pollDiv.addEventListener('click', () => {
+ this.pollManager.selectPoll(poll.id);
+ });
+
+ pollsList.appendChild(pollDiv);
+ });
+ }
+}