admin e5c32ad25a Add health check utility, logger, metrics, backup, and SMTP toggle scripts
- 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.
2025-10-23 11:33:00 -06:00

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();
});