feat(campaigns): add highlighted campaign feature with admin controls and UI updates
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user