From f7032e37c8a93f6d6be2cb983d0b5d8ec170678d Mon Sep 17 00:00:00 2001 From: Potenz Date: Tue, 30 Sep 2025 01:53:44 +0530 Subject: [PATCH] feat(preview modal added) --- .../documents/file-preview-modal.tsx | 229 ++++++++++++++++++ apps/Frontend/src/pages/documents-page.tsx | 61 +++-- 2 files changed, 258 insertions(+), 32 deletions(-) create mode 100644 apps/Frontend/src/components/documents/file-preview-modal.tsx diff --git a/apps/Frontend/src/components/documents/file-preview-modal.tsx b/apps/Frontend/src/components/documents/file-preview-modal.tsx new file mode 100644 index 0000000..231b21d --- /dev/null +++ b/apps/Frontend/src/components/documents/file-preview-modal.tsx @@ -0,0 +1,229 @@ +import React, { useEffect, useState } from "react"; +import { apiRequest } from "@/lib/queryClient"; +import { toast } from "@/hooks/use-toast"; +import { Maximize2, Minimize2, Download, X } from "lucide-react"; + +type Props = { + fileId: number | null; + isOpen: boolean; + onClose: () => void; + initialFileName?: string | null; +}; + +export default function DocumentsFilePreviewModal({ + fileId, + isOpen, + onClose, + initialFileName, +}: Props) { + const [loading, setLoading] = useState(false); + const [mime, setMime] = useState(null); + const [fileName, setFileName] = useState( + initialFileName ?? null + ); + const [blobUrl, setBlobUrl] = useState(null); + const [error, setError] = useState(null); + const [isFullscreen, setIsFullscreen] = useState(false); + + useEffect(() => { + if (!isOpen || !fileId) return; + let cancelled = false; + let createdUrl: string | null = null; + + async function load() { + setLoading(true); + setError(null); + setMime(null); + setFileName(initialFileName ?? null); + setBlobUrl(null); + + try { + // Fetch the file bytes using the Documents API endpoint (returns raw bytes) + const res = await apiRequest( + "GET", + `/api/documents/pdf-files/${fileId}` + ); + + if (!res.ok) { + // try to parse error message from JSON body + let msg = `Preview request failed (${res.status})`; + try { + const j = await res.json(); + msg = j?.message ?? msg; + } catch {} + throw new Error(msg); + } + + // try to infer MIME from headers; fallback to application/pdf + const contentType = + res.headers.get("content-type") ?? "application/pdf"; + setMime(contentType); + // If server provided filename in headers (Content-Disposition), we could parse it here. + // Use initialFileName if provided, otherwise keep unset until download. + if (!fileName && initialFileName) setFileName(initialFileName); + + const arrayBuffer = await res.arrayBuffer(); + if (cancelled) return; + const blob = new Blob([arrayBuffer], { type: contentType }); + createdUrl = URL.createObjectURL(blob); + setBlobUrl(createdUrl); + } catch (err: any) { + if (!cancelled) setError(err?.message ?? String(err)); + } finally { + if (!cancelled) setLoading(false); + } + } + + load(); + + return () => { + cancelled = true; + if (createdUrl) URL.revokeObjectURL(createdUrl); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isOpen, fileId]); + + useEffect(() => { + function onKey(e: KeyboardEvent) { + if (e.key === "Escape") onClose(); + } + if (isOpen) { + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + } + }, [isOpen, onClose]); + + if (!isOpen) return null; + + async function handleDownload() { + if (!fileId) return; + try { + const res = await apiRequest("GET", `/api/documents/pdf-files/${fileId}`); + if (!res.ok) { + const j = await res.json().catch(() => ({})); + throw new Error(j?.message || `Download failed (${res.status})`); + } + const arrayBuffer = await res.arrayBuffer(); + const blob = new Blob([arrayBuffer], { + type: mime ?? "application/octet-stream", + }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = fileName ?? `file-${fileId}`; + document.body.appendChild(a); + a.click(); + a.remove(); + setTimeout(() => URL.revokeObjectURL(url), 5000); + } catch (err: any) { + toast({ + title: "Download failed", + description: err?.message ?? String(err), + variant: "destructive", + }); + } + } + + const containerBase = + "bg-white rounded-md p-3 flex flex-col overflow-hidden shadow-xl"; + const sizeClass = isFullscreen + ? "w-[95vw] h-[95vh]" + : "w-[min(1200px,95vw)] h-[85vh]"; + + return ( +
+
+
+
+

+ {fileName ?? `File #${fileId}`} +

+
{mime ?? ""}
+
+ +
+ + + + + +
+
+ +
+ {loading && ( +
+ Loading preview… +
+ )} + {error &&
{error}
} + + {!loading && !error && blobUrl && mime?.startsWith("image/") && ( +
+ {fileName +
+ )} + + {!loading && + !error && + blobUrl && + (mime === "application/pdf" || mime?.endsWith("/pdf")) && ( +
+