import React, { useState, useEffect, useRef } from "react"; import * as Dialog from "@radix-ui/react-dialog"; import { X, Download, FileText, FileCode, FileImage, FileAudio, FileVideo, FileArchive, Presentation, Mail, } from "lucide-react"; import DocViewer, { DocViewerRenderers } from "@cyntler/react-doc-viewer"; import ReactMarkdown from "react-markdown"; import { renderAsync } from "docx-preview"; import { PPTXViewer, parsePPTX } from "@kandiforge/pptx-renderer"; import { Button } from "@/components/ds/ui/button"; import { EmailViewer } from "./EmailViewer"; import { useTranslate } from "ra-core"; // Maximum file size: 50MB const MAX_FILE_SIZE = 50 * 1024 * 1024; interface DocumentViewerProps { url: string; title: string; type?: string; file?: any; open: boolean; onOpenChange: (open: boolean) => void; } export const DocumentViewer = ({ url, title, type, file, open, onOpenChange, }: DocumentViewerProps) => { const [content, setContent] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [docxBuffer, setDocxBuffer] = useState(null); const [emlBuffer, setEmlBuffer] = useState(null); const docxRef = useRef(null); const translate = useTranslate(); // Effect for DOCX content rendering useEffect(() => { if (docxBuffer && docxRef.current) { const renderDocx = async () => { try { if (docxRef.current) { docxRef.current.innerHTML = ""; await renderAsync(docxBuffer, docxRef.current); // Clear buffer after successful render to prevent memory leak setDocxBuffer(null); } } catch (err) { console.error("Failed to render DOCX:", err); setError(translate("crm.document_viewer.error.docx")); } }; renderDocx(); } }, [docxBuffer, translate]); useEffect(() => { if (!open) { setDocxBuffer(null); setContent(null); setError(null); setLoading(false); setEmlBuffer(null); return; } const abortController = new AbortController(); const extension = title.split(".").pop()?.toLowerCase(); const mimeType = type || getMimeTypeFromExtension(extension); const isSpreadsheet = extension === "xlsx" || extension === "xls"; setLoading(true); setError(null); const loadContent = async () => { const getArrayBuffer = async () => { // Priority 1: Use File object if available (handles blob URLs) if (file && typeof file.arrayBuffer === "function") { return await file.arrayBuffer(); } // Priority 2: For blob URLs, fetch them directly if (url.startsWith("blob:")) { const response = await fetch(url, { signal: abortController.signal }); if (!response.ok) throw new Error(`Failed to fetch blob (${response.status})`); return await response.arrayBuffer(); } // Priority 3: Regular HTTP URLs with size check const response = await fetch(url, { signal: abortController.signal }); if (!response.ok) throw new Error(`Failed to fetch (${response.status})`); // Check file size before downloading const contentLength = response.headers.get("content-length"); if (contentLength && parseInt(contentLength) > MAX_FILE_SIZE) { throw new Error( translate("crm.document_viewer.error.too_large", { size: (parseInt(contentLength) / 1024 / 1024).toFixed(1), }), ); } return await response.arrayBuffer(); }; const getText = async () => { // Priority 1: Use File object if available if (file && typeof file.text === "function") { return await file.text(); } // Priority 2: For blob URLs, fetch them directly if (url.startsWith("blob:")) { const response = await fetch(url, { signal: abortController.signal }); if (!response.ok) throw new Error(`Failed to fetch blob (${response.status})`); return await response.text(); } // Priority 3: Regular HTTP URLs with size check const response = await fetch(url, { signal: abortController.signal }); if (!response.ok) throw new Error(`Failed to fetch (${response.status})`); // Check file size before downloading const contentLength = response.headers.get("content-length"); if (contentLength && parseInt(contentLength) > MAX_FILE_SIZE) { throw new Error( translate("crm.document_viewer.error.too_large", { size: (parseInt(contentLength) / 1024 / 1024).toFixed(1), }), ); } return await response.text(); }; try { if (extension === "eml") { const buffer = await getArrayBuffer(); setEmlBuffer(buffer); } else if (extension === "docx") { const buffer = await getArrayBuffer(); setDocxBuffer(buffer); setContent(
, ); } else if (extension === "md" || extension === "markdown") { const text = await getText(); setContent(
{text}
, ); } else if ( isSpreadsheet || mimeType?.startsWith("image/") || mimeType?.startsWith("video/") || mimeType?.startsWith("audio/") || mimeType === "application/pdf" ) { setContent( , ); } else if (extension === "pptx") { const buffer = await getArrayBuffer(); const pptxData = await parsePPTX(buffer); setContent(
, ); } else if (mimeType?.startsWith("text/")) { const text = await getText(); setContent(
              {text}
            
, ); } else { setContent(

{title}

{translate("crm.document_viewer.error.not_available")}

, ); } } catch (err) { // Ignore aborted requests if (err instanceof Error && err.name === "AbortError") { return; } console.error("Failed to load document:", err); const isFetchError = err instanceof TypeError && err.message.includes("Failed to fetch"); const msg = isFetchError ? translate("crm.document_viewer.error.access") : err instanceof Error ? err.message : translate("crm.document_viewer.error.load"); setError(msg); } finally { setLoading(false); } }; loadContent(); // Cleanup: abort pending requests when component unmounts or URL changes return () => abortController.abort(); }, [url, title, type, open, file, translate]); return (
{title}
{loading && (
)} {error && (

{error}

)} {!loading && !error && (emlBuffer ? ( ) : ( content ))}
); }; const FileIcon = ({ extension, className, }: { extension?: string; className?: string; }) => { switch (extension) { case "pdf": return ; case "docx": case "doc": return ; case "xlsx": case "xls": case "csv": return ; case "jpg": case "jpeg": case "png": case "gif": case "webp": return ; case "mp4": case "mov": case "avi": return ; case "mp3": case "wav": case "ogg": return ; case "pptx": case "ppt": return ; case "eml": return ; default: return ; } }; const getMimeTypeFromExtension = (ext?: string): string => { const mimeTypes: Record = { pdf: "application/pdf", docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", doc: "application/msword", xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", xls: "application/vnd.ms-excel", csv: "text/csv", pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation", ppt: "application/vnd.ms-powerpoint", png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif", webp: "image/webp", txt: "text/plain", md: "text/markdown", markdown: "text/markdown", mp4: "video/mp4", mp3: "audio/mpeg", eml: "message/rfc822", }; return ext ? mimeTypes[ext] || "application/octet-stream" : "application/octet-stream"; };