feat(campaigns): add highlighted campaign feature with admin controls and UI updates

This commit is contained in:
2025-11-07 10:15:41 -07:00
parent 4bbf772061
commit 564685abed
13 changed files with 601 additions and 5 deletions

View File

@@ -106,6 +106,25 @@
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
.campaign-card.highlighted {
border: 2px solid #ffd700;
box-shadow: 0 0 15px rgba(255, 215, 0, 0.3);
}
.campaign-highlight-badge {
position: absolute;
top: 10px;
right: 10px;
background: #ffd700;
color: #333;
padding: 0.3rem 0.8rem;
border-radius: 20px;
font-size: 0.85rem;
font-weight: bold;
z-index: 10;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
}
.campaign-card-cover {
background-size: cover;
background-position: center;
@@ -1018,6 +1037,10 @@ Sincerely,
<input type="checkbox" id="create-show-response-wall" name="show_response_wall">
<label for="create-show-response-wall">💬 Show Response Wall Button</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="create-highlight-campaign" name="highlight_campaign">
<label for="create-highlight-campaign">⭐ Highlight on Homepage (replaces postal code search)</label>
</div>
</div>
</div>
@@ -1152,6 +1175,10 @@ Sincerely,
<input type="checkbox" id="edit-show-response-wall" name="show_response_wall">
<label for="edit-show-response-wall">💬 Show Response Wall Button</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="edit-highlight-campaign" name="highlight_campaign">
<label for="edit-highlight-campaign">⭐ Highlight on Homepage (replaces postal code search)</label>
</div>
</div>
</div>

View File

@@ -170,6 +170,7 @@ header {
box-shadow: 0 8px 24px rgba(0, 90, 156, 0.2);
overflow: hidden;
text-align: center;
transition: all 0.8s cubic-bezier(0.4, 0, 0.2, 1);
}
@keyframes gradientShift {
@@ -2425,3 +2426,231 @@ footer a:hover {
#share-email {
background: #ea4335;
}
/* Highlighted Campaign Styles */
.highlighted-campaign-container {
background: rgba(255, 255, 255, 0.95);
padding: 0;
border-radius: 12px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
margin-bottom: 0;
max-width: 700px;
margin-left: auto;
margin-right: auto;
position: relative;
z-index: 1;
text-align: center;
transform: translateY(30px);
transition: transform 1.5s cubic-bezier(0.4, 0, 0.2, 1) 0.5s,
opacity 1.5s ease-out 0.5s;
border: 1px solid rgba(255, 255, 255, 0.3);
opacity: 0;
overflow: hidden;
}
.highlighted-campaign-container.visible {
transform: translateY(0);
opacity: 1;
}
.highlighted-campaign-header {
position: relative;
padding: 50px 30px;
border-radius: 12px 12px 0 0;
min-height: 200px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.highlighted-campaign-header .highlighted-campaign-badge {
background: rgba(255, 215, 0, 0.95);
margin-bottom: 15px;
backdrop-filter: blur(8px);
}
.highlighted-campaign-header h2 {
color: #ffffff !important;
text-shadow: 2px 2px 12px rgba(0, 0, 0, 0.8);
margin: 0;
font-size: 2.2rem;
font-weight: bold;
background: rgba(0, 0, 0, 0.75);
padding: 15px 25px;
border-radius: 8px;
backdrop-filter: blur(10px);
display: inline-block;
line-height: 1.3;
}
.highlighted-campaign-content {
padding: 35px 30px;
}
.highlighted-campaign-badge {
display: inline-block;
background: linear-gradient(135deg, #ffd700 0%, #ffed4e 100%);
color: #333;
padding: 8px 20px;
border-radius: 25px;
font-weight: bold;
font-size: 0.9rem;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(255, 215, 0, 0.3);
}
.highlighted-campaign-container h2 {
color: #1a1a1a;
margin: 0 0 15px 0;
font-size: 2rem;
}
.highlighted-campaign-container .campaign-description {
color: #666;
font-size: 1.1rem;
line-height: 1.6;
margin-bottom: 25px;
}
.campaign-stats-inline {
display: flex;
justify-content: center;
gap: 25px;
margin: 30px 0;
flex-wrap: wrap;
}
.campaign-stats-inline .stat {
display: flex;
flex-direction: column;
align-items: center;
font-size: 0.95rem;
color: #555;
gap: 5px;
}
.campaign-stats-inline .stat .stat-icon {
font-size: 1.8rem;
margin-bottom: 5px;
}
.campaign-stats-inline .stat strong {
color: #005a9c;
font-size: 1.5rem;
display: block;
font-weight: bold;
}
.campaign-cta {
margin: 30px 0 20px 0;
}
.btn-large {
padding: 15px 40px;
font-size: 1.1rem;
font-weight: bold;
}
.or-divider {
position: relative;
text-align: center;
margin: 25px 0;
}
.or-divider::before {
content: '';
position: absolute;
top: 50%;
left: 0;
right: 0;
height: 1px;
background: #ddd;
z-index: 0;
}
.or-divider span {
background: white;
padding: 0 20px;
position: relative;
z-index: 1;
color: #999;
font-size: 0.9rem;
}
/* Loading and fade-in animations */
#postal-input-section {
transition: opacity 0.4s ease-in-out;
}
/* Highlighted Campaign Section Styles (inside blue background) */
.highlighted-campaign-section {
display: grid;
grid-template-rows: 0fr;
opacity: 0;
transition: grid-template-rows 2s cubic-bezier(0.4, 0, 0.2, 1),
opacity 1.8s ease-in 0.2s,
margin 2s cubic-bezier(0.4, 0, 0.2, 1),
padding 2s cubic-bezier(0.4, 0, 0.2, 1);
margin-top: 0;
padding-top: 0;
}
.highlighted-campaign-section > div {
overflow: hidden;
}
.highlighted-campaign-section.show {
grid-template-rows: 1fr;
opacity: 1;
margin-top: 40px;
padding-top: 30px;
border-top: 2px solid rgba(255, 255, 255, 0.2);
}
.fade-in-smooth {
animation: fadeInSmooth 1.5s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
@keyframes fadeInSmooth {
0% {
opacity: 0;
transform: translateY(20px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
@media (max-width: 768px) {
.highlighted-campaign-container {
max-width: 100%;
}
.highlighted-campaign-header {
padding: 40px 20px;
min-height: 180px;
}
.highlighted-campaign-header h2 {
font-size: 1.6rem;
}
.highlighted-campaign-content {
padding: 25px 20px;
}
.highlighted-campaign-container h2 {
font-size: 1.5rem;
}
.campaign-stats-inline {
gap: 20px;
}
.btn-large {
padding: 12px 30px;
font-size: 1rem;
}
}

View File

@@ -72,6 +72,13 @@
<p>Looking up your representatives...</p>
</div>
</div>
<!-- Highlighted Campaign Section (inside blue background) -->
<div id="highlighted-campaign-section" class="highlighted-campaign-section" style="display: none;">
<div id="highlighted-campaign-container">
<!-- Highlighted campaign will be dynamically inserted here -->
</div>
</div>
</section>
<!-- Representatives Display Section -->

View File

@@ -488,7 +488,8 @@ class AdminPanel {
}
listDiv.innerHTML = this.campaigns.map(campaign => `
<div class="campaign-card" data-campaign-id="${campaign.id}">
<div class="campaign-card ${campaign.highlight_campaign ? 'highlighted' : ''}" data-campaign-id="${campaign.id}">
${campaign.highlight_campaign ? '<div class="campaign-highlight-badge">⭐ Highlighted</div>' : ''}
${campaign.cover_photo ? `
<div class="campaign-card-cover" style="background-image: url('/uploads/${campaign.cover_photo}');">
<div class="campaign-card-cover-overlay">
@@ -578,6 +579,7 @@ class AdminPanel {
campaignFormData.append('allow_email_editing', formData.get('allow_email_editing') === 'on');
campaignFormData.append('show_response_wall', formData.get('show_response_wall') === 'on');
campaignFormData.append('allow_custom_recipients', formData.get('allow_custom_recipients') === 'on');
campaignFormData.append('highlight_campaign', formData.get('highlight_campaign') === 'on');
// Handle target_government_levels array
const targetLevels = Array.from(formData.getAll('target_government_levels'));
@@ -651,6 +653,7 @@ class AdminPanel {
form.querySelector('[name="allow_email_editing"]').checked = campaign.allow_email_editing;
form.querySelector('[name="show_response_wall"]').checked = campaign.show_response_wall;
form.querySelector('[name="allow_custom_recipients"]').checked = campaign.allow_custom_recipients || false;
form.querySelector('[name="highlight_campaign"]').checked = campaign.highlight_campaign || false;
// Show/hide custom recipients section based on checkbox
this.toggleCustomRecipientsSection(campaign.allow_custom_recipients);
@@ -710,6 +713,7 @@ class AdminPanel {
updateFormData.append('allow_email_editing', formData.get('allow_email_editing') === 'on');
updateFormData.append('show_response_wall', formData.get('show_response_wall') === 'on');
updateFormData.append('allow_custom_recipients', formData.get('allow_custom_recipients') === 'on');
updateFormData.append('highlight_campaign', formData.get('highlight_campaign') === 'on');
// Handle target_government_levels array
const targetLevels = Array.from(formData.getAll('target_government_levels'));

View File

@@ -4,13 +4,23 @@ class MainApp {
this.init();
}
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
@@ -57,6 +67,119 @@ class MainApp {
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

View File

@@ -15,7 +15,12 @@ class PostalLookup {
init() {
this.form.addEventListener('submit', (e) => this.handleSubmit(e));
this.refreshBtn.addEventListener('click', () => this.handleRefresh());
// Only add refresh button listener if it exists
if (this.refreshBtn) {
this.refreshBtn.addEventListener('click', () => this.handleRefresh());
}
this.input.addEventListener('input', (e) => this.formatPostalCode(e));
}