JavaScript 56.7%
TypeScript 42.6%
1"use client";23import { useState, useEffect, useMemo } from "react";4import { File, Folder, Download, Eye, ChevronRight, ChevronDown, Archive } from "lucide-react";5import type { PreviewPluginProps } from "./types";6import { formatFileSize } from "@/lib/utils";78interface ArchiveEntry {9 name: string;10 path: string;11 size: number;12 compressedSize: number;13 isDirectory: boolean;14 date: Date | null;15}1617interface TreeNode {18 name: string;19 path: string;20 isDirectory: boolean;21 size: number;22 date: Date | null;23 children: TreeNode[];24}2526export default function ArchivePreview({ file }: PreviewPluginProps) {27 const [entries, setEntries] = useState<ArchiveEntry[]>([]);28 const [loading, setLoading] = useState(true);29 const [error, setError] = useState<string | null>(null);30 const [previewContent, setPreviewContent] = useState<string | null>(null);31 const [previewName, setPreviewName] = useState("");32 // eslint-disable-next-line @typescript-eslint/no-explicit-any33 const [zipInstance, setZipInstance] = useState<any>(null);3435 useEffect(() => {36 const loadArchive = async () => {37 try {38 const JSZip = (await import("jszip")).default;39 const response = await fetch(`/api/files/${file.id}/preview`);40 const buffer = await response.arrayBuffer();41 const zip = await JSZip.loadAsync(buffer);42 setZipInstance(zip);4344 const items: ArchiveEntry[] = [];45 zip.forEach((relativePath, zipEntry) => {46 items.push({47 name: zipEntry.name.split("/").filter(Boolean).pop() || zipEntry.name,48 path: relativePath,49 // eslint-disable-next-line @typescript-eslint/no-explicit-any50 size: (zipEntry as any)._data?.uncompressedSize || 0,51 // eslint-disable-next-line @typescript-eslint/no-explicit-any52 compressedSize: (zipEntry as any)._data?.compressedSize || 0,53 isDirectory: zipEntry.dir,54 date: zipEntry.date,55 });56 });5758 setEntries(items);59 setLoading(false);60 } catch {61 setError("Impossible de lire l'archive");62 setLoading(false);63 }64 };6566 loadArchive();67 }, [file.id]);6869 const tree = useMemo(() => {70 const root: TreeNode = { name: "", path: "", isDirectory: true, size: 0, date: null, children: [] };7172 for (const entry of entries) {73 const parts = entry.path.split("/").filter(Boolean);74 let current = root;7576 for (let i = 0; i < parts.length; i++) {77 const isLast = i === parts.length - 1;78 let child = current.children.find((c) => c.name === parts[i]);7980 if (!child) {81 child = {82 name: parts[i],83 path: parts.slice(0, i + 1).join("/"),84 isDirectory: isLast ? entry.isDirectory : true,85 size: isLast ? entry.size : 0,86 date: isLast ? entry.date : null,87 children: [],88 };89 current.children.push(child);90 }9192 current = child;93 }94 }9596 // Sort: directories first, then by name97 const sortTree = (node: TreeNode) => {98 node.children.sort((a, b) => {99 if (a.isDirectory && !b.isDirectory) return -1;100 if (!a.isDirectory && b.isDirectory) return 1;101 return a.name.localeCompare(b.name);102 });103 node.children.forEach(sortTree);104 };105 sortTree(root);106107 return root;108 }, [entries]);109110 const handlePreviewFile = async (path: string, name: string) => {111 if (!zipInstance) return;112 try {113 const content = await zipInstance.file(path)?.async("string");114 setPreviewContent(content || "");115 setPreviewName(name);116 } catch {117 setPreviewContent("Impossible de lire ce fichier");118 setPreviewName(name);119 }120 };121122 const handleDownloadFile = async (path: string, name: string) => {123 if (!zipInstance) return;124 try {125 const blob = await zipInstance.file(path)?.async("blob");126 if (!blob) return;127 const url = URL.createObjectURL(blob);128 const a = document.createElement("a");129 a.href = url;130 a.download = name;131 a.click();132 URL.revokeObjectURL(url);133 } catch {134 // ignore135 }136 };137138 if (loading) {139 return (140 <div className="flex items-center justify-center h-full">141 <div className="w-6 h-6 border-2 border-white/20 border-t-white rounded-full animate-spin" />142 </div>143 );144 }145146 if (error) {147 return (148 <div className="flex flex-col items-center justify-center h-full text-gray-400">149 <Archive className="w-12 h-12 mb-4 text-gray-600" />150 <p>{error}</p>151 </div>152 );153 }154155 const fileCount = entries.filter((e) => !e.isDirectory).length;156 const totalSize = entries.reduce((sum, e) => sum + e.size, 0);157158 return (159 <div className="flex flex-col h-full">160 {/* Stats */}161 <div className="flex items-center gap-3 px-3 py-2 border-b border-white/10 bg-white/5 text-xs text-gray-400">162 <span>{fileCount} fichiers</span>163 <span>{formatFileSize(totalSize)} total</span>164 </div>165166 <div className="flex flex-1 overflow-hidden">167 {/* Tree */}168 <div className="flex-1 overflow-auto p-3">169 {tree.children.map((node) => (170 <TreeNodeView171 key={node.path}172 node={node}173 onPreview={handlePreviewFile}174 onDownload={handleDownloadFile}175 />176 ))}177 </div>178179 {/* Preview panel */}180 {previewContent !== null && (181 <div className="w-1/2 border-l border-white/10 flex flex-col">182 <div className="flex items-center justify-between px-3 py-2 border-b border-white/10 bg-white/5">183 <span className="text-xs text-gray-400 truncate">{previewName}</span>184 <button185 onClick={() => setPreviewContent(null)}186 className="text-xs text-gray-500 hover:text-white"187 >188 Fermer189 </button>190 </div>191 <pre className="flex-1 overflow-auto p-3 text-xs text-gray-300 font-mono whitespace-pre-wrap">192 {previewContent}193 </pre>194 </div>195 )}196 </div>197 </div>198 );199}200201function TreeNodeView({202 node,203 onPreview,204 onDownload,205}: {206 node: TreeNode;207 onPreview: (path: string, name: string) => void;208 onDownload: (path: string, name: string) => void;209}) {210 const [expanded, setExpanded] = useState(true);211212 const isTextFile = (name: string) => {213 const ext = name.split(".").pop()?.toLowerCase() || "";214 return ["txt", "md", "json", "xml", "csv", "js", "ts", "jsx", "tsx", "html", "css", "py", "rb", "go", "rs", "java", "c", "cpp", "h", "yml", "yaml", "toml", "sh", "env", "log", "sql", "conf", "cfg"].includes(ext);215 };216217 if (node.isDirectory) {218 return (219 <div>220 <button221 onClick={() => setExpanded(!expanded)}222 className="flex items-center gap-1.5 w-full px-1 py-0.5 text-sm text-gray-300 hover:text-white hover:bg-white/5 rounded transition-colors"223 >224 {expanded ? <ChevronDown className="w-3 h-3 text-gray-500" /> : <ChevronRight className="w-3 h-3 text-gray-500" />}225 <Folder className="w-4 h-4 text-yellow-500/70" />226 <span>{node.name}</span>227 </button>228 {expanded && (229 <div className="ml-4">230 {node.children.map((child) => (231 <TreeNodeView key={child.path} node={child} onPreview={onPreview} onDownload={onDownload} />232 ))}233 </div>234 )}235 </div>236 );237 }238239 return (240 <div className="flex items-center gap-1.5 group px-1 py-0.5 text-sm text-gray-400 hover:text-white hover:bg-white/5 rounded transition-colors">241 <div className="w-3" />242 <File className="w-4 h-4 text-gray-500" />243 <span className="flex-1 truncate">{node.name}</span>244 <span className="text-xs text-gray-600 mr-2">{formatFileSize(node.size)}</span>245 <div className="hidden group-hover:flex items-center gap-1">246 {isTextFile(node.name) && (247 <button248 onClick={() => onPreview(node.path, node.name)}249 className="p-1 text-gray-500 hover:text-white transition-colors"250 title="Aperçu"251 >252 <Eye className="w-3 h-3" />253 </button>254 )}255 <button256 onClick={() => onDownload(node.path, node.name)}257 className="p-1 text-gray-500 hover:text-white transition-colors"258 title="Télécharger"259 >260 <Download className="w-3 h-3" />261 </button>262 </div>263 </div>264 );265}266