2
2
3
3
import "react-pdf/dist/Page/AnnotationLayer.css" ;
4
4
import "react-pdf/dist/Page/TextLayer.css" ;
5
- import { useState } from "react" ;
5
+ import { useState , useRef , useCallback , useEffect } from "react" ;
6
6
import { Document , Page , pdfjs } from "react-pdf" ;
7
7
import { Download , ZoomIn , ZoomOut } from "lucide-react" ;
8
8
import { Button } from "./ui/button" ;
@@ -24,51 +24,98 @@ interface PdfViewerProps {
24
24
export default function PdfViewer ( { url, name } : PdfViewerProps ) {
25
25
const [ numPages , setNumPages ] = useState < number > ( ) ;
26
26
const [ pageNumber , setPageNumber ] = useState < number > ( 1 ) ;
27
- const [ scale , setScale ] = useState < number > ( 1 ) ; // Default zoom level (100%)
27
+ const [ scale , setScale ] = useState < number > ( 1 ) ;
28
+ const pageRefs = useRef < ( HTMLDivElement | null ) [ ] > ( [ ] ) ;
29
+ const containerRef = useRef < HTMLDivElement > ( null ) ;
28
30
29
- // Handle document load success
30
31
function onDocumentLoadSuccess ( { numPages } : { numPages : number } ) : void {
31
32
setNumPages ( numPages ) ;
32
- setPageNumber ( 1 ) ; // Reset to page 1 when new document loads
33
+ setPageNumber ( 1 ) ;
34
+ pageRefs . current = Array ( numPages ) . fill ( null ) as ( HTMLDivElement | null ) [ ] ;
33
35
}
34
36
35
- // Navigate to previous page
37
+ const scrollToPage = useCallback ( ( page : number ) => {
38
+ if ( pageRefs . current [ page - 1 ] && containerRef . current ) {
39
+ const pageElement = pageRefs . current [ page - 1 ] ;
40
+ const container = containerRef . current ;
41
+ if ( pageElement ) {
42
+ const offset = pageElement . offsetTop - container . offsetTop ;
43
+ container . scrollTo ( { top : offset , behavior : "smooth" } ) ;
44
+ setPageNumber ( page ) ;
45
+ }
46
+ }
47
+ } , [ ] ) ;
48
+
49
+ const handleScroll = useCallback ( ( ) => {
50
+ if ( ! containerRef . current || ! pageRefs . current ) return ;
51
+ const container = containerRef . current ;
52
+ const scrollTop = container . scrollTop + container . offsetTop ;
53
+
54
+ for ( let i = 0 ; i < pageRefs . current . length ; i ++ ) {
55
+ const pageEl = pageRefs . current [ i ] ;
56
+ if ( pageEl ) {
57
+ const pageTop = pageEl . offsetTop ;
58
+ const pageBottom = pageTop + pageEl . offsetHeight ;
59
+ if ( scrollTop >= pageTop && scrollTop < pageBottom ) {
60
+ setPageNumber ( i + 1 ) ;
61
+ break ;
62
+ }
63
+ }
64
+ }
65
+ } , [ ] ) ;
66
+
67
+ useEffect ( ( ) => {
68
+ const container = containerRef . current ;
69
+ if ( container ) {
70
+ container . addEventListener ( "scroll" , handleScroll ) ;
71
+ return ( ) => container . removeEventListener ( "scroll" , handleScroll ) ;
72
+ }
73
+ } , [ handleScroll ] ) ;
74
+
36
75
const goToPreviousPage = ( ) => {
37
- setPageNumber ( ( prev ) => Math . max ( 1 , prev - 1 ) ) ;
76
+ setPageNumber ( ( prev ) => {
77
+ const newPage = Math . max ( 1 , prev - 1 ) ;
78
+ scrollToPage ( newPage ) ;
79
+ return newPage ;
80
+ } ) ;
38
81
} ;
39
82
40
- // Navigate to next page
41
83
const goToNextPage = ( ) => {
42
- setPageNumber ( ( prev ) => Math . min ( numPages ?? 1 , prev + 1 ) ) ;
84
+ setPageNumber ( ( prev ) => {
85
+ const newPage = Math . min ( numPages ?? 1 , prev + 1 ) ;
86
+ scrollToPage ( newPage ) ;
87
+ return newPage ;
88
+ } ) ;
43
89
} ;
44
90
45
- // Handle page number input change
46
91
const handlePageChange = ( e : React . ChangeEvent < HTMLInputElement > ) => {
47
92
const value = parseInt ( e . target . value , 10 ) ;
48
93
if ( ! isNaN ( value ) && value >= 1 && value <= ( numPages ?? 1 ) ) {
49
94
setPageNumber ( value ) ;
95
+ scrollToPage ( value ) ;
50
96
}
51
97
} ;
52
98
53
- // Zoom in (increase scale)
54
99
const zoomIn = ( ) => {
55
- setScale ( ( prev ) => Math . min ( prev + 0.25 , 3 ) ) ; // Max scale: 300%
100
+ setScale ( ( prev ) => Math . min ( prev + 0.25 , 3 ) ) ;
56
101
} ;
57
102
58
- // Zoom out (decrease scale)
59
103
const zoomOut = ( ) => {
60
- setScale ( ( prev ) => Math . max ( prev - 0.25 , 0.25 ) ) ; // Min scale: 25%
104
+ setScale ( ( prev ) => Math . max ( prev - 0.25 , 0.25 ) ) ;
61
105
} ;
106
+
62
107
const downloadPDF = async ( ) => {
63
108
const fileName = `${ name } .pdf` ;
64
109
await downloadFile ( url , fileName ) ;
65
110
} ;
111
+
66
112
return (
67
113
< div className = "flex flex-col items-center" >
68
- { /* PDF Document */ }
69
- < div className = "max-h-[70vh] overflow-auto border border-gray-300 shadow-lg" >
114
+ < div
115
+ ref = { containerRef }
116
+ className = "max-h-[70vh] overflow-auto border border-gray-300 shadow-lg"
117
+ >
70
118
< Document
71
- className = "flex justify-center"
72
119
file = { url }
73
120
onLoadSuccess = { onDocumentLoadSuccess }
74
121
error = {
@@ -83,20 +130,27 @@ export default function PdfViewer({ url, name }: PdfViewerProps) {
83
130
< div className = "p-4 text-gray-500" > No PDF file specified.</ div >
84
131
}
85
132
>
86
- < Page
87
- pageNumber = { pageNumber }
88
- scale = { scale }
89
- renderAnnotationLayer = { true }
90
- renderTextLayer = { true }
91
- className = "w-max-[75vw] shadow-md"
92
- />
133
+ { numPages &&
134
+ Array . from ( { length : numPages } , ( _ , index ) => (
135
+ < div
136
+ key = { `page_${ index + 1 } ` }
137
+ ref = { ( el ) => {
138
+ pageRefs . current [ index ] = el ;
139
+ } }
140
+ >
141
+ < Page
142
+ pageNumber = { index + 1 }
143
+ scale = { scale }
144
+ renderAnnotationLayer = { true }
145
+ renderTextLayer = { true }
146
+ className = "w-max-[75vw] mb-4 shadow-md"
147
+ />
148
+ </ div >
149
+ ) ) }
93
150
</ Document >
94
151
</ div >
95
152
96
- { /* Controls */ }
97
153
< div className = "mt-4 flex flex-col items-center gap-4 rounded-lg bg-[#262635] p-4 shadow sm:flex-row" >
98
- { /* Page Navigation */ }
99
-
100
154
< div className = "flex items-center gap-2" >
101
155
< Button
102
156
onClick = { goToPreviousPage }
@@ -111,7 +165,7 @@ export default function PdfViewer({ url, name }: PdfViewerProps) {
111
165
onChange = { handlePageChange }
112
166
min = { 1 }
113
167
max = { numPages }
114
- className = "h-10 w-16 rounded border p-1 text-center"
168
+ className = "h-10 w-16 rounded border p-1 text-center [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none "
115
169
/>
116
170
< span > of { numPages ?? 1 } </ span >
117
171
< Button
@@ -123,9 +177,7 @@ export default function PdfViewer({ url, name }: PdfViewerProps) {
123
177
</ Button >
124
178
</ div >
125
179
126
- { /* Zoom Controls */ }
127
180
< div className = "flex items-center gap-2" >
128
- { " " }
129
181
< Button
130
182
onClick = { zoomOut }
131
183
disabled = { scale <= 0.25 }
@@ -139,7 +191,7 @@ export default function PdfViewer({ url, name }: PdfViewerProps) {
139
191
disabled = { scale >= 3 }
140
192
className = "h-10 w-10 rounded p-0 text-white transition hover:bg-[#6536c1] disabled:bg-gray-300"
141
193
>
142
- { < ZoomIn /> }
194
+ < ZoomIn />
143
195
</ Button >
144
196
< ShareButton />
145
197
< Button onClick = { downloadPDF } className = "aspect-square h-10 w-10 p-0" >
0 commit comments