import React, { useState } from 'react'; import { useParams, Link } from 'react-router-dom'; import { AlertCircle, Clock } from 'lucide-react'; import { differenceInDays, parseISO } from 'date-fns'; import { useTranslation } from 'react-i18next'; import { useLocalizedDate } from '../hooks/useLocalizedDate'; import { useQuery } from '@tanstack/react-query'; import { Card, CardContent, Input, Button, Loading, ReCaptcha } from '../components/common'; import { useGalleryAuth, useTheme } from '../contexts'; import { useGalleryInfo } from '../hooks/useGallery'; import { GalleryView } from '../components/gallery'; import { analyticsService } from '../services/analytics.service'; import { api } from '../config/api'; import { GALLERY_THEME_PRESETS } from '../types/theme.types'; import { buildResourceUrl } from '../utils/url'; import { isGalleryPublic, normalizeRequirePassword } from '../utils/accessControl'; export const GalleryPage: React.FC = () => { const { slug, token } = useParams<{ slug: string; token?: string }>(); const { isAuthenticated, login, event } = useGalleryAuth(); const { t, i18n } = useTranslation(); const { format } = useLocalizedDate(); const { setTheme } = useTheme(); const [password, setPassword] = useState(''); const [isLoggingIn, setIsLoggingIn] = useState(false); const [loginError, setLoginError] = useState(null); const [recaptchaToken, setRecaptchaToken] = useState(null); const [autoLoginAttempted, setAutoLoginAttempted] = useState(false); // Fetch gallery info (public data) const { data: galleryInfo, isLoading: isLoadingInfo, error: infoError } = useGalleryInfo(slug!, token); const requiresPassword = normalizeRequirePassword(galleryInfo?.requires_password, true); // Fetch branding settings const { data: settingsData } = useQuery({ queryKey: ['gallery-settings'], queryFn: async () => { const response = await api.get('/public/settings'); return response.data; }, staleTime: 5 * 60 * 1000, // Cache for 5 minutes }); // Set language from admin settings when on login page React.useEffect(() => { if (!isAuthenticated && settingsData?.default_language) { i18n.changeLanguage(settingsData.default_language); } }, [settingsData, isAuthenticated, i18n]); // Apply theme for login page React.useEffect(() => { if (!isAuthenticated && galleryInfo && settingsData) { let themeToApply = null; if (galleryInfo.color_theme) { try { // Check if it's a valid JSON string if (galleryInfo.color_theme.startsWith('{')) { themeToApply = JSON.parse(galleryInfo.color_theme); } else { // Handle legacy theme names - check if it's a preset const preset = GALLERY_THEME_PRESETS[galleryInfo.color_theme]; if (preset) { themeToApply = preset.config; } else { // Unknown theme name, fall back to global theme if (settingsData.theme_config) { themeToApply = settingsData.theme_config; } } } } catch (e) { console.error('Failed to parse event theme:', e); // Fall back to global theme if (settingsData.theme_config) { themeToApply = settingsData.theme_config; } } } else if (settingsData.theme_config) { // No event theme, use global theme themeToApply = settingsData.theme_config; } // Apply theme if (themeToApply) { setTheme(themeToApply); } } }, [galleryInfo, settingsData, isAuthenticated, setTheme]); React.useEffect(() => { if (!slug) { return; } if (galleryInfo && isGalleryPublic(galleryInfo.requires_password) && !isAuthenticated && !autoLoginAttempted) { setAutoLoginAttempted(true); setIsLoggingIn(true); login(slug, '') .then(() => { setLoginError(null); }) .catch((error: any) => { const message = error?.response?.data?.error; if (message) { setLoginError(message); } }) .finally(() => { setIsLoggingIn(false); }); } }, [galleryInfo, isAuthenticated, autoLoginAttempted, login, slug]); // Calculate days until expiration const daysUntilExpiration = galleryInfo ? differenceInDays(parseISO(galleryInfo.expires_at), new Date()) : null; const handleLogin = async (e: React.FormEvent) => { e.preventDefault(); e.stopPropagation(); // Prevent any bubbling if (requiresPassword && !password.trim()) { setLoginError(t('auth.pleaseEnterPassword')); return; } try { setIsLoggingIn(true); setLoginError(null); await login(slug!, requiresPassword ? password : '', recaptchaToken); if (requiresPassword) { analyticsService.trackGalleryEvent('password_entry', { gallery: slug, success: true }); } } catch (error: any) { console.error('Login error:', error); const errorMessage = error.response?.data?.error || 'Invalid password'; const statusCode = error.response?.status; // Map backend error messages to user-friendly translations if (statusCode === 401 || errorMessage.toLowerCase().includes('invalid password')) { setLoginError(t('auth.wrongPassword')); } else if (statusCode === 429 || errorMessage.toLowerCase().includes('too many')) { setLoginError(t('auth.tooManyAttempts')); } else if (statusCode === 404) { setLoginError(t('errors.galleryNotFound')); } else { setLoginError(t('auth.invalidPassword')); } // Track failed password entry if (requiresPassword) { analyticsService.trackGalleryEvent('password_entry', { gallery: slug, success: false, statusCode }); } // Keep the password field to allow retry // Do not clear the password } finally { setIsLoggingIn(false); } }; // Show loading state if (isLoadingInfo) { return (
); } // Show error state if (infoError) { // Check if it's an archived gallery error const errorMessage = (infoError as any)?.response?.data?.error; const isArchived = errorMessage?.includes('archived'); return (
{/* Logo at top */} {settingsData?.branding_logo_url && (
{settingsData.branding_company_name
)}

{t(isArchived ? 'errors.galleryArchived' : 'errors.galleryNotFound')}

{t(isArchived ? 'errors.galleryArchivedMessage' : 'errors.galleryNotFoundMessage')}

{/* Legal Links */}
{t('legal.impressum')} | {t('legal.datenschutz')}

Powered by PicPeak

); } // Show expired state if (galleryInfo?.is_expired) { return (
{/* Logo at top */} {settingsData?.branding_logo_url && (
{settingsData.branding_company_name
)}

{t('gallery.expired')}

{t('gallery.expiredOn', { date: format(parseISO(galleryInfo.expires_at), 'PP') })}

{t('gallery.contactOrganizer')}

{/* Legal Links */}
{t('legal.impressum')} | {t('legal.datenschutz')}

Powered by PicPeak

); } // Show gallery view if authenticated if (isAuthenticated && event) { return ; } // Show login form return (
{/* Logo/Header */}
{settingsData?.branding_company_name

{galleryInfo?.event_name}

{/* Expiration Warning */} {daysUntilExpiration !== null && daysUntilExpiration <= 7 && (

{t('gallery.expiresIn', { count: daysUntilExpiration })}

{t('gallery.downloadBefore')}

)} {requiresPassword ? ( <>

{t('auth.enterPassword')}

setPassword(e.target.value)} error={loginError || undefined} autoFocus className="text-sm sm:text-base" /> setRecaptchaToken(null)} />

{t('auth.passwordHint')}

) : (

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

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

{loginError && (

{loginError}

)}
)}
{/* Legal Links */}
{t('legal.impressum')} | {t('legal.datenschutz')}

Powered by PicPeak

); };