import React, { useEffect, useState } from 'react'; import { Page } from './_util'; import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; import { usePollGalleryDelta } from '../polling/usePollGalleryDelta'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import FiltersBar, { type GalleryFilter } from '../components/FiltersBar'; import { Heart, Users, Image as ImageIcon, Camera, Package as PackageIcon, AlertTriangle } from 'lucide-react'; import { likePhoto } from '../services/photosApi'; import PhotoLightbox from './PhotoLightbox'; import { fetchEvent, getEventPackage, fetchStats, type EventData, type EventPackage, type EventStats } from '../services/eventApi'; import { useTranslation } from '../i18n/useTranslation'; export default function GalleryPage() { const { token } = useParams<{ token?: string }>(); const navigate = useNavigate(); const { photos, loading, newCount, acknowledgeNew } = usePollGalleryDelta(token ?? ''); const [filter, setFilter] = React.useState('latest'); const [currentPhotoIndex, setCurrentPhotoIndex] = React.useState(null); const [hasOpenedPhoto, setHasOpenedPhoto] = useState(false); const [event, setEvent] = useState(null); const [eventPackage, setEventPackage] = useState(null); const [stats, setStats] = useState(null); const [eventLoading, setEventLoading] = useState(true); const { t } = useTranslation(); const locale = typeof window !== 'undefined' ? window.navigator.language : 'de-DE'; const [searchParams] = useSearchParams(); const photoIdParam = searchParams.get('photoId'); // Auto-open lightbox if photoId in query params useEffect(() => { if (photoIdParam && photos.length > 0 && currentPhotoIndex === null && !hasOpenedPhoto) { const index = photos.findIndex((photo: any) => photo.id === parseInt(photoIdParam, 10)); if (index !== -1) { setCurrentPhotoIndex(index); setHasOpenedPhoto(true); } } }, [photos, photoIdParam, currentPhotoIndex, hasOpenedPhoto]); // Load event and package info useEffect(() => { if (!token) return; const loadEventData = async () => { try { setEventLoading(true); const [eventData, packageData, statsData] = await Promise.all([ fetchEvent(token), getEventPackage(token), fetchStats(token), ]); setEvent(eventData); setEventPackage(packageData); setStats(statsData); } catch (err) { console.error('Failed to load event data', err); } finally { setEventLoading(false); } }; loadEventData(); }, [token]); const myPhotoIds = React.useMemo(() => { try { const raw = localStorage.getItem('my-photo-ids'); return new Set(raw ? JSON.parse(raw) : []); } catch { return new Set(); } }, []); const list = React.useMemo(() => { let arr = photos.slice(); if (filter === 'popular') { arr.sort((a: any, b: any) => (b.likes_count ?? 0) - (a.likes_count ?? 0)); } else if (filter === 'mine') { arr = arr.filter((p: any) => myPhotoIds.has(p.id)); } else { arr.sort((a: any, b: any) => new Date(b.created_at ?? 0).getTime() - new Date(a.created_at ?? 0).getTime()); } return arr; }, [photos, filter, myPhotoIds]); const [liked, setLiked] = React.useState>(new Set()); const [counts, setCounts] = React.useState>({}); const photoLimits = eventPackage?.limits?.photos ?? null; const guestLimits = eventPackage?.limits?.guests ?? null; const galleryLimits = eventPackage?.limits?.gallery ?? null; const galleryCountdown = React.useMemo(() => { if (!galleryLimits) { return null; } if (galleryLimits.state === 'expired') { return { tone: 'danger' as const, label: t('galleryCountdown.expired'), description: t('galleryCountdown.expiredDescription'), cta: null, }; } if (galleryLimits.state === 'warning') { const days = Math.max(0, galleryLimits.days_remaining ?? 0); const label = days <= 1 ? t('galleryCountdown.expiresToday') : t('galleryCountdown.expiresIn').replace('{days}', `${days}`); return { tone: days <= 1 ? ('danger' as const) : ('warning' as const), label, description: t('galleryCountdown.description'), cta: { type: 'upload' as const, label: t('galleryCountdown.ctaUpload'), }, }; } return null; }, [galleryLimits, t]); const handleCountdownCta = React.useCallback(() => { if (!galleryCountdown?.cta || !token) { return; } if (galleryCountdown.cta.type === 'upload') { navigate(`/e/${encodeURIComponent(token)}/upload`); } }, [galleryCountdown?.cta, navigate, token]); const packageWarnings = React.useMemo(() => { const warnings: { id: string; tone: 'warning' | 'danger'; message: string }[] = []; if (photoLimits?.state === 'limit_reached' && typeof photoLimits.limit === 'number') { warnings.push({ id: 'photos-blocked', tone: 'danger', message: t('upload.limitReached') .replace('{used}', `${photoLimits.used}`) .replace('{max}', `${photoLimits.limit}`), }); } else if ( photoLimits?.state === 'warning' && typeof photoLimits.remaining === 'number' && typeof photoLimits.limit === 'number' ) { warnings.push({ id: 'photos-warning', tone: 'warning', message: t('upload.limitWarning') .replace('{remaining}', `${photoLimits.remaining}`) .replace('{max}', `${photoLimits.limit}`), }); } if (galleryLimits?.state === 'expired') { warnings.push({ id: 'gallery-expired', tone: 'danger', message: t('upload.errors.galleryExpired'), }); } else if (galleryLimits?.state === 'warning') { const days = Math.max(0, galleryLimits.days_remaining ?? 0); const key = days === 1 ? 'upload.galleryWarningDay' : 'upload.galleryWarningDays'; warnings.push({ id: 'gallery-warning', tone: 'warning', message: t(key).replace('{days}', `${days}`), }); } return warnings; }, [photoLimits, galleryLimits, t]); const formatDate = React.useCallback((value: string | null) => { if (!value) return null; const date = new Date(value); if (Number.isNaN(date.getTime())) return null; try { return new Intl.DateTimeFormat(locale, { day: '2-digit', month: 'short', year: 'numeric' }).format(date); } catch { return date.toISOString().slice(0, 10); } }, [locale]); async function onLike(id: number) { if (liked.has(id)) return; setLiked(new Set(liked).add(id)); try { const c = await likePhoto(id); setCounts((m) => ({ ...m, [id]: c })); // keep a simple record of liked items try { const raw = localStorage.getItem('liked-photo-ids'); const arr: number[] = raw ? JSON.parse(raw) : []; if (!arr.includes(id)) localStorage.setItem('liked-photo-ids', JSON.stringify([...arr, id])); } catch {} } catch { const s = new Set(liked); s.delete(id); setLiked(s); } } if (!token) { return

Event nicht gefunden.

; } if (eventLoading) { return

Lade Event-Info...

; } return (
Galerie: {event?.name || 'Event'} {galleryCountdown && ( {galleryCountdown.label} )} {galleryCountdown?.cta && ( )}
{galleryCountdown && ( {galleryCountdown.description} )}
{packageWarnings.length > 0 && (
{packageWarnings.map((warning) => ( {warning.message} ))}
)}

Online Gäste

{stats?.onlineGuests || 0}

Gesamt Likes

{photos.reduce((sum, p) => sum + ((p as any).likes_count || 0), 0)}

Gesamt Fotos

{photos.length}

{eventPackage && (

Package

{eventPackage.package?.name ?? '—'}

{photoLimits?.limit ? ( <>

{photoLimits.used} / {photoLimits.limit} Fotos

) : (

{t('upload.limitUnlimited')}

)} {guestLimits?.limit ? (

Gäste: {guestLimits.used} / {guestLimits.limit}

) : null} {galleryLimits?.expires_at ? (

Galerie bis {formatDate(galleryLimits.expires_at)}

) : null}
)}
{newCount > 0 && ( {newCount} neue Fotos verfügbar.{' '} )} {loading &&

Lade…

}
{list.map((p: any) => { // Debug: Log image URLs const imgSrc = p.thumbnail_path || p.file_path; // Normalize image URL let imageUrl = imgSrc; let cleanPath = ''; if (imageUrl) { // Remove leading/trailing slashes for processing cleanPath = imageUrl.replace(/^\/+|\/+$/g, ''); // Check if path already contains storage prefix if (cleanPath.startsWith('storage/')) { // Already has storage prefix, just ensure it starts with / imageUrl = `/${cleanPath}`; } else { // Add storage prefix imageUrl = `/storage/${cleanPath}`; } // Remove double slashes imageUrl = imageUrl.replace(/\/+/g, '/'); } // Production: avoid heavy console logging for each image return (
{ const index = list.findIndex(photo => photo.id === p.id); setCurrentPhotoIndex(index >= 0 ? index : null); }} className="cursor-pointer" > {`Foto { (e.target as HTMLImageElement).src = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjRjNGNEY2Ii8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxNCIgZmlsbD0iIzk5OSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPk5vIEltYWdlPC90ZXh0Pjwvc3ZnPg=='; }} loading="lazy" />
{p.task_title && (

{p.task_title}

)}
{counts[p.id] ?? (p.likes_count || 0)}
); })}
{currentPhotoIndex !== null && list.length > 0 && ( setCurrentPhotoIndex(null)} onIndexChange={(index: number) => setCurrentPhotoIndex(index)} token={token} /> )} ); }