'use client' import { useState } from 'react' import { trpc } from '@/lib/trpc/client' import { Button } from '@/components/ui/button' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' import { Skeleton } from '@/components/ui/skeleton' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, } from '@/components/ui/dialog' import { FileText, Video, File, Download, ExternalLink, Play, FileImage, Loader2, AlertCircle, X, History, PackageOpen, } from 'lucide-react' import { cn } from '@/lib/utils' import { toast } from 'sonner' const OFFICE_MIME_TYPES = [ 'application/vnd.openxmlformats-officedocument.presentationml.presentation', // .pptx 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // .docx 'application/vnd.ms-powerpoint', // .ppt 'application/msword', // .doc ] const OFFICE_EXTENSIONS = ['.pptx', '.ppt', '.docx', '.doc'] function isOfficeFile(mimeType: string, fileName: string): boolean { if (OFFICE_MIME_TYPES.includes(mimeType)) return true const ext = fileName.toLowerCase().slice(fileName.lastIndexOf('.')) return OFFICE_EXTENSIONS.includes(ext) } interface ProjectFile { id: string fileType: 'EXEC_SUMMARY' | 'PRESENTATION' | 'VIDEO' | 'OTHER' | 'BUSINESS_PLAN' | 'VIDEO_PITCH' | 'SUPPORTING_DOC' fileName: string mimeType: string size: number bucket: string objectKey: string version?: number isLate?: boolean } interface RoundGroup { roundId: string | null roundName: string sortOrder: number files: Array } interface FileViewerProps { files?: ProjectFile[] groupedFiles?: RoundGroup[] projectId?: string className?: string } function formatFileSize(bytes: number): string { if (bytes === 0) return '0 Bytes' const k = 1024 const sizes = ['Bytes', 'KB', 'MB', 'GB'] const i = Math.floor(Math.log(bytes) / Math.log(k)) return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] } function getFileIcon(fileType: string, mimeType: string) { if (mimeType.startsWith('video/')) return Video if (mimeType.startsWith('image/')) return FileImage if (mimeType === 'application/pdf') return FileText if (fileType === 'EXEC_SUMMARY' || fileType === 'PRESENTATION') return FileText if (fileType === 'VIDEO') return Video return File } function getFileTypeLabel(fileType: string) { switch (fileType) { case 'EXEC_SUMMARY': return 'Executive Summary' case 'PRESENTATION': return 'Presentation' case 'VIDEO': return 'Video' case 'BUSINESS_PLAN': return 'Business Plan' case 'VIDEO_PITCH': return 'Video Pitch' case 'SUPPORTING_DOC': return 'Supporting Document' default: return 'Attachment' } } export function FileViewer({ files, groupedFiles, projectId, className }: FileViewerProps) { // Render grouped view if groupedFiles is provided if (groupedFiles) { return } // Render flat view (backward compatible) if (!files || files.length === 0) { return (

No files attached

This project has no files uploaded yet

) } // Sort files by type order const sortOrder = ['EXEC_SUMMARY', 'BUSINESS_PLAN', 'PRESENTATION', 'VIDEO', 'VIDEO_PITCH', 'SUPPORTING_DOC', 'OTHER'] const sortedFiles = [...files].sort( (a, b) => sortOrder.indexOf(a.fileType) - sortOrder.indexOf(b.fileType) ) return ( Project Files {projectId && files.length > 1 && ( f.id)} /> )} {sortedFiles.map((file) => ( ))} ) } function GroupedFileViewer({ groupedFiles, className }: { groupedFiles: RoundGroup[], className?: string }) { const hasAnyFiles = groupedFiles.some(group => group.files.length > 0) if (!hasAnyFiles) { return (

No files attached

This project has no files uploaded yet

) } // Sort groups by sortOrder const sortedGroups = [...groupedFiles].sort((a, b) => a.sortOrder - b.sortOrder) // Sort files within each group by type order const fileTypeSortOrder = ['EXEC_SUMMARY', 'BUSINESS_PLAN', 'PRESENTATION', 'VIDEO', 'VIDEO_PITCH', 'SUPPORTING_DOC', 'OTHER'] return ( Project Files {sortedGroups.map((group) => { if (group.files.length === 0) return null const sortedFiles = [...group.files].sort( (a, b) => fileTypeSortOrder.indexOf(a.fileType) - fileTypeSortOrder.indexOf(b.fileType) ) return (
{/* Round header */}

{group.roundName}

{group.files.length} {group.files.length === 1 ? 'file' : 'files'}
{/* Files in this round */}
{sortedFiles.map((file) => ( ))}
) })}
) } function FileItem({ file }: { file: ProjectFile }) { const [showPreview, setShowPreview] = useState(false) const Icon = getFileIcon(file.fileType, file.mimeType) const { data: urlData, isLoading: isLoadingUrl } = trpc.file.getDownloadUrl.useQuery( { bucket: file.bucket, objectKey: file.objectKey }, { enabled: showPreview } ) const canPreview = file.mimeType.startsWith('video/') || file.mimeType === 'application/pdf' || file.mimeType.startsWith('image/') || isOfficeFile(file.mimeType, file.fileName) return (

{file.fileName}

{file.version != null && file.version > 1 && ( v{file.version} )}
{getFileTypeLabel(file.fileType)} {file.isLate && ( Late )} {formatFileSize(file.size)}
{file.version != null && file.version > 1 && ( )} {canPreview && ( )}
{/* Preview area */} {showPreview && (
{isLoadingUrl ? (
) : urlData?.url ? ( ) : (
Failed to load preview
)}
)}
) } function VersionHistoryButton({ fileId }: { fileId: string }) { const [open, setOpen] = useState(false) const { data: versions, isLoading } = trpc.file.getVersionHistory.useQuery( { fileId }, { enabled: open } ) return ( Version History
{isLoading ? (
{[1, 2, 3].map((i) => ( ))}
) : versions && (versions as Array>).length > 0 ? ( (versions as Array>).map((v) => (

{String(v.fileName)}

v{String(v.version)}
{formatFileSize(Number(v.size))} {v.createdAt ? new Date(String(v.createdAt)).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', }) : ''}
)) ) : (

No version history available

)}
) } function VersionDownloadButton({ bucket, objectKey }: { bucket: string; objectKey: string }) { const [downloading, setDownloading] = useState(false) const { refetch } = trpc.file.getDownloadUrl.useQuery( { bucket, objectKey }, { enabled: false } ) const handleDownload = async () => { setDownloading(true) try { const result = await refetch() if (result.data?.url) { window.open(result.data.url, '_blank') } } catch { toast.error('Failed to get download URL') } finally { setDownloading(false) } } return ( ) } function BulkDownloadButton({ projectId, fileIds }: { projectId: string; fileIds: string[] }) { const [downloading, setDownloading] = useState(false) const { refetch } = trpc.file.getBulkDownloadUrls.useQuery( { projectId, fileIds }, { enabled: false } ) const handleBulkDownload = async () => { setDownloading(true) try { const result = await refetch() if (result.data && Array.isArray(result.data)) { // Open each download URL with a small delay to avoid popup blocking for (let i = 0; i < result.data.length; i++) { const item = result.data[i] as { downloadUrl: string } if (item.downloadUrl) { // Use link element to trigger download without popup const link = document.createElement('a') link.href = item.downloadUrl link.target = '_blank' link.rel = 'noopener noreferrer' document.body.appendChild(link) link.click() document.body.removeChild(link) // Small delay between downloads if (i < result.data.length - 1) { await new Promise((resolve) => setTimeout(resolve, 300)) } } } toast.success(`Downloading ${result.data.length} files`) } } catch { toast.error('Failed to download files') } finally { setDownloading(false) } } return ( ) } function FileOpenButton({ file }: { file: ProjectFile }) { const [loading, setLoading] = useState(false) const { refetch } = trpc.file.getDownloadUrl.useQuery( { bucket: file.bucket, objectKey: file.objectKey }, { enabled: false } ) const handleOpen = async () => { setLoading(true) try { const result = await refetch() if (result.data?.url) { window.open(result.data.url, '_blank') } } catch (error) { console.error('Failed to get URL:', error) } finally { setLoading(false) } } return ( ) } function FileDownloadButton({ file }: { file: ProjectFile }) { const [downloading, setDownloading] = useState(false) const { refetch } = trpc.file.getDownloadUrl.useQuery( { bucket: file.bucket, objectKey: file.objectKey }, { enabled: false } ) const handleDownload = async () => { setDownloading(true) try { const result = await refetch() if (result.data?.url) { // Force browser download via const link = document.createElement('a') link.href = result.data.url link.download = file.fileName link.rel = 'noopener noreferrer' document.body.appendChild(link) link.click() document.body.removeChild(link) } } catch (error) { console.error('Failed to get download URL:', error) } finally { setDownloading(false) } } return ( ) } function FilePreview({ file, url }: { file: ProjectFile; url: string }) { if (file.mimeType.startsWith('video/')) { return ( ) } if (file.mimeType === 'application/pdf') { return (