| @@ -6,10 +6,10 @@ const isVideoUrl = (url: string) => /\.(mp4|webm|mov|m4v|ogv)(\?|$)/i.test(url); | |||
| function MediaCarousel({ | |||
| items, | |||
| onImageClick, | |||
| onMediaClick, | |||
| }: { | |||
| items: MediaItem[]; | |||
| onImageClick?: (index: number) => void; | |||
| onMediaClick?: (index: number) => void; | |||
| }) { | |||
| const [current, setCurrent] = useState(0); | |||
| const touchStartX = useRef<number | null>(null); | |||
| @@ -70,21 +70,35 @@ function MediaCarousel({ | |||
| className={`absolute inset-0 transition-opacity duration-300 ${isActive ? 'opacity-100' : 'opacity-0 pointer-events-none'}`} | |||
| > | |||
| {video ? ( | |||
| <video | |||
| ref={el => { videoRefs.current[i] = el; }} | |||
| src={item.url} | |||
| className="w-full h-full object-contain bg-black" | |||
| controls | |||
| playsInline | |||
| muted={!!item.autoplay} | |||
| preload="metadata" | |||
| /> | |||
| <> | |||
| <video | |||
| ref={el => { videoRefs.current[i] = el; }} | |||
| src={item.url} | |||
| className="w-full h-full object-contain bg-black" | |||
| controls | |||
| controlsList="nofullscreen" | |||
| disablePictureInPicture | |||
| playsInline | |||
| muted={!!item.autoplay} | |||
| preload="metadata" | |||
| /> | |||
| <button | |||
| onClick={(e) => { e.stopPropagation(); onMediaClick?.(i); }} | |||
| className="absolute top-3 left-3 bg-black/60 hover:bg-black/80 text-white w-9 h-9 rounded-full flex items-center justify-center transition-colors shadow-lg z-10" | |||
| title="Expand fullscreen" | |||
| aria-label="Expand fullscreen" | |||
| > | |||
| <svg viewBox="0 0 24 24" className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"> | |||
| <path d="M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5" /> | |||
| </svg> | |||
| </button> | |||
| </> | |||
| ) : ( | |||
| <img | |||
| src={item.url} | |||
| alt="" | |||
| className="w-full h-full object-cover cursor-zoom-in" | |||
| onClick={() => onImageClick?.(i)} | |||
| onClick={() => onMediaClick?.(i)} | |||
| title="Click to view fullscreen" | |||
| /> | |||
| )} | |||
| @@ -178,6 +192,8 @@ function FullscreenViewer({ | |||
| src={item.url} | |||
| className="max-w-full max-h-full" | |||
| controls | |||
| controlsList="nofullscreen" | |||
| disablePictureInPicture | |||
| playsInline | |||
| autoPlay={!!item.autoplay} | |||
| muted={!!item.autoplay} | |||
| @@ -316,7 +332,7 @@ export default function PublicGrid({ cards, maxCols = 5 }: { cards: Card[], maxC | |||
| <div className="relative"> | |||
| <MediaCarousel | |||
| items={carouselItems} | |||
| onImageClick={(i) => setFullscreenIndex(i)} | |||
| onMediaClick={(i) => setFullscreenIndex(i)} | |||
| /> | |||
| <button | |||
| onClick={() => setActiveCard(null)} | |||