Reuben_OS / app /components /FilePreview.tsx
Reubencf's picture
fix: File preview for secure data and open .dart files in Flutter IDE with content
c52a01c
'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]}`
}