327 lines
16 KiB
JavaScript
327 lines
16 KiB
JavaScript
// 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 = `
|
|
<div class="campaigns-empty">
|
|
<p>No active campaigns at the moment. Check back soon!</p>
|
|
</div>
|
|
`;
|
|
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
|
|
? `<div class="campaign-card-stat">
|
|
<span class="stat-icon">📧</span>
|
|
<span class="stat-value">${campaign.emailCount}</span>
|
|
<span class="stat-label">emails sent</span>
|
|
</div>`
|
|
: '';
|
|
|
|
const callCountBadge = campaign.show_call_count && campaign.callCount !== null
|
|
? `<div class="campaign-card-stat">
|
|
<span class="stat-icon">📞</span>
|
|
<span class="stat-value">${campaign.callCount}</span>
|
|
<span class="stat-label">calls made</span>
|
|
</div>`
|
|
: '';
|
|
|
|
const targetLevels = Array.isArray(campaign.target_government_levels) && campaign.target_government_levels.length > 0
|
|
? campaign.target_government_levels.map(level => `<span class="level-badge">${level}</span>`).join('')
|
|
: '';
|
|
|
|
// Truncate description to reasonable length
|
|
const description = campaign.description || '';
|
|
const truncatedDescription = description.length > 150
|
|
? description.substring(0, 150) + '...'
|
|
: description;
|
|
|
|
return `
|
|
<div class="campaign-card" data-slug="${campaign.slug}">
|
|
<div class="campaign-card-image" style="${coverPhotoStyle}">
|
|
<div class="campaign-card-overlay"></div>
|
|
</div>
|
|
<div class="campaign-card-content">
|
|
<h3 class="campaign-card-title">${this.escapeHtml(campaign.title)}</h3>
|
|
<p class="campaign-card-description">${this.escapeHtml(truncatedDescription)}</p>
|
|
${targetLevels ? `<div class="campaign-card-levels">${targetLevels}</div>` : ''}
|
|
<div class="campaign-card-stats">
|
|
${emailCountBadge}
|
|
${callCountBadge}
|
|
</div>
|
|
<div class="campaign-card-social-share">
|
|
<span class="share-label">Share:</span>
|
|
<button class="share-btn share-twitter" data-platform="twitter" title="Share on Twitter/X" aria-label="Share on Twitter/X">
|
|
<svg viewBox="0 0 24 24" fill="currentColor">
|
|
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
|
|
</svg>
|
|
</button>
|
|
<button class="share-btn share-facebook" data-platform="facebook" title="Share on Facebook" aria-label="Share on Facebook">
|
|
<svg viewBox="0 0 24 24" fill="currentColor">
|
|
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/>
|
|
</svg>
|
|
</button>
|
|
<button class="share-btn share-linkedin" data-platform="linkedin" title="Share on LinkedIn" aria-label="Share on LinkedIn">
|
|
<svg viewBox="0 0 24 24" fill="currentColor">
|
|
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
|
|
</svg>
|
|
</button>
|
|
<button class="share-btn share-reddit" data-platform="reddit" title="Share on Reddit" aria-label="Share on Reddit">
|
|
<svg viewBox="0 0 24 24" fill="currentColor">
|
|
<path d="M12 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0zm5.01 4.744c.688 0 1.25.561 1.25 1.249a1.25 1.25 0 0 1-2.498.056l-2.597-.547-.8 3.747c1.824.07 3.48.632 4.674 1.488.308-.309.73-.491 1.207-.491.968 0 1.754.786 1.754 1.754 0 .716-.435 1.333-1.01 1.614a3.111 3.111 0 0 1 .042.52c0 2.694-3.13 4.87-7.004 4.87-3.874 0-7.004-2.176-7.004-4.87 0-.183.015-.366.043-.534A1.748 1.748 0 0 1 4.028 12c0-.968.786-1.754 1.754-1.754.463 0 .898.196 1.207.49 1.207-.883 2.878-1.43 4.744-1.487l.885-4.182a.342.342 0 0 1 .14-.197.35.35 0 0 1 .238-.042l2.906.617a1.214 1.214 0 0 1 1.108-.701zM9.25 12C8.561 12 8 12.562 8 13.25c0 .687.561 1.248 1.25 1.248.687 0 1.248-.561 1.248-1.249 0-.688-.561-1.249-1.249-1.249zm5.5 0c-.687 0-1.248.561-1.248 1.25 0 .687.561 1.248 1.249 1.248.688 0 1.249-.561 1.249-1.249 0-.687-.562-1.249-1.25-1.249zm-5.466 3.99a.327.327 0 0 0-.231.094.33.33 0 0 0 0 .463c.842.842 2.484.913 2.961.913.477 0 2.105-.056 2.961-.913a.361.361 0 0 0 .029-.463.33.33 0 0 0-.464 0c-.547.533-1.684.73-2.512.73-.828 0-1.979-.196-2.512-.73a.326.326 0 0 0-.232-.095z"/>
|
|
</svg>
|
|
</button>
|
|
<button class="share-btn share-email" data-platform="email" title="Share via Email" aria-label="Share via Email">
|
|
<svg viewBox="0 0 24 24" fill="currentColor">
|
|
<path d="M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z"/>
|
|
</svg>
|
|
</button>
|
|
<button class="share-btn share-copy" data-platform="copy" title="Copy Link" aria-label="Copy Link">
|
|
<svg viewBox="0 0 24 24" fill="currentColor">
|
|
<path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
<div class="campaign-card-action">
|
|
<span class="btn-link">Learn More & Participate →</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
showLoading() {
|
|
if (!this.container) return;
|
|
|
|
this.container.innerHTML = `
|
|
<div class="campaigns-loading">
|
|
<div class="spinner"></div>
|
|
<p>Loading campaigns...</p>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
showError(message) {
|
|
if (!this.container) return;
|
|
|
|
this.container.innerHTML = `
|
|
<div class="campaigns-error">
|
|
<p>⚠️ ${this.escapeHtml(message)}</p>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
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;
|
|
}
|