Files
dataroom/components/viewer-upload-component.tsx
2026-02-28 08:23:12 +11:00

249 lines
7.9 KiB
TypeScript

import { useRef, useState } from "react";
import { FileUp } from "lucide-react";
import { toast } from "sonner";
import { usePendingUploads } from "@/context/pending-uploads-context";
import { DocumentData } from "@/lib/documents/create-document";
import { newId } from "@/lib/id-helper";
import { cn } from "@/lib/utils";
import { Progress } from "@/components/ui/progress";
import ViewerUploadZone from "@/components/viewer-upload-zone";
export function ViewerUploadComponent({
viewerData,
teamId,
folderId,
onUploadSuccess,
}: {
viewerData: { id: string; linkId: string; dataroomId?: string };
teamId: string;
folderId?: string;
onUploadSuccess?: () => void;
}) {
const [uploads, setUploads] = useState<
{ uploadId: string; fileName: string; progress: number }[]
>([]);
const [rejectedFiles, setRejectedFiles] = useState<
{ fileName: string; message: string }[]
>([]);
const { addPendingUpload, updatePendingUpload } = usePendingUploads();
// Map each active upload item to its pending upload record
const pendingUploadIds = useRef<Map<string, string>>(new Map());
const activeUploadIds = useRef<Set<string>>(new Set());
const failedCountRef = useRef(0);
const finalizeSessionIfIdle = () => {
if (activeUploadIds.current.size > 0) return;
const hasFailures = failedCountRef.current > 0;
failedCountRef.current = 0;
if (!hasFailures) {
onUploadSuccess?.();
}
};
const handleUploadStart = (
newUploads: { uploadId: string; fileName: string; progress: number }[],
) => {
const isStartingFreshSession = activeUploadIds.current.size === 0;
if (isStartingFreshSession) {
failedCountRef.current = 0;
setRejectedFiles([]);
}
setUploads((prev) => [...prev, ...newUploads]);
newUploads.forEach((upload) => {
activeUploadIds.current.add(upload.uploadId);
const pendingId = newId("pending");
pendingUploadIds.current.set(upload.uploadId, pendingId);
addPendingUpload({
id: pendingId,
name: upload.fileName,
folderId: folderId ?? null,
uploadedAt: new Date(),
status: "uploading",
progress: 0,
});
});
};
const handleUploadProgress = (uploadId: string, progress: number) => {
setUploads((prev) => {
const updated = prev.map((upload) =>
upload.uploadId === uploadId ? { ...upload, progress } : upload,
);
const pendingId = pendingUploadIds.current.get(uploadId);
if (pendingId) {
updatePendingUpload(pendingId, { progress });
}
return updated;
});
};
const settleUpload = (uploadId: string, failed: boolean) => {
if (failed) {
failedCountRef.current += 1;
}
activeUploadIds.current.delete(uploadId);
pendingUploadIds.current.delete(uploadId);
setUploads((prev) => prev.filter((upload) => upload.uploadId !== uploadId));
finalizeSessionIfIdle();
};
const handleUploadComplete = async (
documentData: DocumentData,
uploadId: string,
) => {
const pendingId = pendingUploadIds.current.get(uploadId);
// Update status to processing (file uploaded to S3, now being processed by backend)
if (pendingId) {
updatePendingUpload(pendingId, {
status: "processing",
progress: 100,
});
}
// Call the API to add the document to the dataroom
try {
const response = await fetch(`/api/links/${viewerData.linkId}/upload`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
documentData,
dataroomId: viewerData.dataroomId,
folderId: folderId,
}),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || "Failed to process upload");
}
const result = await response.json();
if (!result.document) {
throw new Error("Upload response missing document metadata");
}
// Determine if the file needs trigger processing (PDF, docs, slides)
// Images and Excel/CSV files are ready immediately after upload
const FILE_TYPES_NEEDING_PROCESSING = ["pdf", "docs", "slides"];
const needsProcessing = FILE_TYPES_NEEDING_PROCESSING.includes(
result.document.fileType,
);
// Update pending upload with real document data
if (pendingId) {
updatePendingUpload(pendingId, {
status: needsProcessing ? "processing" : "complete",
documentId: result.document.id,
dataroomDocumentId: result.document.dataroomDocumentId,
documentVersionId: result.document.documentVersionId,
fileType: result.document.fileType,
});
}
settleUpload(uploadId, false);
} catch (error) {
console.error("Error processing upload:", error);
toast.error((error as Error).message || "Failed to upload document");
if (pendingId) {
updatePendingUpload(pendingId, {
status: "error",
errorMessage: (error as Error).message || "Failed to upload document",
});
}
settleUpload(uploadId, true);
}
};
const isUploading = uploads.length > 0;
return (
<ViewerUploadZone
onUploadStart={handleUploadStart}
onUploadProgress={handleUploadProgress}
onUploadComplete={handleUploadComplete}
onUploadRejected={(rejected) => setRejectedFiles(rejected)}
viewerData={viewerData}
teamId={teamId}
>
{isUploading ? (
<div className="space-y-3">
{uploads.map((upload) => (
<div
key={upload.uploadId}
className="flex items-center gap-3 rounded-lg border border-gray-200 bg-gray-50 p-3 dark:border-gray-700 dark:bg-gray-800"
>
<FileUp className="h-5 w-5 shrink-0 text-muted-foreground" />
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium text-foreground">
{upload.fileName}
</p>
<div className="mt-1.5 flex items-center gap-2">
<Progress
value={upload.progress}
className="h-1.5 flex-1"
/>
<span className="shrink-0 text-xs tabular-nums text-muted-foreground">
{upload.progress}%
</span>
</div>
</div>
</div>
))}
</div>
) : (
<div
className={cn(
"flex flex-col items-center justify-center rounded-xl border-2 border-dashed px-6 py-10 text-center transition-colors",
"border-gray-300 hover:border-gray-400 dark:border-gray-600 dark:hover:border-gray-500",
)}
>
<div className="mb-3 flex h-10 w-10 items-center justify-center rounded-full bg-gray-100 dark:bg-gray-800">
<FileUp className="h-5 w-5 text-muted-foreground" />
</div>
<p className="text-sm font-medium text-foreground">
Drag & drop files here, or click to select files
</p>
<p className="mt-1 text-xs text-muted-foreground">
Supported file types: PDF, Excel, CSV, Images
</p>
</div>
)}
{/* Display rejected files */}
{rejectedFiles.length > 0 && (
<div className="mt-3 rounded-lg border border-red-200 bg-red-50 p-3 dark:border-red-800 dark:bg-red-950/30">
<p className="text-xs font-medium text-red-600 dark:text-red-400">
Some files were rejected:
</p>
<ul className="mt-1.5 space-y-0.5">
{rejectedFiles.map((file, index) => (
<li
key={index}
className="text-xs text-red-500 dark:text-red-400"
>
{file.fileName}: {file.message}
</li>
))}
</ul>
</div>
)}
</ViewerUploadZone>
);
}