fix: resolve external media dimensions, gallery theme race condition, and add email color customization
- Fix dimension repair for external media by using photoResolver instead of hardcoded paths - Extract photo dimensions via Sharp during external media import - Remove duplicate theme useEffect from GalleryView to prevent flash/revert race condition - Pass event welcome_message to Story layout footer for per-event customization - Add email_primary_color/email_secondary_color settings with admin UI color pickers - Add i18n keys for email branding in all 4 locales (en, de, ru, pt)
This commit is contained in:
@@ -5,6 +5,7 @@ const { adminAuth } = require('../middleware/auth');
|
||||
const { requirePermission } = require('../middleware/permissions');
|
||||
const { list, resolveExternalPath, getExternalMediaRoot } = require('../services/externalMediaService');
|
||||
const { db, logActivity } = require('../database/db');
|
||||
const sharp = require('sharp');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
const router = express.Router();
|
||||
@@ -108,6 +109,18 @@ router.post('/events/:id/import-external', adminAuth, requirePermission('photos.
|
||||
.first();
|
||||
if (exists) { skipped++; continue; }
|
||||
const stats = await fs.stat(f.full);
|
||||
|
||||
// Extract dimensions via Sharp
|
||||
let width = null;
|
||||
let height = null;
|
||||
try {
|
||||
const metadata = await sharp(f.full).metadata();
|
||||
width = metadata.width || null;
|
||||
height = metadata.height || null;
|
||||
} catch (dimErr) {
|
||||
logger.warn(`Could not extract dimensions for ${f.rel}: ${dimErr.message}`);
|
||||
}
|
||||
|
||||
const inserted = await db('photos')
|
||||
.insert({
|
||||
event_id: eventId,
|
||||
@@ -117,6 +130,8 @@ router.post('/events/:id/import-external', adminAuth, requirePermission('photos.
|
||||
thumbnail_path: null,
|
||||
type,
|
||||
size_bytes: stats.size,
|
||||
width,
|
||||
height,
|
||||
source_origin: 'external',
|
||||
external_relpath: f.rel
|
||||
})
|
||||
|
||||
@@ -3,11 +3,10 @@ const router = express.Router();
|
||||
const { db } = require('../database/db');
|
||||
const { adminAuth } = require('../middleware/auth');
|
||||
const { requirePermission } = require('../middleware/permissions');
|
||||
const path = require('path');
|
||||
const fs = require('fs').promises;
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
const getStoragePath = () => process.env.STORAGE_PATH || path.join(__dirname, '../../../storage');
|
||||
const { resolvePhotoFilePath } = require('../services/photoResolver');
|
||||
|
||||
// Module-level progress state
|
||||
let repairProgress = {
|
||||
@@ -23,13 +22,18 @@ router.post('/repair-dimensions', adminAuth, requirePermission('photos.edit'), a
|
||||
}
|
||||
|
||||
const photos = await db('photos')
|
||||
.join('events', 'photos.event_id', 'events.id')
|
||||
.where(function () {
|
||||
this.whereNull('width').orWhereNull('height');
|
||||
this.whereNull('photos.width').orWhereNull('photos.height');
|
||||
})
|
||||
.where(function () {
|
||||
this.where('media_type', '!=', 'video').orWhereNull('media_type');
|
||||
this.where('photos.media_type', '!=', 'video').orWhereNull('photos.media_type');
|
||||
})
|
||||
.select('id', 'path', 'filename');
|
||||
.select(
|
||||
'photos.id', 'photos.path', 'photos.filename',
|
||||
'photos.source_origin', 'photos.external_relpath', 'photos.event_id',
|
||||
'events.source_mode', 'events.external_path', 'events.slug'
|
||||
);
|
||||
|
||||
if (photos.length === 0) {
|
||||
return res.json({ message: 'No photos need dimension repair', count: 0 });
|
||||
@@ -61,15 +65,16 @@ router.post('/repair-dimensions', adminAuth, requirePermission('photos.edit'), a
|
||||
|
||||
for (const photo of photos) {
|
||||
try {
|
||||
if (!photo.path) {
|
||||
logger.warn(`Photo ${photo.id} has no path, skipping dimension repair`);
|
||||
const event = { source_mode: photo.source_mode, external_path: photo.external_path, slug: photo.slug };
|
||||
let fullPath;
|
||||
try {
|
||||
fullPath = resolvePhotoFilePath(event, photo);
|
||||
} catch (err) {
|
||||
logger.warn(`Photo ${photo.id} has no resolvable path, skipping dimension repair: ${err.message}`);
|
||||
errorCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const storagePath = getStoragePath();
|
||||
const fullPath = path.join(storagePath, 'events/active', photo.path);
|
||||
|
||||
try {
|
||||
await fs.access(fullPath);
|
||||
} catch (err) {
|
||||
@@ -85,8 +90,7 @@ router.post('/repair-dimensions', adminAuth, requirePermission('photos.edit'), a
|
||||
.where({ id: photo.id })
|
||||
.update({
|
||||
width: metadata.width,
|
||||
height: metadata.height,
|
||||
updated_at: db.fn.now()
|
||||
height: metadata.height
|
||||
});
|
||||
successCount++;
|
||||
|
||||
|
||||
@@ -111,35 +111,48 @@ async function getRecipientLanguage(email, eventId = null) {
|
||||
return 'en'; // Default to English
|
||||
}
|
||||
|
||||
// Darken a hex color by a percentage (0-1)
|
||||
function darkenColor(hex, amount = 0.15) {
|
||||
const num = parseInt(hex.replace('#', ''), 16);
|
||||
const r = Math.max(0, Math.min(255, ((num >> 16) & 0xFF) * (1 - amount)));
|
||||
const g = Math.max(0, Math.min(255, ((num >> 8) & 0xFF) * (1 - amount)));
|
||||
const b = Math.max(0, Math.min(255, (num & 0xFF) * (1 - amount)));
|
||||
return `#${(1 << 24 | Math.round(r) << 16 | Math.round(g) << 8 | Math.round(b)).toString(16).slice(1)}`;
|
||||
}
|
||||
|
||||
// Wrap HTML body in the styled email template with header, footer, and logo
|
||||
async function wrapEmailHtml(htmlBody, subject, language = 'en') {
|
||||
// Get branding settings for logo
|
||||
// Get branding settings for logo and email colors
|
||||
let logoUrl = '';
|
||||
let companyName = 'PicPeak';
|
||||
let primaryColor = '#5C8762';
|
||||
let secondaryColor = '#f9f9f9';
|
||||
try {
|
||||
const brandingSettings = await db('app_settings')
|
||||
.whereIn('setting_key', ['branding_logo_url', 'branding_company_name'])
|
||||
.whereIn('setting_key', [
|
||||
'branding_logo_url', 'branding_company_name',
|
||||
'email_primary_color', 'email_secondary_color'
|
||||
])
|
||||
.select('setting_key', 'setting_value');
|
||||
|
||||
brandingSettings.forEach(setting => {
|
||||
if (setting.setting_key === 'branding_logo_url' && setting.setting_value) {
|
||||
try {
|
||||
logoUrl = JSON.parse(setting.setting_value);
|
||||
} catch (e) {
|
||||
logoUrl = setting.setting_value;
|
||||
}
|
||||
} else if (setting.setting_key === 'branding_company_name' && setting.setting_value) {
|
||||
try {
|
||||
companyName = JSON.parse(setting.setting_value);
|
||||
} catch (e) {
|
||||
companyName = setting.setting_value;
|
||||
}
|
||||
const val = setting.setting_value;
|
||||
if (setting.setting_key === 'branding_logo_url' && val) {
|
||||
try { logoUrl = JSON.parse(val); } catch (e) { logoUrl = val; }
|
||||
} else if (setting.setting_key === 'branding_company_name' && val) {
|
||||
try { companyName = JSON.parse(val); } catch (e) { companyName = val; }
|
||||
} else if (setting.setting_key === 'email_primary_color' && val) {
|
||||
try { primaryColor = JSON.parse(val); } catch (e) { primaryColor = val; }
|
||||
} else if (setting.setting_key === 'email_secondary_color' && val) {
|
||||
try { secondaryColor = JSON.parse(val); } catch (e) { secondaryColor = val; }
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error fetching branding settings:', error);
|
||||
}
|
||||
|
||||
const hoverColor = darkenColor(primaryColor, 0.15);
|
||||
|
||||
// If no custom logo, use default PicPeak logo
|
||||
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3000';
|
||||
const logoFullUrl = `${frontendUrl}${logoUrl || '/picpeak-logo-transparent.png'}`;
|
||||
@@ -172,7 +185,7 @@ async function wrapEmailHtml(htmlBody, subject, language = 'en') {
|
||||
overflow: hidden;
|
||||
}
|
||||
.email-header {
|
||||
background-color: #5C8762;
|
||||
background-color: ${primaryColor};
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -185,7 +198,7 @@ async function wrapEmailHtml(htmlBody, subject, language = 'en') {
|
||||
padding: 40px 30px;
|
||||
}
|
||||
.email-content h2 {
|
||||
color: #5C8762;
|
||||
color: ${primaryColor};
|
||||
margin-top: 0;
|
||||
margin-bottom: 20px;
|
||||
font-size: 24px;
|
||||
@@ -206,7 +219,7 @@ async function wrapEmailHtml(htmlBody, subject, language = 'en') {
|
||||
.button {
|
||||
display: inline-block;
|
||||
padding: 12px 30px;
|
||||
background-color: #5C8762;
|
||||
background-color: ${primaryColor};
|
||||
color: white !important;
|
||||
text-decoration: none;
|
||||
border-radius: 5px;
|
||||
@@ -214,10 +227,10 @@ async function wrapEmailHtml(htmlBody, subject, language = 'en') {
|
||||
margin: 20px 0;
|
||||
}
|
||||
.button:hover {
|
||||
background-color: #4a6f4f;
|
||||
background-color: ${hoverColor};
|
||||
}
|
||||
.email-footer {
|
||||
background-color: #f9f9f9;
|
||||
background-color: ${secondaryColor};
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
border-top: 1px solid #eee;
|
||||
@@ -234,11 +247,11 @@ async function wrapEmailHtml(htmlBody, subject, language = 'en') {
|
||||
margin: 5px 0;
|
||||
}
|
||||
a {
|
||||
color: #5C8762;
|
||||
color: ${primaryColor};
|
||||
text-decoration: underline;
|
||||
}
|
||||
a:hover {
|
||||
color: #4a6f4f;
|
||||
color: ${hoverColor};
|
||||
}
|
||||
strong {
|
||||
color: #333;
|
||||
|
||||
Reference in New Issue
Block a user