import { useEffect, useMemo, useState } from "react"; type Props = { url?: string; }; export function PDFPreview({ url }: Props) { const [loaded, setLoaded] = useState(false); const [blobUrl, setBlobUrl] = useState(null); const [error, setError] = useState(null); const [stage, setStage] = useState<"fetch" | "viewer">("fetch"); if (!url) { return null; } const fileUrl = useMemo(() => { try { const u = new URL(url); if (import.meta.env.DEV && u.pathname.startsWith("/anon/")) { return `${u.pathname}${u.search}`; } return url; } catch { return url; } }, [url]); useEffect(() => { let revokedUrl: string | null = null; const controller = new AbortController(); const timeoutMs = 20000; setLoaded(false); setError(null); setStage("fetch"); setBlobUrl((prev) => { if (prev) URL.revokeObjectURL(prev); return null; }); (async () => { try { const resp = (await Promise.race([ fetch(fileUrl, { credentials: "include", signal: controller.signal, }), new Promise((_, reject) => setTimeout(() => { try { controller.abort(); } catch { // ignore } reject(new Error("PDF fetch timeout")); }, timeoutMs) ), ])) as Response; if (!resp.ok) { throw new Error(`HTTP ${resp.status}`); } const buf = await resp.arrayBuffer(); const head = new Uint8Array(buf.slice(0, 5)); const isPdf = head.length === 5 && head[0] === 0x25 && head[1] === 0x50 && head[2] === 0x44 && head[3] === 0x46 && head[4] === 0x2d; if (!isPdf) { throw new Error("Response is not a PDF"); } const blob = new Blob([buf], { type: "application/pdf" }); const nextUrl = URL.createObjectURL(blob); revokedUrl = nextUrl; setBlobUrl(nextUrl); setStage("viewer"); } catch (e) { if ((e as Error)?.name === "AbortError") return; setError((e as Error)?.message || "Failed to load PDF"); } })(); return () => { controller.abort(); if (revokedUrl) URL.revokeObjectURL(revokedUrl); }; }, [fileUrl]); const viewerSrc = useMemo(() => { if (!blobUrl) return null; const base = import.meta.env.BASE_URL; const viewerUrl = `${base}pdfjs/web/viewer.html?file=${encodeURIComponent(blobUrl)}`; return viewerUrl; }, [blobUrl]); return (
{!!error && (
{error}
)} {!error && !loaded && (
{stage === "fetch" ? "Loading..." : "Opening viewer..."}
)} {viewerSrc && (