SPB Git forge

spb/spb-cloud

Public
10commits 1branches 0releases
3.2 MBsize
maindefault branch
3 h agolast push
JavaScript 56.7% TypeScript 42.6%
8.6 KB · 282 lines tsx
Raw Blame History
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