482 lines
19 KiB
JavaScript
482 lines
19 KiB
JavaScript
// Representatives Display Module
|
|
class RepresentativesDisplay {
|
|
constructor() {
|
|
this.container = document.getElementById('representatives-container');
|
|
}
|
|
|
|
displayRepresentatives(representatives) {
|
|
if (!representatives || representatives.length === 0) {
|
|
this.container.innerHTML = `
|
|
<div class="rep-category">
|
|
<h3>No Representatives Found</h3>
|
|
<p>No representatives were found for this postal code. This might be due to:</p>
|
|
<ul>
|
|
<li>The postal code is not in our database</li>
|
|
<li>Temporary API issues</li>
|
|
<li>The postal code is not currently assigned to electoral districts</li>
|
|
</ul>
|
|
<p>Please try again later or verify your postal code.</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
// Group representatives by level/type
|
|
const grouped = this.groupRepresentatives(representatives);
|
|
|
|
let html = '';
|
|
|
|
// Order of importance for display
|
|
const displayOrder = [
|
|
'Federal',
|
|
'Provincial',
|
|
'Municipal',
|
|
'School Board',
|
|
'Other'
|
|
];
|
|
|
|
displayOrder.forEach(level => {
|
|
if (grouped[level] && grouped[level].length > 0) {
|
|
html += this.renderRepresentativeCategory(level, grouped[level]);
|
|
}
|
|
});
|
|
|
|
this.container.innerHTML = html;
|
|
this.attachEventListeners();
|
|
}
|
|
|
|
groupRepresentatives(representatives) {
|
|
const groups = {
|
|
'Federal': [],
|
|
'Provincial': [],
|
|
'Municipal': [],
|
|
'School Board': [],
|
|
'Other': []
|
|
};
|
|
|
|
representatives.forEach(rep => {
|
|
const setName = rep.representative_set_name || '';
|
|
const office = rep.elected_office || '';
|
|
|
|
if (setName.toLowerCase().includes('house of commons') ||
|
|
setName.toLowerCase().includes('federal') ||
|
|
office.toLowerCase().includes('member of parliament') ||
|
|
office.toLowerCase().includes('mp')) {
|
|
groups['Federal'].push(rep);
|
|
} else if (setName.toLowerCase().includes('provincial') ||
|
|
setName.toLowerCase().includes('legislative assembly') ||
|
|
setName.toLowerCase().includes('mla') ||
|
|
office.toLowerCase().includes('mla')) {
|
|
groups['Provincial'].push(rep);
|
|
} else if (setName.toLowerCase().includes('municipal') ||
|
|
setName.toLowerCase().includes('city council') ||
|
|
setName.toLowerCase().includes('mayor') ||
|
|
office.toLowerCase().includes('councillor') ||
|
|
office.toLowerCase().includes('mayor')) {
|
|
groups['Municipal'].push(rep);
|
|
} else if (setName.toLowerCase().includes('school') ||
|
|
office.toLowerCase().includes('school') ||
|
|
office.toLowerCase().includes('trustee')) {
|
|
groups['School Board'].push(rep);
|
|
} else {
|
|
groups['Other'].push(rep);
|
|
}
|
|
});
|
|
|
|
return groups;
|
|
}
|
|
|
|
renderRepresentativeCategory(categoryName, representatives) {
|
|
const cards = representatives.map(rep => this.renderRepresentativeCard(rep)).join('');
|
|
|
|
return `
|
|
<div class="rep-category">
|
|
<h3>${categoryName} Representatives</h3>
|
|
<div class="rep-cards">
|
|
${cards}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
renderRepresentativeCard(rep) {
|
|
const name = rep.name || 'Name not available';
|
|
const email = rep.email || null;
|
|
const office = rep.elected_office || 'Office not specified';
|
|
const district = rep.district_name || 'District not specified';
|
|
const party = rep.party_name || 'Party not specified';
|
|
const photoUrl = rep.photo_url || null;
|
|
|
|
// Extract phone numbers from offices array
|
|
const phoneNumbers = this.extractPhoneNumbers(rep.offices || []);
|
|
const primaryPhone = phoneNumbers.length > 0 ? phoneNumbers[0] : null;
|
|
|
|
const emailButton = email ?
|
|
`<button class="btn btn-primary compose-email"
|
|
data-email="${email}"
|
|
data-name="${name}"
|
|
data-office="${office}"
|
|
data-district="${district}">
|
|
📧 Send Email
|
|
</button>` :
|
|
'<span class="text-muted">No email available</span>';
|
|
|
|
// Add call button if phone number is available
|
|
const callButton = primaryPhone ?
|
|
`<button class="btn btn-success call-representative"
|
|
data-phone="${primaryPhone.number}"
|
|
data-office-type="${primaryPhone.type}"
|
|
data-name="${name}"
|
|
data-office="${office}">
|
|
📞 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>` : '';
|
|
|
|
// Generate initials for fallback
|
|
const initials = name.split(' ')
|
|
.map(word => word.charAt(0))
|
|
.join('')
|
|
.toUpperCase()
|
|
.slice(0, 2);
|
|
|
|
const photoElement = photoUrl ?
|
|
`<div class="rep-photo">
|
|
<img src="${photoUrl}"
|
|
alt="${name}"
|
|
data-fallback-initials="${initials}"
|
|
loading="lazy">
|
|
<div class="rep-photo-fallback" style="display: none;">
|
|
${initials}
|
|
</div>
|
|
</div>` :
|
|
`<div class="rep-photo">
|
|
<div class="rep-photo-fallback">
|
|
${initials}
|
|
</div>
|
|
</div>`;
|
|
|
|
return `
|
|
<div class="rep-card">
|
|
${photoElement}
|
|
<div class="rep-content">
|
|
<div class="rep-header">
|
|
<h4>${name}</h4>
|
|
</div>
|
|
<div class="rep-info">
|
|
<p><strong>Office:</strong> ${office}</p>
|
|
<p><strong>District:</strong> ${district}</p>
|
|
${party !== 'Party not specified' ? `<p><strong>Party:</strong> ${party}</p>` : ''}
|
|
${email ? `<p><strong>Email:</strong> ${email}</p>` : ''}
|
|
${primaryPhone ? `<p><strong>Phone:</strong> ${primaryPhone.number} ${primaryPhone.type ? `(${primaryPhone.type})` : ''}</p>` : ''}
|
|
</div>
|
|
<div class="rep-actions">
|
|
${emailButton}
|
|
${callButton}
|
|
${profileUrl}
|
|
</div>
|
|
${visitButtons ? `<div class="rep-visit-buttons">${visitButtons}</div>` : ''}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
extractPhoneNumbers(offices) {
|
|
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 && !phoneNumbers.find(p => p.number === office.tel)) {
|
|
phoneNumbers.push({
|
|
number: office.tel,
|
|
type: office.type || 'office'
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
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');
|
|
composeButtons.forEach(button => {
|
|
button.addEventListener('click', (e) => {
|
|
const email = e.target.dataset.email;
|
|
const name = e.target.dataset.name;
|
|
const office = e.target.dataset.office;
|
|
const district = e.target.dataset.district;
|
|
|
|
window.emailComposer.openModal({
|
|
email,
|
|
name,
|
|
office,
|
|
district
|
|
});
|
|
});
|
|
});
|
|
|
|
// Add event listeners for call buttons
|
|
const callButtons = this.container.querySelectorAll('.call-representative');
|
|
callButtons.forEach(button => {
|
|
button.addEventListener('click', (e) => {
|
|
const phone = e.target.dataset.phone;
|
|
const name = e.target.dataset.name;
|
|
const office = e.target.dataset.office;
|
|
const officeType = e.target.dataset.officeType;
|
|
|
|
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) {
|
|
// 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}${officeInfo}?\n\nPhone: ${phone}`;
|
|
|
|
if (confirm(message)) {
|
|
// Attempt to initiate the call
|
|
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
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
window.representativesDisplay = new RepresentativesDisplay();
|
|
}); |