import { useEffect, useState, type CSSProperties } from 'react'; import { AnimatePresence } from 'framer-motion'; import { downloadMedia, favoriteMedia, getDownloadAllStatus, startDownloadAll, } from '../../lib/api'; import { formatDuration } from '../../lib/format'; import type { DownloadJob, MediaItem, PublicGallery } from '../../types/gallery'; import { PhotoViewer } from './PhotoViewer'; interface ClientGalleryProps { gallery: PublicGallery; } export function ClientGallery({ gallery }: ClientGalleryProps) { const [items, setItems] = useState(gallery.media); const [viewerIndex, setViewerIndex] = useState(null); const [downloadJob, setDownloadJob] = useState(null); const photos = items.filter((item) => item.mimeType.startsWith('image/')); const cover = gallery.cover || photos[0]; const theme = gallery.themeConfig || {}; const branding = gallery.brandingConfig || {}; const style = { '--gallery-accent': theme.accent || '#a85e55' } as CSSProperties; useEffect(() => setItems(gallery.media), [gallery.media]); useEffect(() => { if (!downloadJob || !['QUEUED', 'PROCESSING'].includes(downloadJob.status)) return undefined; const timer = window.setTimeout(async () => { try { const updated = await getDownloadAllStatus(gallery.slug, downloadJob.jobId); setDownloadJob(updated); } catch (reason) { setDownloadJob((current) => current ? { ...current, status: 'FAILED', error: reason instanceof Error ? reason.message : 'Download failed.', } : current, ); } }, 1300); return () => window.clearTimeout(timer); }, [downloadJob, gallery.slug]); async function toggleFavorite(item: MediaItem) { if (gallery.favoritesEnabled === false) return; const next = !item.favorited; setItems((current) => current.map((candidate) => candidate.id === item.id ? { ...candidate, favorited: next } : candidate, ), ); try { await favoriteMedia(gallery.slug, item.id, next); } catch { setItems((current) => current.map((candidate) => candidate.id === item.id ? { ...candidate, favorited: !next } : candidate, ), ); } } async function downloadOne(item: MediaItem) { try { const url = await downloadMedia(gallery.slug, item.id); const anchor = document.createElement('a'); anchor.href = url; anchor.download = item.originalFilename; anchor.target = '_blank'; anchor.rel = 'noreferrer'; anchor.click(); } catch { // The client gallery stays usable if a single signed URL cannot be issued. } } async function downloadAll() { if (downloadJob?.status === 'QUEUED' || downloadJob?.status === 'PROCESSING') return; try { setDownloadJob(await startDownloadAll(gallery.slug)); } catch (reason) { setDownloadJob({ jobId: '', status: 'FAILED', error: reason instanceof Error ? reason.message : 'Download failed.', }); } } function openPhoto(item: MediaItem) { const index = photos.findIndex((photo) => photo.id === item.id); setViewerIndex(index >= 0 ? index : null); } const mode = theme.mode === 'dark' ? 'dark' : 'light'; const layout = theme.layout || 'editorial'; return (
{gallery.preview && (
Preview mode This is how your clients will see it
)}
{ if (!branding.websiteUrl) event.preventDefault(); }} > {branding.logoUrl ? ( ) : ( {branding.studioName?.slice(0, 1) || 'N'} )} {branding.studioName || 'Your studio'}
{gallery.downloadAllEnabled !== false && gallery.downloadsEnabled !== false && ( )} {gallery.clientName}

A collection for {gallery.clientName}

{gallery.title}

{gallery.description && (

{gallery.description}

)}
{cover?.previewUrl ? ( ) : (
{gallery.clientName.slice(0, 1)}
)}
Open to remember {items.length} pieces
Scroll to wander

The collection

The day, held still.

{photos.length} photographs /{' '} {items.filter((item) => item.mimeType.startsWith('video/')).length} films
{items.length === 0 ? (
+

Your gallery is still being arranged.

) : (
{items.map((item) => ( openPhoto(item)} onFavorite={() => void toggleFavorite(item)} onDownload={() => void downloadOne(item)} /> ))}
)}

Take it with you

The moments are
yours to keep.

Save the full-resolution photographs and revisit this chapter whenever you like.

{gallery.downloadsEnabled !== false && ( )} {downloadJob?.status === 'READY' && downloadJob.url && ( Your ZIP is ready )} {downloadJob?.status === 'FAILED' && (

{downloadJob.error || 'The download could not be prepared.'}

)}
{branding.studioName?.slice(0, 1) || 'N'}
{branding.studioName || 'Your studio'} {branding.tagline || 'Photographs for keeps.'}
{branding.websiteUrl && ( Website ↗ )} {branding.instagramUrl && ( Instagram ↗ )}
Delivered with intention
{viewerIndex !== null && ( setViewerIndex(null)} onChange={setViewerIndex} onFavorite={(item) => void toggleFavorite(item)} onDownload={(item) => void downloadOne(item)} /> )}
); } function MediaCard({ item, gallery, onOpen, onFavorite, onDownload, }: { item: MediaItem; gallery: PublicGallery; onOpen: () => void; onFavorite: () => void; onDownload: () => void; }) { const isVideo = item.mimeType.startsWith('video/'); const ready = item.processingStatus === 'READY'; return (
{isVideo ? (
{ready && item.previewUrl ? (
) : ( )}
{item.originalFilename} {gallery.favoritesEnabled !== false && ( )} {gallery.downloadsEnabled !== false && ( )}
); }