@thetechfossil/upfiles 0.6.0 → 1.0.2

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.mjs CHANGED
@@ -153,13 +153,18 @@ var Uploader = ({
153
153
  maxFiles = 10,
154
154
  onComplete,
155
155
  onError,
156
+ onUploadBegin,
157
+ onUploadProgress,
158
+ onClientUploadComplete,
156
159
  className,
157
160
  buttonClassName,
158
161
  dropzoneClassName,
159
162
  children,
160
163
  projectId,
161
164
  folderPath,
162
- fetchThumbnails
165
+ fetchThumbnails,
166
+ autoRecordToDb = false,
167
+ recordUrl = "/api/files"
163
168
  }) => {
164
169
  const inputRef = useRef(null);
165
170
  const client = useMemo(() => new UpfilesClient(clientOptions ?? {}), [clientOptions]);
@@ -167,19 +172,19 @@ var Uploader = ({
167
172
  const [isUploading, setIsUploading] = useState(false);
168
173
  const [isDragOver, setIsDragOver] = useState(false);
169
174
  const addFiles = useCallback((incoming) => {
170
- const filtered = incoming.filter((f) => {
171
- if (f.size > maxFileSize) {
175
+ const filtered = incoming.filter((f2) => {
176
+ if (f2.size > maxFileSize) {
172
177
  return false;
173
178
  }
174
179
  return true;
175
180
  });
176
181
  const limited = filtered.slice(0, Math.max(0, maxFiles - files.length));
177
- const mapped = limited.map((f) => ({
182
+ const mapped = limited.map((f2) => ({
178
183
  id: Math.random().toString(36).slice(2),
179
- file: f,
180
- name: f.name,
181
- size: f.size,
182
- type: f.type,
184
+ file: f2,
185
+ name: f2.name,
186
+ size: f2.size,
187
+ type: f2.type,
183
188
  progress: 0,
184
189
  status: "pending"
185
190
  }));
@@ -198,9 +203,10 @@ var Uploader = ({
198
203
  }, [addFiles]);
199
204
  const uploadOne = useCallback(async (item) => {
200
205
  const update = (patch) => {
201
- setFiles((prev) => prev.map((f) => f.id === item.id ? { ...f, ...patch } : f));
206
+ setFiles((prev) => prev.map((f2) => f2.id === item.id ? { ...f2, ...patch } : f2));
202
207
  };
203
208
  try {
209
+ onUploadBegin?.(item.name);
204
210
  update({ status: "uploading", progress: 1 });
205
211
  const presign = await client.getPresignedUrl({
206
212
  fileName: item.name,
@@ -216,6 +222,7 @@ var Uploader = ({
216
222
  if (ev.lengthComputable) {
217
223
  const pct = Math.min(99, Math.floor(ev.loaded / ev.total * 100));
218
224
  update({ progress: pct });
225
+ onUploadProgress?.(item.name, pct);
219
226
  }
220
227
  };
221
228
  xhr.onload = () => {
@@ -236,8 +243,39 @@ var Uploader = ({
236
243
  }
237
244
  } catch {
238
245
  }
239
- update({ status: "success", progress: 100, fileKey: presign.fileKey, publicUrl: presign.publicUrl });
240
- return {
246
+ if (autoRecordToDb && projectId) {
247
+ try {
248
+ const baseUrl = clientOptions?.baseUrl || "";
249
+ const url = baseUrl ? `${baseUrl}${recordUrl}` : recordUrl;
250
+ const headers = {
251
+ "content-type": "application/json",
252
+ ...clientOptions?.headers || {}
253
+ };
254
+ if (clientOptions?.apiKey) {
255
+ const keyHeader = clientOptions.apiKeyHeader || "authorization";
256
+ if (keyHeader === "authorization") {
257
+ headers["authorization"] = clientOptions.apiKey.startsWith("upk_") ? `Bearer ${clientOptions.apiKey}` : clientOptions.apiKey;
258
+ } else {
259
+ headers[keyHeader] = clientOptions.apiKey;
260
+ }
261
+ }
262
+ await fetch(url, {
263
+ method: "POST",
264
+ headers,
265
+ credentials: clientOptions?.withCredentials ? "include" : "same-origin",
266
+ body: JSON.stringify({
267
+ key: presign.fileKey,
268
+ originalName: item.name,
269
+ size: item.size,
270
+ contentType: item.type,
271
+ projectId
272
+ })
273
+ });
274
+ } catch (e) {
275
+ console.warn("Failed to record file to database:", e);
276
+ }
277
+ }
278
+ const result = {
241
279
  ...item,
242
280
  status: "success",
243
281
  progress: 100,
@@ -248,26 +286,29 @@ var Uploader = ({
248
286
  // @ts-ignore - allow downstream to access thumbs if present
249
287
  thumbnails: thumbs
250
288
  };
289
+ update({ status: "success", progress: 100, fileKey: presign.fileKey, publicUrl: presign.publicUrl });
290
+ onClientUploadComplete?.(result);
291
+ return result;
251
292
  } catch (err) {
252
293
  const message = err instanceof Error ? err.message : "Upload failed";
253
294
  update({ status: "error", error: message });
254
295
  throw err;
255
296
  }
256
- }, [client, projectId, folderPath, fetchThumbnails]);
297
+ }, [client, projectId, folderPath, fetchThumbnails, autoRecordToDb, recordUrl, clientOptions, onUploadBegin, onUploadProgress, onClientUploadComplete]);
257
298
  const uploadAll = useCallback(async () => {
258
- const targets = files.filter((f) => f.status === "pending");
299
+ const targets = files.filter((f2) => f2.status === "pending");
259
300
  if (!targets.length) return;
260
301
  setIsUploading(true);
261
302
  try {
262
303
  const results = [];
263
- for (const f of targets) {
304
+ for (const f2 of targets) {
264
305
  try {
265
- const done = await uploadOne(f);
306
+ const done = await uploadOne(f2);
266
307
  results.push(done);
267
308
  } catch (e) {
268
309
  }
269
310
  }
270
- onComplete?.(results.filter((f) => f.status === "success"));
311
+ onComplete?.(results.filter((f2) => f2.status === "success"));
271
312
  } catch (e) {
272
313
  onError?.(e);
273
314
  } finally {
@@ -321,7 +362,7 @@ var Uploader = ({
321
362
  "button",
322
363
  {
323
364
  className: buttonClassName,
324
- disabled: isUploading || files.every((f) => f.status !== "pending"),
365
+ disabled: isUploading || files.every((f2) => f2.status !== "pending"),
325
366
  onClick: uploadAll,
326
367
  children: isUploading ? "Uploading..." : "Upload"
327
368
  }
@@ -336,21 +377,21 @@ var Uploader = ({
336
377
  }
337
378
  )
338
379
  ] }),
339
- /* @__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: [
380
+ /* @__PURE__ */ jsx("ul", { style: { display: "grid", gap: 8, listStyle: "none", padding: 0 }, children: files.map((f2) => /* @__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: [
340
381
  /* @__PURE__ */ jsxs("div", { style: { minWidth: 0 }, children: [
341
- /* @__PURE__ */ jsx("div", { style: { fontWeight: 500, overflow: "hidden", textOverflow: "ellipsis" }, children: f.name }),
382
+ /* @__PURE__ */ jsx("div", { style: { fontWeight: 500, overflow: "hidden", textOverflow: "ellipsis" }, children: f2.name }),
342
383
  /* @__PURE__ */ jsxs("div", { style: { fontSize: 12, color: "#666" }, children: [
343
- (f.size / (1024 * 1024)).toFixed(2),
384
+ (f2.size / (1024 * 1024)).toFixed(2),
344
385
  " MB"
345
386
  ] }),
346
- f.error && /* @__PURE__ */ jsx("div", { style: { fontSize: 12, color: "#b91c1c" }, children: f.error }),
347
- f.publicUrl && f.status === "success" && /* @__PURE__ */ jsx("a", { href: f.publicUrl, target: "_blank", rel: "noreferrer", style: { fontSize: 12, color: "#2563eb" }, children: "View" })
387
+ f2.error && /* @__PURE__ */ jsx("div", { style: { fontSize: 12, color: "#b91c1c" }, children: f2.error }),
388
+ f2.publicUrl && f2.status === "success" && /* @__PURE__ */ jsx("a", { href: f2.publicUrl, target: "_blank", rel: "noreferrer", style: { fontSize: 12, color: "#2563eb" }, children: "View" })
348
389
  ] }),
349
390
  /* @__PURE__ */ jsxs("div", { style: { minWidth: 120, textAlign: "right" }, children: [
350
- /* @__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" } }) }),
351
- /* @__PURE__ */ jsx("div", { style: { fontSize: 12, color: "#666", marginTop: 4 }, children: f.status })
391
+ /* @__PURE__ */ jsx("div", { style: { height: 6, background: "#e5e7eb", borderRadius: 999, overflow: "hidden" }, children: /* @__PURE__ */ jsx("div", { style: { width: `${f2.progress}%`, height: "100%", background: f2.status === "error" ? "#ef4444" : "#2563eb" } }) }),
392
+ /* @__PURE__ */ jsx("div", { style: { fontSize: 12, color: "#666", marginTop: 4 }, children: f2.status })
352
393
  ] })
353
- ] }) }, f.id)) })
394
+ ] }) }, f2.id)) })
354
395
  ] })
355
396
  ] });
356
397
  };
@@ -367,7 +408,10 @@ var ProjectFilesWidget = ({
367
408
  onSelect,
368
409
  saveUrl,
369
410
  onSave,
370
- onSaved
411
+ onSaved,
412
+ onDelete,
413
+ deleteUrl = "/api/files",
414
+ showDelete = false
371
415
  }) => {
372
416
  const client = useMemo2(() => new UpfilesClient(clientOptions ?? {}), [clientOptions]);
373
417
  const [files, setFiles] = useState2([]);
@@ -375,6 +419,7 @@ var ProjectFilesWidget = ({
375
419
  const [error, setError] = useState2(null);
376
420
  const [query, setQuery] = useState2("");
377
421
  const [savingKey, setSavingKey] = useState2(null);
422
+ const [deletingKey, setDeletingKey] = useState2(null);
378
423
  useEffect(() => {
379
424
  let cancelled = false;
380
425
  const run = async () => {
@@ -395,8 +440,44 @@ var ProjectFilesWidget = ({
395
440
  };
396
441
  }, [client, folderPath]);
397
442
  const visible = files.filter(
398
- (f) => !query ? true : (f.originalName || "").toLowerCase().includes(query.toLowerCase())
443
+ (f2) => !query ? true : (f2.originalName || "").toLowerCase().includes(query.toLowerCase())
399
444
  );
445
+ const handleDelete = async (key) => {
446
+ if (!confirm("Delete this file?")) return;
447
+ try {
448
+ setDeletingKey(key);
449
+ if (onDelete) {
450
+ await onDelete(key);
451
+ } else {
452
+ const baseUrl = clientOptions?.baseUrl || "";
453
+ const url = baseUrl ? `${baseUrl}${deleteUrl}` : deleteUrl;
454
+ const headers = {
455
+ "content-type": "application/json",
456
+ ...clientOptions?.headers || {}
457
+ };
458
+ if (clientOptions?.apiKey) {
459
+ const keyHeader = clientOptions.apiKeyHeader || "authorization";
460
+ if (keyHeader === "authorization") {
461
+ headers["authorization"] = clientOptions.apiKey.startsWith("upk_") ? `Bearer ${clientOptions.apiKey}` : clientOptions.apiKey;
462
+ } else {
463
+ headers[keyHeader] = clientOptions.apiKey;
464
+ }
465
+ }
466
+ const res = await fetch(url, {
467
+ method: "DELETE",
468
+ headers,
469
+ credentials: clientOptions?.withCredentials ? "include" : "same-origin",
470
+ body: JSON.stringify({ fileKey: key })
471
+ });
472
+ if (!res.ok) throw new Error("Delete failed");
473
+ }
474
+ setFiles((prev) => prev.filter((f2) => f2.key !== key));
475
+ } catch (e) {
476
+ alert(e?.message || "Failed to delete");
477
+ } finally {
478
+ setDeletingKey(null);
479
+ }
480
+ };
400
481
  return /* @__PURE__ */ jsxs2("div", { className, children: [
401
482
  /* @__PURE__ */ jsxs2("div", { style: { display: "flex", gap: 8, alignItems: "center", marginBottom: 8 }, children: [
402
483
  /* @__PURE__ */ jsx2(
@@ -435,31 +516,31 @@ var ProjectFilesWidget = ({
435
516
  loading && /* @__PURE__ */ jsx2("div", { style: { fontSize: 12, color: "#666" }, children: "Loading files\u2026" }),
436
517
  error && /* @__PURE__ */ jsx2("div", { style: { fontSize: 12, color: "#b91c1c" }, children: error }),
437
518
  !loading && !error && /* @__PURE__ */ jsxs2("ul", { className: listClassName, style: { listStyle: "none", padding: 0, margin: 0, display: "grid", gap: 8 }, children: [
438
- visible.map((f) => /* @__PURE__ */ jsxs2(
519
+ visible.map((f2) => /* @__PURE__ */ jsxs2(
439
520
  "li",
440
521
  {
441
522
  className: itemClassName,
442
523
  style: { border: "1px solid #e5e7eb", borderRadius: 8, padding: 10, display: "flex", justifyContent: "space-between", alignItems: "center" },
443
524
  children: [
444
525
  /* @__PURE__ */ jsxs2("div", { style: { minWidth: 0 }, children: [
445
- /* @__PURE__ */ jsx2("div", { style: { fontWeight: 500, overflow: "hidden", textOverflow: "ellipsis" }, children: f.originalName }),
526
+ /* @__PURE__ */ jsx2("div", { style: { fontWeight: 500, overflow: "hidden", textOverflow: "ellipsis" }, children: f2.originalName }),
446
527
  /* @__PURE__ */ jsxs2("div", { style: { fontSize: 12, color: "#666" }, children: [
447
- (f.size / (1024 * 1024)).toFixed(2),
528
+ (f2.size / (1024 * 1024)).toFixed(2),
448
529
  " MB \u2022 ",
449
- f.contentType
530
+ f2.contentType
450
531
  ] })
451
532
  ] }),
452
533
  /* @__PURE__ */ jsxs2("div", { style: { display: "flex", gap: 8, alignItems: "center" }, children: [
453
- /* @__PURE__ */ jsx2("a", { href: f.url, target: "_blank", rel: "noreferrer", style: { fontSize: 12, color: "#2563eb" }, children: "View" }),
534
+ /* @__PURE__ */ jsx2("a", { href: f2.url, target: "_blank", rel: "noreferrer", style: { fontSize: 12, color: "#2563eb" }, children: "View" }),
454
535
  /* @__PURE__ */ jsx2(
455
536
  "button",
456
537
  {
457
538
  onClick: async () => {
458
- const selected = { name: f.originalName, key: f.key, url: f.url, size: f.size, contentType: f.contentType };
539
+ const selected = { name: f2.originalName, key: f2.key, url: f2.url, size: f2.size, contentType: f2.contentType };
459
540
  onSelect(selected);
460
541
  if (typeof onSave === "function" || !!saveUrl) {
461
542
  try {
462
- setSavingKey(f.key);
543
+ setSavingKey(f2.key);
463
544
  if (typeof onSave === "function") {
464
545
  await onSave(selected);
465
546
  onSaved?.();
@@ -483,22 +564,579 @@ var ProjectFilesWidget = ({
483
564
  }
484
565
  }
485
566
  },
486
- disabled: savingKey === f.key,
487
- style: { padding: "6px 10px", background: "#2563eb", color: "#fff", border: "1px solid #1d4ed8", borderRadius: 6, opacity: savingKey === f.key ? 0.7 : 1 },
488
- children: savingKey === f.key ? "Saving\u2026" : "Use"
567
+ disabled: savingKey === f2.key,
568
+ style: { padding: "6px 10px", background: "#2563eb", color: "#fff", border: "1px solid #1d4ed8", borderRadius: 6, opacity: savingKey === f2.key ? 0.7 : 1 },
569
+ children: savingKey === f2.key ? "Saving\u2026" : "Use"
570
+ }
571
+ ),
572
+ showDelete && /* @__PURE__ */ jsx2(
573
+ "button",
574
+ {
575
+ onClick: () => handleDelete(f2.key),
576
+ disabled: deletingKey === f2.key,
577
+ style: { padding: "6px 10px", background: "#dc2626", color: "#fff", border: "1px solid #b91c1c", borderRadius: 6, opacity: deletingKey === f2.key ? 0.7 : 1 },
578
+ children: deletingKey === f2.key ? "Deleting\u2026" : "Delete"
489
579
  }
490
580
  )
491
581
  ] })
492
582
  ]
493
583
  },
494
- f.key
584
+ f2.key
495
585
  )),
496
586
  visible.length === 0 && /* @__PURE__ */ jsx2("li", { style: { fontSize: 12, color: "#666" }, children: "No files found" })
497
587
  ] })
498
588
  ] });
499
589
  };
590
+
591
+ // src/ConnectProjectDialog.tsx
592
+ import * as React3 from "react";
593
+ import * as Dialog from "@radix-ui/react-dialog";
594
+
595
+ // src/projectConnect.ts
596
+ var f = (globalThis.fetch || fetch).bind(globalThis);
597
+ async function httpJson(input, init, useFetch) {
598
+ const doFetch = useFetch ?? f;
599
+ const res = await doFetch(input, init);
600
+ if (!res.ok) {
601
+ let msg = `Request failed (${res.status})`;
602
+ try {
603
+ const j = await res.json();
604
+ msg = j?.error || msg;
605
+ } catch {
606
+ }
607
+ throw new Error(msg);
608
+ }
609
+ return res.json();
610
+ }
611
+ async function listProjects(opts) {
612
+ const url = `${opts.baseUrl.replace(/\/$/, "")}/api/projects`;
613
+ const data = await httpJson(
614
+ url,
615
+ { method: "GET", credentials: "include" },
616
+ opts.fetch
617
+ );
618
+ return data?.projects ?? [];
619
+ }
620
+ async function createProject(opts) {
621
+ const base = opts.baseUrl.replace(/\/$/, "");
622
+ const created = await httpJson(
623
+ `${base}/api/projects`,
624
+ {
625
+ method: "POST",
626
+ credentials: "include",
627
+ headers: { "content-type": "application/json" },
628
+ body: JSON.stringify({ name: opts.name })
629
+ },
630
+ opts.fetch
631
+ );
632
+ const projectId = created?.project?.id;
633
+ if (!projectId) throw new Error("Failed to create project");
634
+ const keyLabel = opts.keyName || `Plugin key ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`;
635
+ const key = await httpJson(
636
+ `${base}/api/projects/${projectId}/keys`,
637
+ {
638
+ method: "POST",
639
+ credentials: "include",
640
+ headers: { "content-type": "application/json" },
641
+ body: JSON.stringify({ name: keyLabel })
642
+ },
643
+ opts.fetch
644
+ );
645
+ const plaintext = key?.key?.plaintext;
646
+ if (!plaintext) throw new Error("Failed to create API key");
647
+ return { apiKey: plaintext, projectId };
648
+ }
649
+ async function connectProject(opts) {
650
+ const base = opts.baseUrl.replace(/\/$/, "");
651
+ const keyLabel = opts.keyName || `Plugin key ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`;
652
+ const key = await httpJson(
653
+ `${base}/api/projects/${opts.projectId}/keys`,
654
+ {
655
+ method: "POST",
656
+ credentials: "include",
657
+ headers: { "content-type": "application/json" },
658
+ body: JSON.stringify({ name: keyLabel })
659
+ },
660
+ opts.fetch
661
+ );
662
+ const plaintext = key?.key?.plaintext;
663
+ if (!plaintext) throw new Error("Failed to create API key");
664
+ return { apiKey: plaintext };
665
+ }
666
+ function addApiKeyManually(apiKey) {
667
+ const k = (apiKey || "").trim();
668
+ if (!k) throw new Error("API key is required");
669
+ if (!/^upk_[A-Za-z0-9]+/.test(k)) {
670
+ console.warn("Provided API key does not match expected format upk_*");
671
+ }
672
+ return { apiKey: k };
673
+ }
674
+ function createClientWithKey(baseUrl, apiKey) {
675
+ return new UpfilesClient({ baseUrl, apiKey, apiKeyHeader: "authorization" });
676
+ }
677
+ function connectUsingApiKey(opts) {
678
+ const baseUrl = opts.baseUrl.replace(/\/$/, "");
679
+ const apiKey = (opts.apiKey || "").trim();
680
+ if (!apiKey) throw new Error("apiKey is required");
681
+ const client = new UpfilesClient({ baseUrl, apiKey, apiKeyHeader: opts.apiKeyHeader ?? "authorization" });
682
+ return { apiKey, client };
683
+ }
684
+
685
+ // src/ConnectProjectDialog.tsx
686
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
687
+ function ConnectProjectDialog(props) {
688
+ const { baseUrl, open, onOpenChange, onConnected, fetchImpl, title, description, className } = props;
689
+ const [mode, setMode] = React3.useState("existing");
690
+ const [loading, setLoading] = React3.useState(false);
691
+ const [error, setError] = React3.useState(null);
692
+ const [projects, setProjects] = React3.useState([]);
693
+ const [selectedProjectId, setSelectedProjectId] = React3.useState("");
694
+ const [manualKey, setManualKey] = React3.useState("");
695
+ const [newProjectName, setNewProjectName] = React3.useState("");
696
+ const resetState = React3.useCallback(() => {
697
+ setMode("existing");
698
+ setLoading(false);
699
+ setError(null);
700
+ setProjects([]);
701
+ setSelectedProjectId("");
702
+ setManualKey("");
703
+ setNewProjectName("");
704
+ }, []);
705
+ React3.useEffect(() => {
706
+ const run = async () => {
707
+ if (!open || mode !== "existing") return;
708
+ try {
709
+ setLoading(true);
710
+ setError(null);
711
+ const opts = { baseUrl, fetch: fetchImpl };
712
+ const list = await listProjects(opts);
713
+ setProjects(list);
714
+ if (list.length && !selectedProjectId) setSelectedProjectId(list[0].id);
715
+ } catch (e) {
716
+ setError(e?.message || "Failed to fetch projects");
717
+ } finally {
718
+ setLoading(false);
719
+ }
720
+ };
721
+ run();
722
+ }, [open, mode, baseUrl]);
723
+ const handleConnectExisting = async () => {
724
+ try {
725
+ setLoading(true);
726
+ setError(null);
727
+ if (!selectedProjectId) throw new Error("Please select a project");
728
+ const { apiKey } = await connectProject({ baseUrl, projectId: selectedProjectId, fetch: fetchImpl });
729
+ onConnected(apiKey, { projectId: selectedProjectId, source: "existing" });
730
+ onOpenChange(false);
731
+ resetState();
732
+ } catch (e) {
733
+ setError(e?.message || "Failed to connect project");
734
+ } finally {
735
+ setLoading(false);
736
+ }
737
+ };
738
+ const handleManual = async () => {
739
+ try {
740
+ setLoading(true);
741
+ setError(null);
742
+ const { apiKey } = addApiKeyManually(manualKey);
743
+ onConnected(apiKey, { source: "manual" });
744
+ onOpenChange(false);
745
+ resetState();
746
+ } catch (e) {
747
+ setError(e?.message || "Invalid API key");
748
+ } finally {
749
+ setLoading(false);
750
+ }
751
+ };
752
+ const handleCreate = async () => {
753
+ try {
754
+ setLoading(true);
755
+ setError(null);
756
+ if (!newProjectName.trim()) throw new Error("Project name is required");
757
+ const { apiKey, projectId } = await createProject({ baseUrl, name: newProjectName.trim(), fetch: fetchImpl });
758
+ onConnected(apiKey, { projectId, source: "new" });
759
+ onOpenChange(false);
760
+ resetState();
761
+ } catch (e) {
762
+ setError(e?.message || "Failed to create project");
763
+ } finally {
764
+ setLoading(false);
765
+ }
766
+ };
767
+ const ModeSwitcher = /* @__PURE__ */ jsxs3("div", { className: "grid grid-cols-3 gap-2", children: [
768
+ /* @__PURE__ */ jsx3(
769
+ "button",
770
+ {
771
+ type: "button",
772
+ className: `px-3 py-2 rounded border text-sm ${mode === "existing" ? "bg-primary text-primary-foreground" : "bg-background"} `,
773
+ onClick: () => setMode("existing"),
774
+ children: "Connect existing"
775
+ }
776
+ ),
777
+ /* @__PURE__ */ jsx3(
778
+ "button",
779
+ {
780
+ type: "button",
781
+ className: `px-3 py-2 rounded border text-sm ${mode === "manual" ? "bg-primary text-primary-foreground" : "bg-background"} `,
782
+ onClick: () => setMode("manual"),
783
+ children: "Add API key"
784
+ }
785
+ ),
786
+ /* @__PURE__ */ jsx3(
787
+ "button",
788
+ {
789
+ type: "button",
790
+ className: `px-3 py-2 rounded border text-sm ${mode === "new" ? "bg-primary text-primary-foreground" : "bg-background"} `,
791
+ onClick: () => setMode("new"),
792
+ children: "Create new"
793
+ }
794
+ )
795
+ ] });
796
+ return /* @__PURE__ */ jsx3(Dialog.Root, { open, onOpenChange: (o) => {
797
+ if (!o) resetState();
798
+ onOpenChange(o);
799
+ }, children: /* @__PURE__ */ jsxs3(Dialog.Portal, { children: [
800
+ /* @__PURE__ */ jsx3(Dialog.Overlay, { className: "fixed inset-0 bg-black/40" }),
801
+ /* @__PURE__ */ jsx3(Dialog.Content, { className: `fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-[95vw] max-w-md rounded-lg border bg-white p-4 shadow-lg ${className ?? ""}`, children: /* @__PURE__ */ jsxs3("div", { className: "space-y-3", children: [
802
+ /* @__PURE__ */ jsx3(Dialog.Title, { className: "text-lg font-semibold", children: title ?? "Connect to Upfiles" }),
803
+ /* @__PURE__ */ jsx3(Dialog.Description, { className: "text-sm text-muted-foreground", children: description ?? "Choose how to connect your project to retrieve an API key." }),
804
+ ModeSwitcher,
805
+ error && /* @__PURE__ */ jsx3("div", { className: "rounded border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700", children: error }),
806
+ mode === "existing" && /* @__PURE__ */ jsxs3("div", { className: "space-y-2", children: [
807
+ /* @__PURE__ */ jsx3("label", { className: "text-sm font-medium", children: "Project" }),
808
+ /* @__PURE__ */ jsxs3(
809
+ "select",
810
+ {
811
+ className: "w-full rounded border px-3 py-2 text-sm",
812
+ value: selectedProjectId,
813
+ onChange: (e) => setSelectedProjectId(e.target.value),
814
+ disabled: loading,
815
+ children: [
816
+ projects.length === 0 && /* @__PURE__ */ jsx3("option", { value: "", children: loading ? "Loading..." : "No projects found" }),
817
+ projects.map((p) => /* @__PURE__ */ jsx3("option", { value: p.id, children: p.name }, p.id))
818
+ ]
819
+ }
820
+ ),
821
+ /* @__PURE__ */ jsxs3("div", { className: "flex justify-end gap-2 pt-2", children: [
822
+ /* @__PURE__ */ jsx3(Dialog.Close, { asChild: true, children: /* @__PURE__ */ jsx3("button", { type: "button", className: "px-3 py-2 text-sm rounded border", children: "Cancel" }) }),
823
+ /* @__PURE__ */ jsx3(
824
+ "button",
825
+ {
826
+ type: "button",
827
+ className: "px-3 py-2 text-sm rounded bg-blue-600 text-white disabled:opacity-60",
828
+ onClick: handleConnectExisting,
829
+ disabled: loading || !selectedProjectId,
830
+ children: loading ? "Connecting\u2026" : "Connect"
831
+ }
832
+ )
833
+ ] })
834
+ ] }),
835
+ mode === "manual" && /* @__PURE__ */ jsxs3("div", { className: "space-y-2", children: [
836
+ /* @__PURE__ */ jsx3("label", { className: "text-sm font-medium", children: "API key" }),
837
+ /* @__PURE__ */ jsx3(
838
+ "input",
839
+ {
840
+ className: "w-full rounded border px-3 py-2 text-sm",
841
+ placeholder: "upk_...",
842
+ value: manualKey,
843
+ onChange: (e) => setManualKey(e.target.value),
844
+ disabled: loading
845
+ }
846
+ ),
847
+ /* @__PURE__ */ jsxs3("div", { className: "flex justify-end gap-2 pt-2", children: [
848
+ /* @__PURE__ */ jsx3(Dialog.Close, { asChild: true, children: /* @__PURE__ */ jsx3("button", { type: "button", className: "px-3 py-2 text-sm rounded border", children: "Cancel" }) }),
849
+ /* @__PURE__ */ jsx3(
850
+ "button",
851
+ {
852
+ type: "button",
853
+ className: "px-3 py-2 text-sm rounded bg-blue-600 text-white disabled:opacity-60",
854
+ onClick: handleManual,
855
+ disabled: loading || !manualKey.trim(),
856
+ children: loading ? "Adding\u2026" : "Add key"
857
+ }
858
+ )
859
+ ] })
860
+ ] }),
861
+ mode === "new" && /* @__PURE__ */ jsxs3("div", { className: "space-y-2", children: [
862
+ /* @__PURE__ */ jsx3("label", { className: "text-sm font-medium", children: "Project name" }),
863
+ /* @__PURE__ */ jsx3(
864
+ "input",
865
+ {
866
+ className: "w-full rounded border px-3 py-2 text-sm",
867
+ placeholder: "My project",
868
+ value: newProjectName,
869
+ onChange: (e) => setNewProjectName(e.target.value),
870
+ disabled: loading
871
+ }
872
+ ),
873
+ /* @__PURE__ */ jsx3("p", { className: "text-xs text-muted-foreground", children: "A project will be created in your Upfiles account and an API key generated." }),
874
+ /* @__PURE__ */ jsxs3("div", { className: "flex justify-end gap-2 pt-2", children: [
875
+ /* @__PURE__ */ jsx3(Dialog.Close, { asChild: true, children: /* @__PURE__ */ jsx3("button", { type: "button", className: "px-3 py-2 text-sm rounded border", children: "Cancel" }) }),
876
+ /* @__PURE__ */ jsx3(
877
+ "button",
878
+ {
879
+ type: "button",
880
+ className: "px-3 py-2 text-sm rounded bg-blue-600 text-white disabled:opacity-60",
881
+ onClick: handleCreate,
882
+ disabled: loading || !newProjectName.trim(),
883
+ children: loading ? "Creating\u2026" : "Create & connect"
884
+ }
885
+ )
886
+ ] })
887
+ ] })
888
+ ] }) })
889
+ ] }) });
890
+ }
891
+
892
+ // src/files.ts
893
+ async function fetchProjectFiles(clientOptions, folderPath) {
894
+ const client = new UpfilesClient(clientOptions);
895
+ return client.getProjectFiles({ folderPath });
896
+ }
897
+ async function fetchFileThumbnails(clientOptions, fileKey) {
898
+ const client = new UpfilesClient(clientOptions);
899
+ return client.getThumbnails(fileKey);
900
+ }
901
+ async function fetchProjectImagesWithThumbs(clientOptions, opts) {
902
+ const files = await fetchProjectFiles(clientOptions, opts?.folderPath);
903
+ const limit = Math.max(1, Math.min(opts?.limitConcurrent ?? 6, 12));
904
+ const queue = [...files];
905
+ const results = [];
906
+ async function worker() {
907
+ while (queue.length) {
908
+ const f2 = queue.shift();
909
+ try {
910
+ const thumbs = await fetchFileThumbnails(clientOptions, f2.key).catch(() => void 0);
911
+ results.push({ ...f2, thumbnails: thumbs });
912
+ } catch {
913
+ results.push({ ...f2 });
914
+ }
915
+ }
916
+ }
917
+ await Promise.all(Array.from({ length: Math.min(limit, files.length || 1) }, () => worker()));
918
+ return results;
919
+ }
920
+
921
+ // src/ImageManager.tsx
922
+ import * as React4 from "react";
923
+ import * as Dialog2 from "@radix-ui/react-dialog";
924
+ import { Fragment, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
925
+ function ImageManager(props) {
926
+ const {
927
+ open,
928
+ onOpenChange,
929
+ clientOptions,
930
+ projectId,
931
+ folderPath,
932
+ title,
933
+ description,
934
+ className,
935
+ gridClassName,
936
+ onSelect,
937
+ onDelete,
938
+ deleteUrl = "/api/files",
939
+ autoRecordToDb = true,
940
+ fetchThumbnails = true,
941
+ maxFileSize = 100 * 1024 * 1024,
942
+ maxFiles = 10,
943
+ mode = "full",
944
+ showDelete = true
945
+ } = props;
946
+ const [tab, setTab] = React4.useState(mode === "upload" ? "upload" : "browse");
947
+ const [loading, setLoading] = React4.useState(false);
948
+ const [error, setError] = React4.useState(null);
949
+ const [items, setItems] = React4.useState([]);
950
+ const [query, setQuery] = React4.useState("");
951
+ const [deleting, setDeleting] = React4.useState(null);
952
+ const load = React4.useCallback(async () => {
953
+ try {
954
+ setLoading(true);
955
+ setError(null);
956
+ const list = await fetchProjectImagesWithThumbs(clientOptions, { folderPath, limitConcurrent: 6 });
957
+ setItems(list);
958
+ } catch (e) {
959
+ setError(e?.message || "Failed to load images");
960
+ } finally {
961
+ setLoading(false);
962
+ }
963
+ }, [clientOptions, folderPath]);
964
+ React4.useEffect(() => {
965
+ if (!open) return;
966
+ if (mode !== "upload" && tab === "browse") {
967
+ load();
968
+ }
969
+ }, [open, tab, load, mode]);
970
+ const visible = React4.useMemo(() => {
971
+ const q = query.trim().toLowerCase();
972
+ return !q ? items : items.filter((i) => (i.originalName || "").toLowerCase().includes(q));
973
+ }, [items, query]);
974
+ const handleDelete = React4.useCallback(
975
+ async (key) => {
976
+ if (!confirm("Delete this file?")) return;
977
+ try {
978
+ setDeleting(key);
979
+ if (onDelete) {
980
+ await onDelete(key);
981
+ } else {
982
+ const baseUrl = clientOptions?.baseUrl || "";
983
+ const url = baseUrl ? `${baseUrl}${deleteUrl}` : deleteUrl;
984
+ const headers = {
985
+ "content-type": "application/json",
986
+ ...clientOptions?.headers || {}
987
+ };
988
+ if (clientOptions?.apiKey) {
989
+ const keyHeader = clientOptions.apiKeyHeader || "authorization";
990
+ if (keyHeader === "authorization") {
991
+ headers["authorization"] = clientOptions.apiKey.startsWith("upk_") ? `Bearer ${clientOptions.apiKey}` : clientOptions.apiKey;
992
+ } else {
993
+ headers[keyHeader] = clientOptions.apiKey;
994
+ }
995
+ }
996
+ const res = await fetch(url, {
997
+ method: "DELETE",
998
+ headers,
999
+ credentials: clientOptions?.withCredentials ? "include" : "same-origin",
1000
+ body: JSON.stringify({ fileKey: key })
1001
+ });
1002
+ if (!res.ok) throw new Error("Delete failed");
1003
+ }
1004
+ setItems((prev) => prev.filter((f2) => f2.key !== key));
1005
+ } catch (e) {
1006
+ alert(e?.message || "Failed to delete");
1007
+ } finally {
1008
+ setDeleting(null);
1009
+ }
1010
+ },
1011
+ [clientOptions, deleteUrl, onDelete]
1012
+ );
1013
+ return /* @__PURE__ */ jsx4(Dialog2.Root, { open, onOpenChange, children: /* @__PURE__ */ jsxs4(Dialog2.Portal, { children: [
1014
+ /* @__PURE__ */ jsx4(Dialog2.Overlay, { className: "fixed inset-0 bg-black/40" }),
1015
+ /* @__PURE__ */ jsx4(
1016
+ Dialog2.Content,
1017
+ {
1018
+ className: `fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-[95vw] max-w-4xl rounded-lg border bg-white p-4 shadow-lg max-h-[90vh] overflow-auto ${className ?? ""}`,
1019
+ children: /* @__PURE__ */ jsxs4("div", { className: "space-y-3", children: [
1020
+ /* @__PURE__ */ jsx4(Dialog2.Title, { className: "text-lg font-semibold", children: title ?? "Image Manager" }),
1021
+ /* @__PURE__ */ jsx4(Dialog2.Description, { className: "text-sm text-muted-foreground", children: description ?? "Upload new images or select from existing ones." }),
1022
+ mode === "full" && /* @__PURE__ */ jsxs4("div", { className: "flex gap-2 border-b", children: [
1023
+ /* @__PURE__ */ jsx4(
1024
+ "button",
1025
+ {
1026
+ type: "button",
1027
+ onClick: () => setTab("browse"),
1028
+ className: `px-4 py-2 text-sm font-medium border-b-2 transition-colors ${tab === "browse" ? "border-blue-600 text-blue-600" : "border-transparent text-gray-600 hover:text-gray-900"}`,
1029
+ children: "Browse"
1030
+ }
1031
+ ),
1032
+ /* @__PURE__ */ jsx4(
1033
+ "button",
1034
+ {
1035
+ type: "button",
1036
+ onClick: () => setTab("upload"),
1037
+ className: `px-4 py-2 text-sm font-medium border-b-2 transition-colors ${tab === "upload" ? "border-blue-600 text-blue-600" : "border-transparent text-gray-600 hover:text-gray-900"}`,
1038
+ children: "Upload"
1039
+ }
1040
+ )
1041
+ ] }),
1042
+ (mode === "browse" || mode === "full" && tab === "browse") && /* @__PURE__ */ jsxs4(Fragment, { children: [
1043
+ /* @__PURE__ */ jsxs4("div", { className: "flex gap-2 items-center", children: [
1044
+ /* @__PURE__ */ jsx4(
1045
+ "input",
1046
+ {
1047
+ className: "w-full rounded border px-3 py-2 text-sm",
1048
+ placeholder: "Search by filename...",
1049
+ value: query,
1050
+ onChange: (e) => setQuery(e.target.value)
1051
+ }
1052
+ ),
1053
+ /* @__PURE__ */ jsx4("button", { type: "button", className: "px-3 py-2 text-sm rounded border", onClick: load, disabled: loading, children: loading ? "Loading\u2026" : "Refresh" })
1054
+ ] }),
1055
+ error && /* @__PURE__ */ jsx4("div", { className: "rounded border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700", children: error }),
1056
+ /* @__PURE__ */ jsxs4("div", { className: gridClassName ?? "grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-3 max-h-[50vh] overflow-auto", children: [
1057
+ !loading && visible.map((f2) => {
1058
+ const thumbUrl = f2.thumbnails?.[0]?.url || f2.url;
1059
+ return /* @__PURE__ */ jsxs4("div", { className: "group relative aspect-square overflow-hidden rounded border", children: [
1060
+ /* @__PURE__ */ jsx4("img", { src: thumbUrl, alt: f2.originalName, className: "h-full w-full object-cover" }),
1061
+ /* @__PURE__ */ jsxs4("div", { className: "absolute inset-0 bg-black/0 group-hover:bg-black/40 transition-colors flex items-center justify-center gap-2", children: [
1062
+ /* @__PURE__ */ jsx4(
1063
+ "button",
1064
+ {
1065
+ type: "button",
1066
+ onClick: () => {
1067
+ onSelect({
1068
+ url: f2.url,
1069
+ key: f2.key,
1070
+ originalName: f2.originalName,
1071
+ size: f2.size,
1072
+ contentType: f2.contentType,
1073
+ thumbnails: f2.thumbnails
1074
+ });
1075
+ onOpenChange(false);
1076
+ },
1077
+ className: "opacity-0 group-hover:opacity-100 px-3 py-1 text-xs bg-blue-600 text-white rounded",
1078
+ children: "Select"
1079
+ }
1080
+ ),
1081
+ showDelete && /* @__PURE__ */ jsx4(
1082
+ "button",
1083
+ {
1084
+ type: "button",
1085
+ onClick: () => handleDelete(f2.key),
1086
+ disabled: deleting === f2.key,
1087
+ className: "opacity-0 group-hover:opacity-100 px-3 py-1 text-xs bg-red-600 text-white rounded disabled:opacity-50",
1088
+ children: deleting === f2.key ? "..." : "Delete"
1089
+ }
1090
+ )
1091
+ ] }),
1092
+ /* @__PURE__ */ jsx4("div", { className: "pointer-events-none absolute inset-x-0 bottom-0 bg-black/50 p-1 text-[10px] text-white truncate", children: f2.originalName })
1093
+ ] }, f2.key);
1094
+ }),
1095
+ loading && /* @__PURE__ */ jsx4("div", { className: "col-span-full text-sm text-muted-foreground", children: "Loading\u2026" }),
1096
+ !loading && visible.length === 0 && /* @__PURE__ */ jsx4("div", { className: "col-span-full text-sm text-muted-foreground", children: "No images found" })
1097
+ ] })
1098
+ ] }),
1099
+ (mode === "upload" || mode === "full" && tab === "upload") && /* @__PURE__ */ jsx4(
1100
+ Uploader,
1101
+ {
1102
+ clientOptions,
1103
+ projectId,
1104
+ folderPath,
1105
+ accept: ["image/*"],
1106
+ maxFileSize,
1107
+ maxFiles,
1108
+ autoRecordToDb,
1109
+ fetchThumbnails,
1110
+ onComplete: (files) => {
1111
+ if (mode === "full") {
1112
+ load();
1113
+ setTab("browse");
1114
+ } else {
1115
+ }
1116
+ },
1117
+ dropzoneClassName: "border-2 border-dashed border-gray-300 rounded-lg p-8 text-center cursor-pointer hover:border-blue-500 transition-colors",
1118
+ buttonClassName: "px-4 py-2 text-sm rounded border bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50"
1119
+ }
1120
+ ),
1121
+ /* @__PURE__ */ jsx4("div", { className: "flex justify-end gap-2 pt-2 border-t", children: /* @__PURE__ */ jsx4(Dialog2.Close, { asChild: true, children: /* @__PURE__ */ jsx4("button", { type: "button", className: "px-4 py-2 text-sm rounded border", children: "Close" }) }) })
1122
+ ] })
1123
+ }
1124
+ )
1125
+ ] }) });
1126
+ }
500
1127
  export {
1128
+ ConnectProjectDialog,
1129
+ ImageManager,
501
1130
  ProjectFilesWidget,
502
1131
  UpfilesClient,
503
- Uploader
1132
+ Uploader,
1133
+ addApiKeyManually,
1134
+ connectProject,
1135
+ connectUsingApiKey,
1136
+ createClientWithKey,
1137
+ createProject,
1138
+ fetchFileThumbnails,
1139
+ fetchProjectFiles,
1140
+ fetchProjectImagesWithThumbs,
1141
+ listProjects
504
1142
  };