diff --git a/influence/README.MD b/influence/README.MD index 31e9a89..618195c 100644 --- a/influence/README.MD +++ b/influence/README.MD @@ -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 diff --git a/influence/app/controllers/campaigns.js b/influence/app/controllers/campaigns.js index 2d30d71..48336b7 100644 --- a/influence/app/controllers/campaigns.js +++ b/influence/app/controllers/campaigns.js @@ -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) { diff --git a/influence/app/public/admin.html b/influence/app/public/admin.html index c94d446..59860b1 100644 --- a/influence/app/public/admin.html +++ b/influence/app/public/admin.html @@ -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, +
Looking up your representatives...
+ + + diff --git a/influence/app/public/js/admin.js b/influence/app/public/js/admin.js index 1bb44ca..fae0ccd 100644 --- a/influence/app/public/js/admin.js +++ b/influence/app/public/js/admin.js @@ -488,7 +488,8 @@ class AdminPanel { } listDiv.innerHTML = this.campaigns.map(campaign => ` -