forked from quic-issues/427e7578-d7bf-49c8-aee9-2dd999e25316
- 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
420 lines
15 KiB
JavaScript
420 lines
15 KiB
JavaScript
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 = `
|
|
<input type="text" class="option-text" placeholder="Option ${optionCount + 1}">
|
|
<button class="remove-option-btn">Remove</button>
|
|
`;
|
|
|
|
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 = `
|
|
<div class="option-input">
|
|
<input type="text" class="option-text" placeholder="Option 1">
|
|
<button class="remove-option-btn hidden">Remove</button>
|
|
</div>
|
|
<div class="option-input">
|
|
<input type="text" class="option-text" placeholder="Option 2">
|
|
<button class="remove-option-btn hidden">Remove</button>
|
|
</div>
|
|
`;
|
|
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 = `
|
|
<div class="option-header">
|
|
<span class="option-text">${option.text}</span>
|
|
<span class="vote-count">${option.votes} votes</span>
|
|
</div>
|
|
<div class="vote-bar">
|
|
<div class="vote-fill" style="width: ${percentage}%"></div>
|
|
</div>
|
|
`;
|
|
|
|
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 = '<p class="no-polls">No polls created yet</p>';
|
|
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 = `
|
|
<div class="poll-item-title">${poll.question}</div>
|
|
<div class="poll-item-meta">${totalVotes} votes • ${createdDate}</div>
|
|
`;
|
|
|
|
pollDiv.addEventListener('click', () => {
|
|
this.pollManager.selectPoll(poll.id);
|
|
});
|
|
|
|
pollsList.appendChild(pollDiv);
|
|
});
|
|
}
|
|
}
|