@sparkstudio/storage-ui 1.0.7 → 1.0.9

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/dist/index.cjs CHANGED
@@ -20,7 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
- AWSCredentialsDTO: () => AWSCredentialsDTO,
23
+ AWSPresignedUrlDTO: () => AWSPresignedUrlDTO,
24
24
  Container: () => Container,
25
25
  ContainerDTO: () => ContainerDTO,
26
26
  ContainerType: () => ContainerType,
@@ -123,8 +123,8 @@ var Container = class {
123
123
  if (!response.ok) throw new Error(await response.text());
124
124
  return await response.json();
125
125
  }
126
- async GetUploadCredentials() {
127
- const url = `${this.baseUrl}/api/Container/GetUploadCredentials`;
126
+ async GetPreSignedUrl(fileName, size, contentType) {
127
+ const url = `${this.baseUrl}/api/Container/GetPreSignedUrl/` + fileName + `/` + size + `/` + contentType;
128
128
  const token = localStorage.getItem("auth_token");
129
129
  const requestOptions = {
130
130
  method: "GET",
@@ -157,19 +157,15 @@ var SparkStudioStorageSDK = class {
157
157
  }
158
158
  };
159
159
 
160
- // src/api/DTOs/AWSCredentialsDTO.ts
161
- var AWSCredentialsDTO = class {
162
- AccessKeyId;
163
- SecretAccessKey;
164
- SessionToken;
165
- BucketName;
166
- Region;
160
+ // src/api/DTOs/AWSPresignedUrlDTO.ts
161
+ var AWSPresignedUrlDTO = class {
162
+ PresignedUrl;
163
+ PublicUrl;
164
+ Key;
167
165
  constructor(init) {
168
- this.AccessKeyId = init.AccessKeyId;
169
- this.SecretAccessKey = init.SecretAccessKey;
170
- this.SessionToken = init.SessionToken;
171
- this.BucketName = init.BucketName;
172
- this.Region = init.Region;
166
+ this.PresignedUrl = init.PresignedUrl;
167
+ this.PublicUrl = init.PublicUrl;
168
+ this.Key = init.Key;
173
169
  }
174
170
  };
175
171
 
@@ -178,6 +174,7 @@ var ContainerDTO = class {
178
174
  Id;
179
175
  ContainerType;
180
176
  Name;
177
+ ContentType;
181
178
  CreatedDate;
182
179
  FileSize;
183
180
  UserId;
@@ -186,6 +183,7 @@ var ContainerDTO = class {
186
183
  this.Id = init.Id;
187
184
  this.ContainerType = init.ContainerType;
188
185
  this.Name = init.Name;
186
+ this.ContentType = init.ContentType;
189
187
  this.CreatedDate = init.CreatedDate;
190
188
  this.FileSize = init.FileSize;
191
189
  this.UserId = init.UserId;
@@ -209,10 +207,67 @@ var UploadContainer = ({
209
207
  description = "Drag and drop files here, or click the button to browse.",
210
208
  multiple = true,
211
209
  accept,
212
- onFilesSelected
210
+ onFilesSelected,
211
+ autoUpload = false,
212
+ getPresignedUrl,
213
+ onUploadComplete,
214
+ onUploadError
213
215
  }) => {
214
216
  const [isDragging, setIsDragging] = (0, import_react.useState)(false);
215
217
  const [fileNames, setFileNames] = (0, import_react.useState)([]);
218
+ const [uploads, setUploads] = (0, import_react.useState)([]);
219
+ const startUploadsIfNeeded = (files) => {
220
+ if (!autoUpload || !getPresignedUrl) return;
221
+ const newUploads = Array.from(files).map((file) => ({
222
+ id: `${file.name}-${file.size}-${file.lastModified}-${Math.random()}`,
223
+ file,
224
+ progress: 0,
225
+ status: "pending"
226
+ }));
227
+ setUploads((prev) => [...prev, ...newUploads]);
228
+ newUploads.forEach((upload) => {
229
+ uploadFile(upload);
230
+ });
231
+ };
232
+ const uploadFile = async (upload) => {
233
+ setUploads(
234
+ (prev) => prev.map(
235
+ (u) => u.id === upload.id ? { ...u, status: "uploading", progress: 0 } : u
236
+ )
237
+ );
238
+ try {
239
+ if (!getPresignedUrl) {
240
+ throw new Error("getPresignedUrl is not provided.");
241
+ }
242
+ const presignedUrl = await getPresignedUrl(upload.file);
243
+ const url = presignedUrl?.PresignedUrl ?? "";
244
+ await uploadFileToS3(upload.file, url, (progress) => {
245
+ setUploads(
246
+ (prev) => prev.map(
247
+ (u) => u.id === upload.id ? { ...u, progress } : u
248
+ )
249
+ );
250
+ });
251
+ const fileUrl = url.split("?")[0];
252
+ setUploads(
253
+ (prev) => prev.map(
254
+ (u) => u.id === upload.id ? { ...u, status: "success", progress: 100, s3Url: fileUrl, publicUrl: presignedUrl.PublicUrl } : u
255
+ )
256
+ );
257
+ onUploadComplete?.(upload.file, fileUrl);
258
+ } catch (err) {
259
+ let message = "Upload failed";
260
+ if (err instanceof Error) {
261
+ message = err.message;
262
+ }
263
+ setUploads(
264
+ (prev) => prev.map(
265
+ (u) => u.id === upload.id ? { ...u, status: "error", error: message } : u
266
+ )
267
+ );
268
+ onUploadError?.(upload.file, err instanceof Error ? err : new Error(message));
269
+ }
270
+ };
216
271
  const handleDragOver = (e) => {
217
272
  e.preventDefault();
218
273
  setIsDragging(true);
@@ -228,12 +283,14 @@ var UploadContainer = ({
228
283
  if (!files || files.length === 0) return;
229
284
  setFileNames(Array.from(files).map((f) => f.name));
230
285
  onFilesSelected?.(files);
286
+ startUploadsIfNeeded(files);
231
287
  };
232
288
  const handleFileChange = (e) => {
233
289
  const files = e.target.files;
234
290
  if (!files || files.length === 0) return;
235
291
  setFileNames(Array.from(files).map((f) => f.name));
236
292
  onFilesSelected?.(files);
293
+ startUploadsIfNeeded(files);
237
294
  };
238
295
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "container my-3", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "card shadow-sm", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "card-body", children: [
239
296
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h5", { className: "card-title mb-2", children: title }),
@@ -272,9 +329,60 @@ var UploadContainer = ({
272
329
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "small text-muted", children: "Selected:" }),
273
330
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { className: "mb-0 small", children: fileNames.map((name) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: name }, name)) })
274
331
  ] })
275
- ] })
332
+ ] }),
333
+ uploads.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "mt-3", children: uploads.map((u) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "mb-2", children: [
334
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "d-flex justify-content-between small mb-1", children: [
335
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { children: [
336
+ u.file.name,
337
+ " - ",
338
+ u?.publicUrl ?? ""
339
+ ] }),
340
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: u.status === "success" ? "Completed" : u.status === "error" ? "Error" : `${u.progress}%` })
341
+ ] }),
342
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "progress", style: { height: "6px" }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
343
+ "div",
344
+ {
345
+ className: "progress-bar " + (u.status === "success" ? "bg-success" : u.status === "error" ? "bg-danger" : ""),
346
+ role: "progressbar",
347
+ style: { width: `${u.progress}%` },
348
+ "aria-valuenow": u.progress,
349
+ "aria-valuemin": 0,
350
+ "aria-valuemax": 100
351
+ }
352
+ ) }),
353
+ u.status === "error" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "text-danger small mt-1", children: u.error ?? "Upload failed" })
354
+ ] }, u.id)) })
276
355
  ] }) }) });
277
356
  };
357
+ async function uploadFileToS3(file, presignedUrl, onProgress) {
358
+ return new Promise((resolve, reject) => {
359
+ const xhr = new XMLHttpRequest();
360
+ xhr.open("PUT", presignedUrl);
361
+ xhr.upload.onprogress = (event) => {
362
+ if (!event.lengthComputable) return;
363
+ const percent = Math.round(event.loaded / event.total * 100);
364
+ onProgress(percent);
365
+ };
366
+ xhr.onload = () => {
367
+ if (xhr.status >= 200 && xhr.status < 300) {
368
+ onProgress(100);
369
+ resolve();
370
+ } else {
371
+ reject(
372
+ new Error(`S3 upload failed with status ${xhr.status}: ${xhr.statusText}`)
373
+ );
374
+ }
375
+ };
376
+ xhr.onerror = () => {
377
+ reject(new Error("Network error while uploading to S3"));
378
+ };
379
+ xhr.setRequestHeader(
380
+ "Content-Type",
381
+ file.type || "application/octet-stream"
382
+ );
383
+ xhr.send(file);
384
+ });
385
+ }
278
386
 
279
387
  // src/views/HomeView.tsx
280
388
  var import_authentication_ui = require("@sparkstudio/authentication-ui");
@@ -283,6 +391,11 @@ function HomeView() {
283
391
  function handleOnLoginSuccess(user) {
284
392
  alert(user?.Id);
285
393
  }
394
+ const getPresignedUrl = async (file) => {
395
+ const sdk = new SparkStudioStorageSDK("https://lf9zyufpuk.execute-api.us-east-2.amazonaws.com/Prod");
396
+ const result = await sdk.container.GetPreSignedUrl(file.name, file.size, encodeURIComponent(file.type));
397
+ return result;
398
+ };
286
399
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
287
400
  import_authentication_ui.AuthenticatorProvider,
288
401
  {
@@ -292,14 +405,30 @@ function HomeView() {
292
405
  onLoginSuccess: handleOnLoginSuccess,
293
406
  children: [
294
407
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_authentication_ui.UserInfoCard, {}),
295
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(UploadContainer, {})
408
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
409
+ UploadContainer,
410
+ {
411
+ title: "Upload your files to S3",
412
+ description: "Drag & drop or browse files. Each file will upload with its own progress bar.",
413
+ multiple: true,
414
+ accept: "*/*",
415
+ autoUpload: true,
416
+ getPresignedUrl,
417
+ onUploadComplete: (file, s3Url) => {
418
+ console.log("Uploaded", file.name, "to", s3Url);
419
+ },
420
+ onUploadError: (file, error) => {
421
+ console.error("Failed to upload", file.name, error);
422
+ }
423
+ }
424
+ )
296
425
  ]
297
426
  }
298
427
  );
299
428
  }
300
429
  // Annotate the CommonJS export names for ESM import in node:
301
430
  0 && (module.exports = {
302
- AWSCredentialsDTO,
431
+ AWSPresignedUrlDTO,
303
432
  Container,
304
433
  ContainerDTO,
305
434
  ContainerType,
package/dist/index.d.cts CHANGED
@@ -14,6 +14,7 @@ interface IContainerDTO {
14
14
  Id: string;
15
15
  ContainerType: ContainerType;
16
16
  Name?: string;
17
+ ContentType?: string;
17
18
  CreatedDate: string;
18
19
  FileSize: number;
19
20
  UserId: string;
@@ -24,6 +25,7 @@ declare class ContainerDTO implements IContainerDTO {
24
25
  Id: string;
25
26
  ContainerType: ContainerType;
26
27
  Name?: string;
28
+ ContentType?: string;
27
29
  CreatedDate: string;
28
30
  FileSize: number;
29
31
  UserId: string;
@@ -32,23 +34,19 @@ declare class ContainerDTO implements IContainerDTO {
32
34
  }
33
35
 
34
36
  /**
35
- * Represents an Auto-generated model for AWSCredentialsDTO.
37
+ * Represents an Auto-generated model for AWSPresignedUrlDTO.
36
38
  */
37
- interface IAWSCredentialsDTO {
38
- AccessKeyId?: string;
39
- SecretAccessKey?: string;
40
- SessionToken?: string;
41
- BucketName?: string;
42
- Region?: string;
39
+ interface IAWSPresignedUrlDTO {
40
+ PresignedUrl?: string;
41
+ PublicUrl?: string;
42
+ Key?: string;
43
43
  }
44
- type AWSCredentialsDTOInit = Partial<IAWSCredentialsDTO>;
45
- declare class AWSCredentialsDTO implements IAWSCredentialsDTO {
46
- AccessKeyId?: string;
47
- SecretAccessKey?: string;
48
- SessionToken?: string;
49
- BucketName?: string;
50
- Region?: string;
51
- constructor(init: AWSCredentialsDTOInit);
44
+ type AWSPresignedUrlDTOInit = Partial<IAWSPresignedUrlDTO>;
45
+ declare class AWSPresignedUrlDTO implements IAWSPresignedUrlDTO {
46
+ PresignedUrl?: string;
47
+ PublicUrl?: string;
48
+ Key?: string;
49
+ constructor(init: AWSPresignedUrlDTOInit);
52
50
  }
53
51
 
54
52
  /**
@@ -63,7 +61,7 @@ declare class Container {
63
61
  Create(containerDTO: ContainerDTO): Promise<ContainerDTO>;
64
62
  Update(containerDTO: ContainerDTO): Promise<ContainerDTO>;
65
63
  Delete(id: string): Promise<ContainerDTO>;
66
- GetUploadCredentials(): Promise<AWSCredentialsDTO>;
64
+ GetPreSignedUrl(fileName: string, size: number, contentType: string): Promise<AWSPresignedUrlDTO>;
67
65
  }
68
66
 
69
67
  /**
@@ -89,9 +87,28 @@ interface UploadContainerProps {
89
87
  multiple?: boolean;
90
88
  accept?: string;
91
89
  onFilesSelected?: (files: FileList) => void;
90
+ /**
91
+ * When true, the component will immediately start uploading to S3
92
+ * using getPresignedUrl for each file.
93
+ */
94
+ autoUpload?: boolean;
95
+ /**
96
+ * Your function that returns a pre-signed URL for a given file.
97
+ * Typically calls your backend: /api/uploads/presign
98
+ */
99
+ getPresignedUrl?: (file: File) => Promise<AWSPresignedUrlDTO>;
100
+ /**
101
+ * Called when a file has successfully finished uploading.
102
+ * s3Url is usually the final S3 object URL or key (depends on your backend).
103
+ */
104
+ onUploadComplete?: (file: File, s3Url: string) => void;
105
+ /**
106
+ * Called when a file upload fails.
107
+ */
108
+ onUploadError?: (file: File, error: Error) => void;
92
109
  }
93
110
  declare const UploadContainer: React.FC<UploadContainerProps>;
94
111
 
95
112
  declare function HomeView(): react_jsx_runtime.JSX.Element;
96
113
 
97
- export { AWSCredentialsDTO, Container, ContainerDTO, ContainerType, Home, HomeView, type IAWSCredentialsDTO, type IContainerDTO, SparkStudioStorageSDK, UploadContainer, type UploadContainerProps };
114
+ export { AWSPresignedUrlDTO, Container, ContainerDTO, ContainerType, Home, HomeView, type IAWSPresignedUrlDTO, type IContainerDTO, SparkStudioStorageSDK, UploadContainer, type UploadContainerProps };
package/dist/index.d.ts CHANGED
@@ -14,6 +14,7 @@ interface IContainerDTO {
14
14
  Id: string;
15
15
  ContainerType: ContainerType;
16
16
  Name?: string;
17
+ ContentType?: string;
17
18
  CreatedDate: string;
18
19
  FileSize: number;
19
20
  UserId: string;
@@ -24,6 +25,7 @@ declare class ContainerDTO implements IContainerDTO {
24
25
  Id: string;
25
26
  ContainerType: ContainerType;
26
27
  Name?: string;
28
+ ContentType?: string;
27
29
  CreatedDate: string;
28
30
  FileSize: number;
29
31
  UserId: string;
@@ -32,23 +34,19 @@ declare class ContainerDTO implements IContainerDTO {
32
34
  }
33
35
 
34
36
  /**
35
- * Represents an Auto-generated model for AWSCredentialsDTO.
37
+ * Represents an Auto-generated model for AWSPresignedUrlDTO.
36
38
  */
37
- interface IAWSCredentialsDTO {
38
- AccessKeyId?: string;
39
- SecretAccessKey?: string;
40
- SessionToken?: string;
41
- BucketName?: string;
42
- Region?: string;
39
+ interface IAWSPresignedUrlDTO {
40
+ PresignedUrl?: string;
41
+ PublicUrl?: string;
42
+ Key?: string;
43
43
  }
44
- type AWSCredentialsDTOInit = Partial<IAWSCredentialsDTO>;
45
- declare class AWSCredentialsDTO implements IAWSCredentialsDTO {
46
- AccessKeyId?: string;
47
- SecretAccessKey?: string;
48
- SessionToken?: string;
49
- BucketName?: string;
50
- Region?: string;
51
- constructor(init: AWSCredentialsDTOInit);
44
+ type AWSPresignedUrlDTOInit = Partial<IAWSPresignedUrlDTO>;
45
+ declare class AWSPresignedUrlDTO implements IAWSPresignedUrlDTO {
46
+ PresignedUrl?: string;
47
+ PublicUrl?: string;
48
+ Key?: string;
49
+ constructor(init: AWSPresignedUrlDTOInit);
52
50
  }
53
51
 
54
52
  /**
@@ -63,7 +61,7 @@ declare class Container {
63
61
  Create(containerDTO: ContainerDTO): Promise<ContainerDTO>;
64
62
  Update(containerDTO: ContainerDTO): Promise<ContainerDTO>;
65
63
  Delete(id: string): Promise<ContainerDTO>;
66
- GetUploadCredentials(): Promise<AWSCredentialsDTO>;
64
+ GetPreSignedUrl(fileName: string, size: number, contentType: string): Promise<AWSPresignedUrlDTO>;
67
65
  }
68
66
 
69
67
  /**
@@ -89,9 +87,28 @@ interface UploadContainerProps {
89
87
  multiple?: boolean;
90
88
  accept?: string;
91
89
  onFilesSelected?: (files: FileList) => void;
90
+ /**
91
+ * When true, the component will immediately start uploading to S3
92
+ * using getPresignedUrl for each file.
93
+ */
94
+ autoUpload?: boolean;
95
+ /**
96
+ * Your function that returns a pre-signed URL for a given file.
97
+ * Typically calls your backend: /api/uploads/presign
98
+ */
99
+ getPresignedUrl?: (file: File) => Promise<AWSPresignedUrlDTO>;
100
+ /**
101
+ * Called when a file has successfully finished uploading.
102
+ * s3Url is usually the final S3 object URL or key (depends on your backend).
103
+ */
104
+ onUploadComplete?: (file: File, s3Url: string) => void;
105
+ /**
106
+ * Called when a file upload fails.
107
+ */
108
+ onUploadError?: (file: File, error: Error) => void;
92
109
  }
93
110
  declare const UploadContainer: React.FC<UploadContainerProps>;
94
111
 
95
112
  declare function HomeView(): react_jsx_runtime.JSX.Element;
96
113
 
97
- export { AWSCredentialsDTO, Container, ContainerDTO, ContainerType, Home, HomeView, type IAWSCredentialsDTO, type IContainerDTO, SparkStudioStorageSDK, UploadContainer, type UploadContainerProps };
114
+ export { AWSPresignedUrlDTO, Container, ContainerDTO, ContainerType, Home, HomeView, type IAWSPresignedUrlDTO, type IContainerDTO, SparkStudioStorageSDK, UploadContainer, type UploadContainerProps };
package/dist/index.js CHANGED
@@ -90,8 +90,8 @@ var Container = class {
90
90
  if (!response.ok) throw new Error(await response.text());
91
91
  return await response.json();
92
92
  }
93
- async GetUploadCredentials() {
94
- const url = `${this.baseUrl}/api/Container/GetUploadCredentials`;
93
+ async GetPreSignedUrl(fileName, size, contentType) {
94
+ const url = `${this.baseUrl}/api/Container/GetPreSignedUrl/` + fileName + `/` + size + `/` + contentType;
95
95
  const token = localStorage.getItem("auth_token");
96
96
  const requestOptions = {
97
97
  method: "GET",
@@ -124,19 +124,15 @@ var SparkStudioStorageSDK = class {
124
124
  }
125
125
  };
126
126
 
127
- // src/api/DTOs/AWSCredentialsDTO.ts
128
- var AWSCredentialsDTO = class {
129
- AccessKeyId;
130
- SecretAccessKey;
131
- SessionToken;
132
- BucketName;
133
- Region;
127
+ // src/api/DTOs/AWSPresignedUrlDTO.ts
128
+ var AWSPresignedUrlDTO = class {
129
+ PresignedUrl;
130
+ PublicUrl;
131
+ Key;
134
132
  constructor(init) {
135
- this.AccessKeyId = init.AccessKeyId;
136
- this.SecretAccessKey = init.SecretAccessKey;
137
- this.SessionToken = init.SessionToken;
138
- this.BucketName = init.BucketName;
139
- this.Region = init.Region;
133
+ this.PresignedUrl = init.PresignedUrl;
134
+ this.PublicUrl = init.PublicUrl;
135
+ this.Key = init.Key;
140
136
  }
141
137
  };
142
138
 
@@ -145,6 +141,7 @@ var ContainerDTO = class {
145
141
  Id;
146
142
  ContainerType;
147
143
  Name;
144
+ ContentType;
148
145
  CreatedDate;
149
146
  FileSize;
150
147
  UserId;
@@ -153,6 +150,7 @@ var ContainerDTO = class {
153
150
  this.Id = init.Id;
154
151
  this.ContainerType = init.ContainerType;
155
152
  this.Name = init.Name;
153
+ this.ContentType = init.ContentType;
156
154
  this.CreatedDate = init.CreatedDate;
157
155
  this.FileSize = init.FileSize;
158
156
  this.UserId = init.UserId;
@@ -176,10 +174,67 @@ var UploadContainer = ({
176
174
  description = "Drag and drop files here, or click the button to browse.",
177
175
  multiple = true,
178
176
  accept,
179
- onFilesSelected
177
+ onFilesSelected,
178
+ autoUpload = false,
179
+ getPresignedUrl,
180
+ onUploadComplete,
181
+ onUploadError
180
182
  }) => {
181
183
  const [isDragging, setIsDragging] = useState(false);
182
184
  const [fileNames, setFileNames] = useState([]);
185
+ const [uploads, setUploads] = useState([]);
186
+ const startUploadsIfNeeded = (files) => {
187
+ if (!autoUpload || !getPresignedUrl) return;
188
+ const newUploads = Array.from(files).map((file) => ({
189
+ id: `${file.name}-${file.size}-${file.lastModified}-${Math.random()}`,
190
+ file,
191
+ progress: 0,
192
+ status: "pending"
193
+ }));
194
+ setUploads((prev) => [...prev, ...newUploads]);
195
+ newUploads.forEach((upload) => {
196
+ uploadFile(upload);
197
+ });
198
+ };
199
+ const uploadFile = async (upload) => {
200
+ setUploads(
201
+ (prev) => prev.map(
202
+ (u) => u.id === upload.id ? { ...u, status: "uploading", progress: 0 } : u
203
+ )
204
+ );
205
+ try {
206
+ if (!getPresignedUrl) {
207
+ throw new Error("getPresignedUrl is not provided.");
208
+ }
209
+ const presignedUrl = await getPresignedUrl(upload.file);
210
+ const url = presignedUrl?.PresignedUrl ?? "";
211
+ await uploadFileToS3(upload.file, url, (progress) => {
212
+ setUploads(
213
+ (prev) => prev.map(
214
+ (u) => u.id === upload.id ? { ...u, progress } : u
215
+ )
216
+ );
217
+ });
218
+ const fileUrl = url.split("?")[0];
219
+ setUploads(
220
+ (prev) => prev.map(
221
+ (u) => u.id === upload.id ? { ...u, status: "success", progress: 100, s3Url: fileUrl, publicUrl: presignedUrl.PublicUrl } : u
222
+ )
223
+ );
224
+ onUploadComplete?.(upload.file, fileUrl);
225
+ } catch (err) {
226
+ let message = "Upload failed";
227
+ if (err instanceof Error) {
228
+ message = err.message;
229
+ }
230
+ setUploads(
231
+ (prev) => prev.map(
232
+ (u) => u.id === upload.id ? { ...u, status: "error", error: message } : u
233
+ )
234
+ );
235
+ onUploadError?.(upload.file, err instanceof Error ? err : new Error(message));
236
+ }
237
+ };
183
238
  const handleDragOver = (e) => {
184
239
  e.preventDefault();
185
240
  setIsDragging(true);
@@ -195,12 +250,14 @@ var UploadContainer = ({
195
250
  if (!files || files.length === 0) return;
196
251
  setFileNames(Array.from(files).map((f) => f.name));
197
252
  onFilesSelected?.(files);
253
+ startUploadsIfNeeded(files);
198
254
  };
199
255
  const handleFileChange = (e) => {
200
256
  const files = e.target.files;
201
257
  if (!files || files.length === 0) return;
202
258
  setFileNames(Array.from(files).map((f) => f.name));
203
259
  onFilesSelected?.(files);
260
+ startUploadsIfNeeded(files);
204
261
  };
205
262
  return /* @__PURE__ */ jsx("div", { className: "container my-3", children: /* @__PURE__ */ jsx("div", { className: "card shadow-sm", children: /* @__PURE__ */ jsxs("div", { className: "card-body", children: [
206
263
  /* @__PURE__ */ jsx("h5", { className: "card-title mb-2", children: title }),
@@ -239,9 +296,60 @@ var UploadContainer = ({
239
296
  /* @__PURE__ */ jsx("div", { className: "small text-muted", children: "Selected:" }),
240
297
  /* @__PURE__ */ jsx("ul", { className: "mb-0 small", children: fileNames.map((name) => /* @__PURE__ */ jsx("li", { children: name }, name)) })
241
298
  ] })
242
- ] })
299
+ ] }),
300
+ uploads.length > 0 && /* @__PURE__ */ jsx("div", { className: "mt-3", children: uploads.map((u) => /* @__PURE__ */ jsxs("div", { className: "mb-2", children: [
301
+ /* @__PURE__ */ jsxs("div", { className: "d-flex justify-content-between small mb-1", children: [
302
+ /* @__PURE__ */ jsxs("span", { children: [
303
+ u.file.name,
304
+ " - ",
305
+ u?.publicUrl ?? ""
306
+ ] }),
307
+ /* @__PURE__ */ jsx("span", { children: u.status === "success" ? "Completed" : u.status === "error" ? "Error" : `${u.progress}%` })
308
+ ] }),
309
+ /* @__PURE__ */ jsx("div", { className: "progress", style: { height: "6px" }, children: /* @__PURE__ */ jsx(
310
+ "div",
311
+ {
312
+ className: "progress-bar " + (u.status === "success" ? "bg-success" : u.status === "error" ? "bg-danger" : ""),
313
+ role: "progressbar",
314
+ style: { width: `${u.progress}%` },
315
+ "aria-valuenow": u.progress,
316
+ "aria-valuemin": 0,
317
+ "aria-valuemax": 100
318
+ }
319
+ ) }),
320
+ u.status === "error" && /* @__PURE__ */ jsx("div", { className: "text-danger small mt-1", children: u.error ?? "Upload failed" })
321
+ ] }, u.id)) })
243
322
  ] }) }) });
244
323
  };
324
+ async function uploadFileToS3(file, presignedUrl, onProgress) {
325
+ return new Promise((resolve, reject) => {
326
+ const xhr = new XMLHttpRequest();
327
+ xhr.open("PUT", presignedUrl);
328
+ xhr.upload.onprogress = (event) => {
329
+ if (!event.lengthComputable) return;
330
+ const percent = Math.round(event.loaded / event.total * 100);
331
+ onProgress(percent);
332
+ };
333
+ xhr.onload = () => {
334
+ if (xhr.status >= 200 && xhr.status < 300) {
335
+ onProgress(100);
336
+ resolve();
337
+ } else {
338
+ reject(
339
+ new Error(`S3 upload failed with status ${xhr.status}: ${xhr.statusText}`)
340
+ );
341
+ }
342
+ };
343
+ xhr.onerror = () => {
344
+ reject(new Error("Network error while uploading to S3"));
345
+ };
346
+ xhr.setRequestHeader(
347
+ "Content-Type",
348
+ file.type || "application/octet-stream"
349
+ );
350
+ xhr.send(file);
351
+ });
352
+ }
245
353
 
246
354
  // src/views/HomeView.tsx
247
355
  import { AppSettings, AuthenticatorProvider, UserInfoCard } from "@sparkstudio/authentication-ui";
@@ -250,6 +358,11 @@ function HomeView() {
250
358
  function handleOnLoginSuccess(user) {
251
359
  alert(user?.Id);
252
360
  }
361
+ const getPresignedUrl = async (file) => {
362
+ const sdk = new SparkStudioStorageSDK("https://lf9zyufpuk.execute-api.us-east-2.amazonaws.com/Prod");
363
+ const result = await sdk.container.GetPreSignedUrl(file.name, file.size, encodeURIComponent(file.type));
364
+ return result;
365
+ };
253
366
  return /* @__PURE__ */ jsxs2(
254
367
  AuthenticatorProvider,
255
368
  {
@@ -259,13 +372,29 @@ function HomeView() {
259
372
  onLoginSuccess: handleOnLoginSuccess,
260
373
  children: [
261
374
  /* @__PURE__ */ jsx2(UserInfoCard, {}),
262
- /* @__PURE__ */ jsx2(UploadContainer, {})
375
+ /* @__PURE__ */ jsx2(
376
+ UploadContainer,
377
+ {
378
+ title: "Upload your files to S3",
379
+ description: "Drag & drop or browse files. Each file will upload with its own progress bar.",
380
+ multiple: true,
381
+ accept: "*/*",
382
+ autoUpload: true,
383
+ getPresignedUrl,
384
+ onUploadComplete: (file, s3Url) => {
385
+ console.log("Uploaded", file.name, "to", s3Url);
386
+ },
387
+ onUploadError: (file, error) => {
388
+ console.error("Failed to upload", file.name, error);
389
+ }
390
+ }
391
+ )
263
392
  ]
264
393
  }
265
394
  );
266
395
  }
267
396
  export {
268
- AWSCredentialsDTO,
397
+ AWSPresignedUrlDTO,
269
398
  Container,
270
399
  ContainerDTO,
271
400
  ContainerType,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sparkstudio/storage-ui",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "type": "module",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",
@@ -28,13 +28,15 @@
28
28
  "build": "npm run generateIndex && tsup src/index.ts --format cjs,esm --dts --tsconfig tsconfig.lib.json && sass --load-path=node_modules src/index.scss dist/index.css",
29
29
  "lint": "eslint .",
30
30
  "preview": "vite preview",
31
- "start": "vite"
31
+ "start": "npm run generateIndex && vite"
32
32
  },
33
33
  "peerDependencies": {
34
34
  "react": "18.3.1",
35
35
  "react-dom": "18.3.1"
36
36
  },
37
37
  "dependencies": {
38
+ "@aws-sdk/client-s3": "^3.958.0",
39
+ "@aws-sdk/s3-request-presigner": "^3.958.0",
38
40
  "@sparkstudio/authentication-ui": "^1.0.29",
39
41
  "@sparkstudio/common-ui": "^1.0.5",
40
42
  "barrelsby": "^2.8.1"