11"use client"
22import { createPluginRegistration } from '@embedpdf/core' ;
3- import { EmbedPDF } from '@embedpdf/core/react' ;
4- import { usePdfiumEngine } from '@embedpdf/engines/react' ;
5-
6- import { Download , ZoomIn , ZoomOut , Maximize2 , Minimize2 } from "lucide-react" ;
7- import { memo , useCallback , useEffect , useMemo , useRef , useState } from "react" ;
8- import { Button } from "./ui/button" ;
9- import { downloadFile } from "../lib/utils/download" ;
10-
11- import ShareButton from "./ShareButton" ;
12- import ReportButton from "./ReportButton" ;
13-
3+ import { EmbedPDF } from '@embedpdf/core/react' ;
4+ import { usePdfiumEngine } from '@embedpdf/engines/react' ;
145import { Viewport , ViewportPluginPackage } from '@embedpdf/plugin-viewport/react' ;
156import { useScroll , Scroller , ScrollPluginPackage } from '@embedpdf/plugin-scroll/react' ;
167import { DocumentContent , DocumentManagerPluginPackage } from '@embedpdf/plugin-document-manager/react' ;
178import { useZoom , ZoomPluginPackage , ZoomMode } from '@embedpdf/plugin-zoom/react' ;
189import { RenderLayer , RenderPluginPackage } from '@embedpdf/plugin-render/react' ;
1910import { ExportPluginPackage } from '@embedpdf/plugin-export/react' ;
2011
12+ import { Download , ZoomIn , ZoomOut , Maximize2 , Minimize2 } from "lucide-react" ;
13+ import { memo , useCallback , useEffect , useMemo , useRef , useState } from "react" ;
14+ import { downloadFile } from "../lib/utils/download" ;
15+ import { Button } from "./ui/button" ;
16+ import ShareButton from "./ShareButton" ;
17+ import ReportButton from "./ReportButton" ;
18+
2119interface ControlProps {
2220 documentId : string ;
2321 toggleFullscreen : ( ) => void ;
2422 isFullscreen : boolean ;
2523 onDownload : ( ) => Promise < void > ;
24+ forceMobile ?: boolean ;
25+ isMobile : boolean ;
26+ isSmall : boolean ;
2627}
2728
2829interface PdfViewerProps {
2930 url : string ;
3031 name : string ;
3132}
3233
33- // memo — only re-renders when documentId / isFullscreen / onDownload / toggleFullscreen refs change.
34- // All function props must be stable (useCallback) in the parent for this to be effective.
35- const Controls = memo ( function Controls ( {
36- documentId,
37- toggleFullscreen,
38- isFullscreen,
39- onDownload,
40- } : ControlProps ) {
34+ function useBreakpoint ( ) {
35+ const [ width , setWidth ] = useState ( ( ) => window . innerWidth ) ;
36+
37+ useEffect ( ( ) => {
38+ setWidth ( window . innerWidth ) ;
39+ const handler = ( ) => setWidth ( window . innerWidth ) ;
40+ window . addEventListener ( "resize" , handler ) ;
41+ return ( ) => window . removeEventListener ( "resize" , handler ) ;
42+ } , [ ] ) ;
43+
44+ return { isMobile : width < 768 , isSmall : width < 640 } ;
45+ }
46+
47+ const Controls = memo ( function Controls ( { documentId, toggleFullscreen, isFullscreen, onDownload,
48+ forceMobile, isMobile, isSmall} : ControlProps ) {
49+
4150 const { provides : zoomProv , state : zoomState } = useZoom ( documentId ) ;
4251 const { provides : scrollProv , state : scrollState } = useScroll ( documentId ) ;
4352 const [ pageNo , setPageNo ] = useState ( "1" ) ;
4453
4554 useEffect ( ( ) => {
4655 if ( ! scrollProv ) return ;
47- // Subscribe to page changes and keep the input in sync.
4856 const unsub = scrollProv . onPageChange ( ( ) =>
4957 setPageNo ( String ( scrollProv . getCurrentPage ( ) ) )
5058 ) ;
5159 return ( ) => unsub ( ) ;
5260 } , [ scrollProv ] ) ;
5361
54- // pageNo and totalPages are reactive values read inside the handler,
55- // so they must be listed as deps to avoid a stale closure.
5662 const pageChange = useCallback (
5763 ( e : React . KeyboardEvent < HTMLInputElement > ) => {
5864 if ( e . key !== "Enter" ) return ;
@@ -70,44 +76,39 @@ const Controls = memo(function Controls({
7076 const zoomOut = ( ) => zoomProv . zoomOut ( ) ;
7177 const { zoomLevel } = zoomState ;
7278 const { totalPages } = scrollState ;
79+
80+ const fullScreenStyle = {
81+ bottom : 20 ,
82+ left : "50%" ,
83+ transform : "translateX(-50%)" ,
84+ }
7385
74- return (
75- < div
76- style = { {
77- position : "absolute" ,
78- ...( isFullscreen ? {
79- bottom : 20 ,
80- left : "50%" ,
81- transform : "translateX(-50%)" ,
82- flexDirection : "row" ,
83- } : {
84- top : "40%" ,
85- right : 20 ,
86- transform : "translateY(-50%)" ,
87- flexDirection : "column" ,
88- } ) ,
89- zIndex : 1 ,
90- padding : "20px 12px" ,
91- display : "flex" ,
92- gap : 16 ,
93- alignItems : "center" ,
94- alignSelf : "center" ,
95- width : isFullscreen ? "auto" : "96px" ,
96- background : "#262635" ,
97- borderRadius : 8 ,
98- backdropFilter : "blur(6px)" ,
99- } }
100- >
86+ const pageInput = (
87+ < div className = { ( ! isFullscreen && ! isMobile ) ? "flex flex-col items-center gap-2" : "flex flex-row items-center gap-2" } >
88+ < input
89+ type = "text"
90+ value = { pageNo }
91+ onChange = { ( e ) => setPageNo ( e . target . value ) }
92+ onKeyDown = { pageChange }
93+ onFocus = { ( ) => setPageNo ( "" ) }
94+ className = "h-9 w-14 rounded border bg-[#e7e9ff] p-1 text-center text-sm [appearance:textfield] dark:bg-[#1f1f2a] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
95+ />
96+ < span className = "text-xs font-medium text-white" > of { totalPages ?? 1 } </ span >
97+ </ div >
98+ )
99+
100+ const toolSet = (
101+ < >
101102 < Button
102- onClick = { toggleFullscreen }
103- className = "h-12 w-12 rounded p-0 text-white bg-[#6536c1] transition hover:bg-[#7d4fc7]"
103+ onClick = { toggleFullscreen }
104+ className = "h-10 w-10 rounded p-0 text-white bg-[#6536c1] transition hover:bg-[#7d4fc7]"
104105 >
105- { isFullscreen ? < Minimize2 size = { 24 } /> : < Maximize2 size = { 24 } /> }
106+ { isFullscreen ? < Minimize2 size = { 24 } /> : < Maximize2 size = { 24 } /> }
106107 </ Button >
107108
108109 < Button
109110 onClick = { onDownload }
110- className = "h-12 w-12 rounded p-0 text-white bg-[#6536c1] transition hover:bg-[#7d4fc7]"
111+ className = "h-10 w-10 rounded p-0 text-white bg-[#6536c1] transition hover:bg-[#7d4fc7]"
111112 >
112113 < Download size = { 24 } />
113114 </ Button >
@@ -117,49 +118,137 @@ const Controls = memo(function Controls({
117118 < Button
118119 onClick = { zoomOut }
119120 disabled = { typeof zoomLevel === "number" && zoomLevel <= 0.25 }
120- className = "h-12 w-12 rounded p-0 text-white bg-[#6536c1] transition hover:bg-[#7d4fc7] disabled:bg-gray-400"
121+ className = "h-10 w-10 rounded p-0 text-white bg-[#6536c1] transition hover:bg-[#7d4fc7] disabled:bg-gray-400"
121122 >
122123 < ZoomOut size = { 24 } />
123124 </ Button >
124125
125- < span className = "text-xs text-[16px] py-2" >
126- { typeof zoomLevel === "number"
127- ? `${ Math . round ( zoomLevel * 100 ) } %`
128- : zoomLevel }
126+ < span className = "text-xs text-[16px] py-2 text-white font-medium bg-[#000000]" >
127+ { typeof zoomLevel === "number" && `${ Math . round ( zoomLevel * 100 ) } %` }
129128 </ span >
130129
131130 < Button
132131 onClick = { zoomIn }
133132 disabled = { typeof zoomLevel === "number" && zoomLevel >= 3 }
134- className = "h-12 w-12 rounded p-0 text-white bg-[#6536c1] transition hover:bg-[#7d4fc7] disabled:bg-gray-400"
133+ className = "h-10 w-10 rounded p-0 text-white bg-[#6536c1] transition hover:bg-[#7d4fc7] disabled:bg-gray-400"
135134 >
136135 < ZoomIn size = { 24 } />
137136 </ Button >
138137
139- < div className = { isFullscreen ? "flex flex-row items-center gap-2" : "flex flex-col items-center gap-2" } >
140- < input
141- type = "text"
142- value = { pageNo }
143- onChange = { ( e ) => setPageNo ( e . target . value ) }
144- onKeyDown = { pageChange }
145- onFocus = { ( ) => setPageNo ( "" ) }
146- className = "h-9 w-14 rounded border bg-[#e7e9ff] p-1 text-center text-sm [appearance:textfield] dark:bg-[#1f1f2a] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
147- />
148- < span className = "text-xs font-medium" > of { totalPages ?? 1 } </ span >
149- </ div >
138+ { isSmall && < ReportButton /> }
139+
140+ </ >
141+ )
142+
143+ if ( ! forceMobile ) {
144+ return (
145+ < >
146+ { ! isSmall ?
147+ < div
148+ style = { {
149+ position : "absolute" ,
150+ ...( isFullscreen ? { ...fullScreenStyle , flexDirection : "row" } : {
151+ top : "40%" ,
152+ right : 20 ,
153+ transform : "translateY(-50%)" ,
154+ flexDirection : "column" as const ,
155+ } ) ,
156+ zIndex : 1 ,
157+ padding : "10px 8px" ,
158+ display : "flex" ,
159+ gap : 16 ,
160+ alignItems : "center" ,
161+ alignSelf : "center" ,
162+ width : isFullscreen ? "auto" : "96px" ,
163+ background : "#262635" ,
164+ borderRadius : 8 ,
165+ backdropFilter : "blur(6px)" ,
166+ } }
167+ >
168+ { toolSet }
169+ { pageInput }
170+ { ! isSmall && < ReportButton /> }
171+ </ div > :
172+ < div
173+ style = { {
174+ position : "absolute" ,
175+ ...( isFullscreen ? fullScreenStyle : {
176+ top : "40%" ,
177+ right : 20 ,
178+ transform : "translateY(-50%)" ,
179+ } ) ,
180+ flexDirection : "column" as const ,
181+ zIndex : 1 ,
182+ padding : "20px 12px" ,
183+ display : "flex" ,
184+ gap : 16 ,
185+ alignItems : "center" ,
186+ alignSelf : "center" ,
187+ width : isFullscreen ? "auto" : "96px" ,
188+ background : "#262635" ,
189+ borderRadius : 8 ,
190+ backdropFilter : "blur(6px)" ,
191+ } }
192+ >
193+ { pageInput }
194+ < div
195+ style = { {
196+ display : "flex" ,
197+ flexDirection : isFullscreen ? "row" as const : "column" as const ,
198+ gap : 16 ,
199+ alignItems : "center" ,
200+ } }
201+ >
202+ { toolSet }
203+ </ div >
204+
205+ </ div > }
206+ </ >
207+
208+ ) ;
209+ }
150210
151- < ReportButton />
211+ return (
212+ < div
213+ style = { {
214+ top : "40%" ,
215+ flexDirection : isSmall ? "column" : "row" ,
216+ zIndex : 1 ,
217+ padding : "10px 12px" ,
218+ display : "flex" ,
219+ gap : 16 ,
220+ alignItems : "center" ,
221+ alignSelf : "center" ,
222+ width : "auto" ,
223+ background : "#262635" ,
224+ borderRadius : 12 ,
225+ backdropFilter : "blur(6px)" ,
226+ } }
227+ >
228+ { isSmall ? (
229+ < >
230+ { pageInput }
231+ < div style = { { display : "flex" , flexDirection : "row" , gap : 16 , alignItems : "center" } } >
232+ { toolSet }
233+ </ div >
234+ </ >
235+ ) : (
236+ < >
237+ { toolSet }
238+ { pageInput }
239+ < ReportButton />
240+ </ >
241+ ) }
152242 </ div >
153- ) ;
243+ )
154244} ) ;
155245
156246export default function PDFViewer ( { url, name } : PdfViewerProps ) {
157247 const { engine, isLoading } = usePdfiumEngine ( ) ;
248+ const { isMobile, isSmall} = useBreakpoint ( ) ;
158249 const [ isFullscreen , setIsFullscreen ] = useState ( false ) ;
159250 const viewerRef = useRef < HTMLDivElement > ( null ) ;
160251
161- // Stable reference — only changes if url/name change (which should be never
162- // during a viewer session). Avoids re-creating the fn on every PDFViewer render.
163252 const handleDownload = useCallback ( async ( ) => {
164253 window . dataLayer ?. push ( {
165254 event : "pdf_download_start" ,
@@ -169,9 +258,6 @@ export default function PDFViewer({ url, name }: PdfViewerProps) {
169258 await downloadFile ( url , `${ name } .pdf` ) ;
170259 } , [ url , name ] ) ;
171260
172- // toggleFullscreen reads only from a ref and DOM APIs — no React state
173- // is captured, so an empty dep array is correct and the reference never
174- // needs to change, keeping memo(Controls) happy.
175261 const toggleFullscreen = useCallback ( ( ) => {
176262 if ( ! document . fullscreenElement ) {
177263 void viewerRef . current ?. requestFullscreen ( ) ;
@@ -187,8 +273,6 @@ export default function PDFViewer({ url, name }: PdfViewerProps) {
187273 return ( ) => document . removeEventListener ( "fullscreenchange" , handleFullscreenChange ) ;
188274 } , [ ] ) ;
189275
190- // useMemo prevents the plugin array from being recreated on every render.
191- // url is the only real dep — if the url changes, a fresh plugin set is correct.
192276 const plugins = useMemo ( ( ) => [
193277 createPluginRegistration ( DocumentManagerPluginPackage , {
194278 initialDocuments : [ { url } ] ,
@@ -211,12 +295,22 @@ export default function PDFViewer({ url, name }: PdfViewerProps) {
211295 return (
212296 < div
213297 ref = { viewerRef }
214- style = { { height : "100vh " , width : "100%" , position : "relative" , backgroundColor : "#070114" } }
298+ style = { { height : "100dvh " , width : "100%" , position : "relative" , backgroundColor : "#070114" , display : "flex" , flexDirection : "column " } }
215299 >
216300 < EmbedPDF engine = { engine } plugins = { plugins } >
217301 { ( { activeDocumentId } ) =>
218302 activeDocumentId && (
219303 < >
304+ { ( isMobile && ! isFullscreen ) &&
305+ < Controls
306+ documentId = { activeDocumentId }
307+ toggleFullscreen = { toggleFullscreen }
308+ isFullscreen = { isFullscreen }
309+ onDownload = { handleDownload }
310+ forceMobile = { true } // pass the isMobile flag to Controls`
311+ isMobile = { isMobile }
312+ isSmall = { isSmall }
313+ /> }
220314 < DocumentContent documentId = { activeDocumentId } >
221315 { ( { isLoaded } ) =>
222316 isLoaded && (
@@ -239,13 +333,19 @@ export default function PDFViewer({ url, name }: PdfViewerProps) {
239333 )
240334 }
241335 </ DocumentContent >
242-
243- < Controls
244- documentId = { activeDocumentId }
245- toggleFullscreen = { toggleFullscreen }
246- isFullscreen = { isFullscreen }
247- onDownload = { handleDownload }
248- />
336+
337+ { ( ! isMobile || isFullscreen ) && (
338+ < Controls
339+ documentId = { activeDocumentId }
340+ toggleFullscreen = { toggleFullscreen }
341+ isFullscreen = { isFullscreen }
342+ onDownload = { handleDownload }
343+ forceMobile = { false }
344+ isMobile = { isMobile }
345+ isSmall = { isSmall }
346+ />
347+ ) }
348+
249349 </ >
250350 )
251351 }
0 commit comments