@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/README.md +397 -70
- package/dist/index.d.mts +134 -1
- package/dist/index.d.ts +134 -1
- package/dist/index.js +699 -40
- package/dist/index.mjs +677 -39
- package/package.json +7 -6
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((
|
|
171
|
-
if (
|
|
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((
|
|
182
|
+
const mapped = limited.map((f2) => ({
|
|
178
183
|
id: Math.random().toString(36).slice(2),
|
|
179
|
-
file:
|
|
180
|
-
name:
|
|
181
|
-
size:
|
|
182
|
-
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((
|
|
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
|
-
|
|
240
|
-
|
|
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((
|
|
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
|
|
304
|
+
for (const f2 of targets) {
|
|
264
305
|
try {
|
|
265
|
-
const done = await uploadOne(
|
|
306
|
+
const done = await uploadOne(f2);
|
|
266
307
|
results.push(done);
|
|
267
308
|
} catch (e) {
|
|
268
309
|
}
|
|
269
310
|
}
|
|
270
|
-
onComplete?.(results.filter((
|
|
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((
|
|
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((
|
|
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:
|
|
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
|
-
(
|
|
384
|
+
(f2.size / (1024 * 1024)).toFixed(2),
|
|
344
385
|
" MB"
|
|
345
386
|
] }),
|
|
346
|
-
|
|
347
|
-
|
|
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: `${
|
|
351
|
-
/* @__PURE__ */ jsx("div", { style: { fontSize: 12, color: "#666", marginTop: 4 }, children:
|
|
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
|
-
] }) },
|
|
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
|
-
(
|
|
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((
|
|
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:
|
|
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
|
-
(
|
|
528
|
+
(f2.size / (1024 * 1024)).toFixed(2),
|
|
448
529
|
" MB \u2022 ",
|
|
449
|
-
|
|
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:
|
|
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:
|
|
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(
|
|
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 ===
|
|
487
|
-
style: { padding: "6px 10px", background: "#2563eb", color: "#fff", border: "1px solid #1d4ed8", borderRadius: 6, opacity: savingKey ===
|
|
488
|
-
children: savingKey ===
|
|
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
|
-
|
|
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
|
};
|