@sparkstudio/storage-ui 1.0.7 → 1.0.8

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,66 @@ 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
+ await uploadFileToS3(upload.file, presignedUrl, (progress) => {
244
+ setUploads(
245
+ (prev) => prev.map(
246
+ (u) => u.id === upload.id ? { ...u, progress } : u
247
+ )
248
+ );
249
+ });
250
+ const fileUrl = presignedUrl.split("?")[0];
251
+ setUploads(
252
+ (prev) => prev.map(
253
+ (u) => u.id === upload.id ? { ...u, status: "success", progress: 100, s3Url: fileUrl } : u
254
+ )
255
+ );
256
+ onUploadComplete?.(upload.file, fileUrl);
257
+ } catch (err) {
258
+ let message = "Upload failed";
259
+ if (err instanceof Error) {
260
+ message = err.message;
261
+ }
262
+ setUploads(
263
+ (prev) => prev.map(
264
+ (u) => u.id === upload.id ? { ...u, status: "error", error: message } : u
265
+ )
266
+ );
267
+ onUploadError?.(upload.file, err instanceof Error ? err : new Error(message));
268
+ }
269
+ };
216
270
  const handleDragOver = (e) => {
217
271
  e.preventDefault();
218
272
  setIsDragging(true);
@@ -228,12 +282,14 @@ var UploadContainer = ({
228
282
  if (!files || files.length === 0) return;
229
283
  setFileNames(Array.from(files).map((f) => f.name));
230
284
  onFilesSelected?.(files);
285
+ startUploadsIfNeeded(files);
231
286
  };
232
287
  const handleFileChange = (e) => {
233
288
  const files = e.target.files;
234
289
  if (!files || files.length === 0) return;
235
290
  setFileNames(Array.from(files).map((f) => f.name));
236
291
  onFilesSelected?.(files);
292
+ startUploadsIfNeeded(files);
237
293
  };
238
294
  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
295
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h5", { className: "card-title mb-2", children: title }),
@@ -272,9 +328,56 @@ var UploadContainer = ({
272
328
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "small text-muted", children: "Selected:" }),
273
329
  /* @__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
330
  ] })
275
- ] })
331
+ ] }),
332
+ 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: [
333
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "d-flex justify-content-between small mb-1", children: [
334
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: u.file.name }),
335
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: u.status === "success" ? "Completed" : u.status === "error" ? "Error" : `${u.progress}%` })
336
+ ] }),
337
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "progress", style: { height: "6px" }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
338
+ "div",
339
+ {
340
+ className: "progress-bar " + (u.status === "success" ? "bg-success" : u.status === "error" ? "bg-danger" : ""),
341
+ role: "progressbar",
342
+ style: { width: `${u.progress}%` },
343
+ "aria-valuenow": u.progress,
344
+ "aria-valuemin": 0,
345
+ "aria-valuemax": 100
346
+ }
347
+ ) }),
348
+ u.status === "error" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "text-danger small mt-1", children: u.error ?? "Upload failed" })
349
+ ] }, u.id)) })
276
350
  ] }) }) });
277
351
  };
352
+ async function uploadFileToS3(file, presignedUrl, onProgress) {
353
+ return new Promise((resolve, reject) => {
354
+ const xhr = new XMLHttpRequest();
355
+ xhr.open("PUT", presignedUrl);
356
+ xhr.upload.onprogress = (event) => {
357
+ if (!event.lengthComputable) return;
358
+ const percent = Math.round(event.loaded / event.total * 100);
359
+ onProgress(percent);
360
+ };
361
+ xhr.onload = () => {
362
+ if (xhr.status >= 200 && xhr.status < 300) {
363
+ onProgress(100);
364
+ resolve();
365
+ } else {
366
+ reject(
367
+ new Error(`S3 upload failed with status ${xhr.status}: ${xhr.statusText}`)
368
+ );
369
+ }
370
+ };
371
+ xhr.onerror = () => {
372
+ reject(new Error("Network error while uploading to S3"));
373
+ };
374
+ xhr.setRequestHeader(
375
+ "Content-Type",
376
+ file.type || "application/octet-stream"
377
+ );
378
+ xhr.send(file);
379
+ });
380
+ }
278
381
 
279
382
  // src/views/HomeView.tsx
280
383
  var import_authentication_ui = require("@sparkstudio/authentication-ui");
@@ -283,6 +386,11 @@ function HomeView() {
283
386
  function handleOnLoginSuccess(user) {
284
387
  alert(user?.Id);
285
388
  }
389
+ const getPresignedUrl = async (file) => {
390
+ const sdk = new SparkStudioStorageSDK("https://localhost:5001");
391
+ const result = await sdk.container.GetPreSignedUrl(file.name, file.size, encodeURIComponent(file.type));
392
+ return result?.PresignedUrl ?? "";
393
+ };
286
394
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
287
395
  import_authentication_ui.AuthenticatorProvider,
288
396
  {
@@ -292,14 +400,30 @@ function HomeView() {
292
400
  onLoginSuccess: handleOnLoginSuccess,
293
401
  children: [
294
402
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_authentication_ui.UserInfoCard, {}),
295
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(UploadContainer, {})
403
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
404
+ UploadContainer,
405
+ {
406
+ title: "Upload your files to S3",
407
+ description: "Drag & drop or browse files. Each file will upload with its own progress bar.",
408
+ multiple: true,
409
+ accept: "*/*",
410
+ autoUpload: true,
411
+ getPresignedUrl,
412
+ onUploadComplete: (file, s3Url) => {
413
+ console.log("Uploaded", file.name, "to", s3Url);
414
+ },
415
+ onUploadError: (file, error) => {
416
+ console.error("Failed to upload", file.name, error);
417
+ }
418
+ }
419
+ )
296
420
  ]
297
421
  }
298
422
  );
299
423
  }
300
424
  // Annotate the CommonJS export names for ESM import in node:
301
425
  0 && (module.exports = {
302
- AWSCredentialsDTO,
426
+ AWSPresignedUrlDTO,
303
427
  Container,
304
428
  ContainerDTO,
305
429
  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<string>;
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<string>;
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,66 @@ 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
+ await uploadFileToS3(upload.file, presignedUrl, (progress) => {
211
+ setUploads(
212
+ (prev) => prev.map(
213
+ (u) => u.id === upload.id ? { ...u, progress } : u
214
+ )
215
+ );
216
+ });
217
+ const fileUrl = presignedUrl.split("?")[0];
218
+ setUploads(
219
+ (prev) => prev.map(
220
+ (u) => u.id === upload.id ? { ...u, status: "success", progress: 100, s3Url: fileUrl } : u
221
+ )
222
+ );
223
+ onUploadComplete?.(upload.file, fileUrl);
224
+ } catch (err) {
225
+ let message = "Upload failed";
226
+ if (err instanceof Error) {
227
+ message = err.message;
228
+ }
229
+ setUploads(
230
+ (prev) => prev.map(
231
+ (u) => u.id === upload.id ? { ...u, status: "error", error: message } : u
232
+ )
233
+ );
234
+ onUploadError?.(upload.file, err instanceof Error ? err : new Error(message));
235
+ }
236
+ };
183
237
  const handleDragOver = (e) => {
184
238
  e.preventDefault();
185
239
  setIsDragging(true);
@@ -195,12 +249,14 @@ var UploadContainer = ({
195
249
  if (!files || files.length === 0) return;
196
250
  setFileNames(Array.from(files).map((f) => f.name));
197
251
  onFilesSelected?.(files);
252
+ startUploadsIfNeeded(files);
198
253
  };
199
254
  const handleFileChange = (e) => {
200
255
  const files = e.target.files;
201
256
  if (!files || files.length === 0) return;
202
257
  setFileNames(Array.from(files).map((f) => f.name));
203
258
  onFilesSelected?.(files);
259
+ startUploadsIfNeeded(files);
204
260
  };
205
261
  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
262
  /* @__PURE__ */ jsx("h5", { className: "card-title mb-2", children: title }),
@@ -239,9 +295,56 @@ var UploadContainer = ({
239
295
  /* @__PURE__ */ jsx("div", { className: "small text-muted", children: "Selected:" }),
240
296
  /* @__PURE__ */ jsx("ul", { className: "mb-0 small", children: fileNames.map((name) => /* @__PURE__ */ jsx("li", { children: name }, name)) })
241
297
  ] })
242
- ] })
298
+ ] }),
299
+ uploads.length > 0 && /* @__PURE__ */ jsx("div", { className: "mt-3", children: uploads.map((u) => /* @__PURE__ */ jsxs("div", { className: "mb-2", children: [
300
+ /* @__PURE__ */ jsxs("div", { className: "d-flex justify-content-between small mb-1", children: [
301
+ /* @__PURE__ */ jsx("span", { children: u.file.name }),
302
+ /* @__PURE__ */ jsx("span", { children: u.status === "success" ? "Completed" : u.status === "error" ? "Error" : `${u.progress}%` })
303
+ ] }),
304
+ /* @__PURE__ */ jsx("div", { className: "progress", style: { height: "6px" }, children: /* @__PURE__ */ jsx(
305
+ "div",
306
+ {
307
+ className: "progress-bar " + (u.status === "success" ? "bg-success" : u.status === "error" ? "bg-danger" : ""),
308
+ role: "progressbar",
309
+ style: { width: `${u.progress}%` },
310
+ "aria-valuenow": u.progress,
311
+ "aria-valuemin": 0,
312
+ "aria-valuemax": 100
313
+ }
314
+ ) }),
315
+ u.status === "error" && /* @__PURE__ */ jsx("div", { className: "text-danger small mt-1", children: u.error ?? "Upload failed" })
316
+ ] }, u.id)) })
243
317
  ] }) }) });
244
318
  };
319
+ async function uploadFileToS3(file, presignedUrl, onProgress) {
320
+ return new Promise((resolve, reject) => {
321
+ const xhr = new XMLHttpRequest();
322
+ xhr.open("PUT", presignedUrl);
323
+ xhr.upload.onprogress = (event) => {
324
+ if (!event.lengthComputable) return;
325
+ const percent = Math.round(event.loaded / event.total * 100);
326
+ onProgress(percent);
327
+ };
328
+ xhr.onload = () => {
329
+ if (xhr.status >= 200 && xhr.status < 300) {
330
+ onProgress(100);
331
+ resolve();
332
+ } else {
333
+ reject(
334
+ new Error(`S3 upload failed with status ${xhr.status}: ${xhr.statusText}`)
335
+ );
336
+ }
337
+ };
338
+ xhr.onerror = () => {
339
+ reject(new Error("Network error while uploading to S3"));
340
+ };
341
+ xhr.setRequestHeader(
342
+ "Content-Type",
343
+ file.type || "application/octet-stream"
344
+ );
345
+ xhr.send(file);
346
+ });
347
+ }
245
348
 
246
349
  // src/views/HomeView.tsx
247
350
  import { AppSettings, AuthenticatorProvider, UserInfoCard } from "@sparkstudio/authentication-ui";
@@ -250,6 +353,11 @@ function HomeView() {
250
353
  function handleOnLoginSuccess(user) {
251
354
  alert(user?.Id);
252
355
  }
356
+ const getPresignedUrl = async (file) => {
357
+ const sdk = new SparkStudioStorageSDK("https://localhost:5001");
358
+ const result = await sdk.container.GetPreSignedUrl(file.name, file.size, encodeURIComponent(file.type));
359
+ return result?.PresignedUrl ?? "";
360
+ };
253
361
  return /* @__PURE__ */ jsxs2(
254
362
  AuthenticatorProvider,
255
363
  {
@@ -259,13 +367,29 @@ function HomeView() {
259
367
  onLoginSuccess: handleOnLoginSuccess,
260
368
  children: [
261
369
  /* @__PURE__ */ jsx2(UserInfoCard, {}),
262
- /* @__PURE__ */ jsx2(UploadContainer, {})
370
+ /* @__PURE__ */ jsx2(
371
+ UploadContainer,
372
+ {
373
+ title: "Upload your files to S3",
374
+ description: "Drag & drop or browse files. Each file will upload with its own progress bar.",
375
+ multiple: true,
376
+ accept: "*/*",
377
+ autoUpload: true,
378
+ getPresignedUrl,
379
+ onUploadComplete: (file, s3Url) => {
380
+ console.log("Uploaded", file.name, "to", s3Url);
381
+ },
382
+ onUploadError: (file, error) => {
383
+ console.error("Failed to upload", file.name, error);
384
+ }
385
+ }
386
+ )
263
387
  ]
264
388
  }
265
389
  );
266
390
  }
267
391
  export {
268
- AWSCredentialsDTO,
392
+ AWSPresignedUrlDTO,
269
393
  Container,
270
394
  ContainerDTO,
271
395
  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.8",
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"