# Dropzone

Import: `import { Dropzone } from '@neo4j-ndl/react'`

## Props

### Dropzone

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `acceptedFileExtensions` | `string[]` |  |  | The accepted file extensions for the Drag-and-Drop. |
| `dropZoneOptions` | `DropzoneOptions` | ✅ |  | Props for the underlying `react-dropzone` component. Don't use `accept`. If using custom file extensions as this option is broken. Instead use the `acceptedFileExtensions` property. |
| `heading` | `ReactNode` |  |  | Use when wanting a custom heading. |
| `loadingElement` | `ReactNode` |  |  | Element to display when uploading the file. |
| `ref` | `Ref<HTMLDivElement>` |  |  | A ref to apply to the root element. |
| `rejectedText` | `ReactNode` |  |  | Message to be shown when file is rejected. |
| `supportedFilesDescription` | `ReactNode` |  |  | Informational text that highlights the supported file types, will be displayed in the footer. |

Where `DropzoneOptions` is the props for the `react-dropzone` library. For more information, see the [react-dropzone documentation](https://react-dropzone.js.org/#src).

### Dropzone.LoadingProgressBar

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `progressBarMinute` | `number` |  |  | The time in minute left to upload the file. |
| `progressBarPrecentage` | `number` | ✅ |  | The percentage of the progress bar. |
| `ref` | `Ref<HTMLDivElement>` |  |  | A ref to apply to the root element. |

## Examples

### Default

```tsx
import { Dropzone } from '@neo4j-ndl/react';
import { type DropzoneOptions } from 'react-dropzone';

const Component = () => {
  const onDrop: DropzoneOptions['onDrop'] = (files) => {
    alert(
      `attempted to upload ${
        files.length
      } file(s). Check console logs for more info`,
    );
  };

  return (
    <Dropzone
      dropZoneOptions={{
        onDrop,
      }}
    />
  );
};

export default Component;
```

### Csv Files

```tsx
import { Dropzone } from '@neo4j-ndl/react';
import { type DropzoneOptions } from 'react-dropzone';

const Component = () => {
  const onDrop: DropzoneOptions['onDrop'] = (files) => {
    alert(
      `attempted to upload ${
        files.length
      } file(s). Check console logs for more info`,
    );
  };

  return (
    <Dropzone
      supportedFilesDescription="Supports: CSV"
      dropZoneOptions={{
        accept: { 'text/csv': ['.csv'] },
        onDrop,
      }}
    />
  );
};

export default Component;
```

### Custom Extensions

```tsx
import { Dropzone } from '@neo4j-ndl/react';
import { type DropzoneOptions } from 'react-dropzone';

const Component = () => {
  const onDrop: DropzoneOptions['onDrop'] = (files) => {
    alert(
      `attempted to upload ${
        files.length
      } file(s). Check console logs for more info`,
    );
  };

  return (
    <Dropzone
      supportedFilesDescription="Supports: .dump"
      acceptedFileExtensions={['.dump']}
      dropZoneOptions={{
        onDrop,
      }}
    />
  );
};

export default Component;
```

### Disabled

```tsx
import { Dropzone } from '@neo4j-ndl/react';
import { type DropzoneOptions } from 'react-dropzone';

const Component = () => {
  const onDrop: DropzoneOptions['onDrop'] = (files) => {
    alert(
      `attempted to upload ${
        files.length
      } file(s). Check console logs for more info`,
    );
  };

  return (
    <Dropzone
      supportedFilesDescription="Supports: CSV Files"
      dropZoneOptions={{
        accept: { 'text/csv': ['.csv'] },
        disabled: true,
        onDrop,
      }}
    />
  );
};

export default Component;
```

### Full

```tsx
import { XMarkIcon } from '@heroicons/react/24/solid';
import { Dropzone, FilledButton, Typography } from '@neo4j-ndl/react';
import { useCallback, useState } from 'react';
import {
  type DropEvent,
  type DropzoneOptions,
  type FileRejection,
} from 'react-dropzone';

// Configuration constants
const CONFIG = {
  MAX_FILES: 3,
  MAX_FILE_SIZE: 5 * 1024 * 1024, // 5MB
  PROGRESS_INCREMENT: 5,
  UPDATE_INTERVAL: 100,
  ACCEPTED_EXTENSIONS: ['.txt', '.json'] as string[],
} as const;

enum UploadState {
  Idle = 'idle',
  Uploading = 'uploading',
  Success = 'success',
  Error = 'error',
}

interface UploadData {
  state: UploadState;
  progress: number;
  files: File[];
  errors: string[];
}

const Component = () => {
  const [uploadData, setUploadData] = useState<UploadData>({
    errors: [],
    files: [],
    progress: 0,
    state: UploadState.Idle,
  });

  const resetUpload = useCallback(() => {
    setUploadData({
      errors: [],
      files: [],
      progress: 0,
      state: UploadState.Idle,
    });
  }, []);

  const simulateUpload = useCallback((files: File[]) => {
    setUploadData((prev) => ({
      ...prev,
      state: UploadState.Uploading,
      files,
      progress: 0,
    }));

    const interval = setInterval(() => {
      setUploadData((prev) => {
        const newProgress = prev.progress + CONFIG.PROGRESS_INCREMENT;
        if (newProgress >= 100) {
          clearInterval(interval);
          setTimeout(
            () =>
              setUploadData((current) => ({
                ...current,
                state: UploadState.Success,
              })),
            500,
          );
          return { ...prev, progress: 100 };
        }
        return { ...prev, progress: newProgress };
      });
    }, CONFIG.UPDATE_INTERVAL);
  }, []);

  const handleDrop: DropzoneOptions['onDrop'] = useCallback(
    (
      acceptedFiles: File[],
      rejectedFiles: FileRejection[],
      event: DropEvent,
    ) => {
      console.info('Drop event:', { acceptedFiles, event, rejectedFiles });

      if (acceptedFiles.length > 0) {
        simulateUpload(acceptedFiles);
      }
    },
    [simulateUpload],
  );

  const handleDropRejected: DropzoneOptions['onDropRejected'] = useCallback(
    (rejectedFiles: FileRejection[]) => {
      const errors = rejectedFiles.map(
        (file) =>
          `${file.file.name}: ${file.errors.map((e) => e.message).join(', ')}`,
      );
      setUploadData((prev) => ({ ...prev, state: UploadState.Error, errors }));
    },
    [],
  );

  const handleError = useCallback((error: Error) => {
    console.error('Dropzone error:', error);
    setUploadData((prev) => ({
      ...prev,
      state: UploadState.Error,
      errors: [error.message],
    }));
  }, []);

  const renderStatusContent = () => {
    const { state, progress, files, errors } = uploadData;

    switch (state) {
      case UploadState.Uploading:
        return (
          <Dropzone.LoadingProgressBar
            progressBarPrecentage={Math.round(progress)}
          />
        );

      case UploadState.Success:
        return (
          <div className="n-flex n-flex-col n-justify-center n-items-center n-gap-y-token-32">
            <Typography
              variant="title-4"
              className="n-text-success-text n-font-semibold"
            >
              Upload Complete!
            </Typography>
            <Typography
              variant="body-small"
              className="n-text-neutral-text-weak"
            >
              Successfully uploaded: {files.map((file) => file.name).join(', ')}
            </Typography>
            <FilledButton onClick={resetUpload}>Upload More Files</FilledButton>
          </div>
        );

      case UploadState.Error:
        return (
          <div className="n-flex n-flex-col n-justify-center n-items-center n-gap-y-token-32">
            <XMarkIcon className="n-w-token-32 n-h-token-32 n-text-danger-text" />
            <div className="n-flex n-flex-col n-gap-y-token-2">
              {errors.map((message, index) => (
                <Typography
                  key={index}
                  variant="label"
                  className="n-text-danger-text n-font-semibold"
                  htmlAttributes={{ role: 'alert' }}
                >
                  {message}
                </Typography>
              ))}
            </div>
            <FilledButton onClick={resetUpload}>Try Again</FilledButton>
          </div>
        );

      default:
        return undefined;
    }
  };

  return (
    <Dropzone
      dropZoneOptions={{
        disabled: uploadData.state === UploadState.Uploading,
        maxFiles: CONFIG.MAX_FILES,
        maxSize: CONFIG.MAX_FILE_SIZE,
        multiple: true,
        onDrop: handleDrop,
        onDropRejected: handleDropRejected,
        onError: handleError,
      }}
      supportedFilesDescription={
        <div className="n-text-center n-flex n-flex-col n-gap-y-token-2">
          <Typography variant="body-small" className="n-text-neutral-text-weak">
            Supports: Text files (.txt, .json) only
          </Typography>
          <Typography variant="body-small" className="n-text-neutral-text-weak">
            Max {CONFIG.MAX_FILES} files •{' '}
            {CONFIG.MAX_FILE_SIZE / (1024 * 1024)}MB per file
          </Typography>
        </div>
      }
      acceptedFileExtensions={CONFIG.ACCEPTED_EXTENSIONS}
      loadingElement={renderStatusContent()}
      rejectedText="Error: Please make sure to only upload .json or .txt files"
      heading="Drag & Drop your files"
    />
  );
};

export default Component;
```

### Loading Progress

```tsx
import { Dropzone } from '@neo4j-ndl/react';

const Component = () => {
  return (
    <Dropzone
      loadingElement={
        <Dropzone.LoadingProgressBar
          progressBarPrecentage={50}
          progressBarMinute={5}
        />
      }
      dropZoneOptions={{
        onDrop: () => void 0,
      }}
    />
  );
};

export default Component;
```
