125 lines
4.1 KiB
TypeScript
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; |