Spaces:
Running
Running
| import React, { useState, useEffect, useRef } from "react"; | |
| import { motion } from "framer-motion"; | |
| import { FileText, ZoomIn, ZoomOut, RotateCw, Maximize2 } from "lucide-react"; | |
| import { Button } from "@/components/ui/button"; | |
| export default function DocumentPreview({ file, isProcessing }) { | |
| const [previewUrls, setPreviewUrls] = useState([]); | |
| const [zoom, setZoom] = useState(100); | |
| const [rotation, setRotation] = useState(0); | |
| const objectUrlsRef = useRef([]); | |
| useEffect(() => { | |
| if (!file) { | |
| // Cleanup previous URLs | |
| objectUrlsRef.current.forEach((url) => { | |
| if (url && url.startsWith("blob:")) { | |
| URL.revokeObjectURL(url); | |
| } | |
| }); | |
| objectUrlsRef.current = []; | |
| setPreviewUrls([]); | |
| return; | |
| } | |
| const loadPreview = async () => { | |
| const urls = []; | |
| const newObjectUrls = []; | |
| // Check if it's a PDF | |
| if (file.type === "application/pdf" || file.name?.toLowerCase().endsWith(".pdf")) { | |
| try { | |
| // Use pdf.js to render PDF pages | |
| const pdfjsLib = await import("pdfjs-dist"); | |
| // Configure worker - use jsdelivr CDN which is more reliable | |
| // This will use the same version as the installed package | |
| const version = pdfjsLib.version || "4.0.379"; | |
| pdfjsLib.GlobalWorkerOptions.workerSrc = `https://cdn.jsdelivr.net/npm/pdfjs-dist@${version}/build/pdf.worker.min.mjs`; | |
| const arrayBuffer = await file.arrayBuffer(); | |
| const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise; | |
| const numPages = pdf.numPages; | |
| for (let pageNum = 1; pageNum <= numPages; pageNum++) { | |
| const page = await pdf.getPage(pageNum); | |
| const viewport = page.getViewport({ scale: 2.0 }); | |
| const canvas = document.createElement("canvas"); | |
| const context = canvas.getContext("2d"); | |
| canvas.height = viewport.height; | |
| canvas.width = viewport.width; | |
| await page.render({ | |
| canvasContext: context, | |
| viewport: viewport, | |
| }).promise; | |
| urls.push(canvas.toDataURL("image/jpeg", 0.95)); | |
| } | |
| } catch (error) { | |
| console.error("Error loading PDF:", error); | |
| // Fallback: show error message | |
| urls.push(null); | |
| } | |
| } else { | |
| // For images, create object URL | |
| const url = URL.createObjectURL(file); | |
| urls.push(url); | |
| newObjectUrls.push(url); | |
| } | |
| // Cleanup old object URLs | |
| objectUrlsRef.current.forEach((url) => { | |
| if (url && url.startsWith("blob:")) { | |
| URL.revokeObjectURL(url); | |
| } | |
| }); | |
| objectUrlsRef.current = newObjectUrls; | |
| setPreviewUrls(urls); | |
| }; | |
| loadPreview(); | |
| // Cleanup function - revoke object URLs when component unmounts or file changes | |
| return () => { | |
| objectUrlsRef.current.forEach((url) => { | |
| if (url && url.startsWith("blob:")) { | |
| URL.revokeObjectURL(url); | |
| } | |
| }); | |
| objectUrlsRef.current = []; | |
| }; | |
| }, [file]); | |
| return ( | |
| <div className="h-full flex flex-col bg-white rounded-2xl border border-slate-200 overflow-hidden"> | |
| {/* Header */} | |
| <div className="flex items-center justify-between px-5 py-4 border-b border-slate-100"> | |
| <div className="flex items-center gap-3"> | |
| <div className="h-8 w-8 rounded-lg bg-indigo-50 flex items-center justify-center"> | |
| <FileText className="h-4 w-4 text-indigo-600" /> | |
| </div> | |
| <div> | |
| <h3 className="font-semibold text-slate-800 text-sm">Document Preview</h3> | |
| <p className="text-xs text-slate-400">{file?.name || "No file selected"}</p> | |
| </div> | |
| </div> | |
| {file && ( | |
| <div className="flex items-center gap-1"> | |
| <Button | |
| variant="ghost" | |
| size="icon" | |
| className="h-8 w-8 text-slate-400 hover:text-slate-600" | |
| onClick={() => setZoom(Math.max(50, zoom - 25))} | |
| > | |
| <ZoomOut className="h-4 w-4" /> | |
| </Button> | |
| <span className="text-xs text-slate-500 w-12 text-center">{zoom}%</span> | |
| <Button | |
| variant="ghost" | |
| size="icon" | |
| className="h-8 w-8 text-slate-400 hover:text-slate-600" | |
| onClick={() => setZoom(Math.min(200, zoom + 25))} | |
| > | |
| <ZoomIn className="h-4 w-4" /> | |
| </Button> | |
| <div className="w-px h-4 bg-slate-200 mx-2" /> | |
| <Button | |
| variant="ghost" | |
| size="icon" | |
| className="h-8 w-8 text-slate-400 hover:text-slate-600" | |
| onClick={() => setRotation((rotation + 90) % 360)} | |
| > | |
| <RotateCw className="h-4 w-4" /> | |
| </Button> | |
| <Button | |
| variant="ghost" | |
| size="icon" | |
| className="h-8 w-8 text-slate-400 hover:text-slate-600" | |
| onClick={() => { | |
| setZoom(100); | |
| setRotation(0); | |
| }} | |
| > | |
| <Maximize2 className="h-4 w-4" /> | |
| </Button> | |
| </div> | |
| )} | |
| </div> | |
| {/* Preview Area */} | |
| <div className="flex-1 p-6 bg-slate-50/50 overflow-auto"> | |
| {!file ? ( | |
| <div className="h-full flex items-center justify-center"> | |
| <div className="text-center"> | |
| <div className="h-20 w-20 mx-auto rounded-2xl bg-slate-100 flex items-center justify-center mb-4"> | |
| <FileText className="h-10 w-10 text-slate-300" /> | |
| </div> | |
| <p className="text-slate-400 text-sm">Upload a document to preview</p> | |
| </div> | |
| </div> | |
| ) : previewUrls.length === 0 ? ( | |
| <div className="h-full flex items-center justify-center"> | |
| <div className="text-center"> | |
| <div className="h-20 w-20 mx-auto rounded-2xl bg-slate-100 flex items-center justify-center mb-4"> | |
| <FileText className="h-10 w-10 text-slate-300" /> | |
| </div> | |
| <p className="text-slate-400 text-sm">Loading preview...</p> | |
| </div> | |
| </div> | |
| ) : ( | |
| <div className="space-y-4"> | |
| {previewUrls.map((url, index) => ( | |
| <motion.div | |
| key={index} | |
| initial={{ opacity: 0, y: 20 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| transition={{ delay: index * 0.1 }} | |
| className="relative bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden flex items-center justify-center" | |
| style={{ | |
| minHeight: "400px", | |
| }} | |
| > | |
| {url ? ( | |
| <img | |
| src={url} | |
| alt={`Page ${index + 1}`} | |
| className="w-full h-auto" | |
| style={{ | |
| transform: `scale(${zoom / 100}) rotate(${rotation}deg)`, | |
| maxWidth: "100%", | |
| objectFit: "contain", | |
| transition: "transform 0.2s ease", | |
| }} | |
| /> | |
| ) : ( | |
| <div className="p-8 text-center"> | |
| <p className="text-slate-400 text-sm">Unable to load preview</p> | |
| </div> | |
| )} | |
| {/* Processing overlay */} | |
| {isProcessing && ( | |
| <motion.div | |
| initial={{ opacity: 0 }} | |
| animate={{ opacity: 1 }} | |
| className="absolute inset-0 bg-indigo-600/5 backdrop-blur-[1px] pointer-events-none" | |
| > | |
| <motion.div | |
| initial={{ top: 0 }} | |
| animate={{ top: "100%" }} | |
| transition={{ | |
| duration: 2, | |
| repeat: Infinity, | |
| ease: "linear", | |
| }} | |
| className="absolute left-0 right-0 h-1 bg-gradient-to-r from-transparent via-indigo-500 to-transparent" | |
| /> | |
| </motion.div> | |
| )} | |
| {/* Page number */} | |
| {previewUrls.length > 1 && ( | |
| <div className="absolute bottom-3 right-3 text-xs text-slate-400 bg-white/90 px-2 py-1 rounded"> | |
| Page {index + 1} | |
| </div> | |
| )} | |
| </motion.div> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| } | |