| import React, { useState, useEffect, useRef } from 'react'; | |
| import { motion, AnimatePresence } from 'framer-motion'; | |
| import { | |
| FileText, Download, Eye, ZoomIn, ZoomOut, RotateCw, | |
| Maximize2, ArrowLeft, Settings, Printer, Share2 | |
| } from 'lucide-react'; | |
| const DocumentViewer = ({ file, onBack }) => { | |
| const [viewMode, setViewMode] = useState('preview'); | |
| const [zoom, setZoom] = useState(1); | |
| const [rotation, setRotation] = useState(0); | |
| const [isFullscreen, setIsFullscreen] = useState(false); | |
| const [documentUrl, setDocumentUrl] = useState(null); | |
| const [isLoading, setIsLoading] = useState(true); | |
| const viewerRef = useRef(null); | |
| useEffect(() => { | |
| if (file) { | |
| const url = URL.createObjectURL(file); | |
| setDocumentUrl(url); | |
| setIsLoading(false); | |
| return () => URL.revokeObjectURL(url); | |
| } | |
| }, [file]); | |
| const handleZoomIn = () => setZoom(prev => Math.min(prev + 0.25, 3)); | |
| const handleZoomOut = () => setZoom(prev => Math.max(prev - 0.25, 0.5)); | |
| const handleRotate = () => setRotation(prev => (prev + 90) % 360); | |
| const toggleFullscreen = async () => { | |
| try { | |
| if (!document.fullscreenElement) { | |
| await viewerRef.current?.requestFullscreen(); | |
| setIsFullscreen(true); | |
| } else { | |
| await document.exitFullscreen(); | |
| setIsFullscreen(false); | |
| } | |
| } catch (error) { | |
| setIsFullscreen(!isFullscreen); | |
| } | |
| }; | |
| const handleDownload = () => { | |
| if (documentUrl) { | |
| const a = document.createElement('a'); | |
| a.href = documentUrl; | |
| a.download = file.name; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| } | |
| }; | |
| const renderPDFViewer = () => ( | |
| <div className="pdf-viewer-container"> | |
| <div className="pdf-embed-wrapper" style={{ | |
| transform: `scale(${zoom}) rotate(${rotation}deg)`, | |
| transformOrigin: 'center center' | |
| }}> | |
| <embed | |
| src={documentUrl} | |
| type="application/pdf" | |
| className="pdf-embed" | |
| title={file.name} | |
| /> | |
| </div> | |
| </div> | |
| ); | |
| const renderHTMLViewer = () => ( | |
| <div className="html-viewer-container"> | |
| <div className="html-content-wrapper" style={{ | |
| transform: `scale(${zoom}) rotate(${rotation}deg)`, | |
| transformOrigin: 'center center' | |
| }}> | |
| <iframe | |
| src={documentUrl} | |
| className="html-iframe" | |
| title={file.name} | |
| sandbox="allow-same-origin allow-scripts" | |
| /> | |
| </div> | |
| </div> | |
| ); | |
| if (!file) return null; | |
| const isPDF = file.type === 'application/pdf'; | |
| const isHTML = file.type === 'text/html' || file.name.toLowerCase().endsWith('.html'); | |
| return ( | |
| <div className={`document-viewer ${isFullscreen ? 'fullscreen' : ''}`} ref={viewerRef}> | |
| {/* Glassmorphism Background */} | |
| <div className="viewer-background"> | |
| <div className="orb-background-container"> | |
| <div className="floating-orb orb-1"></div> | |
| <div className="floating-orb orb-2"></div> | |
| <div className="floating-orb orb-3"></div> | |
| </div> | |
| </div> | |
| {/* Header Controls */} | |
| <motion.header | |
| className="viewer-header glass-panel" | |
| initial={{ opacity: 0, y: -20 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| transition={{ duration: 0.6 }} | |
| > | |
| <div className="header-left"> | |
| <motion.button | |
| className="control-btn back-btn" | |
| onClick={onBack} | |
| whileHover={{ scale: 1.05 }} | |
| whileTap={{ scale: 0.95 }} | |
| > | |
| <ArrowLeft size={18} /> | |
| Back | |
| </motion.button> | |
| <div className="document-info"> | |
| <h2 className="document-title">{file.name}</h2> | |
| <div className="document-meta"> | |
| <span className="format-badge"> | |
| {isPDF ? 'PDF' : 'HTML'} | |
| </span> | |
| <span className="file-size"> | |
| {(file.size / 1024 / 1024).toFixed(2)} MB | |
| </span> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="header-controls"> | |
| <div className="zoom-controls glass-group"> | |
| <motion.button | |
| className="control-btn" | |
| onClick={handleZoomOut} | |
| disabled={zoom <= 0.5} | |
| whileHover={{ scale: 1.05 }} | |
| whileTap={{ scale: 0.95 }} | |
| > | |
| <ZoomOut size={16} /> | |
| </motion.button> | |
| <span className="zoom-display">{Math.round(zoom * 100)}%</span> | |
| <motion.button | |
| className="control-btn" | |
| onClick={handleZoomIn} | |
| disabled={zoom >= 3} | |
| whileHover={{ scale: 1.05 }} | |
| whileTap={{ scale: 0.95 }} | |
| > | |
| <ZoomIn size={16} /> | |
| </motion.button> | |
| </div> | |
| <motion.button | |
| className="control-btn" | |
| onClick={handleRotate} | |
| whileHover={{ scale: 1.05 }} | |
| whileTap={{ scale: 0.95 }} | |
| > | |
| <RotateCw size={16} /> | |
| </motion.button> | |
| <motion.button | |
| className="control-btn" | |
| onClick={toggleFullscreen} | |
| whileHover={{ scale: 1.05 }} | |
| whileTap={{ scale: 0.95 }} | |
| > | |
| <Maximize2 size={16} /> | |
| </motion.button> | |
| <motion.button | |
| className="control-btn primary" | |
| onClick={handleDownload} | |
| whileHover={{ scale: 1.05 }} | |
| whileTap={{ scale: 0.95 }} | |
| > | |
| <Download size={16} /> | |
| Download | |
| </motion.button> | |
| </div> | |
| </motion.header> | |
| {/* Document Content */} | |
| <motion.main | |
| className="viewer-content glass-panel" | |
| initial={{ opacity: 0, scale: 0.95 }} | |
| animate={{ opacity: 1, scale: 1 }} | |
| transition={{ duration: 0.6, delay: 0.1 }} | |
| > | |
| <AnimatePresence mode="wait"> | |
| {isLoading ? ( | |
| <motion.div | |
| key="loading" | |
| className="loading-state" | |
| initial={{ opacity: 0 }} | |
| animate={{ opacity: 1 }} | |
| exit={{ opacity: 0 }} | |
| > | |
| <div className="loading-spinner"></div> | |
| <p>Loading document...</p> | |
| </motion.div> | |
| ) : ( | |
| <motion.div | |
| key="content" | |
| className="document-content" | |
| initial={{ opacity: 0 }} | |
| animate={{ opacity: 1 }} | |
| exit={{ opacity: 0 }} | |
| > | |
| {isPDF ? renderPDFViewer() : isHTML ? renderHTMLViewer() : ( | |
| <div className="unsupported-format"> | |
| <FileText size={48} /> | |
| <p>Unsupported file format</p> | |
| </div> | |
| )} | |
| </motion.div> | |
| )} | |
| </AnimatePresence> | |
| </motion.main> | |
| <style jsx>{` | |
| .document-viewer { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100vw; | |
| height: 100vh; | |
| background: #0f0f23; | |
| z-index: 1000; | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| } | |
| .document-viewer.fullscreen { | |
| z-index: 9999; | |
| } | |
| .viewer-background { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| pointer-events: none; | |
| z-index: -1; | |
| } | |
| .orb-background-container { | |
| position: relative; | |
| width: 100%; | |
| height: 100%; | |
| overflow: hidden; | |
| } | |
| .floating-orb { | |
| position: absolute; | |
| border-radius: 50%; | |
| filter: blur(80px); | |
| opacity: 0.3; | |
| animation: float 20s ease-in-out infinite; | |
| } | |
| .orb-1 { | |
| top: 10%; | |
| right: 20%; | |
| width: 300px; | |
| height: 300px; | |
| background: linear-gradient(45deg, #4facfe, #8b5cf6); | |
| animation-delay: 0s; | |
| } | |
| .orb-2 { | |
| bottom: 20%; | |
| left: 15%; | |
| width: 250px; | |
| height: 250px; | |
| background: linear-gradient(45deg, #f093fb, #f5576c); | |
| animation-delay: -7s; | |
| } | |
| .orb-3 { | |
| top: 50%; | |
| left: 50%; | |
| width: 200px; | |
| height: 200px; | |
| background: linear-gradient(45deg, #06b6d4, #10b981); | |
| animation-delay: -14s; | |
| } | |
| @keyframes float { | |
| 0%, 100% { transform: translate(0, 0) rotate(0deg); } | |
| 25% { transform: translate(-20px, -30px) rotate(90deg); } | |
| 50% { transform: translate(10px, -20px) rotate(180deg); } | |
| 75% { transform: translate(-10px, 10px) rotate(270deg); } | |
| } | |
| .viewer-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: 16px 24px; | |
| margin: 12px; | |
| margin-bottom: 0; | |
| border-radius: 16px; | |
| backdrop-filter: blur(25px); | |
| -webkit-backdrop-filter: blur(25px); | |
| background: rgba(0, 0, 0, 0.2); | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| flex-shrink: 0; | |
| } | |
| .header-left { | |
| display: flex; | |
| align-items: center; | |
| gap: 20px; | |
| } | |
| .back-btn { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| padding: 8px 16px; | |
| background: rgba(255, 255, 255, 0.05); | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| border-radius: 8px; | |
| color: #ffffff; | |
| font-size: 0.875rem; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| } | |
| .back-btn:hover { | |
| background: rgba(255, 255, 255, 0.1); | |
| border-color: rgba(79, 172, 254, 0.3); | |
| } | |
| .document-info { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 4px; | |
| } | |
| .document-title { | |
| font-size: 1.25rem; | |
| font-weight: 600; | |
| color: #ffffff; | |
| margin: 0; | |
| } | |
| .document-meta { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| font-size: 0.75rem; | |
| } | |
| .format-badge { | |
| background: rgba(79, 172, 254, 0.2); | |
| color: #4facfe; | |
| padding: 4px 12px; | |
| border-radius: 6px; | |
| font-weight: 500; | |
| font-size: 0.75rem; | |
| letter-spacing: 0.5px; | |
| } | |
| .file-size { | |
| color: rgba(255, 255, 255, 0.6); | |
| } | |
| .header-controls { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| } | |
| .glass-group { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| background: rgba(255, 255, 255, 0.05); | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| border-radius: 8px; | |
| padding: 4px 8px; | |
| } | |
| .control-btn { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| padding: 8px 12px; | |
| background: rgba(255, 255, 255, 0.05); | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| border-radius: 6px; | |
| color: rgba(255, 255, 255, 0.8); | |
| font-size: 0.875rem; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| } | |
| .control-btn:hover:not(:disabled) { | |
| background: rgba(255, 255, 255, 0.1); | |
| color: #ffffff; | |
| border-color: rgba(79, 172, 254, 0.3); | |
| } | |
| .control-btn:disabled { | |
| opacity: 0.5; | |
| cursor: not-allowed; | |
| } | |
| .control-btn.primary { | |
| background: linear-gradient(135deg, #4facfe, #8b5cf6); | |
| border-color: rgba(79, 172, 254, 0.3); | |
| color: #ffffff; | |
| } | |
| .control-btn.primary:hover { | |
| box-shadow: 0 4px 15px rgba(79, 172, 254, 0.3); | |
| } | |
| .zoom-display { | |
| font-size: 0.75rem; | |
| color: rgba(255, 255, 255, 0.8); | |
| min-width: 40px; | |
| text-align: center; | |
| } | |
| .viewer-content { | |
| flex: 1; | |
| margin: 12px; | |
| margin-top: 0; | |
| border-radius: 16px; | |
| backdrop-filter: blur(25px); | |
| -webkit-backdrop-filter: blur(25px); | |
| background: rgba(0, 0, 0, 0.1); | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| overflow: hidden; | |
| position: relative; | |
| } | |
| .document-content { | |
| width: 100%; | |
| height: 100%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| overflow: auto; | |
| } | |
| .pdf-viewer-container, | |
| .html-viewer-container { | |
| width: 100%; | |
| height: 100%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 20px; | |
| } | |
| .pdf-embed-wrapper, | |
| .html-content-wrapper { | |
| transition: transform 0.3s ease; | |
| border-radius: 12px; | |
| overflow: hidden; | |
| box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); | |
| } | |
| .pdf-embed { | |
| width: 80vw; | |
| height: 80vh; | |
| border: none; | |
| border-radius: 12px; | |
| background: rgba(255, 255, 255, 0.95); | |
| } | |
| .html-iframe { | |
| width: 80vw; | |
| height: 80vh; | |
| border: none; | |
| border-radius: 12px; | |
| background: rgba(255, 255, 255, 0.95); | |
| } | |
| .loading-state { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 16px; | |
| color: rgba(255, 255, 255, 0.8); | |
| } | |
| .loading-spinner { | |
| width: 40px; | |
| height: 40px; | |
| border: 3px solid rgba(79, 172, 254, 0.3); | |
| border-top: 3px solid #4facfe; | |
| border-radius: 50%; | |
| animation: spin 1s linear infinite; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| .unsupported-format { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 16px; | |
| color: rgba(255, 255, 255, 0.6); | |
| } | |
| @media (max-width: 768px) { | |
| .viewer-header { | |
| flex-direction: column; | |
| gap: 16px; | |
| padding: 16px; | |
| } | |
| .header-left { | |
| width: 100%; | |
| justify-content: space-between; | |
| } | |
| .header-controls { | |
| width: 100%; | |
| justify-content: center; | |
| flex-wrap: wrap; | |
| } | |
| .pdf-embed, | |
| .html-iframe { | |
| width: 95vw; | |
| height: 70vh; | |
| } | |
| } | |
| `}</style> | |
| </div> | |
| ); | |
| }; | |
| export default DocumentViewer; |