✨ add menu pdf & pdf lib
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import type { PDFDocumentProxy, RenderTask } from 'pdfjs-dist'
|
||||
|
||||
type Props = {
|
||||
src: string
|
||||
}
|
||||
|
||||
// pdfjs-dist touches the DOM/Canvas at import time, so it's loaded lazily inside
|
||||
// the effect below instead of at module scope — this file also runs during SSR.
|
||||
export function PdfMenu({ src }: Props) {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||
const pdfRef = useRef<PDFDocumentProxy | null>(null)
|
||||
const directionRef = useRef(1)
|
||||
const hasRenderedOnceRef = useRef(false)
|
||||
const [error, setError] = useState(false)
|
||||
const [numPages, setNumPages] = useState(0)
|
||||
const [pageNumber, setPageNumber] = useState(1)
|
||||
const [isBusy, setIsBusy] = useState(false)
|
||||
|
||||
function goToPage(next: number) {
|
||||
directionRef.current = next > pageNumber ? 1 : -1
|
||||
setPageNumber(next)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
setError(false)
|
||||
setNumPages(0)
|
||||
setPageNumber(1)
|
||||
pdfRef.current = null
|
||||
hasRenderedOnceRef.current = false
|
||||
|
||||
async function load() {
|
||||
const pdfjsLib = await import('pdfjs-dist')
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||
import.meta.url,
|
||||
).href
|
||||
|
||||
try {
|
||||
const pdf = await pdfjsLib.getDocument({ url: src }).promise
|
||||
if (cancelled) return
|
||||
pdfRef.current = pdf
|
||||
setNumPages(pdf.numPages)
|
||||
} catch (err) {
|
||||
console.error('PdfMenu failed to load', err)
|
||||
if (!cancelled) setError(true)
|
||||
}
|
||||
}
|
||||
|
||||
load()
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [src])
|
||||
|
||||
useEffect(() => {
|
||||
const pdf = pdfRef.current
|
||||
const canvas = canvasRef.current
|
||||
const container = containerRef.current
|
||||
if (!pdf || !canvas || !container || numPages === 0) return
|
||||
|
||||
let cancelled = false
|
||||
let renderTask: RenderTask | undefined
|
||||
let exitTimeout: ReturnType<typeof setTimeout> | undefined
|
||||
|
||||
function wait(ms: number) {
|
||||
return new Promise<void>((resolve) => {
|
||||
exitTimeout = setTimeout(resolve, ms)
|
||||
})
|
||||
}
|
||||
|
||||
async function render(pdf: PDFDocumentProxy, canvas: HTMLCanvasElement, container: HTMLDivElement) {
|
||||
const direction = directionRef.current
|
||||
setIsBusy(true)
|
||||
|
||||
if (hasRenderedOnceRef.current) {
|
||||
canvas.style.transition = 'opacity 0.18s ease, transform 0.18s ease'
|
||||
canvas.style.opacity = '0'
|
||||
canvas.style.transform = `translateX(${direction * -2}rem)`
|
||||
await wait(180)
|
||||
if (cancelled) return
|
||||
}
|
||||
|
||||
try {
|
||||
const page = await pdf.getPage(pageNumber)
|
||||
if (cancelled) return
|
||||
const unscaledViewport = page.getViewport({ scale: 1 })
|
||||
const scale = container.clientWidth / unscaledViewport.width
|
||||
const viewport = page.getViewport({ scale })
|
||||
|
||||
const pixelRatio = window.devicePixelRatio || 1
|
||||
canvas.width = viewport.width * pixelRatio
|
||||
canvas.height = viewport.height * pixelRatio
|
||||
canvas.style.width = `${viewport.width}px`
|
||||
canvas.style.height = `${viewport.height}px`
|
||||
|
||||
const context = canvas.getContext('2d')
|
||||
if (!context) return
|
||||
context.scale(pixelRatio, pixelRatio)
|
||||
|
||||
renderTask = page.render({ canvasContext: context, viewport, canvas })
|
||||
await renderTask.promise
|
||||
if (cancelled) return
|
||||
|
||||
// Jump to the entry offset with transitions off, then force the browser to
|
||||
// commit that position before re-enabling them — otherwise the two style
|
||||
// writes land in the same frame and the entry animation never paints.
|
||||
canvas.style.transition = 'none'
|
||||
canvas.style.transform = `translateX(${direction * 2}rem)`
|
||||
canvas.offsetHeight
|
||||
canvas.style.transition = 'opacity 0.22s ease, transform 0.22s ease'
|
||||
canvas.style.opacity = '1'
|
||||
canvas.style.transform = 'translateX(0)'
|
||||
hasRenderedOnceRef.current = true
|
||||
} catch (err) {
|
||||
if (!cancelled) {
|
||||
console.error('PdfMenu failed to render page', err)
|
||||
setError(true)
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) setIsBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
render(pdf, canvas, container)
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
clearTimeout(exitTimeout)
|
||||
renderTask?.cancel()
|
||||
}
|
||||
}, [numPages, pageNumber])
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<p className="field-hint">
|
||||
Die Speisekarte konnte nicht geladen werden.{' '}
|
||||
<a href={src} target="_blank" rel="noopener noreferrer">
|
||||
Als PDF öffnen
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pdf-menu" ref={containerRef}>
|
||||
<div className="pdf-menu-viewport">
|
||||
<canvas className="pdf-menu-page" ref={canvasRef} />
|
||||
{numPages === 0 && (
|
||||
<div className="pdf-menu-loading" role="status">
|
||||
<div className="pdf-menu-spinner" aria-hidden="true" />
|
||||
<p>Die Speisekarte wird aufgetischt …</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{numPages > 1 && (
|
||||
<div className="pdf-menu-pagination">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
onClick={() => goToPage(pageNumber - 1)}
|
||||
disabled={isBusy || pageNumber <= 1}
|
||||
>
|
||||
Zurück
|
||||
</button>
|
||||
<span>
|
||||
Seite {pageNumber} von {numPages}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
onClick={() => goToPage(pageNumber + 1)}
|
||||
disabled={isBusy || pageNumber >= numPages}
|
||||
>
|
||||
Weiter
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<p className="field-hint">
|
||||
Speisekarte nicht geladen?{' '}
|
||||
<a href={src} target="_blank" rel="noopener noreferrer">
|
||||
Klick hier
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user