Single-image
See it in action
drag & drop to upload
Usage
tsx
'use client';import { SingleImageDropzone } from '@/components/SingleImageDropzone';import { useEdgeStore } from '@/lib/edgestore';import { useState } from 'react';export function SingleImageDropzoneUsage() {const [file, setFile] = useState<File>();const { edgestore } = useEdgeStore();return (<div><SingleImageDropzonewidth={200}height={200}value={file}onChange={(file) => {setFile(file);}}/><buttononClick={async () => {if (file) {const res = await edgestore.publicFiles.upload({file,onProgressChange: (progress) => {// you can use this to show a progress barconsole.log(progress);},});// you can run some server action or api here// to add the necessary data to your databaseconsole.log(res);}}}>Upload</button></div>);}
Installation
info
This component uses tailwind for styling.
Feel free to change lucide-react
to any other icon library you prefer.
First, let's install the required dependencies:
- npm
- pnpm
- yarn
bash
npm install tailwind-merge react-dropzone lucide-react
bash
pnpm add tailwind-merge react-dropzone lucide-react
bash
yarn add tailwind-merge react-dropzone lucide-react
Now just copy the following component into your components folder.
tsx
'use client';import { formatFileSize } from '@edgestore/react/utils';import { UploadCloudIcon, X } from 'lucide-react';import * as React from 'react';import { useDropzone, type DropzoneOptions } from 'react-dropzone';import { twMerge } from 'tailwind-merge';const variants = {base: 'relative rounded-md flex justify-center items-center flex-col cursor-pointer min-h-[150px] min-w-[200px] border border-dashed border-gray-400 dark:border-gray-300 transition-colors duration-200 ease-in-out',image:'border-0 p-0 min-h-0 min-w-0 relative shadow-md bg-slate-200 dark:bg-slate-900 rounded-md',active: 'border-2',disabled:'bg-gray-200 border-gray-300 cursor-default pointer-events-none bg-opacity-30 dark:bg-gray-700',accept: 'border border-blue-500 bg-blue-500 bg-opacity-10',reject: 'border border-red-700 bg-red-700 bg-opacity-10',};type InputProps = {width: number;height: number;className?: string;value?: File | string;onChange?: (file?: File) => void | Promise<void>;disabled?: boolean;dropzoneOptions?: Omit<DropzoneOptions, 'disabled'>;};const ERROR_MESSAGES = {fileTooLarge(maxSize: number) {return `The file is too large. Max size is ${formatFileSize(maxSize)}.`;},fileInvalidType() {return 'Invalid file type.';},tooManyFiles(maxFiles: number) {return `You can only add ${maxFiles} file(s).`;},fileNotSupported() {return 'The file is not supported.';},};const SingleImageDropzone = React.forwardRef<HTMLInputElement, InputProps>(({ dropzoneOptions, width, height, value, className, disabled, onChange },ref,) => {const imageUrl = React.useMemo(() => {if (typeof value === 'string') {// in case an url is passed in, use it to display the imagereturn value;} else if (value) {// in case a file is passed in, create a base64 url to display the imagereturn URL.createObjectURL(value);}return null;}, [value]);// dropzone configurationconst {getRootProps,getInputProps,acceptedFiles,fileRejections,isFocused,isDragAccept,isDragReject,} = useDropzone({accept: { 'image/*': [] },multiple: false,disabled,onDrop: (acceptedFiles) => {const file = acceptedFiles[0];if (file) {void onChange?.(file);}},...dropzoneOptions,});// stylingconst dropZoneClassName = React.useMemo(() =>twMerge(variants.base,isFocused && variants.active,disabled && variants.disabled,imageUrl && variants.image,(isDragReject ?? fileRejections[0]) && variants.reject,isDragAccept && variants.accept,className,).trim(),[isFocused,imageUrl,fileRejections,isDragAccept,isDragReject,disabled,className,],);// error validation messagesconst errorMessage = React.useMemo(() => {if (fileRejections[0]) {const { errors } = fileRejections[0];if (errors[0]?.code === 'file-too-large') {return ERROR_MESSAGES.fileTooLarge(dropzoneOptions?.maxSize ?? 0);} else if (errors[0]?.code === 'file-invalid-type') {return ERROR_MESSAGES.fileInvalidType();} else if (errors[0]?.code === 'too-many-files') {return ERROR_MESSAGES.tooManyFiles(dropzoneOptions?.maxFiles ?? 0);} else {return ERROR_MESSAGES.fileNotSupported();}}return undefined;}, [fileRejections, dropzoneOptions]);return (<div><div{...getRootProps({className: dropZoneClassName,style: {width,height,},})}>{/* Main File Input */}<input ref={ref} {...getInputProps()} />{imageUrl ? (// Image Preview<imgclassName="h-full w-full rounded-md object-cover"src={imageUrl}alt={acceptedFiles[0]?.name}/>) : (// Upload Icon<div className="flex flex-col items-center justify-center text-xs text-gray-400"><UploadCloudIcon className="mb-2 h-7 w-7" /><div className="text-gray-400">drag & drop to upload</div><div className="mt-3"><Button type="button" disabled={disabled}>select</Button></div></div>)}{/* Remove Image Icon */}{imageUrl && !disabled && (<divclassName="group absolute right-0 top-0 -translate-y-1/4 translate-x-1/4 transform"onClick={(e) => {e.stopPropagation();void onChange?.(undefined);}}><div className="flex h-5 w-5 items-center justify-center rounded-md border border-solid border-gray-500 bg-white transition-all duration-300 hover:h-6 hover:w-6 dark:border-gray-400 dark:bg-black"><XclassName="text-gray-500 dark:text-gray-400"width={16}height={16}/></div></div>)}</div>{/* Error Text */}<div className="mt-1 text-xs text-red-500">{errorMessage}</div></div>);},);SingleImageDropzone.displayName = 'SingleImageDropzone';const Button = React.forwardRef<HTMLButtonElement,React.ButtonHTMLAttributes<HTMLButtonElement>>(({ className, ...props }, ref) => {return (<buttonclassName={twMerge(// base'focus-visible:ring-ring inline-flex cursor-pointer items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50',// color'border border-gray-400 text-gray-400 shadow hover:bg-gray-100 hover:text-gray-500 dark:border-gray-600 dark:text-gray-100 dark:hover:bg-gray-700',// size'h-6 rounded-md px-2 text-xs',className,)}ref={ref}{...props}/>);});Button.displayName = 'Button';export { SingleImageDropzone };