From 486239aeb9b5f56551d5aa90f0bad3008eedc3bb Mon Sep 17 00:00:00 2001 From: Paul Nothaft <53005142+the-luap@users.noreply.github.com> Date: Tue, 17 Mar 2026 17:16:50 +0100 Subject: [PATCH] fix: address beta feedback - gallery layout fixes, Russian locale, email logo (#249) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Russian (Русский) to admin settings language dropdown - Fix Premium layout hero using thumbnail instead of hero_url - Hide "Uncategorized" section header in Story layout for uncategorized photos - Add PhotoLightbox to Story layout so photo clicks open full-screen view - Use full-res images in StoryPhotoCard instead of thumbnails - Defer public gallery auto-login text until settings/locale are loaded - Add validation and debug logging for email logo URL construction Co-authored-by: Paul Nothaft --- backend/src/services/emailProcessor.js | 8 +++-- .../gallery/layouts/GalleryPremiumLayout.tsx | 2 +- .../gallery/layouts/GalleryStoryLayout.tsx | 29 +++++++++++++-- .../gallery/layouts/story/StoryPhotoCard.tsx | 2 +- .../gallery/layouts/story/StoryScene.tsx | 36 ++++++++++--------- .../src/features/settings/tabs/GeneralTab.tsx | 1 + frontend/src/pages/GalleryPage.tsx | 32 ++++++++++------- 7 files changed, 73 insertions(+), 37 deletions(-) diff --git a/backend/src/services/emailProcessor.js b/backend/src/services/emailProcessor.js index 8381c5a7..e8ea628b 100644 --- a/backend/src/services/emailProcessor.js +++ b/backend/src/services/emailProcessor.js @@ -153,9 +153,11 @@ async function wrapEmailHtml(htmlBody, subject, language = 'en') { 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'}`; + // Build full logo URL - ensure logoUrl is a valid non-empty string + const frontendUrl = (process.env.FRONTEND_URL || 'http://localhost:3000').replace(/\/+$/, ''); + const logoPath = (typeof logoUrl === 'string' && logoUrl.trim()) ? logoUrl : '/picpeak-logo-transparent.png'; + const logoFullUrl = `${frontendUrl}${logoPath.startsWith('/') ? '' : '/'}${logoPath}`; + logger.debug('Email logo URL:', { frontendUrl, logoPath, logoFullUrl }); return ` diff --git a/frontend/src/components/gallery/layouts/GalleryPremiumLayout.tsx b/frontend/src/components/gallery/layouts/GalleryPremiumLayout.tsx index 188a3077..722ef5e8 100644 --- a/frontend/src/components/gallery/layouts/GalleryPremiumLayout.tsx +++ b/frontend/src/components/gallery/layouts/GalleryPremiumLayout.tsx @@ -343,7 +343,7 @@ export const GalleryPremiumLayout: React.FC = ({
diff --git a/frontend/src/components/gallery/layouts/GalleryStoryLayout.tsx b/frontend/src/components/gallery/layouts/GalleryStoryLayout.tsx index f369fe73..07a69887 100644 --- a/frontend/src/components/gallery/layouts/GalleryStoryLayout.tsx +++ b/frontend/src/components/gallery/layouts/GalleryStoryLayout.tsx @@ -17,6 +17,7 @@ import { StoryFeedbackSheet, StoryScrollToTop } from './story'; +import { PhotoLightbox } from '../PhotoLightbox'; import './GalleryStoryLayout.css'; @@ -71,6 +72,7 @@ export const GalleryStoryLayout: React.FC = ({ const [searchQuery, setSearchQuery] = useState(''); const [favorites, setFavorites] = useState>(new Set()); const [selectedPhotoForFeedback, setSelectedPhotoForFeedback] = useState(null); + const [lightboxIndex, setLightboxIndex] = useState(null); const [comments, setComments] = useState>>({}); const [ratings, setRatings] = useState>({}); const [savedIdentity, setSavedIdentity] = useState<{ name: string; email: string } | null>(null); @@ -112,7 +114,7 @@ export const GalleryStoryLayout: React.FC = ({ // Group by category filteredPhotos.forEach(photo => { - const categoryName = photo.category_name || t('gallery.uncategorized', 'Gallery'); + const categoryName = photo.category_name || ''; if (!photosByCategory[categoryName]) { photosByCategory[categoryName] = []; } @@ -163,6 +165,11 @@ export const GalleryStoryLayout: React.FC = ({ setSelectedPhotoForFeedback(photo); }, []); + const handleOpenLightbox = useCallback((photo: Photo) => { + const index = photos.findIndex(p => p.id === photo.id); + setLightboxIndex(index >= 0 ? index : 0); + }, [photos]); + const handleCloseFeedback = useCallback(() => { setSelectedPhotoForFeedback(null); }, []); @@ -312,7 +319,7 @@ export const GalleryStoryLayout: React.FC = ({ photos={scene.photos} favorites={favorites} onToggleFavorite={handleToggleFavorite} - onPhotoClick={handleOpenFeedback} + onPhotoClick={handleOpenLightbox} slug={slug} allowDownloads={allowDownloads} protectionLevel={protectionLevel} @@ -328,7 +335,7 @@ export const GalleryStoryLayout: React.FC = ({ index={index} isFavorite={favorites.has(photo.id)} onToggleFavorite={handleToggleFavorite} - onClick={() => handleOpenFeedback(photo)} + onClick={() => handleOpenLightbox(photo)} slug={slug} galleryId={`gallery-${scene.id}`} allowDownloads={allowDownloads} @@ -359,6 +366,22 @@ export const GalleryStoryLayout: React.FC = ({ )} + {/* Lightbox */} + {lightboxIndex !== null && ( + setLightboxIndex(null)} + slug={slug} + feedbackEnabled={feedbackEnabled} + allowDownloads={allowDownloads} + protectionLevel={protectionLevel} + useEnhancedProtection={useEnhancedProtection} + useCanvasRendering={useCanvasRendering} + onFeedbackChange={onFeedbackChange} + /> + )} + {/* Feedback Sheet */} {feedbackEnabled && ( = ({ className="block w-full h-full" > setIsLoaded(true)} className={`w-full h-full object-cover transition-all duration-700 ease-out will-change-transform ${ diff --git a/frontend/src/components/gallery/layouts/story/StoryScene.tsx b/frontend/src/components/gallery/layouts/story/StoryScene.tsx index 7672a25f..87003810 100644 --- a/frontend/src/components/gallery/layouts/story/StoryScene.tsx +++ b/frontend/src/components/gallery/layouts/story/StoryScene.tsx @@ -18,27 +18,29 @@ export const StoryScene: React.FC = ({ }) => { return (
-
- - {title} - - {subtitle && ( - + - {subtitle} - - )} -
+ {title} + + {subtitle && ( + + {subtitle} + + )} +
+ )} {children} ); diff --git a/frontend/src/features/settings/tabs/GeneralTab.tsx b/frontend/src/features/settings/tabs/GeneralTab.tsx index bd4d6cce..1873d532 100644 --- a/frontend/src/features/settings/tabs/GeneralTab.tsx +++ b/frontend/src/features/settings/tabs/GeneralTab.tsx @@ -247,6 +247,7 @@ export const GeneralTab: React.FC = ({ +

{t('settings.general.defaultLanguageHelp')} diff --git a/frontend/src/pages/GalleryPage.tsx b/frontend/src/pages/GalleryPage.tsx index 8eec1a08..35f75578 100644 --- a/frontend/src/pages/GalleryPage.tsx +++ b/frontend/src/pages/GalleryPage.tsx @@ -106,7 +106,7 @@ export const GalleryPage: React.FC = () => { }, [resolvedSlug]); // Fetch branding settings - const { data: settingsData } = useQuery({ + const { data: settingsData, isLoading: isLoadingSettings } = useQuery({ queryKey: ['gallery-settings'], queryFn: async () => { const response = await api.get('/public/settings'); @@ -177,7 +177,7 @@ export const GalleryPage: React.FC = () => { return; } - if (galleryInfo && isGalleryPublic(galleryInfo.requires_password) && !isAuthenticated && !autoLoginAttempted) { + if (galleryInfo && isGalleryPublic(galleryInfo.requires_password) && !isAuthenticated && !autoLoginAttempted && !isLoadingSettings) { setAutoLoginAttempted(true); setIsLoggingIn(true); login(resolvedSlug, '') @@ -194,7 +194,7 @@ export const GalleryPage: React.FC = () => { setIsLoggingIn(false); }); } - }, [galleryInfo, isAuthenticated, autoLoginAttempted, login, resolvedSlug, isResolvingIdentifier]); + }, [galleryInfo, isAuthenticated, autoLoginAttempted, login, resolvedSlug, isResolvingIdentifier, isLoadingSettings]); // Calculate days until expiration (null if no expiration set) const daysUntilExpiration = galleryInfo?.expires_at @@ -526,15 +526,23 @@ export const GalleryPage: React.FC = () => { ) : (

-

- {t('gallery.publicGalleryTitle', 'This gallery is publicly accessible')} -

-

- {t('gallery.publicGallerySubtitle', 'Loading the photos now...')} -

-
- -
+ {isLoadingSettings ? ( +
+ +
+ ) : ( + <> +

+ {t('gallery.publicGalleryTitle', 'This gallery is publicly accessible')} +

+

+ {t('gallery.publicGallerySubtitle', 'Loading the photos now...')} +

+
+ +
+ + )} {loginError && (

{loginError}

)}