Spaces:
Sleeping
Sleeping
| import { useState, useRef, useEffect } from 'react'; | |
| import { Document, Page, pdfjs } from 'react-pdf'; | |
| import 'react-pdf/dist/Page/AnnotationLayer.css'; | |
| import 'react-pdf/dist/Page/TextLayer.css'; | |
| pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.min.js'; | |
| const DocumentViewer = ({ selectedFile, documentData, onPageChange }) => { | |
| const pdfContainerRef = useRef(null); | |
| const [numPages, setNumPages] = useState(null); | |
| const [currentPage, setCurrentPage] = useState(1); | |
| const [zoomLevel, setZoomLevel] = useState(1); | |
| const [visiblePages, setVisiblePages] = useState(new Set([1])); | |
| const [containerWidth, setContainerWidth] = useState(0); | |
| // Update container width on mount and resize | |
| useEffect(() => { | |
| const updateContainerWidth = () => { | |
| if (pdfContainerRef.current) { | |
| const width = pdfContainerRef.current.clientWidth; | |
| // Subtract padding and some margin for optimal viewing | |
| const availableWidth = width - 32; // 16px padding on each side | |
| setContainerWidth(availableWidth); | |
| } | |
| }; | |
| updateContainerWidth(); | |
| window.addEventListener('resize', updateContainerWidth); | |
| return () => window.removeEventListener('resize', updateContainerWidth); | |
| }, []); | |
| // Expose goToPage function to parent component (only once when component mounts) | |
| useEffect(() => { | |
| if (onPageChange) { | |
| onPageChange({ goToPage }); | |
| } | |
| }, [onPageChange]); | |
| // Calculate optimal page width | |
| const getPageWidth = () => { | |
| if (!containerWidth) return 600; // Fallback | |
| // Use container width minus some margin, but respect zoom level | |
| const baseWidth = Math.min(containerWidth * 0.99, 2000); // Max 800px for readability | |
| return baseWidth * zoomLevel; | |
| }; | |
| // Handle scroll to update current page and track visible pages | |
| const handleScroll = () => { | |
| if (!pdfContainerRef.current || !numPages) return; | |
| const container = pdfContainerRef.current; | |
| const scrollTop = container.scrollTop; | |
| const containerHeight = container.clientHeight; | |
| const totalScrollHeight = container.scrollHeight - containerHeight; | |
| // Calculate which page we're viewing based on scroll position | |
| const scrollPercent = scrollTop / totalScrollHeight; | |
| const newPage = Math.min(Math.floor(scrollPercent * numPages) + 1, numPages); | |
| if (newPage !== currentPage) { | |
| setCurrentPage(newPage); | |
| } | |
| // Track visible pages based on zoom level | |
| const newVisiblePages = new Set(); | |
| const visibleRange = Math.max(1, Math.ceil(2 / zoomLevel)); | |
| for (let i = Math.max(1, newPage - visibleRange); i <= Math.min(numPages, newPage + visibleRange); i++) { | |
| newVisiblePages.add(i); | |
| } | |
| // Update visible pages if changed | |
| if (newVisiblePages.size !== visiblePages.size || | |
| ![...newVisiblePages].every(page => visiblePages.has(page))) { | |
| setVisiblePages(newVisiblePages); | |
| } | |
| }; | |
| // Jump to specific page | |
| const goToPage = (pageNumber) => { | |
| if (!pdfContainerRef.current || !numPages || pageNumber < 1 || pageNumber > numPages) return; | |
| // Update visible pages immediately for target page | |
| const newVisiblePages = new Set(); | |
| const visibleRange = Math.max(1, Math.ceil(2 / zoomLevel)); | |
| for (let i = Math.max(1, pageNumber - visibleRange); i <= Math.min(numPages, pageNumber + visibleRange); i++) { | |
| newVisiblePages.add(i); | |
| } | |
| setVisiblePages(newVisiblePages); | |
| // Use setTimeout to ensure pages are rendered before scrolling | |
| setTimeout(() => { | |
| const container = pdfContainerRef.current; | |
| if (!container) return; | |
| // Find the target page element by its data attribute or position | |
| const pageElements = container.querySelectorAll('[data-page-number]'); | |
| let targetElement = null; | |
| // If we can't find elements by data attribute, calculate position manually | |
| if (pageElements.length === 0) { | |
| // Calculate approximate position based on page height | |
| // Each page has some margin (mb-4 = 16px) plus the actual page height | |
| const containerHeight = container.clientHeight; | |
| const totalContent = container.scrollHeight; | |
| const avgPageHeight = totalContent / numPages; | |
| const targetPosition = (pageNumber - 1) * avgPageHeight; | |
| // Center the page in viewport | |
| const scrollPosition = Math.max(0, targetPosition - containerHeight / 4); | |
| container.scrollTo({ | |
| top: scrollPosition, | |
| behavior: 'smooth' | |
| }); | |
| } else { | |
| // Find the specific page element | |
| for (const element of pageElements) { | |
| if (parseInt(element.getAttribute('data-page-number')) === pageNumber) { | |
| targetElement = element; | |
| break; | |
| } | |
| } | |
| if (targetElement) { | |
| // Scroll to center the page in viewport | |
| const elementRect = targetElement.getBoundingClientRect(); | |
| const containerRect = container.getBoundingClientRect(); | |
| const scrollOffset = container.scrollTop; | |
| const targetPosition = scrollOffset + elementRect.top - containerRect.top - (container.clientHeight - elementRect.height) / 4; | |
| container.scrollTo({ | |
| top: Math.max(0, targetPosition), | |
| behavior: 'smooth' | |
| }); | |
| } | |
| } | |
| }, 100); // Small delay to ensure rendering | |
| }; | |
| // Zoom controls | |
| const zoomIn = () => setZoomLevel(prev => Math.min(prev + 0.25, 3)); | |
| const zoomOut = () => setZoomLevel(prev => Math.max(prev - 0.25, 0.5)); | |
| const resetZoom = () => setZoomLevel(1); | |
| if (!selectedFile) { | |
| return ( | |
| <div className="bg-white rounded-lg shadow-sm flex items-center justify-center h-full"> | |
| <div className="text-center text-gray-500"> | |
| <p>No PDF selected</p> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| return ( | |
| <div className="bg-white rounded-lg shadow-sm flex flex-col relative" style={{ width: '100%', height: '100%' }}> | |
| <div className="sticky top-0 bg-white rounded-t-lg px-6 py-4 border-b border-gray-200 z-10"> | |
| <h2 className="text-lg font-semibold text-left text-gray-800"> | |
| {documentData?.filename || 'Document'} | |
| </h2> | |
| </div> | |
| {/* PDF Container */} | |
| <div | |
| ref={pdfContainerRef} | |
| className="flex-1 overflow-auto flex justify-center bg-gray-100" | |
| onScroll={handleScroll} | |
| > | |
| <div className="py-4"> | |
| <Document | |
| file={selectedFile} | |
| onLoadSuccess={({ numPages }) => setNumPages(numPages)} | |
| > | |
| {/* Render all pages continuously */} | |
| {numPages && Array.from(new Array(numPages), (_, index) => { | |
| const pageNum = index + 1; | |
| const isVisible = visiblePages.has(pageNum); | |
| return ( | |
| <div key={pageNum} className="mb-4 flex justify-center" data-page-number={pageNum}> | |
| <Page | |
| pageNumber={pageNum} | |
| width={isVisible ? getPageWidth() : getPageWidth() / zoomLevel} | |
| /> | |
| </div> | |
| ); | |
| })} | |
| </Document> | |
| </div> | |
| </div> | |
| {/* Pagination overlay - floating pill */} | |
| {numPages && ( | |
| <div className="absolute bottom-4 left-1/2 transform -translate-x-1/2 z-10"> | |
| <div className="flex items-center bg-gray-800/90 backdrop-blur-sm rounded-full shadow-lg px-3 py-2 space-x-3"> | |
| <button | |
| onClick={() => goToPage(Math.max(currentPage - 1, 1))} | |
| disabled={currentPage <= 1} | |
| className="w-8 h-8 rounded-full bg-gray-600 hover:bg-gray-500 disabled:opacity-30 disabled:cursor-not-allowed flex items-center justify-center transition-colors text-white" | |
| > | |
| <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"> | |
| <path d="M10 12l-4-4 4-4v8z"/> | |
| </svg> | |
| </button> | |
| <span className="px-3 py-1 text-sm font-medium text-white min-w-[60px] text-center"> | |
| {currentPage}/{numPages} | |
| </span> | |
| <button | |
| onClick={() => goToPage(Math.min(currentPage + 1, numPages))} | |
| disabled={currentPage >= numPages} | |
| className="w-8 h-8 rounded-full bg-gray-600 hover:bg-gray-500 disabled:opacity-30 disabled:cursor-not-allowed flex items-center justify-center transition-colors text-white" | |
| > | |
| <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"> | |
| <path d="M6 4l4 4-4 4V4z"/> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| )} | |
| {/* Zoom controls overlay - bottom right */} | |
| {numPages && ( | |
| <div className="absolute bottom-4 right-4 z-10 flex flex-col items-center space-y-2"> | |
| {/* Main zoom pill - vertical */} | |
| <div className="flex flex-col items-center bg-gray-800/90 backdrop-blur-sm rounded-full shadow-lg px-2 py-2 space-y-1"> | |
| <button | |
| onClick={zoomIn} | |
| disabled={zoomLevel >= 3} | |
| className="w-6 h-6 rounded-full bg-gray-600 hover:bg-gray-500 disabled:opacity-30 disabled:cursor-not-allowed flex items-center justify-center transition-colors text-white" | |
| > | |
| <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"> | |
| <path d="M8 4v4H4v1h4v4h1V9h4V8H9V4z"/> | |
| </svg> | |
| </button> | |
| <button | |
| onClick={zoomOut} | |
| disabled={zoomLevel <= 0.5} | |
| className="w-6 h-6 rounded-full bg-gray-600 hover:bg-gray-500 disabled:opacity-30 disabled:cursor-not-allowed flex items-center justify-center transition-colors text-white" | |
| > | |
| <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"> | |
| <path d="M4 8h8v1H4z"/> | |
| </svg> | |
| </button> | |
| </div> | |
| {/* Reset button below */} | |
| <button | |
| onClick={resetZoom} | |
| className="w-10 h-10 bg-gray-700 hover:bg-gray-500 backdrop-blur-sm rounded-full shadow-lg flex items-center justify-center text-white transition-colors" | |
| > | |
| <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" stroke="currentColor" strokeWidth="0.5"> | |
| <path d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z" strokeWidth="1"/> | |
| <path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z"/> | |
| </svg> | |
| </button> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| }; | |
| export default DocumentViewer; |