Enhance email templates with clickable links, branding, and improved styling
- Add clickable gallery links in all email templates - Include application logo in email header and footer (custom or PicPeak default) - Redesign emails with professional styling matching gallery login page - Gray background with white content box - PicPeak green header with centered logo - Clean typography and proper spacing - Responsive design for mobile devices - Styled call-to-action buttons - Footer with branding and copyright - Update email processor to fetch branding settings dynamically - Use proper API URLs for logo images in emails 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ const crypto = require('crypto');
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
const { archiveEvent } = require('../services/archiveService');
|
||||
const { formatDate } = require('../utils/dateFormatter');
|
||||
|
||||
// Create new event
|
||||
router.post('/', adminAuth, [
|
||||
@@ -20,12 +21,15 @@ router.post('/', adminAuth, [
|
||||
body('expiration_days').isInt({ min: 1, max: 365 }).optional(),
|
||||
body('welcome_message').optional().trim(),
|
||||
body('color_theme').optional().trim(),
|
||||
body('allow_user_uploads').optional().isBoolean(),
|
||||
body('upload_category_id').optional().isInt()
|
||||
body('allow_user_uploads').optional().isBoolean().toBoolean(),
|
||||
body('upload_category_id').optional({ nullable: true, checkFalsy: true }).isInt(),
|
||||
body('host_name').notEmpty().trim()
|
||||
], async (req, res) => {
|
||||
try {
|
||||
console.log('Create event request body:', req.body);
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
console.error('Validation errors:', errors.array());
|
||||
return res.status(400).json({ errors: errors.array() });
|
||||
}
|
||||
|
||||
@@ -33,6 +37,7 @@ router.post('/', adminAuth, [
|
||||
event_type,
|
||||
event_name,
|
||||
event_date,
|
||||
host_name,
|
||||
host_email,
|
||||
admin_email,
|
||||
password,
|
||||
@@ -76,6 +81,7 @@ router.post('/', adminAuth, [
|
||||
event_type,
|
||||
event_name,
|
||||
event_date,
|
||||
host_name,
|
||||
host_email,
|
||||
admin_email,
|
||||
password_hash,
|
||||
@@ -96,17 +102,20 @@ router.post('/', adminAuth, [
|
||||
);
|
||||
|
||||
// Queue creation email
|
||||
// Determine language based on email domain
|
||||
const emailLang = host_email.endsWith('.de') ? 'de' : 'en';
|
||||
|
||||
await db('email_queue').insert({
|
||||
event_id: eventId,
|
||||
recipient_email: host_email,
|
||||
email_type: 'gallery_created',
|
||||
email_data: JSON.stringify({
|
||||
host_name: host_email.split('@')[0], // Extract name from email
|
||||
host_name: host_name,
|
||||
event_name,
|
||||
event_date: new Date(event_date).toLocaleDateString(),
|
||||
event_date: await formatDate(event_date, emailLang),
|
||||
gallery_link: shareLink,
|
||||
gallery_password: password,
|
||||
expiry_date: expires_at.toLocaleDateString()
|
||||
expiry_date: await formatDate(expires_at, emailLang)
|
||||
})
|
||||
// scheduled_at will use default value
|
||||
});
|
||||
@@ -245,10 +254,28 @@ router.get('/:id', adminAuth, async (req, res) => {
|
||||
.limit(10)
|
||||
.select('filename', 'type', 'size_bytes', 'uploaded_at');
|
||||
|
||||
// Get view and download statistics
|
||||
const [{ totalViews }] = await db('access_logs')
|
||||
.where('event_id', id)
|
||||
.where('action', 'view')
|
||||
.count('* as totalViews');
|
||||
|
||||
const [{ totalDownloads }] = await db('access_logs')
|
||||
.where('event_id', id)
|
||||
.where('action', 'download')
|
||||
.count('* as totalDownloads');
|
||||
|
||||
const [{ uniqueVisitors }] = await db('access_logs')
|
||||
.where('event_id', id)
|
||||
.countDistinct('ip_address as uniqueVisitors');
|
||||
|
||||
res.json({
|
||||
...event,
|
||||
photo_count: parseInt(photoCount) || 0,
|
||||
total_size: parseInt(totalSize) || 0,
|
||||
total_views: parseInt(totalViews) || 0,
|
||||
total_downloads: parseInt(totalDownloads) || 0,
|
||||
unique_visitors: parseInt(uniqueVisitors) || 0,
|
||||
recent_photos: recentPhotos
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -263,20 +290,44 @@ router.put('/:id', adminAuth, [
|
||||
body('admin_email').optional().isEmail(),
|
||||
body('is_active').optional().isBoolean(),
|
||||
body('expires_at').optional().isISO8601(),
|
||||
body('welcome_message').optional().trim(),
|
||||
body('color_theme').optional().trim(),
|
||||
body('welcome_message').optional({ nullable: true, checkFalsy: true }).trim(),
|
||||
body('color_theme').optional({ nullable: true }),
|
||||
body('allow_user_uploads').optional().isBoolean(),
|
||||
body('upload_category_id').optional().isInt()
|
||||
body('host_name').optional().trim().notEmpty(),
|
||||
body('upload_category_id').optional().custom((value) => {
|
||||
// Accept null, undefined, or integer values
|
||||
if (value === null || value === undefined) return true;
|
||||
return Number.isInteger(Number(value));
|
||||
}).withMessage('upload_category_id must be an integer or null'),
|
||||
body('hero_photo_id').optional().custom((value) => {
|
||||
// Accept null, undefined, or numeric values
|
||||
if (value === null || value === undefined) return true;
|
||||
// Check if it's a number or can be converted to a valid integer
|
||||
const num = Number(value);
|
||||
return !isNaN(num) && Number.isInteger(num);
|
||||
}).withMessage('hero_photo_id must be an integer or null')
|
||||
], async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
console.log('Update event validation errors:', JSON.stringify(errors.array(), null, 2));
|
||||
console.log('Request body:', req.body);
|
||||
return res.status(400).json({ errors: errors.array() });
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
const updates = req.body;
|
||||
|
||||
// Log the update request for debugging
|
||||
console.log('Update event request:', {
|
||||
id,
|
||||
updates,
|
||||
color_theme_length: updates.color_theme ? updates.color_theme.length : 0,
|
||||
color_theme_type: typeof updates.color_theme,
|
||||
hero_photo_id: updates.hero_photo_id,
|
||||
hero_photo_id_type: typeof updates.hero_photo_id
|
||||
});
|
||||
|
||||
// Check if event exists
|
||||
const event = await db('events').where('id', id).first();
|
||||
if (!event) {
|
||||
@@ -286,10 +337,7 @@ router.put('/:id', adminAuth, [
|
||||
// Update event
|
||||
await db('events')
|
||||
.where('id', id)
|
||||
.update({
|
||||
...updates,
|
||||
updated_at: new Date()
|
||||
});
|
||||
.update(updates);
|
||||
|
||||
// Log activity
|
||||
await logActivity('event_updated',
|
||||
|
||||
Reference in New Issue
Block a user