- Implemented a comprehensive health check utility to monitor system dependencies including NocoDB, SMTP, Represent API, disk space, and memory usage. - Created a logger utility using Winston for structured logging with daily rotation and various log levels. - Developed a metrics utility using Prometheus client to track application performance metrics such as email sends, HTTP requests, and user activity. - Added a backup script for automated backups of NocoDB data, uploaded files, and environment configurations with optional S3 support. - Introduced a toggle script to switch between development (MailHog) and production (ProtonMail) SMTP configurations.
969 lines
39 KiB
JavaScript
969 lines
39 KiB
JavaScript
// 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;
|
|
|
|
// 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 = 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');
|
|
});
|
|
|
|
// Bluesky share
|
|
document.getElementById('share-bluesky')?.addEventListener('click', () => {
|
|
const text = this.campaign ? `Check out this campaign: ${this.campaign.title}` : 'Check out this campaign';
|
|
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 = this.campaign ? `${this.campaign.title}` : 'Check out this campaign';
|
|
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 = this.campaign ? `Check out this campaign: ${this.campaign.title}` : 'Check out this campaign';
|
|
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 = this.campaign ? `Check out this campaign: ${this.campaign.title}` : 'Check out this campaign';
|
|
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 = this.campaign ? `Check out this campaign: ${this.campaign.title}` : 'Check out this campaign';
|
|
// 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 = this.campaign ? `Check out this campaign: ${this.campaign.title}` : 'Check out this campaign';
|
|
const body = text + ' ' + shareUrl;
|
|
// Use Web Share API if available, otherwise fallback to SMS protocol
|
|
if (navigator.share) {
|
|
navigator.share({
|
|
title: this.campaign ? this.campaign.title : 'Campaign',
|
|
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 = 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 = `<p><strong>${this.campaign.call_to_action}</strong></p>`;
|
|
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 = '<p>No representatives found for your area. Please check your postal code.</p>';
|
|
return;
|
|
}
|
|
|
|
list.innerHTML = this.representatives.map(rep => `
|
|
<div class="rep-card">
|
|
<div class="rep-info">
|
|
${rep.photo_url ?
|
|
`<img src="${rep.photo_url}" alt="${rep.name}" class="rep-photo">` :
|
|
`<div class="rep-photo"></div>`
|
|
}
|
|
<div class="rep-details">
|
|
<h4>${rep.name}</h4>
|
|
<p>${rep.elected_office || 'Representative'}</p>
|
|
<p>${rep.party_name || ''}</p>
|
|
${rep.email ? `<p>📧 ${rep.email}</p>` : ''}
|
|
${this.getPhoneNumber(rep) ? `<p>📞 ${this.getPhoneNumber(rep)}</p>` : ''}
|
|
</div>
|
|
</div>
|
|
<div class="rep-actions">
|
|
${rep.email ? `
|
|
<button class="btn btn-primary" data-action="send-email"
|
|
data-email="${rep.email}"
|
|
data-name="${rep.name}"
|
|
data-title="${rep.elected_office || ''}"
|
|
data-level="${this.getGovernmentLevel(rep)}">
|
|
Send Email
|
|
</button>
|
|
` : ''}
|
|
${this.getPhoneNumber(rep) ? `
|
|
<button class="btn btn-success" data-action="call-representative"
|
|
data-phone="${this.getPhoneNumber(rep)}"
|
|
data-name="${rep.name}"
|
|
data-title="${rep.elected_office || ''}"
|
|
data-office-type="${this.getPhoneOfficeType(rep)}">
|
|
📞 Call
|
|
</button>
|
|
` : ''}
|
|
${!rep.email && !this.getPhoneNumber(rep) ? '<p style="text-align: center; color: #6c757d;">No contact information available</p>' : ''}
|
|
</div>
|
|
</div>
|
|
`).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();
|
|
}); |