Updated campagin cover photos

This commit is contained in:
admin 2025-10-01 12:20:55 -06:00
parent ba246d5dc8
commit 8915299707
14 changed files with 804 additions and 292 deletions

2
.gitignore vendored
View File

@ -14,3 +14,5 @@
.excalidraw .excalidraw
/.VSCodeCounter /.VSCodeCounter
/influence/app/public/uploads

View File

@ -2,6 +2,45 @@ const nocoDB = require('../services/nocodb');
const emailService = require('../services/email'); const emailService = require('../services/email');
const representAPI = require('../services/represent-api'); const representAPI = require('../services/represent-api');
const { generateSlug, validateSlug } = require('../utils/validators'); const { generateSlug, validateSlug } = require('../utils/validators');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
// Configure multer for file uploads
const storage = multer.diskStorage({
destination: function (req, file, cb) {
const uploadDir = path.join(__dirname, '../public/uploads');
// Ensure the upload directory exists
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
cb(null, uploadDir);
},
filename: function (req, file, cb) {
// Generate unique filename with timestamp
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
cb(null, 'cover-' + uniqueSuffix + path.extname(file.originalname));
}
});
const upload = multer({
storage: storage,
limits: {
fileSize: 5 * 1024 * 1024 // 5MB limit
},
fileFilter: function (req, file, cb) {
// Accept only image files
const allowedTypes = /jpeg|jpg|png|gif|webp/;
const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase());
const mimetype = allowedTypes.test(file.mimetype);
if (mimetype && extname) {
return cb(null, true);
} else {
cb(new Error('Only image files are allowed (jpeg, jpg, png, gif, webp)'));
}
}
});
const VALID_CAMPAIGN_STATUSES = ['draft', 'active', 'paused', 'archived']; const VALID_CAMPAIGN_STATUSES = ['draft', 'active', 'paused', 'archived'];
@ -76,6 +115,7 @@ class CampaignsController {
email_subject: campaign['Email Subject'] || campaign.email_subject, email_subject: campaign['Email Subject'] || campaign.email_subject,
email_body: campaign['Email Body'] || campaign.email_body, email_body: campaign['Email Body'] || campaign.email_body,
call_to_action: campaign['Call to Action'] || campaign.call_to_action, call_to_action: campaign['Call to Action'] || campaign.call_to_action,
cover_photo: campaign['Cover Photo'] || campaign.cover_photo,
status: normalizeStatus(campaign['Status'] || campaign.status), status: normalizeStatus(campaign['Status'] || campaign.status),
allow_smtp_email: campaign['Allow SMTP Email'] || campaign.allow_smtp_email, allow_smtp_email: campaign['Allow SMTP Email'] || campaign.allow_smtp_email,
allow_mailto_link: campaign['Allow Mailto Link'] || campaign.allow_mailto_link, allow_mailto_link: campaign['Allow Mailto Link'] || campaign.allow_mailto_link,
@ -157,6 +197,7 @@ class CampaignsController {
email_subject: campaign['Email Subject'] || campaign.email_subject, email_subject: campaign['Email Subject'] || campaign.email_subject,
email_body: campaign['Email Body'] || campaign.email_body, email_body: campaign['Email Body'] || campaign.email_body,
call_to_action: campaign['Call to Action'] || campaign.call_to_action, call_to_action: campaign['Call to Action'] || campaign.call_to_action,
cover_photo: campaign['Cover Photo'] || campaign.cover_photo,
status: normalizeStatus(campaign['Status'] || campaign.status), status: normalizeStatus(campaign['Status'] || campaign.status),
allow_smtp_email: campaign['Allow SMTP Email'] || campaign.allow_smtp_email, allow_smtp_email: campaign['Allow SMTP Email'] || campaign.allow_smtp_email,
allow_mailto_link: campaign['Allow Mailto Link'] || campaign.allow_mailto_link, allow_mailto_link: campaign['Allow Mailto Link'] || campaign.allow_mailto_link,
@ -214,6 +255,10 @@ class CampaignsController {
} }
} }
// Debug cover photo value
const coverPhoto = campaign['Cover Photo'] || campaign.cover_photo;
console.log('Raw cover_photo from NocoDB:', coverPhoto, 'Type:', typeof coverPhoto);
res.json({ res.json({
success: true, success: true,
campaign: { campaign: {
@ -222,6 +267,7 @@ class CampaignsController {
title: campaign['Campaign Title'] || campaign.title, title: campaign['Campaign Title'] || campaign.title,
description: campaign['Description'] || campaign.description, description: campaign['Description'] || campaign.description,
call_to_action: campaign['Call to Action'] || campaign.call_to_action, call_to_action: campaign['Call to Action'] || campaign.call_to_action,
cover_photo: coverPhoto || null,
email_subject: campaign['Email Subject'] || campaign.email_subject, email_subject: campaign['Email Subject'] || campaign.email_subject,
email_body: campaign['Email Body'] || campaign.email_body, email_body: campaign['Email Body'] || campaign.email_body,
allow_smtp_email: campaign['Allow SMTP Email'] || campaign.allow_smtp_email, allow_smtp_email: campaign['Allow SMTP Email'] || campaign.allow_smtp_email,
@ -294,7 +340,9 @@ class CampaignsController {
// Add user ownership data // Add user ownership data
created_by_user_id: ownerUserId, created_by_user_id: ownerUserId,
created_by_user_email: ownerEmail, created_by_user_email: ownerEmail,
created_by_user_name: ownerName created_by_user_name: ownerName,
// Add cover photo if uploaded
cover_photo: req.file ? req.file.filename : null
}; };
const campaign = await nocoDB.createCampaign(campaignData); const campaign = await nocoDB.createCampaign(campaignData);
@ -310,6 +358,7 @@ class CampaignsController {
email_subject: campaign['Email Subject'] || campaign.email_subject, email_subject: campaign['Email Subject'] || campaign.email_subject,
email_body: campaign['Email Body'] || campaign.email_body, email_body: campaign['Email Body'] || campaign.email_body,
call_to_action: campaign['Call to Action'] || campaign.call_to_action, call_to_action: campaign['Call to Action'] || campaign.call_to_action,
cover_photo: campaign['Cover Photo'] || campaign.cover_photo,
status: normalizeStatus(campaign['Status'] || campaign.status), status: normalizeStatus(campaign['Status'] || campaign.status),
allow_smtp_email: campaign['Allow SMTP Email'] || campaign.allow_smtp_email, allow_smtp_email: campaign['Allow SMTP Email'] || campaign.allow_smtp_email,
allow_mailto_link: campaign['Allow Mailto Link'] || campaign.allow_mailto_link, allow_mailto_link: campaign['Allow Mailto Link'] || campaign.allow_mailto_link,
@ -390,6 +439,16 @@ class CampaignsController {
updates.target_government_levels = normalizeTargetLevels(updates.target_government_levels); updates.target_government_levels = normalizeTargetLevels(updates.target_government_levels);
} }
// Handle cover photo upload
if (req.file) {
console.log('Cover photo file received:', req.file.filename);
updates.cover_photo = req.file.filename;
} else {
console.log('No cover photo file in request');
}
console.log('Updates object before saving:', updates);
if (updates.status !== undefined) { if (updates.status !== undefined) {
const sanitizedStatus = normalizeStatus(updates.status, null); const sanitizedStatus = normalizeStatus(updates.status, null);
if (!sanitizedStatus) { if (!sanitizedStatus) {
@ -423,6 +482,7 @@ class CampaignsController {
email_subject: campaign['Email Subject'] || campaign.email_subject, email_subject: campaign['Email Subject'] || campaign.email_subject,
email_body: campaign['Email Body'] || campaign.email_body, email_body: campaign['Email Body'] || campaign.email_body,
call_to_action: campaign['Call to Action'] || campaign.call_to_action, call_to_action: campaign['Call to Action'] || campaign.call_to_action,
cover_photo: campaign['Cover Photo'] || campaign.cover_photo,
status: normalizeStatus(campaign['Status'] || campaign.status), status: normalizeStatus(campaign['Status'] || campaign.status),
allow_smtp_email: campaign['Allow SMTP Email'] || campaign.allow_smtp_email, allow_smtp_email: campaign['Allow SMTP Email'] || campaign.allow_smtp_email,
allow_mailto_link: campaign['Allow Mailto Link'] || campaign.allow_mailto_link, allow_mailto_link: campaign['Allow Mailto Link'] || campaign.allow_mailto_link,
@ -834,4 +894,8 @@ class CampaignsController {
} }
} }
module.exports = new CampaignsController(); // Export controller instance and upload middleware
const controller = new CampaignsController();
controller.upload = upload;
module.exports = controller;

View File

@ -27,7 +27,8 @@
"axios": "^1.5.0", "axios": "^1.5.0",
"nodemailer": "^6.9.4", "nodemailer": "^6.9.4",
"express-session": "^1.17.3", "express-session": "^1.17.3",
"bcryptjs": "^2.4.3" "bcryptjs": "^2.4.3",
"multer": "^1.4.5-lts.1"
}, },
"devDependencies": { "devDependencies": {
"nodemon": "^3.0.1", "nodemon": "^3.0.1",

View File

@ -84,13 +84,55 @@
gap: 0.5rem; gap: 0.5rem;
} }
.campaign-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.campaign-card { .campaign-card {
border: 1px solid #ddd; border: 1px solid #ddd;
border-radius: 8px; border-radius: 8px;
padding: 1.5rem; padding: 0;
margin-bottom: 1rem;
background: white; background: white;
box-shadow: 0 2px 4px rgba(0,0,0,0.1); box-shadow: 0 2px 4px rgba(0,0,0,0.1);
overflow: hidden;
transition: transform 0.2s, box-shadow 0.2s;
}
.campaign-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
.campaign-card-cover {
background-size: cover;
background-position: center;
min-height: 180px;
position: relative;
display: flex;
align-items: flex-end;
}
.campaign-card-cover-overlay {
background: linear-gradient(to top, rgba(0,0,0,0.8) 0%, rgba(0,0,0,0.4) 60%, transparent 100%);
width: 100%;
padding: 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.campaign-card-cover-overlay h3 {
margin: 0;
color: white;
text-shadow: 0 2px 4px rgba(0,0,0,0.5);
}
.campaign-card > .campaign-meta,
.campaign-card > .campaign-actions {
padding: 1.5rem;
} }
.campaign-header { .campaign-header {
@ -98,6 +140,7 @@
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 1rem; margin-bottom: 1rem;
padding: 1.5rem;
} }
.campaign-header h3 { .campaign-header h3 {
@ -150,7 +193,7 @@
} }
.btn { .btn {
padding: 0.75rem 1.5rem; padding: 0.5rem 1rem;
border: none; border: none;
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
@ -700,7 +743,7 @@
<p>Loading campaigns...</p> <p>Loading campaigns...</p>
</div> </div>
<div id="campaigns-list" class="campaign-list"> <div id="campaigns-list" class="campaign-grid">
<!-- Campaigns will be loaded here --> <!-- Campaigns will be loaded here -->
</div> </div>
</div> </div>
@ -770,6 +813,12 @@ Sincerely,
placeholder="Join thousands of Albertans in protecting our provincial parks. Send an email to your representatives today!"></textarea> placeholder="Join thousands of Albertans in protecting our provincial parks. Send an email to your representatives today!"></textarea>
</div> </div>
<div class="form-group">
<label for="create-cover-photo">🖼️ Cover Photo</label>
<input type="file" id="create-cover-photo" name="cover_photo" accept="image/jpeg,image/jpg,image/png,image/gif,image/webp">
<small style="color: #666; font-size: 0.9em;">Upload a cover image for your campaign (max 5MB, jpeg/jpg/png/gif/webp)</small>
</div>
<div class="section-header">⚙️ Campaign Settings</div> <div class="section-header">⚙️ Campaign Settings</div>
<div class="form-group"> <div class="form-group">
<div class="checkbox-group"> <div class="checkbox-group">
@ -880,6 +929,13 @@ Sincerely,
<textarea id="edit-call-to-action" name="call_to_action" rows="3"></textarea> <textarea id="edit-call-to-action" name="call_to_action" rows="3"></textarea>
</div> </div>
<div class="form-group">
<label for="edit-cover-photo">🖼️ Cover Photo</label>
<div id="current-cover-photo" style="margin-bottom: 0.5rem;"></div>
<input type="file" id="edit-cover-photo" name="cover_photo" accept="image/jpeg,image/jpg,image/png,image/gif,image/webp">
<small style="color: #666; font-size: 0.9em;">Upload a new cover image (max 5MB, jpeg/jpg/png/gif/webp) or leave empty to keep current</small>
</div>
<div class="form-group"> <div class="form-group">
<label>Campaign Settings</label> <label>Campaign Settings</label>
<div class="checkbox-group"> <div class="checkbox-group">

View File

@ -12,11 +12,40 @@
padding: 3rem 0; padding: 3rem 0;
text-align: center; text-align: center;
margin-bottom: 2rem; margin-bottom: 2rem;
position: relative;
overflow: hidden;
}
.campaign-header.has-cover {
background-size: cover;
background-position: center;
background-repeat: no-repeat;
min-height: 350px;
display: flex;
align-items: center;
justify-content: center;
}
.campaign-header.has-cover::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 1;
}
.campaign-header > * {
position: relative;
z-index: 2;
} }
.campaign-header h1 { .campaign-header h1 {
font-size: 2.5rem; font-size: 2.5rem;
margin-bottom: 1rem; margin-bottom: 1rem;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.7);
} }
.campaign-stats { .campaign-stats {

View File

@ -85,10 +85,11 @@
.campaign-card { .campaign-card {
border: 1px solid #ddd; border: 1px solid #ddd;
border-radius: 8px; border-radius: 8px;
padding: 1.5rem; padding: 0;
background: white; background: white;
box-shadow: 0 2px 4px rgba(0,0,0,0.1); box-shadow: 0 2px 4px rgba(0,0,0,0.1);
transition: transform 0.2s, box-shadow 0.2s; transition: transform 0.2s, box-shadow 0.2s;
overflow: hidden;
} }
.campaign-card:hover { .campaign-card:hover {
@ -96,11 +97,42 @@
box-shadow: 0 4px 8px rgba(0,0,0,0.15); box-shadow: 0 4px 8px rgba(0,0,0,0.15);
} }
.campaign-card-cover {
background-size: cover;
background-position: center;
min-height: 180px;
position: relative;
display: flex;
align-items: flex-end;
}
.campaign-card-cover-overlay {
background: linear-gradient(to top, rgba(0,0,0,0.8) 0%, rgba(0,0,0,0.4) 60%, transparent 100%);
width: 100%;
padding: 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.campaign-card-cover-overlay h3 {
margin: 0;
color: white;
text-shadow: 0 2px 4px rgba(0,0,0,0.5);
font-size: 1.2rem;
}
.campaign-card > .campaign-meta,
.campaign-card > .campaign-actions {
padding: 1.5rem;
}
.campaign-header { .campaign-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: flex-start; align-items: flex-start;
margin-bottom: 1rem; margin-bottom: 1rem;
padding: 1.5rem;
} }
.campaign-header h3 { .campaign-header h3 {

View File

@ -467,10 +467,19 @@ class AdminPanel {
listDiv.innerHTML = this.campaigns.map(campaign => ` listDiv.innerHTML = this.campaigns.map(campaign => `
<div class="campaign-card" data-campaign-id="${campaign.id}"> <div class="campaign-card" data-campaign-id="${campaign.id}">
${campaign.cover_photo ? `
<div class="campaign-card-cover" style="background-image: url('/uploads/${campaign.cover_photo}');">
<div class="campaign-card-cover-overlay">
<h3>${this.escapeHtml(campaign.title)}</h3>
<span class="status-badge status-${campaign.status}">${campaign.status}</span>
</div>
</div>
` : `
<div class="campaign-header"> <div class="campaign-header">
<h3>${this.escapeHtml(campaign.title)}</h3> <h3>${this.escapeHtml(campaign.title)}</h3>
<span class="status-badge status-${campaign.status}">${campaign.status}</span> <span class="status-badge status-${campaign.status}">${campaign.status}</span>
</div> </div>
`}
<div class="campaign-meta"> <div class="campaign-meta">
<p><strong>Slug:</strong> <code>/campaign/${campaign.slug}</code></p> <p><strong>Slug:</strong> <code>/campaign/${campaign.slug}</code></p>
@ -531,22 +540,34 @@ class AdminPanel {
e.preventDefault(); e.preventDefault();
const formData = new FormData(e.target); const formData = new FormData(e.target);
const campaignData = {
title: formData.get('title'), // Convert checkboxes to boolean values
description: formData.get('description'), const campaignFormData = new FormData();
email_subject: formData.get('email_subject'), campaignFormData.append('title', formData.get('title'));
email_body: formData.get('email_body'), campaignFormData.append('description', formData.get('description') || '');
call_to_action: formData.get('call_to_action'), campaignFormData.append('email_subject', formData.get('email_subject'));
status: formData.get('status'), campaignFormData.append('email_body', formData.get('email_body'));
allow_smtp_email: formData.get('allow_smtp_email') === 'on', campaignFormData.append('call_to_action', formData.get('call_to_action') || '');
allow_mailto_link: formData.get('allow_mailto_link') === 'on', campaignFormData.append('status', formData.get('status'));
collect_user_info: formData.get('collect_user_info') === 'on', campaignFormData.append('allow_smtp_email', formData.get('allow_smtp_email') === 'on');
show_email_count: formData.get('show_email_count') === 'on', campaignFormData.append('allow_mailto_link', formData.get('allow_mailto_link') === 'on');
target_government_levels: Array.from(formData.getAll('target_government_levels')) campaignFormData.append('collect_user_info', formData.get('collect_user_info') === 'on');
}; campaignFormData.append('show_email_count', formData.get('show_email_count') === 'on');
// Handle target_government_levels array
const targetLevels = Array.from(formData.getAll('target_government_levels'));
targetLevels.forEach(level => {
campaignFormData.append('target_government_levels', level);
});
// Handle cover photo file upload
const coverPhotoFile = formData.get('cover_photo');
if (coverPhotoFile && coverPhotoFile.size > 0) {
campaignFormData.append('cover_photo', coverPhotoFile);
}
try { try {
const response = await window.apiClient.post('/admin/campaigns', campaignData); const response = await window.apiClient.postFormData('/admin/campaigns', campaignFormData);
if (response.success) { if (response.success) {
this.showMessage('Campaign created successfully!', 'success'); this.showMessage('Campaign created successfully!', 'success');
@ -584,6 +605,19 @@ class AdminPanel {
// Status select // Status select
form.querySelector('[name="status"]').value = campaign.status || 'draft'; form.querySelector('[name="status"]').value = campaign.status || 'draft';
// Show current cover photo if exists
const currentCoverDiv = document.getElementById('current-cover-photo');
if (campaign.cover_photo) {
currentCoverDiv.innerHTML = `
<div style="margin-bottom: 0.5rem;">
<small style="color: #666;">Current cover photo:</small><br>
<img src="/uploads/${campaign.cover_photo}" alt="Current cover" style="max-width: 200px; max-height: 150px; border-radius: 4px; margin-top: 0.25rem;">
</div>
`;
} else {
currentCoverDiv.innerHTML = '<small style="color: #999;">No cover photo uploaded</small>';
}
// Checkboxes // Checkboxes
form.querySelector('[name="allow_smtp_email"]').checked = campaign.allow_smtp_email; form.querySelector('[name="allow_smtp_email"]').checked = campaign.allow_smtp_email;
form.querySelector('[name="allow_mailto_link"]').checked = campaign.allow_mailto_link; form.querySelector('[name="allow_mailto_link"]').checked = campaign.allow_mailto_link;
@ -612,25 +646,34 @@ class AdminPanel {
if (!this.currentCampaign) return; if (!this.currentCampaign) return;
const formData = new FormData(e.target); const formData = new FormData(e.target);
const updates = {
title: formData.get('title'), // Convert checkboxes to boolean values and build FormData for upload
description: formData.get('description'), const updateFormData = new FormData();
email_subject: formData.get('email_subject'), updateFormData.append('title', formData.get('title'));
email_body: formData.get('email_body'), updateFormData.append('description', formData.get('description') || '');
call_to_action: formData.get('call_to_action'), updateFormData.append('email_subject', formData.get('email_subject'));
status: formData.get('status'), updateFormData.append('email_body', formData.get('email_body'));
allow_smtp_email: formData.get('allow_smtp_email') === 'on', updateFormData.append('call_to_action', formData.get('call_to_action') || '');
allow_mailto_link: formData.get('allow_mailto_link') === 'on', updateFormData.append('status', formData.get('status'));
collect_user_info: formData.get('collect_user_info') === 'on', updateFormData.append('allow_smtp_email', formData.get('allow_smtp_email') === 'on');
show_email_count: formData.get('show_email_count') === 'on', updateFormData.append('allow_mailto_link', formData.get('allow_mailto_link') === 'on');
target_government_levels: Array.from(formData.getAll('target_government_levels')) updateFormData.append('collect_user_info', formData.get('collect_user_info') === 'on');
}; updateFormData.append('show_email_count', formData.get('show_email_count') === 'on');
// Handle target_government_levels array
const targetLevels = Array.from(formData.getAll('target_government_levels'));
targetLevels.forEach(level => {
updateFormData.append('target_government_levels', level);
});
// Handle cover photo file upload if a new one is selected
const coverPhotoFile = formData.get('cover_photo');
if (coverPhotoFile && coverPhotoFile.size > 0) {
updateFormData.append('cover_photo', coverPhotoFile);
}
try { try {
const response = await window.apiClient.makeRequest(`/admin/campaigns/${this.currentCampaign.id}`, { const response = await window.apiClient.putFormData(`/admin/campaigns/${this.currentCampaign.id}`, updateFormData);
method: 'PUT',
body: JSON.stringify(updates)
});
if (response.success) { if (response.success) {
this.showMessage('Campaign updated successfully!', 'success'); this.showMessage('Campaign updated successfully!', 'success');

View File

@ -45,6 +45,56 @@ class APIClient {
}); });
} }
async postFormData(endpoint, formData) {
// Don't set Content-Type header - browser will set it with boundary for multipart/form-data
const config = {
method: 'POST',
body: formData
};
try {
const response = await fetch(`${this.baseURL}${endpoint}`, config);
const data = await response.json();
if (!response.ok) {
const error = new Error(data.error || data.message || `HTTP ${response.status}`);
error.status = response.status;
error.data = data;
throw error;
}
return data;
} catch (error) {
console.error('API request failed:', error);
throw error;
}
}
async putFormData(endpoint, formData) {
// Don't set Content-Type header - browser will set it with boundary for multipart/form-data
const config = {
method: 'PUT',
body: formData
};
try {
const response = await fetch(`${this.baseURL}${endpoint}`, config);
const data = await response.json();
if (!response.ok) {
const error = new Error(data.error || data.message || `HTTP ${response.status}`);
error.status = response.status;
error.data = data;
throw error;
}
return data;
} catch (error) {
console.error('API request failed:', error);
throw error;
}
}
// Health check // Health check
async checkHealth() { async checkHealth() {
return this.get('/health'); return this.get('/health');

View File

@ -39,6 +39,8 @@ class CampaignPage {
} }
this.campaign = data.campaign; this.campaign = data.campaign;
console.log('Campaign data loaded:', this.campaign);
console.log('Cover photo value:', this.campaign.cover_photo);
this.renderCampaign(); this.renderCampaign();
} catch (error) { } catch (error) {
this.showError('Failed to load campaign: ' + error.message); this.showError('Failed to load campaign: ' + error.message);
@ -54,6 +56,25 @@ class CampaignPage {
document.getElementById('campaign-title').textContent = this.campaign.title; document.getElementById('campaign-title').textContent = this.campaign.title;
document.getElementById('campaign-description').textContent = this.campaign.description; document.getElementById('campaign-description').textContent = this.campaign.description;
// Add cover photo if available
const headerElement = document.querySelector('.campaign-header');
if (this.campaign.cover_photo) {
// Clean the cover_photo value - it should just be a filename
const coverPhotoFilename = String(this.campaign.cover_photo).trim();
console.log('Cover photo filename:', coverPhotoFilename);
if (coverPhotoFilename && coverPhotoFilename !== 'null' && coverPhotoFilename !== 'undefined') {
headerElement.classList.add('has-cover');
headerElement.style.backgroundImage = `url('/uploads/${coverPhotoFilename}')`;
} else {
headerElement.classList.remove('has-cover');
headerElement.style.backgroundImage = '';
}
} else {
headerElement.classList.remove('has-cover');
headerElement.style.backgroundImage = '';
}
// Show email count if enabled // Show email count if enabled
if (this.campaign.show_email_count && this.campaign.emailCount !== null) { if (this.campaign.show_email_count && this.campaign.emailCount !== null) {
document.getElementById('email-count').textContent = this.campaign.emailCount; document.getElementById('email-count').textContent = this.campaign.emailCount;

View File

@ -482,10 +482,19 @@ class UserDashboard {
listDiv.innerHTML = this.campaigns.map(campaign => ` listDiv.innerHTML = this.campaigns.map(campaign => `
<div class="campaign-card" data-campaign-id="${campaign.id}"> <div class="campaign-card" data-campaign-id="${campaign.id}">
${campaign.cover_photo ? `
<div class="campaign-card-cover" style="background-image: url('/uploads/${campaign.cover_photo}');">
<div class="campaign-card-cover-overlay">
<h3>${this.escapeHtml(campaign.title)}</h3>
<span class="status-badge status-${campaign.status}">${campaign.status}</span>
</div>
</div>
` : `
<div class="campaign-header"> <div class="campaign-header">
<h3>${this.escapeHtml(campaign.title)}</h3> <h3>${this.escapeHtml(campaign.title)}</h3>
<span class="status-badge status-${campaign.status}">${campaign.status}</span> <span class="status-badge status-${campaign.status}">${campaign.status}</span>
</div> </div>
`}
<div class="campaign-meta"> <div class="campaign-meta">
<p><strong>Slug:</strong> <code>/campaign/${campaign.slug}</code></p> <p><strong>Slug:</strong> <code>/campaign/${campaign.slug}</code></p>

View File

@ -101,6 +101,7 @@ router.get('/admin/campaigns/:id', requireAdmin, rateLimiter.general, campaignsC
router.post( router.post(
'/admin/campaigns', '/admin/campaigns',
requireAdmin, requireAdmin,
campaignsController.upload.single('cover_photo'),
rateLimiter.general, rateLimiter.general,
[ [
body('title').notEmpty().withMessage('Campaign title is required'), body('title').notEmpty().withMessage('Campaign title is required'),
@ -110,7 +111,7 @@ router.post(
handleValidationErrors, handleValidationErrors,
campaignsController.createCampaign campaignsController.createCampaign
); );
router.put('/admin/campaigns/:id', requireAdmin, rateLimiter.general, campaignsController.updateCampaign); router.put('/admin/campaigns/:id', requireAdmin, campaignsController.upload.single('cover_photo'), rateLimiter.general, campaignsController.updateCampaign);
router.delete('/admin/campaigns/:id', requireAdmin, rateLimiter.general, campaignsController.deleteCampaign); router.delete('/admin/campaigns/:id', requireAdmin, rateLimiter.general, campaignsController.deleteCampaign);
router.get('/admin/campaigns/:id/analytics', requireAdmin, rateLimiter.general, campaignsController.getCampaignAnalytics); router.get('/admin/campaigns/:id/analytics', requireAdmin, rateLimiter.general, campaignsController.getCampaignAnalytics);
@ -119,6 +120,7 @@ router.get('/campaigns', requireAuth, rateLimiter.general, campaignsController.g
router.post( router.post(
'/campaigns', '/campaigns',
requireNonTemp, requireNonTemp,
campaignsController.upload.single('cover_photo'),
rateLimiter.general, rateLimiter.general,
[ [
body('title').notEmpty().withMessage('Campaign title is required'), body('title').notEmpty().withMessage('Campaign title is required'),
@ -131,6 +133,7 @@ router.post(
router.put( router.put(
'/campaigns/:id', '/campaigns/:id',
requireNonTemp, requireNonTemp,
campaignsController.upload.single('cover_photo'),
rateLimiter.general, rateLimiter.general,
campaignsController.updateCampaign campaignsController.updateCampaign
); );

View File

@ -424,6 +424,7 @@ class NocoDBService {
'Email Subject': campaignData.email_subject, 'Email Subject': campaignData.email_subject,
'Email Body': campaignData.email_body, 'Email Body': campaignData.email_body,
'Call to Action': campaignData.call_to_action, 'Call to Action': campaignData.call_to_action,
'Cover Photo': campaignData.cover_photo,
'Status': campaignData.status, 'Status': campaignData.status,
'Allow SMTP Email': campaignData.allow_smtp_email, 'Allow SMTP Email': campaignData.allow_smtp_email,
'Allow Mailto Link': campaignData.allow_mailto_link, 'Allow Mailto Link': campaignData.allow_mailto_link,
@ -455,6 +456,7 @@ class NocoDBService {
if (updates.email_subject !== undefined) mappedUpdates['Email Subject'] = updates.email_subject; if (updates.email_subject !== undefined) mappedUpdates['Email Subject'] = updates.email_subject;
if (updates.email_body !== undefined) mappedUpdates['Email Body'] = updates.email_body; if (updates.email_body !== undefined) mappedUpdates['Email Body'] = updates.email_body;
if (updates.call_to_action !== undefined) mappedUpdates['Call to Action'] = updates.call_to_action; if (updates.call_to_action !== undefined) mappedUpdates['Call to Action'] = updates.call_to_action;
if (updates.cover_photo !== undefined) mappedUpdates['Cover Photo'] = updates.cover_photo;
if (updates.status !== undefined) mappedUpdates['Status'] = updates.status; if (updates.status !== undefined) mappedUpdates['Status'] = updates.status;
if (updates.allow_smtp_email !== undefined) mappedUpdates['Allow SMTP Email'] = updates.allow_smtp_email; if (updates.allow_smtp_email !== undefined) mappedUpdates['Allow SMTP Email'] = updates.allow_smtp_email;
if (updates.allow_mailto_link !== undefined) mappedUpdates['Allow Mailto Link'] = updates.allow_mailto_link; if (updates.allow_mailto_link !== undefined) mappedUpdates['Allow Mailto Link'] = updates.allow_mailto_link;

View File

@ -2,332 +2,527 @@
This document explains the purpose and functionality of each file in the BNKops Influence Campaign Tool. This document explains the purpose and functionality of each file in the BNKops Influence Campaign Tool.
## Overview
The BNKops Influence Campaign Tool is a comprehensive political engagement platform that allows users to create and participate in advocacy campaigns, contact their elected representatives, and track campaign analytics. The application features campaign management, user authentication, representative lookup via the Represent API, email composition and sending, and detailed analytics tracking.
## Authentication System ## Authentication System
The application includes a complete authentication system for admin panel access, following the same patterns used in the map application. Authentication is implemented using NocoDB as the user database and express-session for session management. The application includes a complete authentication system supporting both admin and regular user access. Authentication is implemented using NocoDB as the user database, bcryptjs for password hashing, and express-session for session management. The system supports temporary users with expiration dates for campaign-specific access.
## Cover Photo Feature
Campaign cover photos enhance visual engagement and branding for advocacy campaigns. This feature allows administrators and authorized users to upload custom images that display as hero banners on campaign landing pages.
**Implementation Details:**
- **Backend:** Multer middleware handles file uploads with validation (JPEG/PNG/GIF/WebP, max 5MB)
- **Storage:** Files stored in `/app/public/uploads/` with unique timestamp-based filenames
- **Database:** NocoDB `cover_photo` field stores filename references in campaigns table
- **Frontend:** Dynamic background-image rendering with responsive CSS and overlay effects
- **API Routes:** FormData multipart/form-data support in create/update campaign endpoints
- **Security:** File type validation, size limits, and sanitized filenames prevent malicious uploads
- **Fallback:** Gradient background displays when no cover photo is uploaded
**User Experience:**
- Admin interface includes file input with current photo preview
- Campaign pages display cover photos as full-width hero backgrounds with semi-transparent overlays
- Mobile-responsive scaling ensures optimal display across all devices
- Optional feature - campaigns function normally without cover photos
## Root Directory Files ## Root Directory Files
### Configuration Files ### Configuration Files
- **`.env`** - Environment variables (database URLs, SMTP config, API keys, email testing config) - **`.env`** - Environment configuration containing NocoDB API credentials, SMTP settings, session secrets, API tokens, and table URLs for database integration
- **`.env.development`** - Development environment configuration with MailHog SMTP settings - **`.env.backup.20250930_105913`** - Automated backup of environment configuration from September 30, 2025 at 10:59:13
- **`.env.example`** - Template for environment configuration - **`.env.backup.20250930_163827`** - Automated backup of environment configuration from September 30, 2025 at 16:38:27
- **`docker-compose.yml`** - Docker container orchestration with MailHog for email testing - **`example.env`** - Template for environment configuration with placeholder values and documentation
- **`Dockerfile`** - Container definition for the Node.js application - **`docker-compose.yml`** - Docker container orchestration with MailHog for email testing in development mode
- **`package.json`** - Node.js dependencies and scripts - **`fix-campaigns-table.sh`** - Database repair script for fixing campaign table structure and data integrity issues
- **`package-lock.json`** - Locked dependency versions for reproducible builds
### Documentation ### Documentation
- **`README.MD`** - Main project documentation with setup and usage instructions - **`API Reference Represent Elected Officials and Electoral Districts API for Canada.md`** - Complete API documentation for the Represent OpenNorth API used for representative lookup
- **`files-explainer.md`** - This file, explaining the project structure - **`README.MD`** - Main project documentation with features overview, setup instructions, and usage guidelines
- **`instruct.md`** - Implementation instructions and development notes - **`files-explainer.md`** - This file, providing comprehensive documentation of the project structure and file purposes
- **`instruct.md`** - Development guidelines, implementation notes, and contribution instructions
- **`influence-campaign-setup.md`** - Detailed setup guide for deploying and configuring the influence campaign tool
## Application Structure (`app/`) ## Application Structure (`app/`)
### Entry Point ### Entry Point
- **`server.js`** - Express.js application entry point, middleware setup, route mounting - **`server.js`** - Express.js application entry point with comprehensive middleware setup, route mounting, security configuration, session management, and static file serving. Includes protected routes for admin and user areas, error handling, and environment-specific configurations
- **`Dockerfile`** - Container definition for the Node.js application with multi-stage build, security optimizations, and production configurations
- **`package.json`** - Node.js project manifest defining dependencies including Express.js, authentication libraries (bcryptjs, express-session), email services (nodemailer), API clients (axios), security middleware (helmet, cors), validation libraries, and development tools
### Controllers (`app/controllers/`) ### Controllers (`app/controllers/`)
Business logic layer that handles HTTP requests and responses: Business logic layer that handles HTTP requests and responses:
- **`authController.js`** - Authentication controller for admin login/logout - **`authController.js`** - Authentication controller managing user login/logout operations
- `login()` - Handles admin login with email/password validation - `login()` - Handles user authentication with email/password validation, bcryptjs password verification, and session creation
- `logout()` - Destroys user session and logs out - `logout()` - Destroys user sessions and clears authentication state
- `checkSession()` - Verifies current authentication status - `checkSession()` - Verifies current authentication status and returns user information
- Integrates with NocoDB users table for credential verification - `register()` - User registration with password hashing and account creation
- Updates last login timestamps and manages session state - Integrates with NocoDB users table for credential storage and verification
- Updates last login timestamps and manages session persistence
- **`representatives.js`** - Core functionality for postal code lookups - **`campaigns.js`** - Core campaign management functionality with comprehensive CRUD operations
- `getByPostalCode()` - Main endpoint for finding representatives - `getAllCampaigns()` - Retrieves campaigns with filtering, pagination, and user permissions
- `refreshPostalCode()` - Force refresh of cached data - `createCampaign()` - Creates new campaigns with validation, slug generation, and cover photo upload handling using multer
- `testConnection()` - Health check for Represent API - `updateCampaign()` - Updates campaign details, status management, and cover photo processing
- Handles caching logic and fallback to API when cache fails - `deleteCampaign()` - Removes campaigns with data cleanup and permission checks
- `getCampaignBySlug()` - Public campaign access for landing pages and participation
- `participateInCampaign()` - Handles user participation, representative lookup, and email sending
- `getCampaignAnalytics()` - Generates campaign performance metrics and participation statistics
- Cover photo feature: Supports image uploads (JPEG, PNG, GIF, WebP) up to 5MB, stores files in `/public/uploads/` with unique filenames, and saves filename references to NocoDB
- Advanced features: attachment URL normalization, representative caching, email templating, and analytics tracking
- File upload middleware configuration using multer with diskStorage, file size limits, and image type validation
- **`emails.js`** - Email composition and sending functionality - **`representatives.js`** - Representative lookup and caching functionality
- `sendEmail()` - Process and send emails to representatives with test mode support - `getByPostalCode()` - Main endpoint for finding representatives by postal code using Represent API
- `previewEmail()` - Generate email preview without sending for testing purposes - `refreshPostalCode()` - Force refresh of cached representative data
- `sendTestEmail()` - Send test emails to configured test recipient - `testConnection()` - Health check for Represent API connectivity
- `getEmailLogs()` - Retrieve email history with filtering and pagination - `searchRepresentatives()` - Search functionality for finding specific representatives
- Handles intelligent caching logic, fallback to API when cache fails, and data normalization
- **`emails.js`** - Email composition and delivery management
- `sendEmail()` - Process and send emails to representatives with template support and test mode
- `previewEmail()` - Generate email previews without sending for content review
- `sendTestEmail()` - Send test emails to configured recipients for system verification
- `sendCampaignEmail()` - Campaign-specific email sending with participant tracking
- `getEmailLogs()` - Retrieve email history with filtering, pagination, and analytics
- `testSMTPConnection()` - Test SMTP server connectivity for diagnostics - `testSMTPConnection()` - Test SMTP server connectivity for diagnostics
- Integrates with SMTP service and logs to database with test mode tracking - Integrates with email templates, logs all activity to database, and supports both HTML and text formats
- **`usersController.js`** - User management and administration
- `getAll()` - Retrieve all users with role filtering and pagination
- `create()` - Create new users with password generation and email notification
- `update()` - Update user details, roles, and account settings
- `delete()` - Remove users with data cleanup and session invalidation
- `sendLoginDetails()` - Email login credentials to users with secure password handling
- `broadcastEmail()` - Send mass emails to user groups with template support
- `getExpiredUsers()` - Identify and manage temporary user account expiration
- Features temporary user support, role-based access control, and comprehensive user lifecycle management
### Routes (`app/routes/`) ### Routes (`app/routes/`)
API endpoint definitions and request validation: API endpoint definitions and request validation:
- **`auth.js`** - Authentication API routes - **`auth.js`** - Authentication API routes with comprehensive security
- POST `/api/auth/login` - Admin login endpoint - POST `/api/auth/login` - User login endpoint with rate limiting and validation
- POST `/api/auth/logout` - Admin logout endpoint - POST `/api/auth/logout` - User logout endpoint with session cleanup
- GET `/api/auth/session` - Session verification endpoint - GET `/api/auth/session` - Session verification and user information endpoint
- Handles authentication requests with proper validation and error handling - POST `/api/auth/register` - User registration endpoint with password requirements
- Handles authentication requests with proper validation, error handling, and security measures
- **`api.js`** - Main API routes with validation middleware - **`api.js`** - Main API routes with extensive validation middleware
- Representatives endpoints with postal code validation - Campaign management endpoints: CRUD operations for campaigns, participation, analytics
- Email endpoints with input sanitization and test mode support - Representative endpoints with postal code validation and caching
- Email testing endpoints: `/api/emails/preview`, `/api/emails/test`, `/api/emails/logs` - Email endpoints with input sanitization, template support, and test mode
- SMTP testing endpoint: `/api/test-smtp` for connection diagnostics - Email management: `/api/emails/preview`, `/api/emails/send`, `/api/emails/logs`, `/api/emails/test`
- Health check and testing endpoints - File upload endpoints for campaign attachments and media
- Rate limiting and error handling middleware - Health check and system testing endpoints
- Comprehensive rate limiting, input validation, and error handling middleware
- **`users.js`** - User management API routes (admin access)
- GET `/api/users` - List all users with filtering and pagination
- POST `/api/users` - Create new users with role assignment
- PUT `/api/users/:id` - Update user details and permissions
- DELETE `/api/users/:id` - Remove users with data cleanup
- POST `/api/users/broadcast` - Send broadcast emails to user groups
- Includes admin authorization checks and comprehensive user lifecycle management
- **`dashboard.js`** - User dashboard and analytics API routes
- GET `/api/dashboard/stats` - User-specific dashboard statistics
- GET `/api/dashboard/campaigns` - User's campaign participation history
- GET `/api/dashboard/analytics` - Personal analytics and activity tracking
- Provides personalized data views with role-based access control
### Services (`app/services/`) ### Services (`app/services/`)
External system integrations and data access layer: External system integrations and data access layer:
- **`nocodb.js`** - NocoDB database integration - **`nocodb.js`** - Comprehensive NocoDB database integration service
- User management methods: `getUserByEmail()`, `createUser()`, `updateUser()`, `getAllUsers()` - User management: `getUserByEmail()`, `createUser()`, `updateUser()`, `deleteUser()`, `getAllUsers()`
/* Lines 76-80 omitted */ - Campaign operations: `createCampaign()`, `updateCampaign()`, `getCampaigns()`, `deleteCampaign()`
- Caching logic with automatic retry mechanisms - Representative caching: `getRepresentativesByPostalCode()`, `storeRepresentatives()`, `cacheRepresentatives()`
- Email logging: `logEmail()`, `getEmailLogs()`, `updateEmailStatus()`
- Generic CRUD operations: `create()`, `read()`, `update()`, `delete()`, `getAll()` with filtering and pagination
- Advanced features: automatic table URL resolution, attachment handling, file upload support, and connection pooling
- Robust error handling with automatic retry mechanisms and fallback strategies
- **`represent-api.js`** - Represent OpenNorth API integration - **`represent-api.js`** - Represent OpenNorth API integration for Canadian electoral data
- Postal code lookup against Canadian electoral data - `getRepresentativesByPostalCode()` - Fetch representatives for postal codes with multiple data sources
/* Lines 84-86 omitted */ - `getElectoralDistricts()` - Retrieve electoral district information and boundaries
- Support for both concordance and centroid representative data - `searchRepresentatives()` - Search functionality for finding specific representatives
- `validatePostalCode()` - Canadian postal code format validation and normalization
- Support for both concordance and centroid representative data with intelligent merging
- Comprehensive error handling, rate limiting, and response caching
- **`emailTemplates.js`** - Email template service for managing HTML/text email templates - **`emailTemplates.js`** - Advanced email template management service
- Template loading and caching system with support for conditional blocks - Template loading and caching system with support for conditional blocks and dynamic content
- Variable replacement and template processing for dynamic content - Variable replacement and template processing for personalized emails
- Template rendering for both HTML and text formats with fallback support - Multi-format support: HTML and text templates with automatic fallback
- Available templates: representative-contact, campaign-email, test-email - Template rendering with context-aware variable substitution
- Available templates: representative-contact, campaign-email, test-email, login-details, user-broadcast
- Template validation, error handling, and performance optimization through caching
- **`email.js`** - SMTP email service with comprehensive testing support - **`email.js`** - Comprehensive SMTP email service with extensive features
- Template-based email composition using emailTemplates service - Template-based email composition using emailTemplates service
- Legacy sendEmail method for backward compatibility - Multiple sending methods: `sendRepresentativeEmail()`, `sendCampaignEmail()`, `sendTestEmail()`, `sendLoginDetails()`
- New templated methods: sendRepresentativeEmail(), sendCampaignEmail(), sendTestEmail() - Email preview functionality with full template rendering without sending
- Email preview functionality with template support - SMTP connection testing and diagnostics for troubleshooting
- SMTP connection testing for diagnostics and troubleshooting - Support for multiple SMTP providers with automatic failover
- Email logging, delivery tracking, and bounce handling
- Development mode with MailHog integration for local testing
### Utilities (`app/utils/`) ### Utilities (`app/utils/`)
Helper functions and shared utilities: Helper functions and shared utilities:
- **`validation.js`** - Input validation and sanitization - **`validators.js`** - Comprehensive input validation and sanitization
- Postal code format validation (Alberta T-prefix) - Postal code format validation for Canadian addresses (including Alberta T-prefix)
- Email address validation - Email address validation with domain checking and format verification
- HTML content sanitization for security - Campaign slug generation and validation for URL-safe identifiers
- HTML content sanitization for security and XSS prevention
- Form input validation with detailed error messaging
- Data type validation and conversion utilities
- **`rate-limiter.js`** - API rate limiting configuration - **`rate-limiter.js`** - Advanced API rate limiting configuration
- Different limits for various endpoint types - Endpoint-specific rate limiting with different limits for various operations
- IP-based rate limiting with sliding windows - IP-based rate limiting with sliding windows and burst protection
- Configurable limits via environment variables - User-based rate limiting for authenticated requests
- Configurable limits via environment variables for different deployment environments
- Integration with express-rate-limit for production-ready protection
- **`helpers.js`** - General utility functions and data processing helpers
- User data sanitization for removing sensitive information from API responses
- ID extraction and normalization utilities for database operations
- Date and time formatting functions for consistent display
- String manipulation utilities for text processing and cleanup
- Data transformation helpers for API response formatting
- Error handling utilities and logging helpers
### Middleware (`app/middleware/`) ### Middleware (`app/middleware/`)
Express.js middleware functions: Express.js middleware functions for request processing:
- **`auth.js`** - Authentication middleware for protecting routes - **`auth.js`** - Comprehensive authentication middleware for route protection
- `requireAuth()` - Ensures user is logged in - `requireAuth()` - Ensures user is logged in with valid session
- `requireAdmin()` - Ensures user is logged in and has admin privileges - `requireAdmin()` - Ensures user has administrative privileges
- Redirects unauthenticated requests to login page - `requireOwnership()` - Ensures user can only access their own resources
- Sets up `req.user` object for authenticated requests - `checkTempUser()` - Handles temporary user restrictions and expiration
- Standardized error response formatting - Redirects unauthenticated requests to appropriate login pages
- Error logging and classification - Sets up `req.user` object for authenticated requests with full user context
- Production vs development error detail levels - Session validation and renewal functionality
- Standardized error response formatting with consistent structure
- Comprehensive error logging and security event tracking
- Development vs production error detail levels for appropriate information disclosure
### Email Templates (`app/templates/email/`) ### Email Templates (`app/templates/email/`)
Professional HTML and text email templates with variable substitution: Professional HTML and text email templates with variable substitution:
- **`representative-contact.html/.txt`** - Template for direct constituent communications - **`representative-contact.html/.txt`** - Template for direct constituent communications to representatives
- Variables: MESSAGE, SENDER_NAME, SENDER_EMAIL, POSTAL_CODE, APP_NAME, TIMESTAMP - Variables: MESSAGE, SENDER_NAME, SENDER_EMAIL, POSTAL_CODE, APP_NAME, TIMESTAMP
- Professional styling with constituent information display and platform branding - Professional styling with constituent information display and platform branding
- Responsive design for various email clients and devices
- **`campaign-email.html/.txt`** - Template for organized campaign emails - **`campaign-email.html/.txt`** - Template for organized campaign participation emails
- Variables: MESSAGE, USER_NAME, USER_EMAIL, POSTAL_CODE, CAMPAIGN_TITLE, RECIPIENT_NAME, RECIPIENT_LEVEL - Variables: MESSAGE, USER_NAME, USER_EMAIL, POSTAL_CODE, CAMPAIGN_TITLE, RECIPIENT_NAME, RECIPIENT_LEVEL
- Campaign-specific styling with participant information and campaign branding - Campaign-specific styling with participant information and campaign branding
- Conditional recipient information display - Conditional recipient information display and campaign context
- Call-to-action buttons and engagement tracking
- **`test-email.html/.txt`** - Template for email system testing - **`test-email.html/.txt`** - Template for email system testing and diagnostics
- Variables: MESSAGE, APP_NAME, TIMESTAMP - Variables: MESSAGE, APP_NAME, TIMESTAMP
- Warning styling and test indicators for system verification emails - Warning styling and test indicators for system verification emails
- Development-friendly formatting for debugging email delivery
- **`login-details.html/.txt`** - Template for sending user credentials and account information
- Variables: USER_NAME, EMAIL, PASSWORD, LOGIN_URL, APP_NAME
- Secure credential display with clear login instructions
- Security warnings and best practices for password management
- **`user-broadcast.html/.txt`** - Template for administrator mass communications
- Variables: MESSAGE, RECIPIENT_NAME, SENDER_NAME, APP_NAME, TIMESTAMP
- Professional broadcast styling for announcements and updates
- Rich text support for formatted content and multimedia
### Frontend Assets (`app/public/`) ### Frontend Assets (`app/public/`)
#### HTML #### HTML Pages
- **`index.html`** - Main application interface - **`index.html`** - Main application landing page and campaign discovery interface
- Campaign listing with search and filtering capabilities
- Featured campaigns display with engaging visuals
- User registration and login integration
- Responsive design with mobile-first approach
- **`login.html`** - Admin login page - **`login.html`** - User authentication page with comprehensive login functionality
- Clean, professional login interface for admin access - Clean, professional login interface for user access
- Email/password form with client-side validation - Email/password form with client-side validation and security features
- Integration with authentication API - Integration with authentication API and session management
- Automatic redirect to admin panel on successful login - Automatic redirect to dashboard on successful login
- Session persistence and auto-login detection - Session persistence and auto-login detection for returning users
- Password reset functionality and security messaging
- **`admin.html`** - Campaign management admin panel (protected) - **`admin.html`** - Administrative panel for campaign and user management (protected)
- Requires admin authentication to access - Requires admin authentication with role verification
- Includes authentication checks and user info display - Campaign creation and management interface with rich text editing and cover photo uploads
- Logout functionality integrated into interface - Cover photo upload: File input with image validation (jpeg/jpg/png/gif/webp, max 5MB)
- Postal code input form with validation - Current cover photo preview in edit form with thumbnail display
- Representatives display sections - User management with role assignment and account lifecycle controls
- Email composition modal - Email composition tools for broadcast communications
- Responsive design with accessibility features - System analytics dashboard with campaign performance metrics
- Representative data management and caching controls
- File upload interface for campaign assets and attachments using FormData for multipart uploads
- **`dashboard.html`** - User dashboard for campaign participation and personal analytics
- Personalized campaign recommendations and participation history
- User profile management with account settings
- Campaign creation tools for non-admin users (if permitted)
- Email history and communication tracking
- Personal analytics and engagement metrics
- Responsive interface optimized for various screen sizes
- **`campaign.html`** - Dynamic campaign landing pages for public participation
- Campaign-specific branding and visual design with cover photo hero sections
- Cover photo display: Dynamically applies uploaded cover images as hero backgrounds with overlay effects
- Responsive cover photo sizing with background-size cover and center positioning
- Fallback gradient background for campaigns without cover photos
- Representative lookup integration with postal code input
- Email composition interface for campaign participation
- Social sharing functionality and engagement tracking
- Mobile-optimized interface for accessibility
- Progress tracking and participation confirmation
- **`email-test.html`** - Comprehensive email testing interface (admin-only) - **`email-test.html`** - Comprehensive email testing interface (admin-only)
- Protected admin interface for email testing and diagnostics - Protected admin interface for email system testing and diagnostics
- Quick test email functionality with one-click testing - Quick test email functionality with predefined templates
- Email preview system to see emails before sending - Email preview system with real-time rendering for content review
- Email composition form with real-time preview - Email composition form with WYSIWYG editing capabilities
- Email logs viewer with filtering by test/live mode - Email delivery logs with filtering and search functionality
- SMTP connection testing and diagnostics - SMTP connection testing and configuration validation
- Current configuration display showing test mode status - Test mode configuration and environment switching
- Real-time feedback and error handling for all operations
- **`terms.html`** - Terms of service and privacy policy page
- Legal documentation for user agreement and data handling
- Privacy policy with data collection and usage transparency
- User rights and responsibilities outlined clearly
- Contact information for legal and privacy inquiries
#### Stylesheets (`app/public/css/`) #### Stylesheets (`app/public/css/`)
- **`styles.css`** - Complete application styling - **`styles.css`** - Comprehensive application styling with modern design principles
- Responsive grid layouts for representative cards - Responsive grid layouts for campaign and representative cards
- Photo display with fallback styling - Professional form styling with validation states and user feedback
- Modal and form styling - Modal dialog systems for email composition and content editing
- Mobile-first responsive design - Mobile-first responsive design with progressive enhancement
- Loading states and error message styling - Loading states, progress indicators, and skeleton screens
- Error message styling and user notification systems
- Campaign-specific theming and branding customization
- Accessibility features including focus management and color contrast
- Print-friendly styles for campaign materials and representative information
#### JavaScript (`app/public/js/`) #### JavaScript (`app/public/js/`)
- **`main.js`** - Application initialization and utilities - **`main.js`** - Core application initialization and shared utilities
- Global error handling and message display system - Global error handling and user message display system
- Utility functions (postal code formatting, sanitization) - Utility functions including postal code formatting and data sanitization
- Mobile detection and responsive behavior - Mobile device detection and responsive behavior management
- Application-wide constants and configuration management
- Event delegation and global event handling
- **`api-client.js`** - Frontend API communication - **`api-client.js`** - Frontend API communication layer
- HTTP client with error handling - HTTP client with comprehensive error handling and retry logic
- API endpoint wrappers for all backend services - API endpoint wrappers for all backend services with consistent interfaces
- Request/response processing and error propagation - Request/response processing with automatic data transformation
- Authentication token management and automatic session renewal
- **`postal-lookup.js`** - Postal code search functionality - Error propagation and user-friendly error message generation
- Form handling and input validation - FormData upload methods: `postFormData()` and `putFormData()` for file uploads
- API integration for representative lookup - Automatic content-type handling for multipart/form-data requests
- Loading states and error display
- Results processing and display coordination
- **`representatives-display.js`** - Representative data presentation
- Dynamic HTML generation for representative cards
- Photo display with fallback to initials
- Government level categorization and sorting
- Contact information formatting and display
- **`auth.js`** - Client-side authentication management - **`auth.js`** - Client-side authentication management
- `AuthManager` class for handling login/logout operations - `AuthManager` class for handling complete authentication lifecycle
- Session state management and persistence - Login/logout operations with secure session management
- UI updates based on authentication status - Session state persistence across browser sessions
- Auto-redirect for protected pages requiring authentication - UI updates based on authentication status and user roles
- Integration with login forms and logout buttons - Automatic redirect for protected pages requiring authentication
- Integration with login forms, logout buttons, and protected routes
- Role-based UI element visibility and feature access control
- **`email-composer.js`** - Email composition interface - **`campaign.js`** - Campaign participation and interaction management
- Modal-based email composition form - `CampaignPage` class managing campaign landing page functionality
- Pre-population of recipient data - Dynamic campaign loading with slug-based routing
- Form validation and submission handling - Cover photo rendering: Dynamically applies cover images as background with `.has-cover` class
- Success/error feedback to users - Responsive cover photo display with overlay effects and text shadowing for readability
- Fallback to gradient background when no cover photo exists
- User information collection and validation for campaign participation
- Representative lookup integration with postal code processing
- Email composition interface with campaign context and template integration
- Progress tracking through campaign participation workflow
- Social sharing and engagement tracking functionality
- **`dashboard.js`** - User dashboard and analytics interface
- `UserDashboard` class managing personalized user experience
- Campaign management interface for user-created campaigns
- Personal analytics display with charts and metrics
- Campaign participation history and tracking
- User profile management and account settings
- Campaign creation wizard for authorized users
- **`admin.js`** - Administrative interface management
- `AdminPanel` class for comprehensive system administration
- Campaign management with CRUD operations, advanced editing, and cover photo upload handling
- Cover photo management: FormData-based file uploads with postFormData/putFormData methods
- Current cover photo preview with thumbnail display in edit forms
- File validation client-side before upload (image types, size limits)
- User management with role assignment and account lifecycle
- System analytics and performance monitoring
- Email broadcast functionality with template selection
- File upload management for campaign assets with multipart/form-data encoding
- **`postal-lookup.js`** - Postal code search and representative discovery
- Form handling and input validation for Canadian postal codes
- API integration for representative lookup with caching
- Loading states and error display for user feedback
- Results processing and display coordination with visual feedback
- Representative data presentation with government level organization
- **`representatives-display.js`** - Representative data presentation and interaction
- Dynamic HTML generation for representative cards with rich information display
- Photo display with intelligent fallback to generated initials
- Government level categorization and hierarchical sorting
- Contact information formatting with clickable actions
- Representative profile modal displays with detailed information
- **`representatives-map.js`** - Interactive map integration for representative boundaries
- Map visualization of electoral districts and representative coverage
- Interactive boundary display with postal code correlation
- Representative location plotting with contextual information
- Geographic search functionality and boundary visualization
- **`email-composer.js`** - Advanced email composition interface
- Modal-based email composition with rich text editing capabilities
- Template selection and dynamic content generation
- Recipient data pre-population with representative information
- Form validation and submission handling with progress tracking
- Email preview functionality and template rendering
- Attachment handling and file upload integration
- **`email-testing.js`** - Comprehensive email testing interface management - **`email-testing.js`** - Comprehensive email testing interface management
- `EmailTesting` class managing all testing functionality - `EmailTesting` class managing all diagnostic functionality
- Quick test email sending with default content - Quick test email sending with predefined content and templates
- SMTP connection testing and diagnostics - SMTP connection testing and configuration validation
- Email preview generation with real-time rendering - Email preview generation with real-time template rendering
- Test email sending with form validation - Test email composition with form validation and sending
- Email logs management with filtering and pagination - Email delivery logs management with filtering and pagination
- Configuration status display and management - Configuration status display and troubleshooting tools
- Real-time UI updates and error handling - Real-time UI updates and comprehensive error handling
- Integration with authentication system for admin protection
- **`login.js`** - Login page functionality and user experience
- Login form handling with client-side validation
- Integration with authentication API and session management
- Password visibility toggles and security features
- Auto-redirect logic for authenticated users
- Error display and user feedback for authentication issues
## Scripts Directory (`scripts/`) ## Scripts Directory (`scripts/`)
- **`build-nocodb.sh`** - Database setup automation - **`build-nocodb.sh`** - Comprehensive database setup automation with migration capabilities
- Now includes users table creation for authentication system - Creates complete NocoDB base with all required tables for the influence campaign system
- Creates influence_users table with email, name, password, admin flag, and last_login fields - Table creation includes: users, campaigns, representatives, emails, postal_codes, campaign_emails
- Updates .env file with NOCODB_TABLE_USERS configuration - Advanced data migration support with `--migrate-data` flag for preserving existing data
- Creates NocoDB base and tables using v2 API - Interactive source base and table selection for migration operations
- Configures table schemas for representatives, emails, postal codes - Preserves original data while creating new base with improved schema
- Handles authentication and error recovery - Automatic .env file updates with new base ID and table URLs
- Updates environment with new base ID - Comprehensive dependency checking (requires jq, curl, and valid NocoDB credentials)
- Robust error handling with rollback capabilities and user feedback
- User authentication table with email, name, password hash, admin flag, and activity tracking
- Campaign management tables with full metadata, attachments, and analytics support
## Data Flow Architecture ## Data Flow Architecture
### Request Processing Flow ### Campaign Participation Flow
1. **User Input** → Postal code entered in frontend form 1. **Campaign Discovery** → Users browse campaigns on main page or access via direct links
2. **Validation** → Client-side and server-side validation 2. **Campaign Access** → Dynamic campaign pages loaded based on URL slug with branding
3. **Cache Check** → NocoDB queried for existing data 3. **User Information** → Postal code collection and user details for representative lookup
4. **API Fallback** → Represent API called if cache miss or NocoDB down 4. **Representative Lookup** → API call to Represent service with intelligent caching
5. **Data Processing** → Representative data normalized and processed 5. **Email Composition** → Template-based email composition with campaign context
6. **Caching** → Results stored in NocoDB for future requests 6. **Email Delivery** → SMTP sending with comprehensive logging and tracking
7. **Response** → Formatted data returned to frontend 7. **Analytics Tracking** → Participation metrics and campaign performance data collection
8. **Display** → Representatives rendered with photos and contact info
### Email Flow ### Campaign Management Flow
1. **Composition** → User fills email form in modal 1. **Campaign Creation** → Admin/user creates campaign with details, messaging, and targeting
2. **Validation** → Input sanitization and validation 2. **Content Management** → Rich text editing, file uploads, and visual customization
3. **Processing** → Email formatted and prepared for sending 3. **Publication** → Campaign status management (draft, active, paused, archived)
4. **SMTP Delivery** → Email sent via configured SMTP service 4. **Monitoring** → Real-time analytics, participation tracking, and performance metrics
5. **Logging** → Email attempt logged to database 5. **Communication** → Broadcast messaging to participants and stakeholder updates
6. **Confirmation** → User notified of success/failure
### Error Handling Strategy ### Authentication and Session Management
- **Graceful Degradation** → App works even if NocoDB is down 1. **User Registration** → Account creation with role assignment and email verification
- **User Feedback** → Clear error messages and recovery suggestions 2. **Login Process** → Secure authentication with bcryptjs password verification
- **Logging** → Comprehensive error logging for debugging 3. **Session Management** → Express-session with secure cookie configuration and expiration
- **Fallbacks** → Multiple data sources and retry mechanisms 4. **Role-Based Access** → Middleware-enforced permissions for admin, user, and temporary access
5. **Account Lifecycle** → User management including temporary accounts with expiration dates
### Email System Architecture
1. **Template Selection** → Dynamic template selection based on email type and context
2. **Content Generation** → Variable substitution and personalization with user/campaign data
3. **SMTP Delivery** → Configurable SMTP providers with failover and retry logic
4. **Delivery Tracking** → Comprehensive logging with status tracking and analytics
5. **Test Mode** → Development-friendly email redirection and preview capabilities
### Representative Data Management
1. **Postal Code Input** → User provides postal code with client-side validation
2. **Cache Check** → NocoDB queried for existing representative data with TTL management
3. **API Fallback** → Represent API integration when cache miss or data refresh needed
4. **Data Processing** → Representative data normalization and government level organization
5. **Caching Strategy** → Intelligent caching with automatic invalidation and refresh policies
6. **Display Rendering** → Rich representative cards with photos, contact information, and actions
## Key Integration Points ## Key Integration Points
### Authentication System ### Advanced Authentication System
- **Admin Login** → Email/password authentication for admin panel access - **Multi-Level Access** → Support for admin, regular users, and temporary campaign participants
- **Session Management** → Express-session with secure cookie configuration - **Session Security** → HttpOnly cookies, CSRF protection, and automatic session expiration
- **Route Protection** → Middleware-based protection for admin endpoints and pages - **Password Management** → Bcryptjs hashing with secure password generation and recovery
- **User Management** → NocoDB-based user storage with admin role support - **Role-Based UI** → Dynamic interface adaptation based on user permissions and account type
- **Security** → Password validation, session expiration, and automatic logout - **Account Expiration** → Temporary user support with automatic cleanup and lifecycle management
### NocoDB Integration ### Comprehensive Campaign Management
- **Authentication** → Token-based API authentication - **Rich Content Creation** → WYSIWYG editing, file uploads, and multimedia attachment support
- **User Storage** → Users table with email, password, admin flag, and login tracking - **URL-Safe Routing** → Automatic slug generation with validation and conflict resolution
- **Table Management** → Dynamic table ID resolution - **Status Workflow** → Campaign lifecycle management from draft through archive
- **Error Recovery** → Automatic fallback to API when database unavailable - **Analytics Integration** → Built-in tracking for participation, engagement, and conversion metrics
- **Performance** → Efficient caching with intelligent cache invalidation - **Social Features** → Sharing capabilities and viral growth mechanisms
### Represent API Integration ### Enhanced NocoDB Integration
- **Data Sources** → Both concordance and centroid representative data - **Advanced Schema Management** → Dynamic table creation with complex relationship support
- **Rate Limiting** → Respectful API usage with built-in rate limiting - **File Upload Handling** → Attachment management with URL normalization and CDN support
- **Error Handling** → Robust error handling with user-friendly messages - **Bulk Operations** → Efficient data migration and batch processing capabilities
- **Data Processing** → Normalization of API responses for consistent display - **Connection Resilience** → Automatic retry logic and graceful degradation strategies
- **Performance Optimization** → Query optimization, caching layers, and connection pooling
### SMTP Integration ### Sophisticated Email Infrastructure
- **Security** → Secure authentication and encrypted connections - **Multi-Template System** → Context-aware template selection with rich variable support
- **Reliability** → Error handling and delivery confirmation - **SMTP Flexibility** → Multiple provider support with automatic failover and load balancing
- **Logging** → Complete audit trail of email activity with test mode tracking - **Development Tools** → MailHog integration for local testing and email preview capabilities
- **Configuration** → Flexible SMTP provider support with development/production modes - **Delivery Assurance** → Comprehensive logging, bounce handling, and retry mechanisms
- **Testing** → Comprehensive test mode with email redirection and preview capabilities - **Personalization Engine** → Advanced variable substitution with conditional content blocks
### Email Testing System ### Representative Data Excellence
- **Test Mode** → Automatic email redirection to configured test recipient - **Multi-Source Integration** → Represent API with intelligent data merging and validation
- **Preview System** → Generate email previews without sending for content review - **Geographic Accuracy** → Postal code validation with boundary checking and error correction
- **SMTP Diagnostics** → Connection testing and troubleshooting tools - **Performance Caching** → Multi-level caching with intelligent invalidation strategies
- **Email Logging** → Complete audit trail with test/live mode classification - **Data Enrichment** → Photo fetching, contact validation, and information completeness scoring
- **Development Tools** → MailHog integration for local email catching and review - **Real-Time Updates** → Automatic data refresh with change detection and notification
- **Admin Interface** → Dedicated testing interface accessible only to authenticated admins
### Docker Configuration ### Security and Compliance Framework
- **Production Mode** → Standard application container with external SMTP - **Input Validation** → Comprehensive sanitization and validation across all user inputs
- **Development Mode** → Application + MailHog containers for local email testing - **Rate Limiting** → Advanced rate limiting with user-based and IP-based protection
- **Profile-based Deployment** → MailHog only runs in development profile - **CSRF Protection** → Cross-site request forgery prevention with token validation
- **Email Catching** → All development emails caught by MailHog web interface at port 8025 - **Content Security** → HTML sanitization and XSS prevention throughout the application
- **Environment Flexibility** → Easy switching between development and production SMTP settings - **Audit Logging** → Comprehensive activity logging for security monitoring and compliance
## Development Patterns ### Deployment and Operations
- **Docker Integration** → Production-ready containerization with multi-stage builds
- **Environment Management** → Flexible configuration with development/production modes
- **Monitoring Capabilities** → Built-in health checks and performance monitoring
- **Backup Automation** → Automated environment backups with restoration capabilities
- **Scaling Support** → Load balancer compatibility and horizontal scaling preparation
### Error Handling This architecture provides a robust, scalable, and secure solution for political engagement and advocacy campaign management, enabling Alberta residents and organizations to effectively connect with their elected representatives and coordinate grassroots political action.
- All async operations wrapped in try-catch blocks
- Consistent error response formatting across APIs
- User-friendly error messages with technical details logged
- Graceful degradation when external services unavailable
### Data Validation
- Input validation at both client and server levels
- Sanitization to prevent XSS and injection attacks
- Type checking and format validation throughout
- Clear validation error messages for users
### Performance Optimization
- Smart caching with configurable TTL
- Lazy loading of images and non-critical resources
- Efficient database queries with minimal data transfer
- Client-side debouncing for user inputs
### Security Measures
- **Authentication Protection** → Admin panel and API endpoints protected by login
- **Session Security** → HttpOnly cookies, secure session configuration, automatic expiration
- Rate limiting to prevent abuse
- Input sanitization and validation
- Secure SMTP configuration
- Environment variable protection of sensitive data
This architecture provides a robust, scalable, and maintainable solution for connecting Alberta residents with their elected representatives.

View File

@ -1010,6 +1010,11 @@ create_campaigns_table() {
"title": "Call to Action", "title": "Call to Action",
"uidt": "LongText" "uidt": "LongText"
}, },
{
"column_name": "cover_photo",
"title": "Cover Photo",
"uidt": "SingleLineText"
},
{ {
"column_name": "status", "column_name": "status",
"title": "Status", "title": "Status",