'use client' import { useState, useRef } from 'react' import { trpc } from '@/lib/trpc/client' import { toast } from 'sonner' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Skeleton } from '@/components/ui/skeleton' import { Input } from '@/components/ui/input' import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from '@/components/ui/alert-dialog' import { Eye, FileText, Upload, Download, Trash2, MessageSquare, X, Loader2 } from 'lucide-react' import { formatDistanceToNow } from 'date-fns' import { FilePreview, isOfficeFile } from '@/components/shared/file-viewer' interface Props { /** Project the workspace belongs to — drives file list (project-scoped). */ projectId: string /** * One MentorAssignment id on this project — needed only to mint upload tokens * (the token is signed against the assignment + project pair, but the file * itself is project-scoped so co-mentors see it). */ mentorAssignmentId: string /** Set true on the applicant side to label uploads as "Team upload" — purely cosmetic. */ asApplicant?: boolean } function formatSize(bytes: number): string { if (bytes === 0) return '0 B' const k = 1024 const sizes = ['B', 'KB', 'MB', 'GB'] const i = Math.floor(Math.log(bytes) / Math.log(k)) return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i] } export function WorkspaceFilesPanel({ projectId, mentorAssignmentId, asApplicant }: Props) { const utils = trpc.useUtils() const inputRef = useRef(null) const [uploading, setUploading] = useState(false) const [description, setDescription] = useState('') const { data: files, isLoading } = trpc.mentor.workspaceGetFiles.useQuery( { projectId }, { enabled: !!projectId } ) const presign = trpc.mentor.workspaceGetUploadUrl.useMutation() const recordUpload = trpc.mentor.workspaceUploadFile.useMutation({ onSuccess: () => { utils.mentor.workspaceGetFiles.invalidate({ projectId }) setDescription('') toast.success('File uploaded') }, }) const downloadMutation = trpc.mentor.workspaceGetFileDownloadUrl.useMutation() const deleteMutation = trpc.mentor.workspaceDeleteFile.useMutation({ onSuccess: () => { utils.mentor.workspaceGetFiles.invalidate({ projectId }) toast.success('File deleted') }, onError: (e) => toast.error(e.message), }) const handleFileSelected = async (e: React.ChangeEvent) => { const file = e.target.files?.[0] if (!file) return e.target.value = '' setUploading(true) try { const { uploadUrl, uploadToken } = await presign.mutateAsync({ mentorAssignmentId, fileName: file.name, mimeType: file.type || 'application/octet-stream', size: file.size, }) const putRes = await fetch(uploadUrl, { method: 'PUT', body: file, headers: { 'Content-Type': file.type || 'application/octet-stream' }, }) if (!putRes.ok) throw new Error(`Upload failed: HTTP ${putRes.status}`) await recordUpload.mutateAsync({ uploadToken, description: description || undefined }) } catch (err) { toast.error(err instanceof Error ? err.message : 'Upload failed') } finally { setUploading(false) } } const [previewFileId, setPreviewFileId] = useState(null) const [previewUrl, setPreviewUrl] = useState(null) const [previewLoading, setPreviewLoading] = useState(false) const canPreviewMime = (m: string, name: string) => m.startsWith('video/') || m === 'application/pdf' || m.startsWith('image/') || isOfficeFile(m, name) const togglePreview = async (file: { id: string; mimeType: string; fileName: string }) => { if (previewFileId === file.id) { setPreviewFileId(null) setPreviewUrl(null) return } setPreviewFileId(file.id) setPreviewUrl(null) setPreviewLoading(true) try { const { url } = await downloadMutation.mutateAsync({ mentorFileId: file.id, disposition: 'inline' }) setPreviewUrl(url) } catch (err) { toast.error(err instanceof Error ? err.message : 'Preview failed') setPreviewFileId(null) } finally { setPreviewLoading(false) } } const handleDownload = async (mentorFileId: string) => { try { const { url } = await downloadMutation.mutateAsync({ mentorFileId, disposition: 'attachment' }) const a = document.createElement('a') a.href = url a.download = '' a.rel = 'noopener' document.body.appendChild(a) a.click() a.remove() } catch (err) { toast.error(err instanceof Error ? err.message : 'Download failed') } } if (isLoading) { return ( Workspace Files ) } return ( Workspace Files {asApplicant ? 'Files shared with your mentor in this workspace.' : 'Files you and the team have shared in this workspace.'}
setDescription(e.target.value)} placeholder="Optional description for the next upload" className="flex-1" />
{files && files.length === 0 && (
No files in this workspace yet.
)}
    {(files ?? []).map((f) => { const isOpen = previewFileId === f.id const previewable = canPreviewMime(f.mimeType, f.fileName) return (
  • {f.fileName}
    {f.uploadedBy.name ?? f.uploadedBy.email} · {formatSize(f.size)} ·{' '} {formatDistanceToNow(new Date(f.createdAt), { addSuffix: true })} {f._count.comments > 0 && ( {f._count.comments} )}
    {f.description && (
    {f.description}
    )}
    {previewable && ( )} Delete this file? This removes the file from MinIO and the workspace. Comments on the file are deleted with it. Cancel deleteMutation.mutate({ mentorFileId: f.id })}> Delete
    {isOpen && (
    {previewLoading || !previewUrl ? (
    Loading preview…
    ) : ( )}
    )}
  • ) })}
) }