File size: 5,114 Bytes
dd30f85 1d87e2c dd30f85 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 |
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";
// Configure PDF Worker (Required for pdfjs-dist)
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);
// 1. Fetch and Load PDF Data
useEffect(() => {
let isMounted = true;
const loadPdf = async () => {
try {
setLoading(true);
setError(null);
// Fetch securely using existing Axios setup (sends Auth header)
const blob = await fetchNoteBlob(noteId);
const arrayBuffer = await blob.arrayBuffer();
// Load into PDF.js
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]);
// 2. Render Page on Canvas
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")!;
// Handle High DPI screens
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, // <--- FIX APPLIED HERE
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;
|