Updated campagin cover photos

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

View File

@@ -84,13 +84,55 @@
gap: 0.5rem;
}
.campaign-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.campaign-card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 1rem;
padding: 0;
background: white;
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 {
@@ -98,6 +140,7 @@
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
padding: 1.5rem;
}
.campaign-header h3 {
@@ -150,7 +193,7 @@
}
.btn {
padding: 0.75rem 1.5rem;
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
@@ -700,7 +743,7 @@
<p>Loading campaigns...</p>
</div>
<div id="campaigns-list" class="campaign-list">
<div id="campaigns-list" class="campaign-grid">
<!-- Campaigns will be loaded here -->
</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>
</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="form-group">
<div class="checkbox-group">
@@ -880,6 +929,13 @@ Sincerely,
<textarea id="edit-call-to-action" name="call_to_action" rows="3"></textarea>
</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">
<label>Campaign Settings</label>
<div class="checkbox-group">

View File

@@ -12,11 +12,40 @@
padding: 3rem 0;
text-align: center;
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 {
font-size: 2.5rem;
margin-bottom: 1rem;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.7);
}
.campaign-stats {

View File

@@ -85,10 +85,11 @@
.campaign-card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 1.5rem;
padding: 0;
background: white;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
transition: transform 0.2s, box-shadow 0.2s;
overflow: hidden;
}
.campaign-card:hover {
@@ -96,11 +97,42 @@
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 {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1rem;
padding: 1.5rem;
}
.campaign-header h3 {

View File

@@ -467,10 +467,19 @@ class AdminPanel {
listDiv.innerHTML = this.campaigns.map(campaign => `
<div class="campaign-card" data-campaign-id="${campaign.id}">
<div class="campaign-header">
<h3>${this.escapeHtml(campaign.title)}</h3>
<span class="status-badge status-${campaign.status}">${campaign.status}</span>
</div>
${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">
<h3>${this.escapeHtml(campaign.title)}</h3>
<span class="status-badge status-${campaign.status}">${campaign.status}</span>
</div>
`}
<div class="campaign-meta">
<p><strong>Slug:</strong> <code>/campaign/${campaign.slug}</code></p>
@@ -531,22 +540,34 @@ class AdminPanel {
e.preventDefault();
const formData = new FormData(e.target);
const campaignData = {
title: formData.get('title'),
description: formData.get('description'),
email_subject: formData.get('email_subject'),
email_body: formData.get('email_body'),
call_to_action: formData.get('call_to_action'),
status: formData.get('status'),
allow_smtp_email: formData.get('allow_smtp_email') === 'on',
allow_mailto_link: formData.get('allow_mailto_link') === 'on',
collect_user_info: formData.get('collect_user_info') === 'on',
show_email_count: formData.get('show_email_count') === 'on',
target_government_levels: Array.from(formData.getAll('target_government_levels'))
};
// Convert checkboxes to boolean values
const campaignFormData = new FormData();
campaignFormData.append('title', formData.get('title'));
campaignFormData.append('description', formData.get('description') || '');
campaignFormData.append('email_subject', formData.get('email_subject'));
campaignFormData.append('email_body', formData.get('email_body'));
campaignFormData.append('call_to_action', formData.get('call_to_action') || '');
campaignFormData.append('status', formData.get('status'));
campaignFormData.append('allow_smtp_email', formData.get('allow_smtp_email') === 'on');
campaignFormData.append('allow_mailto_link', formData.get('allow_mailto_link') === 'on');
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 {
const response = await window.apiClient.post('/admin/campaigns', campaignData);
const response = await window.apiClient.postFormData('/admin/campaigns', campaignFormData);
if (response.success) {
this.showMessage('Campaign created successfully!', 'success');
@@ -584,6 +605,19 @@ class AdminPanel {
// Status select
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
form.querySelector('[name="allow_smtp_email"]').checked = campaign.allow_smtp_email;
form.querySelector('[name="allow_mailto_link"]').checked = campaign.allow_mailto_link;
@@ -612,25 +646,34 @@ class AdminPanel {
if (!this.currentCampaign) return;
const formData = new FormData(e.target);
const updates = {
title: formData.get('title'),
description: formData.get('description'),
email_subject: formData.get('email_subject'),
email_body: formData.get('email_body'),
call_to_action: formData.get('call_to_action'),
status: formData.get('status'),
allow_smtp_email: formData.get('allow_smtp_email') === 'on',
allow_mailto_link: formData.get('allow_mailto_link') === 'on',
collect_user_info: formData.get('collect_user_info') === 'on',
show_email_count: formData.get('show_email_count') === 'on',
target_government_levels: Array.from(formData.getAll('target_government_levels'))
};
// Convert checkboxes to boolean values and build FormData for upload
const updateFormData = new FormData();
updateFormData.append('title', formData.get('title'));
updateFormData.append('description', formData.get('description') || '');
updateFormData.append('email_subject', formData.get('email_subject'));
updateFormData.append('email_body', formData.get('email_body'));
updateFormData.append('call_to_action', formData.get('call_to_action') || '');
updateFormData.append('status', formData.get('status'));
updateFormData.append('allow_smtp_email', formData.get('allow_smtp_email') === 'on');
updateFormData.append('allow_mailto_link', formData.get('allow_mailto_link') === 'on');
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 {
const response = await window.apiClient.makeRequest(`/admin/campaigns/${this.currentCampaign.id}`, {
method: 'PUT',
body: JSON.stringify(updates)
});
const response = await window.apiClient.putFormData(`/admin/campaigns/${this.currentCampaign.id}`, updateFormData);
if (response.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
async checkHealth() {
return this.get('/health');

View File

@@ -39,6 +39,8 @@ class CampaignPage {
}
this.campaign = data.campaign;
console.log('Campaign data loaded:', this.campaign);
console.log('Cover photo value:', this.campaign.cover_photo);
this.renderCampaign();
} catch (error) {
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-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
if (this.campaign.show_email_count && this.campaign.emailCount !== null) {
document.getElementById('email-count').textContent = this.campaign.emailCount;

View File

@@ -482,10 +482,19 @@ class UserDashboard {
listDiv.innerHTML = this.campaigns.map(campaign => `
<div class="campaign-card" data-campaign-id="${campaign.id}">
<div class="campaign-header">
<h3>${this.escapeHtml(campaign.title)}</h3>
<span class="status-badge status-${campaign.status}">${campaign.status}</span>
</div>
${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">
<h3>${this.escapeHtml(campaign.title)}</h3>
<span class="status-badge status-${campaign.status}">${campaign.status}</span>
</div>
`}
<div class="campaign-meta">
<p><strong>Slug:</strong> <code>/campaign/${campaign.slug}</code></p>