Files
sitemente/components/mission-control/PDFViewerClient.tsx
T

248 lines
8.4 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(() => {
// Load PDF.js from CDN
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;
// Disable worker to avoid CSP issues
window.pdfjsLib.GlobalWorkerOptions.workerSrc = "";
window.pdfjsLib.GlobalWorkerOptions.workerPort = null;
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;
// Use synchronous rendering to avoid worker
const renderContext = {
canvasContext: context,
viewport: viewport,
};
// For older pdf.js without worker, use direct render
await page.render(renderContext).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();
// Disable worker for this load
const loadingTask = window.pdfjsLib.getDocument({
data: arrayBuffer,
disableWorker: true,
verbosity: 0
});
const pdf = await loadingTask.promise;
pdfDocRef.current = pdf;
setNumPages(pdf.numPages);
setPdfData("local");
await renderPage(1);
} catch (err) {
setError("Failed to load PDF. Try using 'Load from URL' instead.");
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 Google Docs viewer for external URLs
setPdfData(`https://docs.google.com/gview?url=${encodeURIComponent(input)}&embedded=true`);
setNumPages(1); // We don't know the page count for external URLs
} catch (err) {
setError("Failed to load PDF from URL.");
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: For best results, use "Load from URL" with a public PDF link</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">
{pdfData.startsWith("https://docs.google.com") ? (
// Google Docs viewer for external URLs
<iframe
src={pdfData}
className="w-full h-full min-h-[700px] bg-white rounded shadow-2xl"
title="PDF Viewer"
/>
) : (
// Canvas for uploaded files
<canvas
ref={canvasRef}
className="shadow-2xl rounded bg-white"
style={{ maxWidth: "100%", height: "auto" }}
/>
)}
</div>
)}
</div>
</div>
);
}