Spaces:
Running
Running
| 'use client' | |
| import React, { useState, useEffect } from 'react' | |
| import { X, Download, MagnifyingGlassPlus, MagnifyingGlassMinus } from '@phosphor-icons/react' | |
| import mammoth from 'mammoth' | |
| import * as XLSX from 'xlsx' | |
| import { PDFViewer } from './PDFViewer' | |
| interface FilePreviewProps { | |
| file: { | |
| name: string | |
| path: string | |
| extension?: string | |
| size?: number | |
| } | |
| onClose: () => void | |
| onDownload: () => void | |
| passkey?: string | |
| isPublic?: boolean | |
| } | |
| export function FilePreview({ file, onClose, onDownload, passkey, isPublic = true }: FilePreviewProps) { | |
| const [content, setContent] = useState<any>(null) | |
| const [loading, setLoading] = useState(true) | |
| const [error, setError] = useState<string | null>(null) | |
| const [scale, setScale] = useState(1) | |
| // Excel specific state | |
| const [activeSheet, setActiveSheet] = useState(0) | |
| // For secure data, we read directly from /api/data | |
| // For public, use the download API | |
| const previewUrl = isPublic | |
| ? `/api/download?path=${encodeURIComponent(file.path)}&preview=true` | |
| : `/api/data?key=${encodeURIComponent(passkey || '')}&path=${encodeURIComponent(file.path)}` | |
| const ext = file.extension?.toLowerCase() || '' | |
| useEffect(() => { | |
| loadFileContent() | |
| }, [file]) | |
| const loadFileContent = async () => { | |
| setLoading(true) | |
| setError(null) | |
| try { | |
| switch (ext) { | |
| case 'docx': | |
| case 'doc': | |
| await loadWordDocument() | |
| break | |
| case 'xlsx': | |
| case 'xls': | |
| await loadExcelDocument() | |
| break | |
| case 'pptx': | |
| case 'ppt': | |
| await loadPowerPointDocument() | |
| break | |
| case 'txt': | |
| case 'md': | |
| case 'json': | |
| case 'js': | |
| case 'ts': | |
| case 'jsx': | |
| case 'tsx': | |
| case 'css': | |
| case 'html': | |
| case 'xml': | |
| case 'py': | |
| case 'java': | |
| case 'cpp': | |
| case 'c': | |
| case 'h': | |
| case 'sh': | |
| case 'yaml': | |
| case 'yml': | |
| case 'dart': | |
| case 'tex': | |
| await loadTextFile() | |
| break | |
| case 'csv': | |
| await loadCSVFile() | |
| break | |
| default: | |
| setContent(null) | |
| } | |
| } catch (err) { | |
| setError(err instanceof Error ? err.message : 'Failed to load file') | |
| } finally { | |
| setLoading(false) | |
| } | |
| } | |
| const loadWordDocument = async () => { | |
| try { | |
| const response = await fetch(previewUrl) | |
| const arrayBuffer = await response.arrayBuffer() | |
| const result = await mammoth.convertToHtml({ arrayBuffer }) | |
| setContent({ type: 'html', data: result.value }) | |
| } catch (err) { | |
| throw new Error('Failed to load Word document') | |
| } | |
| } | |
| const loadExcelDocument = async () => { | |
| try { | |
| const response = await fetch(previewUrl) | |
| const arrayBuffer = await response.arrayBuffer() | |
| const workbook = XLSX.read(arrayBuffer, { type: 'array' }) | |
| // Convert all sheets to HTML | |
| const sheets = workbook.SheetNames.map(name => ({ | |
| name, | |
| html: XLSX.utils.sheet_to_html(workbook.Sheets[name], { | |
| header: '<table class="excel-table">', | |
| footer: '</table>' | |
| }) | |
| })) | |
| setContent({ type: 'excel', data: sheets }) | |
| } catch (err) { | |
| throw new Error('Failed to load Excel document') | |
| } | |
| } | |
| const loadPowerPointDocument = async () => { | |
| // For PowerPoint, we'll show a message to download | |
| // Full PowerPoint rendering requires more complex libraries | |
| setContent({ type: 'powerpoint', data: null }) | |
| } | |
| const loadTextFile = async () => { | |
| try { | |
| // For secure data files, read from /api/data which returns file list with content | |
| if (!isPublic && passkey) { | |
| const response = await fetch(`/api/data?key=${encodeURIComponent(passkey)}&folder=`) | |
| const data = await response.json() | |
| const fileData = data.files?.find((f: any) => f.name === file.name) | |
| if (fileData && fileData.content) { | |
| setContent({ type: 'text', data: fileData.content }) | |
| } else { | |
| throw new Error('File content not available') | |
| } | |
| } else { | |
| const response = await fetch(previewUrl) | |
| const text = await response.text() | |
| setContent({ type: 'text', data: text }) | |
| } | |
| } catch (err) { | |
| throw new Error('Failed to load text file') | |
| } | |
| } | |
| const loadCSVFile = async () => { | |
| try { | |
| const response = await fetch(previewUrl) | |
| const text = await response.text() | |
| const workbook = XLSX.read(text, { type: 'string' }) | |
| const sheet = workbook.Sheets[workbook.SheetNames[0]] | |
| const html = XLSX.utils.sheet_to_html(sheet, { | |
| header: '<table class="csv-table">', | |
| footer: '</table>' | |
| }) | |
| setContent({ type: 'html', data: html }) | |
| } catch (err) { | |
| throw new Error('Failed to load CSV file') | |
| } | |
| } | |
| const renderContent = () => { | |
| if (loading) { | |
| return ( | |
| <div className="flex items-center justify-center h-full"> | |
| <div className="text-gray-600">Loading...</div> | |
| </div> | |
| ) | |
| } | |
| if (error) { | |
| return ( | |
| <div className="flex flex-col items-center justify-center h-full"> | |
| <p className="text-red-600 mb-4">{error}</p> | |
| <button | |
| onClick={onDownload} | |
| className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600" | |
| > | |
| Download to View | |
| </button> | |
| </div> | |
| ) | |
| } | |
| // PDF files | |
| if (ext === 'pdf') { | |
| return ( | |
| <PDFViewer | |
| url={previewUrl} | |
| scale={scale} | |
| onZoomIn={() => setScale(prev => Math.min(2, prev + 0.1))} | |
| onZoomOut={() => setScale(prev => Math.max(0.5, prev - 0.1))} | |
| /> | |
| ) | |
| } | |
| // Image files | |
| if (['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp', 'bmp'].includes(ext)) { | |
| return ( | |
| <div className="flex items-center justify-center h-full p-4"> | |
| <img | |
| src={previewUrl} | |
| alt={file.name} | |
| className="max-w-full max-h-full object-contain" | |
| style={{ transform: `scale(${scale})` }} | |
| /> | |
| <div className="absolute bottom-4 left-1/2 -translate-x-1/2 flex items-center gap-2 bg-white rounded-lg shadow-lg p-2"> | |
| <button | |
| onClick={() => setScale(prev => Math.max(0.5, prev - 0.1))} | |
| className="p-1 rounded hover:bg-gray-100" | |
| > | |
| <MagnifyingGlassMinus size={16} /> | |
| </button> | |
| <span className="text-sm px-2">{Math.round(scale * 100)}%</span> | |
| <button | |
| onClick={() => setScale(prev => Math.min(3, prev + 0.1))} | |
| className="p-1 rounded hover:bg-gray-100" | |
| > | |
| <MagnifyingGlassPlus size={16} /> | |
| </button> | |
| </div> | |
| </div> | |
| ) | |
| } | |
| // HTML content (Word docs, converted HTML) | |
| if (content?.type === 'html') { | |
| return ( | |
| <div className="p-4 overflow-auto h-full"> | |
| <div | |
| dangerouslySetInnerHTML={{ __html: content.data }} | |
| className="prose max-w-none" | |
| /> | |
| </div> | |
| ) | |
| } | |
| // Excel sheets | |
| if (content?.type === 'excel') { | |
| return ( | |
| <div className="flex flex-col h-full"> | |
| <div className="flex gap-2 p-2 border-b"> | |
| {content.data.map((sheet: any, index: number) => ( | |
| <button | |
| key={index} | |
| onClick={() => setActiveSheet(index)} | |
| className={`px-3 py-1 rounded ${activeSheet === index ? 'bg-blue-500 text-white' : 'bg-gray-100' | |
| }`} | |
| > | |
| {sheet.name} | |
| </button> | |
| ))} | |
| </div> | |
| <div className="flex-1 overflow-auto p-4"> | |
| <div | |
| dangerouslySetInnerHTML={{ __html: content.data[activeSheet].html }} | |
| className="excel-preview" | |
| /> | |
| </div> | |
| </div> | |
| ) | |
| } | |
| // Text files | |
| if (content?.type === 'text') { | |
| return ( | |
| <div className="h-full overflow-auto"> | |
| <pre className="p-4 text-sm font-mono whitespace-pre-wrap"> | |
| {content.data} | |
| </pre> | |
| </div> | |
| ) | |
| } | |
| // PowerPoint placeholder | |
| if (content?.type === 'powerpoint') { | |
| return ( | |
| <div className="flex flex-col items-center justify-center h-full"> | |
| <div className="text-6xl mb-4">📊</div> | |
| <p className="text-gray-600 mb-4">PowerPoint presentation</p> | |
| <p className="text-sm text-gray-500 mb-4">Preview requires download</p> | |
| <button | |
| onClick={onDownload} | |
| className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600" | |
| > | |
| Download to View | |
| </button> | |
| </div> | |
| ) | |
| } | |
| // Default fallback | |
| return ( | |
| <iframe | |
| src={previewUrl} | |
| className="w-full h-full border-0" | |
| title={file.name} | |
| /> | |
| ) | |
| } | |
| return ( | |
| <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50"> | |
| <div className="bg-white rounded-lg w-[90%] h-[90%] flex flex-col"> | |
| <div className="flex items-center justify-between p-4 border-b"> | |
| <div> | |
| <h2 className="text-lg font-semibold">{file.name}</h2> | |
| <p className="text-sm text-gray-500"> | |
| {ext.toUpperCase()} • {file.size ? formatFileSize(file.size) : 'Unknown size'} | |
| </p> | |
| </div> | |
| <div className="flex gap-2"> | |
| <button | |
| onClick={onDownload} | |
| className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 flex items-center gap-2" | |
| > | |
| <Download size={20} /> | |
| Download | |
| </button> | |
| <button | |
| onClick={onClose} | |
| className="p-2 hover:bg-gray-100 rounded" | |
| > | |
| <X size={20} /> | |
| </button> | |
| </div> | |
| </div> | |
| <div className="flex-1 overflow-hidden relative"> | |
| {renderContent()} | |
| </div> | |
| </div> | |
| <style jsx global>{` | |
| .excel-table, .csv-table { | |
| border-collapse: collapse; | |
| width: 100%; | |
| } | |
| .excel-table td, .excel-table th, | |
| .csv-table td, .csv-table th { | |
| border: 1px solid #ddd; | |
| padding: 8px; | |
| text-align: left; | |
| } | |
| .excel-table th, .csv-table th { | |
| background-color: #f2f2f2; | |
| font-weight: bold; | |
| } | |
| .excel-table tr:nth-child(even), | |
| .csv-table tr:nth-child(even) { | |
| background-color: #f9f9f9; | |
| } | |
| .excel-preview { | |
| font-family: 'Arial', sans-serif; | |
| } | |
| .prose { | |
| max-width: none; | |
| } | |
| .prose h1 { font-size: 2em; font-weight: bold; margin: 1em 0 0.5em; } | |
| .prose h2 { font-size: 1.5em; font-weight: bold; margin: 1em 0 0.5em; } | |
| .prose h3 { font-size: 1.2em; font-weight: bold; margin: 1em 0 0.5em; } | |
| .prose p { margin: 1em 0; } | |
| .prose ul, .prose ol { margin: 1em 0; padding-left: 2em; } | |
| .prose li { margin: 0.5em 0; } | |
| .prose strong { font-weight: bold; } | |
| .prose em { font-style: italic; } | |
| `}</style> | |
| </div> | |
| ) | |
| } | |
| function formatFileSize(bytes: number): string { | |
| const units = ['B', 'KB', 'MB', 'GB'] | |
| let size = bytes | |
| let unitIndex = 0 | |
| while (size >= 1024 && unitIndex < units.length - 1) { | |
| size /= 1024 | |
| unitIndex++ | |
| } | |
| return `${size.toFixed(1)} ${units[unitIndex]}` | |
| } |