| | 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; |
| |
|