Files
sitemente/components/mission-control/PDFViewerClient.tsx
T
horus 746615e095 fix(pdf-viewer): load PDF.js from CDN instead of npm
- Loads PDF.js from CDN to avoid SSR issues
- Component handles loading state while PDF.js initializes
- No more dynamic import errors
2026-03-23 21:54:13 +01:00

264 lines
8.5 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 = () => {
window.pdfjsLib = window.pdfjsLib;
setPdfjsLoaded(true);
};
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");
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");
const response = await fetch(input);
const arrayBuffer = await response.arrayBuffer();
const pdf = await window.pdfjsLib.getDocument({ data: arrayBuffer }).promise;
pdfDocRef.current = pdf;
setNumPages(pdf.numPages);
setPdfData("url");
await renderPage(1);
} catch (err) {
setError("Failed to load PDF from URL. Make sure the URL is publicly accessible.");
console.error(err);
}
setLoading(false);
};
const goToPrevPage = () => {
if (currentPage > 1) {
setCurrentPage(currentPage - 1);
}
};
const goToNextPage = () => {
if (currentPage < numPages) {
setCurrentPage(currentPage + 1);
}
};
const zoomIn = () => setScale(scale + 0.25);
const zoomOut = () => setScale(Math.max(0.5, scale - 0.25));
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-slate-400 text-sm">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 disabled:cursor-not-allowed text-white px-3 py-2 rounded-lg text-sm font-medium transition-colors"
>
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 disabled:cursor-not-allowed text-white px-3 py-2 rounded-lg text-sm font-medium transition-colors"
>
Next
</button>
<div className="h-6 w-px bg-slate-600 mx-2" />
<button
onClick={zoomOut}
className="bg-slate-700 hover:bg-slate-600 text-white px-3 py-2 rounded-lg text-sm font-medium transition-colors"
>
</button>
<span className="text-slate-300 text-sm w-16 text-center">
{Math.round(scale * 100)}%
</span>
<button
onClick={zoomIn}
className="bg-slate-700 hover:bg-slate-600 text-white px-3 py-2 rounded-lg text-sm font-medium transition-colors"
>
</button>
</>
)}
</div>
</div>
{/* Content */}
<div className="flex-1 overflow-auto bg-slate-800 p-4">
{error && (
<div className="bg-red-900/50 border border-red-700 rounded-lg p-4 mb-4 max-w-md">
<h3 className="text-red-400 font-bold mb-2">Error</h3>
<p className="text-red-300">{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>
)}
{!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 to view it</p>
<div className="text-slate-500 text-sm space-y-1">
<p> Upload your resume to let Horus analyze it</p>
<p> Supports PDF files up to 50MB</p>
<p> Or load from any public PDF URL</p>
</div>
</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"
/>
</div>
)}
</div>
{/* Instructions */}
<div className="bg-slate-900/50 border-t border-slate-800 px-6 py-2 flex-shrink-0">
<p className="text-slate-500 text-xs">
💡 Tip: Upload your resume PDF and Horus can analyze the design to recreate it
</p>
</div>
</div>
);
}