- 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
234 lines
6.7 KiB
JavaScript
234 lines
6.7 KiB
JavaScript
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();
|
|
}
|
|
}
|
|
}
|