Untitled
unknown
plain_text
4 months ago
9.4 kB
3
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