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 && (