Skip to content

Commit 4b40353

Browse files
committed
fix: PDF preview
1 parent e144f3f commit 4b40353

5 files changed

Lines changed: 286 additions & 2 deletions

File tree

app/globals.css

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
@import "tailwindcss";
22
@import "tw-animate-css";
33
@import "shadcn/tailwind.css";
4+
@import "react-pdf/dist/Page/AnnotationLayer.css";
5+
@import "react-pdf/dist/Page/TextLayer.css";
46

57
@custom-variant dark (&:is(.dark *));
68

components/object/pdf-viewer.tsx

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
"use client"
2+
3+
import * as React from "react"
4+
import { useTranslation } from "react-i18next"
5+
import { Document, Page, pdfjs } from "react-pdf"
6+
import { Spinner } from "@/components/ui/spinner"
7+
8+
pdfjs.GlobalWorkerOptions.workerSrc = new URL("pdfjs-dist/build/pdf.worker.min.mjs", import.meta.url).toString()
9+
10+
interface PdfViewerProps {
11+
url: string
12+
}
13+
14+
export function PdfViewer({ url }: PdfViewerProps) {
15+
const { t } = useTranslation()
16+
const containerRef = React.useRef<HTMLDivElement | null>(null)
17+
const [numPages, setNumPages] = React.useState(0)
18+
const [containerWidth, setContainerWidth] = React.useState(0)
19+
const [loadError, setLoadError] = React.useState<string | null>(null)
20+
21+
React.useEffect(() => {
22+
setNumPages(0)
23+
setLoadError(null)
24+
}, [url])
25+
26+
React.useEffect(() => {
27+
const container = containerRef.current
28+
if (!container) return
29+
30+
const observer = new ResizeObserver((entries) => {
31+
for (const entry of entries) {
32+
setContainerWidth(Math.floor(entry.contentRect.width))
33+
}
34+
})
35+
36+
observer.observe(container)
37+
setContainerWidth(Math.floor(container.getBoundingClientRect().width))
38+
39+
return () => {
40+
observer.disconnect()
41+
}
42+
}, [])
43+
44+
if (!url) {
45+
return (
46+
<div className="flex h-[70vh] items-center justify-center text-sm text-muted-foreground">{t("Preview unavailable")}</div>
47+
)
48+
}
49+
50+
const pageWidth = containerWidth > 0 ? Math.max(Math.min(containerWidth - 24, 960), 240) : 800
51+
52+
return (
53+
<div ref={containerRef} className="h-[70vh] w-full overflow-auto rounded-md bg-muted/20 p-3">
54+
<Document
55+
file={url}
56+
loading={
57+
<div className="flex h-[50vh] items-center justify-center">
58+
<Spinner className="size-8 text-muted-foreground" />
59+
</div>
60+
}
61+
error={<div className="p-4 text-sm text-destructive">{loadError ?? t("Preview unavailable")}</div>}
62+
onLoadSuccess={({ numPages: pages }) => {
63+
setNumPages(pages)
64+
}}
65+
onLoadError={() => {
66+
setLoadError(t("Preview unavailable"))
67+
}}
68+
>
69+
<div className="mx-auto flex w-full max-w-[960px] flex-col gap-3">
70+
{Array.from({ length: numPages }, (_, index) => (
71+
<div key={index + 1} className="overflow-hidden rounded-md border bg-background shadow-sm">
72+
<Page pageNumber={index + 1} width={pageWidth} loading={null} renderTextLayer renderAnnotationLayer />
73+
</div>
74+
))}
75+
</div>
76+
</Document>
77+
</div>
78+
)
79+
}

components/object/preview-modal.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@ import { Spinner } from "@/components/ui/spinner"
77
import { Button } from "@/components/ui/button"
88
import { cn } from "@/lib/utils"
99
import { RiFullscreenExitLine, RiFullscreenLine } from "@remixicon/react"
10+
import { PdfViewer } from "@/components/object/pdf-viewer"
1011

1112
const SAFE_TEXT_MIMES = ["application/json", "application/xml", "text/plain", "text/xml", "text/csv", "text/markdown"]
1213
const SAFE_TEXT_EXTENSIONS = [".txt", ".json", ".xml", ".csv", ".md", ".yml", ".yaml"]
1314
const SAFE_IMAGE_EXTENSIONS = [".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".ico", ".tif", ".tiff"]
1415
const ALLOWED_SIZE = 1024 * 1024 * 2 // 2MB
1516

16-
type PreviewMode = "text" | "image" | "sandbox" | "download"
17+
type PreviewMode = "text" | "image" | "pdf" | "sandbox" | "download"
1718

1819
interface ObjectPreviewModalProps {
1920
show: boolean
@@ -51,6 +52,11 @@ function getPreviewMode(hasPreviewUrl: boolean, canRenderText: boolean, canRende
5152
return canRenderText ? "text" : "sandbox"
5253
}
5354

55+
function isPdfPreview(contentType: string, objectKey: string) {
56+
const keyLower = objectKey.toLowerCase()
57+
return contentType === "application/pdf" || keyLower.endsWith(".pdf")
58+
}
59+
5460
export function ObjectPreviewModal({ show, onShowChange, object }: ObjectPreviewModalProps) {
5561
const { t } = useTranslation()
5662
const [textContent, setTextContent] = React.useState("")
@@ -75,7 +81,8 @@ export function ObjectPreviewModal({ show, onShowChange, object }: ObjectPreview
7581
const isJson = normalizedContentType === "application/json" || objectKeyLower.endsWith(".json")
7682
const canRenderText = hasPreviewUrl && isSafeTextPreview(normalizedContentType, objectKey, objectSize)
7783
const canRenderImage = hasPreviewUrl && isImagePreview(normalizedContentType, objectKey)
78-
const previewMode = getPreviewMode(hasPreviewUrl, canRenderText, canRenderImage)
84+
const canRenderPdf = hasPreviewUrl && isPdfPreview(normalizedContentType, objectKey)
85+
const previewMode = canRenderPdf ? "pdf" : getPreviewMode(hasPreviewUrl, canRenderText, canRenderImage)
7986
const isImageMode = previewMode === "image"
8087

8188
const getFormattedContent = () => {
@@ -276,6 +283,8 @@ export function ObjectPreviewModal({ show, onShowChange, object }: ObjectPreview
276283
return (
277284
<iframe src={previewUrl} className="h-[70vh] w-full" frameBorder={0} title="Sandbox preview" sandbox="" />
278285
)
286+
case "pdf":
287+
return <PdfViewer url={previewUrl} />
279288
case "download":
280289
default:
281290
return (

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,13 @@
3737
"next": "16.1.6",
3838
"next-themes": "^0.4.6",
3939
"node-forge": "^1.3.3",
40+
"pdfjs-dist": "^5.4.296",
4041
"radix-ui": "^1.4.3",
4142
"react": "19.2.3",
4243
"react-day-picker": "^9.13.0",
4344
"react-dom": "19.2.3",
4445
"react-i18next": "^16.5.4",
46+
"react-pdf": "^10.4.1",
4547
"react-resizable-panels": "^4.5.6",
4648
"recharts": "2.15.4",
4749
"shadcn": "^3.8.0",

0 commit comments

Comments
 (0)