// 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);
});
// Load campaign data
this.loadCampaign();
}
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;
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;
// Show email count if enabled
if (this.campaign.show_email_count && this.campaign.emailCount !== null) {
document.getElementById('email-count').textContent = this.campaign.emailCount;
document.getElementById('campaign-stats').style.display = 'block';
}
// 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 email preview
document.getElementById('preview-subject').textContent = this.campaign.email_subject;
document.getElementById('preview-body').textContent = this.campaign.email_body;
document.getElementById('email-preview').style.display = 'block';
// 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';
}
}
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.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 {
await fetch(`/api/campaigns/${this.campaignSlug}/track-call`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
userEmail: this.userInfo.userEmail,
userName: this.userInfo.userName,
postalCode: this.userInfo.postalCode,
recipientPhone: phone,
recipientName: name,
recipientTitle: title,
officeType: officeType
})
});
} 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.campaign.email_subject);
const body = encodeURIComponent(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 response = 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: 'smtp'
})
});
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 countElement = document.getElementById('email-count');
const currentCount = parseInt(countElement.textContent) || 0;
countElement.textContent = currentCount + 1;
}
// You could show a toast or update UI to indicate success
alert(message); // Simple for now, could be improved with better UI
}
}
// Initialize the campaign page when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
window.campaignPage = new CampaignPage();
});