From d91ab436e8be6831f87bdab90f92e8153721db0b Mon Sep 17 00:00:00 2001
From: Claude
Date: Tue, 25 Nov 2025 19:31:07 +0000
Subject: [PATCH] Fix multiple bugs: thumbnail generation, branding settings,
categories, theme, feedback icons, upload limit, email errors
Bug fixes included:
#52 - Thumbnail Generation: Added proper parsing of settings values and validation
of Sharp fit parameter to handle JSON-encoded strings correctly
#61 - Branding Settings Not Persisting: Added _parseBoolean helper for reliable
boolean parsing, added hide_powered_by option for white-label support
#55 - Categories Not Applied: Fixed category update logic to properly handle
numeric category IDs, added updated_at timestamp, improved cache invalidation
#59/#56 - Gallery Layout & Apply Theme: Set isPreviewMode=true so theme changes
immediately propagate to parent state, hidden redundant Apply button
#58 - Feedback Icons Show When Disabled: Added feedbackEnabled check to comment
and like buttons in MasonryGalleryLayout and GridGalleryLayout
#57 - Upload Limit 100MB: Increased body parser limit from 100MB to 500MB to
support larger batch uploads
#54 - Wrong Error Message: Enhanced email error handling with specific error
codes and translation keys for better user feedback
---
backend/server.js | 6 +-
backend/src/routes/adminEmail.js | 47 +++++++++++++---
backend/src/routes/adminPhotos.js | 55 ++++++++++++++-----
backend/src/routes/adminSettings.js | 6 +-
backend/src/services/imageProcessor.js | 34 ++++++++++--
.../src/components/admin/AdminPhotoViewer.tsx | 7 ++-
.../src/components/gallery/GalleryLayout.tsx | 6 +-
.../src/components/gallery/GalleryView.tsx | 7 +++
.../gallery/layouts/GridGalleryLayout.tsx | 2 +-
.../gallery/layouts/MasonryGalleryLayout.tsx | 4 +-
frontend/src/pages/admin/BrandingPage.tsx | 29 ++++++++++
frontend/src/pages/admin/EventDetailsPage.tsx | 3 +-
frontend/src/services/settings.service.ts | 17 ++++--
13 files changed, 182 insertions(+), 41 deletions(-)
diff --git a/backend/server.js b/backend/server.js
index fb7776d..cdc345c 100644
--- a/backend/server.js
+++ b/backend/server.js
@@ -324,9 +324,9 @@ async function initializeRateLimiters() {
// Note: Rate limiters will be initialized after database connection
-// Body parsing middleware with increased limits for large uploads
-app.use(express.json({ limit: '100mb' }));
-app.use(express.urlencoded({ extended: true, limit: '100mb' }));
+// Body parsing middleware with increased limits for large batch uploads
+app.use(express.json({ limit: '500mb' }));
+app.use(express.urlencoded({ extended: true, limit: '500mb' }));
// Request logging for API routes (with timestamps)
const apiRequestLogger = (req, res, next) => {
diff --git a/backend/src/routes/adminEmail.js b/backend/src/routes/adminEmail.js
index 45d17be..02b0057 100644
--- a/backend/src/routes/adminEmail.js
+++ b/backend/src/routes/adminEmail.js
@@ -173,26 +173,57 @@ router.post('/test', adminAuth, async (req, res) => {
} catch (error) {
console.error('Test email error:', error);
console.error('Error stack:', error.stack);
-
- // Provide more specific error messages
- let errorMessage = 'Failed to send test email';
+
+ // Provide more specific error messages with translation keys
+ let errorMessage = 'Error sending email';
+ let errorKey = 'email.errors.sendFailed';
let details = error.message;
-
+ let detailsKey = 'email.errors.unknownError';
+
if (error.code === 'ECONNREFUSED') {
errorMessage = 'Failed to connect to SMTP server';
+ errorKey = 'email.errors.connectionRefused';
details = 'Please check your SMTP host and port settings';
+ detailsKey = 'email.errors.checkHostPort';
} else if (error.code === 'EAUTH') {
errorMessage = 'SMTP authentication failed';
+ errorKey = 'email.errors.authFailed';
details = 'Please check your SMTP username and password';
+ detailsKey = 'email.errors.checkCredentials';
} else if (error.code === 'ESOCKET') {
- errorMessage = 'Network error';
+ errorMessage = 'Network error connecting to SMTP server';
+ errorKey = 'email.errors.networkError';
details = 'Could not establish connection to SMTP server';
+ detailsKey = 'email.errors.connectionFailed';
+ } else if (error.code === 'ETIMEDOUT') {
+ errorMessage = 'Connection to SMTP server timed out';
+ errorKey = 'email.errors.timeout';
+ details = 'The server took too long to respond. Please check your network and SMTP settings.';
+ detailsKey = 'email.errors.timeoutDetails';
+ } else if (error.code === 'ENOTFOUND') {
+ errorMessage = 'SMTP server not found';
+ errorKey = 'email.errors.serverNotFound';
+ details = 'The SMTP host could not be resolved. Please verify the hostname.';
+ detailsKey = 'email.errors.checkHostname';
+ } else if (error.responseCode >= 500) {
+ errorMessage = 'SMTP server error';
+ errorKey = 'email.errors.serverError';
+ details = `Server returned error code ${error.responseCode}`;
+ detailsKey = 'email.errors.serverErrorDetails';
+ } else if (error.responseCode >= 400) {
+ errorMessage = 'Email rejected by server';
+ errorKey = 'email.errors.rejected';
+ details = error.response || 'The email was rejected. Check recipient address and settings.';
+ detailsKey = 'email.errors.rejectedDetails';
}
-
- res.status(500).json({
+
+ res.status(500).json({
error: errorMessage,
+ errorKey: errorKey,
details: details,
- code: error.code
+ detailsKey: detailsKey,
+ code: error.code,
+ responseCode: error.responseCode
});
}
});
diff --git a/backend/src/routes/adminPhotos.js b/backend/src/routes/adminPhotos.js
index bb2834e..c3eed7f 100644
--- a/backend/src/routes/adminPhotos.js
+++ b/backend/src/routes/adminPhotos.js
@@ -484,24 +484,42 @@ router.patch('/:eventId/photos/:photoId', adminAuth, async (req, res) => {
}
// Prepare update data
- const updateData = {};
+ const updateData = {
+ updated_at: new Date()
+ };
// Handle type-based categories ('individual' or 'collage')
// These are string values that map to the photo.type field
if (category_id === 'individual' || category_id === 'collage') {
updateData.type = category_id;
updateData.category_id = null; // Clear legacy category_id
+ } else if (category_id === null || category_id === undefined) {
+ // Explicitly clear category
+ updateData.category_id = null;
} else {
- // Handle legacy numeric category IDs
- updateData.category_id = category_id || null;
+ // Handle numeric category IDs from photo_categories table
+ const numericCategoryId = parseInt(category_id, 10);
+ if (!isNaN(numericCategoryId)) {
+ updateData.category_id = numericCategoryId;
+ } else {
+ updateData.category_id = null;
+ }
}
// Update photo
await db('photos')
- .where({ id: photoId })
+ .where({ id: photoId, event_id: eventId })
.update(updateData);
- res.json({ message: 'Photo updated successfully' });
+ // Fetch and return updated photo for confirmation
+ const updatedPhoto = await db('photos')
+ .where({ id: photoId })
+ .first();
+
+ res.json({
+ message: 'Photo updated successfully',
+ photo: updatedPhoto
+ });
} catch (error) {
console.error('Error updating photo:', error);
res.status(500).json({ error: 'Failed to update photo' });
@@ -581,33 +599,44 @@ router.post('/:eventId/photos/bulk-update', adminAuth, async (req, res) => {
try {
const { eventId } = req.params;
const { photoIds, updates } = req.body;
-
+
if (!Array.isArray(photoIds) || photoIds.length === 0) {
return res.status(400).json({ error: 'Invalid photo IDs' });
}
-
+
// Verify all photos belong to the event
const photoCount = await db('photos')
.whereIn('id', photoIds)
.where('event_id', eventId)
.count('id as count')
.first();
-
- if (photoCount.count !== photoIds.length) {
+
+ if (parseInt(photoCount.count) !== photoIds.length) {
return res.status(400).json({ error: 'Some photos do not belong to this event' });
}
-
+
// Prepare update data
- const updateData = {};
+ const updateData = {
+ updated_at: new Date()
+ };
+
if (updates.category_id !== undefined) {
// Handle type-based categories ('individual' or 'collage')
// These are string values that map to the photo.type field
if (updates.category_id === 'individual' || updates.category_id === 'collage') {
updateData.type = updates.category_id;
updateData.category_id = null; // Clear legacy category_id
+ } else if (updates.category_id === null) {
+ // Explicitly clear category
+ updateData.category_id = null;
} else {
- // Handle legacy numeric category IDs
- updateData.category_id = updates.category_id || null;
+ // Handle numeric category IDs from photo_categories table
+ const numericCategoryId = parseInt(updates.category_id, 10);
+ if (!isNaN(numericCategoryId)) {
+ updateData.category_id = numericCategoryId;
+ } else {
+ updateData.category_id = null;
+ }
}
}
diff --git a/backend/src/routes/adminSettings.js b/backend/src/routes/adminSettings.js
index c0e6aac..01ea906 100644
--- a/backend/src/routes/adminSettings.js
+++ b/backend/src/routes/adminSettings.js
@@ -191,7 +191,8 @@ router.put('/branding', adminAuth, async (req, res) => {
logo_position,
logo_display_header,
logo_display_hero,
- logo_display_mode
+ logo_display_mode,
+ hide_powered_by
} = req.body;
const brandingSettings = {
@@ -211,7 +212,8 @@ router.put('/branding', adminAuth, async (req, res) => {
logo_position,
logo_display_header,
logo_display_hero,
- logo_display_mode
+ logo_display_mode,
+ hide_powered_by
};
// Handle favicon deletion if empty string or null is provided
diff --git a/backend/src/services/imageProcessor.js b/backend/src/services/imageProcessor.js
index 09161f3..c729658 100644
--- a/backend/src/services/imageProcessor.js
+++ b/backend/src/services/imageProcessor.js
@@ -18,6 +18,29 @@ const DEFAULT_THUMBNAIL_FORMAT = 'jpeg';
const getStoragePath = () => process.env.STORAGE_PATH || path.join(__dirname, '../../../storage');
const getThumbnailPath = () => path.join(getStoragePath(), 'thumbnails');
+// Helper to parse setting value (handles both JSON-encoded and plain values)
+function parseSettingValue(value) {
+ if (value === null || value === undefined) {
+ return null;
+ }
+ // Try to parse as JSON first (in case it's a JSON-encoded string like '"cover"')
+ try {
+ return JSON.parse(value);
+ } catch (e) {
+ // If it's not valid JSON, return the raw value
+ return value;
+ }
+}
+
+// Validate that fit value is valid for Sharp
+function validateFitValue(fit) {
+ const validFitValues = ['cover', 'contain', 'fill', 'inside', 'outside'];
+ if (fit && validFitValues.includes(fit)) {
+ return fit;
+ }
+ return DEFAULT_THUMBNAIL_FIT;
+}
+
// Get thumbnail settings from database
async function getThumbnailSettings() {
try {
@@ -30,16 +53,19 @@ async function getThumbnailSettings() {
'thumbnail_format'
])
.select('setting_key', 'setting_value');
-
+
const settingsMap = {};
settings.forEach(s => {
- settingsMap[s.setting_key] = s.setting_value;
+ settingsMap[s.setting_key] = parseSettingValue(s.setting_value);
});
-
+
+ // Parse and validate fit value
+ const fitValue = validateFitValue(settingsMap.thumbnail_fit);
+
return {
width: parseInt(settingsMap.thumbnail_width) || DEFAULT_THUMBNAIL_WIDTH,
height: parseInt(settingsMap.thumbnail_height) || DEFAULT_THUMBNAIL_HEIGHT,
- fit: settingsMap.thumbnail_fit || DEFAULT_THUMBNAIL_FIT,
+ fit: fitValue,
quality: parseInt(settingsMap.thumbnail_quality) || DEFAULT_THUMBNAIL_QUALITY,
format: settingsMap.thumbnail_format || DEFAULT_THUMBNAIL_FORMAT
};
diff --git a/frontend/src/components/admin/AdminPhotoViewer.tsx b/frontend/src/components/admin/AdminPhotoViewer.tsx
index 3b8d2a0..9eb22d0 100644
--- a/frontend/src/components/admin/AdminPhotoViewer.tsx
+++ b/frontend/src/components/admin/AdminPhotoViewer.tsx
@@ -109,8 +109,11 @@ export const AdminPhotoViewer: React.FC = ({
await photosService.updatePhotoCategory(eventId, currentPhoto.id, categoryId);
toast.success('Category updated');
setShowCategoryMenu(false);
- // Trigger refresh to update the photo data
- onPhotoDeleted(); // This will refresh the photos list
+ // Invalidate photos query to refresh data
+ await queryClient.invalidateQueries({ queryKey: ['admin-event-photos', eventId.toString()] });
+ await queryClient.invalidateQueries({ queryKey: ['admin-event-photos', eventId] });
+ // Also trigger the parent's refresh callback
+ onPhotoDeleted();
} catch (error) {
toast.error('Failed to update category');
}
diff --git a/frontend/src/components/gallery/GalleryLayout.tsx b/frontend/src/components/gallery/GalleryLayout.tsx
index 658412f..d3d3033 100644
--- a/frontend/src/components/gallery/GalleryLayout.tsx
+++ b/frontend/src/components/gallery/GalleryLayout.tsx
@@ -29,6 +29,7 @@ interface GalleryLayoutProps {
logo_display_header?: boolean;
logo_display_hero?: boolean;
logo_display_mode?: 'logo_only' | 'text_only' | 'logo_and_text';
+ hide_powered_by?: boolean;
};
showLogout?: boolean;
onLogout?: () => void;
@@ -438,7 +439,10 @@ export const GalleryLayout: React.FC = ({
)}
- {brandingSettings?.footer_text || '© 2024 Your Company. All rights reserved.'} | Powered by PicPeak
+ {brandingSettings?.footer_text || '© 2024 Your Company. All rights reserved.'}
+ {!brandingSettings?.hide_powered_by && (
+ <> | Powered by PicPeak>
+ )}
{brandingSettings?.company_name && brandingSettings?.company_tagline && (
diff --git a/frontend/src/components/gallery/GalleryView.tsx b/frontend/src/components/gallery/GalleryView.tsx
index 2e8600f..65fd57f 100644
--- a/frontend/src/components/gallery/GalleryView.tsx
+++ b/frontend/src/components/gallery/GalleryView.tsx
@@ -165,6 +165,13 @@ export const GalleryView: React.FC = ({ slug, event }) => {
footer_text: settingsData.branding_footer_text || '© 2024 Your Company. All rights reserved.',
watermark_enabled: settingsData.branding_watermark_enabled || false,
logo_url: settingsData.branding_logo_url || null,
+ logo_size: settingsData.branding_logo_size || 'medium',
+ logo_max_height: settingsData.branding_logo_max_height || 48,
+ logo_position: settingsData.branding_logo_position || 'left',
+ logo_display_header: settingsData.branding_logo_display_header !== false,
+ logo_display_hero: settingsData.branding_logo_display_hero !== false,
+ logo_display_mode: settingsData.branding_logo_display_mode || 'logo_and_text',
+ hide_powered_by: settingsData.branding_hide_powered_by === true,
});
}
}, [settingsData]);
diff --git a/frontend/src/components/gallery/layouts/GridGalleryLayout.tsx b/frontend/src/components/gallery/layouts/GridGalleryLayout.tsx
index a558c90..01763fb 100644
--- a/frontend/src/components/gallery/layouts/GridGalleryLayout.tsx
+++ b/frontend/src/components/gallery/layouts/GridGalleryLayout.tsx
@@ -231,7 +231,7 @@ const GridPhoto: React.FC = ({
)}
- {showFeedbackActions && onQuickComment && (
+ {showFeedbackActions && feedbackOptions?.allowComments && onQuickComment && (
)}
- {onQuickComment && (
+ {feedbackEnabled && feedbackOptions?.allowComments && onQuickComment && (
)}
- {feedbackOptions?.allowLikes && (
+ {feedbackEnabled && feedbackOptions?.allowLikes && (