| @@ -6,10 +6,10 @@ const isVideoUrl = (url: string) => /\.(mp4|webm|mov|m4v|ogv)(\?|$)/i.test(url); | |||||
| function MediaCarousel({ | function MediaCarousel({ | ||||
| items, | items, | ||||
| onImageClick, | |||||
| onMediaClick, | |||||
| }: { | }: { | ||||
| items: MediaItem[]; | items: MediaItem[]; | ||||
| onImageClick?: (index: number) => void; | |||||
| onMediaClick?: (index: number) => void; | |||||
| }) { | }) { | ||||
| const [current, setCurrent] = useState(0); | const [current, setCurrent] = useState(0); | ||||
| const touchStartX = useRef<number | null>(null); | 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'}`} | className={`absolute inset-0 transition-opacity duration-300 ${isActive ? 'opacity-100' : 'opacity-0 pointer-events-none'}`} | ||||
| > | > | ||||
| {video ? ( | {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 | <img | ||||
| src={item.url} | src={item.url} | ||||
| alt="" | alt="" | ||||
| className="w-full h-full object-cover cursor-zoom-in" | className="w-full h-full object-cover cursor-zoom-in" | ||||
| onClick={() => onImageClick?.(i)} | |||||
| onClick={() => onMediaClick?.(i)} | |||||
| title="Click to view fullscreen" | title="Click to view fullscreen" | ||||
| /> | /> | ||||
| )} | )} | ||||
| @@ -178,6 +192,8 @@ function FullscreenViewer({ | |||||
| src={item.url} | src={item.url} | ||||
| className="max-w-full max-h-full" | className="max-w-full max-h-full" | ||||
| controls | controls | ||||
| controlsList="nofullscreen" | |||||
| disablePictureInPicture | |||||
| playsInline | playsInline | ||||
| autoPlay={!!item.autoplay} | autoPlay={!!item.autoplay} | ||||
| muted={!!item.autoplay} | muted={!!item.autoplay} | ||||
| @@ -316,7 +332,7 @@ export default function PublicGrid({ cards, maxCols = 5 }: { cards: Card[], maxC | |||||
| <div className="relative"> | <div className="relative"> | ||||
| <MediaCarousel | <MediaCarousel | ||||
| items={carouselItems} | items={carouselItems} | ||||
| onImageClick={(i) => setFullscreenIndex(i)} | |||||
| onMediaClick={(i) => setFullscreenIndex(i)} | |||||
| /> | /> | ||||
| <button | <button | ||||
| onClick={() => setActiveCard(null)} | onClick={() => setActiveCard(null)} | ||||