mirror of
https://github.com/hanzoai/dataroom.git
synced 2026-10-30 18:41:07 +00:00
200 lines
6.2 KiB
TypeScript
200 lines
6.2 KiB
TypeScript
import { useCallback } from "react";
|
|
|
|
import { DocumentStorageType } from "@prisma/client";
|
|
import { FileRejection, useDropzone } from "react-dropzone";
|
|
import { toast } from "sonner";
|
|
|
|
import { VIEWER_ACCEPTED_FILE_TYPES } from "@/lib/constants";
|
|
import { DocumentData } from "@/lib/documents/create-document";
|
|
import { viewerUpload } from "@/lib/files/viewer-tus-upload";
|
|
import { newId } from "@/lib/id-helper";
|
|
import { cn } from "@/lib/utils";
|
|
import { getSupportedContentType } from "@/lib/utils/get-content-type";
|
|
import { getPagesCount } from "@/lib/utils/get-page-number-count";
|
|
|
|
// File types allowed for viewer uploads
|
|
const acceptableViewerFileTypes = VIEWER_ACCEPTED_FILE_TYPES;
|
|
|
|
export default function ViewerUploadZone({
|
|
children,
|
|
onUploadStart,
|
|
onUploadProgress,
|
|
onUploadComplete,
|
|
onUploadRejected,
|
|
viewerData,
|
|
teamId,
|
|
maxFileSize = 30, // 30 MB default
|
|
disabled = false,
|
|
}: {
|
|
children: React.ReactNode;
|
|
onUploadStart: (
|
|
uploads: { uploadId: string; fileName: string; progress: number }[],
|
|
) => void;
|
|
onUploadProgress: (uploadId: string, progress: number) => void;
|
|
onUploadComplete: (documentData: DocumentData, uploadId: string) => void;
|
|
onUploadRejected: (rejected: { fileName: string; message: string }[]) => void;
|
|
viewerData: {
|
|
id: string;
|
|
linkId: string;
|
|
dataroomId?: string;
|
|
};
|
|
teamId: string;
|
|
maxFileSize?: number;
|
|
disabled?: boolean;
|
|
}) {
|
|
const onDropRejected = useCallback(
|
|
(rejectedFiles: FileRejection[]) => {
|
|
const rejected = rejectedFiles.map(({ file, errors }) => {
|
|
let message = "";
|
|
if (errors.find(({ code }) => code === "file-too-large")) {
|
|
message = `File size too big (max. ${maxFileSize} MB).`;
|
|
} else if (errors.find(({ code }) => code === "file-invalid-type")) {
|
|
message = `File type not supported. Please upload PDF, Word, or Excel files.`;
|
|
}
|
|
return { fileName: file.name, message };
|
|
});
|
|
onUploadRejected(rejected);
|
|
},
|
|
[onUploadRejected, maxFileSize],
|
|
);
|
|
|
|
const onDrop = useCallback(
|
|
async (acceptedFiles: File[]) => {
|
|
const trackedFiles = acceptedFiles.map((file) => ({
|
|
uploadId: newId("pending"),
|
|
file,
|
|
}));
|
|
|
|
const newUploads = trackedFiles.map(({ uploadId, file }) => ({
|
|
uploadId,
|
|
fileName: file.name,
|
|
progress: 0,
|
|
}));
|
|
|
|
onUploadStart(newUploads);
|
|
|
|
const uploadPromises = trackedFiles.map(async ({ uploadId, file }) => {
|
|
// count the number of pages in the file
|
|
let numPages = 1;
|
|
if (file.type === "application/pdf") {
|
|
const buffer = await file.arrayBuffer();
|
|
numPages = await getPagesCount(buffer);
|
|
}
|
|
|
|
const { complete } = await viewerUpload({
|
|
file,
|
|
onProgress: (bytesUploaded, bytesTotal) => {
|
|
const progress = Math.min(
|
|
Math.round((bytesUploaded / bytesTotal) * 100),
|
|
99,
|
|
);
|
|
onUploadProgress(uploadId, progress);
|
|
},
|
|
onError: (error) => {
|
|
console.error("Upload error:", error);
|
|
toast.error(`Failed to upload ${file.name}`);
|
|
},
|
|
viewerData,
|
|
teamId,
|
|
numPages,
|
|
});
|
|
|
|
const uploadResult = await complete;
|
|
|
|
let contentType = uploadResult.fileType;
|
|
let supportedFileType = getSupportedContentType(contentType) ?? "";
|
|
|
|
if (
|
|
uploadResult.fileName.endsWith(".dwg") ||
|
|
uploadResult.fileName.endsWith(".dxf")
|
|
) {
|
|
supportedFileType = "cad";
|
|
contentType = `image/vnd.${uploadResult.fileName.split(".").pop()}`;
|
|
}
|
|
|
|
if (uploadResult.fileName.endsWith(".xlsm")) {
|
|
supportedFileType = "sheet";
|
|
contentType = "application/vnd.ms-excel.sheet.macroEnabled.12";
|
|
}
|
|
|
|
if (
|
|
uploadResult.fileName.endsWith(".kml") ||
|
|
uploadResult.fileName.endsWith(".kmz")
|
|
) {
|
|
supportedFileType = "map";
|
|
contentType = `application/vnd.google-earth.${uploadResult.fileName.endsWith(".kml") ? "kml+xml" : "kmz"}`;
|
|
}
|
|
|
|
const documentData: DocumentData = {
|
|
key: uploadResult.id,
|
|
supportedFileType: supportedFileType,
|
|
name: file.name,
|
|
storageType: DocumentStorageType.S3_PATH,
|
|
contentType: contentType,
|
|
fileSize: file.size,
|
|
numPages: numPages,
|
|
};
|
|
|
|
onUploadComplete(documentData, uploadId);
|
|
|
|
onUploadProgress(uploadId, 100); // Mark upload as complete
|
|
|
|
return uploadResult;
|
|
});
|
|
|
|
try {
|
|
await Promise.all(uploadPromises);
|
|
toast.success("File upload complete!");
|
|
} catch (error) {
|
|
console.error("Upload error:", error);
|
|
toast.error("An error occurred during upload");
|
|
}
|
|
},
|
|
[onUploadStart, onUploadProgress, onUploadComplete, viewerData, teamId],
|
|
);
|
|
|
|
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
|
accept: acceptableViewerFileTypes,
|
|
multiple: true,
|
|
maxSize: maxFileSize * 1024 * 1024,
|
|
onDrop,
|
|
onDropRejected,
|
|
disabled,
|
|
});
|
|
|
|
return (
|
|
<div {...getRootProps()} className="relative min-h-[200px]">
|
|
<div
|
|
className={cn(
|
|
"absolute inset-0 z-40 -m-1 rounded-lg border-2 border-dashed",
|
|
isDragActive
|
|
? "pointer-events-auto border-primary/50 bg-gray-100/75 backdrop-blur-sm dark:bg-gray-800/75"
|
|
: "pointer-events-none border-none",
|
|
)}
|
|
>
|
|
<input
|
|
{...getInputProps()}
|
|
name="file"
|
|
id="viewer-upload-files-zone"
|
|
className="sr-only"
|
|
/>
|
|
|
|
{isDragActive && (
|
|
<div className="flex h-full items-center justify-center">
|
|
<div className="inline-flex flex-col rounded-lg bg-background/95 px-6 py-4 text-center ring-1 ring-gray-900/5 dark:bg-gray-900/95 dark:ring-white/10">
|
|
<span className="font-medium text-foreground">
|
|
Drop your file(s) here
|
|
</span>
|
|
<p className="mt-1 text-xs leading-5 text-muted-foreground">
|
|
Only *.pdf, *.xls, *.xlsx, *.csv, *.tsv, *.ods files
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|