✨ add menu pdf & pdf lib
This commit is contained in:
Generated
+278
@@ -10,6 +10,7 @@
|
|||||||
"@tanstack/react-start": "latest",
|
"@tanstack/react-start": "latest",
|
||||||
"better-auth": "^1.6.29",
|
"better-auth": "^1.6.29",
|
||||||
"drizzle-orm": "^0.45.2",
|
"drizzle-orm": "^0.45.2",
|
||||||
|
"pdfjs-dist": "^6.2.108",
|
||||||
"postgres": "^3.4.9",
|
"postgres": "^3.4.9",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
@@ -1310,6 +1311,271 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@napi-rs/canvas": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-/SaLcvlqGWdm0HSCWMgHu7cjJiQXfP8/mOY+6dUyV9flQz7sPBBZ+ed2zYtoukojPmxOaL7bm+d/G4GeWWoN7g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"workspaces": [
|
||||||
|
"e2e/*"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@napi-rs/canvas-android-arm64": "1.0.8",
|
||||||
|
"@napi-rs/canvas-darwin-arm64": "1.0.8",
|
||||||
|
"@napi-rs/canvas-darwin-x64": "1.0.8",
|
||||||
|
"@napi-rs/canvas-linux-arm-gnueabihf": "1.0.8",
|
||||||
|
"@napi-rs/canvas-linux-arm64-gnu": "1.0.8",
|
||||||
|
"@napi-rs/canvas-linux-arm64-musl": "1.0.8",
|
||||||
|
"@napi-rs/canvas-linux-riscv64-gnu": "1.0.8",
|
||||||
|
"@napi-rs/canvas-linux-x64-gnu": "1.0.8",
|
||||||
|
"@napi-rs/canvas-linux-x64-musl": "1.0.8",
|
||||||
|
"@napi-rs/canvas-win32-arm64-msvc": "1.0.8",
|
||||||
|
"@napi-rs/canvas-win32-x64-msvc": "1.0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-android-arm64": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-5+nkh8i3gt6lqS/d2jTZ1xAn6tdgtB4Lf1mW6T0Qm5/rXNwBuV1sAEyLEWan5o9gJPU/GuvHR3rvSeZ+FaGrbw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-darwin-arm64": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-7jQ47gi+fZ7KJmfc/5rNyy1CYw/cu4kZ0KPIYbo9UUgSdW0bKQJpt+WihEor6s4Lyp7+xc3a+3HeyXmAEbbnPg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-darwin-x64": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-rRjDMZs9pIRKGxgijwezplKc1RnJsqUokrA9h88bbTkqQ+7ePj0ZN4ZnZDy8Vu0tXs7KRlI2tQLaK4mx9QlxHg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-linux-arm-gnueabihf": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-jGcCd+8ra6Q61xKqZeiItujTpp9a9eRLcQ0jW6qYNku+WpupqOPFPY0SrsuSnXFviJwkpKYT9p7QrB4lsf3LNQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-linux-arm64-gnu": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-od6I2Y7kU7i1SwZYG2EKW8rWz6JiedtPpko4WEe1DDsiikrfaotVBCRaUTM5/yeZKaZ92EatoAS+5xG+6uJlYA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-linux-arm64-musl": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-yYkPbJDJiWj6N0gASA3CAvRypZmVpJnxU0DQg3aBhneLDQde9TPLKADsQkobNoJUtTT/lj46aWpzT48PDb3Qcg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-linux-riscv64-gnu": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-PB00MSKAp4VwK/xwe6duKxRKmH8UH4GIl1pqHSbxng0jnU9Dr7FwaDypDiqwNFZ774N+8G7mJLGuLtg9NTcQsg==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-linux-x64-gnu": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-TWM2XWJoitLiIPCvgJh7SriC+L/T9qkYCVzC66AidsZy0QP1hkKzBzVwshCdcA3q6fIn3yE0ISbq4lMJSy8jFw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-linux-x64-musl": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-hb20MxKXXb5IB7AAwN8UHz9WRsa2HmdZfjsDCzjElwJoeV1aotVEwFU4FrFQcYQVzsJQLeaCc/2Qdt/0Q72mMg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-win32-arm64-msvc": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-WwPN08IXE4SkL+FhJyPz/iFnycMAUkbphFIT4cmKLlvbSU0Zfn1R7BGJ3Hqky1S89QUYc0Q4IOScXb/42Re9wQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-win32-x64-msvc": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-XkrVqKb+pxyba7kjy2LJvABFVBTE0DNpEl7MrG4OYUmaWarrXH+t54z/Czj2YxCKtizYTV4mg6phNm3x24qjhQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@noble/ciphers": {
|
"node_modules/@noble/ciphers": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-2.3.0.tgz",
|
||||||
@@ -3732,6 +3998,18 @@
|
|||||||
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/pdfjs-dist": {
|
||||||
|
"version": "6.2.108",
|
||||||
|
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-6.2.108.tgz",
|
||||||
|
"integrity": "sha512-YxFb+SQcodN2rnX9Tn3dHYlqfb7NjlzzfONPpJd+AKoKtUjEdevTfbC07d5TcczzOK6261auRkP/M8OBHs9vFQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22.13.0 || >=24"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@napi-rs/canvas": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
"@tanstack/react-start": "latest",
|
"@tanstack/react-start": "latest",
|
||||||
"better-auth": "^1.6.29",
|
"better-auth": "^1.6.29",
|
||||||
"drizzle-orm": "^0.45.2",
|
"drizzle-orm": "^0.45.2",
|
||||||
|
"pdfjs-dist": "^6.2.108",
|
||||||
"postgres": "^3.4.9",
|
"postgres": "^3.4.9",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
|
|||||||
Binary file not shown.
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { createFileRoute, useRouter } from '@tanstack/react-router'
|
import { createFileRoute, useRouter } from '@tanstack/react-router'
|
||||||
|
|
||||||
|
import { PdfMenu } from '#/components/pdf-menu'
|
||||||
import { ReservationConfirmedModal } from '#/components/reservation-confirmed-modal'
|
import { ReservationConfirmedModal } from '#/components/reservation-confirmed-modal'
|
||||||
import { ReservationDateTimeField } from '#/components/reservation-datetime-field'
|
import { ReservationDateTimeField } from '#/components/reservation-datetime-field'
|
||||||
import { SiteFooter } from '#/components/site-footer'
|
import { SiteFooter } from '#/components/site-footer'
|
||||||
@@ -227,6 +228,16 @@ function Home() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section className="section" id="speisekarte">
|
||||||
|
<div className="section-grid">
|
||||||
|
<div>
|
||||||
|
<span className="section-kicker">Was uns schmeckt</span>
|
||||||
|
<h2 className="section-heading">Speisekarte</h2>
|
||||||
|
</div>
|
||||||
|
<PdfMenu src="/menu.pdf" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section className="section" id="reservieren">
|
<section className="section" id="reservieren">
|
||||||
<div className="section-grid">
|
<div className="section-grid">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -1508,6 +1508,83 @@ a {
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pdf-menu {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-menu-viewport {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 0.5rem 1.5rem rgb(0 0 0 / 25%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-menu-loading {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 20rem;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
opacity: 0;
|
||||||
|
animation: pdf-menu-spinner-fade-in 0.15s ease 0.15s forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-menu-loading p {
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
font-family: 'Fraunces', Georgia, serif;
|
||||||
|
font-style: italic;
|
||||||
|
color: #6b5f4d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-menu-spinner {
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 3px solid var(--line);
|
||||||
|
border-top-color: var(--gold);
|
||||||
|
animation: pdf-menu-spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pdf-menu-spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pdf-menu-spinner-fade-in {
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-menu-page {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
aspect-ratio: 210 / 297;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-menu-pagination {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
/* A classic map-pin: a square with three rounded corners (50% 50% 50% 0 leaves
|
/* A classic map-pin: a square with three rounded corners (50% 50% 50% 0 leaves
|
||||||
the bottom-left one sharp), rotated -45deg around that exact sharp corner so
|
the bottom-left one sharp), rotated -45deg around that exact sharp corner so
|
||||||
the corner itself — not the box center — lands on the `left`/`top` coordinate
|
the corner itself — not the box center — lands on the `left`/`top` coordinate
|
||||||
|
|||||||
Reference in New Issue
Block a user