Docs
Dropzone (File Upload)
Dropzone (File Upload)
Displays a control for easier uploading of files directly to Supabase Storage
Loading...
Installation
Folder structure
1'use client' 2 3import { cn } from '@/lib/utils' 4import { type UseSupabaseUploadReturn } from '@/hooks/use-supabase-upload' 5import { Button } from '@/components/ui/button' 6import { CheckCircle, File, Loader2, Upload, X } from 'lucide-react' 7import { createContext, type PropsWithChildren, useCallback, useContext } from 'react' 8 9export const formatBytes = ( 10 bytes: number, 11 decimals = 2, 12 size?: 'bytes' | 'KB' | 'MB' | 'GB' | 'TB' | 'PB' | 'EB' | 'ZB' | 'YB' 13) => { 14 const k = 1000 15 const dm = decimals < 0 ? 0 : decimals 16 const sizes = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] 17 18 if (bytes === 0 || bytes === undefined) return size !== undefined ? `0 ${size}` : '0 bytes' 19 const i = size !== undefined ? sizes.indexOf(size) : Math.floor(Math.log(bytes) / Math.log(k)) 20 return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i] 21} 22 23type DropzoneContextType = Omit<UseSupabaseUploadReturn, 'getRootProps' | 'getInputProps'> 24 25const DropzoneContext = createContext<DropzoneContextType | undefined>(undefined) 26 27type DropzoneProps = UseSupabaseUploadReturn & { 28 className?: string 29} 30 31const Dropzone = ({ 32 className, 33 children, 34 getRootProps, 35 getInputProps, 36 ...restProps 37}: PropsWithChildren<DropzoneProps>) => { 38 const isSuccess = restProps.isSuccess 39 const isActive = restProps.isDragActive 40 const isInvalid = 41 (restProps.isDragActive && restProps.isDragReject) || 42 (restProps.errors.length > 0 && !restProps.isSuccess) || 43 restProps.files.some((file) => file.errors.length !== 0) 44 45 return ( 46 <DropzoneContext.Provider value={{ ...restProps }}> 47 <div 48 {...getRootProps({ 49 className: cn( 50 'border-2 border-gray-300 rounded-lg p-6 text-center bg-card transition-colors duration-300 text-foreground', 51 className, 52 isSuccess ? 'border-solid' : 'border-dashed', 53 isActive && 'border-primary bg-primary/10', 54 isInvalid && 'border-destructive bg-destructive/10' 55 ), 56 })} 57 > 58 <input {...getInputProps()} /> 59 {children} 60 </div> 61 </DropzoneContext.Provider> 62 ) 63} 64const DropzoneContent = ({ className }: { className?: string }) => { 65 const { 66 files, 67 setFiles, 68 onUpload, 69 loading, 70 successes, 71 errors, 72 maxFileSize, 73 maxFiles, 74 isSuccess, 75 } = useDropzoneContext() 76 77 const exceedMaxFiles = files.length > maxFiles 78 79 const handleRemoveFile = useCallback( 80 (fileName: string) => { 81 setFiles(files.filter((file) => file.name !== fileName)) 82 }, 83 [files, setFiles] 84 ) 85 86 if (isSuccess) { 87 return ( 88 <div className={cn('flex flex-row items-center gap-x-2 justify-center', className)}> 89 <CheckCircle size={16} className="text-primary" /> 90 <p className="text-primary text-sm"> 91 Successfully uploaded {files.length} file{files.length > 1 ? 's' : ''} 92 </p> 93 </div> 94 ) 95 } 96 97 return ( 98 <div className={cn('flex flex-col', className)}> 99 {files.map((file, idx) => { 100 const fileError = errors.find((e) => e.name === file.name) 101 const isSuccessfullyUploaded = !!successes.find((e) => e === file.name) 102 103 return ( 104 <div 105 key={`${file.name}-${idx}`} 106 className="flex items-center gap-x-4 border-b py-2 first:mt-4 last:mb-4 " 107 > 108 {file.type.startsWith('image/') ? ( 109 <div className="h-10 w-10 rounded border overflow-hidden shrink-0 bg-muted flex items-center justify-center"> 110 <img src={file.preview} alt={file.name} className="object-cover" /> 111 </div> 112 ) : ( 113 <div className="h-10 w-10 rounded border bg-muted flex items-center justify-center"> 114 <File size={18} /> 115 </div> 116 )} 117 118 <div className="shrink grow flex flex-col items-start truncate"> 119 <p title={file.name} className="text-sm truncate max-w-full"> 120 {file.name} 121 </p> 122 {file.errors.length > 0 ? ( 123 <p className="text-xs text-destructive"> 124 {file.errors 125 .map((e) => 126 e.message.startsWith('File is larger than') 127 ? `File is larger than ${formatBytes(maxFileSize, 2)} (Size: ${formatBytes(file.size, 2)})` 128 : e.message 129 ) 130 .join(', ')} 131 </p> 132 ) : loading && !isSuccessfullyUploaded ? ( 133 <p className="text-xs text-muted-foreground">Uploading file...</p> 134 ) : !!fileError ? ( 135 <p className="text-xs text-destructive">Failed to upload: {fileError.message}</p> 136 ) : isSuccessfullyUploaded ? ( 137 <p className="text-xs text-primary">Successfully uploaded file</p> 138 ) : ( 139 <p className="text-xs text-muted-foreground">{formatBytes(file.size, 2)}</p> 140 )} 141 </div> 142 143 {!loading && !isSuccessfullyUploaded && ( 144 <Button 145 size="icon" 146 variant="link" 147 className="shrink-0 justify-self-end text-muted-foreground hover:text-foreground" 148 onClick={() => handleRemoveFile(file.name)} 149 > 150 <X /> 151 </Button> 152 )} 153 </div> 154 ) 155 })} 156 {exceedMaxFiles && ( 157 <p className="text-sm text-left mt-2 text-destructive"> 158 You may upload only up to {maxFiles} files, please remove {files.length - maxFiles} file 159 {files.length - maxFiles > 1 ? 's' : ''}. 160 </p> 161 )} 162 {files.length > 0 && !exceedMaxFiles && ( 163 <div className="mt-2"> 164 <Button 165 variant="outline" 166 onClick={onUpload} 167 disabled={files.some((file) => file.errors.length !== 0) || loading} 168 > 169 {loading ? ( 170 <> 171 <Loader2 className="mr-2 h-4 w-4 animate-spin" /> 172 Uploading... 173 </> 174 ) : ( 175 <>Upload files</> 176 )} 177 </Button> 178 </div> 179 )} 180 </div> 181 ) 182} 183 184const DropzoneEmptyState = ({ className }: { className?: string }) => { 185 const { maxFiles, maxFileSize, inputRef, isSuccess } = useDropzoneContext() 186 187 if (isSuccess) { 188 return null 189 } 190 191 return ( 192 <div className={cn('flex flex-col items-center gap-y-2', className)}> 193 <Upload size={20} className="text-muted-foreground" /> 194 <p className="text-sm"> 195 Upload{!!maxFiles && maxFiles > 1 ? ` ${maxFiles}` : ''} file 196 {!maxFiles || maxFiles > 1 ? 's' : ''} 197 </p> 198 <div className="flex flex-col items-center gap-y-1"> 199 <p className="text-xs text-muted-foreground"> 200 Drag and drop or{' '} 201 <a 202 onClick={() => inputRef.current?.click()} 203 className="underline cursor-pointer transition hover:text-foreground" 204 > 205 select {maxFiles === 1 ? `file` : 'files'} 206 </a>{' '} 207 to upload 208 </p> 209 {maxFileSize !== Number.POSITIVE_INFINITY && ( 210 <p className="text-xs text-muted-foreground"> 211 Maximum file size: {formatBytes(maxFileSize, 2)} 212 </p> 213 )} 214 </div> 215 </div> 216 ) 217} 218 219const useDropzoneContext = () => { 220 const context = useContext(DropzoneContext) 221 222 if (!context) { 223 throw new Error('useDropzoneContext must be used within a Dropzone') 224 } 225 226 return context 227} 228 229export { Dropzone, DropzoneContent, DropzoneEmptyState, useDropzoneContext }
Introduction
Uploading files should be easy—this component handles the tricky parts for you.
The File Upload component makes it easy to add file uploads to your app, with built-in support for drag-and-drop, file type restrictions, image previews, and configurable limits on file size and number of files. All the essentials, ready to go.
Features
- Drag-and-drop support
- Multiple file uploads
- File size and count limits
- Image previews for supported file types
- MIME type restrictions
- Invalid file handling
- Success and error states with clear feedback
Usage
- Simply add this
<Dropzone />
component to your page and it will handle the rest. - For control over file upload, you can pass in a
props
object to the component.
'use client' import { Dropzone, DropzoneContent, DropzoneEmptyState } from '@/components/dropzone' import { useSupabaseUpload } from '@/hooks/use-supabase-upload' const FileUploadDemo = () => { const props = useSupabaseUpload({ bucketName: 'test', path: 'test', allowedMimeTypes: ['image/*'], maxFiles: 2, maxFileSize: 1000 * 1000 * 10, // 10MB, }) return ( <div className="w-[500px]"> <Dropzone {...props}> <DropzoneEmptyState /> <DropzoneContent /> </Dropzone> </div> ) } export { FileUploadDemo }
Props
Prop | Type | Default | Description |
---|---|---|---|
bucketName | string | null | The name of the Supabase Storage bucket to upload to |
path | string | null | The path or subfolder to upload the file to |
allowedMimeTypes | string[] | [] | The MIME types to allow for upload |
maxFiles | number | 1 | Maximum number of files to upload |
maxFileSize | number | 1000 | Maximum file size in bytes |