PDF View
Continuous-scrollable PDF View component.
Preview
'use client'
import { PDFView } from '@/components/pdfview/pdfview'
import {
PDFViewProvider,
usePDFViewActions,
usePDFViewState,
} from '@/components/pdfview/pdfview-provider'
import {
ChevronDownIcon,
ChevronUpIcon,
MinusIcon,
PlusIcon,
} from 'lucide-react'
import { useRef } from 'react'
export default function Preview(props: { src: string }) {
const containerRef = useRef<HTMLDivElement>(null)
return (
<PDFViewProvider>
<div
ref={containerRef}
className="relative h-[450px] w-full overflow-hidden rounded-md border border-slate-100 bg-white">
<PDFView containerRef={containerRef} src={props.src} />
<PageIndicator />
<ZoomControls />
</div>
</PDFViewProvider>
)
}
function PageIndicator() {
const state = usePDFViewState()
const actions = usePDFViewActions()
return (
<div className="absolute right-2 bottom-2 flex flex-row items-center gap-2">
<button
onClick={() => {
actions.goToPage(state.currentPage - 1)
}}
type="button"
className="grid size-[30px] cursor-pointer place-content-center rounded-full border border-slate-100">
<ChevronUpIcon className="size-4" />
</button>
<p className="w-[80px] rounded-full border border-slate-100 bg-white px-2 py-1 text-center text-sm text-slate-900 shadow-xs">
{state.currentPage}
<span className="text-xs text-slate-500">/ {state.totalPages}</span>
</p>
<button
onClick={() => {
actions.goToPage(state.currentPage + 1)
}}
type="button"
className="grid size-[30px] cursor-pointer place-content-center rounded-full border border-slate-100">
<ChevronDownIcon className="size-4" />
</button>
</div>
)
}
function ZoomControls() {
const state = usePDFViewState()
const actions = usePDFViewActions()
return (
<div className="absolute bottom-2 left-2 flex flex-row items-center gap-2">
<button
onClick={() => {
actions.setViewportScale(state.scale - 0.25)
}}
type="button"
className="grid size-[30px] cursor-pointer place-content-center rounded-full border border-slate-100">
<MinusIcon className="size-4" />
</button>
<p className="w-[60px] rounded-full border border-slate-100 bg-white px-2 py-1 text-center text-sm text-slate-900 shadow-xs">
{(state.scale * 100).toFixed(0)}
<span className="text-xs">%</span>
</p>
<button
onClick={() => {
actions.setViewportScale(state.scale + 0.25)
}}
type="button"
className="grid size-[30px] cursor-pointer place-content-center rounded-full border border-slate-100">
<PlusIcon className="size-4" />
</button>
</div>
)
}Installation
npx shadcn@latest install https://sats-lab.github.io/components/r/pdfview.jsonThis will install these components under @/components/pdfview:
pdfviewpdfview-providerpdfview-utilspdfview-loaderpdfview-constants
and these packages if not already installed:
nanoidkonvareact-konvapdfjs-dist
Usage
Basic usage – rendering PDF
In Next.js, make sure to skip SSR when importing the pdfview modules. Here's how to do this in Pages Router and App Router.
import { PDFView } from '@/components/pdfview/pdfview'
import {
PDFViewProvider,
} from '@/components/pdfview/pdfview-provider'
import { useRef } from 'react'
export default function PDFViewer() {
const divRef = useRef<HTMLDivElement>(null)
return (
<PDFViewProvider>
<div
ref={divRef}
className="relative h-[800px] w-[90vw] border border-slate-100">
<PDFView
src="/sample.pdf"
containerRef={divRef}
/>
</div>
</PDFViewProvider>
)
}Rendering objects on pages and displaying page indicator.
import { PDFView } from '@/components/pdfview/pdfview'
import {
Object,
PDFViewProvider,
usePDFViewActions,
usePDFViewState,
} from '@/components/pdfview/pdfview-provider'
import { nanoid } from 'nanoid'
import { useEffect, useRef, useState } from 'react'
import { Image } from 'react-konva'
export default function PDFViewer() {
const divRef = useRef<HTMLDivElement>(null)
return (
<PDFViewProvider>
<ObjectButtons />
<div
ref={divRef}
className="relative h-[800px] w-[90vw] border border-slate-100">
<PDFView
src="/sample.pdf"
containerRef={divRef}
RenderObject={RenderObject}
/>
<PageIndicator />
</div>
</PDFViewProvider>
)
}
function ObjectButtons() {
const actions = usePDFViewActions()
return (
<div>
{/* eslint-disable-next-line */}
<img
onClick={() => {
actions.setObjectToPlace({
id: nanoid(),
dimensions: { width: 38, height: 38 },
tag: 'image',
data: {
src: 'https://picsum.photos/100/100',
},
})
}}
src="https://picsum.photos/100/100"
className="size-6"
/>
</div>
)
}
function RenderObject(props: { object: Object }) {
const [image, setImage] = useState<HTMLImageElement | null>(null)
useEffect(() => {
const img = new window.Image()
img.src = props.object.data.src
img.onload = () => {
setImage(img)
}
}, [props.object])
if (!image) return null
return (
<Image
image={image}
width={props.object.dimensions.width}
height={props.object.dimensions.height}
listening={props.object.placed}
alt={''}
/>
)
}
function PageIndicator() {
const { currentPage, totalPages } = usePDFViewState()
return (
<div className="absolute bottom-2 left-2 rounded-md border border-slate-300 bg-white px-2 py-1 text-slate-900 shadow-xs">
{currentPage} / {totalPages}
</div>
)
}Accessing PDFView state & actions
PDFView state
The state of PDFView can be accessed via usePDFViewState hook from the child component tree of PDFViewProvider. The return type of usePDFViewState is as follows:
| Prop | Type | Default |
|---|---|---|
status | 'loading' | 'success' | 'error' | loading |
scale | number | 1 |
currentPage | number | 1 |
totalPages | number | 1 |
PDFView actions
The actions of PDFView can be accessed via usePDFViewActions hook from the child component tree of PDFViewProvider. The return type of usePDFViewActions is as follows:
| Prop | Type | Default |
|---|---|---|
setViewportScale | (scale: number) => void | - |
goToPage | (pageNumber: number) => void | - |
setObjectToPlace | (objectToPlace: Omit<Object, 'position' | 'placed'> | null) => void | - |
getObjects | () => { objects: Map<string, Object>; pageObjects: Map<number, string[]> } | - |
The Object is from the pdfview-provider.tsx module. Not the global Object.