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