// Campaigns Grid Module // Displays public campaigns in a responsive grid on the homepage class CampaignsGrid { constructor() { this.campaigns = []; this.container = null; this.loading = false; this.error = null; } async init() { this.container = document.getElementById('campaigns-grid'); if (!this.container) { console.error('Campaigns grid container not found'); return; } await this.loadCampaigns(); } async loadCampaigns() { if (this.loading) return; this.loading = true; this.showLoading(); try { const response = await fetch('/api/public/campaigns'); const data = await response.json(); if (!data.success) { throw new Error(data.error || 'Failed to load campaigns'); } this.campaigns = data.campaigns || []; this.renderCampaigns(); // Show or hide the entire campaigns section based on availability const campaignsSection = document.getElementById('campaigns-section'); if (this.campaigns.length > 0) { campaignsSection.style.display = 'block'; } else { campaignsSection.style.display = 'none'; } } catch (error) { console.error('Error loading campaigns:', error); this.showError('Unable to load campaigns. Please try again later.'); } finally { this.loading = false; } } renderCampaigns() { if (!this.container) return; if (this.campaigns.length === 0) { this.container.innerHTML = `

No active campaigns at the moment. Check back soon!

`; return; } // Sort campaigns by created_at date (newest first) const sortedCampaigns = [...this.campaigns].sort((a, b) => { const dateA = new Date(a.created_at || 0); const dateB = new Date(b.created_at || 0); return dateB - dateA; }); const campaignsHTML = sortedCampaigns.map(campaign => this.renderCampaignCard(campaign)).join(''); this.container.innerHTML = campaignsHTML; // Add click event listeners to campaign cards (no inline handlers) this.attachCardClickHandlers(); } attachCardClickHandlers() { const campaignCards = this.container.querySelectorAll('.campaign-card'); campaignCards.forEach(card => { const slug = card.getAttribute('data-slug'); if (slug) { // Handle card click (but not share buttons) card.addEventListener('click', (e) => { // Don't navigate if clicking on share buttons if (e.target.closest('.share-btn') || e.target.closest('.campaign-card-social-share')) { return; } window.location.href = `/campaign/${slug}`; }); // Add keyboard accessibility card.setAttribute('tabindex', '0'); card.setAttribute('role', 'link'); card.setAttribute('aria-label', `View campaign: ${card.querySelector('.campaign-card-title')?.textContent || 'campaign'}`); card.addEventListener('keypress', (e) => { if (e.target.closest('.share-btn')) { return; // Let share buttons handle their own keyboard events } if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); window.location.href = `/campaign/${slug}`; } }); // Attach share button handlers const shareButtons = card.querySelectorAll('.share-btn'); shareButtons.forEach(btn => { btn.addEventListener('click', (e) => { e.stopPropagation(); e.preventDefault(); const platform = btn.getAttribute('data-platform'); const title = card.querySelector('.campaign-card-title')?.textContent || 'Campaign'; const description = card.querySelector('.campaign-card-description')?.textContent || ''; this.handleShare(platform, slug, title, description); }); }); } }); } renderCampaignCard(campaign) { const coverPhotoStyle = campaign.cover_photo ? `background-image: url('/uploads/${campaign.cover_photo}'); background-size: cover; background-position: center;` : 'background: linear-gradient(135deg, #3498db, #2c3e50);'; const emailCountBadge = campaign.show_email_count && campaign.emailCount !== null ? `
📧 ${campaign.emailCount} emails sent
` : ''; const callCountBadge = campaign.show_call_count && campaign.callCount !== null ? `
📞 ${campaign.callCount} calls made
` : ''; const targetLevels = Array.isArray(campaign.target_government_levels) && campaign.target_government_levels.length > 0 ? campaign.target_government_levels.map(level => `${level}`).join('') : ''; // Truncate description to reasonable length const description = campaign.description || ''; const truncatedDescription = description.length > 150 ? description.substring(0, 150) + '...' : description; return `

${this.escapeHtml(campaign.title)}

${this.escapeHtml(truncatedDescription)}

${targetLevels ? `
${targetLevels}
` : ''}
${emailCountBadge} ${callCountBadge}
Learn More & Participate →
`; } showLoading() { if (!this.container) return; this.container.innerHTML = `

Loading campaigns...

`; } showError(message) { if (!this.container) return; this.container.innerHTML = `

⚠️ ${this.escapeHtml(message)}

`; } escapeHtml(text) { const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; return text.replace(/[&<>"']/g, (m) => map[m]); } handleShare(platform, slug, title, description) { const campaignUrl = `${window.location.origin}/campaign/${slug}`; const shareText = `${title} - ${description}`; let shareUrl = ''; switch(platform) { case 'twitter': shareUrl = `https://twitter.com/intent/tweet?text=${encodeURIComponent(shareText)}&url=${encodeURIComponent(campaignUrl)}`; window.open(shareUrl, '_blank', 'width=550,height=420'); break; case 'facebook': shareUrl = `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(campaignUrl)}`; window.open(shareUrl, '_blank', 'width=550,height=420'); break; case 'linkedin': shareUrl = `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(campaignUrl)}`; window.open(shareUrl, '_blank', 'width=550,height=420'); break; case 'reddit': shareUrl = `https://www.reddit.com/submit?url=${encodeURIComponent(campaignUrl)}&title=${encodeURIComponent(title)}`; window.open(shareUrl, '_blank', 'width=550,height=420'); break; case 'email': const emailSubject = `Check out this campaign: ${title}`; const emailBody = `${shareText}\n\nLearn more and participate: ${campaignUrl}`; window.location.href = `mailto:?subject=${encodeURIComponent(emailSubject)}&body=${encodeURIComponent(emailBody)}`; break; case 'copy': this.copyToClipboard(campaignUrl); break; } } async copyToClipboard(text) { try { if (navigator.clipboard && window.isSecureContext) { await navigator.clipboard.writeText(text); } else { // Fallback for older browsers const textArea = document.createElement('textarea'); textArea.value = text; textArea.style.position = 'fixed'; textArea.style.left = '-999999px'; document.body.appendChild(textArea); textArea.select(); document.execCommand('copy'); document.body.removeChild(textArea); } this.showShareFeedback('Link copied to clipboard!'); } catch (err) { console.error('Failed to copy:', err); this.showShareFeedback('Failed to copy link', true); } } showShareFeedback(message, isError = false) { // Create or get feedback element let feedback = document.getElementById('share-feedback'); if (!feedback) { feedback = document.createElement('div'); feedback.id = 'share-feedback'; feedback.className = 'share-feedback'; document.body.appendChild(feedback); } feedback.textContent = message; feedback.className = `share-feedback ${isError ? 'error' : 'success'} show`; // Auto-hide after 3 seconds setTimeout(() => { feedback.classList.remove('show'); }, 3000); } } // Export for use in main.js if (typeof window !== 'undefined') { window.CampaignsGrid = CampaignsGrid; }