// Response Wall JavaScript let currentCampaignSlug = null; let currentCampaign = null; let currentOffset = 0; let currentSort = 'recent'; let currentLevel = ''; const LIMIT = 20; let loadedRepresentatives = []; // 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); } // Postal code lookup button const lookupBtn = document.getElementById('lookup-rep-btn'); if (lookupBtn) { lookupBtn.addEventListener('click', handlePostalLookup); } // Postal code input formatting const postalInput = document.getElementById('modal-postal-code'); if (postalInput) { postalInput.addEventListener('input', formatPostalCodeInput); postalInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { e.preventDefault(); handlePostalLookup(); } }); } // Representative selection const repSelect = document.getElementById('rep-select'); if (repSelect) { repSelect.addEventListener('change', handleRepresentativeSelect); } console.log('Response Wall: Initialization complete'); }); // Postal Code Lookup Functions function formatPostalCodeInput(e) { let value = e.target.value.toUpperCase().replace(/[^A-Z0-9]/g, ''); // Format as A1A 1A1 if (value.length > 3) { value = value.slice(0, 3) + ' ' + value.slice(3, 6); } e.target.value = value; } function validatePostalCode(postalCode) { const cleaned = postalCode.replace(/\s/g, ''); // Check format: Letter-Number-Letter Number-Letter-Number const regex = /^[A-Z]\d[A-Z]\d[A-Z]\d$/; if (!regex.test(cleaned)) { return { valid: false, message: 'Please enter a valid postal code format (A1A 1A1)' }; } // Check if it's an Alberta postal code (starts with T) if (!cleaned.startsWith('T')) { return { valid: false, message: 'This tool is designed for Alberta postal codes only (starting with T)' }; } return { valid: true }; } async function handlePostalLookup() { const postalInput = document.getElementById('modal-postal-code'); const postalCode = postalInput.value.trim(); if (!postalCode) { showError('Please enter a postal code'); return; } const validation = validatePostalCode(postalCode); if (!validation.valid) { showError(validation.message); return; } const lookupBtn = document.getElementById('lookup-rep-btn'); lookupBtn.disabled = true; lookupBtn.textContent = '🔄 Searching...'; try { const response = await window.apiClient.getRepresentativesByPostalCode(postalCode); const data = response.data || response; loadedRepresentatives = data.representatives || []; if (loadedRepresentatives.length === 0) { showError('No representatives found for this postal code'); document.getElementById('rep-select-group').style.display = 'none'; } else { displayRepresentativeOptions(loadedRepresentatives); showSuccess(`Found ${loadedRepresentatives.length} representatives`); } } catch (error) { console.error('Postal lookup failed:', error); showError('Failed to lookup representatives: ' + error.message); } finally { lookupBtn.disabled = false; lookupBtn.textContent = '🔍 Search'; } } function displayRepresentativeOptions(representatives) { const repSelect = document.getElementById('rep-select'); const repSelectGroup = document.getElementById('rep-select-group'); // Clear existing options repSelect.innerHTML = ''; // Add representatives as options representatives.forEach((rep, index) => { const option = document.createElement('option'); option.value = index; // Format display text let displayText = rep.name; if (rep.district_name) { displayText += ` - ${rep.district_name}`; } if (rep.party_name) { displayText += ` (${rep.party_name})`; } displayText += ` [${rep.elected_office || 'Representative'}]`; option.textContent = displayText; repSelect.appendChild(option); }); // Show the select group repSelectGroup.style.display = 'block'; } function handleRepresentativeSelect(e) { const selectedIndex = e.target.value; if (selectedIndex === '') return; const rep = loadedRepresentatives[selectedIndex]; if (!rep) return; // Auto-fill form fields document.getElementById('representative-name').value = rep.name || ''; document.getElementById('representative-title').value = rep.elected_office || ''; // Set government level based on elected office const level = determineGovernmentLevel(rep.elected_office); document.getElementById('representative-level').value = level; // Store email for verification option if (rep.email) { // Handle email being either string or array const emailValue = Array.isArray(rep.email) ? rep.email[0] : rep.email; document.getElementById('representative-email').value = emailValue; // Enable verification checkbox if we have an email const verificationCheckbox = document.getElementById('send-verification'); verificationCheckbox.disabled = false; } else { document.getElementById('representative-email').value = ''; // Disable verification checkbox if no email const verificationCheckbox = document.getElementById('send-verification'); verificationCheckbox.disabled = true; verificationCheckbox.checked = false; } showSuccess('Representative details filled. Please complete the rest of the form.'); } function determineGovernmentLevel(electedOffice) { if (!electedOffice) return ''; const office = electedOffice.toLowerCase(); if (office.includes('mp') || office.includes('member of parliament')) { return 'Federal'; } else if (office.includes('mla') || office.includes('member of the legislative assembly')) { return 'Provincial'; } else if (office.includes('councillor') || office.includes('councilor') || office.includes('mayor')) { return 'Municipal'; } else if (office.includes('trustee') || office.includes('school board')) { return 'School Board'; } return ''; } // Load response statistics and campaign details async function loadResponseStats() { try { const response = await fetch(`/api/campaigns/${currentCampaignSlug}/response-stats`); const data = await response.json(); if (data.success) { // Store campaign data currentCampaign = data.campaign; // Update stats 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'; // Render campaign header with campaign info renderCampaignHeader(); } } catch (error) { console.error('Error loading stats:', error); } } // Render campaign header with background image and navigation function renderCampaignHeader() { console.log('renderCampaignHeader called with campaign:', currentCampaign); if (!currentCampaign) { console.warn('No campaign data available'); return; } const headerElement = document.querySelector('.response-wall-header'); if (!headerElement) { console.warn('Header element not found'); return; } // Update campaign name subtitle const campaignNameElement = document.getElementById('campaign-name'); const descriptionElement = document.getElementById('campaign-description'); console.log('Campaign name element:', campaignNameElement); console.log('Campaign title:', currentCampaign.title); if (campaignNameElement && currentCampaign.title) { campaignNameElement.textContent = currentCampaign.title; campaignNameElement.style.display = 'block'; console.log('Campaign name set to:', currentCampaign.title); } if (descriptionElement) { descriptionElement.textContent = currentCampaign.description || 'See what representatives are saying back to constituents'; } // Add cover photo if available if (currentCampaign.cover_photo) { headerElement.classList.add('has-cover'); headerElement.style.backgroundImage = `url(/uploads/${currentCampaign.cover_photo})`; } else { headerElement.classList.remove('has-cover'); headerElement.style.backgroundImage = ''; } // Set up navigation button listeners const campaignBtn = document.getElementById('nav-to-campaign'); const homeBtn = document.getElementById('nav-to-home'); if (campaignBtn) { campaignBtn.addEventListener('click', () => { window.location.href = `/campaign/${currentCampaign.slug}`; }); } if (homeBtn) { homeBtn.addEventListener('click', () => { window.location.href = '/'; }); } // Set up social share buttons setupShareButtons(); } // Setup social share buttons function setupShareButtons() { const shareUrl = window.location.href; // Social menu toggle const socialsToggle = document.getElementById('share-socials-toggle'); const socialsMenu = document.getElementById('share-socials-menu'); if (socialsToggle && socialsMenu) { socialsToggle.addEventListener('click', (e) => { e.stopPropagation(); socialsMenu.classList.toggle('show'); socialsToggle.classList.toggle('active'); }); // Close menu when clicking outside document.addEventListener('click', (e) => { if (!e.target.closest('.share-socials-container')) { socialsMenu.classList.remove('show'); socialsToggle.classList.remove('active'); } }); } // Facebook share document.getElementById('share-facebook')?.addEventListener('click', () => { const url = `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(shareUrl)}`; window.open(url, '_blank', 'width=600,height=400'); }); // Twitter share document.getElementById('share-twitter')?.addEventListener('click', () => { const text = currentCampaign ? `Check out responses from representatives for: ${currentCampaign.title}` : 'Check out representative responses'; const url = `https://twitter.com/intent/tweet?url=${encodeURIComponent(shareUrl)}&text=${encodeURIComponent(text)}`; window.open(url, '_blank', 'width=600,height=400'); }); // LinkedIn share document.getElementById('share-linkedin')?.addEventListener('click', () => { const url = `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(shareUrl)}`; window.open(url, '_blank', 'width=600,height=400'); }); // WhatsApp share document.getElementById('share-whatsapp')?.addEventListener('click', () => { const text = currentCampaign ? `Check out responses from representatives for: ${currentCampaign.title}` : 'Check out representative responses'; const url = `https://wa.me/?text=${encodeURIComponent(text + ' ' + shareUrl)}`; window.open(url, '_blank'); }); // Bluesky share document.getElementById('share-bluesky')?.addEventListener('click', () => { const text = currentCampaign ? `Check out responses from representatives for: ${currentCampaign.title}` : 'Check out representative responses'; const url = `https://bsky.app/intent/compose?text=${encodeURIComponent(text + ' ' + shareUrl)}`; window.open(url, '_blank', 'width=600,height=400'); }); // Instagram share (note: Instagram doesn't have direct web share, opens Instagram web) document.getElementById('share-instagram')?.addEventListener('click', () => { alert('To share on Instagram:\n1. Copy the link (use the copy button)\n2. Open Instagram app\n3. Create a post or story\n4. Paste the link in your caption'); // Automatically copy the link navigator.clipboard.writeText(shareUrl).catch(() => { console.log('Failed to copy link automatically'); }); }); // Reddit share document.getElementById('share-reddit')?.addEventListener('click', () => { const title = currentCampaign ? `Representative Responses: ${currentCampaign.title}` : 'Check out these representative responses'; const url = `https://www.reddit.com/submit?url=${encodeURIComponent(shareUrl)}&title=${encodeURIComponent(title)}`; window.open(url, '_blank', 'width=800,height=600'); }); // Threads share document.getElementById('share-threads')?.addEventListener('click', () => { const text = currentCampaign ? `Check out responses from representatives for: ${currentCampaign.title}` : 'Check out representative responses'; const url = `https://threads.net/intent/post?text=${encodeURIComponent(text + ' ' + shareUrl)}`; window.open(url, '_blank', 'width=600,height=400'); }); // Telegram share document.getElementById('share-telegram')?.addEventListener('click', () => { const text = currentCampaign ? `Check out responses from representatives for: ${currentCampaign.title}` : 'Check out representative responses'; const url = `https://t.me/share/url?url=${encodeURIComponent(shareUrl)}&text=${encodeURIComponent(text)}`; window.open(url, '_blank', 'width=600,height=400'); }); // Mastodon share document.getElementById('share-mastodon')?.addEventListener('click', () => { const text = currentCampaign ? `Check out responses from representatives for: ${currentCampaign.title}` : 'Check out representative responses'; // Mastodon requires instance selection - opens a composer with text const instance = prompt('Enter your Mastodon instance (e.g., mastodon.social):'); if (instance) { const url = `https://${instance}/share?text=${encodeURIComponent(text + ' ' + shareUrl)}`; window.open(url, '_blank', 'width=600,height=400'); } }); // SMS share document.getElementById('share-sms')?.addEventListener('click', () => { const text = currentCampaign ? `Check out responses from representatives for: ${currentCampaign.title}` : 'Check out representative responses'; const body = text + ' ' + shareUrl; // Use Web Share API if available, otherwise fallback to SMS protocol if (navigator.share) { navigator.share({ title: currentCampaign ? currentCampaign.title : 'Response Wall', text: body }).catch(() => { // Fallback to SMS protocol window.location.href = `sms:?&body=${encodeURIComponent(body)}`; }); } else { // SMS protocol (works on mobile) window.location.href = `sms:?&body=${encodeURIComponent(body)}`; } }); // Slack share document.getElementById('share-slack')?.addEventListener('click', () => { const url = `https://slack.com/intl/en-ca/share?url=${encodeURIComponent(shareUrl)}`; window.open(url, '_blank', 'width=600,height=400'); }); // Discord share document.getElementById('share-discord')?.addEventListener('click', () => { alert('To share on Discord:\n1. Copy the link (use the copy button)\n2. Open Discord\n3. Paste the link in any channel or DM\n\nDiscord will automatically create a preview!'); // Automatically copy the link navigator.clipboard.writeText(shareUrl).catch(() => { console.log('Failed to copy link automatically'); }); }); // Print/PDF share document.getElementById('share-print')?.addEventListener('click', () => { window.print(); }); // Email share document.getElementById('share-email')?.addEventListener('click', () => { const subject = currentCampaign ? `Representative Responses: ${currentCampaign.title}` : 'Check out these representative responses'; const body = currentCampaign ? `I thought you might be interested in seeing what representatives are saying about this campaign:\n\n${currentCampaign.title}\n\n${shareUrl}` : `Check out these representative responses:\n\n${shareUrl}`; window.location.href = `mailto:?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`; }); // Copy link document.getElementById('share-copy')?.addEventListener('click', async () => { const copyBtn = document.getElementById('share-copy'); try { await navigator.clipboard.writeText(shareUrl); copyBtn.classList.add('copied'); copyBtn.title = 'Copied!'; setTimeout(() => { copyBtn.classList.remove('copied'); copyBtn.title = 'Copy Link'; }, 2000); } catch (err) { // Fallback for older browsers const textArea = document.createElement('textarea'); textArea.value = shareUrl; textArea.style.position = 'fixed'; textArea.style.left = '-999999px'; document.body.appendChild(textArea); textArea.select(); try { document.execCommand('copy'); copyBtn.classList.add('copied'); copyBtn.title = 'Copied!'; setTimeout(() => { copyBtn.classList.remove('copied'); copyBtn.title = 'Copy Link'; }, 2000); } catch (err) { console.error('Failed to copy:', err); alert('Failed to copy link. Please copy manually: ' + shareUrl); } document.body.removeChild(textArea); } }); // QR code share document.getElementById('share-qrcode')?.addEventListener('click', () => { openQRCodeModal(); }); } function openQRCodeModal() { const modal = document.getElementById('qrcode-modal'); const qrcodeImage = document.getElementById('qrcode-image'); const closeBtn = modal.querySelector('.qrcode-close'); const downloadBtn = document.getElementById('download-qrcode-btn'); // Build QR code URL for response wall const qrcodeUrl = `/api/campaigns/${currentCampaignSlug}/qrcode?type=response-wall`; qrcodeImage.src = qrcodeUrl; // Show modal modal.classList.add('show'); // Close button handler const closeModal = () => { modal.classList.remove('show'); }; closeBtn.onclick = closeModal; // Close when clicking outside the modal content modal.onclick = (event) => { if (event.target === modal) { closeModal(); } }; // Download button handler downloadBtn.onclick = async () => { try { const response = await fetch(qrcodeUrl); const blob = await response.blob(); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${currentCampaignSlug}-response-wall-qrcode.png`; document.body.appendChild(a); a.click(); document.body.removeChild(a); window.URL.revokeObjectURL(url); } catch (error) { console.error('Failed to download QR code:', error); alert('Failed to download QR code. Please try again.'); } }; // Close on Escape key const handleEscape = (e) => { if (e.key === 'Escape') { closeModal(); document.removeEventListener('keydown', handleEscape); } }; document.addEventListener('keydown', handleEscape); } // 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 = `${escapeHtml(response.representative_level)}`; badges += `${escapeHtml(response.response_type)}`; if (response.is_verified) { badges = `✓ Verified` + 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 = `