BelkacemYerfa / shadcn-extension

An open source component collection , that extends your ui library , built using shadcn component
https://shadcn-extension.vercel.app/
MIT License
895 stars 33 forks source link

Feedback request: File Upload + React Hook Form #83

Closed osman-sultan closed 2 months ago

osman-sultan commented 3 months ago

I am getting error:

GET /favicon.ico 200 in 38ms
 ⨯ src\components\file-upload-zone.tsx (47:20) @ File
 ⨯ ReferenceError: File is not defined
    at eval (./src/components/file-upload-zone.tsx:84:120)
    at (ssr)/./src/components/file-upload-zone.tsx (C:\Users\Name\Documents\projects\portfolio-builder\.next\server\app\page.js:184:1)
    at __webpack_require__ (C:\Users\Name\Documents\projects\portfolio-builder\.next\server\webpack-runtime.js:33:43)
    at eval (./src/app/page.tsx:7:86)
    at (ssr)/./src/app/page.tsx (C:\Users\Osman\Documents\projects\portfolio-builder\.next\server\app\page.js:173:1)
    at Object.__webpack_require__ [as require] (C:\Users\Name\Documents\projects\portfolio-builder\.next\server\webpack-runtime.js:33:43)
    at JSON.parse (<anonymous>)
digest: "1978502960"
  45 |   files: z
  46 |     .array(
> 47 |       z.instanceof(File).refine((file) => file.size < 4 * 1024 * 1024, {
     |                    ^
  48 |         message: "File size must be less than 4MB",
  49 |       })
  50 |     )
 GET / 500 in 60ms

I tried adding react-hook-form to it functionally my app is fine but this error occurs on initial page load and thus my app doesn't build. Not sure whats causing it.

Here is my full code:

"use client";

import { Button } from "@/components/ui/button";
import { Form, FormField, FormItem, FormMessage } from "@/components/ui/form";
import {
  FileInput,
  FileUploader,
  FileUploaderContent,
} from "@/components/ui/upload";
import { cn } from "@/lib/utils";
import { zodResolver } from "@hookform/resolvers/zod";
import { Paperclip, Trash } from "lucide-react";
import Papa from "papaparse";
import { DropzoneOptions } from "react-dropzone";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import z from "zod";

const FileSvgDraw = () => (
  <>
    <svg
      className="w-8 h-8 mb-3 text-gray-500 dark:text-gray-400"
      aria-hidden="true"
      xmlns="http://www.w3.org/2000/svg"
      fill="none"
      viewBox="0 0 20 16"
    >
      <path
        stroke="currentColor"
        strokeLinecap="round"
        strokeLinejoin="round"
        strokeWidth="2"
        d="M13 13h3a3 3 0 0 0 0-6h-.025A5.56 5.56 0 0 0 16 6.5 5.5 5.5 0 0 0 5.207 5.021C5.137 5.017 5.071 5 5 5a4 4 0 0 0 0 8h2.167M10 15V6m0 0L8 8m2-2 2 2"
      />
    </svg>
    <p className="mb-1 text-sm text-gray-500 dark:text-gray-400">
      <span className="font-semibold">Click to upload your ticker data</span>
      &nbsp;or drag and drop
    </p>
    <p className="text-xs text-gray-500 dark:text-gray-400">.CSV only</p>
  </>
);

const CardForm = z.object({
  files: z
    .array(
      z.instanceof(File).refine((file) => file.size < 4 * 1024 * 1024, {
        message: "File size must be less than 4MB",
      })
    )
    .max(1, {
      message: "Only one file is allowed",
    })
    .nullable(),
});

type CardFormType = z.infer<typeof CardForm>;

const validateCSV = (data: string[][]): boolean => {
  if (data.length === 0) {
    toast.error("CSV file is empty.");
    return false;
  }

  const headers = data[0];
  if (headers.length < 2) {
    toast.error("CSV must have at least two columns: Date and one ticker.");
    return false;
  }

  if (headers[0].toLowerCase() !== "date") {
    toast.error('The first column must be "Date".');
    return false;
  }

  return true;
};

type FileUploadZoneProps = {
  onDataParsed: (
    tickerData: { value: string; label: string }[],
    fullData: any[]
  ) => void;
};

export const FileUploadZone: React.FC<FileUploadZoneProps> = ({
  onDataParsed,
}) => {
  const form = useForm<CardFormType>({
    resolver: zodResolver(CardForm),
    defaultValues: {
      files: null,
    },
  });

  const dropZoneConfig: DropzoneOptions = {
    maxFiles: 1,
    maxSize: 4 * 1024 * 1024,
    multiple: true,
    accept: {
      "text/csv": [".csv"],
    },
  };

  const handleFileRemove = () => {
    form.setValue("files", []);
  };

  const onSubmit = (data: CardFormType) => {
    if (data.files && data.files.length > 0) {
      const file = data.files[0];
      Papa.parse(file, {
        header: true,
        dynamicTyping: true,
        skipEmptyLines: true,
        complete: (results: Papa.ParseResult<{ [key: string]: any }>) => {
          if (results.errors.length) {
            toast.error("Errors while parsing the CSV file.");
          } else {
            const isValid = validateCSV(
              results.data.map((row) => Object.keys(row))
            );
            if (isValid) {
              const headers = results.data.map((row) => Object.keys(row))[0];
              const tickerData = headers.slice(1).map((header) => ({
                value: header.toLowerCase(),
                label: header.toUpperCase(),
              }));
              onDataParsed(tickerData, results.data); // Pass both tickerData and full CSV data to parent component
              toast.success("Your data is now ready for use!");
            }
          }
        },
      });
    } else {
      toast.error("No valid CSV data to submit.");
    }
  };

  return (
    <Form {...form}>
      <form
        onSubmit={form.handleSubmit(onSubmit)}
        className="relative w-full flex flex-col items-center"
      >
        <div className="max-w-md w-full">
          <FormField
            control={form.control}
            name="files"
            render={({ field }) => (
              <FormItem>
                <FileUploader
                  value={field.value}
                  onValueChange={field.onChange}
                  dropzoneOptions={dropZoneConfig}
                  reSelect={true}
                  className="relative bg-background rounded-lg p-2"
                >
                  <FileInput className="outline-dashed outline-2 outline-gray-300 dark:outline-white">
                    <div className="flex items-center justify-center flex-col pt-3 pb-4 w-full">
                      <FileSvgDraw />
                    </div>
                  </FileInput>
                  {field.value && field.value.length > 0 && (
                    <FileUploaderContent>
                      {field.value.map((file, i) => (
                        <div
                          key={i}
                          className="flex items-center space-x-2 py-1"
                        >
                          <Paperclip className="h-4 w-4 stroke-current" />
                          <span>{file.name}</span>
                          <button
                            type="button"
                            onClick={handleFileRemove}
                            className="text-red-600 hover:text-red-800"
                          >
                            <Trash className="h-4 w-4" />
                          </button>
                        </div>
                      ))}
                    </FileUploaderContent>
                  )}
                </FileUploader>
                <FormMessage />
              </FormItem>
            )}
          />
        </div>
        <Button
          type="submit"
          className={cn(
            "mt-4 mx-auto block",
            !form.watch("files")?.length && "cursor-not-allowed opacity-50"
          )}
          disabled={!form.watch("files")?.length}
        >
          {form.watch("files")?.length
            ? "Use this data"
            : "No data uploaded yet"}
        </Button>
      </form>
    </Form>
  );
};

export default FileUploadZone;
BelkacemYerfa commented 3 months ago

Hi @osman-sultan

i was checking u're issue, and it is not related to the component it self, it seems the FIle object offered by js it self , it not recognizable in u're it self, but for the component it self it working normally and there is no issue regarding in it, as u see in the image that i shared with u. image

Some fixes that i saw people do when having issues with ts declaration in this sense, is to make custom inferences, like this type FileType = typeof File extends new (...args: any[]) => infer T ? T : never; And u can make it available across all u're app by declaring an global object, something like this declare global { type FileType = FileType; }, You can put it inside a types file, then add it to compiler option { "compilerOptions": { // ... other options "typeRoots": ["./node_modules/@types", "./types"] } } So when compiling the code from ts to js, the compiler will understand those options.