| @@ -12,9 +12,20 @@ function MediaCarousel({ | |||||
| onMediaClick?: (index: number) => void; | onMediaClick?: (index: number) => void; | ||||
| }) { | }) { | ||||
| const [current, setCurrent] = useState(0); | const [current, setCurrent] = useState(0); | ||||
| const [playing, setPlaying] = useState<Set<number>>(new Set()); | |||||
| const touchStartX = useRef<number | null>(null); | const touchStartX = useRef<number | null>(null); | ||||
| const videoRefs = useRef<Record<number, HTMLVideoElement | null>>({}); | const videoRefs = useRef<Record<number, HTMLVideoElement | null>>({}); | ||||
| const markPlaying = (i: number) => setPlaying(p => { const n = new Set(p); n.add(i); return n; }); | |||||
| const markPaused = (i: number) => setPlaying(p => { const n = new Set(p); n.delete(i); return n; }); | |||||
| const togglePlay = (i: number) => { | |||||
| const v = videoRefs.current[i]; | |||||
| if (!v) return; | |||||
| if (v.paused) v.play().catch(() => {}); | |||||
| else v.pause(); | |||||
| }; | |||||
| const prev = useCallback(() => setCurrent(i => (i - 1 + items.length) % items.length), [items.length]); | const prev = useCallback(() => setCurrent(i => (i - 1 + items.length) % items.length), [items.length]); | ||||
| const next = useCallback(() => setCurrent(i => (i + 1) % items.length), [items.length]); | const next = useCallback(() => setCurrent(i => (i + 1) % items.length), [items.length]); | ||||
| @@ -70,18 +81,28 @@ 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 ? ( | ||||
| <> | |||||
| <div | |||||
| className="relative w-full h-full cursor-pointer" | |||||
| onClick={(e) => { e.stopPropagation(); togglePlay(i); }} | |||||
| > | |||||
| <video | <video | ||||
| ref={el => { videoRefs.current[i] = el; }} | ref={el => { videoRefs.current[i] = el; }} | ||||
| src={item.url} | src={item.url} | ||||
| className="w-full h-full object-contain bg-black" | |||||
| controls | |||||
| controlsList="nofullscreen" | |||||
| disablePictureInPicture | |||||
| className="w-full h-full object-contain bg-black pointer-events-none" | |||||
| playsInline | playsInline | ||||
| muted={!!item.autoplay} | muted={!!item.autoplay} | ||||
| preload="metadata" | preload="metadata" | ||||
| onPlay={() => markPlaying(i)} | |||||
| onPause={() => markPaused(i)} | |||||
| onEnded={() => markPaused(i)} | |||||
| /> | /> | ||||
| {/* Custom play overlay (shown when paused) */} | |||||
| {!playing.has(i) && ( | |||||
| <div className="absolute inset-0 flex items-center justify-center pointer-events-none"> | |||||
| <span className="bg-black/60 text-white w-20 h-20 rounded-full flex items-center justify-center text-4xl shadow-2xl pl-1">▶</span> | |||||
| </div> | |||||
| )} | |||||
| {/* Custom expand button */} | |||||
| <button | <button | ||||
| onClick={(e) => { e.stopPropagation(); onMediaClick?.(i); }} | 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" | 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" | ||||
| @@ -92,7 +113,7 @@ function MediaCarousel({ | |||||
| <path d="M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5" /> | <path d="M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5" /> | ||||
| </svg> | </svg> | ||||
| </button> | </button> | ||||
| </> | |||||
| </div> | |||||
| ) : ( | ) : ( | ||||
| <img | <img | ||||
| src={item.url} | src={item.url} | ||||
| @@ -149,7 +170,19 @@ function FullscreenViewer({ | |||||
| onClose: () => void; | onClose: () => void; | ||||
| }) { | }) { | ||||
| const [current, setCurrent] = useState(startIndex); | const [current, setCurrent] = useState(startIndex); | ||||
| const [playing, setPlaying] = useState<Set<number>>(new Set()); | |||||
| const touchStartX = useRef<number | null>(null); | const touchStartX = useRef<number | null>(null); | ||||
| const videoRefs = useRef<Record<number, HTMLVideoElement | null>>({}); | |||||
| const markPlaying = (i: number) => setPlaying(p => { const n = new Set(p); n.add(i); return n; }); | |||||
| const markPaused = (i: number) => setPlaying(p => { const n = new Set(p); n.delete(i); return n; }); | |||||
| const togglePlay = (i: number) => { | |||||
| const v = videoRefs.current[i]; | |||||
| if (!v) return; | |||||
| if (v.paused) v.play().catch(() => {}); | |||||
| else v.pause(); | |||||
| }; | |||||
| const prev = useCallback(() => setCurrent(i => (i - 1 + items.length) % items.length), [items.length]); | const prev = useCallback(() => setCurrent(i => (i - 1 + items.length) % items.length), [items.length]); | ||||
| const next = useCallback(() => setCurrent(i => (i + 1) % items.length), [items.length]); | const next = useCallback(() => setCurrent(i => (i + 1) % items.length), [items.length]); | ||||
| @@ -188,16 +221,27 @@ function FullscreenViewer({ | |||||
| className={`absolute inset-0 flex items-center justify-center p-4 pb-28 transition-opacity duration-200 ${isActive ? 'opacity-100' : 'opacity-0 pointer-events-none'}`} | className={`absolute inset-0 flex items-center justify-center p-4 pb-28 transition-opacity duration-200 ${isActive ? 'opacity-100' : 'opacity-0 pointer-events-none'}`} | ||||
| > | > | ||||
| {video ? ( | {video ? ( | ||||
| <video | |||||
| src={item.url} | |||||
| className="max-w-full max-h-full" | |||||
| controls | |||||
| controlsList="nofullscreen" | |||||
| disablePictureInPicture | |||||
| playsInline | |||||
| autoPlay={!!item.autoplay} | |||||
| muted={!!item.autoplay} | |||||
| /> | |||||
| <div | |||||
| className="relative flex items-center justify-center max-w-full max-h-full cursor-pointer" | |||||
| onClick={(e) => { e.stopPropagation(); togglePlay(i); }} | |||||
| > | |||||
| <video | |||||
| ref={el => { videoRefs.current[i] = el; }} | |||||
| src={item.url} | |||||
| className="max-w-full max-h-full pointer-events-none" | |||||
| playsInline | |||||
| autoPlay={!!item.autoplay} | |||||
| muted={!!item.autoplay} | |||||
| onPlay={() => markPlaying(i)} | |||||
| onPause={() => markPaused(i)} | |||||
| onEnded={() => markPaused(i)} | |||||
| /> | |||||
| {!playing.has(i) && ( | |||||
| <div className="absolute inset-0 flex items-center justify-center pointer-events-none"> | |||||
| <span className="bg-black/60 text-white w-24 h-24 rounded-full flex items-center justify-center text-5xl shadow-2xl pl-1">▶</span> | |||||
| </div> | |||||
| )} | |||||
| </div> | |||||
| ) : ( | ) : ( | ||||
| <img | <img | ||||
| src={item.url} | src={item.url} | ||||
| @@ -304,7 +348,7 @@ export default function PublicGrid({ cards, maxCols = 5 }: { cards: Card[], maxC | |||||
| <div className="absolute inset-0 flex items-center justify-center bg-gradient-to-br from-blue-100 to-gray-200 text-gray-400">No Image</div> | <div className="absolute inset-0 flex items-center justify-center bg-gradient-to-br from-blue-100 to-gray-200 text-gray-400">No Image</div> | ||||
| )} | )} | ||||
| {galleryCount > 1 && ( | {galleryCount > 1 && ( | ||||
| <div className="absolute top-2 right-2 bg-black/60 text-white text-xs font-semibold px-1.5 py-0.5 rounded-full flex items-center gap-1"> | |||||
| <div className="absolute top-2 left-2 bg-black/60 text-white text-xs font-semibold px-1.5 py-0.5 rounded-full flex items-center gap-1 z-10"> | |||||
| <span>⊞</span> | <span>⊞</span> | ||||
| <span>{galleryCount}</span> | <span>{galleryCount}</span> | ||||
| </div> | </div> | ||||