Bunch of buag fixes and updates
This commit is contained in:
@@ -251,6 +251,7 @@ class AdminPanel {
|
||||
email_subject: formData.get('email_subject'),
|
||||
email_body: formData.get('email_body'),
|
||||
call_to_action: formData.get('call_to_action'),
|
||||
status: formData.get('status'),
|
||||
allow_smtp_email: formData.get('allow_smtp_email') === 'on',
|
||||
allow_mailto_link: formData.get('allow_mailto_link') === 'on',
|
||||
collect_user_info: formData.get('collect_user_info') === 'on',
|
||||
@@ -302,6 +303,7 @@ class AdminPanel {
|
||||
form.querySelector('[name="allow_mailto_link"]').checked = campaign.allow_mailto_link;
|
||||
form.querySelector('[name="collect_user_info"]').checked = campaign.collect_user_info;
|
||||
form.querySelector('[name="show_email_count"]').checked = campaign.show_email_count;
|
||||
form.querySelector('[name="allow_email_editing"]').checked = campaign.allow_email_editing;
|
||||
|
||||
// Government levels
|
||||
const targetLevels = campaign.target_government_levels ?
|
||||
|
||||
@@ -71,6 +71,11 @@ class APIClient {
|
||||
async sendEmail(emailData) {
|
||||
return this.post('/emails/send', emailData);
|
||||
}
|
||||
|
||||
// Preview email before sending
|
||||
async previewEmail(emailData) {
|
||||
return this.post('/emails/preview', emailData);
|
||||
}
|
||||
}
|
||||
|
||||
// Create global instance
|
||||
|
||||
@@ -66,10 +66,8 @@ class CampaignPage {
|
||||
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 preview
|
||||
this.setupEmailPreview();
|
||||
|
||||
// Set up email method options
|
||||
this.setupEmailMethodOptions();
|
||||
@@ -122,6 +120,115 @@ class CampaignPage {
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
// 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 - toggle between edit and preview mode
|
||||
previewBtn.addEventListener('click', () => {
|
||||
this.toggleEmailPreview();
|
||||
});
|
||||
|
||||
// Save button - save changes
|
||||
saveBtn.addEventListener('click', () => {
|
||||
this.saveEmailChanges();
|
||||
});
|
||||
}
|
||||
|
||||
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 the current values and show confirmation
|
||||
document.getElementById('preview-subject').textContent = this.currentEmailSubject;
|
||||
document.getElementById('preview-body').textContent = this.currentEmailBody;
|
||||
|
||||
// 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) {
|
||||
@@ -364,8 +471,8 @@ class CampaignPage {
|
||||
}
|
||||
|
||||
openMailtoLink(recipientEmail) {
|
||||
const subject = encodeURIComponent(this.campaign.email_subject);
|
||||
const body = encodeURIComponent(this.campaign.email_body);
|
||||
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
|
||||
@@ -378,21 +485,29 @@ class CampaignPage {
|
||||
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({
|
||||
userEmail: this.userInfo.userEmail,
|
||||
userName: this.userInfo.userName,
|
||||
postalCode: this.userInfo.postalCode,
|
||||
recipientEmail,
|
||||
recipientName,
|
||||
recipientTitle,
|
||||
recipientLevel,
|
||||
emailMethod: 'smtp'
|
||||
})
|
||||
body: JSON.stringify(emailData)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
@@ -2,34 +2,67 @@
|
||||
class EmailComposer {
|
||||
constructor() {
|
||||
this.modal = document.getElementById('email-modal');
|
||||
this.previewModal = document.getElementById('email-preview-modal');
|
||||
this.form = document.getElementById('email-form');
|
||||
this.closeBtn = document.getElementById('close-modal');
|
||||
this.closePreviewBtn = document.getElementById('close-preview-modal');
|
||||
this.cancelBtn = document.getElementById('cancel-email');
|
||||
this.cancelPreviewBtn = document.getElementById('cancel-preview');
|
||||
this.editBtn = document.getElementById('edit-email');
|
||||
this.confirmSendBtn = document.getElementById('confirm-send');
|
||||
this.messageTextarea = document.getElementById('email-message');
|
||||
this.charCounter = document.querySelector('.char-counter');
|
||||
|
||||
this.currentRecipient = null;
|
||||
this.currentEmailData = null;
|
||||
this.lastPreviewTime = 0; // Track last preview request time
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// Modal controls
|
||||
this.closeBtn.addEventListener('click', () => this.closeModal());
|
||||
this.closePreviewBtn.addEventListener('click', () => this.closePreviewModal());
|
||||
this.cancelBtn.addEventListener('click', () => this.closeModal());
|
||||
this.cancelPreviewBtn.addEventListener('click', () => this.closePreviewModal());
|
||||
this.editBtn.addEventListener('click', () => this.editEmail());
|
||||
this.confirmSendBtn.addEventListener('click', () => this.confirmSend());
|
||||
|
||||
// Click outside modal to close
|
||||
this.modal.addEventListener('click', (e) => {
|
||||
if (e.target === this.modal) this.closeModal();
|
||||
});
|
||||
this.previewModal.addEventListener('click', (e) => {
|
||||
if (e.target === this.previewModal) this.closePreviewModal();
|
||||
});
|
||||
|
||||
// Form handling
|
||||
this.form.addEventListener('submit', (e) => this.handleSubmit(e));
|
||||
// Form handling - now shows preview instead of sending directly
|
||||
this.form.addEventListener('submit', (e) => this.handlePreview(e));
|
||||
|
||||
// Character counter
|
||||
this.messageTextarea.addEventListener('input', () => this.updateCharCounter());
|
||||
|
||||
// Escape key to close modal
|
||||
// Add event listener to sender name field to update subject dynamically
|
||||
const senderNameField = document.getElementById('sender-name');
|
||||
const subjectField = document.getElementById('email-subject');
|
||||
const postalCodeField = document.getElementById('sender-postal-code');
|
||||
|
||||
senderNameField.addEventListener('input', () => {
|
||||
const senderName = senderNameField.value.trim() || 'your constituent';
|
||||
const postalCode = postalCodeField.value.trim();
|
||||
if (postalCode) {
|
||||
subjectField.value = `Message from ${senderName} from ${postalCode}`;
|
||||
}
|
||||
});
|
||||
|
||||
// Escape key to close modals
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && this.modal.style.display === 'block') {
|
||||
this.closeModal();
|
||||
if (e.key === 'Escape') {
|
||||
if (this.previewModal.style.display === 'block') {
|
||||
this.closePreviewModal();
|
||||
} else if (this.modal.style.display === 'block') {
|
||||
this.closeModal();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -57,7 +90,7 @@ class EmailComposer {
|
||||
document.getElementById('email-message').value = '';
|
||||
|
||||
// Set default subject
|
||||
document.getElementById('email-subject').value = `Message from your constituent in ${postalCode}`;
|
||||
document.getElementById('email-subject').value = `Message from your constituent from ${postalCode}`;
|
||||
|
||||
this.updateCharCounter();
|
||||
this.modal.style.display = 'block';
|
||||
@@ -68,7 +101,24 @@ class EmailComposer {
|
||||
|
||||
closeModal() {
|
||||
this.modal.style.display = 'none';
|
||||
// Only clear data if we're not showing preview (user is canceling)
|
||||
if (this.previewModal.style.display !== 'block') {
|
||||
this.currentRecipient = null;
|
||||
this.currentEmailData = null;
|
||||
}
|
||||
}
|
||||
|
||||
closePreviewModal() {
|
||||
this.previewModal.style.display = 'none';
|
||||
// Clear email data when closing preview (user canceling)
|
||||
this.currentRecipient = null;
|
||||
this.currentEmailData = null;
|
||||
}
|
||||
|
||||
editEmail() {
|
||||
// Close preview modal and return to compose modal without clearing data
|
||||
this.previewModal.style.display = 'none';
|
||||
this.modal.style.display = 'block';
|
||||
}
|
||||
|
||||
updateCharCounter() {
|
||||
@@ -141,9 +191,17 @@ class EmailComposer {
|
||||
return suspiciousPatterns.some(pattern => pattern.test(text));
|
||||
}
|
||||
|
||||
async handleSubmit(e) {
|
||||
async handlePreview(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Prevent duplicate calls within 2 seconds
|
||||
const currentTime = Date.now();
|
||||
if (currentTime - this.lastPreviewTime < 2000) {
|
||||
console.log('Preview request blocked - too soon since last request');
|
||||
return;
|
||||
}
|
||||
this.lastPreviewTime = currentTime;
|
||||
|
||||
const errors = this.validateForm();
|
||||
if (errors.length > 0) {
|
||||
window.messageDisplay.show(errors.join('<br>'), 'error');
|
||||
@@ -155,22 +213,118 @@ class EmailComposer {
|
||||
|
||||
try {
|
||||
submitButton.disabled = true;
|
||||
submitButton.textContent = 'Sending...';
|
||||
submitButton.textContent = 'Loading Preview...';
|
||||
|
||||
const emailData = {
|
||||
this.currentEmailData = {
|
||||
recipientEmail: document.getElementById('recipient-email').value,
|
||||
senderName: document.getElementById('sender-name').value.trim(),
|
||||
senderEmail: document.getElementById('sender-email').value.trim(),
|
||||
subject: document.getElementById('email-subject').value.trim(),
|
||||
message: document.getElementById('email-message').value.trim(),
|
||||
postalCode: document.getElementById('sender-postal-code').value
|
||||
postalCode: document.getElementById('sender-postal-code').value,
|
||||
recipientName: this.currentRecipient ? this.currentRecipient.name : null
|
||||
};
|
||||
|
||||
const result = await window.apiClient.sendEmail(emailData);
|
||||
const preview = await window.apiClient.previewEmail(this.currentEmailData);
|
||||
|
||||
if (preview.success) {
|
||||
this.showPreview(preview.preview);
|
||||
this.previewModal.style.display = 'block'; // Show preview modal first
|
||||
this.closeModal(); // Close the compose modal
|
||||
} else {
|
||||
throw new Error(preview.message || 'Failed to generate preview');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Email preview failed:', error);
|
||||
window.messageDisplay.show(`Failed to generate preview: ${error.message}`, 'error');
|
||||
} finally {
|
||||
submitButton.disabled = false;
|
||||
submitButton.textContent = originalText;
|
||||
}
|
||||
}
|
||||
|
||||
showPreview(preview) {
|
||||
// Populate preview modal with email details
|
||||
document.getElementById('preview-recipient').textContent = preview.to;
|
||||
document.getElementById('preview-sender').textContent = `${this.currentEmailData.senderName} <${this.currentEmailData.senderEmail}>`;
|
||||
document.getElementById('preview-subject').textContent = preview.subject;
|
||||
|
||||
// Show the HTML preview content using iframe for complete isolation
|
||||
const previewContent = document.getElementById('preview-content');
|
||||
|
||||
if (preview.html) {
|
||||
// Use iframe to completely isolate the email HTML from the parent page
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.style.width = '100%';
|
||||
iframe.style.minHeight = '400px';
|
||||
iframe.style.border = '1px solid #dee2e6';
|
||||
iframe.style.borderRadius = '6px';
|
||||
iframe.style.backgroundColor = '#ffffff';
|
||||
iframe.sandbox = 'allow-same-origin'; // Safe sandbox settings
|
||||
|
||||
// Clear previous content and add iframe
|
||||
previewContent.innerHTML = '';
|
||||
previewContent.appendChild(iframe);
|
||||
|
||||
// Write the HTML content to the iframe
|
||||
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
|
||||
iframeDoc.open();
|
||||
iframeDoc.write(preview.html);
|
||||
iframeDoc.close();
|
||||
|
||||
// Auto-resize iframe to content height
|
||||
iframe.onload = () => {
|
||||
try {
|
||||
const body = iframe.contentDocument.body;
|
||||
const html = iframe.contentDocument.documentElement;
|
||||
const height = Math.max(
|
||||
body.scrollHeight,
|
||||
body.offsetHeight,
|
||||
html.clientHeight,
|
||||
html.scrollHeight,
|
||||
html.offsetHeight
|
||||
);
|
||||
iframe.style.height = Math.min(height + 20, 600) + 'px'; // Max height of 600px
|
||||
} catch (e) {
|
||||
// Fallback height if auto-resize fails
|
||||
iframe.style.height = '400px';
|
||||
}
|
||||
};
|
||||
|
||||
} else if (preview.text) {
|
||||
previewContent.innerHTML = `<pre style="white-space: pre-wrap; font-family: inherit; padding: 20px; background-color: #f8f9fa; border-radius: 6px; border: 1px solid #dee2e6;">${this.escapeHtml(preview.text)}</pre>`;
|
||||
} else {
|
||||
previewContent.innerHTML = '<p style="padding: 20px; text-align: center; color: #666;">No preview content available</p>';
|
||||
}
|
||||
}
|
||||
|
||||
// Escape HTML to prevent injection when showing text content
|
||||
escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
async confirmSend() {
|
||||
if (!this.currentEmailData) {
|
||||
window.messageDisplay.show('No email data to send', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmButton = this.confirmSendBtn;
|
||||
const originalText = confirmButton.textContent;
|
||||
|
||||
try {
|
||||
confirmButton.disabled = true;
|
||||
confirmButton.textContent = 'Sending...';
|
||||
|
||||
const result = await window.apiClient.sendEmail(this.currentEmailData);
|
||||
|
||||
if (result.success) {
|
||||
window.messageDisplay.show('Email sent successfully! Your representative will receive your message.', 'success');
|
||||
this.closeModal();
|
||||
this.closePreviewModal();
|
||||
this.currentEmailData = null;
|
||||
} else {
|
||||
throw new Error(result.message || 'Failed to send email');
|
||||
}
|
||||
@@ -195,8 +349,8 @@ class EmailComposer {
|
||||
window.messageDisplay.show(`Failed to send email: ${error.message}`, 'error');
|
||||
}
|
||||
} finally {
|
||||
submitButton.disabled = false;
|
||||
submitButton.textContent = originalText;
|
||||
confirmButton.disabled = false;
|
||||
confirmButton.textContent = originalText;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ class EmailTesting {
|
||||
try {
|
||||
const response = await this.apiClient.post('/api/emails/test', {
|
||||
subject: 'Quick Test Email',
|
||||
message: 'This is a quick test email sent from the Alberta Influence Campaign Tool email testing interface.'
|
||||
message: 'This is a quick test email sent from the BNKops Influence Campaign Tool email testing interface.'
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
|
||||
@@ -13,15 +13,38 @@ class MainApp {
|
||||
|
||||
// Add global error handling
|
||||
window.addEventListener('error', (e) => {
|
||||
console.error('Global error:', e.error);
|
||||
window.messageDisplay.show('An unexpected error occurred. Please refresh the page and try again.', 'error');
|
||||
// Only log and show message for actual errors, not null/undefined
|
||||
if (e.error) {
|
||||
console.error('Global error:', e.error);
|
||||
console.error('Error details:', {
|
||||
message: e.message,
|
||||
filename: e.filename,
|
||||
lineno: e.lineno,
|
||||
colno: e.colno,
|
||||
error: e.error
|
||||
});
|
||||
window.messageDisplay?.show('An unexpected error occurred. Please refresh the page and try again.', 'error');
|
||||
} else {
|
||||
// Just log these non-critical errors without showing popup
|
||||
console.log('Non-critical error event:', {
|
||||
message: e.message,
|
||||
filename: e.filename,
|
||||
lineno: e.lineno,
|
||||
colno: e.colno,
|
||||
type: e.type
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Add unhandled promise rejection handling
|
||||
window.addEventListener('unhandledrejection', (e) => {
|
||||
console.error('Unhandled promise rejection:', e.reason);
|
||||
window.messageDisplay.show('An unexpected error occurred. Please try again.', 'error');
|
||||
e.preventDefault();
|
||||
if (e.reason) {
|
||||
console.error('Unhandled promise rejection:', e.reason);
|
||||
window.messageDisplay?.show('An unexpected error occurred. Please try again.', 'error');
|
||||
e.preventDefault();
|
||||
} else {
|
||||
console.log('Non-critical promise rejection:', e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -136,9 +136,8 @@ class PostalLookup {
|
||||
locationText += ` • ${data.city}, ${data.province}`;
|
||||
}
|
||||
if (data.source || apiResponse.source) {
|
||||
locationText += ` • Data source: ${data.source || apiResponse.source}`;
|
||||
locationText += ` • Pulled From: ${data.source || apiResponse.source}`;
|
||||
}
|
||||
locationText += ` • Data source: api`;
|
||||
this.locationDetails.textContent = locationText;
|
||||
|
||||
// Show representatives
|
||||
|
||||
@@ -117,7 +117,7 @@ class RepresentativesDisplay {
|
||||
data-name="${name}"
|
||||
data-office="${office}"
|
||||
data-district="${district}">
|
||||
Send Email
|
||||
📧 Send Email
|
||||
</button>` :
|
||||
'<span class="text-muted">No email available</span>';
|
||||
|
||||
@@ -131,8 +131,11 @@ class RepresentativesDisplay {
|
||||
📞 Call
|
||||
</button>` : '';
|
||||
|
||||
// Add visit buttons for all available office addresses
|
||||
const visitButtons = this.createVisitButtons(rep.offices || [], name, office);
|
||||
|
||||
const profileUrl = rep.url ?
|
||||
`<a href="${rep.url}" target="_blank" class="btn btn-secondary">View Profile</a>` : '';
|
||||
`<a href="${rep.url}" target="_blank" class="btn btn-secondary">👤 View Profile</a>` : '';
|
||||
|
||||
// Generate initials for fallback
|
||||
const initials = name.split(' ')
|
||||
@@ -145,7 +148,7 @@ class RepresentativesDisplay {
|
||||
`<div class="rep-photo">
|
||||
<img src="${photoUrl}"
|
||||
alt="${name}"
|
||||
onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';"
|
||||
data-fallback-initials="${initials}"
|
||||
loading="lazy">
|
||||
<div class="rep-photo-fallback" style="display: none;">
|
||||
${initials}
|
||||
@@ -176,6 +179,7 @@ class RepresentativesDisplay {
|
||||
${callButton}
|
||||
${profileUrl}
|
||||
</div>
|
||||
${visitButtons ? `<div class="rep-visit-buttons">${visitButtons}</div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -185,8 +189,63 @@ class RepresentativesDisplay {
|
||||
const phoneNumbers = [];
|
||||
|
||||
if (Array.isArray(offices)) {
|
||||
// Priority order for office types (prefer local over remote)
|
||||
const officePriorities = ['constituency', 'district', 'local', 'regional', 'legislature'];
|
||||
|
||||
// First, try to find offices with Alberta addresses (for MPs)
|
||||
const albertaOffices = offices.filter(office => {
|
||||
const address = office.postal || office.address || '';
|
||||
return address.toLowerCase().includes('alberta') ||
|
||||
address.toLowerCase().includes(' ab ') ||
|
||||
address.toLowerCase().includes('edmonton') ||
|
||||
address.toLowerCase().includes('calgary') ||
|
||||
address.toLowerCase().includes('red deer') ||
|
||||
address.toLowerCase().includes('lethbridge') ||
|
||||
address.toLowerCase().includes('medicine hat');
|
||||
});
|
||||
|
||||
// Add phone numbers from Alberta offices first
|
||||
if (albertaOffices.length > 0) {
|
||||
for (const priority of officePriorities) {
|
||||
const priorityOffice = albertaOffices.find(office =>
|
||||
office.type === priority && office.tel
|
||||
);
|
||||
if (priorityOffice) {
|
||||
phoneNumbers.push({
|
||||
number: priorityOffice.tel,
|
||||
type: priorityOffice.type || 'office'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Add any remaining Alberta office phone numbers
|
||||
albertaOffices.forEach(office => {
|
||||
if (office.tel && !phoneNumbers.find(p => p.number === office.tel)) {
|
||||
phoneNumbers.push({
|
||||
number: office.tel,
|
||||
type: office.type || 'office'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Then add phone numbers from other offices by priority
|
||||
for (const priority of officePriorities) {
|
||||
const priorityOffice = offices.find(office =>
|
||||
office.type === priority && office.tel &&
|
||||
!phoneNumbers.find(p => p.number === office.tel)
|
||||
);
|
||||
if (priorityOffice) {
|
||||
phoneNumbers.push({
|
||||
number: priorityOffice.tel,
|
||||
type: priorityOffice.type || 'office'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, add any remaining phone numbers
|
||||
offices.forEach(office => {
|
||||
if (office.tel) {
|
||||
if (office.tel && !phoneNumbers.find(p => p.number === office.tel)) {
|
||||
phoneNumbers.push({
|
||||
number: office.tel,
|
||||
type: office.type || 'office'
|
||||
@@ -198,6 +257,102 @@ class RepresentativesDisplay {
|
||||
return phoneNumbers;
|
||||
}
|
||||
|
||||
createVisitButtons(offices, repName, repOffice) {
|
||||
if (!Array.isArray(offices) || offices.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const validOffices = offices.filter(office => office.postal || office.address);
|
||||
if (validOffices.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Sort offices by priority (local first)
|
||||
const sortedOffices = validOffices.sort((a, b) => {
|
||||
const aAddress = (a.postal || a.address || '').toLowerCase();
|
||||
const bAddress = (b.postal || b.address || '').toLowerCase();
|
||||
|
||||
// Check if address is in Alberta
|
||||
const aIsAlberta = aAddress.includes('alberta') || aAddress.includes(' ab ') ||
|
||||
aAddress.includes('edmonton') || aAddress.includes('calgary');
|
||||
const bIsAlberta = bAddress.includes('alberta') || bAddress.includes(' ab ') ||
|
||||
bAddress.includes('edmonton') || bAddress.includes('calgary');
|
||||
|
||||
if (aIsAlberta && !bIsAlberta) return -1;
|
||||
if (!aIsAlberta && bIsAlberta) return 1;
|
||||
|
||||
// If both are Alberta or both are not, prefer constituency over legislature
|
||||
const typePriority = { 'constituency': 1, 'district': 2, 'local': 3, 'regional': 4, 'legislature': 5 };
|
||||
const aPriority = typePriority[a.type] || 6;
|
||||
const bPriority = typePriority[b.type] || 6;
|
||||
|
||||
return aPriority - bPriority;
|
||||
});
|
||||
|
||||
return sortedOffices.map(office => {
|
||||
const address = office.postal || office.address;
|
||||
const officeType = this.getOfficeTypeLabel(office.type, address);
|
||||
const isLocal = this.isLocalAddress(address);
|
||||
|
||||
return `
|
||||
<button class="btn btn-sm btn-secondary visit-office"
|
||||
data-address="${address}"
|
||||
data-name="${repName}"
|
||||
data-office="${repOffice}"
|
||||
title="Visit ${officeType} office">
|
||||
🗺️ ${officeType}${isLocal ? ' 📍' : ''}
|
||||
<small class="office-location">${this.getShortAddress(address)}</small>
|
||||
</button>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
getOfficeTypeLabel(type, address) {
|
||||
if (!type) {
|
||||
// Try to determine type from address
|
||||
const addr = address.toLowerCase();
|
||||
if (addr.includes('ottawa') || addr.includes('parliament') || addr.includes('house of commons')) {
|
||||
return 'Ottawa';
|
||||
} else if (addr.includes('legislature') || addr.includes('provincial')) {
|
||||
return 'Legislature';
|
||||
} else if (addr.includes('city hall')) {
|
||||
return 'City Hall';
|
||||
}
|
||||
return 'Office';
|
||||
}
|
||||
|
||||
const typeLabels = {
|
||||
'constituency': 'Local Office',
|
||||
'district': 'District Office',
|
||||
'local': 'Local Office',
|
||||
'regional': 'Regional Office',
|
||||
'legislature': 'Legislature'
|
||||
};
|
||||
|
||||
return typeLabels[type] || type.charAt(0).toUpperCase() + type.slice(1);
|
||||
}
|
||||
|
||||
isLocalAddress(address) {
|
||||
const addr = address.toLowerCase();
|
||||
return addr.includes('alberta') || addr.includes(' ab ') ||
|
||||
addr.includes('edmonton') || addr.includes('calgary') ||
|
||||
addr.includes('red deer') || addr.includes('lethbridge') ||
|
||||
addr.includes('medicine hat');
|
||||
}
|
||||
|
||||
getShortAddress(address) {
|
||||
// Extract city and province/state for short display
|
||||
const parts = address.split(',');
|
||||
if (parts.length >= 2) {
|
||||
const city = parts[parts.length - 2].trim();
|
||||
const province = parts[parts.length - 1].trim();
|
||||
return `${city}, ${province}`;
|
||||
}
|
||||
|
||||
// Fallback: just show first part
|
||||
return parts[0].trim();
|
||||
}
|
||||
|
||||
attachEventListeners() {
|
||||
// Add event listeners for compose email buttons
|
||||
const composeButtons = this.container.querySelectorAll('.compose-email');
|
||||
@@ -229,6 +384,33 @@ class RepresentativesDisplay {
|
||||
this.handleCallClick(phone, name, office, officeType);
|
||||
});
|
||||
});
|
||||
|
||||
// Add event listeners for visit buttons
|
||||
const visitButtons = this.container.querySelectorAll('.visit-office');
|
||||
visitButtons.forEach(button => {
|
||||
button.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
// Use currentTarget to ensure we get the button, not nested elements
|
||||
const address = button.dataset.address;
|
||||
const name = button.dataset.name;
|
||||
const office = button.dataset.office;
|
||||
|
||||
this.handleVisitClick(address, name, office);
|
||||
});
|
||||
});
|
||||
|
||||
// Add event listeners for image error handling
|
||||
const repImages = this.container.querySelectorAll('.rep-photo img');
|
||||
repImages.forEach(img => {
|
||||
img.addEventListener('error', (e) => {
|
||||
// Hide the image and show the fallback
|
||||
e.target.style.display = 'none';
|
||||
const fallback = e.target.nextElementSibling;
|
||||
if (fallback && fallback.classList.contains('rep-photo-fallback')) {
|
||||
fallback.style.display = 'flex';
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
handleCallClick(phone, name, office, officeType) {
|
||||
@@ -247,6 +429,51 @@ class RepresentativesDisplay {
|
||||
window.location.href = telLink;
|
||||
}
|
||||
}
|
||||
|
||||
handleVisitClick(address, name, office) {
|
||||
// Clean and format the address for URL encoding
|
||||
const cleanAddress = address.replace(/\n/g, ', ').trim();
|
||||
|
||||
// Show confirmation dialog
|
||||
const message = `Open directions to ${name}'s office?\n\nAddress: ${cleanAddress}`;
|
||||
|
||||
if (confirm(message)) {
|
||||
// Create maps URL - this will work on most platforms
|
||||
// For mobile devices, it will open the default maps app
|
||||
// For desktop, it will open Google Maps in browser
|
||||
const encodedAddress = encodeURIComponent(cleanAddress);
|
||||
|
||||
// Try different map services based on user agent
|
||||
const userAgent = navigator.userAgent.toLowerCase();
|
||||
let mapsUrl;
|
||||
|
||||
if (userAgent.includes('iphone') || userAgent.includes('ipad')) {
|
||||
// iOS - use Apple Maps
|
||||
mapsUrl = `maps://maps.apple.com/?q=${encodedAddress}`;
|
||||
|
||||
// Fallback to Google Maps if Apple Maps doesn't work
|
||||
setTimeout(() => {
|
||||
window.open(`https://www.google.com/maps/search/${encodedAddress}`, '_blank');
|
||||
}, 500);
|
||||
|
||||
window.location.href = mapsUrl;
|
||||
} else if (userAgent.includes('android')) {
|
||||
// Android - use Google Maps app if available
|
||||
mapsUrl = `geo:0,0?q=${encodedAddress}`;
|
||||
|
||||
// Fallback to Google Maps web
|
||||
setTimeout(() => {
|
||||
window.open(`https://www.google.com/maps/search/${encodedAddress}`, '_blank');
|
||||
}, 500);
|
||||
|
||||
window.location.href = mapsUrl;
|
||||
} else {
|
||||
// Desktop or other - open Google Maps in new tab
|
||||
mapsUrl = `https://www.google.com/maps/search/${encodedAddress}`;
|
||||
window.open(mapsUrl, '_blank');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize when DOM is loaded
|
||||
|
||||
583
influence/app/public/js/representatives-map.js
Normal file
583
influence/app/public/js/representatives-map.js
Normal file
@@ -0,0 +1,583 @@
|
||||
/**
|
||||
* Representatives Map Module
|
||||
* Handles map initialization, office location display, and popup cards
|
||||
*/
|
||||
|
||||
// Map state
|
||||
let representativesMap = null;
|
||||
let representativeMarkers = [];
|
||||
let currentPostalCode = null;
|
||||
|
||||
// Office location icons
|
||||
const officeIcons = {
|
||||
federal: L.divIcon({
|
||||
className: 'office-marker federal',
|
||||
html: '<div class="marker-content">🏛️</div>',
|
||||
iconSize: [40, 40],
|
||||
iconAnchor: [20, 40],
|
||||
popupAnchor: [0, -40]
|
||||
}),
|
||||
provincial: L.divIcon({
|
||||
className: 'office-marker provincial',
|
||||
html: '<div class="marker-content">🏢</div>',
|
||||
iconSize: [40, 40],
|
||||
iconAnchor: [20, 40],
|
||||
popupAnchor: [0, -40]
|
||||
}),
|
||||
municipal: L.divIcon({
|
||||
className: 'office-marker municipal',
|
||||
html: '<div class="marker-content">🏛️</div>',
|
||||
iconSize: [40, 40],
|
||||
iconAnchor: [20, 40],
|
||||
popupAnchor: [0, -40]
|
||||
})
|
||||
};
|
||||
|
||||
// Initialize the representatives map
|
||||
function initializeRepresentativesMap() {
|
||||
const mapContainer = document.getElementById('main-map');
|
||||
if (!mapContainer) {
|
||||
console.warn('Map container not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Avoid double initialization
|
||||
if (representativesMap) {
|
||||
console.log('Map already initialized, invalidating size instead');
|
||||
representativesMap.invalidateSize();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if Leaflet is available
|
||||
if (typeof L === 'undefined') {
|
||||
console.error('Leaflet (L) is not defined. Map initialization failed.');
|
||||
return;
|
||||
}
|
||||
|
||||
// We'll initialize the map even if not visible, then invalidate size when needed
|
||||
|
||||
console.log('Initializing representatives map...');
|
||||
|
||||
// Center on Alberta
|
||||
representativesMap = L.map('main-map').setView([53.9333, -116.5765], 6);
|
||||
|
||||
// Add tile layer
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
||||
maxZoom: 19,
|
||||
minZoom: 2
|
||||
}).addTo(representativesMap);
|
||||
|
||||
// Trigger size invalidation after a brief moment to ensure proper rendering
|
||||
setTimeout(() => {
|
||||
if (representativesMap) {
|
||||
representativesMap.invalidateSize();
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
|
||||
// Clear all representative markers from the map
|
||||
function clearRepresentativeMarkers() {
|
||||
representativeMarkers.forEach(marker => {
|
||||
representativesMap.removeLayer(marker);
|
||||
});
|
||||
representativeMarkers = [];
|
||||
}
|
||||
|
||||
// Add representative offices to the map
|
||||
function displayRepresentativeOffices(representatives, postalCode) {
|
||||
// Initialize map if not already done
|
||||
if (!representativesMap) {
|
||||
console.log('Map not initialized, initializing now...');
|
||||
initializeRepresentativesMap();
|
||||
}
|
||||
|
||||
if (!representativesMap) {
|
||||
console.error('Failed to initialize map');
|
||||
return;
|
||||
}
|
||||
|
||||
clearRepresentativeMarkers();
|
||||
currentPostalCode = postalCode;
|
||||
|
||||
const validOffices = [];
|
||||
let bounds = [];
|
||||
|
||||
console.log('Processing representatives for map display:', representatives.length);
|
||||
|
||||
// Group representatives by office location to handle shared addresses
|
||||
const locationGroups = new Map();
|
||||
|
||||
representatives.forEach((rep, index) => {
|
||||
console.log(`Processing representative ${index + 1}:`, rep.name, rep.representative_set_name);
|
||||
|
||||
// Try to get office location from various sources
|
||||
const offices = getOfficeLocations(rep);
|
||||
console.log(`Found ${offices.length} offices for ${rep.name}:`, offices);
|
||||
|
||||
offices.forEach((office, officeIndex) => {
|
||||
console.log(`Office ${officeIndex + 1} for ${rep.name}:`, office);
|
||||
|
||||
if (office.lat && office.lng) {
|
||||
const locationKey = `${office.lat.toFixed(6)},${office.lng.toFixed(6)}`;
|
||||
|
||||
if (!locationGroups.has(locationKey)) {
|
||||
locationGroups.set(locationKey, {
|
||||
lat: office.lat,
|
||||
lng: office.lng,
|
||||
address: office.address,
|
||||
representatives: [],
|
||||
offices: []
|
||||
});
|
||||
}
|
||||
|
||||
locationGroups.get(locationKey).representatives.push(rep);
|
||||
locationGroups.get(locationKey).offices.push(office);
|
||||
|
||||
validOffices.push({ rep, office });
|
||||
} else {
|
||||
console.log(`No coordinates found for ${rep.name} office:`, office);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Create markers for each location group
|
||||
let offsetIndex = 0;
|
||||
locationGroups.forEach((locationGroup, locationKey) => {
|
||||
console.log(`Creating markers for location ${locationKey} with ${locationGroup.representatives.length} representatives`);
|
||||
|
||||
if (locationGroup.representatives.length === 1) {
|
||||
// Single representative at this location
|
||||
const rep = locationGroup.representatives[0];
|
||||
const office = locationGroup.offices[0];
|
||||
const marker = createOfficeMarker(rep, office);
|
||||
if (marker) {
|
||||
representativeMarkers.push(marker);
|
||||
marker.addTo(representativesMap);
|
||||
bounds.push([office.lat, office.lng]);
|
||||
}
|
||||
} else {
|
||||
// Multiple representatives at same location - create offset markers
|
||||
locationGroup.representatives.forEach((rep, repIndex) => {
|
||||
const office = locationGroup.offices[repIndex];
|
||||
|
||||
// Add small offset to avoid exact overlap
|
||||
const offsetDistance = 0.0005; // About 50 meters
|
||||
const angle = (repIndex * 2 * Math.PI) / locationGroup.representatives.length;
|
||||
const offsetLat = office.lat + (offsetDistance * Math.cos(angle));
|
||||
const offsetLng = office.lng + (offsetDistance * Math.sin(angle));
|
||||
|
||||
const offsetOffice = {
|
||||
...office,
|
||||
lat: offsetLat,
|
||||
lng: offsetLng
|
||||
};
|
||||
|
||||
console.log(`Creating offset marker for ${rep.name} at ${offsetLat}, ${offsetLng}`);
|
||||
const marker = createOfficeMarker(rep, offsetOffice, locationGroup.representatives.length > 1);
|
||||
if (marker) {
|
||||
representativeMarkers.push(marker);
|
||||
marker.addTo(representativesMap);
|
||||
bounds.push([offsetLat, offsetLng]);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`Total markers created: ${representativeMarkers.length}`);
|
||||
console.log(`Bounds array:`, bounds);
|
||||
|
||||
// Fit map to show all offices, or center on Alberta if no offices found
|
||||
if (bounds.length > 0) {
|
||||
representativesMap.fitBounds(bounds, { padding: [20, 20] });
|
||||
} else {
|
||||
// If no office locations found, show a message and keep Alberta view
|
||||
console.log('No office locations with coordinates found, showing message');
|
||||
showMapMessage('Office locations not available for representatives in this area.');
|
||||
}
|
||||
|
||||
console.log(`Displayed ${validOffices.length} office locations on map`);
|
||||
}
|
||||
|
||||
// Extract office locations from representative data
|
||||
function getOfficeLocations(representative) {
|
||||
const offices = [];
|
||||
|
||||
console.log(`Getting office locations for ${representative.name}`);
|
||||
console.log('Representative offices data:', representative.offices);
|
||||
|
||||
// Check various sources for office location data
|
||||
if (representative.offices && Array.isArray(representative.offices)) {
|
||||
representative.offices.forEach((office, index) => {
|
||||
console.log(`Processing office ${index + 1}:`, office);
|
||||
|
||||
// Use the 'postal' field which contains the address
|
||||
if (office.postal || office.address) {
|
||||
const officeData = {
|
||||
type: office.type || 'office',
|
||||
address: office.postal || office.address || 'Office Address',
|
||||
postal_code: office.postal_code,
|
||||
phone: office.tel || office.phone,
|
||||
fax: office.fax,
|
||||
lat: office.lat,
|
||||
lng: office.lng
|
||||
};
|
||||
console.log('Created office data:', officeData);
|
||||
offices.push(officeData);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// For all offices without coordinates, add approximate coordinates
|
||||
offices.forEach(office => {
|
||||
if (!office.lat || !office.lng) {
|
||||
console.log(`Adding coordinates to office for ${representative.name}`);
|
||||
const approxLocation = getApproximateLocationByDistrict(representative.district_name, representative.representative_set_name);
|
||||
console.log('Approximate location:', approxLocation);
|
||||
|
||||
if (approxLocation) {
|
||||
office.lat = approxLocation.lat;
|
||||
office.lng = approxLocation.lng;
|
||||
console.log('Updated office with coordinates:', office);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// If no offices found at all, create a fallback office
|
||||
if (offices.length === 0 && representative.representative_set_name) {
|
||||
console.log(`No offices found, creating fallback office for ${representative.name}`);
|
||||
const approxLocation = getApproximateLocationByDistrict(representative.district_name, representative.representative_set_name);
|
||||
console.log('Approximate location:', approxLocation);
|
||||
|
||||
if (approxLocation) {
|
||||
const fallbackOffice = {
|
||||
type: 'representative',
|
||||
address: `${representative.name} - ${representative.district_name || representative.representative_set_name}`,
|
||||
lat: approxLocation.lat,
|
||||
lng: approxLocation.lng
|
||||
};
|
||||
console.log('Created fallback office:', fallbackOffice);
|
||||
offices.push(fallbackOffice);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Total offices found for ${representative.name}:`, offices.length);
|
||||
return offices;
|
||||
}
|
||||
|
||||
// Get approximate location based on district and government level
|
||||
function getApproximateLocationByDistrict(district, level) {
|
||||
// Specific locations for Edmonton officials
|
||||
const edmontonLocations = {
|
||||
// City Hall for municipal officials
|
||||
'Edmonton': { lat: 53.5444, lng: -113.4909 }, // Edmonton City Hall
|
||||
"O-day'min": { lat: 53.5444, lng: -113.4909 }, // Edmonton City Hall
|
||||
// Provincial Legislature
|
||||
'Edmonton-Glenora': { lat: 53.5344, lng: -113.5065 }, // Alberta Legislature
|
||||
// Federal offices (approximate downtown Edmonton)
|
||||
'Edmonton Centre': { lat: 53.5461, lng: -113.4938 }
|
||||
};
|
||||
|
||||
// Try specific district first
|
||||
if (district && edmontonLocations[district]) {
|
||||
return edmontonLocations[district];
|
||||
}
|
||||
|
||||
// Fallback based on government level
|
||||
const levelLocations = {
|
||||
'House of Commons': { lat: 53.5461, lng: -113.4938 }, // Downtown Edmonton
|
||||
'Legislative Assembly of Alberta': { lat: 53.5344, lng: -113.5065 }, // Alberta Legislature
|
||||
'Edmonton City Council': { lat: 53.5444, lng: -113.4909 } // Edmonton City Hall
|
||||
};
|
||||
|
||||
return levelLocations[level] || { lat: 53.9333, lng: -116.5765 }; // Default to Alberta center
|
||||
}
|
||||
|
||||
// Create a marker for an office location
|
||||
function createOfficeMarker(representative, office, isSharedLocation = false) {
|
||||
if (!office.lat || !office.lng) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Determine icon based on government level
|
||||
let icon = officeIcons.municipal; // default
|
||||
if (representative.representative_set_name) {
|
||||
if (representative.representative_set_name.includes('House of Commons')) {
|
||||
icon = officeIcons.federal;
|
||||
} else if (representative.representative_set_name.includes('Legislative Assembly')) {
|
||||
icon = officeIcons.provincial;
|
||||
}
|
||||
}
|
||||
|
||||
const marker = L.marker([office.lat, office.lng], { icon });
|
||||
|
||||
// Create popup content
|
||||
const popupContent = createOfficePopupContent(representative, office, isSharedLocation);
|
||||
marker.bindPopup(popupContent, {
|
||||
maxWidth: 300,
|
||||
className: 'office-popup'
|
||||
});
|
||||
|
||||
return marker;
|
||||
}
|
||||
|
||||
// Create popup content for office markers
|
||||
function createOfficePopupContent(representative, office, isSharedLocation = false) {
|
||||
const level = getRepresentativeLevel(representative.representative_set_name);
|
||||
const levelClass = level.toLowerCase().replace(' ', '-');
|
||||
|
||||
return `
|
||||
<div class="office-popup-content">
|
||||
<div class="rep-header ${levelClass}">
|
||||
${representative.photo_url ? `<img src="${representative.photo_url}" alt="${representative.name}" class="rep-photo-small">` : ''}
|
||||
<div class="rep-info">
|
||||
<h4>${representative.name}</h4>
|
||||
<p class="rep-level">${level}</p>
|
||||
<p class="rep-district">${representative.district_name || 'District not specified'}</p>
|
||||
${isSharedLocation ? '<p class="shared-location-note"><small><em>Note: Office location shared with other representatives</em></small></p>' : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="office-details">
|
||||
<h5>Office Information</h5>
|
||||
${office.address ? `<p><strong>Address:</strong> ${office.address}</p>` : ''}
|
||||
${office.phone ? `<p><strong>Phone:</strong> <a href="tel:${office.phone}">${office.phone}</a></p>` : ''}
|
||||
${office.fax ? `<p><strong>Fax:</strong> ${office.fax}</p>` : ''}
|
||||
${office.postal_code ? `<p><strong>Postal Code:</strong> ${office.postal_code}</p>` : ''}
|
||||
</div>
|
||||
|
||||
<div class="office-actions">
|
||||
${representative.email ? `<button class="btn btn-primary btn-small email-btn" data-email="${representative.email}" data-name="${representative.name}" data-level="${representative.representative_set_name}">Send Email</button>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}// Get representative level for display
|
||||
function getRepresentativeLevel(representativeSetName) {
|
||||
if (!representativeSetName) return 'Representative';
|
||||
|
||||
if (representativeSetName.includes('House of Commons')) {
|
||||
return 'Federal MP';
|
||||
} else if (representativeSetName.includes('Legislative Assembly')) {
|
||||
return 'Provincial MLA';
|
||||
} else {
|
||||
return 'Municipal Representative';
|
||||
}
|
||||
}
|
||||
|
||||
// Show a message on the map
|
||||
function showMapMessage(message) {
|
||||
const mapContainer = document.getElementById('main-map');
|
||||
if (!mapContainer) return;
|
||||
|
||||
// Remove any existing message
|
||||
const existingMessage = mapContainer.querySelector('.map-message');
|
||||
if (existingMessage) {
|
||||
existingMessage.remove();
|
||||
}
|
||||
|
||||
// Create and show new message
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = 'map-message';
|
||||
messageDiv.innerHTML = `
|
||||
<div class="map-message-content">
|
||||
<p>${message}</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
mapContainer.appendChild(messageDiv);
|
||||
|
||||
// Remove message after 5 seconds
|
||||
setTimeout(() => {
|
||||
if (messageDiv.parentNode) {
|
||||
messageDiv.remove();
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Initialize form handlers
|
||||
function initializePostalForm() {
|
||||
const postalForm = document.getElementById('postal-form');
|
||||
|
||||
// Handle postal code form submission
|
||||
if (postalForm) {
|
||||
postalForm.addEventListener('submit', (event) => {
|
||||
event.preventDefault();
|
||||
const postalCode = document.getElementById('postal-code').value.trim();
|
||||
if (postalCode) {
|
||||
handlePostalCodeSubmission(postalCode);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle email button clicks in popups
|
||||
document.addEventListener('click', (event) => {
|
||||
if (event.target.classList.contains('email-btn')) {
|
||||
event.preventDefault();
|
||||
const email = event.target.dataset.email;
|
||||
const name = event.target.dataset.name;
|
||||
const level = event.target.dataset.level;
|
||||
|
||||
if (window.openEmailModal) {
|
||||
window.openEmailModal(email, name, level);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle postal code submission and fetch representatives
|
||||
async function handlePostalCodeSubmission(postalCode) {
|
||||
try {
|
||||
showLoading();
|
||||
hideError();
|
||||
|
||||
// Normalize postal code
|
||||
const normalizedPostalCode = postalCode.toUpperCase().replace(/\s/g, '');
|
||||
|
||||
// Fetch representatives data
|
||||
const response = await fetch(`/api/representatives/by-postal/${normalizedPostalCode}`);
|
||||
const data = await response.json();
|
||||
|
||||
hideLoading();
|
||||
|
||||
if (data.success && data.data && data.data.representatives) {
|
||||
// Display representatives on map
|
||||
displayRepresentativeOffices(data.data.representatives, normalizedPostalCode);
|
||||
|
||||
// Also update the representatives display section using the existing system
|
||||
if (window.representativesDisplay) {
|
||||
window.representativesDisplay.displayRepresentatives(data.data.representatives);
|
||||
}
|
||||
|
||||
// Update location info manually if the existing system doesn't work
|
||||
const locationDetails = document.getElementById('location-details');
|
||||
if (locationDetails && data.data.location) {
|
||||
const location = data.data.location;
|
||||
locationDetails.textContent = `${location.city}, ${location.province} (${normalizedPostalCode})`;
|
||||
} else if (locationDetails) {
|
||||
locationDetails.textContent = `Postal Code: ${normalizedPostalCode}`;
|
||||
}
|
||||
|
||||
if (window.locationInfo) {
|
||||
window.locationInfo.updateLocationInfo(data.data.location, normalizedPostalCode);
|
||||
}
|
||||
|
||||
// Show the representatives section
|
||||
const representativesSection = document.getElementById('representatives-section');
|
||||
representativesSection.style.display = 'block';
|
||||
|
||||
// Fix map rendering after section becomes visible
|
||||
setTimeout(() => {
|
||||
if (!representativesMap) {
|
||||
initializeRepresentativesMap();
|
||||
}
|
||||
if (representativesMap) {
|
||||
representativesMap.invalidateSize();
|
||||
// Try to fit bounds again if we have markers
|
||||
if (representativeMarkers.length > 0) {
|
||||
const bounds = representativeMarkers.map(marker => marker.getLatLng());
|
||||
if (bounds.length > 0) {
|
||||
representativesMap.fitBounds(bounds, { padding: [20, 20] });
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 300);
|
||||
|
||||
// Show refresh button
|
||||
const refreshBtn = document.getElementById('refresh-btn');
|
||||
if (refreshBtn) {
|
||||
refreshBtn.style.display = 'inline-block';
|
||||
// Store postal code for refresh functionality
|
||||
refreshBtn.dataset.postalCode = normalizedPostalCode;
|
||||
}
|
||||
|
||||
// Show success message
|
||||
if (window.messageDisplay) {
|
||||
window.messageDisplay.show(`Found ${data.data.representatives.length} representatives for ${normalizedPostalCode}`, 'success', 3000);
|
||||
}
|
||||
} else {
|
||||
showError(data.message || 'Unable to find representatives for this postal code.');
|
||||
}
|
||||
} catch (error) {
|
||||
hideLoading();
|
||||
console.error('Error fetching representatives:', error);
|
||||
showError('An error occurred while looking up representatives. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
// Utility functions for loading and error states
|
||||
function showLoading() {
|
||||
const loading = document.getElementById('loading');
|
||||
if (loading) loading.style.display = 'block';
|
||||
}
|
||||
|
||||
function hideLoading() {
|
||||
const loading = document.getElementById('loading');
|
||||
if (loading) loading.style.display = 'none';
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
const errorDiv = document.getElementById('error-message');
|
||||
if (errorDiv) {
|
||||
errorDiv.textContent = message;
|
||||
errorDiv.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
function hideError() {
|
||||
const errorDiv = document.getElementById('error-message');
|
||||
if (errorDiv) {
|
||||
errorDiv.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize form handlers
|
||||
function initializePostalForm() {
|
||||
const postalForm = document.getElementById('postal-form');
|
||||
const refreshBtn = document.getElementById('refresh-btn');
|
||||
|
||||
// Handle postal code form submission
|
||||
if (postalForm) {
|
||||
postalForm.addEventListener('submit', (event) => {
|
||||
event.preventDefault();
|
||||
const postalCode = document.getElementById('postal-code').value.trim();
|
||||
if (postalCode) {
|
||||
handlePostalCodeSubmission(postalCode);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle refresh button
|
||||
if (refreshBtn) {
|
||||
refreshBtn.addEventListener('click', () => {
|
||||
const postalCode = refreshBtn.dataset.postalCode || document.getElementById('postal-code').value.trim();
|
||||
if (postalCode) {
|
||||
handlePostalCodeSubmission(postalCode);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize everything when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initializeRepresentativesMap();
|
||||
initializePostalForm();
|
||||
});
|
||||
|
||||
// Global function for opening email modal from map popups
|
||||
window.openEmailModal = function(email, name, level) {
|
||||
if (window.emailComposer) {
|
||||
window.emailComposer.openModal({
|
||||
email: email,
|
||||
name: name,
|
||||
level: level
|
||||
}, currentPostalCode);
|
||||
}
|
||||
};
|
||||
|
||||
// Export functions for use by other modules
|
||||
window.RepresentativesMap = {
|
||||
displayRepresentativeOffices,
|
||||
initializeRepresentativesMap,
|
||||
clearRepresentativeMarkers,
|
||||
handlePostalCodeSubmission
|
||||
};
|
||||
Reference in New Issue
Block a user