kreativortex/components/VideoThumbnail.tsx
Jessica Rekcah 3a14660c6d update
2025-12-06 10:05:58 +07:00

125 lines
4.1 KiB
TypeScript

/**
* File: VideoThumbnail.tsx
* Created by: AI Assistant
* Date: 2025-12-05
* Purpose: Video thumbnail component with lazy loading for kreatiVortex platform
* Part of: kreatiVortex - Platform Pembelajaran Tari Online
*/
'use client';
import { useState, useRef, useEffect } from 'react';
import { Link } from '@/i18n/routing';
import { Play, Video } from 'lucide-react';
interface VideoThumbnailProps {
video: {
id: string;
title: string;
videoType: 'YOUTUBE' | 'LOCAL';
videoUrl: string;
isPublic: boolean;
};
}
function VideoThumbnail({ video }: VideoThumbnailProps) {
const [isLoaded, setIsLoaded] = useState(false);
const [error, setError] = useState(false);
const imgRef = useRef<HTMLImageElement>(null);
// Generate YouTube thumbnail URL
const getYouTubeThumbnailUrl = (url: string) => {
const videoId = url.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([^&\n?#]+)/);
return videoId ? `https://img.youtube.com/vi/${videoId[1]}/mqdefault.jpg` : null;
};
const thumbnailUrl = video.videoType === 'YOUTUBE' ? getYouTubeThumbnailUrl(video.videoUrl) : null;
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting && imgRef.current && thumbnailUrl) {
imgRef.current.src = thumbnailUrl;
observer.unobserve(entry.target);
}
});
},
{ threshold: 0.1 }
);
if (imgRef.current) {
observer.observe(imgRef.current);
}
return () => observer.disconnect();
}, [thumbnailUrl]);
return (
<div className="aspect-video bg-gray-900 relative overflow-hidden group">
{video.videoType === 'YOUTUBE' && thumbnailUrl ? (
<>
<img
ref={imgRef}
data-src={thumbnailUrl}
alt={video.title}
className={`w-full h-full object-cover transition-opacity duration-300 ${
isLoaded ? 'opacity-100' : 'opacity-0'
}`}
onLoad={() => setIsLoaded(true)}
onError={() => setError(true)}
/>
{!isLoaded && !error && (
<div className="absolute inset-0 bg-gray-800 animate-pulse" />
)}
{error && (
<div className="w-full h-full flex items-center justify-center bg-gray-800">
<div className="text-center">
<Video className="w-12 h-12 text-gray-400 mx-auto mb-2" />
<p className="text-xs text-gray-500">Video Preview</p>
</div>
</div>
)}
</>
) : (
<div className="w-full h-full flex items-center justify-center bg-gray-800">
<div className="text-center">
<Video className="w-12 h-12 text-gray-400 mx-auto mb-2" />
<p className="text-xs text-gray-500">Video Preview</p>
</div>
</div>
)}
{/* Play Button Overlay */}
<div className="absolute inset-0 bg-black/40 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
<Link href={`/dashboard/videos/${video.id}`} className="text-white hover:text-gold-400 transition-colors">
<Play className="w-8 h-8 sm:w-12 sm:h-12" />
</Link>
</div>
{/* Status Badge */}
<div className="absolute top-2 right-2 sm:top-3 sm:right-3">
<span className={`px-1.5 py-0.5 sm:px-2 sm:py-1 rounded-full text-xs font-medium backdrop-blur-sm ${
video.isPublic
? 'bg-green-500/80 text-white'
: 'bg-yellow-500/80 text-white'
}`}>
{video.isPublic ? 'Public' : 'Private'}
</span>
</div>
{/* Video Type Badge */}
<div className="absolute top-2 left-2 sm:top-3 sm:left-3">
<span className={`px-1.5 py-0.5 sm:px-2 sm:py-1 rounded text-xs font-medium backdrop-blur-sm ${
video.videoType === 'YOUTUBE'
? 'bg-red-900/80 text-red-200'
: 'bg-blue-900/80 text-blue-200'
}`}>
{video.videoType}
</span>
</div>
</div>
);
}
export default VideoThumbnail;