@ttoss/components 2.2.32 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,352 @@
1
+ /** Powered by @ttoss/config. https://ttoss.dev/docs/modules/packages/config/ */
2
+
3
+ // src/components/FileUploader/FileUploader.tsx
4
+ import { FormattedMessage } from "@ttoss/react-i18n";
5
+ import { Box, Button, Flex, Stack, Text } from "@ttoss/ui";
6
+ import * as React from "react";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
+ var FileUploader = ({
9
+ uploadFn,
10
+ onUploadStart,
11
+ onUploadProgress,
12
+ onUploadComplete,
13
+ onUploadError,
14
+ onFilesChange,
15
+ onRemoveFile,
16
+ accept,
17
+ multiple = true,
18
+ maxSize = 10 * 1024 * 1024,
19
+ // 10MB
20
+ maxFiles = 5,
21
+ disabled = false,
22
+ autoUpload = true,
23
+ retryAttempts = 2,
24
+ placeholder,
25
+ error,
26
+ children,
27
+ showFileList = true,
28
+ FileListComponent
29
+ }) => {
30
+ const [files, setFiles] = React.useState([]);
31
+ const [isDragOver, setIsDragOver] = React.useState(false);
32
+ const fileInputRef = React.useRef(null);
33
+ const formatFileSize = bytes => {
34
+ if (bytes === 0) return "0 Bytes";
35
+ const k = 1024;
36
+ const sizes = ["Bytes", "KB", "MB", "GB"];
37
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
38
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
39
+ };
40
+ const validateFiles = newFiles => {
41
+ const fileArray = Array.from(newFiles);
42
+ const validFiles = [];
43
+ const currentFileCount = files.length;
44
+ for (const file of fileArray) {
45
+ if (maxSize && file.size > maxSize) {
46
+ continue;
47
+ }
48
+ if (validFiles.length + currentFileCount >= maxFiles) {
49
+ break;
50
+ }
51
+ validFiles.push(file);
52
+ }
53
+ return validFiles;
54
+ };
55
+ const updateFileState = React.useCallback((file, updates) => {
56
+ setFiles(prevFiles => {
57
+ const newFiles = prevFiles.map(f => {
58
+ return f.file === file ? {
59
+ ...f,
60
+ ...updates
61
+ } : f;
62
+ });
63
+ onFilesChange?.(newFiles);
64
+ return newFiles;
65
+ });
66
+ }, [onFilesChange]);
67
+ const uploadFile = React.useCallback(async (fileState, attempts = 0) => {
68
+ try {
69
+ onUploadStart?.(fileState.file);
70
+ updateFileState(fileState.file, {
71
+ status: "uploading",
72
+ progress: 0
73
+ });
74
+ const result = await uploadFn(fileState.file, progress => {
75
+ updateFileState(fileState.file, {
76
+ progress
77
+ });
78
+ onUploadProgress?.(fileState.file, progress);
79
+ });
80
+ updateFileState(fileState.file, {
81
+ status: "completed",
82
+ progress: 100,
83
+ result
84
+ });
85
+ onUploadComplete?.(fileState.file, result);
86
+ } catch (error_) {
87
+ const error2 = error_;
88
+ if (attempts < retryAttempts) {
89
+ setTimeout(() => {
90
+ return uploadFile(fileState, attempts + 1);
91
+ }, 1e3 * (attempts + 1));
92
+ } else {
93
+ updateFileState(fileState.file, {
94
+ status: "error",
95
+ error: error2
96
+ });
97
+ onUploadError?.(fileState.file, error2);
98
+ }
99
+ }
100
+ }, [uploadFn, onUploadStart, onUploadProgress, onUploadComplete, onUploadError, retryAttempts, updateFileState]);
101
+ const addFiles = newFiles => {
102
+ const newFileStates = newFiles.map(file => {
103
+ return {
104
+ file,
105
+ status: "pending"
106
+ };
107
+ });
108
+ setFiles(prevFiles => {
109
+ const updatedFiles = [...prevFiles, ...newFileStates];
110
+ onFilesChange?.(updatedFiles);
111
+ return updatedFiles;
112
+ });
113
+ if (autoUpload) {
114
+ for (const fileState of newFileStates) {
115
+ uploadFile(fileState);
116
+ }
117
+ }
118
+ };
119
+ const handleFileChange = event => {
120
+ if (event.target.files) {
121
+ const validFiles = validateFiles(event.target.files);
122
+ addFiles(validFiles);
123
+ }
124
+ };
125
+ const handleDrop = event => {
126
+ event.preventDefault();
127
+ setIsDragOver(false);
128
+ if (disabled) return;
129
+ const droppedFiles = event.dataTransfer.files;
130
+ if (droppedFiles) {
131
+ const validFiles = validateFiles(droppedFiles);
132
+ addFiles(validFiles);
133
+ }
134
+ };
135
+ const handleDragOver = event => {
136
+ event.preventDefault();
137
+ if (!disabled) {
138
+ setIsDragOver(true);
139
+ }
140
+ };
141
+ const handleDragLeave = event => {
142
+ event.preventDefault();
143
+ setIsDragOver(false);
144
+ };
145
+ const handleClick = () => {
146
+ if (!disabled && fileInputRef.current) {
147
+ fileInputRef.current.click();
148
+ }
149
+ };
150
+ const handleRemoveFile = React.useCallback(index => {
151
+ const fileToRemove = files[index];
152
+ setFiles(prevFiles => {
153
+ const newFiles = prevFiles.filter((_, i) => {
154
+ return i !== index;
155
+ });
156
+ onFilesChange?.(newFiles);
157
+ return newFiles;
158
+ });
159
+ onRemoveFile?.(fileToRemove, index);
160
+ }, [files, onFilesChange, onRemoveFile]);
161
+ const retryUpload = React.useCallback(index => {
162
+ const fileState = files[index];
163
+ if (fileState.status === "error") {
164
+ uploadFile(fileState);
165
+ }
166
+ }, [files, uploadFile]);
167
+ const isUploading = files.some(f => {
168
+ return f.status === "uploading";
169
+ });
170
+ const fileListNode = React.useMemo(() => {
171
+ if (!showFileList || files.length === 0) {
172
+ return null;
173
+ }
174
+ if (FileListComponent) {
175
+ return /* @__PURE__ */jsx(FileListComponent, {
176
+ files,
177
+ onRemoveFile: handleRemoveFile
178
+ });
179
+ }
180
+ return /* @__PURE__ */jsx(Stack, {
181
+ sx: {
182
+ gap: 1
183
+ },
184
+ children: files.map((fileState, index) => {
185
+ return /* @__PURE__ */jsxs(Flex, {
186
+ sx: {
187
+ alignItems: "center",
188
+ justifyContent: "space-between",
189
+ p: 2,
190
+ backgroundColor: "display.background.secondary.default",
191
+ borderRadius: "md",
192
+ gap: 2
193
+ },
194
+ children: [/* @__PURE__ */jsxs(Flex, {
195
+ sx: {
196
+ alignItems: "center",
197
+ gap: 2,
198
+ flex: 1
199
+ },
200
+ children: [/* @__PURE__ */jsx(Text, {
201
+ sx: {
202
+ fontSize: "lg"
203
+ },
204
+ children: fileState.status === "completed" ? "\u2713" : fileState.status === "error" ? "\u2717" : fileState.status === "uploading" ? "\u21BB" : "\u{1F4C4}"
205
+ }), /* @__PURE__ */jsxs(Box, {
206
+ sx: {
207
+ flex: 1
208
+ },
209
+ children: [/* @__PURE__ */jsx(Text, {
210
+ variant: "body",
211
+ sx: {
212
+ fontWeight: "medium"
213
+ },
214
+ children: fileState.file.name
215
+ }), fileState.status === "uploading" && fileState.progress && /* @__PURE__ */jsxs(Text, {
216
+ variant: "caption",
217
+ sx: {
218
+ color: "primary.default"
219
+ },
220
+ children: [fileState.progress.toFixed(0), "%"]
221
+ }), fileState.status === "error" && /* @__PURE__ */jsx(Text, {
222
+ variant: "caption",
223
+ sx: {
224
+ color: "error.default"
225
+ },
226
+ children: "Failed"
227
+ })]
228
+ }), /* @__PURE__ */jsx(Text, {
229
+ variant: "caption",
230
+ sx: {
231
+ color: "text.muted"
232
+ },
233
+ children: formatFileSize(fileState.file.size)
234
+ })]
235
+ }), /* @__PURE__ */jsxs(Flex, {
236
+ sx: {
237
+ gap: 1
238
+ },
239
+ children: [fileState.status === "error" && /* @__PURE__ */jsx(Button, {
240
+ variant: "destructive",
241
+ onClick: () => {
242
+ return retryUpload(index);
243
+ },
244
+ sx: {
245
+ fontSize: "xs"
246
+ },
247
+ children: "Retry"
248
+ }), /* @__PURE__ */jsx(Button, {
249
+ variant: "destructive",
250
+ onClick: () => {
251
+ return handleRemoveFile(index);
252
+ },
253
+ sx: {
254
+ fontSize: "sm",
255
+ color: "text.muted",
256
+ "&:hover": {
257
+ color: "error.default"
258
+ }
259
+ },
260
+ children: "Remove"
261
+ })]
262
+ })]
263
+ }, index);
264
+ })
265
+ });
266
+ }, [FileListComponent, files, handleRemoveFile, retryUpload, showFileList]);
267
+ return /* @__PURE__ */jsxs(Stack, {
268
+ sx: {
269
+ gap: 3
270
+ },
271
+ children: [/* @__PURE__ */jsxs(Box, {
272
+ onDrop: handleDrop,
273
+ onDragOver: handleDragOver,
274
+ onDragLeave: handleDragLeave,
275
+ onClick: handleClick,
276
+ sx: {
277
+ border: "2px dashed",
278
+ borderColor: error ? "error.default" : isDragOver ? "primary.default" : "display.border.muted.default",
279
+ borderRadius: "xl",
280
+ padding: 6,
281
+ textAlign: "center",
282
+ cursor: disabled || isUploading ? "not-allowed" : "pointer",
283
+ backgroundColor: isDragOver ? "primary.muted" : "display.background.secondary.default",
284
+ transition: "all 0.2s ease",
285
+ opacity: disabled ? 0.6 : 1,
286
+ "&:hover": {
287
+ borderColor: !disabled && !isUploading && !error ? "primary.default" : void 0,
288
+ backgroundColor: !disabled && !isUploading ? "primary.muted" : void 0
289
+ }
290
+ },
291
+ children: [/* @__PURE__ */jsx("input", {
292
+ ref: fileInputRef,
293
+ type: "file",
294
+ accept,
295
+ multiple,
296
+ onChange: handleFileChange,
297
+ disabled: disabled || isUploading,
298
+ style: {
299
+ display: "none"
300
+ }
301
+ }), children || /* @__PURE__ */jsxs(Flex, {
302
+ sx: {
303
+ flexDirection: "column",
304
+ alignItems: "center",
305
+ gap: 3,
306
+ justifyContent: "center"
307
+ },
308
+ children: [/* @__PURE__ */jsx(Text, {
309
+ sx: {
310
+ fontSize: "3xl"
311
+ },
312
+ children: "\u{1F4C1}"
313
+ }), /* @__PURE__ */jsxs(Box, {
314
+ sx: {
315
+ textAlign: "center"
316
+ },
317
+ children: [/* @__PURE__ */jsx(Text, {
318
+ variant: "body",
319
+ sx: {
320
+ color: "text.default",
321
+ mb: 1
322
+ },
323
+ children: isUploading ? /* @__PURE__ */jsx(FormattedMessage, {
324
+ defaultMessage: "Uploading..."
325
+ }) : placeholder || /* @__PURE__ */jsx(FormattedMessage, {
326
+ defaultMessage: "Click or drag files here"
327
+ })
328
+ }), /* @__PURE__ */jsx(Text, {
329
+ variant: "caption",
330
+ sx: {
331
+ color: "text.muted"
332
+ },
333
+ children: [accept && accept, maxSize && `Max ${formatFileSize(maxSize)}`, multiple && maxFiles && `Up to ${maxFiles} files`].filter(Boolean).join(" \u2022 ")
334
+ })]
335
+ }), !isUploading && /* @__PURE__ */jsx(Button, {
336
+ variant: "secondary",
337
+ disabled,
338
+ children: /* @__PURE__ */jsx(FormattedMessage, {
339
+ defaultMessage: "Select Files"
340
+ })
341
+ })]
342
+ })]
343
+ }), error && /* @__PURE__ */jsx(Text, {
344
+ variant: "caption",
345
+ sx: {
346
+ color: "error.default"
347
+ },
348
+ children: error
349
+ }), fileListNode]
350
+ });
351
+ };
352
+ export { FileUploader };