import { Play, Volume2, VolumeX } from "lucide-react"; import { type Video } from "@shared/schema"; import { useState, useRef, useEffect } from "react"; import Hls from "hls.js"; interface VideoCardProps { video: Video; onClick: (video: Video) => void; className?: string; hideOverlay?: boolean; } function formatDuration(seconds: number): string { const minutes = Math.floor(seconds / 60); const remainingSeconds = seconds % 60; return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`; } function formatViews(views: number): string { if (views >= 1000000) { return `${(views / 1000000).toFixed(1)}M views`; } else if (views >= 1000) { return `${(views / 1000).toFixed(1)}K views`; } return `${views} views`; } function formatDate(date: Date | string): string { const now = new Date(); const createdDate = typeof date === 'string' ? new Date(date) : date; if (!createdDate || isNaN(createdDate.getTime())) { return "Unknown"; } const diffTime = Math.abs(now.getTime() - createdDate.getTime()); const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24)); if (diffDays === 0) return "Today"; if (diffDays === 1) return "1 day ago"; if (diffDays < 7) return `${diffDays} days ago`; if (diffDays < 30) return `${Math.floor(diffDays / 7)} week${Math.floor(diffDays / 7) > 1 ? 's' : ''} ago`; return `${Math.floor(diffDays / 30)} month${Math.floor(diffDays / 30) > 1 ? 's' : ''} ago`; } export default function VideoCard({ video, onClick, className = "", hideOverlay = false }: VideoCardProps) { // Generate short ID for cleaner URLs (first 8 chars without dashes) const shortId = video.id.replace(/-/g, '').substring(0, 8); const [isHovered, setIsHovered] = useState(false); const [showPreview, setShowPreview] = useState(false); const [isMuted, setIsMuted] = useState(true); const [showMoreInfo, setShowMoreInfo] = useState(false); const [isMobile, setIsMobile] = useState(false); const [isVideoPage, setIsVideoPage] = useState(false); const hoverTimeoutRef = useRef(); const videoRef = useRef(null); const hlsRef = useRef(null); const [currentTime, setCurrentTime] = useState(0); const [duration, setDuration] = useState(0); // Check device type and page location useEffect(() => { const checkDevice = () => { setIsMobile(window.innerWidth < 768); setIsVideoPage(window.location.pathname.startsWith('/video/')); }; checkDevice(); // Initial check window.addEventListener('resize', checkDevice); window.addEventListener('popstate', checkDevice); return () => { window.removeEventListener('resize', checkDevice); window.removeEventListener('popstate', checkDevice); }; }, []); // Load mute preference on component mount useEffect(() => { const savedMuteState = localStorage.getItem('videoPreviewMuted'); const shouldBeMuted = savedMuteState === null ? true : savedMuteState === 'true'; setIsMuted(shouldBeMuted); }, []); // Listen for changes in localStorage from other components useEffect(() => { const handleStorageChange = (e: StorageEvent) => { if (e.key === 'videoPreviewMuted' && e.newValue !== null) { setIsMuted(e.newValue === 'true'); } }; const handleMuteChange = (e: CustomEvent) => { setIsMuted(e.detail.isMuted); }; window.addEventListener('storage', handleStorageChange); window.addEventListener('videoMuteChanged', handleMuteChange as EventListener); return () => { window.removeEventListener('storage', handleStorageChange); window.removeEventListener('videoMuteChanged', handleMuteChange as EventListener); }; }, []); // Enable video preview on hover for desktop devices useEffect(() => { if (isHovered) { // Enable preview for desktop devices after delay if (window.innerWidth >= 768) { const delay = 800; hoverTimeoutRef.current = setTimeout(() => { setShowPreview(true); }, delay); } } else { if (hoverTimeoutRef.current) { clearTimeout(hoverTimeoutRef.current); } setShowPreview(false); // Clean up HLS when not hovering if (hlsRef.current) { hlsRef.current.destroy(); hlsRef.current = null; } } return () => { if (hoverTimeoutRef.current) { clearTimeout(hoverTimeoutRef.current); } if (hlsRef.current) { hlsRef.current.destroy(); } }; }, [isHovered]); // Initialize HLS when preview shows useEffect(() => { if (showPreview && videoRef.current && video.videoUrl.includes('.m3u8')) { const videoElement = videoRef.current; // Ensure video element respects current mute state videoElement.muted = isMuted; if (Hls.isSupported()) { const hls = new Hls({ startLevel: 0, // Start with lowest quality capLevelToPlayerSize: true, maxBufferLength: 5, // Minimal buffering }); hls.loadSource(video.videoUrl); hls.attachMedia(videoElement); hlsRef.current = hls; hls.on(Hls.Events.MANIFEST_PARSED, () => { // Set muted state before playing videoElement.muted = isMuted; videoElement.play().catch(e => console.log('Preview autoplay failed:', e)); }); hls.on(Hls.Events.MEDIA_ATTACHED, () => { // Set muted state after media is attached videoElement.muted = isMuted; videoElement.addEventListener('loadedmetadata', () => { setDuration(videoElement.duration); // Ensure muted state is preserved after metadata loads videoElement.muted = isMuted; }); }); } else if (videoElement.canPlayType('application/vnd.apple.mpegurl')) { // Safari native HLS support videoElement.src = video.videoUrl; videoElement.muted = isMuted; videoElement.addEventListener('loadedmetadata', () => { setDuration(videoElement.duration); // Ensure muted state is preserved after metadata loads videoElement.muted = isMuted; }); videoElement.play().catch(e => console.log('Preview autoplay failed:', e)); } } }, [showPreview, video.videoUrl, isMuted]); return (
!isMobile && setIsHovered(true)} onMouseLeave={() => !isMobile && setIsHovered(false)} > {/* Video preview container */}
onClick?.(video)} > {/* Static thumbnail - always visible */} {video.title} { const target = e.target as HTMLImageElement; console.log('Thumbnail failed to load:', target.src); // Show placeholder immediately instead of trying multiple URLs target.style.display = 'none'; if (target.parentElement && !target.parentElement.querySelector('.thumbnail-placeholder')) { target.parentElement.style.background = 'linear-gradient(135deg, #1f2937, #374151)'; const placeholder = document.createElement('div'); placeholder.className = 'thumbnail-placeholder absolute inset-0 flex flex-col items-center justify-center text-white'; placeholder.innerHTML = '
🎬
Video
'; target.parentElement.appendChild(placeholder); } }} /> {/* Video preview - only load when hovering */} {showPreview && (
)} {/* Desktop gradient overlay with title - hidden on mobile video pages */} {!showPreview && !hideOverlay && !(isMobile && isVideoPage) && (

{(video.title.split(' - ')[0] || 'video.folx.tv').substring(0, 35)}

{(video.title.split(' - ')[1] || video.title).substring(0, 50)}

)}
{/* Mobile info section - below video - only on video pages */} {isMobile && isVideoPage && (
{/* Full title */}

{video.title.length > 60 ? video.title.substring(0, 60) + '...' : video.title}

{/* Views and Date in one line */}
{formatViews(video.views || 0)} {formatDate(video.createdAt)}
{/* Description with expand/collapse */} {(video.title.split(' - ')[1] || video.description) && (

{video.title.split(' - ')[1] || video.description || ''}

{!showMoreInfo && ( )}
{showMoreInfo && ( )}
)}
)}
); }