返回 presentation-ai
file-upload.tsx
根目录 / src / components / ui / file-upload.tsx
1 "use client";
2
3 import { Input } from "@/components/ui/input";
4 import { ScrollArea } from "@/components/ui/scroll-area";
5 import { cn } from "@/lib/utils";
6 import {
7 AudioWaveform,
8 File,
9 FileImage,
10 FolderArchive,
11 UploadCloud,
12 Video,
13 X,
14 } from "lucide-react";
15 import { useCallback } from "react";
16 import { type FileRejection, useDropzone } from "react-dropzone";
17 import { Button } from "./button";
18 import { useToast } from "./use-toast";
19
20 enum FileTypes {
21 Image = "image",
22 Pdf = "pdf",
23 Audio = "audio",
24 Video = "video",
25 Other = "other",
26 }
27
28 const ImageColor = {
29 bgColor: "bg-purple-600",
30 fillColor: "fill-purple-600",
31 };
32
33 const PdfColor = {
34 bgColor: "bg-blue-400",
35 fillColor: "fill-blue-400",
36 };
37
38 const AudioColor = {
39 bgColor: "bg-yellow-400",
40 fillColor: "fill-yellow-400",
41 };
42
43 const VideoColor = {
44 bgColor: "bg-green-400",
45 fillColor: "fill-green-400",
46 };
47
48 const OtherColor = {
49 bgColor: "bg-gray-400",
50 fillColor: "fill-gray-400",
51 };
52
53 export default function FileUpload({
54 files,
55 setFiles,
56 onUpload,
57 isLoading,
58 multiple = false,
59 maxFiles = 1,
60 maxSize = 16 * 1024 * 1024,
61 acceptedTypes = ["pdf", "docx", "txt"],
62 info,
63 showUploadButton = true,
64 dropzoneMinHeightClassName = "min-h-[350px]",
65 dropzoneClassName,
66 }: {
67 files: File[];
68 setFiles: (files: File[] | ((prevFiles: File[]) => File[])) => void;
69 onUpload?: (files: File[]) => void;
70 isLoading: boolean;
71 multiple?: boolean;
72 maxFiles?: number;
73 maxSize?: number;
74 acceptedTypes?: string[];
75 info?: string;
76 showUploadButton?: boolean;
77 dropzoneMinHeightClassName?: string;
78 dropzoneClassName?: string;
79 }) {
80 const { toast } = useToast();
81 const getFileIconAndColor = (file: File) => {
82 if (file.type.includes(FileTypes.Image)) {
83 return {
84 icon: <FileImage size={40} className={ImageColor.fillColor} />,
85 color: ImageColor.bgColor,
86 };
87 }
88
89 if (file.type.includes(FileTypes.Pdf)) {
90 return {
91 icon: <File size={40} className={PdfColor.fillColor} />,
92 color: PdfColor.bgColor,
93 };
94 }
95
96 if (file.type.includes(FileTypes.Audio)) {
97 return {
98 icon: <AudioWaveform size={40} className={AudioColor.fillColor} />,
99 color: AudioColor.bgColor,
100 };
101 }
102
103 if (file.type.includes(FileTypes.Video)) {
104 return {
105 icon: <Video size={40} className={VideoColor.fillColor} />,
106 color: VideoColor.bgColor,
107 };
108 }
109
110 return {
111 icon: <FolderArchive size={40} className={OtherColor.fillColor} />,
112 color: OtherColor.bgColor,
113 };
114 };
115
116 // feel free to mode all these functions to separate utils
117 // here is just for simplicity
118
119 const removeFile = (file: File) => {
120 setFiles((prevUploadProgress) => {
121 return prevUploadProgress.filter((item) => item !== file);
122 });
123 };
124
125 const onDrop = useCallback(
126 (acceptedFiles: File[], fileRejected: FileRejection[]) => {
127 setFiles((prevUploadProgress) => {
128 return [
129 ...prevUploadProgress,
130 ...acceptedFiles.map((file) => {
131 return file;
132 }),
133 ];
134 });
135 if (fileRejected.length > 0) {
136 toast({
137 title: "File type not allowed",
138 description: "Please upload a valid file type",
139 });
140 }
141 },
142 [],
143 );
144
145 const { getRootProps, getInputProps } = useDropzone({
146 onDrop,
147 accept: acceptedTypes.reduce(
148 (acc, type) => {
149 switch (type) {
150 case "pdf":
151 acc["application/pdf"] = [".pdf"];
152 break;
153 case "doc":
154 acc["application/msword"] = [".doc"];
155 break;
156 case "docx":
157 acc[
158 "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
159 ] = [".docx"];
160 break;
161 case "txt":
162 acc["text/plain"] = [".txt"];
163 break;
164 case "webm":
165 acc["video/webm"] = [".webm"];
166 break;
167 case "mp4":
168 acc["video/mp4"] = [".mp4"];
169 break;
170 case "mov":
171 acc["video/quicktime"] = [".mov"];
172 break;
173 // Add more cases for other file types as needed
174 }
175 return acc;
176 },
177 {} as Record<string, string[]>,
178 ),
179 maxFiles,
180 maxSize,
181 multiple,
182 onDragEnter: () => {},
183 onDragLeave: () => {},
184 onDragOver: () => {},
185 });
186
187 return (
188 <div className="min-h-full w-full max-w-full overflow-x-auto">
189 <div className={cn("grid", dropzoneMinHeightClassName)}>
190 <label
191 {...getRootProps()}
192 className={cn(
193 "relative flex h-full w-full cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed border-primary bg-background py-6 hover:bg-muted",
194 dropzoneClassName,
195 )}
196 >
197 <div className="text-center">
198 <div className="mx-auto max-w-min rounded-md border p-2">
199 <UploadCloud className="text-primary" size={20} />
200 </div>
201
202 <p className="mt-2 text-sm text-gray-600">
203 <span className="font-semibold">
204 Click to upload or drag and drop
205 </span>
206 </p>
207 <p className="text-xs text-gray-500">
208 {acceptedTypes.join(", ").toUpperCase()} (MAX{" "}
209 {maxSize / 1024 / 1024} MB)
210 </p>
211 {info && (
212 <p className="text-xs whitespace-pre-line text-gray-500">
213 {info}
214 </p>
215 )}
216 </div>
217 </label>
218
219 <Input
220 {...getInputProps()}
221 id="dropzone-file"
222 accept={acceptedTypes.map((type) => `.${type}`).join(", ")}
223 type="file"
224 className="hidden"
225 />
226 </div>
227
228 {showUploadButton && files.length > 0 && (
229 <div>
230 <p className="my-2 mt-6 text-sm font-medium text-muted-foreground">
231 Files to upload
232 </p>
233 <ScrollArea className="max-h-48">
234 <div className="space-y-2 pr-3">
235 {files.map((file) => {
236 return (
237 <div
238 key={file.lastModified}
239 className="group flex justify-between gap-2 overflow-hidden rounded-lg border border-primary pr-2 hover:pr-0"
240 >
241 <div className="flex flex-1 items-center p-2">
242 <div className="text-primary">
243 {getFileIconAndColor(file).icon}
244 </div>
245
246 <div className="ml-2 w-full space-y-1">
247 <div className="flex justify-between text-sm">
248 <p className="text-muted-foreground">
249 {file.name.slice(0, 25)}
250 </p>
251 </div>
252 </div>
253 </div>
254 <button
255 onClick={() => removeFile(file)}
256 className="hidden items-center justify-center bg-red-500 px-2 text-primary transition-all group-hover:flex"
257 >
258 <X size={20} />
259 </button>
260 </div>
261 );
262 })}
263 </div>
264 </ScrollArea>
265 </div>
266 )}
267
268 {showUploadButton && files.length > 0 && (
269 <div className="flex justify-end pt-4">
270 <Button
271 variant={isLoading ? "loading" : "default"}
272 onClick={async () => onUpload?.(files)}
273 >
274 Upload
275 </Button>
276 </div>
277 )}
278 </div>
279 );
280 }
281
281 lines Plain Text