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