Skip to content

Commit 47869a4

Browse files
committed
feat: add responsive UI for mobile and desktop layouts
1 parent 16a361e commit 47869a4

3 files changed

Lines changed: 190 additions & 88 deletions

File tree

src/components/ShareButton.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export default function ShareButton() {
3030
return (
3131
<Dialog>
3232
<DialogTrigger asChild>
33-
<Button className="aspect-square h-12 w-12 p-0 rounded text-white bg-[#6536c1] transition hover:bg-[#7d4fc7]">
33+
<Button className="aspect-square h-10 w-10 p-0 rounded text-white bg-[#6536c1] transition hover:bg-[#7d4fc7]">
3434
<FaShare />
3535
</Button>
3636
</DialogTrigger>

src/components/newPdfViewer.tsx

Lines changed: 187 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,64 @@
11
"use client"
22
import { 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';
145
import { Viewport, ViewportPluginPackage } from '@embedpdf/plugin-viewport/react';
156
import { useScroll, Scroller, ScrollPluginPackage } from '@embedpdf/plugin-scroll/react';
167
import { DocumentContent, DocumentManagerPluginPackage } from '@embedpdf/plugin-document-manager/react';
178
import { useZoom, ZoomPluginPackage, ZoomMode } from '@embedpdf/plugin-zoom/react';
189
import { RenderLayer, RenderPluginPackage } from '@embedpdf/plugin-render/react';
1910
import { 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+
2119
interface 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

2829
interface 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

156246
export 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

Comments
 (0)