Untitled
unknown
plain_text
9 months ago
9.4 kB
6
Indexable
import { Button } from "@/components/ui/button";
import { cn } from "@/utilities/utils";
import { MAX_IMAGE_SIZE } from "@/validations/media-validation";
import { Link, Upload, X } from "lucide-react";
import { memo, useCallback, useEffect, useState } from "react";
import { FileError, FileRejection, useDropzone } from "react-dropzone";
type Props = {
onChange: (...event: unknown[]) => void;
maxSize?: number;
description?: string;
children?: React.ReactNode;
multiple?: boolean;
inClerkComponents?: boolean;
};
function DropZone({
onChange,
maxSize = MAX_IMAGE_SIZE,
description,
children,
multiple = true,
inClerkComponents,
}: Props) {
const [files, setFiles] = useState<File[]>([]);
const [rejectedFiles, setRejectedFiles] = useState<FileRejection[]>([]);
const onDrop = useCallback(
(acceptedFiles: File[], rejectedFiles: FileRejection[]) => {
if (multiple) {
// Multiple file mode
if (acceptedFiles.length > 0) {
const newFiles = files.concat(acceptedFiles);
setFiles(newFiles);
onChange(newFiles);
}
// Handle rejected files in multiple mode
if (rejectedFiles.length > 0) {
setRejectedFiles((prevRejected) => [
...prevRejected,
...rejectedFiles,
]);
}
} else {
// Single file mode
const tooManyFilesError = rejectedFiles.find((file) =>
file.errors.some((error) => error.code === "too-many-files"),
);
if (tooManyFilesError) {
// Accept the first file with "too-many-files" error
setFiles([tooManyFilesError.file]);
onChange(tooManyFilesError.file);
// Reject the rest, including other types of rejected files
const otherRejectedFiles = rejectedFiles.filter(
(file) => file !== tooManyFilesError,
);
setRejectedFiles((prevRejected) => [
...prevRejected,
...otherRejectedFiles,
]);
} else if (acceptedFiles.length > 0) {
// If no "too-many-files" error, accept the first file
setFiles([acceptedFiles[0]]);
onChange(acceptedFiles[0]);
// Handle any other rejected files
if (rejectedFiles.length > 0) {
setRejectedFiles((prevRejected) => [
...prevRejected,
...rejectedFiles,
]);
}
} else {
// If no accepted files and no "too-many-files" error, just add all rejected files
setRejectedFiles((prevRejected) => [
...prevRejected,
...rejectedFiles,
]);
}
}
},
[files, onChange, multiple],
);
const removeFile = useCallback(
(name: string) => {
const newFiles = files.filter((file) => file.name !== name);
setFiles(newFiles);
onChange(multiple ? newFiles : newFiles[0]);
},
[files, onChange, multiple],
);
const removeRejectedFile = useCallback((name: string) => {
setRejectedFiles((prevFiles) =>
prevFiles.filter(({ file }) => file.name !== name),
);
}, []);
const { getRootProps, getInputProps, isDragActive, isDragReject } =
useDropzone({
accept: {
"application/pdf": [],
"application/msword": [],
"application/vnd.openxmlformats-officedocument.wordprocessingml.document":
[],
},
maxSize,
onDrop,
multiple,
});
useEffect(() => {
if (!multiple && files.length === 0 && rejectedFiles.length > 0) {
const tooManyFilesError = rejectedFiles.find((file) =>
file.errors.some((error) => error.code === "too-many-files"),
);
if (tooManyFilesError) {
setFiles([tooManyFilesError.file]);
onChange(tooManyFilesError.file);
setRejectedFiles((prevRejected) =>
prevRejected.filter((file) => file !== tooManyFilesError),
);
}
}
}, [files, rejectedFiles, multiple, onChange]);
const getErrorMessage = (file: File, errors: FileError[]) => {
if (errors.some((e) => e.code === "file-invalid-type")) {
return `This is not an acceptable document type. Please upload PDF, DOC, or DOCX documents only.`;
}
if (errors.some((e) => e.code === "file-too-large")) {
return `Document exceeds the maximum file size of ${maxSize / (1024 * 1024)}MB.`;
}
if (errors.some((e) => e.code === "too-many-files")) {
return `Only one document is allowed. This document was not uploaded.`;
}
return `Could not be uploaded. Please try again.`;
};
return (
<div className="rounded-md">
<Button
type="button"
variant={"outline"}
{...getRootProps({
className: cn(
"flex w-full items-center justify-center p-10 h-[150px] border-2 border-dashed border-muted-foreground/30 dark:hover:bg-muted/30 hover:bg-muted/70 rounded-md cursor-pointer font-normal",
`${inClerkComponents && "dark:bg-transparent"}`,
),
})}
>
<div className="flex flex-col justify-center gap-y-5">
<input {...getInputProps()} />
<div className="mx-auto grid place-items-center rounded-full border border-dashed border-muted-foreground/30 p-3">
<Upload className="size-5 text-muted-foreground sm:size-6" />
</div>
<p className="text-balance text-xs text-muted-foreground sm:text-sm">
{isDragActive && !isDragReject
? `Drop the file${multiple ? "s" : ""} here`
: isDragReject
? "Only PDF, DOC, DOCX files are allowed"
: `Drag and drop ${multiple ? "files" : "a file"} here or click to select ${multiple ? "files" : "a file"}`}
</p>
</div>
</Button>
<p className="mt-2 text-[0.8rem] text-muted-foreground">{description}</p>
<div className={`${files.length > 0 && "mt-4"}`}>
{files.length > 0 && (
<p className="mb-1 text-sm font-medium">
Accepted Document{multiple ? "s" : ""}
</p>
)}
<div className="@container flex flex-col gap-y-1">
{files.map((file: any, index) => (
<div
key={`${file.name}-${index}`}
className="5m:text-[13px] flex items-center justify-between gap-x-4 text-xs text-muted-foreground"
>
<span className="@sm:max-w-[330px] @md:max-w-[380px] @lg:max-w-[520px] max-w-[250px] truncate leading-none">
{file.path} - {""}
{file.size < 1e6
? (file.size / 1e3).toFixed(2) + " KB"
: (file.size / 1e6).toFixed(2) + " MB"}
</span>
<Button
onClick={() => removeFile(file.name)}
type="button"
variant={"outline"}
className={cn(
"grid size-[19px] flex-shrink-0 place-items-center p-0",
`${inClerkComponents && "dark:bg-transparent"}`,
)}
>
<X size={13} />
</Button>
</div>
))}
</div>
</div>
{children}
<div className={`${rejectedFiles.length > 0 && "mt-4"}`}>
{rejectedFiles.length > 0 && (
<p className="mb-[7px] text-sm font-medium">Rejected Documents</p>
)}
<div className="@container flex flex-col gap-y-3">
{rejectedFiles.map(
({ file, errors }: { file: File; errors: readonly FileError[] }, index) => (
<div key={`${file.name}-${index}`}>
<div className="5m:text-[13px] flex items-start justify-between gap-x-4 text-xs text-muted-foreground">
<div>
<div className="max-w-full">
<div className="@sm:max-w-[350px] @md:max-w-[380px] @lg:max-w-[475px] max-w-[240px] truncate leading-none">
{file.path} - {""}
{file.size < 1e6
? (file.size / 1e3).toFixed(2) + " KB"
: (file.size / 1e6).toFixed(2) + " MB"}
</div>
</div>
<div className="5m:text-[12.5px] mt-1 flex flex-col gap-y-2 text-[12px] font-medium leading-snug text-destructive">
{getErrorMessage(file, errors)}
</div>
</div>
<Button
onClick={() => removeRejectedFile(file.name)}
type="button"
variant={"outline"}
className={cn(
"grid size-[19px] flex-shrink-0 place-items-center p-0",
`${inClerkComponents && "dark:bg-transparent"}`,
)}
>
<X size={13} />
</Button>
</div>
</div>
),
)}
</div>
</div>
</div>
);
}
export default memo(DropZone);
Editor is loading...
Leave a Comment