freealberta/influence/app/public/js/response-wall.js

366 lines
11 KiB
JavaScript

// Response Wall JavaScript
let currentCampaignSlug = null;
let currentOffset = 0;
let currentSort = 'recent';
let currentLevel = '';
const LIMIT = 20;
// Initialize
document.addEventListener('DOMContentLoaded', () => {
console.log('Response Wall: Initializing...');
// Get campaign slug from URL if present
const urlParams = new URLSearchParams(window.location.search);
currentCampaignSlug = urlParams.get('campaign');
console.log('Campaign slug:', currentCampaignSlug);
if (!currentCampaignSlug) {
showError('No campaign specified');
return;
}
// Load initial data
loadResponseStats();
loadResponses(true);
// Set up event listeners
document.getElementById('sort-select').addEventListener('change', (e) => {
currentSort = e.target.value;
loadResponses(true);
});
document.getElementById('level-filter').addEventListener('change', (e) => {
currentLevel = e.target.value;
loadResponses(true);
});
const submitBtn = document.getElementById('submit-response-btn');
console.log('Submit button found:', !!submitBtn);
if (submitBtn) {
submitBtn.addEventListener('click', () => {
console.log('Submit button clicked');
openSubmitModal();
});
}
// Use event delegation for empty state button since it's dynamically shown
document.addEventListener('click', (e) => {
if (e.target.id === 'empty-state-submit-btn') {
console.log('Empty state button clicked');
openSubmitModal();
}
});
const modalCloseBtn = document.getElementById('modal-close-btn');
if (modalCloseBtn) {
modalCloseBtn.addEventListener('click', closeSubmitModal);
}
const cancelBtn = document.getElementById('cancel-submit-btn');
if (cancelBtn) {
cancelBtn.addEventListener('click', closeSubmitModal);
}
const loadMoreBtn = document.getElementById('load-more-btn');
if (loadMoreBtn) {
loadMoreBtn.addEventListener('click', loadMoreResponses);
}
const form = document.getElementById('submit-response-form');
if (form) {
form.addEventListener('submit', handleSubmitResponse);
}
console.log('Response Wall: Initialization complete');
});
// Load response statistics
async function loadResponseStats() {
try {
const response = await fetch(`/api/campaigns/${currentCampaignSlug}/response-stats`);
const data = await response.json();
if (data.success) {
document.getElementById('stat-total-responses').textContent = data.stats.totalResponses;
document.getElementById('stat-verified').textContent = data.stats.verifiedResponses;
document.getElementById('stat-upvotes').textContent = data.stats.totalUpvotes;
document.getElementById('stats-banner').style.display = 'flex';
}
} catch (error) {
console.error('Error loading stats:', error);
}
}
// Load responses
async function loadResponses(reset = false) {
if (reset) {
currentOffset = 0;
document.getElementById('responses-container').innerHTML = '';
}
showLoading(true);
try {
const params = new URLSearchParams({
sort: currentSort,
level: currentLevel,
offset: currentOffset,
limit: LIMIT
});
const response = await fetch(`/api/campaigns/${currentCampaignSlug}/responses?${params}`);
const data = await response.json();
showLoading(false);
if (data.success && data.responses.length > 0) {
renderResponses(data.responses);
// Show/hide load more button
if (data.pagination.hasMore) {
document.getElementById('load-more-container').style.display = 'block';
} else {
document.getElementById('load-more-container').style.display = 'none';
}
} else if (reset) {
showEmptyState();
}
} catch (error) {
showLoading(false);
showError('Failed to load responses');
console.error('Error loading responses:', error);
}
}
// Render responses
function renderResponses(responses) {
const container = document.getElementById('responses-container');
responses.forEach(response => {
const card = createResponseCard(response);
container.appendChild(card);
});
}
// Create response card element
function createResponseCard(response) {
const card = document.createElement('div');
card.className = 'response-card';
card.dataset.responseId = response.id;
const createdDate = new Date(response.created_at).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
let badges = `<span class="badge badge-level">${escapeHtml(response.representative_level)}</span>`;
badges += `<span class="badge badge-type">${escapeHtml(response.response_type)}</span>`;
if (response.is_verified) {
badges = `<span class="badge badge-verified">✓ Verified</span>` + badges;
}
let submittedBy = 'Anonymous';
if (!response.is_anonymous && response.submitted_by_name) {
submittedBy = escapeHtml(response.submitted_by_name);
}
let userCommentHtml = '';
if (response.user_comment) {
userCommentHtml = `
<div class="user-comment">
<span class="user-comment-label">Constituent's Comment:</span>
${escapeHtml(response.user_comment)}
</div>
`;
}
let screenshotHtml = '';
if (response.screenshot_url) {
screenshotHtml = `
<div class="response-screenshot">
<img src="${escapeHtml(response.screenshot_url)}"
alt="Response screenshot"
data-image-url="${escapeHtml(response.screenshot_url)}"
class="screenshot-image">
</div>
`;
}
const upvoteClass = response.hasUpvoted ? 'upvoted' : '';
card.innerHTML = `
<div class="response-header">
<div class="response-rep-info">
<h3>${escapeHtml(response.representative_name)}</h3>
<div class="rep-meta">
${response.representative_title ? `<span>${escapeHtml(response.representative_title)}</span>` : ''}
<span>${createdDate}</span>
</div>
</div>
<div class="response-badges">
${badges}
</div>
</div>
<div class="response-content">
<div class="response-text">${escapeHtml(response.response_text)}</div>
${userCommentHtml}
${screenshotHtml}
</div>
<div class="response-footer">
<div class="response-meta">
Submitted by ${submittedBy}
</div>
<div class="response-actions">
<button class="upvote-btn ${upvoteClass}" data-response-id="${response.id}">
<span class="upvote-icon">👍</span>
<span class="upvote-count">${response.upvote_count || 0}</span>
</button>
</div>
</div>
`;
// Add event listener for upvote button
const upvoteBtn = card.querySelector('.upvote-btn');
upvoteBtn.addEventListener('click', function() {
toggleUpvote(response.id, this);
});
// Add event listener for screenshot image if present
const screenshotImg = card.querySelector('.screenshot-image');
if (screenshotImg) {
screenshotImg.addEventListener('click', function() {
viewImage(this.dataset.imageUrl);
});
}
return card;
}
// Toggle upvote
async function toggleUpvote(responseId, button) {
const isUpvoted = button.classList.contains('upvoted');
const url = `/api/responses/${responseId}/upvote`;
try {
const response = await fetch(url, {
method: isUpvoted ? 'DELETE' : 'POST',
headers: {
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (data.success) {
// Update button state
button.classList.toggle('upvoted');
button.querySelector('.upvote-count').textContent = data.upvoteCount;
// Reload stats
loadResponseStats();
} else {
showError(data.error || 'Failed to update upvote');
}
} catch (error) {
console.error('Error toggling upvote:', error);
showError('Failed to update upvote');
}
}
// Load more responses
function loadMoreResponses() {
currentOffset += LIMIT;
loadResponses(false);
}
// Open submit modal
function openSubmitModal() {
console.log('openSubmitModal called');
const modal = document.getElementById('submit-modal');
if (modal) {
modal.style.display = 'block';
console.log('Modal displayed');
} else {
console.error('Modal element not found');
}
}
// Close submit modal
function closeSubmitModal() {
document.getElementById('submit-modal').style.display = 'none';
document.getElementById('submit-response-form').reset();
}
// Handle response submission
async function handleSubmitResponse(e) {
e.preventDefault();
const formData = new FormData(e.target);
try {
const response = await fetch(`/api/campaigns/${currentCampaignSlug}/responses`, {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.success) {
showSuccess(data.message || 'Response submitted successfully! It will appear after moderation.');
closeSubmitModal();
// Don't reload responses since submission is pending approval
} else {
showError(data.error || 'Failed to submit response');
}
} catch (error) {
console.error('Error submitting response:', error);
showError('Failed to submit response');
}
}
// View image in modal/new tab
function viewImage(url) {
window.open(url, '_blank');
}
// Utility functions
function showLoading(show) {
document.getElementById('loading').style.display = show ? 'block' : 'none';
}
function showEmptyState() {
document.getElementById('empty-state').style.display = 'block';
document.getElementById('responses-container').innerHTML = '';
document.getElementById('load-more-container').style.display = 'none';
}
function showError(message) {
// Simple alert for now - could integrate with existing error display system
alert('Error: ' + message);
}
function showSuccess(message) {
// Simple alert for now - could integrate with existing success display system
alert(message);
}
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Close modal when clicking outside
window.onclick = function(event) {
const modal = document.getElementById('submit-modal');
if (event.target === modal) {
closeSubmitModal();
}
};