diff --git a/components/PublicGrid.tsx b/components/PublicGrid.tsx index 7bdf4d1..8161a5f 100644 --- a/components/PublicGrid.tsx +++ b/components/PublicGrid.tsx @@ -4,7 +4,13 @@ import { Card, MediaItem } from '@/types'; const isVideoUrl = (url: string) => /\.(mp4|webm|mov|m4v|ogv)(\?|$)/i.test(url); -function MediaCarousel({ items }: { items: MediaItem[] }) { +function MediaCarousel({ + items, + onImageClick, +}: { + items: MediaItem[]; + onImageClick?: (index: number) => void; +}) { const [current, setCurrent] = useState(0); const touchStartX = useRef(null); const videoRefs = useRef>({}); @@ -23,26 +29,21 @@ function MediaCarousel({ items }: { items: MediaItem[] }) { useEffect(() => { setCurrent(0); }, [items]); - // Pause all videos that aren't current; autoplay current if flagged + // Pause non-current videos; autoplay current if flagged useEffect(() => { Object.entries(videoRefs.current).forEach(([key, vid]) => { if (!vid) return; const idx = parseInt(key, 10); - if (idx !== current) { - vid.pause(); - return; - } + if (idx !== current) { vid.pause(); return; } const item = items[idx]; if (item && isVideoUrl(item.url) && item.autoplay) { vid.muted = true; - vid.play().catch(() => {/* autoplay blocked, ignore */}); + vid.play().catch(() => {}); } }); }, [current, items]); - const onTouchStart = (e: React.TouchEvent) => { - touchStartX.current = e.touches[0].clientX; - }; + const onTouchStart = (e: React.TouchEvent) => { touchStartX.current = e.touches[0].clientX; }; const onTouchEnd = (e: React.TouchEvent) => { if (touchStartX.current === null) return; const delta = e.changedTouches[0].clientX - touchStartX.current; @@ -79,7 +80,13 @@ function MediaCarousel({ items }: { items: MediaItem[] }) { preload="metadata" /> ) : ( - + onImageClick?.(i)} + title="Click to view fullscreen" + /> )} ); @@ -118,14 +125,133 @@ function MediaCarousel({ items }: { items: MediaItem[] }) { ); } +function FullscreenViewer({ + items, + startIndex, + onClose, +}: { + items: MediaItem[]; + startIndex: number; + onClose: () => void; +}) { + const [current, setCurrent] = useState(startIndex); + const touchStartX = useRef(null); + + const prev = useCallback(() => setCurrent(i => (i - 1 + items.length) % items.length), [items.length]); + const next = useCallback(() => setCurrent(i => (i + 1) % items.length), [items.length]); + + useEffect(() => { + const onKey = (e: KeyboardEvent) => { + if (e.key === 'Escape') onClose(); + else if (e.key === 'ArrowLeft') prev(); + else if (e.key === 'ArrowRight') next(); + }; + window.addEventListener('keydown', onKey); + return () => window.removeEventListener('keydown', onKey); + }, [prev, next, onClose]); + + const onTouchStart = (e: React.TouchEvent) => { touchStartX.current = e.touches[0].clientX; }; + const onTouchEnd = (e: React.TouchEvent) => { + if (touchStartX.current === null) return; + const delta = e.changedTouches[0].clientX - touchStartX.current; + if (Math.abs(delta) > 50) delta < 0 ? next() : prev(); + touchStartX.current = null; + }; + + return ( +
+ {/* Media — full resolution, contained */} + {items.map((item, i) => { + const isActive = i === current; + const video = isVideoUrl(item.url); + return ( +
+ {video ? ( +
+ ); + })} + + {/* Counter — top center */} + {items.length > 1 && ( +
+ {current + 1} / {items.length} +
+ )} + + {/* Side arrows */} + {items.length > 1 && ( + <> + + + + )} + + {/* Close button — bottom center, ABOVE the dots */} + + + {/* Dots — at the very bottom */} + {items.length > 1 && ( +
+ {items.map((_, i) => ( +
+ )} +
+ ); +} + export default function PublicGrid({ cards, maxCols = 5 }: { cards: Card[], maxCols?: number }) { const [activeCard, setActiveCard] = useState(null); + const [fullscreenIndex, setFullscreenIndex] = useState(null); useEffect(() => { - if (activeCard) document.body.style.overflow = 'hidden'; + if (activeCard || fullscreenIndex !== null) document.body.style.overflow = 'hidden'; else document.body.style.overflow = 'unset'; return () => { document.body.style.overflow = 'unset'; }; - }, [activeCard]); + }, [activeCard, fullscreenIndex]); const gridClasses: Record = { 3: 'xl:grid-cols-3 lg:grid-cols-3 md:grid-cols-2 grid-cols-1', @@ -138,6 +264,13 @@ export default function PublicGrid({ cards, maxCols = 5 }: { cards: Card[], maxC const activeGridClass = gridClasses[maxCols] || gridClasses[5]; + const carouselItems: MediaItem[] = activeCard + ? [ + ...(activeCard.imageUrl ? [{ url: activeCard.imageUrl }] : []), + ...(activeCard.extraMedia || []), + ] + : []; + return ( <>
@@ -182,10 +315,8 @@ export default function PublicGrid({ cards, maxCols = 5 }: { cards: Card[], maxC >
setFullscreenIndex(i)} />
)} + + {fullscreenIndex !== null && activeCard && ( + setFullscreenIndex(null)} + /> + )} ); }