304 lines
11 KiB
JavaScript
304 lines
11 KiB
JavaScript
// Main Application Module
|
|
class MainApp {
|
|
constructor() {
|
|
this.init();
|
|
}
|
|
|
|
async init() {
|
|
// Initialize message display system
|
|
window.messageDisplay = new MessageDisplay();
|
|
|
|
// Check API health on startup
|
|
this.checkAPIHealth();
|
|
|
|
// Initialize postal lookup immediately (always show it first)
|
|
this.postalLookup = new PostalLookup(this.updateRepresentatives.bind(this));
|
|
|
|
// Check for highlighted campaign FIRST (before campaigns grid)
|
|
await this.checkHighlightedCampaign();
|
|
|
|
// Initialize campaigns grid AFTER highlighted campaign loads
|
|
this.campaignsGrid = new CampaignsGrid();
|
|
await this.campaignsGrid.init();
|
|
|
|
// Add global error handling
|
|
window.addEventListener('error', (e) => {
|
|
// 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) => {
|
|
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);
|
|
}
|
|
});
|
|
}
|
|
|
|
async checkAPIHealth() {
|
|
try {
|
|
await window.apiClient.checkHealth();
|
|
console.log('API health check passed');
|
|
} catch (error) {
|
|
console.error('API health check failed:', error);
|
|
window.messageDisplay.show('Connection to server failed. Please check your internet connection and try again.', 'error');
|
|
}
|
|
}
|
|
|
|
async checkHighlightedCampaign() {
|
|
try {
|
|
const response = await fetch('/api/public/highlighted-campaign');
|
|
|
|
if (!response.ok) {
|
|
if (response.status === 404) {
|
|
// No highlighted campaign, show normal postal code lookup
|
|
return false;
|
|
}
|
|
throw new Error('Failed to fetch highlighted campaign');
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success && data.campaign) {
|
|
this.displayHighlightedCampaign(data.campaign);
|
|
return true;
|
|
}
|
|
return false;
|
|
} catch (error) {
|
|
console.error('Error checking for highlighted campaign:', error);
|
|
// Continue with normal postal code lookup if there's an error
|
|
return false;
|
|
}
|
|
}
|
|
|
|
displayHighlightedCampaign(campaign) {
|
|
const highlightedSection = document.getElementById('highlighted-campaign-section');
|
|
const highlightedContainer = document.getElementById('highlighted-campaign-container');
|
|
|
|
if (!highlightedSection || !highlightedContainer) return;
|
|
|
|
// Build the campaign display HTML with cover photo
|
|
const coverPhotoStyle = campaign.cover_photo
|
|
? `background-image: linear-gradient(rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.3)), url('/uploads/${campaign.cover_photo}'); background-size: cover; background-position: center;`
|
|
: 'background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);';
|
|
|
|
const statsHTML = [];
|
|
if (campaign.show_email_count && campaign.emailCount !== null) {
|
|
statsHTML.push(`<div class="stat"><span class="stat-icon">📧</span><strong>${campaign.emailCount}</strong> Emails Sent</div>`);
|
|
}
|
|
if (campaign.show_call_count && campaign.callCount !== null) {
|
|
statsHTML.push(`<div class="stat"><span class="stat-icon">📞</span><strong>${campaign.callCount}</strong> Calls Made</div>`);
|
|
}
|
|
if (campaign.show_response_count && campaign.responseCount !== null) {
|
|
statsHTML.push(`<div class="stat"><span class="stat-icon">✅</span><strong>${campaign.responseCount}</strong> Responses</div>`);
|
|
}
|
|
|
|
const highlightedHTML = `
|
|
<div class="highlighted-campaign-container">
|
|
${campaign.cover_photo ? `
|
|
<div class="highlighted-campaign-header" style="${coverPhotoStyle}">
|
|
<div class="highlighted-campaign-badge">⭐ Featured Campaign</div>
|
|
<h2>${this.escapeHtml(campaign.title || campaign.name)}</h2>
|
|
</div>
|
|
` : `
|
|
<div class="highlighted-campaign-badge">⭐ Featured Campaign</div>
|
|
<h2>${this.escapeHtml(campaign.title || campaign.name)}</h2>
|
|
`}
|
|
|
|
<div class="highlighted-campaign-content">
|
|
${campaign.description ? `<p class="campaign-description">${this.escapeHtml(campaign.description)}</p>` : ''}
|
|
|
|
${statsHTML.length > 0 ? `
|
|
<div class="campaign-stats-inline">
|
|
${statsHTML.join('')}
|
|
</div>
|
|
` : ''}
|
|
|
|
<div class="campaign-cta">
|
|
<a href="/campaign/${campaign.slug}" class="btn btn-primary btn-large">
|
|
Join This Campaign
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Insert the HTML
|
|
highlightedContainer.innerHTML = highlightedHTML;
|
|
|
|
// Make section visible but collapsed
|
|
highlightedSection.style.display = 'grid';
|
|
|
|
// Force a reflow to ensure the initial state is applied
|
|
const height = highlightedSection.offsetHeight;
|
|
console.log('Campaign section initial height:', height);
|
|
|
|
// Wait a bit longer before starting animation to ensure it's visible
|
|
setTimeout(() => {
|
|
console.log('Starting campaign expansion animation...');
|
|
highlightedSection.classList.add('show');
|
|
|
|
// Add animation to the container after expansion starts
|
|
setTimeout(() => {
|
|
const container = highlightedContainer.querySelector('.highlighted-campaign-container');
|
|
if (container) {
|
|
console.log('Adding visible class to container...');
|
|
container.classList.add('visible', 'fade-in-smooth');
|
|
}
|
|
}, 300);
|
|
}, 100);
|
|
}
|
|
|
|
updateRepresentatives(representatives) {
|
|
}
|
|
|
|
escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
}
|
|
|
|
// Message Display System
|
|
class MessageDisplay {
|
|
constructor() {
|
|
this.container = document.getElementById('message-display');
|
|
this.timeouts = new Map();
|
|
}
|
|
|
|
show(message, type = 'info', duration = 5000) {
|
|
// Clear existing timeout for this container
|
|
if (this.timeouts.has(this.container)) {
|
|
clearTimeout(this.timeouts.get(this.container));
|
|
}
|
|
|
|
// Set message content and type
|
|
this.container.innerHTML = message;
|
|
this.container.className = `message-display ${type}`;
|
|
this.container.style.display = 'block';
|
|
|
|
// Auto-hide after duration
|
|
const timeout = setTimeout(() => {
|
|
this.hide();
|
|
}, duration);
|
|
|
|
this.timeouts.set(this.container, timeout);
|
|
|
|
// Add click to dismiss
|
|
this.container.style.cursor = 'pointer';
|
|
this.container.onclick = () => this.hide();
|
|
}
|
|
|
|
hide() {
|
|
this.container.style.display = 'none';
|
|
this.container.onclick = null;
|
|
|
|
// Clear timeout
|
|
if (this.timeouts.has(this.container)) {
|
|
clearTimeout(this.timeouts.get(this.container));
|
|
this.timeouts.delete(this.container);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Utility functions
|
|
const Utils = {
|
|
// Format postal code consistently
|
|
formatPostalCode(postalCode) {
|
|
const cleaned = postalCode.replace(/\s/g, '').toUpperCase();
|
|
if (cleaned.length === 6) {
|
|
return `${cleaned.slice(0, 3)} ${cleaned.slice(3)}`;
|
|
}
|
|
return cleaned;
|
|
},
|
|
|
|
// Sanitize text input
|
|
sanitizeText(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
},
|
|
|
|
// Debounce function for input handling
|
|
debounce(func, wait) {
|
|
let timeout;
|
|
return function executedFunction(...args) {
|
|
const later = () => {
|
|
clearTimeout(timeout);
|
|
func(...args);
|
|
};
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(later, wait);
|
|
};
|
|
},
|
|
|
|
// Check if we're on mobile
|
|
isMobile() {
|
|
return window.innerWidth <= 768;
|
|
},
|
|
|
|
// Format date for display
|
|
formatDate(dateString) {
|
|
const date = new Date(dateString);
|
|
return date.toLocaleDateString('en-CA', {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
});
|
|
}
|
|
};
|
|
|
|
// Make utils globally available
|
|
window.Utils = Utils;
|
|
|
|
// Initialize app when DOM is loaded
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
window.mainApp = new MainApp();
|
|
|
|
// Initialize campaigns grid
|
|
if (typeof CampaignsGrid !== 'undefined') {
|
|
window.campaignsGrid = new CampaignsGrid();
|
|
window.campaignsGrid.init();
|
|
}
|
|
|
|
// Add some basic accessibility improvements
|
|
document.addEventListener('keydown', (e) => {
|
|
// Allow Escape to close modals (handled in individual modules)
|
|
// Add tab navigation improvements if needed
|
|
});
|
|
|
|
// Add responsive behavior
|
|
window.addEventListener('resize', Utils.debounce(() => {
|
|
// Handle responsive layout changes if needed
|
|
const isMobile = Utils.isMobile();
|
|
document.body.classList.toggle('mobile', isMobile);
|
|
}, 250));
|
|
|
|
// Initial mobile class
|
|
document.body.classList.toggle('mobile', Utils.isMobile());
|
|
}); |