// @ts-nocheck 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, Share, X } from 'lucide-react'; import { createPhotoShareLink } from '../services/photosApi'; import { getContrastingTextColor } from '../lib/color'; import { applyGuestTheme } from '../lib/guestTheme'; 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(): React.ReactElement | 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 [shareLoading, setShareLoading] = useState(false); const sentinelRef = useRef(null); const localeStorageKey = token ? `guestGalleryLocale_${token}` : 'guestGalleryLocale'; const storedLocale = typeof window !== 'undefined' && token ? localStorage.getItem(localeStorageKey) : null; const effectiveLocale: LocaleCode = storedLocale && isLocaleCode(storedLocale) ? storedLocale : 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 resolvedBranding = useMemo(() => { if (!state.meta) { return null; } const palette = state.meta.branding.palette ?? {}; const primary = palette.primary ?? state.meta.branding.primary_color ?? '#FF5A5F'; const secondary = palette.secondary ?? state.meta.branding.secondary_color ?? '#FFF8F5'; const background = palette.background ?? state.meta.branding.background_color ?? '#ffffff'; const surface = palette.surface ?? state.meta.branding.surface_color ?? background; const mode = state.meta.branding.mode ?? 'auto'; return { primary, secondary, background, surface, mode, }; }, [state.meta]); useEffect(() => { if (!resolvedBranding) { return; } return applyGuestTheme(resolvedBranding); }, [resolvedBranding]); 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 (!resolvedBranding) { return {} as React.CSSProperties; } return { '--gallery-primary': resolvedBranding.primary, '--gallery-secondary': resolvedBranding.secondary, '--gallery-background': resolvedBranding.background, '--gallery-surface': resolvedBranding.surface, } as React.CSSProperties & Record; }, [resolvedBranding]); const headerStyle = useMemo(() => { if (!resolvedBranding) { return {}; } const textColor = getContrastingTextColor(resolvedBranding.primary, '#0f172a', '#ffffff'); return { background: `linear-gradient(135deg, ${resolvedBranding.primary}, ${resolvedBranding.secondary})`, color: textColor, } satisfies React.CSSProperties; }, [resolvedBranding]); const accentStyle = useMemo(() => { if (!resolvedBranding) { return {}; } return { color: resolvedBranding.primary, } satisfies React.CSSProperties; }, [resolvedBranding]); const backgroundStyle = useMemo(() => { if (!resolvedBranding) { return {}; } return { backgroundColor: resolvedBranding.background, } satisfies React.CSSProperties; }, [resolvedBranding]); 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} ❤` : ''}
{(state.meta?.event?.guest_downloads_enabled ?? true) && selectedPhoto?.download_url ? ( ) : null} {(state.meta?.event?.guest_sharing_enabled ?? true) && selectedPhoto ? ( ) : null}
); }