@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,626 @@
1
+ import type React from "react";
2
+ import type {
3
+ UploadItem,
4
+ UseMultiUploadReturn,
5
+ } from "../hooks/use-multi-upload";
6
+ import type { UploadStatus } from "../hooks/use-upload";
7
+
8
+ /**
9
+ * Render props passed to the UploadList children function.
10
+ * Provides organized access to upload items, status groupings, and actions.
11
+ *
12
+ * @property items - All upload items (filtered and sorted if configured)
13
+ * @property itemsByStatus - Upload items grouped by their current status
14
+ * @property multiUpload - Complete multi-upload hook instance
15
+ * @property actions - Helper functions for common item operations
16
+ * @property actions.removeItem - Remove an item from the list
17
+ * @property actions.retryItem - Retry a failed upload
18
+ * @property actions.abortItem - Cancel an active upload
19
+ * @property actions.startItem - Begin uploading an idle item
20
+ */
21
+ export interface UploadListRenderProps {
22
+ /**
23
+ * All upload items
24
+ */
25
+ items: UploadItem[];
26
+
27
+ /**
28
+ * Items filtered by status
29
+ */
30
+ itemsByStatus: {
31
+ idle: UploadItem[];
32
+ uploading: UploadItem[];
33
+ success: UploadItem[];
34
+ error: UploadItem[];
35
+ aborted: UploadItem[];
36
+ };
37
+
38
+ /**
39
+ * Multi-upload state and controls
40
+ */
41
+ multiUpload: UseMultiUploadReturn;
42
+
43
+ /**
44
+ * Helper functions for item management
45
+ */
46
+ actions: {
47
+ removeItem: (id: string) => void;
48
+ retryItem: (item: UploadItem) => void;
49
+ abortItem: (item: UploadItem) => void;
50
+ startItem: (item: UploadItem) => void;
51
+ };
52
+ }
53
+
54
+ /**
55
+ * Props for the UploadList component.
56
+ *
57
+ * @property multiUpload - Multi-upload hook instance to display
58
+ * @property filter - Optional function to filter which items to show
59
+ * @property sortBy - Optional comparator function to sort items
60
+ * @property children - Render function receiving list state and actions
61
+ */
62
+ export interface UploadListProps {
63
+ /**
64
+ * Multi-upload instance from useMultiUpload hook
65
+ */
66
+ multiUpload: UseMultiUploadReturn;
67
+
68
+ /**
69
+ * Optional filter for which items to display
70
+ */
71
+ filter?: (item: UploadItem) => boolean;
72
+
73
+ /**
74
+ * Optional sorting function for items
75
+ */
76
+ sortBy?: (a: UploadItem, b: UploadItem) => number;
77
+
78
+ /**
79
+ * Render prop that receives upload list state and actions
80
+ */
81
+ children: (props: UploadListRenderProps) => React.ReactNode;
82
+ }
83
+
84
+ /**
85
+ * Headless upload list component that provides flexible rendering for upload items.
86
+ * Uses render props pattern to give full control over how upload items are displayed.
87
+ *
88
+ * @param props - Upload list configuration and render prop
89
+ * @returns Rendered upload list using the provided render prop
90
+ *
91
+ * @example
92
+ * ```tsx
93
+ * // Basic upload list with progress bars
94
+ * <UploadList multiUpload={multiUpload}>
95
+ * {({ items, actions }) => (
96
+ * <div>
97
+ * <h3>Upload Queue ({items.length} files)</h3>
98
+ * {items.map((item) => (
99
+ * <div key={item.id} style={{
100
+ * padding: '1rem',
101
+ * border: '1px solid #ccc',
102
+ * marginBottom: '0.5rem',
103
+ * borderRadius: '4px'
104
+ * }}>
105
+ * <div style={{ display: 'flex', justifyContent: 'space-between' }}>
106
+ * <span>{item.file.name}</span>
107
+ * <span>{item.state.status}</span>
108
+ * </div>
109
+ *
110
+ * {item.state.status === 'uploading' && (
111
+ * <div>
112
+ * <progress value={item.state.progress} max={100} />
113
+ * <span>{item.state.progress}%</span>
114
+ * <button onClick={() => actions.abortItem(item)}>Cancel</button>
115
+ * </div>
116
+ * )}
117
+ *
118
+ * {item.state.status === 'error' && (
119
+ * <div>
120
+ * <p style={{ color: 'red' }}>Error: {item.state.error?.message}</p>
121
+ * <button onClick={() => actions.retryItem(item)}>Retry</button>
122
+ * <button onClick={() => actions.removeItem(item.id)}>Remove</button>
123
+ * </div>
124
+ * )}
125
+ *
126
+ * {item.state.status === 'success' && (
127
+ * <div>
128
+ * <p style={{ color: 'green' }}>✓ Uploaded successfully</p>
129
+ * <button onClick={() => actions.removeItem(item.id)}>Remove</button>
130
+ * </div>
131
+ * )}
132
+ *
133
+ * {item.state.status === 'idle' && (
134
+ * <div>
135
+ * <button onClick={() => actions.startItem(item)}>Start Upload</button>
136
+ * <button onClick={() => actions.removeItem(item.id)}>Remove</button>
137
+ * </div>
138
+ * )}
139
+ * </div>
140
+ * ))}
141
+ * </div>
142
+ * )}
143
+ * </UploadList>
144
+ *
145
+ * // Upload list with status filtering and sorting
146
+ * <UploadList
147
+ * multiUpload={multiUpload}
148
+ * filter={(item) => item.state.status !== 'success'} // Hide successful uploads
149
+ * sortBy={(a, b) => {
150
+ * // Sort by status priority, then by filename
151
+ * const statusPriority = { error: 0, uploading: 1, idle: 2, success: 3, aborted: 4 };
152
+ * const aPriority = statusPriority[a.state.status];
153
+ * const bPriority = statusPriority[b.state.status];
154
+ *
155
+ * if (aPriority !== bPriority) {
156
+ * return aPriority - bPriority;
157
+ * }
158
+ *
159
+ * return a.file.name.localeCompare(b.file.name);
160
+ * }}
161
+ * >
162
+ * {({ items, itemsByStatus, multiUpload, actions }) => (
163
+ * <div>
164
+ * {itemsByStatus.error.length > 0 && (
165
+ * <div>
166
+ * <h4 style={{ color: 'red' }}>Failed Uploads ({itemsByStatus.error.length})</h4>
167
+ * {itemsByStatus.error.map((item) => (
168
+ * <UploadListItem key={item.id} item={item} actions={actions} />
169
+ * ))}
170
+ * </div>
171
+ * )}
172
+ *
173
+ * {itemsByStatus.uploading.length > 0 && (
174
+ * <div>
175
+ * <h4>Uploading ({itemsByStatus.uploading.length})</h4>
176
+ * {itemsByStatus.uploading.map((item) => (
177
+ * <UploadListItem key={item.id} item={item} actions={actions} />
178
+ * ))}
179
+ * </div>
180
+ * )}
181
+ *
182
+ * {itemsByStatus.idle.length > 0 && (
183
+ * <div>
184
+ * <h4>Pending ({itemsByStatus.idle.length})</h4>
185
+ * {itemsByStatus.idle.map((item) => (
186
+ * <UploadListItem key={item.id} item={item} actions={actions} />
187
+ * ))}
188
+ * </div>
189
+ * )}
190
+ * </div>
191
+ * )}
192
+ * </UploadList>
193
+ * ```
194
+ */
195
+ export function UploadList({
196
+ multiUpload,
197
+ filter,
198
+ sortBy,
199
+ children,
200
+ }: UploadListProps) {
201
+ // Apply filtering
202
+ let items = multiUpload.items;
203
+ if (filter) {
204
+ items = items.filter(filter);
205
+ }
206
+
207
+ // Apply sorting
208
+ if (sortBy) {
209
+ items = [...items].sort(sortBy);
210
+ }
211
+
212
+ // Group items by status
213
+ const itemsByStatus = {
214
+ idle: items.filter((item) => item.state.status === "idle"),
215
+ uploading: items.filter((item) => item.state.status === "uploading"),
216
+ success: items.filter((item) => item.state.status === "success"),
217
+ error: items.filter((item) => item.state.status === "error"),
218
+ aborted: items.filter((item) => item.state.status === "aborted"),
219
+ };
220
+
221
+ // Create action helpers
222
+ const actions = {
223
+ removeItem: (id: string) => {
224
+ multiUpload.removeItem(id);
225
+ },
226
+ retryItem: (_item: UploadItem) => {
227
+ // Retry failed uploads using multiUpload method
228
+ multiUpload.retryFailed();
229
+ },
230
+ abortItem: (item: UploadItem) => {
231
+ // Remove the item to effectively abort it
232
+ multiUpload.removeItem(item.id);
233
+ },
234
+ startItem: (_item: UploadItem) => {
235
+ // Start all pending uploads
236
+ multiUpload.startAll();
237
+ },
238
+ };
239
+
240
+ // Create render props object
241
+ const renderProps: UploadListRenderProps = {
242
+ items,
243
+ itemsByStatus,
244
+ multiUpload,
245
+ actions,
246
+ };
247
+
248
+ return <>{children(renderProps)}</>;
249
+ }
250
+
251
+ /**
252
+ * Props for the SimpleUploadListItem component.
253
+ *
254
+ * @property item - The upload item to display
255
+ * @property actions - Action functions from UploadList render props
256
+ * @property className - Additional CSS class name
257
+ * @property style - Inline styles for the item container
258
+ * @property showDetails - Whether to display file size and upload details
259
+ */
260
+ export interface SimpleUploadListItemProps {
261
+ /**
262
+ * The upload item to render
263
+ */
264
+ item: UploadItem;
265
+
266
+ /**
267
+ * Actions from UploadList render props
268
+ */
269
+ actions: UploadListRenderProps["actions"];
270
+
271
+ /**
272
+ * Additional CSS class name
273
+ */
274
+ className?: string;
275
+
276
+ /**
277
+ * Inline styles
278
+ */
279
+ style?: React.CSSProperties;
280
+
281
+ /**
282
+ * Whether to show detailed information (file size, speed, etc.)
283
+ */
284
+ showDetails?: boolean;
285
+ }
286
+
287
+ /**
288
+ * Pre-styled upload list item component with status indicators and action buttons.
289
+ * Displays file info, progress, errors, and contextual actions based on upload status.
290
+ *
291
+ * Features:
292
+ * - Status-specific color coding and icons
293
+ * - Progress bar for active uploads
294
+ * - Error message display
295
+ * - File size formatting
296
+ * - Contextual action buttons (start, cancel, retry, remove)
297
+ *
298
+ * @param props - Upload item and configuration
299
+ * @returns Styled upload list item component
300
+ *
301
+ * @example
302
+ * ```tsx
303
+ * // Use with UploadList
304
+ * <UploadList multiUpload={multiUpload}>
305
+ * {({ items, actions }) => (
306
+ * <div>
307
+ * {items.map((item) => (
308
+ * <SimpleUploadListItem
309
+ * key={item.id}
310
+ * item={item}
311
+ * actions={actions}
312
+ * showDetails={true}
313
+ * />
314
+ * ))}
315
+ * </div>
316
+ * )}
317
+ * </UploadList>
318
+ *
319
+ * // Custom styling
320
+ * <SimpleUploadListItem
321
+ * item={uploadItem}
322
+ * actions={actions}
323
+ * className="my-upload-item"
324
+ * style={{ borderRadius: '12px', margin: '1rem' }}
325
+ * showDetails={true}
326
+ * />
327
+ * ```
328
+ */
329
+ export function SimpleUploadListItem({
330
+ item,
331
+ actions,
332
+ className = "",
333
+ style = {},
334
+ showDetails = true,
335
+ }: SimpleUploadListItemProps) {
336
+ const getStatusColor = (status: UploadStatus) => {
337
+ switch (status) {
338
+ case "idle":
339
+ return "#6c757d";
340
+ case "uploading":
341
+ return "#007bff";
342
+ case "success":
343
+ return "#28a745";
344
+ case "error":
345
+ return "#dc3545";
346
+ case "aborted":
347
+ return "#6c757d";
348
+ default:
349
+ return "#6c757d";
350
+ }
351
+ };
352
+
353
+ const getStatusIcon = (status: UploadStatus) => {
354
+ switch (status) {
355
+ case "idle":
356
+ return "⏳";
357
+ case "uploading":
358
+ return "📤";
359
+ case "success":
360
+ return "✅";
361
+ case "error":
362
+ return "❌";
363
+ case "aborted":
364
+ return "⏹️";
365
+ default:
366
+ return "❓";
367
+ }
368
+ };
369
+
370
+ const formatFileSize = (bytes: number) => {
371
+ if (bytes === 0) return "0 Bytes";
372
+ const k = 1024;
373
+ const sizes = ["Bytes", "KB", "MB", "GB"];
374
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
375
+ return `${parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`;
376
+ };
377
+
378
+ return (
379
+ <div
380
+ className={`upload-list-item upload-list-item--${item.state.status} ${className}`}
381
+ style={{
382
+ padding: "12px",
383
+ border: "1px solid #e0e0e0",
384
+ borderRadius: "6px",
385
+ marginBottom: "8px",
386
+ backgroundColor: "#fff",
387
+ transition: "all 0.2s ease",
388
+ ...style,
389
+ }}
390
+ >
391
+ {/* Header with filename and status */}
392
+ <div
393
+ style={{
394
+ display: "flex",
395
+ justifyContent: "space-between",
396
+ alignItems: "center",
397
+ marginBottom: "8px",
398
+ }}
399
+ >
400
+ <div
401
+ style={{ display: "flex", alignItems: "center", gap: "8px", flex: 1 }}
402
+ >
403
+ <span style={{ fontSize: "16px" }}>
404
+ {getStatusIcon(item.state.status)}
405
+ </span>
406
+ <span style={{ fontWeight: "500", flex: 1 }}>
407
+ {item.file instanceof File ? item.file.name : "File"}
408
+ </span>
409
+ </div>
410
+ <span
411
+ style={{
412
+ fontSize: "12px",
413
+ color: getStatusColor(item.state.status),
414
+ fontWeight: "500",
415
+ textTransform: "uppercase",
416
+ }}
417
+ >
418
+ {item.state.status}
419
+ </span>
420
+ </div>
421
+
422
+ {/* Progress bar for uploading items */}
423
+ {item.state.status === "uploading" && (
424
+ <div style={{ marginBottom: "8px" }}>
425
+ <div
426
+ style={{
427
+ display: "flex",
428
+ justifyContent: "space-between",
429
+ alignItems: "center",
430
+ marginBottom: "4px",
431
+ }}
432
+ >
433
+ <span style={{ fontSize: "12px", color: "#666" }}>
434
+ {item.state.progress}%
435
+ </span>
436
+ {showDetails && item.state.totalBytes && (
437
+ <span style={{ fontSize: "12px", color: "#666" }}>
438
+ {formatFileSize(item.state.bytesUploaded)} /{" "}
439
+ {formatFileSize(item.state.totalBytes)}
440
+ </span>
441
+ )}
442
+ </div>
443
+ <div
444
+ style={{
445
+ width: "100%",
446
+ height: "6px",
447
+ backgroundColor: "#e0e0e0",
448
+ borderRadius: "3px",
449
+ overflow: "hidden",
450
+ }}
451
+ >
452
+ <div
453
+ style={{
454
+ width: `${item.state.progress}%`,
455
+ height: "100%",
456
+ backgroundColor: "#007bff",
457
+ transition: "width 0.2s ease",
458
+ }}
459
+ />
460
+ </div>
461
+ </div>
462
+ )}
463
+
464
+ {/* Details section */}
465
+ {showDetails && (
466
+ <div style={{ fontSize: "12px", color: "#666", marginBottom: "8px" }}>
467
+ {item.state.totalBytes && (
468
+ <span>{formatFileSize(item.state.totalBytes)}</span>
469
+ )}
470
+ {item.state.status === "uploading" && item.state.progress > 0 && (
471
+ <span> • Progress: {item.state.progress}%</span>
472
+ )}
473
+ {item.state.status === "error" && item.state.error && (
474
+ <div style={{ color: "#dc3545", marginTop: "4px" }}>
475
+ {item.state.error.message}
476
+ </div>
477
+ )}
478
+ </div>
479
+ )}
480
+
481
+ {/* Action buttons */}
482
+ <div style={{ display: "flex", gap: "8px", flexWrap: "wrap" }}>
483
+ {item.state.status === "idle" && (
484
+ <>
485
+ <button
486
+ type="button"
487
+ onClick={() => actions.startItem(item)}
488
+ style={{
489
+ padding: "4px 8px",
490
+ fontSize: "12px",
491
+ border: "1px solid #007bff",
492
+ backgroundColor: "#007bff",
493
+ color: "white",
494
+ borderRadius: "4px",
495
+ cursor: "pointer",
496
+ }}
497
+ >
498
+ Start
499
+ </button>
500
+ <button
501
+ type="button"
502
+ onClick={() => actions.removeItem(item.id)}
503
+ style={{
504
+ padding: "4px 8px",
505
+ fontSize: "12px",
506
+ border: "1px solid #6c757d",
507
+ backgroundColor: "transparent",
508
+ color: "#6c757d",
509
+ borderRadius: "4px",
510
+ cursor: "pointer",
511
+ }}
512
+ >
513
+ Remove
514
+ </button>
515
+ </>
516
+ )}
517
+
518
+ {item.state.status === "uploading" && (
519
+ <button
520
+ type="button"
521
+ onClick={() => actions.abortItem(item)}
522
+ style={{
523
+ padding: "4px 8px",
524
+ fontSize: "12px",
525
+ border: "1px solid #dc3545",
526
+ backgroundColor: "transparent",
527
+ color: "#dc3545",
528
+ borderRadius: "4px",
529
+ cursor: "pointer",
530
+ }}
531
+ >
532
+ Cancel
533
+ </button>
534
+ )}
535
+
536
+ {item.state.status === "error" && (
537
+ <>
538
+ <button
539
+ type="button"
540
+ onClick={() => actions.retryItem(item)}
541
+ style={{
542
+ padding: "4px 8px",
543
+ fontSize: "12px",
544
+ border: "1px solid #28a745",
545
+ backgroundColor: "#28a745",
546
+ color: "white",
547
+ borderRadius: "4px",
548
+ cursor: "pointer",
549
+ }}
550
+ >
551
+ Retry
552
+ </button>
553
+ <button
554
+ type="button"
555
+ onClick={() => actions.removeItem(item.id)}
556
+ style={{
557
+ padding: "4px 8px",
558
+ fontSize: "12px",
559
+ border: "1px solid #6c757d",
560
+ backgroundColor: "transparent",
561
+ color: "#6c757d",
562
+ borderRadius: "4px",
563
+ cursor: "pointer",
564
+ }}
565
+ >
566
+ Remove
567
+ </button>
568
+ </>
569
+ )}
570
+
571
+ {item.state.status === "success" && (
572
+ <button
573
+ type="button"
574
+ onClick={() => actions.removeItem(item.id)}
575
+ style={{
576
+ padding: "4px 8px",
577
+ fontSize: "12px",
578
+ border: "1px solid #6c757d",
579
+ backgroundColor: "transparent",
580
+ color: "#6c757d",
581
+ borderRadius: "4px",
582
+ cursor: "pointer",
583
+ }}
584
+ >
585
+ Remove
586
+ </button>
587
+ )}
588
+
589
+ {item.state.status === "aborted" && (
590
+ <>
591
+ <button
592
+ type="button"
593
+ onClick={() => actions.retryItem(item)}
594
+ style={{
595
+ padding: "4px 8px",
596
+ fontSize: "12px",
597
+ border: "1px solid #007bff",
598
+ backgroundColor: "#007bff",
599
+ color: "white",
600
+ borderRadius: "4px",
601
+ cursor: "pointer",
602
+ }}
603
+ >
604
+ Retry
605
+ </button>
606
+ <button
607
+ type="button"
608
+ onClick={() => actions.removeItem(item.id)}
609
+ style={{
610
+ padding: "4px 8px",
611
+ fontSize: "12px",
612
+ border: "1px solid #6c757d",
613
+ backgroundColor: "transparent",
614
+ color: "#6c757d",
615
+ borderRadius: "4px",
616
+ cursor: "pointer",
617
+ }}
618
+ >
619
+ Remove
620
+ </button>
621
+ </>
622
+ )}
623
+ </div>
624
+ </div>
625
+ );
626
+ }