Files
sitemente/components/mission-control/PDFViewerClient.tsx
T
horus 76e37e2363 fix(pdf-viewer): use PDF.js with client-side rendering
- Created PDFViewerClient component with PDF.js
- Uses dynamic import with ssr: false to avoid server-side issues
- Full page navigation and zoom controls
- Upload or load from URL
2026-03-23 21:51:16 +01:00

242 lines
7.9 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";
import * as pdfjsLib from "pdfjs-dist";
// Set worker source - use CDN
pdfjsLib.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjsLib.version}/pdf.worker.min.js`;
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 canvasRef = useRef<HTMLCanvasElement>(null);
const pdfDocRef = useRef<any>(null);
const renderPage = async (pageNum: number) => {
if (!pdfDocRef.current || !canvasRef.current) 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) {
renderPage(currentPage);
}
}, [currentPage, scale]);
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) 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 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) 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 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"
/>
</label>
<button
onClick={handleUrlSubmit}
className="bg-slate-700 hover:bg-slate-600 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors"
>
🔗 Load from URL
</button>
{pdfData && (
<>
<div className="h-6 w-px bg-slate-600 mx-2" />
{/* Page navigation */}
<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" />
{/* Zoom controls */}
<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>
);
}