Showing
1 changed file
with
41 additions
and
24 deletions
| @@ -12,6 +12,8 @@ interface ImageCardProps { | @@ -12,6 +12,8 @@ interface ImageCardProps { | ||
| 12 | 12 | ||
| 13 | const ImageCard: React.FC<ImageCardProps> = ({ image, onClick, onLike, currentUser, isVideo = false }) => { | 13 | const ImageCard: React.FC<ImageCardProps> = ({ image, onClick, onLike, currentUser, isVideo = false }) => { |
| 14 | const [isLoaded, setIsLoaded] = useState(false); | 14 | const [isLoaded, setIsLoaded] = useState(false); |
| 15 | + const [isHovered, setIsHovered] = useState(false); | ||
| 16 | + const [videoStarted, setVideoStarted] = useState(false); | ||
| 15 | const videoRef = useRef<HTMLVideoElement>(null); | 17 | const videoRef = useRef<HTMLVideoElement>(null); |
| 16 | 18 | ||
| 17 | const handleLike = (e: React.MouseEvent) => { | 19 | const handleLike = (e: React.MouseEvent) => { |
| @@ -20,60 +22,75 @@ const ImageCard: React.FC<ImageCardProps> = ({ image, onClick, onLike, currentUs | @@ -20,60 +22,75 @@ const ImageCard: React.FC<ImageCardProps> = ({ image, onClick, onLike, currentUs | ||
| 20 | }; | 22 | }; |
| 21 | 23 | ||
| 22 | const handleMouseEnter = () => { | 24 | const handleMouseEnter = () => { |
| 23 | - if (videoRef.current) { | ||
| 24 | - videoRef.current.currentTime = 0; | ||
| 25 | - videoRef.current.play().catch(e => console.error("Video play failed", e)); | ||
| 26 | - } | 25 | + setIsHovered(true); |
| 26 | + // Video will be played via autoPlay once rendered | ||
| 27 | } | 27 | } |
| 28 | 28 | ||
| 29 | const handleMouseLeave = () => { | 29 | const handleMouseLeave = () => { |
| 30 | - if (videoRef.current) { | ||
| 31 | - videoRef.current.pause(); | ||
| 32 | - videoRef.current.currentTime = videoRef.current.duration || 0; | ||
| 33 | - } | ||
| 34 | - } | ||
| 35 | - | ||
| 36 | - const handleVideoMetadata = () => { | ||
| 37 | - if (videoRef.current) { | ||
| 38 | - videoRef.current.currentTime = videoRef.current.duration || 0; | ||
| 39 | - } | 30 | + setIsHovered(false); |
| 31 | + setVideoStarted(false); | ||
| 40 | } | 32 | } |
| 41 | 33 | ||
| 42 | - // If it's a video with a thumbnail, we want to show it immediately (via poster) | ||
| 43 | - // instead of waiting for the video file to load metadata. | 34 | + // Content is considered "ready" if the main image/video is loaded |
| 35 | + // OR if we have a thumbnail to show for a video. | ||
| 44 | const showContent = isLoaded || (isVideo && !!image.thumbnail); | 36 | const showContent = isLoaded || (isVideo && !!image.thumbnail); |
| 45 | 37 | ||
| 46 | return ( | 38 | return ( |
| 47 | <div | 39 | <div |
| 48 | - className="group relative mb-4 break-inside-avoid rounded-2xl overflow-hidden bg-gray-200 cursor-zoom-in shadow-sm hover:shadow-xl transition-all duration-300" | 40 | + className="group relative mb-4 break-inside-avoid rounded-2xl overflow-hidden bg-gray-100 cursor-zoom-in shadow-sm hover:shadow-xl transition-all duration-300" |
| 49 | onClick={() => onClick(image)} | 41 | onClick={() => onClick(image)} |
| 50 | onMouseEnter={handleMouseEnter} | 42 | onMouseEnter={handleMouseEnter} |
| 51 | onMouseLeave={handleMouseLeave} | 43 | onMouseLeave={handleMouseLeave} |
| 52 | > | 44 | > |
| 53 | {/* Placeholder / Skeleton */} | 45 | {/* Placeholder / Skeleton */} |
| 54 | {!showContent && ( | 46 | {!showContent && ( |
| 55 | - <div className="absolute inset-0 bg-gray-200 animate-pulse min-h-[200px]" /> | 47 | + <div className="absolute inset-0 bg-gray-200 animate-pulse min-h-[150px]" /> |
| 56 | )} | 48 | )} |
| 57 | 49 | ||
| 58 | {/* Main Content */} | 50 | {/* Main Content */} |
| 59 | {isVideo ? ( | 51 | {isVideo ? ( |
| 52 | + <div className="relative w-full overflow-hidden bg-gray-100"> | ||
| 53 | + {/* Always show thumbnail first */} | ||
| 54 | + <img | ||
| 55 | + src={image.thumbnail || (isVideo ? '' : image.url)} | ||
| 56 | + alt={image.prompt} | ||
| 57 | + className={`w-full h-auto object-cover transition-opacity duration-500 ${videoStarted ? 'opacity-0' : 'opacity-100'} ${!image.thumbnail && isVideo ? 'opacity-0' : ''}`} | ||
| 58 | + onLoad={() => setIsLoaded(true)} | ||
| 59 | + loading="lazy" | ||
| 60 | + /> | ||
| 61 | + | ||
| 62 | + {isVideo && !image.thumbnail && !videoStarted && ( | ||
| 63 | + <div className="absolute inset-0 flex items-center justify-center bg-gray-200 text-gray-400"> | ||
| 64 | + <Video size={32} /> | ||
| 65 | + </div> | ||
| 66 | + )} | ||
| 67 | + | ||
| 68 | + {/* Load video only on hover */} | ||
| 69 | + {isHovered && ( | ||
| 60 | <video | 70 | <video |
| 61 | ref={videoRef} | 71 | ref={videoRef} |
| 62 | src={image.url} | 72 | src={image.url} |
| 63 | - poster={image.thumbnail} // Use thumbnail as poster | ||
| 64 | - className={`w-full h-auto object-cover transition-opacity duration-300 ${showContent ? 'opacity-100' : 'opacity-0'}`} | ||
| 65 | - onLoadedMetadata={handleVideoMetadata} | ||
| 66 | - onLoadedData={() => setIsLoaded(true)} | 73 | + className={`absolute inset-0 w-full h-full object-cover transition-opacity duration-300 ${videoStarted ? 'opacity-100' : 'opacity-0'}`} |
| 74 | + onLoadedData={() => setVideoStarted(true)} | ||
| 75 | + autoPlay | ||
| 67 | loop | 76 | loop |
| 68 | muted | 77 | muted |
| 69 | playsInline | 78 | playsInline |
| 70 | - preload="metadata" | ||
| 71 | /> | 79 | /> |
| 80 | + )} | ||
| 81 | + | ||
| 82 | + {/* Video Indicator Icon (When not hovered) */} | ||
| 83 | + {!isHovered && ( | ||
| 84 | + <div className="absolute top-3 left-3 p-1.5 bg-black/30 backdrop-blur-md rounded-lg text-white/90"> | ||
| 85 | + <Video size={14} /> | ||
| 86 | + </div> | ||
| 87 | + )} | ||
| 88 | + </div> | ||
| 72 | ) : ( | 89 | ) : ( |
| 73 | <img | 90 | <img |
| 74 | src={image.url} | 91 | src={image.url} |
| 75 | alt={image.prompt} | 92 | alt={image.prompt} |
| 76 | - className={`w-full h-auto object-cover transition-transform duration-700 ease-in-out group-hover:scale-105 ${showContent ? 'opacity-100' : 'opacity-0'}`} | 93 | + className={`w-full h-auto object-cover transition-transform duration-700 ease-in-out group-hover:scale-105 ${isLoaded ? 'opacity-100' : 'opacity-0'}`} |
| 77 | onLoad={() => setIsLoaded(true)} | 94 | onLoad={() => setIsLoaded(true)} |
| 78 | loading="lazy" | 95 | loading="lazy" |
| 79 | /> | 96 | /> |
-
Please register or login to post a comment