'use client'; import { useState, useEffect } from 'react'; import Link from 'next/link'; import { useSearchParams } from 'next/navigation'; import { useLanguage } from '@/context/LanguageContext'; import { useAuth } from '@/context/AuthContext'; import { photosApi, PhotoGallery, Photo } from '@/lib/api'; import Button from '@/components/ui/Button'; import GallerySkeleton from '@/components/gallery/GallerySkeleton'; import PhotoTile from '@/components/gallery/PhotoTile'; import { GalleryContainer, GalleryHeroFrame, MasonryGrid, } from '@/components/gallery/GalleryLayout'; import { useDownloads } from '@/components/gallery/useDownloads'; import Lightbox from '@/components/Lightbox'; import LoginModal from '@/components/LoginModal'; import { CalendarIcon, CameraIcon, LinkIcon, LockClosedIcon, TicketIcon, } from '@heroicons/react/24/outline'; interface GalleryClientProps { /** Fetch by gallery slug (standalone galleries at /photos/[slug]) … */ slug?: string; /** … or by event slug (event galleries at /events/[slug]/gallery). */ eventSlug?: string; initial: { gallery: PhotoGallery; photos: Photo[] } | null; } type DeniedState = 'login' | 'ticket' | 'private' | 'link' | 'notfound' | null; export default function GalleryClient({ slug, eventSlug, initial }: GalleryClientProps) { const { locale } = useLanguage(); const es = locale === 'es'; const { user, isLoading: authLoading } = useAuth(); const searchParams = useSearchParams(); const shareToken = searchParams.get('token') || undefined; const [gallery, setGallery] = useState(initial?.gallery ?? null); const [photos, setPhotos] = useState(initial?.photos ?? []); const [loading, setLoading] = useState(!initial); const [denied, setDenied] = useState(null); const [lightboxIndex, setLightboxIndex] = useState(null); const [loginOpen, setLoginOpen] = useState(false); const downloads = useDownloads(es); const downloadLabels = { download: es ? 'Descargar' : 'Download', downloading: es ? 'Descargando…' : 'Downloading…', }; const downloadPhoto = (photo: Photo) => downloads.start({ id: photo.id, url: photo.urls.original, filename: photo.originalFilename, }); // Server-rendered public galleries need no client fetch. Everything else // (link/ticket/private) is fetched here with the share token and/or the // viewer's own auth token attached. useEffect(() => { if (initial) return; if (authLoading) return; // wait until the session state has resolved let cancelled = false; setLoading(true); const fetcher = eventSlug ? photosApi.getEventGallery(eventSlug, shareToken) : photosApi.getPublicGallery(slug || '', shareToken); fetcher .then((data) => { if (cancelled) return; setGallery(data.gallery); setPhotos(data.photos); setDenied(null); }) .catch((err: Error) => { if (cancelled) return; // The photo-api returns a distinct message per visibility mode so // each gets its own gate page (see accessDenial in access.go). const msg = err.message || ''; if (msg.includes('Authentication required')) setDenied('login'); else if (msg.includes('attendees')) setDenied('ticket'); else if (msg.includes('private')) setDenied('private'); else if (msg.includes('share link')) setDenied('link'); else setDenied('notfound'); }) .finally(() => !cancelled && setLoading(false)); return () => { cancelled = true; }; }, [slug, eventSlug, shareToken, initial, authLoading, user?.id]); // Mirrors the hero + masonry layout below, so the real page drops straight // into the placeholder's geometry instead of replacing it. if (loading || (authLoading && !initial)) { return ; } // Gate pages for restricted galleries. After a successful login in the // pop-up, AuthContext's user changes, which re-runs the fetch effect — // the gate resolves by itself when access is granted. const loginModal = ( setLoginOpen(false)} message={ es ? 'Inicia sesión para ver esta galería.' : 'Log in to view this gallery.' } /> ); if (denied === 'login') { return ( <> setLoginOpen(true)}>{es ? 'Iniciar sesión' : 'Log in'} } /> {loginModal} ); } if (denied === 'ticket') { return ( <> {(eventSlug || gallery?.event) && ( )} } /> {loginModal} ); } if (denied === 'private') { return ( <> } /> {loginModal} ); } if (denied === 'link') { return ( <> } /> {loginModal} ); } if (denied === 'notfound' || !gallery) { return ( } /> ); } const readyPhotos = photos.filter((p) => p.status === 'ready' && p.urls.thumb); const lightboxItems = readyPhotos.map((p) => ({ id: p.id, previewUrl: p.urls.preview || p.urls.original, downloadUrl: p.urls.original, filename: p.originalFilename, thumbUrl: p.urls.thumb, })); const title = es && gallery.titleEs ? gallery.titleEs : gallery.title; const description = es && gallery.descriptionEs ? gallery.descriptionEs : gallery.description; const eventTitle = gallery.event ? es && gallery.event.titleEs ? gallery.event.titleEs : gallery.event.title : null; const eventDate = gallery.event?.startDatetime ? new Date(gallery.event.startDatetime).toLocaleDateString(es ? 'es-ES' : 'en-US', { year: 'numeric', month: 'long', day: 'numeric', timeZone: 'America/Asuncion', }) : null; // Hero backdrop: the cover photo's preview when available, else the // first photo. Falls back to a navy gradient with no image. const coverPhoto = readyPhotos.find((p) => p.id === gallery.coverPhotoId) || readyPhotos[0] || null; const heroUrl = coverPhoto ? coverPhoto.urls.preview || coverPhoto.urls.thumb : null; return (
{/* Hero — same frame the skeleton renders (GalleryLayout). */} {/* eslint-disable-next-line @next/next/no-img-element */}
) : null } >

{title}

{description && (

{description}

)}
{readyPhotos.length} {es ? 'fotos' : 'photos'} {gallery.event && ( {eventTitle} {eventDate && · {eventDate}} )}
{/* Masonry grid */} {readyPhotos.length === 0 ? (

{es ? 'Las fotos estarán disponibles pronto.' : 'Photos will be available soon.'}

) : ( {readyPhotos.map((photo, i) => ( setLightboxIndex(i)} onDownload={() => downloadPhoto(photo)} downloading={downloads.isPending(photo.id)} labels={downloadLabels} /> ))} )} {lightboxIndex !== null && ( setLightboxIndex(null)} onNavigate={setLightboxIndex} onDownload={(it) => downloads.start({ id: it.id, url: it.downloadUrl, filename: it.filename }) } downloadingId={ lightboxItems.find((it) => downloads.isPending(it.id))?.id ?? null } downloadLabels={downloadLabels} /> )}
{/* Call to action: send attendees to their dashboard, everyone else to the next event. Auth state comes from the same useAuth() the gate pages use. */}

{user ? es ? '¿Listo para lo que sigue?' : 'Ready for what’s next?' : es ? '¿Te gustó lo que viste?' : 'Liked what you saw?'}

{user ? es ? 'Revisa tus entradas y próximos eventos en tu panel.' : 'Check your tickets and upcoming events from your dashboard.' : es ? 'Únete a nuestro próximo evento y sé parte de las próximas fotos.' : 'Join our next event and be part of the next set of photos.'}

); } function GateMessage({ icon: Icon, title, body, action, }: { icon: typeof CameraIcon; title: string; body: string; action?: React.ReactNode; }) { return (

{title}

{body}

{action}
); }