diff --git a/src/app/(applicant)/applicant/documents/page.tsx b/src/app/(applicant)/applicant/documents/page.tsx index eed3343..f906723 100644 --- a/src/app/(applicant)/applicant/documents/page.tsx +++ b/src/app/(applicant)/applicant/documents/page.tsx @@ -1,5 +1,6 @@ 'use client' +import { useState } from 'react' import { useSession } from 'next-auth/react' import { trpc } from '@/lib/trpc/client' import { Badge } from '@/components/ui/badge' @@ -13,6 +14,7 @@ import { } from '@/components/ui/card' import { Skeleton } from '@/components/ui/skeleton' import { RequirementUploadList } from '@/components/shared/requirement-upload-slot' +import { FilePreview, isOfficeFile } from '@/components/shared/file-viewer' import { FileText, Upload, @@ -22,7 +24,10 @@ import { File, Download, Eye, + X, + Loader2, } from 'lucide-react' +import { toast } from 'sonner' const fileTypeIcons: Record = { EXEC_SUMMARY: FileText, @@ -44,34 +49,114 @@ const fileTypeLabels: Record = { SUPPORTING_DOC: 'Supporting Document', } -function FileActionButtons({ bucket, objectKey, fileName }: { bucket: string; objectKey: string; fileName: string }) { - const { data: viewData } = trpc.file.getDownloadUrl.useQuery( - { bucket, objectKey, forDownload: false }, - { staleTime: 10 * 60 * 1000 } +function FileRow({ file }: { file: { id: string; fileName: string; fileType: string; createdAt: string | Date; isLate?: boolean; bucket?: string; objectKey?: string; mimeType?: string } }) { + const [showPreview, setShowPreview] = useState(false) + const Icon = fileTypeIcons[file.fileType] || File + const mimeType = file.mimeType || '' + + const canPreview = + mimeType.startsWith('video/') || + mimeType === 'application/pdf' || + mimeType.startsWith('image/') || + isOfficeFile(mimeType, file.fileName) + + const { data: previewData, isLoading: isLoadingPreview } = trpc.file.getDownloadUrl.useQuery( + { bucket: file.bucket!, objectKey: file.objectKey!, purpose: 'preview' as const }, + { enabled: showPreview && !!file.bucket && !!file.objectKey, staleTime: 10 * 60 * 1000 } ) - const { data: dlData } = trpc.file.getDownloadUrl.useQuery( - { bucket, objectKey, forDownload: true, fileName }, - { staleTime: 10 * 60 * 1000 } - ) - const viewUrl = typeof viewData === 'string' ? viewData : viewData?.url - const dlUrl = typeof dlData === 'string' ? dlData : dlData?.url return ( -
- - +
+
+
+ +
+
+

{file.fileName}

+ {file.isLate && ( + + + Late + + )} +
+

+ {fileTypeLabels[file.fileType] || file.fileType} + {' - '} + {new Date(file.createdAt).toLocaleDateString()} +

+
+
+ {file.bucket && file.objectKey && ( +
+ {canPreview && ( + + )} + +
+ )} +
+ + {showPreview && ( +
+ {isLoadingPreview ? ( +
+ +
+ ) : previewData?.url ? ( + + ) : ( +
+ Failed to load preview +
+ )} +
+ )}
) } +function DownloadButton({ bucket, objectKey, fileName }: { bucket: string; objectKey: string; fileName: string }) { + const [downloading, setDownloading] = useState(false) + + const { refetch } = trpc.file.getDownloadUrl.useQuery( + { bucket, objectKey, forDownload: true, fileName, purpose: 'download' as const }, + { enabled: false } + ) + + const handleDownload = async () => { + setDownloading(true) + try { + const result = await refetch() + if (result.data?.url) { + window.location.href = result.data.url + } + } catch { + toast.error('Failed to download file') + } finally { + setTimeout(() => setDownloading(false), 1000) + } + } + + return ( + + ) +} + export default function ApplicantDocumentsPage() { const { status: sessionStatus } = useSession() const isAuthenticated = sessionStatus === 'authenticated' @@ -113,7 +198,6 @@ export default function ApplicantDocumentsPage() { } const { project, openRounds, isRejected } = data - const isDraft = !project.submittedAt return (
@@ -204,41 +288,9 @@ export default function ApplicantDocumentsPage() { ) : (
{project.files.map((file) => { - const Icon = fileTypeIcons[file.fileType] || File - const fileRecord = file as typeof file & { isLate?: boolean; roundId?: string | null; bucket?: string; objectKey?: string } - + const fileRecord = file as typeof file & { isLate?: boolean; bucket?: string; objectKey?: string; mimeType?: string } return ( -
-
- -
-
-

{file.fileName}

- {fileRecord.isLate && ( - - - Late - - )} -
-

- {fileTypeLabels[file.fileType] || file.fileType} - {' - '} - {new Date(file.createdAt).toLocaleDateString()} -

-
-
- {fileRecord.bucket && fileRecord.objectKey && ( - - )} -
+ ) })}