/** Per-user file management with authenticated previews and visible mutation outcomes. */ import { useCallback, useEffect, useRef, useState } from 'react' import { Download, Eye, File as FileIcon, FileImage, FolderOpen, Trash2, TriangleAlert, UploadCloud, } from 'lucide-react' import { useR2Files, type R2FileInfo } from 'deepspace' import { Badge, Button, ConfirmModal, EmptyState, Modal, useToast } from '@/components/ui' function formatBytes(bytes: number): string { if (bytes === 0) return '0 B' const units = ['B', 'KB', 'MB', 'GB'] const unit = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1) return `${parseFloat((bytes / 1024 ** unit).toFixed(1))} ${units[unit]}` } function displayName(file: R2FileInfo): string { return file.originalName ?? file.key.split('/').pop() ?? 'Unnamed' } // SVG is served as a download for origin safety, so it is deliberately not // rendered inline. It remains uploadable and downloadable like every file. function canPreviewImage(name: string): boolean { return /\.(jpg|jpeg|png|gif|webp|bmp|ico)$/i.test(name) } interface DropZoneProps { onFiles: (files: File[]) => void isUploading: boolean } function DropZone({ onFiles, isUploading }: DropZoneProps) { const [isDragging, setIsDragging] = useState(false) const inputRef = useRef(null) const handleDrop = useCallback( (event: React.DragEvent) => { event.preventDefault() setIsDragging(false) const files = Array.from(event.dataTransfer.files) if (files.length > 0) onFiles(files) }, [onFiles], ) return (
{ event.preventDefault() setIsDragging(true) }} onDragLeave={() => setIsDragging(false)} onDrop={handleDrop} className={`rounded-xl border-2 border-dashed p-8 text-center transition-colors ${ isDragging ? 'border-primary bg-primary/5' : 'border-border bg-card/30' }`} > { const files = Array.from(event.target.files ?? []) if (files.length > 0) onFiles(files) event.target.value = '' }} className="hidden" /> {isUploading ? (
) : (
)}
) } interface FileCardProps { file: R2FileInfo onDownload: () => void onDelete: () => void onPreview: () => void } function FileCard({ file, onDownload, onDelete, onPreview }: FileCardProps) { const name = displayName(file) const previewable = canPreviewImage(name) return (
{previewable ? (

{name}

{formatBytes(file.size)} {new Date(file.uploaded).toLocaleDateString()}
{previewable && ( )}
) } function PreviewModal({ file, imageUrl, onClose, }: { file: R2FileInfo | null imageUrl: string | null onClose: () => void }) { return ( {file ? displayName(file) : ''} {imageUrl && file && ( {displayName(file)} )} ) } export default function FileManagerPage({ className }: { className?: string }) { const { upload, downloadFile, readFile, deleteFile, list, isUploading } = useR2Files() const toast = useToast() const [files, setFiles] = useState([]) const [loading, setLoading] = useState(true) const [loadError, setLoadError] = useState(null) const [previewFile, setPreviewFile] = useState(null) const [previewUrl, setPreviewUrl] = useState(null) const [deleteTarget, setDeleteTarget] = useState(null) const [deleting, setDeleting] = useState(false) const previewUrlRef = useRef(null) // `list` throws on failure; rendered as an error state rather than the // empty grid an unreachable or unauthorized account is not. const refreshFiles = useCallback(async () => { try { setFiles(await list()) setLoadError(null) } catch (error) { setLoadError(error instanceof Error ? error.message : 'Please try again.') } finally { setLoading(false) } }, [list]) useEffect(() => { void refreshFiles() }, [refreshFiles]) useEffect( () => () => { if (previewUrlRef.current) URL.revokeObjectURL(previewUrlRef.current) }, [], ) const closePreview = () => { if (previewUrlRef.current) URL.revokeObjectURL(previewUrlRef.current) previewUrlRef.current = null setPreviewUrl(null) setPreviewFile(null) } const handleUpload = async (fileList: File[]) => { let uploaded = 0 for (const file of fileList) { const result = await upload(file) if (!result.success) { toast.error(`Could not upload "${file.name}"`, result.error ?? 'Please try again.') continue } uploaded += 1 } if (uploaded > 0) { toast.success( uploaded === 1 ? 'File uploaded' : `${uploaded} files uploaded`, 'Your file list is up to date.', ) await refreshFiles() } } const handleDownload = async (file: R2FileInfo) => { const result = await downloadFile(file) if (!result.success) { toast.error(`Could not download "${displayName(file)}"`, result.error ?? 'Please try again.') } } const handleDelete = async () => { if (!deleteTarget || deleting) return setDeleting(true) try { const result = await deleteFile(deleteTarget) if (!result.success) { toast.error( `Could not delete "${displayName(deleteTarget)}"`, result.error ?? 'Please try again.', ) return } toast.success('File deleted', `"${displayName(deleteTarget)}" was deleted.`) setDeleteTarget(null) await refreshFiles() } finally { setDeleting(false) } } const handlePreview = async (file: R2FileInfo) => { try { const response = await readFile(file) const url = URL.createObjectURL(await response.blob()) if (previewUrlRef.current) URL.revokeObjectURL(previewUrlRef.current) previewUrlRef.current = url setPreviewUrl(url) setPreviewFile(file) } catch (error) { toast.error( `Could not preview "${displayName(file)}"`, error instanceof Error ? error.message : 'Please try again.', ) } } return (

Files

{files.length} {files.length === 1 ? 'file' : 'files'} stored

{files.length > 0 && ( {formatBytes(files.reduce((sum, file) => sum + file.size, 0))} used )}
void handleUpload(nextFiles)} isUploading={isUploading} /> {loading ? (
) : loadError ? (
{ if (!deleting) setDeleteTarget(null) }} onConfirm={() => void handleDelete()} title={deleteTarget ? `Delete "${displayName(deleteTarget)}"?` : 'Delete file?'} description="This cannot be undone." confirmText="Delete" loading={deleting} />
) }