Add an interactive popup to video cards on hover
Implement a hover effect on video cards to display an interactive popup with video details and action buttons. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 2eb1084e-b728-4449-9231-f1665924c8d5 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/8cc42625-c1f5-4e43-99bd-77f2c4dedee2/2eb1084e-b728-4449-9231-f1665924c8d5/P3O2FU7
This commit is contained in:
parent
9579c54ae4
commit
c631dd9f96
BIN
attached_assets/image_1756463985155.png
Normal file
BIN
attached_assets/image_1756463985155.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 618 KiB |
@ -1,4 +1,4 @@
|
|||||||
import { Play } from "lucide-react";
|
import { Play, Plus, ThumbsUp, ChevronDown } from "lucide-react";
|
||||||
import { type Video } from "@shared/schema";
|
import { type Video } from "@shared/schema";
|
||||||
import HLSPreviewThumbnail from "./hls-preview-thumbnail";
|
import HLSPreviewThumbnail from "./hls-preview-thumbnail";
|
||||||
import { useState, useRef, useEffect } from "react";
|
import { useState, useRef, useEffect } from "react";
|
||||||
@ -47,7 +47,9 @@ function formatDate(date: Date | string): string {
|
|||||||
export default function VideoCard({ video, onClick, className = "" }: VideoCardProps) {
|
export default function VideoCard({ video, onClick, className = "" }: VideoCardProps) {
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
const [showPreview, setShowPreview] = useState(false);
|
const [showPreview, setShowPreview] = useState(false);
|
||||||
|
const [showPopup, setShowPopup] = useState(false);
|
||||||
const hoverTimeoutRef = useRef<NodeJS.Timeout>();
|
const hoverTimeoutRef = useRef<NodeJS.Timeout>();
|
||||||
|
const popupTimeoutRef = useRef<NodeJS.Timeout>();
|
||||||
const videoRef = useRef<HTMLVideoElement>(null);
|
const videoRef = useRef<HTMLVideoElement>(null);
|
||||||
const hlsRef = useRef<any>(null);
|
const hlsRef = useRef<any>(null);
|
||||||
|
|
||||||
@ -87,17 +89,29 @@ export default function VideoCard({ video, onClick, className = "" }: VideoCardP
|
|||||||
hoverTimeoutRef.current = setTimeout(() => {
|
hoverTimeoutRef.current = setTimeout(() => {
|
||||||
setShowPreview(true);
|
setShowPreview(true);
|
||||||
}, 800); // Start preview after 800ms hover
|
}, 800); // Start preview after 800ms hover
|
||||||
|
|
||||||
|
// Show popup after additional delay
|
||||||
|
popupTimeoutRef.current = setTimeout(() => {
|
||||||
|
setShowPopup(true);
|
||||||
|
}, 1200); // Show popup after 1.2s hover
|
||||||
} else {
|
} else {
|
||||||
if (hoverTimeoutRef.current) {
|
if (hoverTimeoutRef.current) {
|
||||||
clearTimeout(hoverTimeoutRef.current);
|
clearTimeout(hoverTimeoutRef.current);
|
||||||
}
|
}
|
||||||
|
if (popupTimeoutRef.current) {
|
||||||
|
clearTimeout(popupTimeoutRef.current);
|
||||||
|
}
|
||||||
setShowPreview(false);
|
setShowPreview(false);
|
||||||
|
setShowPopup(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (hoverTimeoutRef.current) {
|
if (hoverTimeoutRef.current) {
|
||||||
clearTimeout(hoverTimeoutRef.current);
|
clearTimeout(hoverTimeoutRef.current);
|
||||||
}
|
}
|
||||||
|
if (popupTimeoutRef.current) {
|
||||||
|
clearTimeout(popupTimeoutRef.current);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, [isHovered]);
|
}, [isHovered]);
|
||||||
|
|
||||||
@ -159,7 +173,7 @@ export default function VideoCard({ video, onClick, className = "" }: VideoCardP
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-testid={`card-video-${video.id}`}
|
data-testid={`card-video-${video.id}`}
|
||||||
className={`video-card transition-transform duration-200 hover:scale-[1.02] p-1 md:p-2 ${className}`}
|
className={`video-card transition-all duration-300 ${showPopup ? 'z-50 scale-150 -translate-y-8' : 'hover:scale-[1.02]'} p-1 md:p-2 ${className}`}
|
||||||
onMouseEnter={() => setIsHovered(true)}
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
onMouseLeave={() => setIsHovered(false)}
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
>
|
>
|
||||||
@ -232,6 +246,46 @@ export default function VideoCard({ video, onClick, className = "" }: VideoCardP
|
|||||||
{video.title}
|
{video.title}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Netflix-style popup overlay */}
|
||||||
|
{showPopup && (
|
||||||
|
<div className="absolute inset-0 bg-bunny-dark rounded-lg shadow-2xl border border-gray-700 p-4 z-50">
|
||||||
|
{/* Action buttons */}
|
||||||
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
<button
|
||||||
|
onClick={() => onClick?.(video)}
|
||||||
|
className="bg-white text-black rounded-full w-8 h-8 flex items-center justify-center hover:bg-gray-200 transition-colors"
|
||||||
|
>
|
||||||
|
<Play className="w-4 h-4 ml-0.5" fill="currentColor" />
|
||||||
|
</button>
|
||||||
|
<button className="border border-gray-500 text-white rounded-full w-8 h-8 flex items-center justify-center hover:border-white transition-colors">
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<button className="border border-gray-500 text-white rounded-full w-8 h-8 flex items-center justify-center hover:border-white transition-colors">
|
||||||
|
<ThumbsUp className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<button className="border border-gray-500 text-white rounded-full w-8 h-8 flex items-center justify-center hover:border-white transition-colors ml-auto">
|
||||||
|
<ChevronDown className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Video info */}
|
||||||
|
<div className="text-white">
|
||||||
|
<h4 className="font-bold text-sm mb-1 line-clamp-2">{video.title}</h4>
|
||||||
|
<div className="flex items-center gap-2 text-xs text-gray-300 mb-2">
|
||||||
|
<span className="border border-gray-500 px-1 rounded text-xs">HD</span>
|
||||||
|
<span>{formatDuration(video.duration)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1 text-xs text-gray-300">
|
||||||
|
<span>Drama</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span>Thriller</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span>Action</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user