file-upload-dropzone

PreviousNext
Docs
reuicomponent

Preview

Loading preview…
registry/default/components/file-upload/dropzone.tsx
'use client';

import {
  formatBytes,
  useFileUpload,
  type FileMetadata,
  type FileWithPreview,
} from '@/registry/default/hooks/use-file-upload';
import { Alert, AlertContent, AlertDescription, AlertIcon, AlertTitle } from '@/registry/default/ui/alert';
import { Button } from '@/registry/default/ui/button';
import { FileIcon, ImageIcon, TriangleAlert, UploadIcon, XIcon } from 'lucide-react';
import { cn } from '@/lib/utils';

interface FileUploadDropzoneProps {
  maxFiles?: number;
  maxSize?: number;
  accept?: string;
  multiple?: boolean;
  className?: string;
  onFilesChange?: (files: FileWithPreview[]) => void;
}

export default function FileUploadDropzone({
  maxFiles = 5,
  maxSize = 5 * 1024 * 1024, // 5MB
  accept = 'image/*,application/pdf,.doc,.docx',
  multiple = true,
  className,
  onFilesChange,
}: FileUploadDropzoneProps) {
  const [
    { files, isDragging, errors },
    {
      removeFile,
      clearFiles,
      handleDragEnter,
      handleDragLeave,
      handleDragOver,
      handleDrop,
      openFileDialog,
      getInputProps,
    },
  ] = useFileUpload({
    maxFiles,
    maxSize,
    accept,
    multiple,
    onFilesChange,
  });

  const isImage = (file: File | FileMetadata) => {
    const type = file instanceof File ? file.type : file.type;
    return type.startsWith('image/');
  };

  return (
    <div className={cn('w-full max-w-2xl mx-auto', className)}>
      {/* Dropzone */}
      <div
        className={cn(
          'relative border-2 border-dashed rounded-lg p-8 text-center transition-colors',
          isDragging ? 'border-primary bg-primary/5' : 'border-muted-foreground/25 hover:border-muted-foreground/50',
        )}
        onDragEnter={handleDragEnter}
        onDragLeave={handleDragLeave}
        onDragOver={handleDragOver}
        onDrop={handleDrop}
      >
        <input {...getInputProps()} className="sr-only" />

        <div className="flex flex-col items-center gap-4">
          <div
            className={cn(
              'flex h-20 w-20 items-center justify-center rounded-full',
              isDragging ? 'bg-primary/10' : 'bg-muted',
            )}
          >
            <ImageIcon className={cn('h-10 w-10', isDragging ? 'text-primary' : 'text-muted-foreground')} />
          </div>

          <div className="space-y-2">
            <h3 className="text-lg font-semibold">Drop your images here</h3>
            <p className="text-sm text-muted-foreground">SVG, PNG, JPG or GIF (max. {formatBytes(maxSize)})</p>
          </div>

          <Button onClick={openFileDialog} variant="outline" className="gap-2">
            <UploadIcon className="h-4 w-4" />
            Select images
          </Button>
        </div>
      </div>

      {/* Error Messages */}
      {errors.length > 0 && (
        <Alert variant="destructive" appearance="light" className="mt-5">
          <AlertIcon>
            <TriangleAlert />
          </AlertIcon>
          <AlertContent>
            <AlertTitle>File upload error(s)</AlertTitle>
            <AlertDescription>
              {errors.map((error, index) => (
                <p key={index} className="last:mb-0">
                  {error}
                </p>
              ))}
            </AlertDescription>
          </AlertContent>
        </Alert>
      )}

      {/* File List */}
      {files.length > 0 && (
        <div className="mt-6 space-y-3">
          <div className="flex items-center justify-between">
            <h4 className="text-sm font-medium">Uploaded files ({files.length})</h4>
            <Button onClick={clearFiles} variant="ghost" size="sm" className="text-muted-foreground">
              Remove all files
            </Button>
          </div>

          <div className="grid gap-3">
            {files.map((fileItem) => (
              <div key={fileItem.id} className="flex items-center gap-3 rounded-lg border bg-card p-3">
                {/* File Preview */}
                <div className="flex-shrink-0">
                  {isImage(fileItem.file) && fileItem.preview ? (
                    <img
                      src={fileItem.preview}
                      alt={fileItem.file.name}
                      className="h-12 w-12 rounded-lg border object-cover"
                    />
                  ) : (
                    <div className="flex h-12 w-12 items-center justify-center rounded-lg bg-muted">
                      <FileIcon className="h-6 w-6 text-muted-foreground" />
                    </div>
                  )}
                </div>

                {/* File Info */}
                <div className="min-w-0 flex-1">
                  <p className="truncate text-sm font-medium">{fileItem.file.name}</p>
                  <p className="text-xs text-muted-foreground">{formatBytes(fileItem.file.size)}</p>
                </div>

                {/* Remove Button */}
                <Button
                  onClick={() => removeFile(fileItem.id)}
                  variant="ghost"
                  size="sm"
                  className="text-muted-foreground hover:text-destructive"
                >
                  <XIcon className="h-4 w-4" />
                </Button>
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  );
}

Installation

npx shadcn@latest add @reui/file-upload-dropzone

Usage

import { FileUploadDropzone } from "@/components/file-upload-dropzone"
<FileUploadDropzone />