JavaScript 56.7%
TypeScript 42.6%
1"use client";23import { useState, useEffect } from "react";4import { motion, AnimatePresence } from "framer-motion";5import { X, Download, ChevronLeft, ChevronRight, Maximize2, Minimize2 } from "lucide-react";6import { isPreviewable, getLanguageFromMime, formatFileSize, formatDate } from "@/lib/utils";7import dynamic from "next/dynamic";89const SyntaxHighlighter = dynamic(10 () => import("react-syntax-highlighter").then((mod) => mod.Light),11 { ssr: false }12);1314interface FileData {15 id: string;16 name: string;17 size: number;18 mimeType: string;19 createdAt: string;20}2122interface FilePreviewProps {23 file: FileData | null;24 files?: FileData[];25 onClose: () => void;26 onNavigate?: (file: FileData) => void;27}2829export default function FilePreview({ file, files = [], onClose, onNavigate }: FilePreviewProps) {30 const [textContent, setTextContent] = useState<string>("");31 const [fullscreen, setFullscreen] = useState(false);32 const [loading, setLoading] = useState(false);3334 const currentIndex = files.findIndex((f) => f.id === file?.id);3536 useEffect(() => {37 if (!file) return;3839 const isText =40 file.mimeType.startsWith("text/") ||41 file.mimeType.includes("json") ||42 file.mimeType.includes("xml") ||43 file.mimeType.includes("javascript") ||44 file.mimeType.includes("typescript") ||45 file.mimeType.includes("markdown");4647 if (isText) {48 setLoading(true);49 fetch(`/api/files/${file.id}/preview`)50 .then((res) => res.text())51 .then((text) => {52 setTextContent(text);53 setLoading(false);54 })55 .catch(() => setLoading(false));56 }5758 if (file.mimeType === "text/csv") {59 setLoading(true);60 fetch(`/api/files/${file.id}/preview`)61 .then((res) => res.text())62 .then((text) => {63 setTextContent(text);64 setLoading(false);65 })66 .catch(() => setLoading(false));67 }68 }, [file]);6970 if (!file) return null;7172 const handlePrev = () => {73 if (currentIndex > 0 && onNavigate) onNavigate(files[currentIndex - 1]);74 };7576 const handleNext = () => {77 if (currentIndex < files.length - 1 && onNavigate) onNavigate(files[currentIndex + 1]);78 };7980 const renderPreview = () => {81 if (!isPreviewable(file.mimeType)) {82 return (83 <div className="flex flex-col items-center justify-center h-full text-gray-400">84 <p className="text-lg mb-2">Aperçu non disponible</p>85 <p className="text-sm text-gray-500">{file.mimeType}</p>86 <button87 onClick={() => window.open(`/api/files/${file.id}/download`, "_blank")}88 className="mt-4 px-4 py-2 bg-white/10 rounded-lg text-sm hover:bg-white/20 transition-colors"89 >90 Télécharger91 </button>92 </div>93 );94 }9596 if (file.mimeType.startsWith("image/")) {97 return (98 <div className="flex items-center justify-center h-full">99 <img100 src={`/api/files/${file.id}/preview`}101 alt={file.name}102 className="max-w-full max-h-full object-contain"103 />104 </div>105 );106 }107108 if (file.mimeType.startsWith("video/")) {109 return (110 <div className="flex items-center justify-center h-full">111 <video112 src={`/api/files/${file.id}/preview`}113 controls114 className="max-w-full max-h-full"115 >116 Your browser does not support video.117 </video>118 </div>119 );120 }121122 if (file.mimeType.startsWith("audio/")) {123 return (124 <div className="flex items-center justify-center h-full">125 <audio src={`/api/files/${file.id}/preview`} controls className="w-full max-w-md">126 Your browser does not support audio.127 </audio>128 </div>129 );130 }131132 if (file.mimeType === "application/pdf") {133 return (134 <iframe135 src={`/api/files/${file.id}/preview`}136 className="w-full h-full border-0"137 title={file.name}138 />139 );140 }141142 if (file.mimeType === "text/csv") {143 if (loading) return <div className="text-gray-400 p-4">Chargement...</div>;144 const rows = textContent.split("\n").map((row) => row.split(","));145 return (146 <div className="overflow-auto h-full p-4">147 <table className="w-full text-sm border-collapse">148 <thead>149 <tr>150 {rows[0]?.map((header, i) => (151 <th key={i} className="border border-white/10 px-3 py-2 text-left text-gray-300 bg-white/5">152 {header}153 </th>154 ))}155 </tr>156 </thead>157 <tbody>158 {rows.slice(1).map((row, i) => (159 <tr key={i}>160 {row.map((cell, j) => (161 <td key={j} className="border border-white/10 px-3 py-2 text-gray-400">162 {cell}163 </td>164 ))}165 </tr>166 ))}167 </tbody>168 </table>169 </div>170 );171 }172173 // Text / Code / Markdown174 if (loading) return <div className="text-gray-400 p-4">Chargement...</div>;175176 const isCode =177 file.mimeType.includes("javascript") ||178 file.mimeType.includes("typescript") ||179 file.mimeType.includes("json") ||180 file.mimeType.includes("xml") ||181 file.mimeType.includes("css") ||182 file.mimeType.includes("html");183184 if (isCode) {185 const language = getLanguageFromMime(file.mimeType);186 return (187 <div className="overflow-auto h-full p-4">188 <SyntaxHighlighter189 language={language}190 customStyle={{191 background: "transparent",192 padding: 0,193 margin: 0,194 fontSize: "0.875rem",195 }}196 >197 {textContent}198 </SyntaxHighlighter>199 </div>200 );201 }202203 return (204 <div className="overflow-auto h-full p-6">205 <pre className="text-sm text-gray-300 whitespace-pre-wrap font-mono">206 {textContent}207 </pre>208 </div>209 );210 };211212 return (213 <AnimatePresence>214 <motion.div215 initial={{ opacity: 0 }}216 animate={{ opacity: 1 }}217 exit={{ opacity: 0 }}218 className={`fixed inset-0 z-50 flex flex-col bg-gray-950 ${219 fullscreen ? "" : "lg:relative lg:inset-auto lg:h-full"220 }`}221 >222 {/* Header */}223 <div className="flex items-center justify-between px-4 py-3 border-b border-white/10 flex-shrink-0">224 <div className="flex-1 min-w-0">225 <h3 className="text-sm font-medium text-white truncate">{file.name}</h3>226 <p className="text-xs text-gray-500">227 {formatFileSize(file.size)} - {formatDate(file.createdAt)}228 </p>229 </div>230 <div className="flex items-center gap-1">231 <button232 onClick={() => window.open(`/api/files/${file.id}/download`, "_blank")}233 className="p-2 text-gray-400 hover:text-white transition-colors"234 >235 <Download className="w-4 h-4" />236 </button>237 <button238 onClick={() => setFullscreen(!fullscreen)}239 className="p-2 text-gray-400 hover:text-white transition-colors hidden lg:block"240 >241 {fullscreen ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}242 </button>243 <button244 onClick={onClose}245 className="p-2 text-gray-400 hover:text-white transition-colors"246 >247 <X className="w-4 h-4" />248 </button>249 </div>250 </div>251252 {/* Preview Content */}253 <div className="flex-1 relative overflow-hidden">254 {renderPreview()}255256 {/* Navigation arrows */}257 {files.length > 1 && (258 <>259 {currentIndex > 0 && (260 <button261 onClick={handlePrev}262 className="absolute left-2 top-1/2 -translate-y-1/2 p-2 bg-black/50 rounded-full text-white hover:bg-black/70 transition-colors backdrop-blur-sm"263 >264 <ChevronLeft className="w-5 h-5" />265 </button>266 )}267 {currentIndex < files.length - 1 && (268 <button269 onClick={handleNext}270 className="absolute right-2 top-1/2 -translate-y-1/2 p-2 bg-black/50 rounded-full text-white hover:bg-black/70 transition-colors backdrop-blur-sm"271 >272 <ChevronRight className="w-5 h-5" />273 </button>274 )}275 </>276 )}277 </div>278 </motion.div>279 </AnimatePresence>280 );281}282