Rewrite PDF viewer to use native browser iframe
All checks were successful
Automated Container Build / build-and-push (push) Successful in 26s
All checks were successful
Automated Container Build / build-and-push (push) Successful in 26s
This commit is contained in:
parent
6ee6ef376c
commit
e392da3699
1 changed files with 6 additions and 151 deletions
|
|
@ -1,156 +1,11 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { useState, useEffect, useRef } from 'react';
|
|
||||||
import { Document, Page, pdfjs } from 'react-pdf';
|
|
||||||
import { ZoomIn, ZoomOut, Loader2 } from 'lucide-react';
|
|
||||||
import { useInView } from 'react-intersection-observer';
|
|
||||||
import 'react-pdf/dist/Page/AnnotationLayer.css';
|
|
||||||
import 'react-pdf/dist/Page/TextLayer.css';
|
|
||||||
|
|
||||||
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
|
|
||||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
|
||||||
import.meta.url,
|
|
||||||
).toString();
|
|
||||||
|
|
||||||
function LazyPage({ pageNumber, scale, containerWidth }: { pageNumber: number; scale: number; containerWidth: number }) {
|
|
||||||
const { ref, inView } = useInView({
|
|
||||||
triggerOnce: false,
|
|
||||||
rootMargin: '150% 0px',
|
|
||||||
});
|
|
||||||
|
|
||||||
const pageWidth = Math.min(containerWidth * 0.9, 900);
|
|
||||||
const estimatedHeight = pageWidth * 1.414 * scale;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
className="flex justify-center w-full mb-4"
|
|
||||||
style={{ minHeight: inView ? undefined : `${estimatedHeight}px` }}
|
|
||||||
>
|
|
||||||
{inView ? (
|
|
||||||
<Page
|
|
||||||
pageNumber={pageNumber}
|
|
||||||
scale={scale}
|
|
||||||
width={pageWidth}
|
|
||||||
renderTextLayer={true}
|
|
||||||
renderAnnotationLayer={true}
|
|
||||||
className="bg-white shadow-xl"
|
|
||||||
loading={
|
|
||||||
<div
|
|
||||||
className="bg-white shadow-xl flex items-center justify-center"
|
|
||||||
style={{ width: `${pageWidth * scale}px`, height: `${estimatedHeight}px` }}
|
|
||||||
>
|
|
||||||
<Loader2 className="w-6 h-6 animate-spin text-gray-400" />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className="bg-gray-100 shadow-xl rounded"
|
|
||||||
style={{ width: `${pageWidth * scale}px`, height: `${estimatedHeight}px` }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function PdfViewer({ url }: { url: string }) {
|
export default function PdfViewer({ url }: { url: string }) {
|
||||||
const [numPages, setNumPages] = useState<number>();
|
|
||||||
const [scale, setScale] = useState<number>(1.0);
|
|
||||||
const [loadError, setLoadError] = useState<string | null>(null);
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
|
||||||
const [containerWidth, setContainerWidth] = useState(800);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const el = containerRef.current;
|
|
||||||
if (!el) return;
|
|
||||||
const ro = new ResizeObserver(([entry]) => {
|
|
||||||
setContainerWidth(entry.contentRect.width);
|
|
||||||
});
|
|
||||||
ro.observe(el);
|
|
||||||
return () => ro.disconnect();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
function onDocumentLoadSuccess({ numPages }: { numPages: number }): void {
|
|
||||||
setNumPages(numPages);
|
|
||||||
setLoadError(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loadError) {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col items-center justify-center h-full gap-2">
|
|
||||||
<span className="text-sm font-medium" style={{ color: 'var(--color-danger, #ef4444)' }}>{loadError}</span>
|
|
||||||
<a
|
|
||||||
href={url}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
download
|
|
||||||
className="px-4 py-2 mt-2 rounded-lg text-sm font-medium transition-opacity hover:opacity-90"
|
|
||||||
style={{ backgroundColor: 'var(--color-accent)', color: 'white' }}
|
|
||||||
>
|
|
||||||
Download Instead
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col w-full h-full">
|
<div className="flex flex-col w-full h-full bg-neutral-100 dark:bg-neutral-900 relative">
|
||||||
{/* PDF Toolbar */}
|
<iframe
|
||||||
<div className="flex items-center justify-between p-2 border-b" style={{ borderColor: 'var(--color-border-subtle)', backgroundColor: 'var(--color-bg-elevated)' }}>
|
src={`${url}#toolbar=1&navpanes=0`}
|
||||||
<div className="flex items-center gap-2">
|
className="w-full h-full border-none absolute inset-0"
|
||||||
<span className="text-sm font-medium min-w-[80px] text-center" style={{ color: 'var(--color-text-secondary)' }}>
|
title="PDF Viewer"
|
||||||
{numPages ? `${numPages} page${numPages !== 1 ? 's' : ''}` : 'Loading...'}
|
/>
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<button
|
|
||||||
onClick={() => setScale(s => Math.max(0.5, +(s - 0.25).toFixed(2)))}
|
|
||||||
className="p-1 rounded hover:bg-white/10"
|
|
||||||
style={{ color: 'var(--color-text-primary)' }}
|
|
||||||
>
|
|
||||||
<ZoomOut className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
<span className="text-sm font-medium w-12 text-center" style={{ color: 'var(--color-text-secondary)' }}>
|
|
||||||
{Math.round(scale * 100)}%
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
onClick={() => setScale(s => Math.min(3, +(s + 0.25).toFixed(2)))}
|
|
||||||
className="p-1 rounded hover:bg-white/10"
|
|
||||||
style={{ color: 'var(--color-text-primary)' }}
|
|
||||||
>
|
|
||||||
<ZoomIn className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* PDF Content */}
|
|
||||||
<div ref={containerRef} className="flex-1 overflow-auto bg-neutral-100 dark:bg-neutral-900 py-4 flex flex-col items-center">
|
|
||||||
<Document
|
|
||||||
file={url}
|
|
||||||
onLoadSuccess={onDocumentLoadSuccess}
|
|
||||||
onLoadError={(error) => {
|
|
||||||
console.error('Failed to load PDF:', error);
|
|
||||||
setLoadError('Failed to load PDF file.');
|
|
||||||
}}
|
|
||||||
loading={
|
|
||||||
<div className="flex flex-col items-center justify-center h-full mt-20 gap-3">
|
|
||||||
<Loader2 className="w-8 h-8 animate-spin" style={{ color: 'var(--color-accent)' }} />
|
|
||||||
<span className="text-sm" style={{ color: 'var(--color-text-secondary)' }}>Loading PDF...</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
className="flex flex-col items-center gap-2 w-full"
|
|
||||||
>
|
|
||||||
{numPages && Array.from(new Array(numPages), (_, index) => (
|
|
||||||
<LazyPage
|
|
||||||
key={`page_${index + 1}`}
|
|
||||||
pageNumber={index + 1}
|
|
||||||
scale={scale}
|
|
||||||
containerWidth={containerWidth}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Document>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Reference in a new issue