import { useState, useRef, useEffect } from 'react';
import { ImageIcon, VideoIcon, Play } from 'lucide-react';
interface MediaThumbnailProps {
src: string;
alt: string;
type?: 'image' | 'video';
className?: string;
thumbnailUrl?: string; // Optional pre-generated thumbnail
}
function isVideoUrl(url: string): boolean {
return /\.(mp4|mov|avi|webm|mkv|m4v)(\?.*)?$/i.test(url);
}
/**
* Renders a media thumbnail that handles:
* - Local video files: captures first frame as thumbnail
* - Images: standard
with lazy loading
* - Broken/unavailable URLs: shows a placeholder icon
*/
export function MediaThumbnail({ src, alt, type, className = "w-full h-full object-cover", thumbnailUrl }: MediaThumbnailProps) {
const [error, setError] = useState(false);
const [thumbnail, setThumbnail] = useState(thumbnailUrl || null);
const [loading, setLoading] = useState(false);
const videoRef = useRef(null);
const canvasRef = useRef(null);
const isVideo = type === 'video' || isVideoUrl(src);
// Generate thumbnail from video first frame
useEffect(() => {
if (!isVideo || thumbnail || error || !src) return;
const video = document.createElement('video');
video.crossOrigin = 'anonymous';
video.muted = true;
video.preload = 'metadata';
const handleLoadedData = () => {
// Seek to 1 second or 10% of duration (whichever is smaller)
const seekTime = Math.min(1, video.duration * 0.1);
video.currentTime = seekTime;
};
const handleSeeked = () => {
try {
const canvas = document.createElement('canvas');
canvas.width = video.videoWidth || 320;
canvas.height = video.videoHeight || 180;
const ctx = canvas.getContext('2d');
if (ctx) {
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
const dataUrl = canvas.toDataURL('image/jpeg', 0.8);
setThumbnail(dataUrl);
}
} catch (e) {
console.warn('Failed to generate video thumbnail:', e);
setError(true);
}
video.remove();
};
const handleError = () => {
// Cas courant et attendu : la vidéo a été supprimée du disque après
// publication. Les médias créés depuis embarquent une vignette persistée,
// qui évite d'en arriver là.
console.warn('Aperçu vidéo indisponible (fichier absent ou illisible):', src);
setError(true);
video.remove();
};
video.addEventListener('loadeddata', handleLoadedData);
video.addEventListener('seeked', handleSeeked);
video.addEventListener('error', handleError);
setLoading(true);
video.src = src;
video.load();
// Timeout fallback
const timeout = setTimeout(() => {
if (!thumbnail) {
setError(true);
video.remove();
}
setLoading(false);
}, 5000);
return () => {
clearTimeout(timeout);
video.removeEventListener('loadeddata', handleLoadedData);
video.removeEventListener('seeked', handleSeeked);
video.removeEventListener('error', handleError);
video.remove();
};
}, [src, isVideo, thumbnail, error]);
// Show placeholder if error or no src
if ((error && !thumbnail) || !src) {
return (
{isVideo
?
:
}
);
}
// Video with generated thumbnail
if (isVideo) {
if (thumbnail) {
return (

setError(true)}
/>
);
}
// Loading state while generating thumbnail
if (loading) {
return (
);
}
// Fallback to video element
return (
);
}
// Image
return (
setError(true)}
/>
);
}