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:
2025-07-10 22:19:15 +02:00
parent 6438374258
commit 5328b4f73a
52 changed files with 3237 additions and 683 deletions
+37 -5
View File
@@ -96,7 +96,8 @@ router.get('/:slug/info', async (req, res) => {
expires_at: event.expires_at,
is_active: event.is_active,
is_expired: !event.is_active || new Date(event.expires_at) < new Date(),
requires_password: true
requires_password: true,
color_theme: event.color_theme
});
} catch (error) {
console.error('Error fetching gallery info:', error);
@@ -142,7 +143,8 @@ router.get('/:slug/photos', verifyGalleryAccess, async (req, res) => {
event_date: req.event.event_date,
welcome_message: req.event.welcome_message,
color_theme: req.event.color_theme,
expires_at: req.event.expires_at
expires_at: req.event.expires_at,
hero_photo_id: req.event.hero_photo_id
},
categories: categories.map(cat => ({
id: cat.id,
@@ -222,12 +224,26 @@ router.get('/:slug/download/:photoId', verifyGalleryAccess, async (req, res) =>
// Download all photos as ZIP
router.get('/:slug/download-all', verifyGalleryAccess, async (req, res) => {
try {
const photos = await db('photos').where('event_id', req.event.id);
// Fetch photos with category information
const photos = await db('photos')
.leftJoin('photo_categories', 'photos.category_id', 'photo_categories.id')
.where('photos.event_id', req.event.id)
.select(
'photos.*',
'photo_categories.name as category_name',
'photo_categories.slug as category_slug'
)
.orderBy('photo_categories.name', 'asc')
.orderBy('photos.uploaded_at', 'desc');
if (photos.length === 0) {
return res.status(404).json({ error: 'No photos found' });
}
// Count unique categories (excluding null)
const uniqueCategories = new Set(photos.filter(p => p.category_id).map(p => p.category_id)).size;
const hasMultipleCategories = uniqueCategories > 1;
res.setHeader('Content-Type', 'application/zip');
res.setHeader('Content-Disposition', `attachment; filename="${req.event.slug}.zip"`);
@@ -245,13 +261,29 @@ router.get('/:slug/download-all', verifyGalleryAccess, async (req, res) => {
for (const photo of photos) {
const filePath = path.join(getStoragePath(), 'events/active', photo.path);
// Determine the file name in the archive
let archiveName;
if (hasMultipleCategories) {
if (photo.category_name) {
// Use category name as folder (sanitize for filesystem)
const folderName = photo.category_name.replace(/[^a-zA-Z0-9-_ ]/g, '').trim();
archiveName = path.join(folderName, photo.filename);
} else {
// Put uncategorized photos in 'Uncategorized' folder
archiveName = path.join('Uncategorized', photo.filename);
}
} else {
// No folders, just the filename
archiveName = photo.filename;
}
if (watermarkSettings && watermarkSettings.enabled) {
// Apply watermark
const watermarkedBuffer = await watermarkService.applyWatermark(filePath, watermarkSettings);
archive.append(watermarkedBuffer, { name: photo.path });
archive.append(watermarkedBuffer, { name: archiveName });
} else {
// Add original file
archive.file(filePath, { name: photo.path });
archive.file(filePath, { name: archiveName });
}
}