import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useParams } from 'react-router-dom'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogFooter } from '@/components/ui/dialog'; import { fetchGalleryMeta, fetchGalleryPhotos, type GalleryMetaResponse, type GalleryPhotoResource } from '../services/galleryApi'; import { useTranslation } from '../i18n/useTranslation'; import { DEFAULT_LOCALE, isLocaleCode } from '../i18n/messages'; import { AlertTriangle, Download, Loader2, X } from 'lucide-react'; interface GalleryState { meta: GalleryMetaResponse | null; photos: GalleryPhotoResource[]; cursor: string | null; loading: boolean; loadingMore: boolean; error: string | null; expired: boolean; } const INITIAL_STATE: GalleryState = { meta: null, photos: [], cursor: null, loading: true, loadingMore: false, error: null, expired: false, }; const GALLERY_PAGE_SIZE = 30; export default function PublicGalleryPage(): JSX.Element | null { const { token } = useParams<{ token: string }>(); const { t } = useTranslation(); const [state, setState] = useState(INITIAL_STATE); const [lightboxOpen, setLightboxOpen] = useState(false); const [selectedPhoto, setSelectedPhoto] = useState(null); const sentinelRef = useRef(null); const localeStorageKey = token ? `guestGalleryLocale_${token}` : 'guestGalleryLocale'; const storedLocale = typeof window !== 'undefined' && token ? localStorage.getItem(localeStorageKey) : null; const effectiveLocale = storedLocale && isLocaleCode(storedLocale as any) ? (storedLocale as any) : DEFAULT_LOCALE; const applyMeta = useCallback((meta: GalleryMetaResponse) => { if (typeof window !== 'undefined' && token) { localStorage.setItem(localeStorageKey, effectiveLocale); } setState((prev) => ({ ...prev, meta, })); }, [effectiveLocale, localeStorageKey, token]); const loadInitial = useCallback(async () => { if (!token) { return; } setState((prev) => ({ ...prev, loading: true, error: null, expired: false, photos: [], cursor: null })); try { const meta = await fetchGalleryMeta(token, effectiveLocale); applyMeta(meta); const photoResponse = await fetchGalleryPhotos(token, null, GALLERY_PAGE_SIZE); setState((prev) => ({ ...prev, loading: false, photos: photoResponse.data, cursor: photoResponse.next_cursor, })); } catch (error) { const err = error as Error & { code?: string | number }; if (err.code === 'gallery_expired' || err.code === 410) { setState((prev) => ({ ...prev, loading: false, expired: true })); } else { setState((prev) => ({ ...prev, loading: false, error: err.message || t('galleryPublic.loadError'), })); } } }, [token, applyMeta, effectiveLocale, t]); useEffect(() => { loadInitial(); }, [loadInitial]); const loadMore = useCallback(async () => { if (!token || !state.cursor || state.loadingMore) { return; } setState((prev) => ({ ...prev, loadingMore: true })); try { const response = await fetchGalleryPhotos(token, state.cursor, GALLERY_PAGE_SIZE); setState((prev) => ({ ...prev, photos: [...prev.photos, ...response.data], cursor: response.next_cursor, loadingMore: false, })); } catch (error) { const err = error as Error; setState((prev) => ({ ...prev, loadingMore: false, error: err.message || t('galleryPublic.loadError'), })); } }, [state.cursor, state.loadingMore, token, t]); useEffect(() => { if (!state.cursor || !sentinelRef.current) { return; } const observer = new IntersectionObserver((entries) => { const firstEntry = entries[0]; if (firstEntry?.isIntersecting) { loadMore(); } }, { rootMargin: '400px', threshold: 0, }); observer.observe(sentinelRef.current); return () => observer.disconnect(); }, [state.cursor, loadMore]); const themeStyles = useMemo(() => { if (!state.meta) { return {} as React.CSSProperties; } return { '--gallery-primary': state.meta.branding.primary_color, '--gallery-secondary': state.meta.branding.secondary_color, '--gallery-background': state.meta.branding.background_color, } as React.CSSProperties & Record; }, [state.meta]); const headerStyle = useMemo(() => { if (!state.meta) { return {}; } return { background: state.meta.branding.primary_color, color: '#ffffff', } satisfies React.CSSProperties; }, [state.meta]); const accentStyle = useMemo(() => { if (!state.meta) { return {}; } return { color: state.meta.branding.primary_color, } satisfies React.CSSProperties; }, [state.meta]); const backgroundStyle = useMemo(() => { if (!state.meta) { return {}; } return { backgroundColor: state.meta.branding.background_color, } satisfies React.CSSProperties; }, [state.meta]); const openLightbox = useCallback((photo: GalleryPhotoResource) => { setSelectedPhoto(photo); setLightboxOpen(true); }, []); const closeLightbox = useCallback(() => { setLightboxOpen(false); setSelectedPhoto(null); }, []); if (!token) { return null; } if (state.expired) { return (

{t('galleryPublic.expiredTitle')}

{t('galleryPublic.expiredDescription')}

); } return (

Fotospiel

{state.meta?.event.name || t('galleryPublic.title')}

{state.meta?.event.gallery_expires_at && (

{new Date(state.meta.event.gallery_expires_at).toLocaleDateString()}

)}
{state.meta?.event.description && (

{state.meta.event.description}

)} {state.error && ( {t('galleryPublic.loadError')} {state.error} )} {state.loading && (

{t('galleryPublic.loading')}

)} {!state.loading && state.photos.length === 0 && !state.error && (

{t('galleryPublic.emptyTitle')}

{t('galleryPublic.emptyDescription')}

)}
{state.photos.map((photo) => ( ))}
{state.loadingMore && (
{t('galleryPublic.loadingMore')}
)} {!state.loading && state.cursor && (
)}
(open ? setLightboxOpen(true) : closeLightbox())}>

{selectedPhoto?.guest_name || t('galleryPublic.lightboxGuestFallback')}

{selectedPhoto?.created_at ? new Date(selectedPhoto.created_at).toLocaleString() : ''}

{selectedPhoto?.full_url && ( {selectedPhoto?.guest_name )}
{selectedPhoto?.likes_count ? `${selectedPhoto.likes_count} ❤` : ''}
{selectedPhoto?.download_url && ( )}
); }