Spaces:
Running
Running
| 'use client' | |
| import React, { useState } from 'react' | |
| import dynamic from 'next/dynamic' | |
| import { MagnifyingGlassPlus, MagnifyingGlassMinus, ArrowLeft, ArrowRight } from '@phosphor-icons/react' | |
| // Dynamically import react-pdf with no SSR | |
| const Document = dynamic( | |
| () => import('react-pdf').then((mod) => mod.Document), | |
| { ssr: false } | |
| ) | |
| const Page = dynamic( | |
| () => import('react-pdf').then((mod) => mod.Page), | |
| { ssr: false } | |
| ) | |
| // Configure PDF.js worker | |
| if (typeof window !== 'undefined') { | |
| import('react-pdf').then((pdfjs) => { | |
| pdfjs.pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.pdfjs.version}/build/pdf.worker.min.mjs` | |
| }) | |
| } | |
| interface PDFViewerProps { | |
| url: string | |
| scale: number | |
| onZoomIn: () => void | |
| onZoomOut: () => void | |
| } | |
| export function PDFViewer({ url, scale, onZoomIn, onZoomOut }: PDFViewerProps) { | |
| const [numPages, setNumPages] = useState<number | null>(null) | |
| const [pageNumber, setPageNumber] = useState(1) | |
| const [error, setError] = useState<string | null>(null) | |
| const [isLoading, setIsLoading] = useState(true) | |
| const onDocumentLoadSuccess = ({ numPages }: { numPages: number }) => { | |
| setNumPages(numPages) | |
| setPageNumber(1) | |
| setError(null) | |
| setIsLoading(false) | |
| } | |
| const changePage = (offset: number) => { | |
| setPageNumber(prevPageNumber => { | |
| const newPageNumber = prevPageNumber + offset | |
| if (newPageNumber < 1 || newPageNumber > (numPages || 1)) { | |
| return prevPageNumber | |
| } | |
| return newPageNumber | |
| }) | |
| } | |
| if (!url) { | |
| return <div className="flex items-center justify-center h-full text-gray-500">No PDF to display</div> | |
| } | |
| return ( | |
| <div className="flex flex-col h-full"> | |
| <div className="flex items-center gap-2 p-3 border-b border-gray-200 bg-gray-50"> | |
| <button | |
| onClick={onZoomOut} | |
| className="p-2 hover:bg-gray-200 rounded-lg transition-colors" | |
| title="Zoom Out" | |
| > | |
| <MagnifyingGlassMinus className="h-5 w-5" /> | |
| </button> | |
| <span className="text-sm text-gray-600 min-w-[60px] text-center">{Math.round(scale * 100)}%</span> | |
| <button | |
| onClick={onZoomIn} | |
| className="p-2 hover:bg-gray-200 rounded-lg transition-colors" | |
| title="Zoom In" | |
| > | |
| <MagnifyingGlassPlus className="h-5 w-5" /> | |
| </button> | |
| {numPages && ( | |
| <> | |
| <div className="mx-4 h-6 w-px bg-gray-300" /> | |
| <button | |
| onClick={() => changePage(-1)} | |
| disabled={pageNumber <= 1} | |
| className="p-2 hover:bg-gray-200 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed" | |
| title="Previous Page" | |
| > | |
| <ArrowLeft className="h-5 w-5" /> | |
| </button> | |
| <span className="text-sm text-gray-600 min-w-[80px] text-center"> | |
| Page {pageNumber} of {numPages} | |
| </span> | |
| <button | |
| onClick={() => changePage(1)} | |
| disabled={pageNumber >= numPages} | |
| className="p-2 hover:bg-gray-200 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed" | |
| title="Next Page" | |
| > | |
| <ArrowRight className="h-5 w-5" /> | |
| </button> | |
| </> | |
| )} | |
| </div> | |
| <div className="flex-1 overflow-auto p-4 bg-gray-50"> | |
| {typeof window !== 'undefined' && url && ( | |
| <Document | |
| file={url} | |
| onLoadSuccess={onDocumentLoadSuccess} | |
| onLoadError={(err) => { | |
| console.warn('PDF load error:', err) | |
| setError('Unable to load PDF. The file might be corrupted or in an unsupported format.') | |
| setIsLoading(false) | |
| }} | |
| loading={ | |
| <div className="flex items-center justify-center h-full"> | |
| <div className="text-gray-600">Loading PDF...</div> | |
| </div> | |
| } | |
| error={ | |
| <div className="flex flex-col items-center justify-center h-full"> | |
| <div className="text-red-500 mb-2">Failed to load PDF</div> | |
| <div className="text-sm text-gray-600">{error || 'The file might be corrupted or in an unsupported format.'}</div> | |
| </div> | |
| } | |
| className="flex justify-center" | |
| > | |
| {!isLoading && !error && ( | |
| <Page | |
| pageNumber={pageNumber} | |
| scale={scale} | |
| renderTextLayer={false} | |
| renderAnnotationLayer={false} | |
| className="shadow-lg bg-white" | |
| loading={ | |
| <div className="flex items-center justify-center h-96 bg-white shadow-lg"> | |
| <div className="text-gray-400">Loading page...</div> | |
| </div> | |
| } | |
| /> | |
| )} | |
| </Document> | |
| )} | |
| </div> | |
| </div> | |
| ) | |
| } |