1'use client'
2import React, { useCallback } from 'react'
3import Image from 'next/image'
4import { Check, CloudUpload, Trash } from 'lucide-react'
5import { useDropzone } from 'react-dropzone'
6import { Controller, useForm } from 'react-hook-form'
7import { toast } from 'sonner'
8
9import { cn } from '@/lib/utils'
10import { Button } from '@/components/ui/button'
11import {
12 Dialog,
13 DialogClose,
14 DialogContent,
15 DialogDescription,
16 DialogFooter,
17 DialogHeader,
18 DialogTitle,
19 DialogTrigger,
20} from '@/components/ui/dialog'
21
22type UploadStatus = 'uploading' | 'success' | 'ready' | null
23type UploadFile = { file: File | null; status: UploadStatus }
24
25export function Upload() {
26 const {
27 control,
28 handleSubmit,
29 setValue,
30 watch,
31 formState: { isSubmitting },
32 } = useForm<{ file: UploadFile }>({
33 defaultValues: {
34 file: { file: null, status: null },
35 },
36 })
37
38 const file = watch('file')
39
40 const onDrop = useCallback(
41 (acceptedFiles: File[]) => {
42 if (!acceptedFiles.length) return
43
44 const uploadedFile = acceptedFiles[0]
45 setValue('file', { file: uploadedFile, status: 'uploading' })
46
47 setTimeout(() => {
48 setValue('file', { file: uploadedFile, status: 'success' })
49 setTimeout(() => {
50 setValue('file', { file: uploadedFile, status: 'ready' })
51 }, 1000)
52 }, 800)
53 },
54 [setValue],
55 )
56
57 const { getRootProps, getInputProps, isDragActive } = useDropzone({
58 onDrop,
59 multiple: false,
60 })
61
62 const onSubmit = async (data: { file: UploadFile }) => {
63 console.log(`data =>`, data)
64 if (!data.file.file) {
65 toast.error('Please select a file to upload.')
66 return
67 }
68
69 await new Promise((resolve) => setTimeout(resolve, 1000))
70 toast.success('File uploaded successfully!')
71 console.log('Uploaded:', data.file)
72 }
73
74 return (
75 <Dialog>
76 <DialogTrigger asChild>
77 <Button variant="outline">Upload file</Button>
78 </DialogTrigger>
79 <DialogContent className="sm:max-w-[425px]">
80 <form onSubmit={handleSubmit(onSubmit)} className="grid gap-4">
81 <DialogHeader>
82 <DialogTitle>Upload file</DialogTitle>
83 <DialogDescription>
84 Select and upload your file
85 </DialogDescription>
86 </DialogHeader>
87 <Controller
88 name="file"
89 control={control}
90 render={() => (
91 <div
92 {...getRootProps()}
93 className={cn(
94 'cursor-pointer overflow-hidden rounded-xl border-2 border-dashed text-center transition focus:outline-none',
95 isDragActive
96 ? 'border-secondary bg-gray-100'
97 : 'border-border hover:bg-secondary/10 bg-gray-100',
98 {
99 'p-10': !file.file,
100 },
101 )}
102 >
103 <input {...getInputProps()} />
104
105 {file.file ? (
106 <div className="flex min-h-40 justify-center">
107 <div className="relative flex w-full flex-col items-center justify-center bg-white">
108 {file.file.type.startsWith(
109 'image/',
110 ) ? (
111 <Image
112 src={URL.createObjectURL(
113 file.file,
114 )}
115 alt={file.file.name}
116 width={200}
117 height={200}
118 className="w-full rounded-xl object-cover"
119 />
120 ) : (
121 <div className="flex w-full items-center justify-center text-4xl text-gray-400">
122 📄
123 </div>
124 )}
125
126 {file.status === 'uploading' && (
127 <div className="absolute inset-0 flex items-center justify-center overflow-hidden rounded-2xl bg-white/80">
128 <div className="upload-bar relative h-4 w-[70%] overflow-hidden rounded-full bg-gray-100">
129 <div className="progress from-primary to-secondary absolute h-full w-0 rounded-full bg-linear-to-r"></div>
130 </div>
131 </div>
132 )}
133
134 {file.status === 'success' && (
135 <div className="absolute inset-0 flex items-center justify-center rounded-2xl">
136 <div className="grid size-12 place-content-center rounded-full bg-white/70 backdrop-blur-xs">
137 <Check className="text-primary size-6 stroke-6" />
138 </div>
139 </div>
140 )}
141
142 {file.status === 'ready' && (
143 <Button
144 type="button"
145 className="absolute top-1 right-1 bg-white/70 backdrop-blur-xs"
146 variant={'outline'}
147 size={'icon'}
148 onClick={(e) => {
149 e.stopPropagation()
150 setValue('file', {
151 file: null,
152 status: null,
153 })
154 }}
155 >
156 <Trash />
157 </Button>
158 )}
159
160 {file.status === 'ready' && (
161 <p className="absolute bottom-2 mt-1 max-w-[80%] truncate rounded-full bg-white/70 p-2 text-xs backdrop-blur-xs">
162 {file.file.name}
163 </p>
164 )}
165 </div>
166 </div>
167 ) : (
168 <div className="flex flex-col items-center justify-center gap-4">
169 <CloudUpload className="text-gray" />
170 {isDragActive ? (
171 <p className="text-gray font-medium">
172 Drop file here...
173 </p>
174 ) : (
175 <p className="text-gray-600">
176 Drag & drop or click to upload
177 file
178 </p>
179 )}
180 <Button
181 variant={'outline'}
182 type="button"
183 >
184 Browse file
185 </Button>
186 </div>
187 )}
188 </div>
189 )}
190 />
191 <style jsx>{`
192 .upload-bar .progress {
193 animation: progressAnim 0.6s ease-in-out forwards;
194 }
195 @keyframes progressAnim {
196 0% {
197 width: 0%;
198 }
199 100% {
200 width: 100%;
201 }
202 }
203 `}</style>
204 <DialogFooter>
205 <DialogClose asChild>
206 <Button variant="outline">Cancel</Button>
207 </DialogClose>
208 <Button type="submit" loading={isSubmitting}>
209 Upload File
210 </Button>
211 </DialogFooter>
212 </form>
213 </DialogContent>
214 </Dialog>
215 )
216}
2171'use client'
2import React, { useCallback } from 'react'
3import Image from 'next/image'
4import { Check, CloudUpload, Trash } from 'lucide-react'
5import { useDropzone } from 'react-dropzone'
6import { Controller, useForm } from 'react-hook-form'
7import { toast } from 'sonner'
8
9import { cn } from '@/lib/utils'
10import { Button } from '@/components/ui/button'
11import {
12 Dialog,
13 DialogClose,
14 DialogContent,
15 DialogDescription,
16 DialogFooter,
17 DialogHeader,
18 DialogTitle,
19 DialogTrigger,
20} from '@/components/ui/dialog'
21
22type UploadStatus = 'uploading' | 'success' | 'ready' | null
23type UploadFile = { file: File | null; status: UploadStatus }
24
25export function Upload() {
26 const {
27 control,
28 handleSubmit,
29 setValue,
30 watch,
31 formState: { isSubmitting },
32 } = useForm<{ file: UploadFile }>({
33 defaultValues: {
34 file: { file: null, status: null },
35 },
36 })
37
38 const file = watch('file')
39
40 const onDrop = useCallback(
41 (acceptedFiles: File[]) => {
42 if (!acceptedFiles.length) return
43
44 const uploadedFile = acceptedFiles[0]
45 setValue('file', { file: uploadedFile, status: 'uploading' })
46
47 setTimeout(() => {
48 setValue('file', { file: uploadedFile, status: 'success' })
49 setTimeout(() => {
50 setValue('file', { file: uploadedFile, status: 'ready' })
51 }, 1000)
52 }, 800)
53 },
54 [setValue],
55 )
56
57 const { getRootProps, getInputProps, isDragActive } = useDropzone({
58 onDrop,
59 multiple: false,
60 })
61
62 const onSubmit = async (data: { file: UploadFile }) => {
63 console.log(`data =>`, data)
64 if (!data.file.file) {
65 toast.error('Please select a file to upload.')
66 return
67 }
68
69 await new Promise((resolve) => setTimeout(resolve, 1000))
70 toast.success('File uploaded successfully!')
71 console.log('Uploaded:', data.file)
72 }
73
74 return (
75 <Dialog>
76 <DialogTrigger asChild>
77 <Button variant="outline">Upload file</Button>
78 </DialogTrigger>
79 <DialogContent className="sm:max-w-[425px]">
80 <form onSubmit={handleSubmit(onSubmit)} className="grid gap-4">
81 <DialogHeader>
82 <DialogTitle>Upload file</DialogTitle>
83 <DialogDescription>
84 Select and upload your file
85 </DialogDescription>
86 </DialogHeader>
87 <Controller
88 name="file"
89 control={control}
90 render={() => (
91 <div
92 {...getRootProps()}
93 className={cn(
94 'cursor-pointer overflow-hidden rounded-xl border-2 border-dashed text-center transition focus:outline-none',
95 isDragActive
96 ? 'border-secondary bg-gray-100'
97 : 'border-border hover:bg-secondary/10 bg-gray-100',
98 {
99 'p-10': !file.file,
100 },
101 )}
102 >
103 <input {...getInputProps()} />
104
105 {file.file ? (
106 <div className="flex min-h-40 justify-center">
107 <div className="relative flex w-full flex-col items-center justify-center bg-white">
108 {file.file.type.startsWith(
109 'image/',
110 ) ? (
111 <Image
112 src={URL.createObjectURL(
113 file.file,
114 )}
115 alt={file.file.name}
116 width={200}
117 height={200}
118 className="w-full rounded-xl object-cover"
119 />
120 ) : (
121 <div className="flex w-full items-center justify-center text-4xl text-gray-400">
122 📄
123 </div>
124 )}
125
126 {file.status === 'uploading' && (
127 <div className="absolute inset-0 flex items-center justify-center overflow-hidden rounded-2xl bg-white/80">
128 <div className="upload-bar relative h-4 w-[70%] overflow-hidden rounded-full bg-gray-100">
129 <div className="progress from-primary to-secondary absolute h-full w-0 rounded-full bg-linear-to-r"></div>
130 </div>
131 </div>
132 )}
133
134 {file.status === 'success' && (
135 <div className="absolute inset-0 flex items-center justify-center rounded-2xl">
136 <div className="grid size-12 place-content-center rounded-full bg-white/70 backdrop-blur-xs">
137 <Check className="text-primary size-6 stroke-6" />
138 </div>
139 </div>
140 )}
141
142 {file.status === 'ready' && (
143 <Button
144 type="button"
145 className="absolute top-1 right-1 bg-white/70 backdrop-blur-xs"
146 variant={'outline'}
147 size={'icon'}
148 onClick={(e) => {
149 e.stopPropagation()
150 setValue('file', {
151 file: null,
152 status: null,
153 })
154 }}
155 >
156 <Trash />
157 </Button>
158 )}
159
160 {file.status === 'ready' && (
161 <p className="absolute bottom-2 mt-1 max-w-[80%] truncate rounded-full bg-white/70 p-2 text-xs backdrop-blur-xs">
162 {file.file.name}
163 </p>
164 )}
165 </div>
166 </div>
167 ) : (
168 <div className="flex flex-col items-center justify-center gap-4">
169 <CloudUpload className="text-gray" />
170 {isDragActive ? (
171 <p className="text-gray font-medium">
172 Drop file here...
173 </p>
174 ) : (
175 <p className="text-gray-600">
176 Drag & drop or click to upload
177 file
178 </p>
179 )}
180 <Button
181 variant={'outline'}
182 type="button"
183 >
184 Browse file
185 </Button>
186 </div>
187 )}
188 </div>
189 )}
190 />
191 <style jsx>{`
192 .upload-bar .progress {
193 animation: progressAnim 0.6s ease-in-out forwards;
194 }
195 @keyframes progressAnim {
196 0% {
197 width: 0%;
198 }
199 100% {
200 width: 100%;
201 }
202 }
203 `}</style>
204 <DialogFooter>
205 <DialogClose asChild>
206 <Button variant="outline">Cancel</Button>
207 </DialogClose>
208 <Button type="submit" loading={isSubmitting}>
209 Upload File
210 </Button>
211 </DialogFooter>
212 </form>
213 </DialogContent>
214 </Dialog>
215 )
216}
2171'use client'
2import React, { useCallback } from 'react'
3import Image from 'next/image'
4import { Check, CloudUpload, Trash } from 'lucide-react'
5import { useDropzone } from 'react-dropzone'
6import {
7 Controller,
8 SubmitHandler,
9 useFieldArray,
10 useForm,
11} from 'react-hook-form'
12import { toast } from 'sonner'
13
14import { Button } from '@/components/ui/button'
15import {
16 Dialog,
17 DialogClose,
18 DialogContent,
19 DialogDescription,
20 DialogFooter,
21 DialogHeader,
22 DialogTitle,
23 DialogTrigger,
24} from '@/components/ui/dialog'
25
26type UploadStatus = 'uploading' | 'success' | 'ready'
27type UploadFile = { file: File; status: UploadStatus }
28
29type FormValues = {
30 files: UploadFile[]
31}
32
33export function Upload() {
34 const {
35 control,
36 handleSubmit,
37 formState: { isSubmitting },
38 } = useForm<FormValues>({
39 defaultValues: { files: [] },
40 })
41
42 const { fields, append, remove, update } = useFieldArray({
43 control,
44 name: 'files',
45 })
46
47 const onDrop = useCallback(
48 (acceptedFiles: File[]) => {
49 const newFiles = acceptedFiles.map((file) => ({
50 file,
51 status: 'uploading' as const,
52 }))
53 newFiles.forEach((file, index) => {
54 append(file)
55
56 const targetIndex = fields.length + index
57
58 setTimeout(
59 () => {
60 update(targetIndex, {
61 ...file,
62 status: 'success',
63 })
64
65 setTimeout(() => {
66 update(targetIndex, {
67 ...file,
68 status: 'ready',
69 })
70 }, 1000)
71 },
72 500 + index * 400,
73 )
74 })
75 },
76 [append, update, fields.length],
77 )
78
79 const { getRootProps, getInputProps, isDragActive } = useDropzone({
80 onDrop,
81 multiple: true,
82 })
83
84 const onSubmit: SubmitHandler<FormValues> = async (data) => {
85 if (!data.files.length) {
86 toast.error('Please select at least one file')
87 return
88 }
89
90 await new Promise((resolve) => setTimeout(resolve, 1000))
91 console.log('Uploaded files:', data.files)
92 toast.success('Files uploaded successfully!')
93 }
94
95 return (
96 <Dialog>
97 <DialogTrigger asChild>
98 <Button variant="outline">Upload files</Button>
99 </DialogTrigger>
100 <DialogContent className="sm:max-w-[425px]">
101 <form onSubmit={handleSubmit(onSubmit)} className="grid gap-4">
102 <DialogHeader>
103 <DialogTitle>Upload files</DialogTitle>
104 <DialogDescription>
105 Select and upload your files
106 </DialogDescription>
107 </DialogHeader>
108
109 <Controller
110 name="files"
111 control={control}
112 render={() => (
113 <div
114 {...getRootProps()}
115 className={`cursor-pointer rounded-xl border-2 border-dashed p-10 text-center transition focus:outline-none ${
116 isDragActive
117 ? 'border-secondary bg-gray-100'
118 : 'border-border hover:bg-secondary/10 bg-gray-100'
119 }`}
120 >
121 <input {...getInputProps()} multiple />
122 <div className="flex flex-col items-center justify-center gap-4">
123 <CloudUpload className="text-gray" />
124 {isDragActive ? (
125 <p className="text-gray font-medium">
126 Drop files here...
127 </p>
128 ) : (
129 <p className="text-gray-600">
130 Drag & drop or click to upload files
131 </p>
132 )}
133 <Button variant={'outline'} type="button">
134 Browse files
135 </Button>
136 </div>
137 </div>
138 )}
139 />
140
141 {fields.length > 0 && (
142 <div className="grid grid-cols-3 gap-3">
143 {fields.map((f, i) => (
144 <div
145 key={f.id}
146 className="relative flex flex-col items-center justify-center rounded-2xl bg-white p-2 shadow-md"
147 >
148 {f.file?.type?.startsWith('image/') ? (
149 <Image
150 src={URL.createObjectURL(f.file)}
151 alt={f.file.name}
152 width={80}
153 height={80}
154 className="size-20 rounded-xl object-cover"
155 />
156 ) : (
157 <div className="flex size-full items-center justify-center text-4xl text-gray-400">
158 📄
159 </div>
160 )}
161
162 {f.status === 'uploading' && (
163 <div className="absolute inset-0 flex items-center justify-center overflow-hidden rounded-2xl bg-white/80">
164 <div className="upload-bar relative h-4 w-[70%] overflow-hidden rounded-full bg-gray-100">
165 <div className="progress from-primary to-secondary absolute h-full w-0 rounded-full bg-linear-to-r"></div>
166 </div>
167 </div>
168 )}
169
170 {f.status === 'success' && (
171 <div className="absolute inset-0 flex items-center justify-center rounded-2xl">
172 <div className="grid size-12 place-content-center rounded-full bg-white/70 backdrop-blur-xs">
173 <Check className="text-primary size-6 stroke-6" />
174 </div>
175 </div>
176 )}
177
178 {f.status === 'ready' && (
179 <Button
180 type="button"
181 className="absolute top-1 right-1 bg-white/70 backdrop-blur-xs"
182 variant={'outline'}
183 size={'icon'}
184 onClick={() => remove(i)}
185 >
186 <Trash />
187 </Button>
188 )}
189
190 <p className="mt-1 max-w-[80%] truncate text-xs">
191 {f.file?.name}
192 </p>
193 </div>
194 ))}
195 </div>
196 )}
197
198 <style jsx>{`
199 .upload-bar .progress {
200 animation: progressAnim 0.6s ease-in-out forwards;
201 }
202 @keyframes progressAnim {
203 0% {
204 width: 0%;
205 }
206 100% {
207 width: 100%;
208 }
209 }
210 `}</style>
211 <DialogFooter>
212 <DialogClose asChild>
213 <Button variant="outline">Cancel</Button>
214 </DialogClose>
215 <Button type="submit" loading={isSubmitting}>
216 Upload Files
217 </Button>
218 </DialogFooter>
219 </form>
220 </DialogContent>
221 </Dialog>
222 )
223}
2241'use client'
2import React, { useCallback } from 'react'
3import Image from 'next/image'
4import { Check, CloudUpload, Trash } from 'lucide-react'
5import { useDropzone } from 'react-dropzone'
6import {
7 Controller,
8 SubmitHandler,
9 useFieldArray,
10 useForm,
11} from 'react-hook-form'
12import { toast } from 'sonner'
13
14import { Button } from '@/components/ui/button'
15import {
16 Dialog,
17 DialogClose,
18 DialogContent,
19 DialogDescription,
20 DialogFooter,
21 DialogHeader,
22 DialogTitle,
23 DialogTrigger,
24} from '@/components/ui/dialog'
25
26type UploadStatus = 'uploading' | 'success' | 'ready'
27type UploadFile = { file: File; status: UploadStatus }
28
29type FormValues = {
30 files: UploadFile[]
31}
32
33export function Upload() {
34 const {
35 control,
36 handleSubmit,
37 formState: { isSubmitting },
38 } = useForm<FormValues>({
39 defaultValues: { files: [] },
40 })
41
42 const { fields, append, remove, update } = useFieldArray({
43 control,
44 name: 'files',
45 })
46
47 const onDrop = useCallback(
48 (acceptedFiles: File[]) => {
49 const newFiles = acceptedFiles.map((file) => ({
50 file,
51 status: 'uploading' as const,
52 }))
53 newFiles.forEach((file, index) => {
54 append(file)
55
56 const targetIndex = fields.length + index
57
58 setTimeout(
59 () => {
60 update(targetIndex, {
61 ...file,
62 status: 'success',
63 })
64
65 setTimeout(() => {
66 update(targetIndex, {
67 ...file,
68 status: 'ready',
69 })
70 }, 1000)
71 },
72 500 + index * 400,
73 )
74 })
75 },
76 [append, update, fields.length],
77 )
78
79 const { getRootProps, getInputProps, isDragActive } = useDropzone({
80 onDrop,
81 multiple: true,
82 })
83
84 const onSubmit: SubmitHandler<FormValues> = async (data) => {
85 if (!data.files.length) {
86 toast.error('Please select at least one file')
87 return
88 }
89
90 await new Promise((resolve) => setTimeout(resolve, 1000))
91 console.log('Uploaded files:', data.files)
92 toast.success('Files uploaded successfully!')
93 }
94
95 return (
96 <Dialog>
97 <DialogTrigger asChild>
98 <Button variant="outline">Upload files</Button>
99 </DialogTrigger>
100 <DialogContent className="sm:max-w-[425px]">
101 <form onSubmit={handleSubmit(onSubmit)} className="grid gap-4">
102 <DialogHeader>
103 <DialogTitle>Upload files</DialogTitle>
104 <DialogDescription>
105 Select and upload your files
106 </DialogDescription>
107 </DialogHeader>
108
109 <Controller
110 name="files"
111 control={control}
112 render={() => (
113 <div
114 {...getRootProps()}
115 className={`cursor-pointer rounded-xl border-2 border-dashed p-10 text-center transition focus:outline-none ${
116 isDragActive
117 ? 'border-secondary bg-gray-100'
118 : 'border-border hover:bg-secondary/10 bg-gray-100'
119 }`}
120 >
121 <input {...getInputProps()} multiple />
122 <div className="flex flex-col items-center justify-center gap-4">
123 <CloudUpload className="text-gray" />
124 {isDragActive ? (
125 <p className="text-gray font-medium">
126 Drop files here...
127 </p>
128 ) : (
129 <p className="text-gray-600">
130 Drag & drop or click to upload files
131 </p>
132 )}
133 <Button variant={'outline'} type="button">
134 Browse files
135 </Button>
136 </div>
137 </div>
138 )}
139 />
140
141 {fields.length > 0 && (
142 <div className="grid grid-cols-3 gap-3">
143 {fields.map((f, i) => (
144 <div
145 key={f.id}
146 className="relative flex flex-col items-center justify-center rounded-2xl bg-white p-2 shadow-md"
147 >
148 {f.file?.type?.startsWith('image/') ? (
149 <Image
150 src={URL.createObjectURL(f.file)}
151 alt={f.file.name}
152 width={80}
153 height={80}
154 className="size-20 rounded-xl object-cover"
155 />
156 ) : (
157 <div className="flex size-full items-center justify-center text-4xl text-gray-400">
158 📄
159 </div>
160 )}
161
162 {f.status === 'uploading' && (
163 <div className="absolute inset-0 flex items-center justify-center overflow-hidden rounded-2xl bg-white/80">
164 <div className="upload-bar relative h-4 w-[70%] overflow-hidden rounded-full bg-gray-100">
165 <div className="progress from-primary to-secondary absolute h-full w-0 rounded-full bg-linear-to-r"></div>
166 </div>
167 </div>
168 )}
169
170 {f.status === 'success' && (
171 <div className="absolute inset-0 flex items-center justify-center rounded-2xl">
172 <div className="grid size-12 place-content-center rounded-full bg-white/70 backdrop-blur-xs">
173 <Check className="text-primary size-6 stroke-6" />
174 </div>
175 </div>
176 )}
177
178 {f.status === 'ready' && (
179 <Button
180 type="button"
181 className="absolute top-1 right-1 bg-white/70 backdrop-blur-xs"
182 variant={'outline'}
183 size={'icon'}
184 onClick={() => remove(i)}
185 >
186 <Trash />
187 </Button>
188 )}
189
190 <p className="mt-1 max-w-[80%] truncate text-xs">
191 {f.file?.name}
192 </p>
193 </div>
194 ))}
195 </div>
196 )}
197
198 <style jsx>{`
199 .upload-bar .progress {
200 animation: progressAnim 0.6s ease-in-out forwards;
201 }
202 @keyframes progressAnim {
203 0% {
204 width: 0%;
205 }
206 100% {
207 width: 100%;
208 }
209 }
210 `}</style>
211 <DialogFooter>
212 <DialogClose asChild>
213 <Button variant="outline">Cancel</Button>
214 </DialogClose>
215 <Button type="submit" loading={isSubmitting}>
216 Upload Files
217 </Button>
218 </DialogFooter>
219 </form>
220 </DialogContent>
221 </Dialog>
222 )
223}
224