Files
components/upload.tsx
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}
217
A single file upload.
upload-01
Files
components/upload.tsx
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 {
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
A multiple file upload.
upload-02