@uploadista/react 0.0.3

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,545 @@
1
+ /**
2
+ * Upload Zone Components
3
+ *
4
+ * Enhanced error handling features:
5
+ * - MIME type validation with detailed error messages
6
+ * - File count validation (single vs multiple mode)
7
+ * - Custom validation error callbacks
8
+ * - Built-in error display in SimpleUploadZone
9
+ * - Configurable error styling
10
+ */
11
+
12
+ import type React from "react";
13
+ import { useCallback } from "react";
14
+ import type {
15
+ DragDropOptions,
16
+ UseDragDropReturn,
17
+ } from "../hooks/use-drag-drop";
18
+ import { useDragDrop } from "../hooks/use-drag-drop";
19
+ import type {
20
+ MultiUploadOptions,
21
+ UseMultiUploadReturn,
22
+ } from "../hooks/use-multi-upload";
23
+ import { useMultiUpload } from "../hooks/use-multi-upload";
24
+ import type { UseUploadOptions, UseUploadReturn } from "../hooks/use-upload";
25
+ import { useUpload } from "../hooks/use-upload";
26
+
27
+ /**
28
+ * Render props passed to the UploadZone children function.
29
+ * Provides access to drag-drop state, upload controls, and helper functions.
30
+ *
31
+ * @property dragDrop - Complete drag-and-drop state and event handlers
32
+ * @property upload - Single upload hook (null when multiple=true)
33
+ * @property multiUpload - Multi-upload hook (null when multiple=false)
34
+ * @property openFilePicker - Programmatically trigger file selection dialog
35
+ * @property isActive - True when dragging over zone or files selected
36
+ * @property isProcessing - True when uploads are in progress
37
+ */
38
+ export interface UploadZoneRenderProps {
39
+ /**
40
+ * Drag and drop state and handlers
41
+ */
42
+ dragDrop: UseDragDropReturn;
43
+
44
+ /**
45
+ * Single upload functionality (if not using multi-upload)
46
+ */
47
+ upload: UseUploadReturn | null;
48
+
49
+ /**
50
+ * Multi-upload functionality (if using multi-upload)
51
+ */
52
+ multiUpload: UseMultiUploadReturn | null;
53
+
54
+ /**
55
+ * Helper function to open file picker
56
+ */
57
+ openFilePicker: () => void;
58
+
59
+ /**
60
+ * Whether the zone is currently active (dragging or uploading)
61
+ */
62
+ isActive: boolean;
63
+
64
+ /**
65
+ * Whether files are being processed
66
+ */
67
+ isProcessing: boolean;
68
+ }
69
+
70
+ /**
71
+ * Props for the UploadZone component.
72
+ * Combines drag-drop options with upload configuration.
73
+ *
74
+ * @property multiple - Enable multi-file selection and upload (default: true)
75
+ * @property multiUploadOptions - Configuration for multi-upload mode
76
+ * @property uploadOptions - Configuration for single-upload mode
77
+ * @property children - Render function receiving upload zone state
78
+ * @property onUploadStart - Called when files pass validation and upload begins
79
+ * @property onValidationError - Called when file validation fails
80
+ * @property accept - Accepted file types (e.g., ['image/*', '.pdf'])
81
+ * @property maxFiles - Maximum number of files allowed
82
+ * @property maxFileSize - Maximum file size in bytes
83
+ * @property validator - Custom validation function
84
+ */
85
+ export interface UploadZoneProps
86
+ extends Omit<DragDropOptions, "onFilesReceived"> {
87
+ /**
88
+ * Whether to enable multi-file upload mode
89
+ */
90
+ multiple?: boolean;
91
+
92
+ /**
93
+ * Multi-upload specific options (only used when multiple=true)
94
+ */
95
+ multiUploadOptions?: MultiUploadOptions;
96
+
97
+ /**
98
+ * Single upload specific options (only used when multiple=false)
99
+ */
100
+ uploadOptions?: UseUploadOptions;
101
+
102
+ /**
103
+ * Render prop that receives upload zone state and handlers
104
+ */
105
+ children: (props: UploadZoneRenderProps) => React.ReactNode;
106
+
107
+ /**
108
+ * Called when files are processed and uploads begin
109
+ */
110
+ onUploadStart?: (files: File[]) => void;
111
+
112
+ /**
113
+ * Called when validation errors occur
114
+ */
115
+ onValidationError?: (errors: string[]) => void;
116
+ }
117
+
118
+ /**
119
+ * Headless upload zone component that combines drag and drop functionality
120
+ * with upload management. Uses render props pattern for maximum flexibility.
121
+ * Includes enhanced error handling for MIME type validation and file count validation.
122
+ *
123
+ * @param props - Upload zone configuration and render prop
124
+ * @returns Rendered upload zone using the provided render prop
125
+ *
126
+ * @example
127
+ * ```tsx
128
+ * // Single file upload zone with error handling
129
+ * <UploadZone
130
+ * multiple={false}
131
+ * accept={['image/*']}
132
+ * maxFileSize={5 * 1024 * 1024}
133
+ * onValidationError={(errors) => {
134
+ * console.error('Validation errors:', errors);
135
+ * }}
136
+ * uploadOptions={{
137
+ * onSuccess: (result) => console.log('Upload complete:', result),
138
+ * onError: (error) => console.error('Upload failed:', error),
139
+ * }}
140
+ * >
141
+ * {({ dragDrop, upload, openFilePicker, isActive }) => (
142
+ * <div {...dragDrop.dragHandlers} onClick={openFilePicker}>
143
+ * {dragDrop.state.isDragging ? (
144
+ * <p>Drop file here...</p>
145
+ * ) : upload?.isUploading ? (
146
+ * <p>Uploading... {upload.state.progress}%</p>
147
+ * ) : (
148
+ * <p>Drag a file here or click to select</p>
149
+ * )}
150
+ *
151
+ * {dragDrop.state.errors.length > 0 && (
152
+ * <div style={{ color: 'red' }}>
153
+ * {dragDrop.state.errors.map((error, index) => (
154
+ * <p key={index}>{error}</p>
155
+ * ))}
156
+ * </div>
157
+ * )}
158
+ *
159
+ * <input {...dragDrop.inputProps} />
160
+ * </div>
161
+ * )}
162
+ * </UploadZone>
163
+ * ```
164
+ */
165
+ export function UploadZone({
166
+ children,
167
+ multiple = true,
168
+ multiUploadOptions = {},
169
+ uploadOptions = {},
170
+ onUploadStart,
171
+ onValidationError,
172
+ ...dragDropOptions
173
+ }: UploadZoneProps) {
174
+ // Always initialize both hooks, but only use the appropriate one
175
+ const singleUpload = useUpload(uploadOptions);
176
+ const multiUpload = useMultiUpload(multiUploadOptions);
177
+
178
+ // Enhanced validation function for better error handling
179
+ const enhancedValidator = useCallback(
180
+ (files: File[]): string[] | null => {
181
+ const errors: string[] = [];
182
+
183
+ // Check file count based on multiple setting
184
+ if (!multiple && files.length > 1) {
185
+ errors.push(
186
+ `Single file mode is enabled. Please select only one file. You selected ${files.length} files.`,
187
+ );
188
+ }
189
+
190
+ // Enhanced MIME type validation with better error messages
191
+ if (dragDropOptions.accept && dragDropOptions.accept.length > 0) {
192
+ const invalidFiles = files.filter((file) => {
193
+ return !dragDropOptions.accept?.some((acceptType) => {
194
+ if (acceptType.startsWith(".")) {
195
+ // File extension check
196
+ return file.name.toLowerCase().endsWith(acceptType.toLowerCase());
197
+ } else {
198
+ // MIME type check (supports wildcards like image/*)
199
+ if (acceptType.endsWith("/*")) {
200
+ const baseType = acceptType.slice(0, -2);
201
+ return file.type.startsWith(baseType);
202
+ } else {
203
+ return file.type === acceptType;
204
+ }
205
+ }
206
+ });
207
+ });
208
+
209
+ if (invalidFiles.length > 0) {
210
+ const fileNames = invalidFiles
211
+ .map((f) => `"${f.name}" (${f.type})`)
212
+ .join(", ");
213
+ const acceptedTypes = dragDropOptions.accept.join(", ");
214
+ errors.push(
215
+ `Invalid file type(s): ${fileNames}. Accepted types: ${acceptedTypes}.`,
216
+ );
217
+ }
218
+ }
219
+
220
+ return errors.length > 0 ? errors : null;
221
+ },
222
+ [multiple, dragDropOptions.accept],
223
+ );
224
+
225
+ // Handle file processing
226
+ const handleFilesReceived = (files: File[]) => {
227
+ onUploadStart?.(files);
228
+
229
+ if (multiple && multiUpload) {
230
+ // Add files to multi-upload queue
231
+ multiUpload.addFiles(files);
232
+
233
+ // Auto-start uploads if configured to do so
234
+ // Note: This could be made configurable with an autoStart prop
235
+ setTimeout(() => multiUpload.startAll(), 0);
236
+ } else if (!multiple && singleUpload && files.length > 0 && files[0]) {
237
+ // Start single file upload
238
+ singleUpload.upload(files[0]);
239
+ }
240
+ };
241
+
242
+ // Handle validation errors
243
+ const handleValidationError = useCallback(
244
+ (errors: string[]) => {
245
+ console.error("Upload zone validation errors:", errors);
246
+ // Call the custom error handler if provided
247
+ onValidationError?.(errors);
248
+ },
249
+ [onValidationError],
250
+ );
251
+
252
+ // Initialize drag and drop with enhanced validation
253
+ const dragDrop = useDragDrop({
254
+ ...dragDropOptions,
255
+ multiple,
256
+ validator: enhancedValidator,
257
+ onFilesReceived: handleFilesReceived,
258
+ onValidationError: handleValidationError,
259
+ });
260
+
261
+ // Determine active state
262
+ const isActive = dragDrop.state.isDragging || dragDrop.state.isOver;
263
+
264
+ // Determine processing state
265
+ const isProcessing = multiple
266
+ ? (multiUpload?.state.isUploading ?? false)
267
+ : (singleUpload?.isUploading ?? false);
268
+
269
+ // Create render props object
270
+ const renderProps: UploadZoneRenderProps = {
271
+ dragDrop,
272
+ upload: singleUpload,
273
+ multiUpload,
274
+ openFilePicker: dragDrop.openFilePicker,
275
+ isActive,
276
+ isProcessing,
277
+ };
278
+
279
+ return <>{children(renderProps)}</>;
280
+ }
281
+
282
+ /**
283
+ * Props for the SimpleUploadZone component with built-in styling.
284
+ *
285
+ * @property className - CSS class name for custom styling
286
+ * @property style - Inline styles for the upload zone container
287
+ * @property text - Custom text labels for different states
288
+ * @property text.idle - Text shown when zone is idle
289
+ * @property text.dragging - Text shown when dragging files over zone
290
+ * @property text.uploading - Text shown during upload
291
+ * @property errorStyle - Custom styles for validation error display
292
+ */
293
+ export interface SimpleUploadZoneProps extends UploadZoneProps {
294
+ /**
295
+ * Additional CSS class name for styling
296
+ */
297
+ className?: string;
298
+
299
+ /**
300
+ * Inline styles for the upload zone
301
+ */
302
+ style?: React.CSSProperties;
303
+
304
+ /**
305
+ * Custom text to display in different states
306
+ */
307
+ text?: {
308
+ idle?: string;
309
+ dragging?: string;
310
+ uploading?: string;
311
+ };
312
+
313
+ /**
314
+ * Custom error message styling
315
+ */
316
+ errorStyle?: React.CSSProperties;
317
+ }
318
+
319
+ /**
320
+ * Simple pre-styled upload zone component with built-in UI and error handling.
321
+ * Provides a ready-to-use drag-and-drop upload interface with minimal configuration.
322
+ *
323
+ * Features:
324
+ * - Built-in drag-and-drop visual feedback
325
+ * - Automatic progress display
326
+ * - File validation error display
327
+ * - Customizable text and styling
328
+ * - Keyboard accessible
329
+ *
330
+ * @param props - Upload zone configuration with styling options
331
+ * @returns Styled upload zone component
332
+ *
333
+ * @example
334
+ * ```tsx
335
+ * // Multi-file upload with validation
336
+ * <SimpleUploadZone
337
+ * multiple={true}
338
+ * accept={['image/*', '.pdf']}
339
+ * maxFiles={5}
340
+ * maxFileSize={10 * 1024 * 1024} // 10MB
341
+ * onUploadStart={(files) => console.log('Starting uploads:', files.length)}
342
+ * onValidationError={(errors) => {
343
+ * errors.forEach(err => console.error(err));
344
+ * }}
345
+ * multiUploadOptions={{
346
+ * maxConcurrent: 3,
347
+ * onComplete: (results) => {
348
+ * console.log(`${results.successful.length}/${results.total} uploaded`);
349
+ * },
350
+ * }}
351
+ * style={{
352
+ * width: '400px',
353
+ * height: '200px',
354
+ * margin: '20px auto',
355
+ * }}
356
+ * text={{
357
+ * idle: 'Drop your files here or click to browse',
358
+ * dragging: 'Release to upload',
359
+ * uploading: 'Uploading files...',
360
+ * }}
361
+ * errorStyle={{
362
+ * backgroundColor: '#fff3cd',
363
+ * borderColor: '#ffeaa7',
364
+ * }}
365
+ * />
366
+ *
367
+ * // Single file upload
368
+ * <SimpleUploadZone
369
+ * multiple={false}
370
+ * accept={['image/*']}
371
+ * uploadOptions={{
372
+ * onSuccess: (result) => console.log('Uploaded:', result),
373
+ * onError: (error) => console.error('Failed:', error),
374
+ * }}
375
+ * text={{
376
+ * idle: 'Click or drag an image to upload',
377
+ * }}
378
+ * />
379
+ * ```
380
+ */
381
+ export function SimpleUploadZone({
382
+ className = "",
383
+ style = {},
384
+ text = {},
385
+ errorStyle = {},
386
+ children,
387
+ ...uploadZoneProps
388
+ }: SimpleUploadZoneProps) {
389
+ const defaultText = {
390
+ idle: uploadZoneProps.multiple
391
+ ? "Drag files here or click to select"
392
+ : "Drag a file here or click to select",
393
+ dragging: uploadZoneProps.multiple
394
+ ? "Drop files here..."
395
+ : "Drop file here...",
396
+ uploading: "Uploading...",
397
+ };
398
+
399
+ const displayText = { ...defaultText, ...text };
400
+
401
+ // If children render prop is provided, use UploadZone directly
402
+ if (children) {
403
+ return <UploadZone {...uploadZoneProps}>{children}</UploadZone>;
404
+ }
405
+
406
+ // Otherwise, provide default UI
407
+ return (
408
+ <UploadZone {...uploadZoneProps}>
409
+ {({
410
+ dragDrop,
411
+ upload,
412
+ multiUpload,
413
+ openFilePicker,
414
+ isActive,
415
+ isProcessing,
416
+ }) => (
417
+ <button
418
+ type="button"
419
+ onKeyDown={(e) => {
420
+ if (e.key === "Enter" || e.key === " ") {
421
+ openFilePicker();
422
+ }
423
+ }}
424
+ onKeyUp={(e) => {
425
+ if (e.key === "Enter" || e.key === " ") {
426
+ openFilePicker();
427
+ }
428
+ }}
429
+ {...dragDrop.dragHandlers}
430
+ onClick={openFilePicker}
431
+ className={`upload-zone ${isActive ? "upload-zone--active" : ""} ${isProcessing ? "upload-zone--processing" : ""} ${className}`}
432
+ style={{
433
+ border: isActive ? "2px dashed #007bff" : "2px dashed #ccc",
434
+ borderRadius: "8px",
435
+ padding: "2rem",
436
+ textAlign: "center",
437
+ cursor: "pointer",
438
+ backgroundColor: isActive ? "#f8f9fa" : "transparent",
439
+ transition: "all 0.2s ease",
440
+ minHeight: "120px",
441
+ display: "flex",
442
+ flexDirection: "column",
443
+ alignItems: "center",
444
+ justifyContent: "center",
445
+ ...style,
446
+ }}
447
+ >
448
+ {dragDrop.state.isDragging ? (
449
+ <p style={{ margin: 0, fontSize: "16px", color: "#007bff" }}>
450
+ {displayText.dragging}
451
+ </p>
452
+ ) : isProcessing ? (
453
+ <div style={{ textAlign: "center" }}>
454
+ <p style={{ margin: "0 0 10px 0", fontSize: "14px" }}>
455
+ {displayText.uploading}
456
+ </p>
457
+ {upload && (
458
+ <div>
459
+ <progress
460
+ value={upload.state.progress}
461
+ max={100}
462
+ style={{ width: "200px", height: "8px" }}
463
+ />
464
+ <p
465
+ style={{
466
+ margin: "5px 0 0 0",
467
+ fontSize: "12px",
468
+ color: "#666",
469
+ }}
470
+ >
471
+ {upload.state.progress}%
472
+ </p>
473
+ </div>
474
+ )}
475
+ {multiUpload && (
476
+ <div>
477
+ <progress
478
+ value={multiUpload.state.progress}
479
+ max={100}
480
+ style={{ width: "200px", height: "8px" }}
481
+ />
482
+ <p
483
+ style={{
484
+ margin: "5px 0 0 0",
485
+ fontSize: "12px",
486
+ color: "#666",
487
+ }}
488
+ >
489
+ {multiUpload.state.progress}% ({multiUpload.state.uploading}{" "}
490
+ uploading, {multiUpload.state.successful} completed)
491
+ </p>
492
+ </div>
493
+ )}
494
+ </div>
495
+ ) : (
496
+ <p style={{ margin: 0, fontSize: "16px", color: "#666" }}>
497
+ {displayText.idle}
498
+ </p>
499
+ )}
500
+
501
+ {dragDrop.state.errors.length > 0 && (
502
+ <div
503
+ style={{
504
+ marginTop: "10px",
505
+ padding: "8px 12px",
506
+ backgroundColor: "#f8d7da",
507
+ border: "1px solid #f5c6cb",
508
+ borderRadius: "4px",
509
+ maxWidth: "100%",
510
+ ...errorStyle,
511
+ }}
512
+ >
513
+ <p
514
+ style={{
515
+ margin: "0 0 5px 0",
516
+ fontSize: "12px",
517
+ fontWeight: "bold",
518
+ color: "#721c24",
519
+ }}
520
+ >
521
+ Validation Errors:
522
+ </p>
523
+ {dragDrop.state.errors.map((error, index) => (
524
+ <p
525
+ // biome-ignore lint/suspicious/noArrayIndexKey: index is used as key
526
+ key={index}
527
+ style={{
528
+ color: "#721c24",
529
+ fontSize: "11px",
530
+ margin: "2px 0",
531
+ lineHeight: "1.3",
532
+ }}
533
+ >
534
+ • {error}
535
+ </p>
536
+ ))}
537
+ </div>
538
+ )}
539
+
540
+ <input {...dragDrop.inputProps} />
541
+ </button>
542
+ )}
543
+ </UploadZone>
544
+ );
545
+ }