Untitled

 avatar
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