@uploadbox/react 0.1.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.
Files changed (52) hide show
  1. package/dist/generate-components.d.ts +5 -0
  2. package/dist/generate-components.d.ts.map +1 -0
  3. package/dist/generate-components.js +9 -0
  4. package/dist/generate-components.js.map +1 -0
  5. package/dist/index.d.ts +11 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +9 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/multipart.d.ts +25 -0
  10. package/dist/multipart.d.ts.map +1 -0
  11. package/dist/multipart.js +130 -0
  12. package/dist/multipart.js.map +1 -0
  13. package/dist/progress-tracker.d.ts +13 -0
  14. package/dist/progress-tracker.d.ts.map +1 -0
  15. package/dist/progress-tracker.js +93 -0
  16. package/dist/progress-tracker.js.map +1 -0
  17. package/dist/provider.d.ts +14 -0
  18. package/dist/provider.d.ts.map +1 -0
  19. package/dist/provider.js +12 -0
  20. package/dist/provider.js.map +1 -0
  21. package/dist/retry.d.ts +5 -0
  22. package/dist/retry.d.ts.map +1 -0
  23. package/dist/retry.js +47 -0
  24. package/dist/retry.js.map +1 -0
  25. package/dist/types.d.ts +86 -0
  26. package/dist/types.d.ts.map +1 -0
  27. package/dist/types.js +2 -0
  28. package/dist/types.js.map +1 -0
  29. package/dist/upload-button.d.ts +4 -0
  30. package/dist/upload-button.d.ts.map +1 -0
  31. package/dist/upload-button.js +25 -0
  32. package/dist/upload-button.js.map +1 -0
  33. package/dist/upload-dropzone.d.ts +4 -0
  34. package/dist/upload-dropzone.d.ts.map +1 -0
  35. package/dist/upload-dropzone.js +47 -0
  36. package/dist/upload-dropzone.js.map +1 -0
  37. package/dist/use-uploadbox.d.ts +4 -0
  38. package/dist/use-uploadbox.d.ts.map +1 -0
  39. package/dist/use-uploadbox.js +246 -0
  40. package/dist/use-uploadbox.js.map +1 -0
  41. package/package.json +56 -0
  42. package/src/generate-components.ts +20 -0
  43. package/src/index.ts +22 -0
  44. package/src/multipart.ts +189 -0
  45. package/src/progress-tracker.ts +107 -0
  46. package/src/provider.tsx +34 -0
  47. package/src/retry.ts +62 -0
  48. package/src/styles.css +126 -0
  49. package/src/types.ts +96 -0
  50. package/src/upload-button.tsx +76 -0
  51. package/src/upload-dropzone.tsx +138 -0
  52. package/src/use-uploadbox.ts +333 -0
@@ -0,0 +1,107 @@
1
+ import type { FileProgress, FileUploadStatus, EnhancedUploadProgressEvent } from "./types.js";
2
+
3
+ interface SpeedSample {
4
+ timestamp: number;
5
+ loaded: number;
6
+ }
7
+
8
+ const SPEED_WINDOW_MS = 3000;
9
+
10
+ export class ProgressTracker {
11
+ private files = new Map<string, FileProgress>();
12
+ private speedSamples = new Map<string, SpeedSample[]>();
13
+
14
+ init(fileId: string, name: string, size: number, type: string): void {
15
+ this.files.set(fileId, {
16
+ fileId,
17
+ name,
18
+ size,
19
+ type,
20
+ status: "pending",
21
+ loaded: 0,
22
+ percent: 0,
23
+ speed: 0,
24
+ eta: 0,
25
+ retryCount: 0,
26
+ });
27
+ this.speedSamples.set(fileId, []);
28
+ }
29
+
30
+ updateProgress(fileId: string, loaded: number): void {
31
+ const file = this.files.get(fileId);
32
+ if (!file) return;
33
+
34
+ file.loaded = loaded;
35
+ file.percent = file.size > 0 ? Math.round((loaded / file.size) * 100) : 0;
36
+ file.status = "uploading";
37
+
38
+ // Update speed samples
39
+ const samples = this.speedSamples.get(fileId) ?? [];
40
+ const now = Date.now();
41
+ samples.push({ timestamp: now, loaded });
42
+
43
+ // Prune old samples
44
+ const cutoff = now - SPEED_WINDOW_MS;
45
+ const validSamples = samples.filter((s) => s.timestamp >= cutoff);
46
+ this.speedSamples.set(fileId, validSamples);
47
+
48
+ // Calculate speed from sliding window
49
+ if (validSamples.length >= 2) {
50
+ const oldest = validSamples[0]!;
51
+ const newest = validSamples[validSamples.length - 1]!;
52
+ const timeDiff = (newest.timestamp - oldest.timestamp) / 1000;
53
+ if (timeDiff > 0) {
54
+ file.speed = (newest.loaded - oldest.loaded) / timeDiff;
55
+ const remaining = file.size - loaded;
56
+ file.eta = file.speed > 0 ? remaining / file.speed : 0;
57
+ }
58
+ }
59
+ }
60
+
61
+ setStatus(fileId: string, status: FileUploadStatus, error?: string): void {
62
+ const file = this.files.get(fileId);
63
+ if (!file) return;
64
+ file.status = status;
65
+ if (error) file.error = error;
66
+ if (status === "complete") {
67
+ file.loaded = file.size;
68
+ file.percent = 100;
69
+ file.eta = 0;
70
+ }
71
+ }
72
+
73
+ setKey(fileId: string, key: string): void {
74
+ const file = this.files.get(fileId);
75
+ if (file) file.key = key;
76
+ }
77
+
78
+ incrementRetry(fileId: string): void {
79
+ const file = this.files.get(fileId);
80
+ if (!file) return;
81
+ file.retryCount++;
82
+ file.status = "retrying";
83
+ file.loaded = 0;
84
+ file.percent = 0;
85
+ file.speed = 0;
86
+ file.eta = 0;
87
+ this.speedSamples.set(fileId, []);
88
+ }
89
+
90
+ getSnapshot(): EnhancedUploadProgressEvent {
91
+ const allFiles = Array.from(this.files.values());
92
+ const totalSize = allFiles.reduce((sum, f) => sum + f.size, 0);
93
+ const totalLoaded = allFiles.reduce((sum, f) => sum + f.loaded, 0);
94
+
95
+ return {
96
+ loaded: totalLoaded,
97
+ total: totalSize,
98
+ percent: totalSize > 0 ? Math.round((totalLoaded / totalSize) * 100) : 0,
99
+ fileProgress: allFiles.map((f) => ({ ...f })),
100
+ };
101
+ }
102
+
103
+ reset(): void {
104
+ this.files.clear();
105
+ this.speedSamples.clear();
106
+ }
107
+ }
@@ -0,0 +1,34 @@
1
+ "use client";
2
+
3
+ import React, { createContext, useContext, useMemo } from "react";
4
+
5
+ export interface UploadboxContextValue {
6
+ /** Base URL for the upload API. Defaults to "/api/uploadbox". */
7
+ apiUrl?: string;
8
+ /** API key for hosted mode — sent as Authorization: Bearer. */
9
+ apiKey?: string;
10
+ /** Extra headers to include with every request. */
11
+ headers?: Record<string, string>;
12
+ }
13
+
14
+ const UploadboxContext = createContext<UploadboxContextValue>({});
15
+
16
+ export function UploadboxProvider({
17
+ children,
18
+ ...config
19
+ }: UploadboxContextValue & { children: React.ReactNode }) {
20
+ const value = useMemo(
21
+ () => ({ apiUrl: config.apiUrl, apiKey: config.apiKey, headers: config.headers }),
22
+ [config.apiUrl, config.apiKey, config.headers]
23
+ );
24
+
25
+ return (
26
+ <UploadboxContext.Provider value={value}>
27
+ {children}
28
+ </UploadboxContext.Provider>
29
+ );
30
+ }
31
+
32
+ export function useUploadboxConfig(): UploadboxContextValue {
33
+ return useContext(UploadboxContext);
34
+ }
package/src/retry.ts ADDED
@@ -0,0 +1,62 @@
1
+ import type { RetryConfig } from "./types.js";
2
+
3
+ export const DEFAULT_RETRY_CONFIG: RetryConfig = {
4
+ maxRetries: 3,
5
+ initialDelayMs: 1000,
6
+ backoffMultiplier: 2,
7
+ maxDelayMs: 30000,
8
+ };
9
+
10
+ export function isRetryableError(status: number): boolean {
11
+ if (status === 0 || status === 408 || status === 429) return true;
12
+ return status >= 500 && status < 600;
13
+ }
14
+
15
+ export async function withRetry<T>(
16
+ fn: (attempt: number) => Promise<T>,
17
+ config: RetryConfig,
18
+ onRetry?: (attempt: number, error: Error) => void,
19
+ signal?: AbortSignal
20
+ ): Promise<T> {
21
+ let lastError: Error | undefined;
22
+
23
+ for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
24
+ if (signal?.aborted) {
25
+ throw new Error("Upload aborted");
26
+ }
27
+
28
+ try {
29
+ return await fn(attempt);
30
+ } catch (err) {
31
+ lastError = err instanceof Error ? err : new Error(String(err));
32
+
33
+ if (attempt >= config.maxRetries) break;
34
+
35
+ // Check if the error contains a retryable status
36
+ const statusMatch = lastError.message.match(/status (\d+)/);
37
+ if (statusMatch) {
38
+ const status = parseInt(statusMatch[1]!, 10);
39
+ if (!isRetryableError(status)) break;
40
+ }
41
+
42
+ const delay = Math.min(
43
+ config.initialDelayMs * Math.pow(config.backoffMultiplier, attempt),
44
+ config.maxDelayMs
45
+ );
46
+ // Add jitter: 0.5x to 1.5x of delay
47
+ const jitteredDelay = delay * (0.5 + Math.random());
48
+
49
+ onRetry?.(attempt + 1, lastError);
50
+
51
+ await new Promise<void>((resolve, reject) => {
52
+ const timeout = setTimeout(resolve, jitteredDelay);
53
+ signal?.addEventListener("abort", () => {
54
+ clearTimeout(timeout);
55
+ reject(new Error("Upload aborted"));
56
+ }, { once: true });
57
+ });
58
+ }
59
+ }
60
+
61
+ throw lastError ?? new Error("Retry failed");
62
+ }
package/src/styles.css ADDED
@@ -0,0 +1,126 @@
1
+ .uploadbox-button-wrapper {
2
+ display: inline-flex;
3
+ flex-direction: column;
4
+ align-items: center;
5
+ gap: 0.5rem;
6
+ }
7
+
8
+ .uploadbox-button {
9
+ display: inline-flex;
10
+ align-items: center;
11
+ justify-content: center;
12
+ padding: 0.5rem 1.25rem;
13
+ font-size: 0.875rem;
14
+ font-weight: 500;
15
+ line-height: 1.25rem;
16
+ color: #fff;
17
+ background-color: #2563eb;
18
+ border: none;
19
+ border-radius: 0.5rem;
20
+ cursor: pointer;
21
+ transition: background-color 0.15s ease;
22
+ min-width: 120px;
23
+ }
24
+
25
+ .uploadbox-button:hover:not(:disabled) {
26
+ background-color: #1d4ed8;
27
+ }
28
+
29
+ .uploadbox-button:disabled {
30
+ opacity: 0.5;
31
+ cursor: not-allowed;
32
+ }
33
+
34
+ .uploadbox-progress-bar {
35
+ width: 100%;
36
+ max-width: 200px;
37
+ height: 4px;
38
+ background-color: #e5e7eb;
39
+ border-radius: 2px;
40
+ overflow: hidden;
41
+ }
42
+
43
+ .uploadbox-progress-bar-fill {
44
+ height: 100%;
45
+ background-color: #2563eb;
46
+ border-radius: 2px;
47
+ transition: width 0.2s ease;
48
+ }
49
+
50
+ /* Dropzone */
51
+ .uploadbox-dropzone {
52
+ display: flex;
53
+ flex-direction: column;
54
+ align-items: center;
55
+ justify-content: center;
56
+ padding: 2rem;
57
+ border: 2px dashed #d1d5db;
58
+ border-radius: 0.75rem;
59
+ cursor: pointer;
60
+ transition: all 0.15s ease;
61
+ text-align: center;
62
+ min-height: 200px;
63
+ background-color: #fafafa;
64
+ }
65
+
66
+ .uploadbox-dropzone:hover {
67
+ border-color: #9ca3af;
68
+ background-color: #f5f5f5;
69
+ }
70
+
71
+ .uploadbox-dropzone--dragover {
72
+ border-color: #2563eb;
73
+ background-color: #eff6ff;
74
+ }
75
+
76
+ .uploadbox-dropzone--uploading {
77
+ border-color: #2563eb;
78
+ cursor: default;
79
+ }
80
+
81
+ .uploadbox-dropzone--complete {
82
+ border-color: #16a34a;
83
+ background-color: #f0fdf4;
84
+ }
85
+
86
+ .uploadbox-dropzone--error {
87
+ border-color: #dc2626;
88
+ background-color: #fef2f2;
89
+ }
90
+
91
+ .uploadbox-dropzone-content {
92
+ display: flex;
93
+ flex-direction: column;
94
+ align-items: center;
95
+ gap: 0.75rem;
96
+ }
97
+
98
+ .uploadbox-dropzone-icon {
99
+ color: #9ca3af;
100
+ }
101
+
102
+ .uploadbox-dropzone-label {
103
+ font-size: 0.875rem;
104
+ color: #6b7280;
105
+ margin: 0;
106
+ }
107
+
108
+ .uploadbox-dropzone-allowed {
109
+ font-size: 0.75rem;
110
+ color: #9ca3af;
111
+ margin: 0;
112
+ }
113
+
114
+ .uploadbox-dropzone-selected {
115
+ display: flex;
116
+ flex-direction: column;
117
+ align-items: center;
118
+ gap: 0.5rem;
119
+ }
120
+
121
+ .uploadbox-dropzone-selected p {
122
+ font-size: 0.8125rem;
123
+ color: #374151;
124
+ margin: 0;
125
+ font-weight: 500;
126
+ }
package/src/types.ts ADDED
@@ -0,0 +1,96 @@
1
+ import type { FileRouter, RouterConfig } from "@uploadbox/core";
2
+
3
+ export type EndpointHelper<TRouter extends FileRouter> = keyof TRouter & string;
4
+
5
+ export interface UploadProgressEvent {
6
+ loaded: number;
7
+ total: number;
8
+ percent: number;
9
+ }
10
+
11
+ export interface UploadedFile {
12
+ key: string;
13
+ name: string;
14
+ size: number;
15
+ type: string;
16
+ url: string;
17
+ customMetadata?: Record<string, string>;
18
+ }
19
+
20
+ export type FileUploadStatus = "pending" | "uploading" | "retrying" | "complete" | "error";
21
+
22
+ export interface FileProgress {
23
+ fileId: string;
24
+ name: string;
25
+ size: number;
26
+ type: string;
27
+ status: FileUploadStatus;
28
+ loaded: number;
29
+ percent: number;
30
+ speed: number;
31
+ eta: number;
32
+ error?: string;
33
+ retryCount: number;
34
+ key?: string;
35
+ }
36
+
37
+ export interface EnhancedUploadProgressEvent extends UploadProgressEvent {
38
+ fileProgress: FileProgress[];
39
+ }
40
+
41
+ export interface RetryConfig {
42
+ maxRetries: number;
43
+ initialDelayMs: number;
44
+ backoffMultiplier: number;
45
+ maxDelayMs: number;
46
+ }
47
+
48
+ export interface UseUploadboxOpts<TRouter extends FileRouter, TEndpoint extends keyof TRouter & string> {
49
+ endpoint: TEndpoint;
50
+ onClientUploadComplete?: (files: UploadedFile[]) => void;
51
+ onUploadError?: (error: Error) => void;
52
+ onUploadProgress?: (progress: EnhancedUploadProgressEvent) => void;
53
+ onBeforeUploadBegin?: (files: File[]) => File[];
54
+ headers?: Record<string, string>;
55
+ getFileMetadata?: (file: File) => Record<string, string>;
56
+ ttlSeconds?: number;
57
+ retry?: RetryConfig | false;
58
+ }
59
+
60
+ export interface UseUploadboxReturn {
61
+ startUpload: (files: File[]) => Promise<UploadedFile[] | undefined>;
62
+ isUploading: boolean;
63
+ progress: number;
64
+ routeConfig: RouterConfig[string] | undefined;
65
+ fileProgress: FileProgress[];
66
+ abort: () => void;
67
+ }
68
+
69
+ export interface UploadButtonProps<TRouter extends FileRouter, TEndpoint extends keyof TRouter & string> {
70
+ endpoint: TEndpoint;
71
+ onClientUploadComplete?: (files: UploadedFile[]) => void;
72
+ onUploadError?: (error: Error) => void;
73
+ onUploadProgress?: (progress: UploadProgressEvent) => void;
74
+ onBeforeUploadBegin?: (files: File[]) => File[];
75
+ className?: string;
76
+ disabled?: boolean;
77
+ content?: {
78
+ button?: string;
79
+ allowedContent?: string;
80
+ };
81
+ }
82
+
83
+ export interface UploadDropzoneProps<TRouter extends FileRouter, TEndpoint extends keyof TRouter & string> {
84
+ endpoint: TEndpoint;
85
+ onClientUploadComplete?: (files: UploadedFile[]) => void;
86
+ onUploadError?: (error: Error) => void;
87
+ onUploadProgress?: (progress: UploadProgressEvent) => void;
88
+ onBeforeUploadBegin?: (files: File[]) => File[];
89
+ className?: string;
90
+ disabled?: boolean;
91
+ content?: {
92
+ label?: string;
93
+ allowedContent?: string;
94
+ button?: string;
95
+ };
96
+ }
@@ -0,0 +1,76 @@
1
+ "use client";
2
+
3
+ import React, { useRef, useCallback } from "react";
4
+ import type { FileRouter } from "@uploadbox/core";
5
+ import type { UploadButtonProps } from "./types.js";
6
+ import { useUploadbox } from "./use-uploadbox.js";
7
+
8
+ export function UploadButton<
9
+ TRouter extends FileRouter,
10
+ TEndpoint extends keyof TRouter & string
11
+ >(props: UploadButtonProps<TRouter, TEndpoint>) {
12
+ const {
13
+ endpoint,
14
+ onClientUploadComplete,
15
+ onUploadError,
16
+ onUploadProgress,
17
+ onBeforeUploadBegin,
18
+ className,
19
+ disabled,
20
+ content,
21
+ } = props;
22
+
23
+ const inputRef = useRef<HTMLInputElement>(null);
24
+
25
+ const { startUpload, isUploading, progress } = useUploadbox<TRouter, TEndpoint>(
26
+ endpoint,
27
+ { onClientUploadComplete, onUploadError, onUploadProgress, onBeforeUploadBegin }
28
+ );
29
+
30
+ const handleClick = useCallback(() => {
31
+ inputRef.current?.click();
32
+ }, []);
33
+
34
+ const handleChange = useCallback(
35
+ async (e: React.ChangeEvent<HTMLInputElement>) => {
36
+ const files = Array.from(e.target.files ?? []);
37
+ if (files.length > 0) {
38
+ await startUpload(files);
39
+ }
40
+ // Reset input so the same file can be selected again
41
+ if (inputRef.current) inputRef.current.value = "";
42
+ },
43
+ [startUpload]
44
+ );
45
+
46
+ return (
47
+ <div className={`uploadbox-button-wrapper ${className ?? ""}`}>
48
+ <input
49
+ ref={inputRef}
50
+ type="file"
51
+ multiple
52
+ onChange={handleChange}
53
+ style={{ display: "none" }}
54
+ disabled={disabled || isUploading}
55
+ />
56
+ <button
57
+ type="button"
58
+ className="uploadbox-button"
59
+ onClick={handleClick}
60
+ disabled={disabled || isUploading}
61
+ >
62
+ {isUploading
63
+ ? `Uploading... ${progress}%`
64
+ : content?.button ?? "Upload Files"}
65
+ </button>
66
+ {isUploading && (
67
+ <div className="uploadbox-progress-bar">
68
+ <div
69
+ className="uploadbox-progress-bar-fill"
70
+ style={{ width: `${progress}%` }}
71
+ />
72
+ </div>
73
+ )}
74
+ </div>
75
+ );
76
+ }
@@ -0,0 +1,138 @@
1
+ "use client";
2
+
3
+ import React, { useState, useCallback } from "react";
4
+ import { useDropzone } from "react-dropzone";
5
+ import type { FileRouter } from "@uploadbox/core";
6
+ import type { UploadDropzoneProps } from "./types.js";
7
+ import { useUploadbox } from "./use-uploadbox.js";
8
+
9
+ type DropzoneState = "idle" | "dragover" | "uploading" | "complete" | "error";
10
+
11
+ export function UploadDropzone<
12
+ TRouter extends FileRouter,
13
+ TEndpoint extends keyof TRouter & string
14
+ >(props: UploadDropzoneProps<TRouter, TEndpoint>) {
15
+ const {
16
+ endpoint,
17
+ onClientUploadComplete,
18
+ onUploadError,
19
+ onUploadProgress,
20
+ onBeforeUploadBegin,
21
+ className,
22
+ disabled,
23
+ content,
24
+ } = props;
25
+
26
+ const [state, setState] = useState<DropzoneState>("idle");
27
+ const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
28
+
29
+ const { startUpload, isUploading, progress } = useUploadbox<TRouter, TEndpoint>(
30
+ endpoint,
31
+ {
32
+ onClientUploadComplete: (files) => {
33
+ setState("complete");
34
+ setSelectedFiles([]);
35
+ onClientUploadComplete?.(files);
36
+ setTimeout(() => setState("idle"), 2000);
37
+ },
38
+ onUploadError: (error) => {
39
+ setState("error");
40
+ onUploadError?.(error);
41
+ setTimeout(() => setState("idle"), 3000);
42
+ },
43
+ onUploadProgress,
44
+ onBeforeUploadBegin,
45
+ }
46
+ );
47
+
48
+ const onDrop = useCallback((acceptedFiles: File[]) => {
49
+ setSelectedFiles(acceptedFiles);
50
+ setState("idle");
51
+ }, []);
52
+
53
+ const { getRootProps, getInputProps, isDragActive } = useDropzone({
54
+ onDrop,
55
+ disabled: disabled || isUploading,
56
+ });
57
+
58
+ const handleUpload = useCallback(async () => {
59
+ if (selectedFiles.length === 0) return;
60
+ setState("uploading");
61
+ await startUpload(selectedFiles);
62
+ }, [selectedFiles, startUpload]);
63
+
64
+ const stateClass = isDragActive ? "dragover" : state;
65
+
66
+ return (
67
+ <div
68
+ {...getRootProps()}
69
+ className={`uploadbox-dropzone uploadbox-dropzone--${stateClass} ${className ?? ""}`}
70
+ >
71
+ <input {...getInputProps()} />
72
+
73
+ {state === "complete" ? (
74
+ <div className="uploadbox-dropzone-content">
75
+ <p className="uploadbox-dropzone-label">Upload complete!</p>
76
+ </div>
77
+ ) : state === "error" ? (
78
+ <div className="uploadbox-dropzone-content">
79
+ <p className="uploadbox-dropzone-label">Upload failed. Try again.</p>
80
+ </div>
81
+ ) : (
82
+ <div className="uploadbox-dropzone-content">
83
+ <svg
84
+ className="uploadbox-dropzone-icon"
85
+ xmlns="http://www.w3.org/2000/svg"
86
+ width="40"
87
+ height="40"
88
+ viewBox="0 0 24 24"
89
+ fill="none"
90
+ stroke="currentColor"
91
+ strokeWidth="1.5"
92
+ strokeLinecap="round"
93
+ strokeLinejoin="round"
94
+ >
95
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
96
+ <polyline points="17 8 12 3 7 8" />
97
+ <line x1="12" y1="3" x2="12" y2="15" />
98
+ </svg>
99
+
100
+ <p className="uploadbox-dropzone-label">
101
+ {isDragActive
102
+ ? "Drop files here"
103
+ : content?.label ?? "Drag & drop files here, or click to browse"}
104
+ </p>
105
+
106
+ {content?.allowedContent && (
107
+ <p className="uploadbox-dropzone-allowed">{content.allowedContent}</p>
108
+ )}
109
+
110
+ {selectedFiles.length > 0 && !isUploading && (
111
+ <div className="uploadbox-dropzone-selected">
112
+ <p>{selectedFiles.length} file(s) selected</p>
113
+ <button
114
+ type="button"
115
+ className="uploadbox-button"
116
+ onClick={(e) => {
117
+ e.stopPropagation();
118
+ handleUpload();
119
+ }}
120
+ >
121
+ {content?.button ?? "Upload"}
122
+ </button>
123
+ </div>
124
+ )}
125
+
126
+ {isUploading && (
127
+ <div className="uploadbox-progress-bar">
128
+ <div
129
+ className="uploadbox-progress-bar-fill"
130
+ style={{ width: `${progress}%` }}
131
+ />
132
+ </div>
133
+ )}
134
+ </div>
135
+ )}
136
+ </div>
137
+ );
138
+ }