Spaces:
Build error
Build error
| 'use client'; | |
| import * as React from 'react'; | |
| import { | |
| Dialog, | |
| DialogContent, | |
| DialogHeader, | |
| DialogTitle, | |
| DialogFooter, | |
| DialogClose, | |
| } from '@/components/ui/dialog'; | |
| import { Button } from '@/components/ui/button'; | |
| import { | |
| ChevronLeft, | |
| ChevronRight, | |
| ZoomIn, | |
| ZoomOut, | |
| Download, | |
| Loader2, | |
| Maximize, | |
| Minimize, | |
| } from 'lucide-react'; | |
| import { Document, Page, pdfjs } from 'react-pdf'; | |
| import 'react-pdf/dist/esm/Page/AnnotationLayer.css'; | |
| import 'react-pdf/dist/esm/Page/TextLayer.css'; | |
| import { cn } from '@/lib/utils'; | |
| // Configure the worker to load pdfs. | |
| pdfjs.GlobalWorkerOptions.workerSrc = `/pdf.worker.min.js`; | |
| interface PdfViewerProps { | |
| isOpen: boolean; | |
| onOpenChange: (isOpen: boolean) => void; | |
| fileUrl: string; | |
| fileName: string; | |
| } | |
| export function PdfViewer({ | |
| isOpen, | |
| onOpenChange, | |
| fileUrl, | |
| fileName, | |
| }: PdfViewerProps) { | |
| const [numPages, setNumPages] = React.useState<number | null>(null); | |
| const [pageNumber, setPageNumber] = React.useState(1); | |
| const [scale, setScale] = React.useState(1.0); | |
| const [isLoading, setIsLoading] = React.useState(true); | |
| const [isFullscreen, setIsFullscreen] = React.useState(false); | |
| const viewerRef = React.useRef<HTMLDivElement>(null); | |
| function onDocumentLoadSuccess({ numPages }: { numPages: number }) { | |
| setNumPages(numPages); | |
| setPageNumber(1); | |
| setIsLoading(false); | |
| } | |
| function onDocumentLoadError(error: Error) { | |
| console.error('Failed to load PDF:', error); | |
| setIsLoading(false); | |
| } | |
| const goToPrevPage = () => { | |
| setPageNumber((prevPageNumber) => Math.max(prevPageNumber - 1, 1)); | |
| }; | |
| const goToNextPage = () => { | |
| setPageNumber((prevPageNumber) => | |
| Math.min(prevPageNumber + 1, numPages || 1) | |
| ); | |
| }; | |
| const zoomIn = () => { | |
| setScale((prevScale) => Math.min(prevScale + 0.2, 3)); | |
| }; | |
| const zoomOut = () => { | |
| setScale((prevScale) => Math.max(prevScale - 0.2, 0.5)); | |
| }; | |
| const toggleFullscreen = () => { | |
| if (!viewerRef.current) return; | |
| if (!document.fullscreenElement) { | |
| viewerRef.current.requestFullscreen().catch(err => { | |
| console.error(`Error attempting to enable full-screen mode: ${err.message} (${err.name})`); | |
| }); | |
| } else { | |
| document.exitFullscreen(); | |
| } | |
| }; | |
| React.useEffect(() => { | |
| const handleFullscreenChange = () => { | |
| setIsFullscreen(!!document.fullscreenElement); | |
| }; | |
| document.addEventListener('fullscreenchange', handleFullscreenChange); | |
| return () => document.removeEventListener('fullscreenchange', handleFullscreenChange); | |
| }, []); | |
| React.useEffect(() => { | |
| if (isOpen) { | |
| setIsLoading(true); | |
| setNumPages(null); | |
| setPageNumber(1); | |
| setScale(1.0); | |
| } | |
| }, [isOpen, fileUrl]); | |
| return ( | |
| <Dialog open={isOpen} onOpenChange={onOpenChange}> | |
| <DialogContent className="max-w-4xl w-full h-[90vh] flex flex-col p-0 gap-0"> | |
| <div ref={viewerRef} className="flex flex-col w-full h-full bg-background"> | |
| <DialogHeader className="p-4 border-b shrink-0"> | |
| <DialogTitle className="truncate">{fileName}</DialogTitle> | |
| </DialogHeader> | |
| <div className="flex-1 overflow-auto flex items-center justify-center bg-muted/20 relative"> | |
| {isLoading && ( | |
| <div className="absolute inset-0 flex items-center justify-center bg-background/80 z-10"> | |
| <Loader2 className="h-8 w-8 animate-spin text-primary" /> | |
| </div> | |
| )} | |
| <Document | |
| file={fileUrl} | |
| onLoadSuccess={onDocumentLoadSuccess} | |
| onLoadError={onDocumentLoadError} | |
| loading="" | |
| > | |
| <Page | |
| pageNumber={pageNumber} | |
| scale={scale} | |
| renderTextLayer={false} | |
| renderAnnotationLayer={false} | |
| loading="" | |
| className="flex justify-center" | |
| /> | |
| </Document> | |
| </div> | |
| <DialogFooter className="p-2 border-t bg-background flex-wrap justify-between shrink-0"> | |
| <div className="flex items-center gap-2"> | |
| <Button | |
| variant="outline" | |
| size="icon" | |
| onClick={zoomOut} | |
| disabled={!numPages} | |
| > | |
| <ZoomOut className="h-4 w-4" /> | |
| </Button> | |
| <span className="text-sm text-muted-foreground">{Math.round(scale * 100)}%</span> | |
| <Button | |
| variant="outline" | |
| size="icon" | |
| onClick={zoomIn} | |
| disabled={!numPages} | |
| > | |
| <ZoomIn className="h-4 w-4" /> | |
| </Button> | |
| <Button variant="outline" size="icon" onClick={toggleFullscreen}> | |
| {isFullscreen ? <Minimize className="h-4 w-4" /> : <Maximize className="h-4 w-4" />} | |
| <span className="sr-only">{isFullscreen ? 'Exit Fullscreen' : 'Enter Fullscreen'}</span> | |
| </Button> | |
| </div> | |
| <div className="flex items-center gap-2"> | |
| <Button | |
| variant="outline" | |
| size="icon" | |
| onClick={goToPrevPage} | |
| disabled={pageNumber <= 1} | |
| > | |
| <ChevronLeft className="h-4 w-4" /> | |
| </Button> | |
| <span className="text-sm text-muted-foreground"> | |
| Page {pageNumber} of {numPages || '...'} | |
| </span> | |
| <Button | |
| variant="outline" | |
| size="icon" | |
| onClick={goToNextPage} | |
| disabled={!numPages || pageNumber >= numPages} | |
| > | |
| <ChevronRight className="h-4 w-4" /> | |
| </Button> | |
| </div> | |
| <div className="flex items-center gap-2"> | |
| <a href={fileUrl} download={fileName}> | |
| <Button variant="default"> | |
| <Download className="mr-2 h-4 w-4" /> | |
| Download | |
| </Button> | |
| </a> | |
| <DialogClose asChild> | |
| <Button variant="outline">Close</Button> | |
| </DialogClose> | |
| </div> | |
| </DialogFooter> | |
| </div> | |
| </DialogContent> | |
| </Dialog> | |
| ); | |
| } | |