Files
senmesh-zc/frontend/src/components/PDFPreview.tsx

138 lines
3.8 KiB
TypeScript
Raw Normal View History

import { useEffect, useMemo, useState } from "react";
type Props = {
url?: string;
};
export function PDFPreview({ url }: Props) {
const [loaded, setLoaded] = useState(false);
const [blobUrl, setBlobUrl] = useState<string | null>(null);
const [error, setError] = useState<string | null>(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 (
<div className="flex flex-col h-full w-full">
<div className="flex-auto overflow-auto bg-gray-100">
<div className="p-4">
<div
className="relative h-[calc(100vh-220px)] min-h-[480px] w-full overflow-auto rounded-md border bg-white"
>
{!!error && (
<div className="absolute inset-0 flex flex-col items-center justify-center gap-3 p-6 text-sm text-red-600">
<div>{error}</div>
</div>
)}
{!error && !loaded && (
<div className="absolute inset-0 flex flex-col items-center justify-center gap-3 p-6 text-sm text-muted-foreground">
<div>
{stage === "fetch" ? "Loading..." : "Opening viewer..."}
</div>
</div>
)}
{viewerSrc && (
<iframe
title="PDF Viewer"
src={viewerSrc}
className="h-full w-full border-0"
onLoad={() => {
setLoaded(true);
}}
/>
)}
</div>
</div>
</div>
</div>
);
}