// Campaign Page Management Module class CampaignPage { constructor() { this.campaign = null; this.representatives = []; this.userInfo = {}; this.currentStep = 1; this.init(); } init() { // Get campaign slug from URL const pathParts = window.location.pathname.split('/'); this.campaignSlug = pathParts[pathParts.length - 1]; // Set up form handlers document.getElementById('user-info-form').addEventListener('submit', (e) => { this.handleUserInfoSubmit(e); }); // Postal code formatting document.getElementById('user-postal-code').addEventListener('input', (e) => { this.formatPostalCode(e); }); // Set up social share buttons this.setupShareButtons(); // Load campaign data this.loadCampaign(); } setupShareButtons() { // Get current URL const shareUrl = window.location.href; // 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 = this.campaign ? `Check out this campaign: ${this.campaign.title}` : 'Check out this campaign'; 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 = this.campaign ? `Check out this campaign: ${this.campaign.title}` : 'Check out this campaign'; const url = `https://wa.me/?text=${encodeURIComponent(text + ' ' + shareUrl)}`; window.open(url, '_blank'); }); // Email share document.getElementById('share-email')?.addEventListener('click', () => { const subject = this.campaign ? `Campaign: ${this.campaign.title}` : 'Check out this campaign'; const body = this.campaign ? `I thought you might be interested in this campaign:\n\n${this.campaign.title}\n\n${shareUrl}` : `Check out this campaign:\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', () => { this.openQRCodeModal(); }); } 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 const qrcodeUrl = `/api/campaigns/${this.campaignSlug}/qrcode?type=campaign`; 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 = `${this.campaignSlug}-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); } async loadCampaign() { this.showLoading('Loading campaign...'); try { const response = await fetch(`/api/campaigns/${this.campaignSlug}`); const data = await response.json(); if (!data.success) { throw new Error(data.error || 'Failed to load campaign'); } this.campaign = data.campaign; console.log('Campaign data loaded:', this.campaign); console.log('Cover photo value:', this.campaign.cover_photo); this.renderCampaign(); } catch (error) { this.showError('Failed to load campaign: ' + error.message); } finally { this.hideLoading(); } } renderCampaign() { // Update page title and header document.title = `${this.campaign.title} - BNKops Influence Tool`; document.getElementById('page-title').textContent = `${this.campaign.title} - BNKops Influence Tool`; document.getElementById('campaign-title').textContent = this.campaign.title; document.getElementById('campaign-description').textContent = this.campaign.description; // Add cover photo if available const headerElement = document.querySelector('.campaign-header'); if (this.campaign.cover_photo) { // Clean the cover_photo value - it should just be a filename const coverPhotoFilename = String(this.campaign.cover_photo).trim(); console.log('Cover photo filename:', coverPhotoFilename); if (coverPhotoFilename && coverPhotoFilename !== 'null' && coverPhotoFilename !== 'undefined') { headerElement.classList.add('has-cover'); headerElement.style.backgroundImage = `url('/uploads/${coverPhotoFilename}')`; } else { headerElement.classList.remove('has-cover'); headerElement.style.backgroundImage = ''; } } else { headerElement.classList.remove('has-cover'); headerElement.style.backgroundImage = ''; } // Show email count if enabled (show even if count is 0) const statsHeaderSection = document.getElementById('campaign-stats-header'); let hasStats = false; if (this.campaign.show_email_count && this.campaign.emailCount !== null && this.campaign.emailCount !== undefined) { // Header stats document.getElementById('email-count-header').textContent = this.campaign.emailCount; document.getElementById('email-stat-circle').style.display = 'flex'; hasStats = true; } // Show call count if enabled (show even if count is 0) if (this.campaign.show_call_count && this.campaign.callCount !== null && this.campaign.callCount !== undefined) { // Header stats document.getElementById('call-count-header').textContent = this.campaign.callCount; document.getElementById('call-stat-circle').style.display = 'flex'; hasStats = true; } // Show stats section if any stat is enabled if (hasStats) { statsHeaderSection.style.display = 'flex'; } // Show call to action if (this.campaign.call_to_action) { document.getElementById('call-to-action').innerHTML = `

${this.campaign.call_to_action}

`; document.getElementById('call-to-action').style.display = 'block'; } // Show response wall button if enabled if (this.campaign.show_response_wall) { const responseWallSection = document.getElementById('response-wall-section'); const responseWallLink = document.getElementById('response-wall-link'); if (responseWallSection && responseWallLink) { responseWallLink.href = `/response-wall.html?campaign=${this.campaignSlug}`; responseWallSection.style.display = 'block'; } } // Set up email preview this.setupEmailPreview(); // Set up email method options this.setupEmailMethodOptions(); // Show optional fields if user info collection is enabled if (this.campaign.collect_user_info) { const optionalFields = document.getElementById('optional-fields'); if (optionalFields) { optionalFields.style.display = 'block'; console.log('Showing optional user info fields'); } } // Set initial step this.setStep(1); } setupEmailMethodOptions() { const emailMethodSection = document.getElementById('email-method-selection'); const allowSMTP = this.campaign.allow_smtp_email; const allowMailto = this.campaign.allow_mailto_link; if (!emailMethodSection) { console.warn('Email method selection element not found'); return; } // Configure existing radio buttons instead of replacing HTML const smtpRadio = document.getElementById('method-smtp'); const mailtoRadio = document.getElementById('method-mailto'); if (allowSMTP && allowMailto) { // Both methods allowed - keep default setup smtpRadio.disabled = false; mailtoRadio.disabled = false; smtpRadio.checked = true; } else if (allowSMTP && !allowMailto) { // Only SMTP allowed smtpRadio.disabled = false; mailtoRadio.disabled = true; smtpRadio.checked = true; } else if (!allowSMTP && allowMailto) { // Only mailto allowed smtpRadio.disabled = true; mailtoRadio.disabled = false; mailtoRadio.checked = true; } else { // Neither allowed - hide the section emailMethodSection.style.display = 'none'; } } setupEmailPreview() { const emailPreview = document.getElementById('email-preview'); const previewDescription = document.getElementById('preview-description'); // Store original email content this.originalEmailSubject = this.campaign.email_subject; this.originalEmailBody = this.campaign.email_body; this.currentEmailSubject = this.campaign.email_subject; this.currentEmailBody = this.campaign.email_body; // Set up preview content document.getElementById('preview-subject').textContent = this.currentEmailSubject; document.getElementById('preview-body').textContent = this.currentEmailBody; // Set up editable fields document.getElementById('edit-subject').value = this.currentEmailSubject; document.getElementById('edit-body').value = this.currentEmailBody; if (this.campaign.allow_email_editing) { // Enable editing mode emailPreview.classList.remove('preview-mode'); emailPreview.classList.add('edit-mode'); previewDescription.textContent = 'You can edit this message before sending to your representatives:'; // Set up event listeners for editing this.setupEmailEditingListeners(); } else { // Read-only preview mode emailPreview.classList.remove('edit-mode'); emailPreview.classList.add('preview-mode'); previewDescription.textContent = 'This is the message that will be sent to your representatives:'; } emailPreview.style.display = 'block'; } setupEmailEditingListeners() { const editSubject = document.getElementById('edit-subject'); const editBody = document.getElementById('edit-body'); const previewBtn = document.getElementById('preview-email-btn'); const saveBtn = document.getElementById('save-email-btn'); const editBtn = document.getElementById('edit-email-btn'); // Auto-update current content as user types editSubject.addEventListener('input', (e) => { this.currentEmailSubject = e.target.value; }); editBody.addEventListener('input', (e) => { this.currentEmailBody = e.target.value; }); // Preview button - switch to preview mode previewBtn.addEventListener('click', () => { this.showEmailPreview(); }); // Save button - save changes and show preview saveBtn.addEventListener('click', () => { this.saveEmailChanges(); }); // Edit button - switch back to edit mode if (editBtn) { editBtn.addEventListener('click', () => { this.showEmailEditor(); }); } } showEmailPreview() { const emailPreview = document.getElementById('email-preview'); // Update preview content document.getElementById('preview-subject').textContent = this.currentEmailSubject; document.getElementById('preview-body').textContent = this.currentEmailBody; // Switch to preview mode emailPreview.classList.remove('edit-mode'); emailPreview.classList.add('preview-mode'); } showEmailEditor() { const emailPreview = document.getElementById('email-preview'); // Update edit fields with current content document.getElementById('edit-subject').value = this.currentEmailSubject; document.getElementById('edit-body').value = this.currentEmailBody; // Switch to edit mode emailPreview.classList.remove('preview-mode'); emailPreview.classList.add('edit-mode'); } toggleEmailPreview() { const emailPreview = document.getElementById('email-preview'); const previewBtn = document.getElementById('preview-email-btn'); if (emailPreview.classList.contains('edit-mode')) { // Switch to preview mode document.getElementById('preview-subject').textContent = this.currentEmailSubject; document.getElementById('preview-body').textContent = this.currentEmailBody; emailPreview.classList.remove('edit-mode'); emailPreview.classList.add('preview-mode'); previewBtn.textContent = '✏️ Edit'; } else { // Switch to edit mode emailPreview.classList.remove('preview-mode'); emailPreview.classList.add('edit-mode'); previewBtn.textContent = '👁️ Preview'; } } saveEmailChanges() { // Update preview content document.getElementById('preview-subject').textContent = this.currentEmailSubject; document.getElementById('preview-body').textContent = this.currentEmailBody; // Switch to preview mode this.showEmailPreview(); // Show success message this.showMessage('Email content updated successfully!', 'success'); // Switch to preview mode const emailPreview = document.getElementById('email-preview'); const previewBtn = document.getElementById('preview-email-btn'); emailPreview.classList.remove('edit-mode'); emailPreview.classList.add('preview-mode'); previewBtn.textContent = '✏️ Edit'; } showMessage(message, type = 'info') { // Use existing message display system if available if (window.messageDisplay) { window.messageDisplay.show(message, type); } else { // Fallback to alert alert(message); } } formatPostalCode(e) { let value = e.target.value.replace(/\s/g, '').toUpperCase(); if (value.length > 3) { value = value.substring(0, 3) + ' ' + value.substring(3, 6); } e.target.value = value; } async handleUserInfoSubmit(e) { e.preventDefault(); const formData = new FormData(e.target); this.userInfo = { postalCode: formData.get('postalCode').replace(/\s/g, '').toUpperCase(), userName: formData.get('userName') || '', userEmail: formData.get('userEmail') || '' }; // Track user info when they click "Find My Representatives" await this.trackUserInfo(); await this.loadRepresentatives(); } async trackUserInfo() { try { const response = await fetch(`/api/campaigns/${this.campaignSlug}/track-user`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userEmail: this.userInfo.userEmail, userName: this.userInfo.userName, postalCode: this.userInfo.postalCode }) }); const data = await response.json(); if (!data.success) { console.warn('Failed to track user info:', data.error); // Don't throw error - this is just tracking, shouldn't block the user } } catch (error) { console.warn('Failed to track user info:', error.message); // Don't throw error - this is just tracking, shouldn't block the user } } async loadRepresentatives() { this.showLoading('Finding your representatives...'); try { const response = await fetch(`/api/campaigns/${this.campaignSlug}/representatives/${this.userInfo.postalCode}`); const data = await response.json(); if (!data.success) { throw new Error(data.error || 'Failed to load representatives'); } this.representatives = data.representatives; this.renderRepresentatives(); this.setStep(2); // Scroll to representatives section document.getElementById('representatives-section').scrollIntoView({ behavior: 'smooth' }); } catch (error) { this.showError('Failed to load representatives: ' + error.message); } finally { this.hideLoading(); } } renderRepresentatives() { const list = document.getElementById('representatives-list'); if (this.representatives.length === 0) { list.innerHTML = '

No representatives found for your area. Please check your postal code.

'; return; } list.innerHTML = this.representatives.map(rep => `
${rep.photo_url ? `${rep.name}` : `
` }

${rep.name}

${rep.elected_office || 'Representative'}

${rep.party_name || ''}

${rep.email ? `

📧 ${rep.email}

` : ''} ${this.getPhoneNumber(rep) ? `

📞 ${this.getPhoneNumber(rep)}

` : ''}
${rep.email ? ` ` : ''} ${this.getPhoneNumber(rep) ? ` ` : ''} ${!rep.email && !this.getPhoneNumber(rep) ? '

No contact information available

' : ''}
`).join(''); // Attach event listeners to send email buttons this.attachEmailButtonListeners(); document.getElementById('representatives-section').style.display = 'block'; } attachEmailButtonListeners() { // Send email buttons document.querySelectorAll('[data-action="send-email"]').forEach(btn => { btn.addEventListener('click', (e) => { const email = e.target.dataset.email; const name = e.target.dataset.name; const title = e.target.dataset.title; const level = e.target.dataset.level; this.sendEmail(email, name, title, level); }); }); // Call buttons document.querySelectorAll('[data-action="call-representative"]').forEach(btn => { btn.addEventListener('click', (e) => { const phone = e.target.dataset.phone; const name = e.target.dataset.name; const title = e.target.dataset.title; const officeType = e.target.dataset.officeType; this.callRepresentative(phone, name, title, officeType); }); }); // Reload page button const reloadBtn = document.querySelector('[data-action="reload-page"]'); if (reloadBtn) { reloadBtn.addEventListener('click', () => { location.reload(); }); } } getGovernmentLevel(rep) { const office = (rep.elected_office || '').toLowerCase(); if (office.includes('mp') || office.includes('member of parliament')) return 'Federal'; if (office.includes('mla') || office.includes('legislative assembly')) return 'Provincial'; if (office.includes('mayor') || office.includes('councillor')) return 'Municipal'; if (office.includes('school')) return 'School Board'; return 'Other'; } getPhoneNumber(rep) { if (!rep.offices || !Array.isArray(rep.offices)) { return null; } // Find the first office with a phone number const officeWithPhone = rep.offices.find(office => office.tel); return officeWithPhone ? officeWithPhone.tel : null; } getPhoneOfficeType(rep) { if (!rep.offices || !Array.isArray(rep.offices)) { return 'office'; } const officeWithPhone = rep.offices.find(office => office.tel); return officeWithPhone ? (officeWithPhone.type || 'office') : 'office'; } callRepresentative(phone, name, title, officeType) { // Clean the phone number for tel: link (remove spaces, dashes, parentheses) const cleanPhone = phone.replace(/[\s\-\(\)]/g, ''); // Create tel: link const telLink = `tel:${cleanPhone}`; // Show confirmation dialog with formatted information const officeInfo = officeType ? ` (${officeType} office)` : ''; const message = `Call ${name}${title ? ` - ${title}` : ''}${officeInfo}?\n\nPhone: ${phone}`; if (confirm(message)) { // Attempt to initiate the call window.location.href = telLink; // Track the call attempt this.trackCall(phone, name, title, officeType); } } async trackCall(phone, name, title, officeType) { try { const response = await fetch(`/api/campaigns/${this.campaignSlug}/track-call`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ representativeName: name, representativeTitle: title || '', phoneNumber: phone, officeType: officeType || '', userEmail: this.userInfo.userEmail, userName: this.userInfo.userName, postalCode: this.userInfo.postalCode }) }); const data = await response.json(); if (data.success) { this.showCallSuccess('Call tracked successfully!'); } } catch (error) { console.error('Failed to track call:', error); } } async sendEmail(recipientEmail, recipientName, recipientTitle, recipientLevel) { const emailMethod = document.querySelector('input[name="emailMethod"]:checked').value; if (emailMethod === 'mailto') { this.openMailtoLink(recipientEmail); } else { await this.sendSMTPEmail(recipientEmail, recipientName, recipientTitle, recipientLevel); } } openMailtoLink(recipientEmail) { const subject = encodeURIComponent(this.currentEmailSubject || this.campaign.email_subject); const body = encodeURIComponent(this.currentEmailBody || this.campaign.email_body); const mailtoUrl = `mailto:${recipientEmail}?subject=${subject}&body=${body}`; // Track the mailto click this.trackEmail(recipientEmail, '', '', '', 'mailto'); window.open(mailtoUrl); } async sendSMTPEmail(recipientEmail, recipientName, recipientTitle, recipientLevel) { this.showLoading('Sending email...'); try { const emailData = { userEmail: this.userInfo.userEmail, userName: this.userInfo.userName, postalCode: this.userInfo.postalCode, recipientEmail, recipientName, recipientTitle, recipientLevel, emailMethod: 'smtp' }; // Include custom email content if email editing is enabled if (this.campaign.allow_email_editing) { emailData.customEmailSubject = this.currentEmailSubject || this.campaign.email_subject; emailData.customEmailBody = this.currentEmailBody || this.campaign.email_body; } const response = await fetch(`/api/campaigns/${this.campaignSlug}/send-email`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(emailData) }); const data = await response.json(); if (data.success) { this.showSuccess('Email sent successfully!'); } else { throw new Error(data.error || 'Failed to send email'); } } catch (error) { // Handle rate limit errors specifically if (error.message && error.message.includes('You can only send one email per representative every 5 minutes')) { this.showError('Rate limit reached: You can only send one email per representative every 5 minutes. Please wait before sending another email to this representative. You can still email other representatives.'); } else if (error.message && error.message.includes('Too many emails')) { this.showError('Too many emails sent. Please wait before sending more emails.'); } else { this.showError('Failed to send email: ' + error.message); } } finally { this.hideLoading(); } } async trackEmail(recipientEmail, recipientName, recipientTitle, recipientLevel, emailMethod) { try { await fetch(`/api/campaigns/${this.campaignSlug}/send-email`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userEmail: this.userInfo.userEmail, userName: this.userInfo.userName, postalCode: this.userInfo.postalCode, recipientEmail, recipientName, recipientTitle, recipientLevel, emailMethod }) }); } catch (error) { console.error('Failed to track email:', error); } } setStep(step) { // Reset all steps document.querySelectorAll('.step').forEach(s => { s.classList.remove('active', 'completed'); }); // Mark completed steps for (let i = 1; i < step; i++) { document.getElementById(`step-${this.getStepName(i)}`).classList.add('completed'); } // Mark current step document.getElementById(`step-${this.getStepName(step)}`).classList.add('active'); this.currentStep = step; } getStepName(step) { const steps = ['', 'info', 'postal', 'send']; return steps[step] || 'info'; } showLoading(message) { document.getElementById('loading-message').textContent = message; document.getElementById('loading-overlay').style.display = 'flex'; } hideLoading() { document.getElementById('loading-overlay').style.display = 'none'; } showError(message) { const errorDiv = document.getElementById('error-message'); errorDiv.textContent = message; errorDiv.style.display = 'block'; setTimeout(() => { errorDiv.style.display = 'none'; }, 5000); } showSuccess(message) { // Update email count if enabled if (this.campaign.show_email_count) { const countHeaderElement = document.getElementById('email-count-header'); const currentCount = parseInt(countHeaderElement?.textContent) || 0; const newCount = currentCount + 1; if (countHeaderElement) { countHeaderElement.textContent = newCount; } } // You could show a toast or update UI to indicate success alert(message); // Simple for now, could be improved with better UI } showCallSuccess(message) { // Update call count if enabled if (this.campaign.show_call_count) { const countHeaderElement = document.getElementById('call-count-header'); const currentCount = parseInt(countHeaderElement?.textContent) || 0; const newCount = currentCount + 1; if (countHeaderElement) { countHeaderElement.textContent = newCount; } } // Show success message alert(message); } } // Initialize the campaign page when DOM is loaded document.addEventListener('DOMContentLoaded', () => { window.campaignPage = new CampaignPage(); });