From 107329618ef82aeefcc6fbeb239d81d1bb89de26 Mon Sep 17 00:00:00 2001 From: Grishma Dhakal Date: Tue, 10 Feb 2026 14:23:43 +0545 Subject: [PATCH 1/2] feat(ui): added reat-pdf for displaying pdf --- package.json | 2 + pnpm-lock.yaml | 197 +++++++++++++++++++++- src/modules/study/features/pdf-viewer.tsx | 158 ++++++++++++++++- 3 files changed, 346 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 9b649aa..2d21ce9 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "lucide-react": "^0.563.0", "next": "^15.2.3", "next-themes": "^0.4.6", + "pdfjs-dist": "5.4.296", "postgres": "^3.4.4", "radix-ui": "^1.4.3", "react": "^19.0.0", @@ -51,6 +52,7 @@ "react-dom": "^19.0.0", "react-hook-form": "^7.71.1", "react-markdown": "^10.1.0", + "react-pdf": "^10.3.0", "react-resizable-panels": "^4.6.2", "react-shiki": "^0.9.1", "rehype-katex": "^7.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6b3d84e..cc095dc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -80,6 +80,9 @@ importers: next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + pdfjs-dist: + specifier: 5.4.296 + version: 5.4.296 postgres: specifier: ^3.4.4 version: 3.4.8 @@ -101,6 +104,9 @@ importers: react-markdown: specifier: ^10.1.0 version: 10.1.0(@types/react@19.2.13)(react@19.2.4) + react-pdf: + specifier: ^10.3.0 + version: 10.3.0(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react-resizable-panels: specifier: ^4.6.2 version: 4.6.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -145,7 +151,7 @@ importers: version: 5.1.0 unpdf: specifier: ^1.4.0 - version: 1.4.0 + version: 1.4.0(@napi-rs/canvas@0.1.91) uploadthing: specifier: ^7.7.4 version: 7.7.4(express@5.2.1)(next@15.5.12(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(tailwindcss@4.1.18) @@ -1091,6 +1097,81 @@ packages: resolution: {integrity: sha512-7G0Uf0yK3f2bjElBLGHIQzgRgMESczOMyYVasq1XK8P5HaXtlW4eQhz9MBL+TQILZLaruq+ClGId+hH0w4jvWw==} engines: {node: '>=18'} + '@napi-rs/canvas-android-arm64@0.1.91': + resolution: {integrity: sha512-SLLzXXgSnfct4zy/BVAfweZQkYkPJsNsJ2e5DOE8DFEHC6PufyUrwb12yqeu2So2IOIDpWJJaDAxKY/xpy6MYQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@napi-rs/canvas-darwin-arm64@0.1.91': + resolution: {integrity: sha512-bzdbCjIjw3iRuVFL+uxdSoMra/l09ydGNX9gsBxO/zg+5nlppscIpj6gg+nL6VNG85zwUarDleIrUJ+FWHvmuA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@napi-rs/canvas-darwin-x64@0.1.91': + resolution: {integrity: sha512-q3qpkpw0IsG9fAS/dmcGIhCVoNxj8ojbexZKWwz3HwxlEWsLncEQRl4arnxrwbpLc2nTNTyj4WwDn7QR5NDAaA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@napi-rs/canvas-linux-arm-gnueabihf@0.1.91': + resolution: {integrity: sha512-Io3g8wJZVhK8G+Fpg1363BE90pIPqg+ZbeehYNxPWDSzbgwU3xV0l8r/JBzODwC7XHi1RpFEk+xyUTMa2POj6w==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@napi-rs/canvas-linux-arm64-gnu@0.1.91': + resolution: {integrity: sha512-HBnto+0rxx1bQSl8bCWA9PyBKtlk2z/AI32r3cu4kcNO+M/5SD4b0v1MWBWZyqMQyxFjWgy3ECyDjDKMC6tY1A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@napi-rs/canvas-linux-arm64-musl@0.1.91': + resolution: {integrity: sha512-/eJtVe2Xw9A86I4kwXpxxoNagdGclu12/NSMsfoL8q05QmeRCbfjhg1PJS7ENAuAvaiUiALGrbVfeY1KU1gztQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@napi-rs/canvas-linux-riscv64-gnu@0.1.91': + resolution: {integrity: sha512-floNK9wQuRWevUhhXRcuis7h0zirdytVxPgkonWO+kQlbvxV7gEUHGUFQyq4n55UHYFwgck1SAfJ1HuXv/+ppQ==} + engines: {node: '>= 10'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@napi-rs/canvas-linux-x64-gnu@0.1.91': + resolution: {integrity: sha512-c3YDqBdf7KETuZy2AxsHFMsBBX1dWT43yFfWUq+j1IELdgesWtxf/6N7csi3VPf6VA3PmnT9EhMyb+M1wfGtqw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@napi-rs/canvas-linux-x64-musl@0.1.91': + resolution: {integrity: sha512-RpZ3RPIwgEcNBHSHSX98adm+4VP8SMT5FN6250s5jQbWpX/XNUX5aLMfAVJS/YnDjS1QlsCgQxFOPU0aCCWgag==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@napi-rs/canvas-win32-arm64-msvc@0.1.91': + resolution: {integrity: sha512-gF8MBp4X134AgVurxqlCdDA2qO0WaDdi9o6Sd5rWRVXRhWhYQ6wkdEzXNLIrmmros0Tsp2J0hQzx4ej/9O8trQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@napi-rs/canvas-win32-x64-msvc@0.1.91': + resolution: {integrity: sha512-++gtW9EV/neKI8TshD8WFxzBYALSPag2kFRahIJV+LYsyt5kBn21b1dBhEUDHf7O+wiZmuFCeUa7QKGHnYRZBA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@napi-rs/canvas@0.1.91': + resolution: {integrity: sha512-eeIe1GoB74P1B0Nkw6pV8BCQ3hfCfvyYr4BntzlCsnFXzVJiPMDnLeIx3gVB0xQMblHYnjK/0nCLvirEhOjr5g==} + engines: {node: '>= 10'} + '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} @@ -3927,6 +4008,12 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + make-cancellable-promise@2.0.0: + resolution: {integrity: sha512-3SEQqTpV9oqVsIWqAcmDuaNeo7yBO3tqPtqGRcKkEo0lrzD3wqbKG9mkxO65KoOgXqj+zH2phJ2LiAsdzlogSw==} + + make-event-props@2.0.0: + resolution: {integrity: sha512-G/hncXrl4Qt7mauJEXSg3AcdYzmpkIITTNl5I+rH9sog5Yw0kK6vseJjCaPfOXqOqQuPUP89Rkhfz5kPS8ijtw==} + markdown-table@3.0.4: resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} @@ -3993,6 +4080,14 @@ packages: resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} engines: {node: '>=18'} + merge-refs@2.0.0: + resolution: {integrity: sha512-3+B21mYK2IqUWnd2EivABLT7ueDhb0b8/dGK8LoFQPrU61YITeCMn14F7y7qZafWNZhUEKb24cJdiT5Wxs3prg==} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -4365,6 +4460,10 @@ packages: path-to-regexp@8.3.0: resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + pdfjs-dist@5.4.296: + resolution: {integrity: sha512-DlOzet0HO7OEnmUmB6wWGJrrdvbyJKftI1bhMitK7O2N8W2gc757yyYBbINy9IDafXAV9wmKr9t7xsTaNKRG5Q==} + engines: {node: '>=20.16.0 || >=22.3.0'} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -4561,6 +4660,16 @@ packages: '@types/react': '>=18' react: '>=18' + react-pdf@10.3.0: + resolution: {integrity: sha512-2LQzC9IgNVAX8gM+6F+1t/70a9/5RWThYxc+CWAmT2LW/BRmnj+35x1os5j/nR2oldyf8L+hCAMBmVKU8wrYFA==} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + react-remove-scroll-bar@2.3.8: resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} engines: {node: '>=10'} @@ -5203,6 +5312,9 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + warning@4.0.3: + resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==} + web-namespaces@2.0.1: resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} @@ -6071,6 +6183,54 @@ snapshots: outvariant: 1.4.3 strict-event-emitter: 0.5.1 + '@napi-rs/canvas-android-arm64@0.1.91': + optional: true + + '@napi-rs/canvas-darwin-arm64@0.1.91': + optional: true + + '@napi-rs/canvas-darwin-x64@0.1.91': + optional: true + + '@napi-rs/canvas-linux-arm-gnueabihf@0.1.91': + optional: true + + '@napi-rs/canvas-linux-arm64-gnu@0.1.91': + optional: true + + '@napi-rs/canvas-linux-arm64-musl@0.1.91': + optional: true + + '@napi-rs/canvas-linux-riscv64-gnu@0.1.91': + optional: true + + '@napi-rs/canvas-linux-x64-gnu@0.1.91': + optional: true + + '@napi-rs/canvas-linux-x64-musl@0.1.91': + optional: true + + '@napi-rs/canvas-win32-arm64-msvc@0.1.91': + optional: true + + '@napi-rs/canvas-win32-x64-msvc@0.1.91': + optional: true + + '@napi-rs/canvas@0.1.91': + optionalDependencies: + '@napi-rs/canvas-android-arm64': 0.1.91 + '@napi-rs/canvas-darwin-arm64': 0.1.91 + '@napi-rs/canvas-darwin-x64': 0.1.91 + '@napi-rs/canvas-linux-arm-gnueabihf': 0.1.91 + '@napi-rs/canvas-linux-arm64-gnu': 0.1.91 + '@napi-rs/canvas-linux-arm64-musl': 0.1.91 + '@napi-rs/canvas-linux-riscv64-gnu': 0.1.91 + '@napi-rs/canvas-linux-x64-gnu': 0.1.91 + '@napi-rs/canvas-linux-x64-musl': 0.1.91 + '@napi-rs/canvas-win32-arm64-msvc': 0.1.91 + '@napi-rs/canvas-win32-x64-msvc': 0.1.91 + optional: true + '@napi-rs/wasm-runtime@0.2.12': dependencies: '@emnapi/core': 1.8.1 @@ -8930,6 +9090,10 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + make-cancellable-promise@2.0.0: {} + + make-event-props@2.0.0: {} + markdown-table@3.0.4: {} math-intrinsics@1.1.0: {} @@ -9117,6 +9281,10 @@ snapshots: merge-descriptors@2.0.0: {} + merge-refs@2.0.0(@types/react@19.2.13): + optionalDependencies: + '@types/react': 19.2.13 + merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -9638,6 +9806,10 @@ snapshots: path-to-regexp@8.3.0: {} + pdfjs-dist@5.4.296: + optionalDependencies: + '@napi-rs/canvas': 0.1.91 + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -9823,6 +9995,21 @@ snapshots: transitivePeerDependencies: - supports-color + react-pdf@10.3.0(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + clsx: 2.1.1 + dequal: 2.0.3 + make-cancellable-promise: 2.0.0 + make-event-props: 2.0.0 + merge-refs: 2.0.0(@types/react@19.2.13) + pdfjs-dist: 5.4.296 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + tiny-invariant: 1.3.3 + warning: 4.0.3 + optionalDependencies: + '@types/react': 19.2.13 + react-remove-scroll-bar@2.3.8(@types/react@19.2.13)(react@19.2.4): dependencies: react: 19.2.4 @@ -10565,7 +10752,9 @@ snapshots: universalify@2.0.1: {} - unpdf@1.4.0: {} + unpdf@1.4.0(@napi-rs/canvas@0.1.91): + optionalDependencies: + '@napi-rs/canvas': 0.1.91 unpipe@1.0.0: {} @@ -10657,6 +10846,10 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 + warning@4.0.3: + dependencies: + loose-envify: 1.4.0 + web-namespaces@2.0.1: {} web-streams-polyfill@3.3.3: {} diff --git a/src/modules/study/features/pdf-viewer.tsx b/src/modules/study/features/pdf-viewer.tsx index c18f6bd..a935a98 100644 --- a/src/modules/study/features/pdf-viewer.tsx +++ b/src/modules/study/features/pdf-viewer.tsx @@ -1,17 +1,63 @@ "use client"; +import { useEffect, useState, useRef } from "react"; +import { Document, Page, pdfjs } from "react-pdf"; +import { + ChevronLeft, + ChevronRight, + ZoomIn, + ZoomOut, + RotateCw, + Loader2, +} from "lucide-react"; +import { Button } from "~/components/ui/button"; +import "react-pdf/dist/Page/AnnotationLayer.css"; +import "react-pdf/dist/Page/TextLayer.css"; + +// Configure worker +pdfjs.GlobalWorkerOptions.workerSrc = new URL( + "pdfjs-dist/build/pdf.worker.min.mjs", + import.meta.url +).toString(); + interface PdfViewerProps { fileUrl: string | null; - title: string; } -export function PdfViewer({ fileUrl, title }: PdfViewerProps) { +export function PdfViewer({ fileUrl }: PdfViewerProps) { + const [numPages, setNumPages] = useState(0); + const [pageNumber, setPageNumber] = useState(1); + const [scale, setScale] = useState(1); + const [rotation, setRotation] = useState(0); + const [loading, setLoading] = useState(true); + const containerRef = useRef(null); + + // Reset state when file changes + useEffect(() => { + setPageNumber(1); + setRotation(0); + setScale(1); + setLoading(true); + }, [fileUrl]); + + function onDocumentLoadSuccess({ numPages }: { numPages: number }) { + setNumPages(numPages); + setLoading(false); + } + + const handleZoomIn = () => setScale((prev) => Math.min(prev + 0.2, 3)); + const handleZoomOut = () => setScale((prev) => Math.max(prev - 0.2, 0.5)); + const handleRotate = () => setRotation((prev) => (prev + 90) % 360); + const handlePrevPage = () => setPageNumber((prev) => Math.max(prev - 1, 1)); + const handleNextPage = () => + setPageNumber((prev) => Math.min(prev + 1, numPages)); + if (!fileUrl) { return (
{/* Glassmorphism Paper Effect */} -
+
@@ -21,7 +67,7 @@ export function PdfViewer({ fileUrl, title }: PdfViewerProps) {
- + No PDF uploaded
@@ -30,10 +76,104 @@ export function PdfViewer({ fileUrl, title }: PdfViewerProps) { } return ( -