'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 { FileText, Upload, Download, Trash2, MessageSquare } from 'lucide-react' import { formatDistanceToNow } from 'date-fns' interface Props { 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({ 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( { mentorAssignmentId }, { enabled: !!mentorAssignmentId } ) const presign = trpc.mentor.workspaceGetUploadUrl.useMutation() const recordUpload = trpc.mentor.workspaceUploadFile.useMutation({ onSuccess: () => { utils.mentor.workspaceGetFiles.invalidate({ mentorAssignmentId }) setDescription('') toast.success('File uploaded') }, }) const downloadMutation = trpc.mentor.workspaceGetFileDownloadUrl.useMutation() const deleteMutation = trpc.mentor.workspaceDeleteFile.useMutation({ onSuccess: () => { utils.mentor.workspaceGetFiles.invalidate({ mentorAssignmentId }) 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 handleDownload = async (mentorFileId: string) => { try { const { url } = await downloadMutation.mutateAsync({ mentorFileId }) window.open(url, '_blank') } 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) => (
  • {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}
    )}
    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
  • ))}
) }