Files
sitemente/components/mission-control/PDFViewerClient.tsx
T
horus 9c017b5967 feat(pdf-viewer): add proxy endpoint to bypass CSP for PDF loading
- Created /api/pdf-proxy route
- PDF viewer now uses proxy for URL loading
- Bypasses Content-Security-Policy restrictions
2026-03-23 22:27:36 +01:00

234 lines
8.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState, useRef, useEffect } from "react";
declare global {
interface Window {
pdfjsLib: any;
}
}
export default function PDFViewerClient() {
const [pdfData, setPdfData] = useState<string | null>(null);
const [pdfName, setPdfName] = useState<string>("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [numPages, setNumPages] = useState(0);
const [currentPage, setCurrentPage] = useState(1);
const [scale, setScale] = useState(1.5);
const [pdfjsLoaded, setPdfjsLoaded] = useState(false);
const canvasRef = useRef<HTMLCanvasElement>(null);
const pdfDocRef = useRef<any>(null);
useEffect(() => {
const script = document.createElement("script");
script.src = "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js";
script.async = true;
script.onload = () => {
try {
window.pdfjsLib = window.pdfjsLib;
window.pdfjsLib.GlobalWorkerOptions.workerSrc = "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js";
setPdfjsLoaded(true);
} catch (e) {
setError("Failed to initialize PDF.js");
}
};
script.onerror = () => {
setError("Failed to load PDF.js library");
};
document.body.appendChild(script);
}, []);
const renderPage = async (pageNum: number) => {
if (!pdfDocRef.current || !canvasRef.current || !window.pdfjsLib) return;
try {
const page = await pdfDocRef.current.getPage(pageNum);
const viewport = page.getViewport({ scale });
const canvas = canvasRef.current;
const context = canvas.getContext("2d");
canvas.height = viewport.height;
canvas.width = viewport.width;
await page.render({
canvasContext: context,
viewport: viewport,
}).promise;
} catch (err) {
console.error("Error rendering page:", err);
}
};
useEffect(() => {
if (pdfDocRef.current && currentPage && pdfjsLoaded) {
renderPage(currentPage);
}
}, [currentPage, scale, pdfjsLoaded]);
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file || !window.pdfjsLib) return;
if (file.type !== "application/pdf") {
setError("Please select a PDF file");
return;
}
setLoading(true);
setError(null);
setPdfName(file.name);
setCurrentPage(1);
try {
const arrayBuffer = await file.arrayBuffer();
const pdf = await window.pdfjsLib.getDocument({ data: arrayBuffer }).promise;
pdfDocRef.current = pdf;
setNumPages(pdf.numPages);
setPdfData("local");
await renderPage(1);
} catch (err) {
setError("Failed to load PDF. Try uploading to a public URL and use 'Load from URL'.");
console.error(err);
}
setLoading(false);
};
const handleUrlSubmit = async () => {
const input = prompt("Enter PDF URL:");
if (!input || !window.pdfjsLib) return;
setLoading(true);
setError(null);
setCurrentPage(1);
try {
new URL(input);
setPdfName(input.split("/").pop() || "document.pdf");
// Use our proxy to fetch the PDF
const proxyUrl = `https://sitemente.com/api/pdf-proxy?url=${encodeURIComponent(input)}`;
const response = await fetch(proxyUrl);
if (!response.ok) throw new Error("Failed to fetch through proxy");
const arrayBuffer = await response.arrayBuffer();
const pdf = await window.pdfjsLib.getDocument({ data: arrayBuffer }).promise;
pdfDocRef.current = pdf;
setNumPages(pdf.numPages);
setPdfData("proxied");
await renderPage(1);
} catch (err) {
setError("Failed to load PDF. Try another URL or upload file directly.");
console.error(err);
}
setLoading(false);
};
const goToPrevPage = () => {
if (currentPage > 1) setCurrentPage(currentPage - 1);
};
const goToNextPage = () => {
if (currentPage < numPages) setCurrentPage(currentPage + 1);
};
return (
<div className="flex flex-col h-full">
{/* Toolbar */}
<div className="bg-slate-900/50 border-b border-slate-800 px-6 py-3 flex-shrink-0">
<div className="flex items-center gap-4 flex-wrap">
<label className="cursor-pointer bg-blue-600 hover:bg-blue-500 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors">
📁 Upload PDF
<input
type="file"
accept="application/pdf"
onChange={handleFileUpload}
className="hidden"
disabled={!pdfjsLoaded}
/>
</label>
<button
onClick={handleUrlSubmit}
disabled={!pdfjsLoaded}
className="bg-slate-700 hover:bg-slate-600 disabled:opacity-50 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors"
>
🔗 Load from URL
</button>
{!pdfjsLoaded && (
<span className="text-yellow-400 text-sm animate-pulse">Loading PDF.js...</span>
)}
{pdfData && (
<>
<div className="h-6 w-px bg-slate-600 mx-2" />
<button onClick={goToPrevPage} disabled={currentPage <= 1}
className="bg-slate-700 hover:bg-slate-600 disabled:opacity-50 text-white px-3 py-2 rounded-lg text-sm">
Prev
</button>
<span className="text-slate-300 text-sm">Page {currentPage} of {numPages}</span>
<button onClick={goToNextPage} disabled={currentPage >= numPages}
className="bg-slate-700 hover:bg-slate-600 disabled:opacity-50 text-white px-3 py-2 rounded-lg text-sm">
Next
</button>
<div className="h-6 w-px bg-slate-600 mx-2" />
<button onClick={() => setScale(Math.max(0.5, scale - 0.25))}
className="bg-slate-700 hover:bg-slate-600 text-white px-3 py-2 rounded-lg text-sm"></button>
<span className="text-slate-300 text-sm w-16 text-center">{Math.round(scale * 100)}%</span>
<button onClick={() => setScale(scale + 0.25)}
className="bg-slate-700 hover:bg-slate-600 text-white px-3 py-2 rounded-lg text-sm"></button>
</>
)}
</div>
</div>
{/* Error */}
{error && (
<div className="bg-red-900/50 border border-red-700 rounded-lg p-4 m-4 max-w-md">
<h3 className="text-red-400 font-bold mb-2">Error</h3>
<p className="text-red-300 text-sm">{error}</p>
<button onClick={() => setError(null)}
className="mt-2 bg-red-700 hover:bg-red-600 text-white px-4 py-1 rounded text-sm">
Dismiss
</button>
</div>
)}
{/* Content */}
<div className="flex-1 overflow-auto bg-slate-800 p-4">
{!pdfData && !loading && (
<div className="flex items-center justify-center h-full">
<div className="text-center">
<div className="text-8xl mb-6 opacity-50">📄</div>
<h2 className="text-xl font-bold text-white mb-2">No PDF Loaded</h2>
<p className="text-slate-400 mb-6">Upload a PDF or enter a URL</p>
<p className="text-slate-500 text-sm">💡 Tip: Upload PDF file directly or paste a public URL</p>
</div>
</div>
)}
{loading && (
<div className="flex items-center justify-center h-full">
<div className="text-center">
<div className="animate-spin text-6xl mb-4"></div>
<p className="text-slate-400">Loading PDF...</p>
</div>
</div>
)}
{pdfData && !loading && (
<div className="flex justify-center">
<canvas
ref={canvasRef}
className="shadow-2xl rounded bg-white"
style={{ maxWidth: "100%", height: "auto" }}
/>
</div>
)}
</div>
</div>
);
}