"use client"; import { useState } from "react"; import { Upload, X, FileText, CheckCircle, AlertCircle, Loader2, } from "lucide-react"; import { Button } from "~/components/ui/button"; import { Progress } from "~/components/ui/progress"; import { api } from "~/trpc/react"; import { toast } from "sonner"; import { cn } from "~/lib/utils"; interface ConsentUploadFormProps { studyId: string; participantId: string; consentFormId: string; onSuccess: () => void; onCancel: () => void; } export function ConsentUploadForm({ studyId, participantId, consentFormId, onSuccess, onCancel, }: ConsentUploadFormProps) { const [file, setFile] = useState(null); const [isUploading, setIsUploading] = useState(false); const [uploadProgress, setUploadProgress] = useState(0); // Mutations const getUploadUrlMutation = api.participants.getConsentUploadUrl.useMutation(); const recordConsentMutation = api.participants.recordConsent.useMutation(); const handleFileChange = (e: React.ChangeEvent) => { if (e.target.files && e.target.files[0]) { const selectedFile = e.target.files[0]; // Validate size (10MB) if (selectedFile.size > 10 * 1024 * 1024) { toast.error("File too large", { description: "Maximum file size is 10MB", }); return; } // Validate type const allowedTypes = [ "application/pdf", "image/png", "image/jpeg", "image/jpg", ]; if (!allowedTypes.includes(selectedFile.type)) { toast.error("Invalid file type", { description: "Please upload a PDF, PNG, or JPG file", }); return; } setFile(selectedFile); } }; const handleUpload = async () => { if (!file) return; try { setIsUploading(true); setUploadProgress(0); // 1. Get Presigned URL const { url, key } = await getUploadUrlMutation.mutateAsync({ studyId, participantId, filename: file.name, contentType: file.type, size: file.size, }); // 2. Upload to MinIO using XMLHttpRequest for progress await new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open("PUT", url, true); xhr.setRequestHeader("Content-Type", file.type); xhr.upload.onprogress = (event) => { if (event.lengthComputable) { const percentCompleted = Math.round( (event.loaded * 100) / event.total, ); setUploadProgress(percentCompleted); } }; xhr.onload = () => { if (xhr.status >= 200 && xhr.status < 300) { resolve(); } else { reject(new Error(`Upload failed with status ${xhr.status}`)); } }; xhr.onerror = () => reject(new Error("Network error during upload")); xhr.send(file); }); // 3. Record Consent in DB await recordConsentMutation.mutateAsync({ participantId, consentFormId, storagePath: key, }); toast.success("Consent Recorded", { description: "The consent form has been uploaded and recorded successfully.", }); onSuccess(); } catch (error) { console.error("Upload failed:", error); toast.error("Upload Failed", { description: error instanceof Error ? error.message : "An unexpected error occurred", }); setIsUploading(false); } }; return (
{!file ? (

Upload Signed Consent

Drag and drop or click to select
PDF, PNG, JPG up to 10MB

) : (

{file.name}

{(file.size / 1024 / 1024).toFixed(2)} MB

{!isUploading && ( )}
{isUploading && (
Uploading... {uploadProgress}%
)}
)}
); }