@thetechfossil/upfiles 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.
package/README.md ADDED
@@ -0,0 +1,106 @@
1
+ # @upfiles/uploader
2
+
3
+ Lightweight JavaScript client and React component to upload files to an UpFiles server (presigned S3 under the hood). Similar to UploadThing-like DX.
4
+
5
+ - Client: get presigned URLs and PUT to S3
6
+ - React `<Uploader />`: drag/drop, progress, single/multiple, accepts/types, size limits
7
+
8
+ ## Installation
9
+
10
+ ```bash
11
+ npm install @upfiles/uploader
12
+ # or
13
+ pnpm add @upfiles/uploader
14
+ # or
15
+ yarn add @upfiles/uploader
16
+ ```
17
+
18
+ ## Server prerequisites
19
+
20
+ Your server must expose a presign endpoint compatible with:
21
+
22
+ - Method: `POST`
23
+ - Path: `/api/upload/presigned-url` (configurable via `clientOptions.presignPath`)
24
+ - Body: `{ fileName, fileType, fileSize? }`
25
+ - Response: `{ presignedUrl, fileKey, publicUrl }`
26
+
27
+ This repository already provides that at `src/app/api/upload/presigned-url/route.ts`.
28
+
29
+ If your server requires auth, pass cookies (withCredentials) or an Authorization header in `clientOptions.headers`.
30
+
31
+ ## Basic usage (React)
32
+
33
+ ```tsx
34
+ import { Uploader } from '@upfiles/uploader';
35
+
36
+ export default function Page() {
37
+ return (
38
+ <Uploader
39
+ clientOptions={{
40
+ baseUrl: process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:4000',
41
+ // headers: { Authorization: `Bearer ${token}` },
42
+ withCredentials: true, // if your server uses NextAuth session cookies
43
+ }}
44
+ multiple
45
+ accept={["image/*", "application/pdf"]}
46
+ maxFileSize={100 * 1024 * 1024}
47
+ onComplete={(files) => {
48
+ console.log('uploaded:', files);
49
+ }}
50
+ onError={(err) => {
51
+ console.error(err);
52
+ }}
53
+ dropzoneClassName="border border-dashed rounded-md p-6 cursor-pointer"
54
+ buttonClassName="px-3 py-2 rounded bg-blue-600 text-white"
55
+ />
56
+ );
57
+ }
58
+ ```
59
+
60
+ ## Basic usage (vanilla JS)
61
+
62
+ ```ts
63
+ import { UpfilesClient } from '@upfiles/uploader';
64
+
65
+ const client = new UpfilesClient({
66
+ baseUrl: 'http://localhost:4000',
67
+ withCredentials: true,
68
+ });
69
+
70
+ async function upload(file) {
71
+ const { presignedUrl, publicUrl } = await client.getPresignedUrl({
72
+ fileName: file.name,
73
+ fileType: file.type,
74
+ fileSize: file.size,
75
+ });
76
+ const res = await client.uploadToS3(presignedUrl, file);
77
+ if (!res.ok) throw new Error('Upload failed');
78
+ return publicUrl;
79
+ }
80
+ ```
81
+
82
+ ## API
83
+
84
+ - `new UpfilesClient(options)`
85
+ - `baseUrl` (required): your UpFiles app base URL
86
+ - `presignPath` (optional): default `/api/upload/presigned-url`
87
+ - `headers` (optional): headers for your server, e.g. Authorization
88
+ - `withCredentials` (optional): `true` to send cookies
89
+ - `client.getPresignedUrl({ fileName, fileType, fileSize? })`
90
+ - `client.uploadToS3(presignedUrl, file)`
91
+
92
+ - `<Uploader />` props
93
+ - `clientOptions`: same as `UpfilesClient` options
94
+ - `multiple` (default true)
95
+ - `accept` (array of MIME patterns)
96
+ - `maxFileSize` (bytes, default 100MB)
97
+ - `maxFiles` (default 10)
98
+ - `onComplete(files)` and `onError(error)`
99
+ - `className`, `buttonClassName`, `dropzoneClassName`
100
+ - `children`: custom dropzone inner content
101
+
102
+ ## Notes
103
+
104
+ - Ensure CORS for your app domain (S3 bucket CORS and your server CORS as needed).
105
+ - For cross-origin usage with cookies (NextAuth), set `withCredentials: true` and configure your server to allow credentials.
106
+ - Consider adding API token auth if distributing the SDK publicly.
@@ -0,0 +1,59 @@
1
+ import React from 'react';
2
+
3
+ type PresignResponse = {
4
+ presignedUrl: string;
5
+ fileKey: string;
6
+ publicUrl: string;
7
+ };
8
+ type UpfilesClientOptions = {
9
+ baseUrl: string;
10
+ presignPath?: string;
11
+ headers?: Record<string, string>;
12
+ withCredentials?: boolean;
13
+ };
14
+ declare class UpfilesClient {
15
+ private baseUrl;
16
+ private presignPath;
17
+ private headers?;
18
+ private withCredentials?;
19
+ constructor(opts: UpfilesClientOptions);
20
+ getPresignedUrl(params: {
21
+ fileName: string;
22
+ fileType: string;
23
+ fileSize?: number;
24
+ projectId?: string;
25
+ folderPath?: string;
26
+ }): Promise<PresignResponse>;
27
+ uploadToS3(presignedUrl: string, file: File): Promise<Response>;
28
+ }
29
+
30
+ type UploaderFile = {
31
+ id: string;
32
+ file: File;
33
+ name: string;
34
+ size: number;
35
+ type: string;
36
+ progress: number;
37
+ status: 'pending' | 'uploading' | 'success' | 'error';
38
+ error?: string;
39
+ publicUrl?: string;
40
+ fileKey?: string;
41
+ };
42
+ type UploaderProps = {
43
+ clientOptions: UpfilesClientOptions;
44
+ multiple?: boolean;
45
+ accept?: string[];
46
+ maxFileSize?: number;
47
+ maxFiles?: number;
48
+ projectId?: string;
49
+ folderPath?: string;
50
+ onComplete?: (files: UploaderFile[]) => void;
51
+ onError?: (error: Error) => void;
52
+ className?: string;
53
+ buttonClassName?: string;
54
+ dropzoneClassName?: string;
55
+ children?: React.ReactNode;
56
+ };
57
+ declare const Uploader: React.FC<UploaderProps>;
58
+
59
+ export { type PresignResponse, UpfilesClient, type UpfilesClientOptions, Uploader, type UploaderFile, type UploaderProps };
@@ -0,0 +1,59 @@
1
+ import React from 'react';
2
+
3
+ type PresignResponse = {
4
+ presignedUrl: string;
5
+ fileKey: string;
6
+ publicUrl: string;
7
+ };
8
+ type UpfilesClientOptions = {
9
+ baseUrl: string;
10
+ presignPath?: string;
11
+ headers?: Record<string, string>;
12
+ withCredentials?: boolean;
13
+ };
14
+ declare class UpfilesClient {
15
+ private baseUrl;
16
+ private presignPath;
17
+ private headers?;
18
+ private withCredentials?;
19
+ constructor(opts: UpfilesClientOptions);
20
+ getPresignedUrl(params: {
21
+ fileName: string;
22
+ fileType: string;
23
+ fileSize?: number;
24
+ projectId?: string;
25
+ folderPath?: string;
26
+ }): Promise<PresignResponse>;
27
+ uploadToS3(presignedUrl: string, file: File): Promise<Response>;
28
+ }
29
+
30
+ type UploaderFile = {
31
+ id: string;
32
+ file: File;
33
+ name: string;
34
+ size: number;
35
+ type: string;
36
+ progress: number;
37
+ status: 'pending' | 'uploading' | 'success' | 'error';
38
+ error?: string;
39
+ publicUrl?: string;
40
+ fileKey?: string;
41
+ };
42
+ type UploaderProps = {
43
+ clientOptions: UpfilesClientOptions;
44
+ multiple?: boolean;
45
+ accept?: string[];
46
+ maxFileSize?: number;
47
+ maxFiles?: number;
48
+ projectId?: string;
49
+ folderPath?: string;
50
+ onComplete?: (files: UploaderFile[]) => void;
51
+ onError?: (error: Error) => void;
52
+ className?: string;
53
+ buttonClassName?: string;
54
+ dropzoneClassName?: string;
55
+ children?: React.ReactNode;
56
+ };
57
+ declare const Uploader: React.FC<UploaderProps>;
58
+
59
+ export { type PresignResponse, UpfilesClient, type UpfilesClientOptions, Uploader, type UploaderFile, type UploaderProps };
package/dist/index.js ADDED
@@ -0,0 +1,266 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ UpfilesClient: () => UpfilesClient,
24
+ Uploader: () => Uploader
25
+ });
26
+ module.exports = __toCommonJS(index_exports);
27
+
28
+ // src/client.ts
29
+ var UpfilesClient = class {
30
+ constructor(opts) {
31
+ this.baseUrl = opts.baseUrl.replace(/\/$/, "");
32
+ this.presignPath = opts.presignPath ?? "/api/upload/presigned-url";
33
+ this.headers = opts.headers;
34
+ this.withCredentials = opts.withCredentials;
35
+ }
36
+ async getPresignedUrl(params) {
37
+ const res = await fetch(`${this.baseUrl}${this.presignPath}`, {
38
+ method: "POST",
39
+ headers: {
40
+ "Content-Type": "application/json",
41
+ ...this.headers || {}
42
+ },
43
+ credentials: this.withCredentials ? "include" : "same-origin",
44
+ body: JSON.stringify(params)
45
+ });
46
+ if (!res.ok) {
47
+ let msg = "Failed to get upload URL";
48
+ try {
49
+ const data = await res.json();
50
+ msg = data.error || msg;
51
+ } catch {
52
+ }
53
+ throw new Error(msg);
54
+ }
55
+ return res.json();
56
+ }
57
+ async uploadToS3(presignedUrl, file) {
58
+ return fetch(presignedUrl, {
59
+ method: "PUT",
60
+ body: file,
61
+ headers: {
62
+ "Content-Type": file.type
63
+ }
64
+ });
65
+ }
66
+ };
67
+
68
+ // src/Uploader.tsx
69
+ var import_react = require("react");
70
+ var import_jsx_runtime = require("react/jsx-runtime");
71
+ var Uploader = ({
72
+ clientOptions,
73
+ multiple = true,
74
+ accept = ["image/*", "video/*", "audio/*", "application/pdf", "text/plain", "application/json"],
75
+ maxFileSize = 100 * 1024 * 1024,
76
+ maxFiles = 10,
77
+ onComplete,
78
+ onError,
79
+ className,
80
+ buttonClassName,
81
+ dropzoneClassName,
82
+ children,
83
+ projectId,
84
+ folderPath
85
+ }) => {
86
+ const inputRef = (0, import_react.useRef)(null);
87
+ const client = (0, import_react.useMemo)(() => new UpfilesClient(clientOptions), [clientOptions]);
88
+ const [files, setFiles] = (0, import_react.useState)([]);
89
+ const [isUploading, setIsUploading] = (0, import_react.useState)(false);
90
+ const [isDragOver, setIsDragOver] = (0, import_react.useState)(false);
91
+ const addFiles = (0, import_react.useCallback)((incoming) => {
92
+ const filtered = incoming.filter((f) => {
93
+ if (f.size > maxFileSize) {
94
+ return false;
95
+ }
96
+ return true;
97
+ });
98
+ const limited = filtered.slice(0, Math.max(0, maxFiles - files.length));
99
+ const mapped = limited.map((f) => ({
100
+ id: Math.random().toString(36).slice(2),
101
+ file: f,
102
+ name: f.name,
103
+ size: f.size,
104
+ type: f.type,
105
+ progress: 0,
106
+ status: "pending"
107
+ }));
108
+ setFiles((prev) => [...prev, ...mapped]);
109
+ }, [files.length, maxFileSize, maxFiles]);
110
+ const onInputChange = (0, import_react.useCallback)((e) => {
111
+ const fs = Array.from(e.target.files || []);
112
+ addFiles(fs);
113
+ if (inputRef.current) inputRef.current.value = "";
114
+ }, [addFiles]);
115
+ const handleDrop = (0, import_react.useCallback)((e) => {
116
+ e.preventDefault();
117
+ e.stopPropagation();
118
+ setIsDragOver(false);
119
+ addFiles(Array.from(e.dataTransfer.files || []));
120
+ }, [addFiles]);
121
+ const uploadOne = (0, import_react.useCallback)(async (item) => {
122
+ const update = (patch) => {
123
+ setFiles((prev) => prev.map((f) => f.id === item.id ? { ...f, ...patch } : f));
124
+ };
125
+ try {
126
+ update({ status: "uploading", progress: 1 });
127
+ const presign = await client.getPresignedUrl({
128
+ fileName: item.name,
129
+ fileType: item.type,
130
+ fileSize: item.size,
131
+ projectId,
132
+ folderPath
133
+ });
134
+ await new Promise((resolve, reject) => {
135
+ const xhr = new XMLHttpRequest();
136
+ xhr.open("PUT", presign.presignedUrl, true);
137
+ xhr.upload.onprogress = (ev) => {
138
+ if (ev.lengthComputable) {
139
+ const pct = Math.min(99, Math.floor(ev.loaded / ev.total * 100));
140
+ update({ progress: pct });
141
+ }
142
+ };
143
+ xhr.onload = () => {
144
+ if (xhr.status >= 200 && xhr.status < 300) {
145
+ resolve();
146
+ } else {
147
+ reject(new Error(`Upload failed with status ${xhr.status}`));
148
+ }
149
+ };
150
+ xhr.onerror = () => reject(new Error("Network error during upload"));
151
+ xhr.setRequestHeader("Content-Type", item.type || "application/octet-stream");
152
+ xhr.send(item.file);
153
+ });
154
+ update({ status: "success", progress: 100, fileKey: presign.fileKey, publicUrl: presign.publicUrl });
155
+ return { ...item, status: "success", progress: 100, fileKey: presign.fileKey, publicUrl: presign.publicUrl };
156
+ } catch (err) {
157
+ const message = err instanceof Error ? err.message : "Upload failed";
158
+ update({ status: "error", error: message });
159
+ throw err;
160
+ }
161
+ }, [client]);
162
+ const uploadAll = (0, import_react.useCallback)(async () => {
163
+ const targets = files.filter((f) => f.status === "pending");
164
+ if (!targets.length) return;
165
+ setIsUploading(true);
166
+ try {
167
+ const results = [];
168
+ for (const f of targets) {
169
+ try {
170
+ const done = await uploadOne(f);
171
+ results.push(done);
172
+ } catch (e) {
173
+ }
174
+ }
175
+ onComplete?.(results.filter((f) => f.status === "success"));
176
+ } catch (e) {
177
+ onError?.(e);
178
+ } finally {
179
+ setIsUploading(false);
180
+ }
181
+ }, [files, onComplete, onError, uploadOne]);
182
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className, children: [
183
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
184
+ "input",
185
+ {
186
+ ref: inputRef,
187
+ type: "file",
188
+ style: { display: "none" },
189
+ multiple,
190
+ accept: accept.join(","),
191
+ onChange: onInputChange
192
+ }
193
+ ),
194
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
195
+ "div",
196
+ {
197
+ className: dropzoneClassName,
198
+ onDragOver: (e) => {
199
+ e.preventDefault();
200
+ e.stopPropagation();
201
+ setIsDragOver(true);
202
+ },
203
+ onDragLeave: (e) => {
204
+ e.preventDefault();
205
+ e.stopPropagation();
206
+ setIsDragOver(false);
207
+ },
208
+ onDrop: handleDrop,
209
+ onClick: () => inputRef.current?.click(),
210
+ role: "button",
211
+ "aria-label": "Upload files",
212
+ children: children ?? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { textAlign: "center", padding: 16 }, children: [
213
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontWeight: 600 }, children: "Drop files here or click to browse" }),
214
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { fontSize: 12, color: "#666" }, children: [
215
+ multiple ? `Up to ${maxFiles} files` : "Single file",
216
+ " \u2022 Max ",
217
+ (maxFileSize / (1024 * 1024)).toFixed(0),
218
+ "MB"
219
+ ] })
220
+ ] })
221
+ }
222
+ ),
223
+ files.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { marginTop: 12 }, children: [
224
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: 8, marginBottom: 8 }, children: [
225
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
226
+ "button",
227
+ {
228
+ className: buttonClassName,
229
+ disabled: isUploading || files.every((f) => f.status !== "pending"),
230
+ onClick: uploadAll,
231
+ children: isUploading ? "Uploading..." : "Upload"
232
+ }
233
+ ),
234
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
235
+ "button",
236
+ {
237
+ className: buttonClassName,
238
+ disabled: isUploading,
239
+ onClick: () => setFiles([]),
240
+ children: "Clear"
241
+ }
242
+ )
243
+ ] }),
244
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { style: { display: "grid", gap: 8, listStyle: "none", padding: 0 }, children: files.map((f) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { style: { border: "1px solid #e5e7eb", borderRadius: 8, padding: 8 }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center" }, children: [
245
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { minWidth: 0 }, children: [
246
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontWeight: 500, overflow: "hidden", textOverflow: "ellipsis" }, children: f.name }),
247
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { fontSize: 12, color: "#666" }, children: [
248
+ (f.size / (1024 * 1024)).toFixed(2),
249
+ " MB"
250
+ ] }),
251
+ f.error && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontSize: 12, color: "#b91c1c" }, children: f.error }),
252
+ f.publicUrl && f.status === "success" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { href: f.publicUrl, target: "_blank", rel: "noreferrer", style: { fontSize: 12, color: "#2563eb" }, children: "View" })
253
+ ] }),
254
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { minWidth: 120, textAlign: "right" }, children: [
255
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { height: 6, background: "#e5e7eb", borderRadius: 999, overflow: "hidden" }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { width: `${f.progress}%`, height: "100%", background: f.status === "error" ? "#ef4444" : "#2563eb" } }) }),
256
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontSize: 12, color: "#666", marginTop: 4 }, children: f.status })
257
+ ] })
258
+ ] }) }, f.id)) })
259
+ ] })
260
+ ] });
261
+ };
262
+ // Annotate the CommonJS export names for ESM import in node:
263
+ 0 && (module.exports = {
264
+ UpfilesClient,
265
+ Uploader
266
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,238 @@
1
+ // src/client.ts
2
+ var UpfilesClient = class {
3
+ constructor(opts) {
4
+ this.baseUrl = opts.baseUrl.replace(/\/$/, "");
5
+ this.presignPath = opts.presignPath ?? "/api/upload/presigned-url";
6
+ this.headers = opts.headers;
7
+ this.withCredentials = opts.withCredentials;
8
+ }
9
+ async getPresignedUrl(params) {
10
+ const res = await fetch(`${this.baseUrl}${this.presignPath}`, {
11
+ method: "POST",
12
+ headers: {
13
+ "Content-Type": "application/json",
14
+ ...this.headers || {}
15
+ },
16
+ credentials: this.withCredentials ? "include" : "same-origin",
17
+ body: JSON.stringify(params)
18
+ });
19
+ if (!res.ok) {
20
+ let msg = "Failed to get upload URL";
21
+ try {
22
+ const data = await res.json();
23
+ msg = data.error || msg;
24
+ } catch {
25
+ }
26
+ throw new Error(msg);
27
+ }
28
+ return res.json();
29
+ }
30
+ async uploadToS3(presignedUrl, file) {
31
+ return fetch(presignedUrl, {
32
+ method: "PUT",
33
+ body: file,
34
+ headers: {
35
+ "Content-Type": file.type
36
+ }
37
+ });
38
+ }
39
+ };
40
+
41
+ // src/Uploader.tsx
42
+ import { useCallback, useMemo, useRef, useState } from "react";
43
+ import { jsx, jsxs } from "react/jsx-runtime";
44
+ var Uploader = ({
45
+ clientOptions,
46
+ multiple = true,
47
+ accept = ["image/*", "video/*", "audio/*", "application/pdf", "text/plain", "application/json"],
48
+ maxFileSize = 100 * 1024 * 1024,
49
+ maxFiles = 10,
50
+ onComplete,
51
+ onError,
52
+ className,
53
+ buttonClassName,
54
+ dropzoneClassName,
55
+ children,
56
+ projectId,
57
+ folderPath
58
+ }) => {
59
+ const inputRef = useRef(null);
60
+ const client = useMemo(() => new UpfilesClient(clientOptions), [clientOptions]);
61
+ const [files, setFiles] = useState([]);
62
+ const [isUploading, setIsUploading] = useState(false);
63
+ const [isDragOver, setIsDragOver] = useState(false);
64
+ const addFiles = useCallback((incoming) => {
65
+ const filtered = incoming.filter((f) => {
66
+ if (f.size > maxFileSize) {
67
+ return false;
68
+ }
69
+ return true;
70
+ });
71
+ const limited = filtered.slice(0, Math.max(0, maxFiles - files.length));
72
+ const mapped = limited.map((f) => ({
73
+ id: Math.random().toString(36).slice(2),
74
+ file: f,
75
+ name: f.name,
76
+ size: f.size,
77
+ type: f.type,
78
+ progress: 0,
79
+ status: "pending"
80
+ }));
81
+ setFiles((prev) => [...prev, ...mapped]);
82
+ }, [files.length, maxFileSize, maxFiles]);
83
+ const onInputChange = useCallback((e) => {
84
+ const fs = Array.from(e.target.files || []);
85
+ addFiles(fs);
86
+ if (inputRef.current) inputRef.current.value = "";
87
+ }, [addFiles]);
88
+ const handleDrop = useCallback((e) => {
89
+ e.preventDefault();
90
+ e.stopPropagation();
91
+ setIsDragOver(false);
92
+ addFiles(Array.from(e.dataTransfer.files || []));
93
+ }, [addFiles]);
94
+ const uploadOne = useCallback(async (item) => {
95
+ const update = (patch) => {
96
+ setFiles((prev) => prev.map((f) => f.id === item.id ? { ...f, ...patch } : f));
97
+ };
98
+ try {
99
+ update({ status: "uploading", progress: 1 });
100
+ const presign = await client.getPresignedUrl({
101
+ fileName: item.name,
102
+ fileType: item.type,
103
+ fileSize: item.size,
104
+ projectId,
105
+ folderPath
106
+ });
107
+ await new Promise((resolve, reject) => {
108
+ const xhr = new XMLHttpRequest();
109
+ xhr.open("PUT", presign.presignedUrl, true);
110
+ xhr.upload.onprogress = (ev) => {
111
+ if (ev.lengthComputable) {
112
+ const pct = Math.min(99, Math.floor(ev.loaded / ev.total * 100));
113
+ update({ progress: pct });
114
+ }
115
+ };
116
+ xhr.onload = () => {
117
+ if (xhr.status >= 200 && xhr.status < 300) {
118
+ resolve();
119
+ } else {
120
+ reject(new Error(`Upload failed with status ${xhr.status}`));
121
+ }
122
+ };
123
+ xhr.onerror = () => reject(new Error("Network error during upload"));
124
+ xhr.setRequestHeader("Content-Type", item.type || "application/octet-stream");
125
+ xhr.send(item.file);
126
+ });
127
+ update({ status: "success", progress: 100, fileKey: presign.fileKey, publicUrl: presign.publicUrl });
128
+ return { ...item, status: "success", progress: 100, fileKey: presign.fileKey, publicUrl: presign.publicUrl };
129
+ } catch (err) {
130
+ const message = err instanceof Error ? err.message : "Upload failed";
131
+ update({ status: "error", error: message });
132
+ throw err;
133
+ }
134
+ }, [client]);
135
+ const uploadAll = useCallback(async () => {
136
+ const targets = files.filter((f) => f.status === "pending");
137
+ if (!targets.length) return;
138
+ setIsUploading(true);
139
+ try {
140
+ const results = [];
141
+ for (const f of targets) {
142
+ try {
143
+ const done = await uploadOne(f);
144
+ results.push(done);
145
+ } catch (e) {
146
+ }
147
+ }
148
+ onComplete?.(results.filter((f) => f.status === "success"));
149
+ } catch (e) {
150
+ onError?.(e);
151
+ } finally {
152
+ setIsUploading(false);
153
+ }
154
+ }, [files, onComplete, onError, uploadOne]);
155
+ return /* @__PURE__ */ jsxs("div", { className, children: [
156
+ /* @__PURE__ */ jsx(
157
+ "input",
158
+ {
159
+ ref: inputRef,
160
+ type: "file",
161
+ style: { display: "none" },
162
+ multiple,
163
+ accept: accept.join(","),
164
+ onChange: onInputChange
165
+ }
166
+ ),
167
+ /* @__PURE__ */ jsx(
168
+ "div",
169
+ {
170
+ className: dropzoneClassName,
171
+ onDragOver: (e) => {
172
+ e.preventDefault();
173
+ e.stopPropagation();
174
+ setIsDragOver(true);
175
+ },
176
+ onDragLeave: (e) => {
177
+ e.preventDefault();
178
+ e.stopPropagation();
179
+ setIsDragOver(false);
180
+ },
181
+ onDrop: handleDrop,
182
+ onClick: () => inputRef.current?.click(),
183
+ role: "button",
184
+ "aria-label": "Upload files",
185
+ children: children ?? /* @__PURE__ */ jsxs("div", { style: { textAlign: "center", padding: 16 }, children: [
186
+ /* @__PURE__ */ jsx("div", { style: { fontWeight: 600 }, children: "Drop files here or click to browse" }),
187
+ /* @__PURE__ */ jsxs("div", { style: { fontSize: 12, color: "#666" }, children: [
188
+ multiple ? `Up to ${maxFiles} files` : "Single file",
189
+ " \u2022 Max ",
190
+ (maxFileSize / (1024 * 1024)).toFixed(0),
191
+ "MB"
192
+ ] })
193
+ ] })
194
+ }
195
+ ),
196
+ files.length > 0 && /* @__PURE__ */ jsxs("div", { style: { marginTop: 12 }, children: [
197
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 8, marginBottom: 8 }, children: [
198
+ /* @__PURE__ */ jsx(
199
+ "button",
200
+ {
201
+ className: buttonClassName,
202
+ disabled: isUploading || files.every((f) => f.status !== "pending"),
203
+ onClick: uploadAll,
204
+ children: isUploading ? "Uploading..." : "Upload"
205
+ }
206
+ ),
207
+ /* @__PURE__ */ jsx(
208
+ "button",
209
+ {
210
+ className: buttonClassName,
211
+ disabled: isUploading,
212
+ onClick: () => setFiles([]),
213
+ children: "Clear"
214
+ }
215
+ )
216
+ ] }),
217
+ /* @__PURE__ */ jsx("ul", { style: { display: "grid", gap: 8, listStyle: "none", padding: 0 }, children: files.map((f) => /* @__PURE__ */ jsx("li", { style: { border: "1px solid #e5e7eb", borderRadius: 8, padding: 8 }, children: /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center" }, children: [
218
+ /* @__PURE__ */ jsxs("div", { style: { minWidth: 0 }, children: [
219
+ /* @__PURE__ */ jsx("div", { style: { fontWeight: 500, overflow: "hidden", textOverflow: "ellipsis" }, children: f.name }),
220
+ /* @__PURE__ */ jsxs("div", { style: { fontSize: 12, color: "#666" }, children: [
221
+ (f.size / (1024 * 1024)).toFixed(2),
222
+ " MB"
223
+ ] }),
224
+ f.error && /* @__PURE__ */ jsx("div", { style: { fontSize: 12, color: "#b91c1c" }, children: f.error }),
225
+ f.publicUrl && f.status === "success" && /* @__PURE__ */ jsx("a", { href: f.publicUrl, target: "_blank", rel: "noreferrer", style: { fontSize: 12, color: "#2563eb" }, children: "View" })
226
+ ] }),
227
+ /* @__PURE__ */ jsxs("div", { style: { minWidth: 120, textAlign: "right" }, children: [
228
+ /* @__PURE__ */ jsx("div", { style: { height: 6, background: "#e5e7eb", borderRadius: 999, overflow: "hidden" }, children: /* @__PURE__ */ jsx("div", { style: { width: `${f.progress}%`, height: "100%", background: f.status === "error" ? "#ef4444" : "#2563eb" } }) }),
229
+ /* @__PURE__ */ jsx("div", { style: { fontSize: 12, color: "#666", marginTop: 4 }, children: f.status })
230
+ ] })
231
+ ] }) }, f.id)) })
232
+ ] })
233
+ ] });
234
+ };
235
+ export {
236
+ UpfilesClient,
237
+ Uploader
238
+ };
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@thetechfossil/upfiles",
3
+ "version": "0.1.0",
4
+ "description": "Lightweight client and React components to upload files to UpFiles API (presigned S3)",
5
+ "license": "MIT",
6
+ "author": "UpFiles",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/your-org/upfiles.git"
10
+ },
11
+ "main": "dist/index.cjs",
12
+ "module": "dist/index.mjs",
13
+ "types": "dist/index.d.ts",
14
+ "exports": {
15
+ ".": {
16
+ "import": "./dist/index.mjs",
17
+ "require": "./dist/index.cjs",
18
+ "types": "./dist/index.d.ts"
19
+ }
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "sideEffects": false,
28
+ "scripts": {
29
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean",
30
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
31
+ "typecheck": "tsc -p tsconfig.json --noEmit",
32
+ "prepublishOnly": "npm run build"
33
+ },
34
+ "peerDependencies": {
35
+ "react": ">=18"
36
+ },
37
+ "devDependencies": {
38
+ "@types/react": "^19",
39
+ "tsup": "^8.3.0",
40
+ "typescript": "^5.6.2"
41
+ }
42
+ }