feat(campaigns): add highlighted campaign feature with admin controls and UI updates
This commit is contained in:
parent
1bdc2b9ae0
commit
4ef4ac414b
@ -221,6 +221,7 @@ RATE_LIMIT_MAX_REQUESTS=100
|
||||
- **👤 Collect User Info**: Request user name and email
|
||||
- **📊 Show Email Count**: Display total emails sent (engagement metric)
|
||||
- **✏️ Allow Email Editing**: Let users customize email template
|
||||
- **⭐ Highlight Campaign**: Feature this campaign on the homepage (replaces postal code search)
|
||||
- **🎯 Target Government Levels**: Select Federal, Provincial, Municipal, School Board
|
||||
|
||||
7. **Set Campaign Status**:
|
||||
@ -248,6 +249,37 @@ The homepage automatically displays all active campaigns in a responsive grid be
|
||||
- Tablet: 2 columns
|
||||
- Mobile: 1 column
|
||||
- **Click Navigation**: Users can click any campaign card to visit the full campaign page
|
||||
|
||||
### Highlighted Campaign Feature
|
||||
|
||||
Promote a priority campaign by highlighting it on the homepage, replacing the postal code search section with featured campaign information.
|
||||
|
||||
**How to Highlight a Campaign**:
|
||||
1. Navigate to Admin Panel → Edit Campaign
|
||||
2. Select the campaign you want to feature
|
||||
3. Check the "⭐ Highlight Campaign" checkbox
|
||||
4. Save changes
|
||||
|
||||
**Highlighted Campaign Display**:
|
||||
- **Homepage Takeover**: Replaces postal code search with campaign showcase
|
||||
- **Featured Badge**: Shows "⭐ Featured Campaign" badge
|
||||
- **Campaign Details**: Displays title, description, and engagement stats
|
||||
- **Primary CTA**: Large "Join This Campaign" button
|
||||
- **Fallback Option**: "Find Representatives by Postal Code" button for users who want standard lookup
|
||||
- **Visual Indicators**: Gold border and badge in admin panel campaign list
|
||||
|
||||
**Important Notes**:
|
||||
- **One at a Time**: Only ONE campaign can be highlighted simultaneously
|
||||
- **Auto-Unset**: Setting a new highlighted campaign automatically removes highlighting from previous campaign
|
||||
- **Requires Active Status**: Campaign must have status="active" to be highlighted
|
||||
- **Admin Control**: Only administrators can set highlighted campaigns
|
||||
|
||||
**Technical Implementation**:
|
||||
- Backend validates and ensures single highlighted campaign via `setHighlightedCampaign()`
|
||||
- Frontend checks `/public/highlighted-campaign` API on page load
|
||||
- Postal code lookup remains accessible via button click
|
||||
- Highlighting state persists across page reloads
|
||||
|
||||
- **Smart Loading**: Shows loading state while fetching campaigns, gracefully hides section if no active campaigns exist
|
||||
- **Security**: HTML content is escaped to prevent XSS attacks
|
||||
- **Sorting**: Campaigns display newest first by creation date
|
||||
|
||||
@ -176,6 +176,75 @@ class CampaignsController {
|
||||
}
|
||||
}
|
||||
|
||||
// Get the currently highlighted campaign (public)
|
||||
async getHighlightedCampaign(req, res, next) {
|
||||
try {
|
||||
const campaign = await nocoDB.getHighlightedCampaign();
|
||||
|
||||
if (!campaign) {
|
||||
return res.json({
|
||||
success: true,
|
||||
campaign: null
|
||||
});
|
||||
}
|
||||
|
||||
const id = campaign.ID || campaign.Id || campaign.id;
|
||||
|
||||
// Get email count if show_email_count is enabled
|
||||
let emailCount = null;
|
||||
const showEmailCount = campaign['Show Email Count'] || campaign.show_email_count;
|
||||
if (showEmailCount && id != null) {
|
||||
emailCount = await nocoDB.getCampaignEmailCount(id);
|
||||
}
|
||||
|
||||
// Get call count if show_call_count is enabled
|
||||
let callCount = null;
|
||||
const showCallCount = campaign['Show Call Count'] || campaign.show_call_count;
|
||||
if (showCallCount && id != null) {
|
||||
callCount = await nocoDB.getCampaignCallCount(id);
|
||||
}
|
||||
|
||||
// Get verified response count
|
||||
let verifiedResponseCount = 0;
|
||||
if (id != null) {
|
||||
verifiedResponseCount = await nocoDB.getCampaignVerifiedResponseCount(id);
|
||||
}
|
||||
|
||||
const rawTargetLevels = campaign['Target Government Levels'] || campaign.target_government_levels;
|
||||
const normalizedTargetLevels = normalizeTargetLevels(rawTargetLevels);
|
||||
|
||||
// Return only public-facing information
|
||||
const highlightedCampaign = {
|
||||
id,
|
||||
slug: campaign['Campaign Slug'] || campaign.slug,
|
||||
title: campaign['Campaign Title'] || campaign.title,
|
||||
description: campaign['Description'] || campaign.description,
|
||||
call_to_action: campaign['Call to Action'] || campaign.call_to_action,
|
||||
cover_photo: campaign['Cover Photo'] || campaign.cover_photo,
|
||||
show_email_count: showEmailCount,
|
||||
show_call_count: showCallCount,
|
||||
show_response_wall: campaign['Show Response Wall Button'] || campaign.show_response_wall,
|
||||
target_government_levels: normalizedTargetLevels,
|
||||
created_at: campaign.CreatedAt || campaign.created_at,
|
||||
emailCount,
|
||||
callCount,
|
||||
verifiedResponseCount
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
campaign: highlightedCampaign
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Get highlighted campaign error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to retrieve highlighted campaign',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Get all campaigns (for admin panel)
|
||||
async getAllCampaigns(req, res, next) {
|
||||
try {
|
||||
@ -210,6 +279,7 @@ class CampaignsController {
|
||||
allow_email_editing: campaign['Allow Email Editing'] || campaign.allow_email_editing,
|
||||
allow_custom_recipients: campaign['Allow Custom Recipients'] || campaign.allow_custom_recipients,
|
||||
show_response_wall: campaign['Show Response Wall Button'] || campaign.show_response_wall,
|
||||
highlight_campaign: campaign['Highlight Campaign'] || campaign.highlight_campaign || false,
|
||||
target_government_levels: normalizedTargetLevels,
|
||||
created_at: campaign.CreatedAt || campaign.created_at,
|
||||
updated_at: campaign.UpdatedAt || campaign.updated_at,
|
||||
@ -587,7 +657,8 @@ class CampaignsController {
|
||||
'show_email_count',
|
||||
'allow_email_editing',
|
||||
'show_response_wall',
|
||||
'allow_custom_recipients'
|
||||
'allow_custom_recipients',
|
||||
'highlight_campaign'
|
||||
];
|
||||
|
||||
booleanFields.forEach(field => {
|
||||
@ -600,6 +671,17 @@ class CampaignsController {
|
||||
}
|
||||
});
|
||||
|
||||
// Handle highlight_campaign special logic
|
||||
// If this campaign is being set to highlighted, unset all others
|
||||
if (updates.highlight_campaign === true) {
|
||||
await nocoDB.setHighlightedCampaign(id);
|
||||
// Remove from updates since we already handled it
|
||||
delete updates.highlight_campaign;
|
||||
} else if (updates.highlight_campaign === false) {
|
||||
// Just unset this one
|
||||
// Keep in updates to let the normal update flow handle it
|
||||
}
|
||||
|
||||
console.log('Updates object before saving:', updates);
|
||||
|
||||
if (updates.status !== undefined) {
|
||||
|
||||
@ -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));
|
||||
|
||||
// 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));
|
||||
}
|
||||
|
||||
|
||||
@ -178,6 +178,7 @@ router.get('/campaigns/:id/analytics', requireAuth, rateLimiter.general, campaig
|
||||
|
||||
// Campaign endpoints (Public)
|
||||
router.get('/public/campaigns', rateLimiter.general, campaignsController.getPublicCampaigns);
|
||||
router.get('/public/highlighted-campaign', rateLimiter.general, campaignsController.getHighlightedCampaign);
|
||||
router.get('/campaigns/:slug', rateLimiter.general, campaignsController.getCampaignBySlug);
|
||||
router.get('/campaigns/:slug/qrcode', rateLimiter.general, campaignsController.generateQRCode);
|
||||
router.get('/campaigns/:slug/representatives/:postalCode', rateLimiter.representAPI, campaignsController.getRepresentativesForCampaign);
|
||||
|
||||
@ -440,6 +440,7 @@ class NocoDBService {
|
||||
'Allow Email Editing': campaignData.allow_email_editing,
|
||||
'Allow Custom Recipients': campaignData.allow_custom_recipients,
|
||||
'Show Response Wall Button': campaignData.show_response_wall,
|
||||
'Highlight Campaign': campaignData.highlight_campaign,
|
||||
'Target Government Levels': campaignData.target_government_levels,
|
||||
'Created By User ID': campaignData.created_by_user_id,
|
||||
'Created By User Email': campaignData.created_by_user_email,
|
||||
@ -474,6 +475,7 @@ class NocoDBService {
|
||||
if (updates.allow_email_editing !== undefined) mappedUpdates['Allow Email Editing'] = updates.allow_email_editing;
|
||||
if (updates.allow_custom_recipients !== undefined) mappedUpdates['Allow Custom Recipients'] = updates.allow_custom_recipients;
|
||||
if (updates.show_response_wall !== undefined) mappedUpdates['Show Response Wall Button'] = updates.show_response_wall;
|
||||
if (updates.highlight_campaign !== undefined) mappedUpdates['Highlight Campaign'] = updates.highlight_campaign;
|
||||
if (updates.target_government_levels !== undefined) mappedUpdates['Target Government Levels'] = updates.target_government_levels;
|
||||
if (updates.updated_at !== undefined) mappedUpdates['UpdatedAt'] = updates.updated_at;
|
||||
|
||||
@ -497,6 +499,54 @@ class NocoDBService {
|
||||
}
|
||||
}
|
||||
|
||||
// Get the currently highlighted campaign
|
||||
async getHighlightedCampaign() {
|
||||
try {
|
||||
const response = await this.getAll(this.tableIds.campaigns, {
|
||||
where: `(Highlight Campaign,eq,true)`,
|
||||
limit: 1
|
||||
});
|
||||
return response.list && response.list.length > 0 ? response.list[0] : null;
|
||||
} catch (error) {
|
||||
console.error('Get highlighted campaign failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Set a campaign as highlighted (and unset all others)
|
||||
async setHighlightedCampaign(campaignId) {
|
||||
try {
|
||||
// First, unset any existing highlighted campaigns
|
||||
const currentHighlighted = await this.getHighlightedCampaign();
|
||||
if (currentHighlighted) {
|
||||
const currentId = currentHighlighted.ID || currentHighlighted.Id || currentHighlighted.id;
|
||||
if (currentId && currentId !== campaignId) {
|
||||
await this.updateCampaign(currentId, { highlight_campaign: false });
|
||||
}
|
||||
}
|
||||
|
||||
// Then set the new highlighted campaign
|
||||
await this.updateCampaign(campaignId, { highlight_campaign: true });
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Set highlighted campaign failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Unset highlighted campaign
|
||||
async unsetHighlightedCampaign(campaignId) {
|
||||
try {
|
||||
await this.updateCampaign(campaignId, { highlight_campaign: false });
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Unset highlighted campaign failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Campaign email tracking methods
|
||||
async logCampaignEmail(emailData) {
|
||||
try {
|
||||
|
||||
@ -56,6 +56,27 @@ The application includes a flexible campaign configuration system that allows ad
|
||||
- Paused: Temporarily disabled
|
||||
- Archived: Completed campaigns, read-only
|
||||
|
||||
**Highlight Campaign** (`highlight_campaign`):
|
||||
- **Purpose**: Features a single priority campaign on the homepage by replacing the postal code search section
|
||||
- **Implementation**: Checkbox field in campaigns table (default: false)
|
||||
- **When Enabled**: Campaign takes over homepage hero section with featured display
|
||||
- **Exclusivity**: Only ONE campaign can be highlighted at a time
|
||||
- **Use Cases**:
|
||||
- ✅ Promote urgent or high-priority campaigns
|
||||
- ✅ Drive maximum participation for time-sensitive issues
|
||||
- ✅ Showcase flagship campaigns to new visitors
|
||||
- ❌ Don't use for evergreen or background campaigns
|
||||
- **Database**: `highlight_campaign` BOOLEAN field in campaigns table
|
||||
- **Backend**: `setHighlightedCampaign()` ensures single highlighted campaign by unsetting others
|
||||
- **Frontend**: `main.js` checks `/public/highlighted-campaign` on page load and replaces UI
|
||||
- **Admin Panel**:
|
||||
- Checkbox labeled "⭐ Highlight on Homepage" in create/edit forms
|
||||
- Gold badge and border on highlighted campaign in campaign list
|
||||
- Visual indicator: "⭐ Highlighted" badge on campaign card
|
||||
- **API Endpoints**:
|
||||
- `GET /public/highlighted-campaign`: Returns current highlighted campaign or null
|
||||
- Backend validates only one campaign is highlighted via database logic
|
||||
|
||||
### Technical Implementation
|
||||
|
||||
**Database Schema** (`build-nocodb.sh`):
|
||||
|
||||
@ -54,7 +54,16 @@ The application supports flexible campaign configuration through the admin panel
|
||||
- Federal, Provincial, Municipal, School Board
|
||||
- Filters which representatives are shown
|
||||
|
||||
8. **Campaign Status** (`status`) - **Required**
|
||||
8. **Highlight Campaign** (`highlight_campaign`) - **Default: OFF** ❌
|
||||
- Displays the campaign prominently on the homepage
|
||||
- Replaces the postal code search section with campaign information
|
||||
- Only ONE campaign can be highlighted at a time
|
||||
- **Database**: Boolean field in campaigns table
|
||||
- **Backend**: `setHighlightedCampaign()` ensures only one campaign is highlighted
|
||||
- **Frontend**: `main.js` checks on page load and replaces postal code section
|
||||
- **Admin Panel**: Shows ⭐ badge on highlighted campaign card
|
||||
|
||||
9. **Campaign Status** (`status`) - **Required**
|
||||
- Draft: Testing only, hidden from public
|
||||
- Active: Visible on main page
|
||||
- Paused: Temporarily disabled
|
||||
|
||||
@ -1093,6 +1093,12 @@ create_campaigns_table() {
|
||||
"uidt": "Checkbox",
|
||||
"cdf": "false"
|
||||
},
|
||||
{
|
||||
"column_name": "highlight_campaign",
|
||||
"title": "Highlight Campaign",
|
||||
"uidt": "Checkbox",
|
||||
"cdf": "false"
|
||||
},
|
||||
{
|
||||
"column_name": "target_government_levels",
|
||||
"title": "Target Government Levels",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user