dropzone-demo

PreviousNext
Docs
kanpekiexample

Preview

Loading preview…
registry/examples/dropzone/dropzone-demo.tsx
"use client";

import { ImageIcon, ImageUpIcon, UploadIcon, XIcon } from "lucide-react";
import { useState } from "react";
import { FileTrigger } from "react-aria-components";
import { toast } from "sonner";
import { Button, RACButton } from "~/registry/ui/button";
import { Dropzone } from "~/registry/ui/dropzone";

export function DropzoneDemo() {
  return (
    <div className="flex flex-col items-start gap-4 md:flex-row">
      <Basic />
      <WithFileTrigger />
      <Clickable />
    </div>
  );
}

function Basic() {
  return (
    <Dropzone.Provider
      onDrop={(e) => {
        const items = e.items.filter((file) => file.kind !== "text");

        toast.info(
          `Dropped items: ${items.map((item) => item.name).join(", ")}`
        );
      }}
    >
      <Dropzone.Root className="min-h-32 w-96">
        <div className="flex items-center gap-2">
          <UploadIcon className="size-4" />
          <Dropzone.Label>Upload files</Dropzone.Label>
        </div>
      </Dropzone.Root>
    </Dropzone.Provider>
  );
}

const maxSizeMB = 5;
const acceptedFileTypes = [
  "image/svg+xml",
  "image/png",
  "image/jpeg",
  "image/gif",
];
function WithFileTrigger() {
  const [file, setFile] = useState<File | null>();
  const previewUrl = file ? URL.createObjectURL(file) : null;

  return (
    <Dropzone.Provider
      onDrop={async (e) => {
        const file = await e.items
          .find((item) => item.kind === "file")
          ?.getFile();

        if (!file) {
          setFile(null);
          return;
        }

        if (file.size > maxSizeMB * 1024 * 1024) {
          toast.error(
            `File size exceeds ${maxSizeMB}MB. Please select a smaller file.`
          );
          return;
        }

        if (!acceptedFileTypes.includes(file.type)) {
          toast.error(
            "Unsupported file type. Please upload an image (SVG, PNG, JPG, GIF)."
          );
          return;
        }

        setFile(file);
      }}
    >
      <Dropzone.Root className="min-h-52 w-96 text-center">
        {previewUrl ? (
          <div className="absolute inset-0 flex items-center justify-center p-4">
            <img
              alt={file?.name || "Uploaded image"}
              className="mx-auto max-h-full rounded object-contain"
              src={previewUrl}
            />
          </div>
        ) : (
          <div className="space-y-2">
            <div
              aria-hidden="true"
              className="flex size-11 shrink-0 items-center justify-center justify-self-center rounded-full border bg-background"
            >
              <ImageIcon className="size-4 opacity-60" />
            </div>
            <Dropzone.Label>Drop your image here</Dropzone.Label>
            <p className="text-muted-foreground text-xs">
              SVG, PNG, JPG or GIF (max. {maxSizeMB}MB)
            </p>
            <FileTrigger
              acceptedFileTypes={acceptedFileTypes}
              onSelect={(e) => {
                const file = e?.item(0);

                if (file && file.size > maxSizeMB * 1024 * 1024) {
                  toast.error(
                    `File size exceeds ${maxSizeMB}MB. Please select a smaller file.`
                  );
                  return;
                }

                setFile(file);
              }}
            >
              <Button className="mt-2" variant="outline">
                <UploadIcon
                  aria-hidden="true"
                  className="-ms-1 size-4 opacity-60"
                />
                Select image
              </Button>
            </FileTrigger>
          </div>
        )}

        {previewUrl && (
          <div className="absolute top-4 right-4">
            <Button
              aria-label="Remove image"
              className="size-8 rounded-full"
              onClick={() => setFile(null)}
              size="icon"
              variant="ghost"
            >
              <XIcon aria-hidden="true" />
            </Button>
          </div>
        )}
      </Dropzone.Root>
    </Dropzone.Provider>
  );
}

function Clickable() {
  const [file, setFile] = useState<File | null>();
  const previewUrl = file ? URL.createObjectURL(file) : null;

  return (
    <Dropzone.Provider
      onDrop={async (e) => {
        const file = await e.items
          .find((item) => item.kind === "file")
          ?.getFile();

        if (!file) {
          setFile(null);
          return;
        }

        if (file.size > maxSizeMB * 1024 * 1024) {
          toast.error(
            `File size exceeds ${maxSizeMB}MB. Please select a smaller file.`
          );
          return;
        }

        if (!acceptedFileTypes.includes(file.type)) {
          toast.error(
            "Unsupported file type. Please upload an image (SVG, PNG, JPG, GIF)."
          );
          return;
        }

        setFile(file);
      }}
    >
      <FileTrigger
        acceptedFileTypes={acceptedFileTypes}
        onSelect={(e) => {
          const file = e?.item(0);

          if (file && file.size > maxSizeMB * 1024 * 1024) {
            toast.error(
              `File size exceeds ${maxSizeMB}MB. Please select a smaller file.`
            );
            return;
          }

          setFile(file);
        }}
      >
        <RACButton>
          <Dropzone.Root className="min-h-52 w-96 text-center">
            {previewUrl ? (
              <div className="absolute inset-0 flex items-center justify-center p-4">
                <img
                  alt={file?.name || "Uploaded image"}
                  className="mx-auto max-h-full rounded object-contain"
                  src={previewUrl}
                />
              </div>
            ) : (
              <div className="space-y-2">
                <div
                  aria-hidden="true"
                  className="flex size-11 shrink-0 items-center justify-center justify-self-center rounded-full border bg-background"
                >
                  <ImageUpIcon className="size-4 opacity-60" />
                </div>
                <Dropzone.Label>
                  Drop your image here or click to browse
                </Dropzone.Label>
                <p className="text-muted-foreground text-xs">
                  Max size: {maxSizeMB}MB
                </p>
              </div>
            )}
          </Dropzone.Root>
        </RACButton>
      </FileTrigger>

      {previewUrl && (
        <div className="absolute top-4 right-4">
          <Button
            aria-label="Remove image"
            className="size-8 rounded-full"
            onClick={() => setFile(null)}
            size="icon"
            variant="ghost"
          >
            <XIcon aria-hidden="true" />
          </Button>
        </div>
      )}
    </Dropzone.Provider>
  );
}

Installation

npx shadcn@latest add @kanpeki/dropzone-demo

Usage

import { DropzoneDemo } from "@/components/dropzone-demo"
<DropzoneDemo />