@kyro-cms/admin 0.1.9 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/package.json +4 -4
- package/src/components/Admin.tsx +2 -1
- package/src/components/ApiKeysManager.tsx +45 -49
- package/src/components/AuditLogsPage.tsx +9 -4
- package/src/components/BrandingHub.tsx +24 -32
- package/src/components/CreateView.tsx +6 -13
- package/src/components/DetailView.tsx +8 -30
- package/src/components/DeveloperCenter.tsx +31 -27
- package/src/components/EnhancedListView.tsx +5 -6
- package/src/components/ListView.tsx +21 -19
- package/src/components/LoginPage.tsx +3 -17
- package/src/components/MediaGallery.tsx +103 -105
- package/src/components/UserManagement.tsx +28 -18
- package/src/components/WebhookManager.tsx +43 -56
- package/src/components/fields/RelationshipBlockField.tsx +4 -7
- package/src/components/fields/RelationshipField.tsx +4 -10
- package/src/components/fields/UploadField.tsx +37 -44
- package/src/components/ui/CommandPalette.tsx +1 -0
|
@@ -28,6 +28,14 @@ import ReactCrop, {
|
|
|
28
28
|
convertToPixelCrop,
|
|
29
29
|
} from "react-image-crop";
|
|
30
30
|
import "react-image-crop/dist/ReactCrop.css";
|
|
31
|
+
import {
|
|
32
|
+
apiGet,
|
|
33
|
+
apiPost,
|
|
34
|
+
apiDelete,
|
|
35
|
+
apiPatch,
|
|
36
|
+
withCacheBust,
|
|
37
|
+
apiUpload,
|
|
38
|
+
} from "@kyro-cms/utils/lib/api";
|
|
31
39
|
|
|
32
40
|
interface MediaItem {
|
|
33
41
|
id: string;
|
|
@@ -176,10 +184,7 @@ export function MediaGallery() {
|
|
|
176
184
|
|
|
177
185
|
const loadFolders = useCallback(async () => {
|
|
178
186
|
try {
|
|
179
|
-
const
|
|
180
|
-
credentials: "include",
|
|
181
|
-
});
|
|
182
|
-
const data = await resp.json();
|
|
187
|
+
const data = await apiGet("/api/media/folders");
|
|
183
188
|
setAvailableFolders((data.folders || []).sort());
|
|
184
189
|
} catch (e) {
|
|
185
190
|
console.error("loadFolders error:", e);
|
|
@@ -199,11 +204,11 @@ export function MediaGallery() {
|
|
|
199
204
|
? `&folder=${encodeURIComponent(currentFolder)}`
|
|
200
205
|
: "";
|
|
201
206
|
|
|
202
|
-
const
|
|
203
|
-
|
|
207
|
+
const result = await apiGet(
|
|
208
|
+
withCacheBust(
|
|
209
|
+
`/api/media?limit=30&page=${currentPage}&sortBy=${sortBy}&sortDir=${sortDir}${typeParam}${searchParam}${folderParam}`,
|
|
210
|
+
),
|
|
204
211
|
);
|
|
205
|
-
if (!response.ok) throw new Error("Failed to load media");
|
|
206
|
-
const result = await response.json();
|
|
207
212
|
|
|
208
213
|
if (reset) {
|
|
209
214
|
setItems(result.docs || []);
|
|
@@ -230,8 +235,7 @@ export function MediaGallery() {
|
|
|
230
235
|
|
|
231
236
|
useEffect(() => {
|
|
232
237
|
// Check if storage settings are configured
|
|
233
|
-
|
|
234
|
-
.then((res) => res.json())
|
|
238
|
+
apiGet("/api/storage-status")
|
|
235
239
|
.then((status) => {
|
|
236
240
|
// If not configured, show modal to configure
|
|
237
241
|
if (!status.configured) {
|
|
@@ -293,15 +297,7 @@ export function MediaGallery() {
|
|
|
293
297
|
formData.append("file", file);
|
|
294
298
|
if (uploadFolder) formData.append("folder", uploadFolder);
|
|
295
299
|
|
|
296
|
-
const
|
|
297
|
-
method: "POST",
|
|
298
|
-
body: formData,
|
|
299
|
-
signal: ac.signal,
|
|
300
|
-
credentials: "include",
|
|
301
|
-
});
|
|
302
|
-
if (!response.ok) continue;
|
|
303
|
-
|
|
304
|
-
const result = await response.json();
|
|
300
|
+
const result = await apiUpload("/api/upload", formData);
|
|
305
301
|
const newItem: MediaItem = {
|
|
306
302
|
...result,
|
|
307
303
|
type: getFileType(result.mimeType || file.type) as MediaItem["type"],
|
|
@@ -332,7 +328,7 @@ export function MediaGallery() {
|
|
|
332
328
|
if (!ids?.length) return;
|
|
333
329
|
try {
|
|
334
330
|
for (const id of ids) {
|
|
335
|
-
await
|
|
331
|
+
await apiDelete(`/api/media/${id}`);
|
|
336
332
|
}
|
|
337
333
|
loadMedia(true);
|
|
338
334
|
setSelectedItems(new Set());
|
|
@@ -397,11 +393,7 @@ export function MediaGallery() {
|
|
|
397
393
|
setLoading(true);
|
|
398
394
|
const ids = Array.from(selectedItems);
|
|
399
395
|
for (const id of ids) {
|
|
400
|
-
await
|
|
401
|
-
method: "PATCH",
|
|
402
|
-
headers: { "Content-Type": "application/json" },
|
|
403
|
-
body: JSON.stringify({ folder: targetFolder }),
|
|
404
|
-
});
|
|
396
|
+
await apiPatch(`/api/media/${id}`, { folder: targetFolder });
|
|
405
397
|
}
|
|
406
398
|
setSelectedItems(new Set());
|
|
407
399
|
loadMedia(true);
|
|
@@ -416,18 +408,14 @@ export function MediaGallery() {
|
|
|
416
408
|
const createFolder = async (name: string) => {
|
|
417
409
|
if (!name) return;
|
|
418
410
|
try {
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
body: JSON.stringify({ name, parentPath: currentFolder || "" }),
|
|
423
|
-
credentials: "include",
|
|
411
|
+
await apiPost("/api/media/folders", {
|
|
412
|
+
name,
|
|
413
|
+
parentPath: currentFolder || "",
|
|
424
414
|
});
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
loadFolders();
|
|
430
|
-
}
|
|
415
|
+
const newPath = currentFolder ? `${currentFolder}/${name}` : name;
|
|
416
|
+
setCurrentFolder(newPath);
|
|
417
|
+
setUploadFolder(newPath);
|
|
418
|
+
loadFolders();
|
|
431
419
|
} catch (e) {
|
|
432
420
|
console.error("Failed to create folder:", e);
|
|
433
421
|
}
|
|
@@ -442,27 +430,19 @@ export function MediaGallery() {
|
|
|
442
430
|
const deleteFolder = async () => {
|
|
443
431
|
if (!folderToDelete) return;
|
|
444
432
|
try {
|
|
445
|
-
const
|
|
433
|
+
const result = await apiDelete(
|
|
446
434
|
`/api/media/folders?path=${encodeURIComponent(folderToDelete)}`,
|
|
447
|
-
{
|
|
448
|
-
method: "DELETE",
|
|
449
|
-
credentials: "include",
|
|
450
|
-
},
|
|
451
435
|
);
|
|
452
|
-
console.log("[deleteFolder] Response status:", resp.status);
|
|
453
|
-
const result = await resp.json();
|
|
454
436
|
console.log("[deleteFolder] Response:", result);
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
setCurrentFolder("");
|
|
460
|
-
}
|
|
461
|
-
loadFolders();
|
|
462
|
-
// Force fresh load by resetting page
|
|
463
|
-
setPage(1);
|
|
464
|
-
loadMedia(true);
|
|
437
|
+
// Clear items first, then reload
|
|
438
|
+
setItems([]);
|
|
439
|
+
if (currentFolder === folderToDelete) {
|
|
440
|
+
setCurrentFolder("");
|
|
465
441
|
}
|
|
442
|
+
loadFolders();
|
|
443
|
+
// Force fresh load by resetting page
|
|
444
|
+
setPage(1);
|
|
445
|
+
loadMedia(true);
|
|
466
446
|
} catch (e) {
|
|
467
447
|
console.error("Failed to delete folder:", e);
|
|
468
448
|
}
|
|
@@ -473,15 +453,12 @@ export function MediaGallery() {
|
|
|
473
453
|
const savePanelMetadata = async () => {
|
|
474
454
|
if (!panelItem) return;
|
|
475
455
|
try {
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
body: JSON.stringify({ title: panelName, alt: panelAlt }),
|
|
456
|
+
await apiPatch(`/api/media/${panelItem.id}`, {
|
|
457
|
+
title: panelName,
|
|
458
|
+
alt: panelAlt,
|
|
480
459
|
});
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
loadMedia(true);
|
|
484
|
-
}
|
|
460
|
+
setPanelItem(null);
|
|
461
|
+
loadMedia(true);
|
|
485
462
|
} catch (e) {
|
|
486
463
|
console.error("Failed to save metadata:", e);
|
|
487
464
|
}
|
|
@@ -531,11 +508,7 @@ export function MediaGallery() {
|
|
|
531
508
|
|
|
532
509
|
setUploading(true);
|
|
533
510
|
try {
|
|
534
|
-
const res = await
|
|
535
|
-
method: "POST",
|
|
536
|
-
body: formData,
|
|
537
|
-
credentials: "include",
|
|
538
|
-
});
|
|
511
|
+
const res = await apiUpload("/api/upload", formData);
|
|
539
512
|
if (res.ok) {
|
|
540
513
|
setShowCrop(false);
|
|
541
514
|
setPanelItem(null);
|
|
@@ -561,7 +534,8 @@ export function MediaGallery() {
|
|
|
561
534
|
type: FilterType;
|
|
562
535
|
label: string;
|
|
563
536
|
}) => (
|
|
564
|
-
<button
|
|
537
|
+
<button
|
|
538
|
+
type="button"
|
|
565
539
|
onClick={() => setFilter(type)}
|
|
566
540
|
className={`px-4 py-2 text-sm font-bold rounded-lg transition-colors ${
|
|
567
541
|
filter === type
|
|
@@ -589,14 +563,16 @@ export function MediaGallery() {
|
|
|
589
563
|
</div>
|
|
590
564
|
|
|
591
565
|
<div className="flex items-center gap-4 px-4">
|
|
592
|
-
<button
|
|
566
|
+
<button
|
|
567
|
+
type="button"
|
|
593
568
|
onClick={handleBulkDelete}
|
|
594
569
|
className="p-3 bg-[var(--kyro-danger-bg)] text-[var(--kyro-danger)] hover:bg-[var(--kyro-danger)] hover:text-white rounded-full transition-all"
|
|
595
570
|
title="Delete Selected"
|
|
596
571
|
>
|
|
597
572
|
<Trash2 className="w-5 h-5" />
|
|
598
573
|
</button>
|
|
599
|
-
<button
|
|
574
|
+
<button
|
|
575
|
+
type="button"
|
|
600
576
|
className="p-3 bg-white/10 text-white hover:bg-white/20 rounded-full transition-all"
|
|
601
577
|
title="Download Collection"
|
|
602
578
|
>
|
|
@@ -604,7 +580,8 @@ export function MediaGallery() {
|
|
|
604
580
|
</button>
|
|
605
581
|
</div>
|
|
606
582
|
|
|
607
|
-
<button
|
|
583
|
+
<button
|
|
584
|
+
type="button"
|
|
608
585
|
onClick={() => setSelectedItems(new Set())}
|
|
609
586
|
className="p-2 hover:bg-white/10 rounded-full transition-all"
|
|
610
587
|
>
|
|
@@ -625,7 +602,8 @@ export function MediaGallery() {
|
|
|
625
602
|
</p>
|
|
626
603
|
</div>
|
|
627
604
|
<div className="flex items-center gap-3">
|
|
628
|
-
<button
|
|
605
|
+
<button
|
|
606
|
+
type="button"
|
|
629
607
|
onClick={() => setViewMode("grid")}
|
|
630
608
|
className={`p-2 rounded-lg ${viewMode === "grid" ? "bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)]" : "text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)]"}`}
|
|
631
609
|
>
|
|
@@ -643,7 +621,8 @@ export function MediaGallery() {
|
|
|
643
621
|
/>
|
|
644
622
|
</svg>
|
|
645
623
|
</button>
|
|
646
|
-
<button
|
|
624
|
+
<button
|
|
625
|
+
type="button"
|
|
647
626
|
onClick={() => setViewMode("list")}
|
|
648
627
|
className={`p-2 rounded-lg ${viewMode === "list" ? "bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)]" : "text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)]"}`}
|
|
649
628
|
>
|
|
@@ -665,7 +644,8 @@ export function MediaGallery() {
|
|
|
665
644
|
</div>
|
|
666
645
|
|
|
667
646
|
<div className="flex flex-col sm:flex-row gap-4 mb-4 items-start sm:items-center w-full">
|
|
668
|
-
<button
|
|
647
|
+
<button
|
|
648
|
+
type="button"
|
|
669
649
|
onClick={() => fileInputRef.current?.click()}
|
|
670
650
|
disabled={uploading}
|
|
671
651
|
className="flex items-center gap-2 px-3 py-1.5 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-full font-bold text-xs hover:opacity-90 transition-colors"
|
|
@@ -691,7 +671,8 @@ export function MediaGallery() {
|
|
|
691
671
|
</button>
|
|
692
672
|
{availableFolders.length > 0 && (
|
|
693
673
|
<div className="flex bg-[var(--kyro-surface-accent)] rounded-full p-1 gap-1 overflow-x-auto items-center">
|
|
694
|
-
<button
|
|
674
|
+
<button
|
|
675
|
+
type="button"
|
|
695
676
|
onClick={() => setCurrentFolder("")}
|
|
696
677
|
className={`flex-shrink-0 px-3 py-1 rounded-full text-xs font-bold transition-colors ${!currentFolder ? "bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] shadow-sm" : "text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface)]"}`}
|
|
697
678
|
>
|
|
@@ -699,13 +680,15 @@ export function MediaGallery() {
|
|
|
699
680
|
</button>
|
|
700
681
|
{availableFolders.map((folder) => (
|
|
701
682
|
<div key={folder} className="flex items-center group">
|
|
702
|
-
<button
|
|
683
|
+
<button
|
|
684
|
+
type="button"
|
|
703
685
|
onClick={() => setCurrentFolder(folder)}
|
|
704
686
|
className={`flex-shrink-0 px-3 py-1 rounded-full text-xs font-bold transition-colors ${currentFolder === folder ? "bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] shadow-sm" : "text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface)]"}`}
|
|
705
687
|
>
|
|
706
688
|
{folder}
|
|
707
689
|
</button>
|
|
708
|
-
<button
|
|
690
|
+
<button
|
|
691
|
+
type="button"
|
|
709
692
|
onClick={() => confirmDeleteFolder(folder)}
|
|
710
693
|
className="flex-shrink-0 p-1 rounded-full text-[var(--kyro-text-muted)] hover:text-[var(--kyro-danger)] opacity-0 group-hover:opacity-100 transition-opacity ml-1"
|
|
711
694
|
title="Delete folder"
|
|
@@ -728,7 +711,8 @@ export function MediaGallery() {
|
|
|
728
711
|
))}
|
|
729
712
|
</div>
|
|
730
713
|
)}
|
|
731
|
-
<button
|
|
714
|
+
<button
|
|
715
|
+
type="button"
|
|
732
716
|
onClick={() => setShowNewFolderModal(true)}
|
|
733
717
|
className="flex items-center gap-2 px-3 py-1.5 border border-[var(--kyro-border)] text-[var(--kyro-text-secondary)] rounded-full font-bold text-xs hover:bg-[var(--kyro-surface-accent)] transition-colors"
|
|
734
718
|
>
|
|
@@ -865,7 +849,8 @@ export function MediaGallery() {
|
|
|
865
849
|
<span className="text-[10px] font-black uppercase tracking-widest opacity-40 mr-2">
|
|
866
850
|
{selectedItems.size} Selected
|
|
867
851
|
</span>
|
|
868
|
-
<button
|
|
852
|
+
<button
|
|
853
|
+
type="button"
|
|
869
854
|
onClick={() => setSelectedItems(new Set())}
|
|
870
855
|
className="text-[10px] font-black uppercase tracking-widest text-[var(--kyro-danger)] hover:underline"
|
|
871
856
|
>
|
|
@@ -950,7 +935,8 @@ export function MediaGallery() {
|
|
|
950
935
|
</div>
|
|
951
936
|
)}
|
|
952
937
|
</div>
|
|
953
|
-
<button
|
|
938
|
+
<button
|
|
939
|
+
type="button"
|
|
954
940
|
onClick={(e) => {
|
|
955
941
|
e.stopPropagation();
|
|
956
942
|
toggleSelect(item.id);
|
|
@@ -973,7 +959,8 @@ export function MediaGallery() {
|
|
|
973
959
|
</svg>
|
|
974
960
|
)}
|
|
975
961
|
</button>
|
|
976
|
-
<button
|
|
962
|
+
<button
|
|
963
|
+
type="button"
|
|
977
964
|
onClick={(e) => {
|
|
978
965
|
e.stopPropagation();
|
|
979
966
|
openMetadataPanel(item);
|
|
@@ -995,7 +982,8 @@ export function MediaGallery() {
|
|
|
995
982
|
/>
|
|
996
983
|
</svg>
|
|
997
984
|
</button>
|
|
998
|
-
<button
|
|
985
|
+
<button
|
|
986
|
+
type="button"
|
|
999
987
|
onClick={(e) => {
|
|
1000
988
|
e.stopPropagation();
|
|
1001
989
|
handleDelete(item.id);
|
|
@@ -1031,7 +1019,8 @@ export function MediaGallery() {
|
|
|
1031
1019
|
) : (
|
|
1032
1020
|
<div className="space-y-2">
|
|
1033
1021
|
<div className="flex items-center gap-4 px-4 py-2 text-xs font-bold text-[var(--kyro-text-muted)] uppercase">
|
|
1034
|
-
<button
|
|
1022
|
+
<button
|
|
1023
|
+
type="button"
|
|
1035
1024
|
onClick={selectAll}
|
|
1036
1025
|
className="w-6 h-6 rounded border border-[var(--kyro-border)] flex items-center justify-center hover:bg-[var(--kyro-surface-accent)]"
|
|
1037
1026
|
>
|
|
@@ -1067,7 +1056,8 @@ export function MediaGallery() {
|
|
|
1067
1056
|
}}
|
|
1068
1057
|
className={`flex items-center gap-4 px-4 py-3 rounded-lg transition-colors cursor-pointer ${selectedItems.has(item.id) ? "bg-[var(--kyro-surface-accent)]" : "hover:bg-[var(--kyro-surface-accent)]"}`}
|
|
1069
1058
|
>
|
|
1070
|
-
<button
|
|
1059
|
+
<button
|
|
1060
|
+
type="button"
|
|
1071
1061
|
onClick={() => toggleSelect(item.id)}
|
|
1072
1062
|
className={`w-6 h-6 rounded border flex items-center justify-center transition-colors ${selectedItems.has(item.id) ? "bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-primary)]" : "border-[var(--kyro-border)] hover:border-[var(--kyro-border-active)]"}`}
|
|
1073
1063
|
>
|
|
@@ -1132,7 +1122,8 @@ export function MediaGallery() {
|
|
|
1132
1122
|
<span className="w-32 text-sm text-[var(--kyro-text-muted)]">
|
|
1133
1123
|
{formatDate(item.createdAt)}
|
|
1134
1124
|
</span>
|
|
1135
|
-
<button
|
|
1125
|
+
<button
|
|
1126
|
+
type="button"
|
|
1136
1127
|
onClick={(e) => {
|
|
1137
1128
|
e.stopPropagation();
|
|
1138
1129
|
openMetadataPanel(item);
|
|
@@ -1154,7 +1145,8 @@ export function MediaGallery() {
|
|
|
1154
1145
|
/>
|
|
1155
1146
|
</svg>
|
|
1156
1147
|
</button>
|
|
1157
|
-
<button
|
|
1148
|
+
<button
|
|
1149
|
+
type="button"
|
|
1158
1150
|
onClick={() => handleDelete(item.id)}
|
|
1159
1151
|
className="w-10 h-10 rounded-lg hover:bg-[var(--kyro-danger-bg)] text-[var(--kyro-text-muted)] hover:text-[var(--kyro-danger)] flex items-center justify-center transition-colors"
|
|
1160
1152
|
>
|
|
@@ -1194,7 +1186,8 @@ export function MediaGallery() {
|
|
|
1194
1186
|
className="fixed inset-0 z-[9999] bg-black/95 flex items-center justify-center p-8 animate-in fade-in duration-200"
|
|
1195
1187
|
onClick={() => setLightboxItem(null)}
|
|
1196
1188
|
>
|
|
1197
|
-
<button
|
|
1189
|
+
<button
|
|
1190
|
+
type="button"
|
|
1198
1191
|
onClick={() => setLightboxItem(null)}
|
|
1199
1192
|
className="absolute top-4 right-4 w-12 h-12 rounded-full bg-white/10 text-white flex items-center justify-center hover:bg-white/20 transition-colors"
|
|
1200
1193
|
>
|
|
@@ -1235,7 +1228,8 @@ export function MediaGallery() {
|
|
|
1235
1228
|
>
|
|
1236
1229
|
Open in new tab
|
|
1237
1230
|
</a>
|
|
1238
|
-
<button
|
|
1231
|
+
<button
|
|
1232
|
+
type="button"
|
|
1239
1233
|
onClick={(e) => {
|
|
1240
1234
|
e.stopPropagation();
|
|
1241
1235
|
navigator.clipboard.writeText(
|
|
@@ -1264,7 +1258,8 @@ export function MediaGallery() {
|
|
|
1264
1258
|
<h2 className="text-lg font-bold text-[var(--kyro-text-primary)]">
|
|
1265
1259
|
Media Details
|
|
1266
1260
|
</h2>
|
|
1267
|
-
<button
|
|
1261
|
+
<button
|
|
1262
|
+
type="button"
|
|
1268
1263
|
onClick={() => setPanelItem(null)}
|
|
1269
1264
|
className="p-2 rounded-lg hover:bg-[var(--kyro-surface-accent)]"
|
|
1270
1265
|
>
|
|
@@ -1355,7 +1350,8 @@ export function MediaGallery() {
|
|
|
1355
1350
|
value={getAbsoluteUrl(panelItem.url)}
|
|
1356
1351
|
className="flex-1 px-3 py-2 bg-[var(--kyro-bg)] border border-[var(--kyro-border)] rounded-lg text-xs text-[var(--kyro-text-muted)] focus:outline-none truncate"
|
|
1357
1352
|
/>
|
|
1358
|
-
<button
|
|
1353
|
+
<button
|
|
1354
|
+
type="button"
|
|
1359
1355
|
onClick={() => {
|
|
1360
1356
|
navigator.clipboard.writeText(
|
|
1361
1357
|
getAbsoluteUrl(panelItem.url),
|
|
@@ -1389,7 +1385,8 @@ export function MediaGallery() {
|
|
|
1389
1385
|
</p>
|
|
1390
1386
|
</div>
|
|
1391
1387
|
{panelItem.type === "image" && (
|
|
1392
|
-
<button
|
|
1388
|
+
<button
|
|
1389
|
+
type="button"
|
|
1393
1390
|
onClick={() => setShowCrop(true)}
|
|
1394
1391
|
className="flex items-center gap-2 px-3 py-1.5 bg-[var(--kyro-surface-accent)] text-[var(--kyro-primary)] rounded-lg font-bold text-xs hover:opacity-80 transition-colors mt-2 w-full"
|
|
1395
1392
|
>
|
|
@@ -1409,13 +1406,15 @@ export function MediaGallery() {
|
|
|
1409
1406
|
>
|
|
1410
1407
|
<Download className="w-5 h-5 text-[var(--kyro-text-secondary)]" />
|
|
1411
1408
|
</a>
|
|
1412
|
-
<button
|
|
1409
|
+
<button
|
|
1410
|
+
type="button"
|
|
1413
1411
|
onClick={savePanelMetadata}
|
|
1414
1412
|
className="flex-1 py-2 px-4 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-lg font-bold text-sm hover:opacity-90"
|
|
1415
1413
|
>
|
|
1416
1414
|
Save
|
|
1417
1415
|
</button>
|
|
1418
|
-
<button
|
|
1416
|
+
<button
|
|
1417
|
+
type="button"
|
|
1419
1418
|
onClick={() => setPanelItem(null)}
|
|
1420
1419
|
className="py-2 px-4 border border-[var(--kyro-border)] rounded-lg font-bold text-sm text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)]"
|
|
1421
1420
|
>
|
|
@@ -1433,13 +1432,15 @@ export function MediaGallery() {
|
|
|
1433
1432
|
<div className="flex w-full justify-between items-center mb-4 text-white">
|
|
1434
1433
|
<h3 className="text-xl font-bold">Crop Image</h3>
|
|
1435
1434
|
<div className="flex gap-3">
|
|
1436
|
-
<button
|
|
1435
|
+
<button
|
|
1436
|
+
type="button"
|
|
1437
1437
|
onClick={() => setShowCrop(false)}
|
|
1438
1438
|
className="px-4 py-2 border border-white/20 text-white/80 hover:bg-white/10 rounded-lg font-bold text-sm transition-colors"
|
|
1439
1439
|
>
|
|
1440
1440
|
Cancel
|
|
1441
1441
|
</button>
|
|
1442
|
-
<button
|
|
1442
|
+
<button
|
|
1443
|
+
type="button"
|
|
1443
1444
|
disabled={uploading}
|
|
1444
1445
|
onClick={onCropComplete}
|
|
1445
1446
|
className="px-4 py-2 bg-[var(--kyro-sidebar-active)] hover:opacity-90 text-[var(--kyro-sidebar-text-active)] rounded-lg font-bold text-sm transition-colors"
|
|
@@ -1513,19 +1514,16 @@ export function MediaGallery() {
|
|
|
1513
1514
|
>
|
|
1514
1515
|
Configure Storage
|
|
1515
1516
|
</a>
|
|
1516
|
-
<button
|
|
1517
|
+
<button
|
|
1518
|
+
type="button"
|
|
1517
1519
|
onClick={() => {
|
|
1518
1520
|
// Set default storage config programmatically
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
uploadDir: "./public/uploads",
|
|
1526
|
-
baseUrl: "/uploads",
|
|
1527
|
-
},
|
|
1528
|
-
}),
|
|
1521
|
+
apiPatch("/api/globals/storage-settings", {
|
|
1522
|
+
provider: "local",
|
|
1523
|
+
local: {
|
|
1524
|
+
uploadDir: "./public/uploads",
|
|
1525
|
+
baseUrl: "/uploads",
|
|
1526
|
+
},
|
|
1529
1527
|
}).then(() => {
|
|
1530
1528
|
setShowStorageConfigModal(false);
|
|
1531
1529
|
setStorageConfigured(true);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React, { useState, useEffect } from "react";
|
|
2
|
+
import { apiGet, apiPatch } from "@kyro-cms/utils/lib/api";
|
|
2
3
|
import {
|
|
3
4
|
Users,
|
|
4
5
|
UserPlus,
|
|
@@ -34,8 +35,7 @@ export function UserManagement() {
|
|
|
34
35
|
const loadUsers = async () => {
|
|
35
36
|
try {
|
|
36
37
|
setLoading(true);
|
|
37
|
-
const
|
|
38
|
-
const result = await response.json();
|
|
38
|
+
const result = await apiGet("/api/auth/users");
|
|
39
39
|
setUsers(result.docs || []);
|
|
40
40
|
} catch (error) {
|
|
41
41
|
console.error("Failed to load users:", error);
|
|
@@ -46,16 +46,10 @@ export function UserManagement() {
|
|
|
46
46
|
|
|
47
47
|
const handleToggleLock = async (user: User) => {
|
|
48
48
|
try {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
});
|
|
54
|
-
if (response.ok) {
|
|
55
|
-
setUsers((prev) =>
|
|
56
|
-
prev.map((u) => (u.id === user.id ? { ...u, locked: !u.locked } : u)),
|
|
57
|
-
);
|
|
58
|
-
}
|
|
49
|
+
await apiPatch(`/api/auth/${user.id}`, { locked: !user.locked });
|
|
50
|
+
setUsers((prev) =>
|
|
51
|
+
prev.map((u) => (u.id === user.id ? { ...u, locked: !u.locked } : u)),
|
|
52
|
+
);
|
|
59
53
|
} catch (error) {
|
|
60
54
|
console.error("Failed to toggle user lock:", error);
|
|
61
55
|
}
|
|
@@ -80,7 +74,10 @@ export function UserManagement() {
|
|
|
80
74
|
</p>
|
|
81
75
|
</div>
|
|
82
76
|
<div className="flex items-center gap-3">
|
|
83
|
-
<button
|
|
77
|
+
<button
|
|
78
|
+
type="button"
|
|
79
|
+
className="flex items-center gap-2 px-6 py-3 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-2xl font-black text-sm shadow-xl active:scale-95 transition-all"
|
|
80
|
+
>
|
|
84
81
|
<UserPlus className="w-4 h-4" />
|
|
85
82
|
Invite Member
|
|
86
83
|
</button>
|
|
@@ -100,13 +97,22 @@ export function UserManagement() {
|
|
|
100
97
|
/>
|
|
101
98
|
</div>
|
|
102
99
|
<div className="flex items-center gap-3 bg-[var(--kyro-bg-secondary)] p-1 rounded-2xl border border-[var(--kyro-border)]">
|
|
103
|
-
<button
|
|
100
|
+
<button
|
|
101
|
+
type="button"
|
|
102
|
+
className="px-4 py-2 text-[10px] font-black uppercase tracking-widest bg-[var(--kyro-surface)] shadow-sm rounded-xl border border-[var(--kyro-border)]"
|
|
103
|
+
>
|
|
104
104
|
All Users
|
|
105
105
|
</button>
|
|
106
|
-
<button
|
|
106
|
+
<button
|
|
107
|
+
type="button"
|
|
108
|
+
className="px-4 py-2 text-[10px] font-black uppercase tracking-widest opacity-40 hover:opacity-100 transition-all"
|
|
109
|
+
>
|
|
107
110
|
Admins
|
|
108
111
|
</button>
|
|
109
|
-
<button
|
|
112
|
+
<button
|
|
113
|
+
type="button"
|
|
114
|
+
className="px-4 py-2 text-[10px] font-black uppercase tracking-widest opacity-40 hover:opacity-100 transition-all"
|
|
115
|
+
>
|
|
110
116
|
Restricted
|
|
111
117
|
</button>
|
|
112
118
|
</div>
|
|
@@ -177,7 +183,8 @@ export function UserManagement() {
|
|
|
177
183
|
</div>
|
|
178
184
|
|
|
179
185
|
<div className="flex items-center gap-3">
|
|
180
|
-
<button
|
|
186
|
+
<button
|
|
187
|
+
type="button"
|
|
181
188
|
onClick={() => handleToggleLock(user)}
|
|
182
189
|
className={`flex-1 py-2.5 rounded-xl font-black text-[10px] uppercase tracking-widest transition-all flex items-center justify-center gap-2 ${user.locked ? "bg-green-500/10 text-green-500 hover:bg-green-500/20" : "bg-red-500/10 text-red-500 hover:bg-red-500/20"}`}
|
|
183
190
|
>
|
|
@@ -188,7 +195,10 @@ export function UserManagement() {
|
|
|
188
195
|
)}
|
|
189
196
|
{user.locked ? "Unlock Account" : "Lock Account"}
|
|
190
197
|
</button>
|
|
191
|
-
<button
|
|
198
|
+
<button
|
|
199
|
+
type="button"
|
|
200
|
+
className="p-2.5 bg-[var(--kyro-bg-secondary)] rounded-xl border border-[var(--kyro-border)] hover:bg-[var(--kyro-surface)] transition-all"
|
|
201
|
+
>
|
|
192
202
|
<MoreVertical className="w-4 h-4 opacity-40" />
|
|
193
203
|
</button>
|
|
194
204
|
</div>
|