|
|
import React, { useEffect, useState, useRef } from "react"; |
|
|
import * as pdfjsLib from "pdfjs-dist"; |
|
|
import { fetchNoteBlob } from "../api/notesService"; |
|
|
import { |
|
|
ChevronLeft, |
|
|
ChevronRight, |
|
|
ZoomIn, |
|
|
ZoomOut, |
|
|
Loader2, |
|
|
} from "lucide-react"; |
|
|
|
|
|
|
|
|
pdfjsLib.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjsLib.version}/build/pdf.worker.min.mjs`; |
|
|
|
|
|
interface Props { |
|
|
noteId: number; |
|
|
} |
|
|
|
|
|
const SecurePdfViewer: React.FC<Props> = ({ noteId }) => { |
|
|
const [pdfDoc, setPdfDoc] = useState<pdfjsLib.PDFDocumentProxy | null>(null); |
|
|
const [pageNum, setPageNum] = useState(1); |
|
|
const [scale, setScale] = useState(1.2); |
|
|
const [loading, setLoading] = useState(true); |
|
|
const [error, setError] = useState<string | null>(null); |
|
|
const canvasRef = useRef<HTMLCanvasElement>(null); |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
let isMounted = true; |
|
|
const loadPdf = async () => { |
|
|
try { |
|
|
setLoading(true); |
|
|
setError(null); |
|
|
|
|
|
|
|
|
const blob = await fetchNoteBlob(noteId); |
|
|
const arrayBuffer = await blob.arrayBuffer(); |
|
|
|
|
|
|
|
|
const loadedPdf = await pdfjsLib.getDocument({ data: arrayBuffer }) |
|
|
.promise; |
|
|
|
|
|
if (isMounted) { |
|
|
setPdfDoc(loadedPdf); |
|
|
setPageNum(1); |
|
|
setLoading(false); |
|
|
} |
|
|
} catch (err) { |
|
|
console.error("PDF Load Error:", err); |
|
|
if (isMounted) setError("Failed to load PDF. Please try again."); |
|
|
setLoading(false); |
|
|
} |
|
|
}; |
|
|
|
|
|
if (noteId) loadPdf(); |
|
|
|
|
|
return () => { |
|
|
isMounted = false; |
|
|
}; |
|
|
}, [noteId]); |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
if (!pdfDoc || !canvasRef.current) return; |
|
|
|
|
|
const renderPage = async () => { |
|
|
try { |
|
|
const page = await pdfDoc.getPage(pageNum); |
|
|
const viewport = page.getViewport({ scale }); |
|
|
const canvas = canvasRef.current!; |
|
|
const context = canvas.getContext("2d")!; |
|
|
|
|
|
|
|
|
const outputScale = window.devicePixelRatio || 1; |
|
|
canvas.width = Math.floor(viewport.width * outputScale); |
|
|
canvas.height = Math.floor(viewport.height * outputScale); |
|
|
canvas.style.width = Math.floor(viewport.width) + "px"; |
|
|
canvas.style.height = Math.floor(viewport.height) + "px"; |
|
|
|
|
|
const transform = |
|
|
outputScale !== 1 |
|
|
? [outputScale, 0, 0, outputScale, 0, 0] |
|
|
: undefined; |
|
|
|
|
|
await page.render({ |
|
|
canvasContext: context, |
|
|
canvas: canvas, |
|
|
viewport: viewport, |
|
|
transform: transform, |
|
|
}).promise; |
|
|
} catch (err) { |
|
|
console.error("Page Render Error:", err); |
|
|
} |
|
|
}; |
|
|
|
|
|
renderPage(); |
|
|
}, [pdfDoc, pageNum, scale]); |
|
|
|
|
|
if (loading) { |
|
|
return ( |
|
|
<div className="flex flex-col items-center justify-center h-full text-white"> |
|
|
<Loader2 className="w-8 h-8 animate-spin text-[#F7E396] mb-2" /> |
|
|
<p>Securely loading document...</p> |
|
|
</div> |
|
|
); |
|
|
} |
|
|
|
|
|
if (error) { |
|
|
return <div className="text-red-400 p-4 text-center">{error}</div>; |
|
|
} |
|
|
|
|
|
return ( |
|
|
<div className="flex flex-col h-full bg-[#525f88] rounded-xl overflow-hidden relative"> |
|
|
{/* Toolbar */} |
|
|
<div className="flex items-center justify-between p-2 bg-[#434E78] border-b border-white/10 text-white z-10 shadow-md"> |
|
|
<div className="flex items-center gap-2"> |
|
|
<button |
|
|
disabled={pageNum <= 1} |
|
|
onClick={() => setPageNum((p) => p - 1)} |
|
|
className="p-1 hover:bg-white/10 rounded disabled:opacity-30 transition" |
|
|
> |
|
|
<ChevronLeft size={20} /> |
|
|
</button> |
|
|
<span className="text-sm font-medium w-16 text-center"> |
|
|
{pageNum} / {pdfDoc?.numPages} |
|
|
</span> |
|
|
<button |
|
|
disabled={!pdfDoc || pageNum >= pdfDoc.numPages} |
|
|
onClick={() => setPageNum((p) => p + 1)} |
|
|
className="p-1 hover:bg-white/10 rounded disabled:opacity-30 transition" |
|
|
> |
|
|
<ChevronRight size={20} /> |
|
|
</button> |
|
|
</div> |
|
|
|
|
|
<div className="flex items-center gap-2"> |
|
|
<button |
|
|
onClick={() => setScale((s) => Math.max(0.5, s - 0.2))} |
|
|
className="p-1 hover:bg-white/10 rounded transition" |
|
|
> |
|
|
<ZoomOut size={18} /> |
|
|
</button> |
|
|
<span className="text-xs w-12 text-center"> |
|
|
{Math.round(scale * 100)}% |
|
|
</span> |
|
|
<button |
|
|
onClick={() => setScale((s) => Math.min(3.0, s + 0.2))} |
|
|
className="p-1 hover:bg-white/10 rounded transition" |
|
|
> |
|
|
<ZoomIn size={18} /> |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
{/* Scrollable Canvas Area */} |
|
|
<div className="flex-1 overflow-auto flex justify-center p-4 bg-[#525f88] custom-scrollbar"> |
|
|
<canvas ref={canvasRef} className="shadow-2xl" /> |
|
|
</div> |
|
|
</div> |
|
|
); |
|
|
}; |
|
|
|
|
|
export default SecurePdfViewer; |
|
|
|