Skip to content
This repository was archived by the owner on Jun 17, 2026. It is now read-only.

Commit 1ef4042

Browse files
feat: implement PDF viewing functionality
1 parent 9049ab3 commit 1ef4042

11 files changed

Lines changed: 771 additions & 40 deletions

File tree

package-lock.json

Lines changed: 596 additions & 30 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,12 @@
5959
},
6060
"dependencies": {
6161
"@hookform/resolvers": "^3.1.1",
62+
"@react-pdf-viewer/core": "^3.12.0",
63+
"@react-pdf-viewer/thumbnail": "^3.12.0",
64+
"@react-pdf-viewer/toolbar": "^3.12.0",
6265
"@reduxjs/toolkit": "^1.9.0",
6366
"@sanity/incompatible-plugin": "^1.0.4",
64-
"@sanity/ui": "^1.9.3",
67+
"@sanity/ui": "^1.0 || ^2.0",
6568
"@sanity/uuid": "^3.0.1",
6669
"@tanem/react-nprogress": "^5.0.0",
6770
"copy-to-clipboard": "^3.3.1",
@@ -71,6 +74,7 @@
7174
"is-hotkey": "^0.2.0",
7275
"nanoid": "^3.3.3",
7376
"npm-run-all": "^4.1.5",
77+
"pdfjs-dist": "3.4.120",
7478
"pluralize": "^8.0.0",
7579
"react-dropzone": "^11.3.1",
7680
"react-file-icon": "^1.1.0",

src/components/Browser/index.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
import {Worker} from '@react-pdf-viewer/core'
12
import type {MutationEvent} from '@sanity/client'
23
import {Card, Flex, PortalProvider, studioTheme, ThemeProvider, ToastProvider} from '@sanity/ui'
34
import {Asset, Tag} from '@types'
45
import groq from 'groq'
5-
import {useEffect, useState} from 'react'
6+
import React, {useEffect, useState} from 'react'
67
import {useDispatch} from 'react-redux'
78
import {useColorScheme, type AssetSourceComponentProps, type SanityDocument} from 'sanity'
89
import {TAG_DOCUMENT_NAME} from '../../constants'
@@ -130,6 +131,14 @@ const BrowserContent = ({onClose}: {onClose?: AssetSourceComponentProps['onClose
130131
)
131132
}
132133

134+
const WorkerProvider = ({children}: {children: React.ReactNode}) => {
135+
return (
136+
<Worker workerUrl="https://unpkg.com/pdfjs-dist@3.11.174/build/pdf.worker.min.js">
137+
{children}
138+
</Worker>
139+
)
140+
}
141+
133142
const Browser = (props: Props) => {
134143
const client = useVersionedClient()
135144
const {scheme} = useColorScheme()
@@ -145,8 +154,9 @@ const Browser = (props: Props) => {
145154
<ToastProvider>
146155
<AssetBrowserDispatchProvider onSelect={props?.onSelect}>
147156
<GlobalStyle />
148-
149-
<BrowserContent onClose={props?.onClose} />
157+
<WorkerProvider>
158+
<BrowserContent onClose={props?.onClose} />
159+
</WorkerProvider>
150160
</AssetBrowserDispatchProvider>
151161
</ToastProvider>
152162
</ThemeProvider>

src/components/CardAsset/index.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
ThemeColorSchemeKey,
1111
Tooltip
1212
} from '@sanity/ui'
13-
import React, {memo, MouseEvent, RefObject} from 'react'
13+
import {memo, MouseEvent, RefObject} from 'react'
1414
import {useDispatch} from 'react-redux'
1515
import {useColorScheme} from 'sanity'
1616
import styled, {css} from 'styled-components'
@@ -20,11 +20,12 @@ import useKeyPress from '../../hooks/useKeyPress'
2020
import useTypedSelector from '../../hooks/useTypedSelector'
2121
import {assetsActions, selectAssetById} from '../../modules/assets'
2222
import {dialogActions} from '../../modules/dialog'
23+
import {getSchemeColor} from '../../utils/getSchemeColor'
2324
import imageDprUrl from '../../utils/imageDprUrl'
24-
import {isFileAsset, isImageAsset} from '../../utils/typeGuards'
25+
import {isFileAsset, isImageAsset, isPdfAsset} from '../../utils/typeGuards'
2526
import FileIcon from '../FileIcon'
27+
import FilePdfPreview from '../FilePdfPreview'
2628
import Image from '../Image'
27-
import {getSchemeColor} from '../../utils/getSchemeColor'
2829

2930
type Props = {
3031
id: string
@@ -160,6 +161,9 @@ const CardAsset = (props: Props) => {
160161
}}
161162
>
162163
<div onClick={handleAssetClick} style={{height: '100%', opacity: opacityPreview}}>
164+
{/* File pdf */}
165+
{isPdfAsset(asset) && <FilePdfPreview url={asset.url} width={460} />}
166+
163167
{/* File icon */}
164168
{isFileAsset(asset) && <FileIcon extension={asset.extension} width="80px" />}
165169

src/components/DialogAssetEdit/index.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@ import {selectTagSelectOptions} from '../../modules/tags'
1616
import {getUniqueDocuments} from '../../utils/getUniqueDocuments'
1717
import imageDprUrl from '../../utils/imageDprUrl'
1818
import sanitizeFormData from '../../utils/sanitizeFormData'
19-
import {isFileAsset, isImageAsset} from '../../utils/typeGuards'
19+
import {isFileAsset, isImageAsset, isPdfAsset} from '../../utils/typeGuards'
2020
import AssetMetadata from '../AssetMetadata'
2121
import Dialog from '../Dialog'
2222
import DocumentList from '../DocumentList'
2323
import FileAssetPreview from '../FileAssetPreview'
24+
import FilePdf from '../FilePdf'
2425
import FormFieldInputText from '../FormFieldInputText'
2526
import FormFieldInputTextarea from '../FormFieldInputTextarea'
2627
import FormFieldTags from '../FormFieldTags'
@@ -378,6 +379,9 @@ const DialogAssetEdit = (props: Props) => {
378379

379380
<Box flex={1} padding={4}>
380381
<Box style={{aspectRatio: '1'}}>
382+
{/* File pdf */}
383+
{isPdfAsset(currentAsset) && <FilePdf url={currentAsset.url} />}
384+
381385
{/* File */}
382386
{isFileAsset(currentAsset) && <FileAssetPreview asset={currentAsset} />}
383387

src/components/FilePdf/index.tsx

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import {Viewer} from '@react-pdf-viewer/core'
2+
import {toolbarPlugin, ToolbarSlot, TransformToolbarSlot} from '@react-pdf-viewer/toolbar'
3+
4+
// Import the styles
5+
import '@react-pdf-viewer/core/lib/styles/index.css'
6+
import '@react-pdf-viewer/toolbar/lib/styles/index.css'
7+
8+
type Props = {
9+
url: string
10+
}
11+
12+
const transform: TransformToolbarSlot = (slot: ToolbarSlot) => {
13+
const {NumberOfPages} = slot
14+
15+
// Override the `NumberOfPages` slot
16+
return Object.assign({}, slot, {
17+
NumberOfPages: () => (
18+
<>
19+
of <NumberOfPages />
20+
</>
21+
),
22+
SwitchTheme: () => <></>,
23+
SwitchThemeMenuItem: () => <></>,
24+
Open: () => <></>,
25+
OpenMenuItem: () => <></>,
26+
Download: () => <></>,
27+
DownloadMenuItem: () => <></>,
28+
Print: () => <></>,
29+
PrintMenuItem: () => <></>
30+
})
31+
}
32+
33+
const FilePdf = (props: Props) => {
34+
const {url} = props
35+
36+
const toolbarPluginInstance = toolbarPlugin()
37+
const {Toolbar, renderDefaultToolbar} = toolbarPluginInstance
38+
39+
return (
40+
<div
41+
className="rpv-core__viewer"
42+
style={{
43+
border: '1px solid rgba(0, 0, 0, 0.3)',
44+
display: 'flex',
45+
flexDirection: 'column',
46+
height: '100%'
47+
}}
48+
>
49+
<div
50+
style={{
51+
alignItems: 'center',
52+
backgroundColor: '#eeeeee',
53+
borderBottom: '1px solid rgba(0, 0, 0, 0.1)',
54+
display: 'flex',
55+
padding: '4px'
56+
}}
57+
>
58+
<Toolbar>{renderDefaultToolbar(transform)}</Toolbar>
59+
</div>
60+
<div
61+
style={{
62+
flex: 1,
63+
overflow: 'hidden'
64+
}}
65+
>
66+
<Viewer fileUrl={url} plugins={[toolbarPluginInstance]} />
67+
</div>
68+
</div>
69+
)
70+
}
71+
72+
export default FilePdf
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import {Viewer} from '@react-pdf-viewer/core'
2+
import {thumbnailPlugin} from '@react-pdf-viewer/thumbnail'
3+
import {thumbnail} from './thumbnail'
4+
5+
type Props = {
6+
url: string
7+
width?: number
8+
}
9+
10+
const FilePdfPreview = (props: Props) => {
11+
const {url, width} = props
12+
13+
const thumbnailPluginInstance = thumbnailPlugin()
14+
const {Cover} = thumbnailPluginInstance
15+
const pageThumbnailPluginInstance = thumbnail({
16+
PageThumbnail: <Cover getPageIndex={() => 0} width={width} />
17+
})
18+
19+
return <Viewer fileUrl={url} plugins={[pageThumbnailPluginInstance, thumbnailPluginInstance]} />
20+
}
21+
22+
export default FilePdfPreview
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import type {Plugin, RenderViewer} from '@react-pdf-viewer/core'
2+
import * as React from 'react'
3+
4+
export interface PageThumbnailPluginProps {
5+
PageThumbnail: React.ReactElement
6+
}
7+
8+
export const thumbnail = (props: PageThumbnailPluginProps): Plugin => {
9+
const {PageThumbnail} = props
10+
11+
return {
12+
renderViewer: (renderProps: RenderViewer) => {
13+
const {slot} = renderProps
14+
15+
slot.children = PageThumbnail
16+
17+
// Reset the sub slot
18+
if (slot.subSlot) {
19+
slot.subSlot.attrs = {}
20+
slot.subSlot.children = <></>
21+
}
22+
23+
return slot
24+
}
25+
}
26+
}

src/components/TableRowAsset/index.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,9 @@ import getAssetResolution from '../../utils/getAssetResolution'
2828
import {getSchemeColor} from '../../utils/getSchemeColor'
2929
import {getUniqueDocuments} from '../../utils/getUniqueDocuments'
3030
import imageDprUrl from '../../utils/imageDprUrl'
31-
import {isFileAsset, isImageAsset} from '../../utils/typeGuards'
31+
import {isFileAsset, isImageAsset, isPdfAsset} from '../../utils/typeGuards'
3232
import FileIcon from '../FileIcon'
33+
import FilePdfPreview from '../FilePdfPreview'
3334
import Image from '../Image'
3435

3536
// Duration (ms) to wait before reference counts (and associated listeners) are rendered
@@ -233,6 +234,9 @@ const TableRowAsset = (props: Props) => {
233234
>
234235
<Flex align="center" justify="center" style={{height: '100%', position: 'relative'}}>
235236
<Box style={{height: '100%', opacity: opacityPreview, position: 'relative'}}>
237+
{/* File pdf */}
238+
{isPdfAsset(asset) && <FilePdfPreview url={asset.url} width={400} />}
239+
236240
{/* File icon */}
237241
{isFileAsset(asset) && <FileIcon extension={asset.extension} width="40px" />}
238242

src/styled/GlobalStyles/index.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,19 @@ const GlobalStyle = createGlobalStyle`
3535
background-color: rgba(15, 17, 18, 0.9);
3636
}
3737
38+
.rpv-thumbnail__cover-image,
39+
.rpv-thumbnail__cover-inner,
40+
.rpv-thumbnail__cover {
41+
width: 100%;
42+
height: 100%;
43+
object-fit: contain;
44+
}
45+
46+
@media screen and (max-width: 974px) {
47+
.rpv-core__display--hidden {
48+
display: none;
49+
}
50+
}
3851
`
3952

4053
export default GlobalStyle

0 commit comments

Comments
 (0)