-
Notifications
You must be signed in to change notification settings - Fork 3
/
useUploader.ts
175 lines (158 loc) · 5.34 KB
/
useUploader.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
import { FileRejection, useDropzone } from "react-dropzone";
import { FileWithData } from "./Uploader.types";
import React from "react";
import { UseUploaderProps } from "./useUploader.types";
import { readAsDataURL } from "../utils/readAsDataURL";
/**
* Hook to handle file selection logic for uploaders. It is a wrapper around react-dropzone.
* @param acceptedFiles - Definition of acceptable file mime types and extensions
* @param disabled - Whether the dropzone is disabled
* @param maxFileSize - Maximum allowed file size in bytes
* @param multiple - Whether multiple files can be selected
* @param filesLimit - Maximum number of files that can be selected
* @param selectedFiles - Array of selected files
* @param onAdd - Callback function to be called when files are added
* @param onDelete - Callback function to be called when files are deleted
* @returns - Object containing dropzone props, handleDelete function and rejection message
*/
export default function useUploader({
acceptedFiles,
disabled = false,
maxFileSize = Infinity,
multiple = false,
filesLimit = 1,
selectedFiles = [],
onAdd,
onDelete
}: UseUploaderProps) {
// state for rejection message
const [rejectionMessage, setRejectionMessage] = React.useState<string | null>(
null
);
// effect to only show rejection message for 3 seconds
React.useEffect(() => {
// hide message after 3000ms
const timer = setTimeout(() => {
if (rejectionMessage) {
setRejectionMessage(null);
}
}, 3000);
return () => clearTimeout(timer);
}, [rejectionMessage]);
// handle file drops by reading them as data URLs, and adding them to the selected files
const onDropAccepted = async (droppedFiles: File[]) => {
// append preview data to file
const fileWithPreview = await Promise.all(
droppedFiles.map(async f => ({
data: await readAsDataURL(f),
file: f
}))
);
// if multiple files are allowed, append to existing selection otherwise replace it
const newSelection = multiple
? [...selectedFiles, ...fileWithPreview]
: fileWithPreview;
// if the number of files exceeds the limit, show error message
if (newSelection.length > filesLimit) {
setRejectionMessage(
`The maximum allowed number of files is ${filesLimit}.`
);
return;
}
// call onAdd callback with new selection
onAdd && onAdd(newSelection);
};
// get the accepted file type extensions
const acceptedFileExtensions = Object.values(acceptedFiles ?? {})
.flat()
.map(s => s.toLowerCase());
// handle file drops that are rejected by showing the first error message as a rejection message
const onDropRejected = (fileRejection: FileRejection[]) => {
// deafult error message
const defaultErrorMessage = fileRejection[0].errors[0].message;
// get the error code
const errorCode = fileRejection[0].errors[0].code;
// set error message based on error code
let errorMessage = "";
switch (errorCode) {
case "file-invalid-type":
errorMessage = `File type must be ${acceptedFileExtensions.join(", ")}.`;
break;
case "file-too-large": {
// get file size limit in MB
const bytesToMb = maxFileSize / 1024 / 1024;
const limitSize = `${bytesToMb.toFixed()} MB`;
errorMessage = `File size exceeds the limit of ${limitSize}.`;
break;
}
case "too-many-files":
errorMessage = `You can only upload ${filesLimit} ${filesLimit > 1 ? "files" : "file"}.`;
break;
default:
errorMessage = defaultErrorMessage;
}
// set error message
setRejectionMessage(errorMessage);
};
// validator for file extension
const validator = (file: File | DataTransferItem) => {
// react-dropzone validator types are incorrect https://github.com/react-dropzone/react-dropzone/issues/1333
if (
file instanceof File && // if we got a file
acceptedFileExtensions && // and we have accepted file extensions
acceptedFileExtensions.length > 0 && // and we have accepted file extensions
!acceptedFileExtensions.includes(
getFileExtension(file.name).toLowerCase()
) // and file extension is not accepted
)
return {
code: "file-invalid-type",
message: `File type must be one of ${acceptedFileExtensions.join(", ")}`
};
return null;
};
// use react-dropzone hook
const dropzone = useDropzone({
accept: acceptedFiles,
disabled,
maxFiles: filesLimit,
maxSize: maxFileSize,
multiple,
onDropAccepted,
onDropRejected,
useFsAccessApi: false,
validator
});
// handle file deletion
const handleDelete = (deletedFile: FileWithData) => {
// delete file from selected files
onDelete &&
onDelete(
selectedFiles.filter(i => i !== deletedFile),
deletedFile
);
};
return {
...dropzone,
handleDelete,
rejectionMessage
};
}
/**
* Extracts the file extension from a given file name.
*
* @param fileName - The name of the file.
* @return The file extension.
*
* @example
* const fileName = "example.txt";
* const extension = getFileExtension(fileName);
* console.log(extension); // Output: ".txt"
*/
function getFileExtension(fileName: string) {
const parts = fileName.split(".");
if (parts.length > 1) {
return "." + parts.pop();
}
return "";
}