@kyro-cms/admin 0.9.0 → 0.9.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.cjs +11715 -11292
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +67 -65
- package/dist/index.css.map +1 -1
- package/dist/index.d.cts +564 -0
- package/dist/index.d.ts +11 -10
- package/dist/index.js +11326 -10912
- package/dist/index.js.map +1 -1
- package/package.json +16 -12
- package/src/components/ActionBar.tsx +25 -161
- package/src/components/Admin.tsx +2 -4
- package/src/components/ApiKeysManager.tsx +5 -5
- package/src/components/AuditLogsPage.tsx +2 -13
- package/src/components/AutoForm.tsx +572 -461
- package/src/components/BrandingHub.tsx +7 -4
- package/src/components/CreateView.tsx +2 -0
- package/src/components/DetailView.tsx +52 -65
- package/src/components/DeveloperCenter.tsx +8 -6
- package/src/components/FieldRenderer.tsx +94 -19
- package/src/components/ListView.tsx +57 -216
- package/src/components/MediaGallery.tsx +334 -367
- package/src/components/PluginsManager.tsx +197 -70
- package/src/components/RestPlayground.tsx +59 -52
- package/src/components/SessionsManager.tsx +1 -1
- package/src/components/SettingsPage.tsx +22 -0
- package/src/components/Sidebar.astro +13 -41
- package/src/components/UserManagement.tsx +153 -15
- package/src/components/UserMenu.tsx +30 -4
- package/src/components/VersionHistoryPanel.tsx +112 -119
- package/src/components/WebhookManager.tsx +6 -4
- package/src/components/blocks/ArrayBlock.tsx +6 -23
- package/src/components/blocks/BlockEditModal.tsx +82 -309
- package/src/components/blocks/CardBlock.tsx +35 -0
- package/src/components/blocks/ChildBlocksTree.tsx +57 -31
- package/src/components/blocks/GenericBlock.tsx +44 -0
- package/src/components/blocks/HeadingSubheadingBlock.tsx +32 -0
- package/src/components/blocks/HeroBlock.tsx +5 -14
- package/src/components/blocks/RichTextBlock.tsx +5 -5
- package/src/components/blocks/index.ts +5 -3
- package/src/components/fields/AccordionField.tsx +2 -2
- package/src/components/fields/ArrayField.tsx +1 -1
- package/src/components/fields/ArrayLayout.tsx +120 -29
- package/src/components/fields/BlocksField.tsx +433 -55
- package/src/components/fields/CardField.tsx +73 -0
- package/src/components/fields/CheckboxField.tsx +7 -3
- package/src/components/fields/DateField.tsx +4 -1
- package/src/components/fields/GroupLayout.tsx +2 -2
- package/src/components/fields/HeadingSubheadingField.tsx +43 -0
- package/src/components/fields/ListField.tsx +2 -2
- package/src/components/fields/NumberField.tsx +4 -1
- package/src/components/fields/RelationshipBlockField.tsx +2 -3
- package/src/components/fields/RelationshipField.tsx +155 -90
- package/src/components/fields/RichTextField.tsx +781 -0
- package/src/components/fields/SecretField.tsx +102 -0
- package/src/components/fields/SelectField.tsx +19 -6
- package/src/components/fields/TabsLayout.tsx +19 -9
- package/src/components/fields/TextField.tsx +4 -1
- package/src/components/fields/UploadField.tsx +122 -56
- package/src/components/fields/extensions/blockComponents.tsx +103 -174
- package/src/components/fields/extensions/blocksStore.ts +8 -1
- package/src/components/fields/index.ts +4 -2
- package/src/components/fix_imports.cjs +23 -0
- package/src/components/fix_imports2.cjs +19 -0
- package/src/components/replace_svgs.cjs +63 -0
- package/src/components/ui/Dropdown.tsx +7 -2
- package/src/components/ui/Modal.tsx +24 -27
- package/src/components/ui/PageHeader.tsx +5 -5
- package/src/components/ui/PromptModal.tsx +2 -10
- package/src/components/ui/SlidePanel.tsx +10 -13
- package/src/components/ui/SplitButton.tsx +107 -0
- package/src/components/ui/Toaster.tsx +0 -1
- package/src/components/ui/icons.tsx +110 -109
- package/src/components/users/UserDetail.tsx +79 -16
- package/src/components/users/UsersList.tsx +8 -85
- package/src/hooks/useAutoFormState.ts +187 -196
- package/src/hooks/useQueue.ts +60 -0
- package/src/integration.ts +148 -46
- package/src/kyro-cms.d.ts +7 -2
- package/src/layouts/AdminLayout.astro +22 -2
- package/src/layouts/AuthLayout.astro +67 -7
- package/src/lib/autoform-store.ts +90 -53
- package/src/lib/change-source.ts +9 -0
- package/src/lib/config.ts +104 -8
- package/src/lib/globals.ts +48 -11
- package/src/lib/normalize-upload-fields.ts +41 -0
- package/src/lib/paths.ts +2 -2
- package/src/lib/resolve-field-value.ts +110 -0
- package/src/lib/shim/use-sync-external-store-with-selector.js +30 -0
- package/src/lib/shim/use-sync-external-store.js +1 -0
- package/src/lib/stores/index.ts +1 -0
- package/src/lib/useResourceManager.ts +4 -4
- package/src/lib/vite-shim-plugin.ts +100 -0
- package/src/pages/[collection]/[id].astro +1 -1
- package/src/pages/auth/register.astro +5 -2
- package/src/pages/preview/[collection]/[id].astro +4 -4
- package/src/pages/settings/[slug].astro +2 -2
- package/src/styles/main.css +60 -54
- package/README.md +0 -46
- package/dist/EditorClient-Q23UXR37.cjs +0 -468
- package/dist/EditorClient-Q23UXR37.cjs.map +0 -1
- package/dist/EditorClient-T5PASFNR.js +0 -466
- package/dist/EditorClient-T5PASFNR.js.map +0 -1
- package/dist/chunk-3BGDYKTD.cjs +0 -348
- package/dist/chunk-3BGDYKTD.cjs.map +0 -1
- package/dist/chunk-EEFXLQVT.js +0 -3
- package/dist/chunk-EEFXLQVT.js.map +0 -1
- package/src/components/blocks/ButtonBlock.tsx +0 -64
- package/src/components/blocks/ColumnsBlock.tsx +0 -55
- package/src/components/blocks/DividerBlock.tsx +0 -43
- package/src/components/blocks/LinkBlock.tsx +0 -65
- package/src/components/blocks/VStackBlock.tsx +0 -29
- package/src/components/fields/EditorClient.tsx +0 -535
- package/src/components/fields/PortableTextField.tsx +0 -155
- package/src/components/fields/PortableTextRenderer.tsx +0 -68
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { Search, Check, Server } from "./ui/icons";
|
|
1
2
|
import React, { useState, useEffect, useCallback, useRef, useMemo } from "react";
|
|
2
3
|
import { createPortal } from "react-dom";
|
|
3
4
|
import { Spinner } from "./ui/Spinner";
|
|
4
5
|
import { Shimmer } from "./ui/Shimmer";
|
|
5
6
|
import { SlidePanel } from "./ui/SlidePanel";
|
|
7
|
+
import { Modal } from "./ui/Modal";
|
|
8
|
+
import { Pagination } from "./ui/Pagination";
|
|
6
9
|
import { Badge } from "./ui/Badge";
|
|
7
10
|
import { Folder } from "./ui/icons";
|
|
8
11
|
|
|
@@ -38,7 +41,7 @@ import {
|
|
|
38
41
|
withCacheBust,
|
|
39
42
|
apiUpload,
|
|
40
43
|
} from "../lib/api";
|
|
41
|
-
import { useAuthStore, useUIStore } from "../lib/stores";
|
|
44
|
+
import { useAuthStore, useUIStore, toast } from "../lib/stores";
|
|
42
45
|
|
|
43
46
|
interface MediaItem {
|
|
44
47
|
id: string;
|
|
@@ -57,7 +60,8 @@ interface MediaItem {
|
|
|
57
60
|
updatedAt?: string;
|
|
58
61
|
}
|
|
59
62
|
|
|
60
|
-
function getAbsoluteUrl(relativeUrl:
|
|
63
|
+
function getAbsoluteUrl(relativeUrl: unknown): string {
|
|
64
|
+
if (typeof relativeUrl !== "string" || !relativeUrl) return "";
|
|
61
65
|
if (typeof window === "undefined") return relativeUrl;
|
|
62
66
|
// Remote URLs and blob URLs are returned as-is
|
|
63
67
|
if (relativeUrl.startsWith("http") || relativeUrl.startsWith("blob:")) {
|
|
@@ -115,10 +119,12 @@ function getFileType(mimeType: string): FilterType {
|
|
|
115
119
|
|
|
116
120
|
export function MediaGallery({
|
|
117
121
|
onSelect,
|
|
118
|
-
multiple =
|
|
122
|
+
multiple = true,
|
|
123
|
+
pickerMode = false,
|
|
119
124
|
}: {
|
|
120
125
|
onSelect?: (items: MediaItem[]) => void;
|
|
121
126
|
multiple?: boolean;
|
|
127
|
+
pickerMode?: boolean;
|
|
122
128
|
}) {
|
|
123
129
|
const { permissions } = useAuthStore();
|
|
124
130
|
const canUpload = permissions?.media?.create !== false;
|
|
@@ -201,15 +207,16 @@ export function MediaGallery({
|
|
|
201
207
|
}, []);
|
|
202
208
|
|
|
203
209
|
useEffect(() => {
|
|
204
|
-
checkStorage();
|
|
205
|
-
}, [checkStorage]);
|
|
210
|
+
if (!pickerMode) checkStorage();
|
|
211
|
+
}, [checkStorage, pickerMode]);
|
|
206
212
|
|
|
207
213
|
useEffect(() => {
|
|
214
|
+
if (pickerMode) return;
|
|
208
215
|
if (storageConfigured === false && !storageChecked) {
|
|
209
216
|
setStorageChecked(true);
|
|
210
217
|
setShowStorageConfigModal(true);
|
|
211
218
|
}
|
|
212
|
-
}, [storageConfigured, storageChecked]);
|
|
219
|
+
}, [pickerMode, storageConfigured, storageChecked]);
|
|
213
220
|
|
|
214
221
|
useEffect(() => {
|
|
215
222
|
loadMedia();
|
|
@@ -220,6 +227,7 @@ export function MediaGallery({
|
|
|
220
227
|
}, [loadFolders]);
|
|
221
228
|
|
|
222
229
|
useEffect(() => {
|
|
230
|
+
if (pickerMode) return;
|
|
223
231
|
const handlePaste = (e: ClipboardEvent) => {
|
|
224
232
|
const files = e.clipboardData?.files;
|
|
225
233
|
if (files && files.length > 0) {
|
|
@@ -228,7 +236,7 @@ export function MediaGallery({
|
|
|
228
236
|
};
|
|
229
237
|
window.addEventListener("paste", handlePaste);
|
|
230
238
|
return () => window.removeEventListener("paste", handlePaste);
|
|
231
|
-
}, [currentFolder, storageConfigured]);
|
|
239
|
+
}, [pickerMode, currentFolder, storageConfigured]);
|
|
232
240
|
|
|
233
241
|
const handleUpload = async (files: FileList | File[]) => {
|
|
234
242
|
if (!storageConfigured) {
|
|
@@ -237,7 +245,8 @@ export function MediaGallery({
|
|
|
237
245
|
}
|
|
238
246
|
|
|
239
247
|
setUploading(true);
|
|
240
|
-
|
|
248
|
+
let successCount = 0;
|
|
249
|
+
let failCount = 0;
|
|
241
250
|
|
|
242
251
|
for (let i = 0; i < files.length; i++) {
|
|
243
252
|
const file = files[i];
|
|
@@ -252,9 +261,10 @@ export function MediaGallery({
|
|
|
252
261
|
[file.name]: progress,
|
|
253
262
|
}));
|
|
254
263
|
});
|
|
264
|
+
successCount++;
|
|
255
265
|
} catch (error) {
|
|
256
266
|
console.error(`Upload failed for ${file.name}:`, error);
|
|
257
|
-
|
|
267
|
+
failCount++;
|
|
258
268
|
}
|
|
259
269
|
}
|
|
260
270
|
|
|
@@ -262,6 +272,12 @@ export function MediaGallery({
|
|
|
262
272
|
setUploadProgress({});
|
|
263
273
|
loadMedia();
|
|
264
274
|
loadFolders();
|
|
275
|
+
if (failCount > 0) {
|
|
276
|
+
toast.error(`${failCount} file(s) failed to upload`);
|
|
277
|
+
}
|
|
278
|
+
if (successCount > 0) {
|
|
279
|
+
toast.success(`${successCount} file(s) uploaded successfully`);
|
|
280
|
+
}
|
|
265
281
|
};
|
|
266
282
|
|
|
267
283
|
const handleBulkDelete = () => {
|
|
@@ -276,9 +292,10 @@ export function MediaGallery({
|
|
|
276
292
|
}
|
|
277
293
|
setSelectedIds(new Set());
|
|
278
294
|
loadMedia();
|
|
295
|
+
toast.success(`${selectedIds.size} item(s) deleted`);
|
|
279
296
|
} catch (error) {
|
|
280
297
|
console.error("Bulk delete failed:", error);
|
|
281
|
-
|
|
298
|
+
toast.error("Failed to delete some items");
|
|
282
299
|
}
|
|
283
300
|
}
|
|
284
301
|
});
|
|
@@ -303,6 +320,7 @@ export function MediaGallery({
|
|
|
303
320
|
setShowNewFolderModal(false);
|
|
304
321
|
} catch (error) {
|
|
305
322
|
console.error("Failed to create folder:", error);
|
|
323
|
+
toast.error("Failed to create folder");
|
|
306
324
|
}
|
|
307
325
|
};
|
|
308
326
|
|
|
@@ -320,7 +338,7 @@ export function MediaGallery({
|
|
|
320
338
|
loadMedia();
|
|
321
339
|
} catch (error) {
|
|
322
340
|
console.error("Failed to delete folder:", error);
|
|
323
|
-
|
|
341
|
+
toast.error("Failed to delete folder");
|
|
324
342
|
}
|
|
325
343
|
}
|
|
326
344
|
});
|
|
@@ -396,6 +414,7 @@ export function MediaGallery({
|
|
|
396
414
|
}
|
|
397
415
|
} catch (err) {
|
|
398
416
|
console.error("Crop failed:", err);
|
|
417
|
+
toast.error("Crop failed");
|
|
399
418
|
} finally {
|
|
400
419
|
setUploading(false);
|
|
401
420
|
}
|
|
@@ -414,47 +433,32 @@ export function MediaGallery({
|
|
|
414
433
|
return (
|
|
415
434
|
<div
|
|
416
435
|
className={`flex flex-col h-full bg-[var(--kyro-bg)] transition-all duration-300 ${isDragging ? "ring-4 ring-[var(--kyro-sidebar-active)] ring-inset" : ""}`}
|
|
417
|
-
|
|
418
|
-
e.preventDefault();
|
|
419
|
-
setIsDragging(
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
onDrop={(e) => {
|
|
423
|
-
e.preventDefault();
|
|
424
|
-
setIsDragging(false);
|
|
425
|
-
if (e.dataTransfer.files) handleUpload(e.dataTransfer.files);
|
|
426
|
-
}}
|
|
436
|
+
{...(pickerMode ? {} : {
|
|
437
|
+
onDragOver: (e) => { e.preventDefault(); setIsDragging(true); },
|
|
438
|
+
onDragLeave: () => setIsDragging(false),
|
|
439
|
+
onDrop: (e) => { e.preventDefault(); setIsDragging(false); if (e.dataTransfer.files) handleUpload(e.dataTransfer.files); },
|
|
440
|
+
})}
|
|
427
441
|
>
|
|
428
442
|
{/* Top Bar */}
|
|
429
|
-
<div className=
|
|
430
|
-
|
|
431
|
-
<div>
|
|
432
|
-
<
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
<
|
|
437
|
-
|
|
438
|
-
|
|
443
|
+
<div className={`flex flex-col lg:flex-row lg:items-center justify-between gap-6 border-b border-[var(--kyro-border)] backdrop-blur-md sticky top-0 z-40 ${pickerMode ? "p-2" : "p-6 rounded-xl surface-tile"}`}>
|
|
444
|
+
{!pickerMode && (
|
|
445
|
+
<div className="flex items-center gap-4">
|
|
446
|
+
<div>
|
|
447
|
+
<h2 className="text-xl font-bold tracking-tighter text-[var(--kyro-text-primary)]">
|
|
448
|
+
Media Library
|
|
449
|
+
</h2>
|
|
450
|
+
<div className="flex items-center gap-3 mt-1">
|
|
451
|
+
<span className="text-[10px] font-bold tracking-widest text-[var(--kyro-text-secondary)] opacity-50">
|
|
452
|
+
{total} Items · {formatFileSize(stats.totalSize)}
|
|
453
|
+
</span>
|
|
454
|
+
</div>
|
|
439
455
|
</div>
|
|
440
456
|
</div>
|
|
441
|
-
|
|
457
|
+
)}
|
|
442
458
|
|
|
443
|
-
<div className=
|
|
444
|
-
<div className="relative group flex-1 min-w-[
|
|
445
|
-
<
|
|
446
|
-
className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--kyro-text-secondary)] opacity-40 group-focus-within:opacity-100 transition-opacity"
|
|
447
|
-
fill="none"
|
|
448
|
-
stroke="currentColor"
|
|
449
|
-
viewBox="0 0 24 24"
|
|
450
|
-
>
|
|
451
|
-
<path
|
|
452
|
-
strokeLinecap="round"
|
|
453
|
-
strokeLinejoin="round"
|
|
454
|
-
strokeWidth="2.5"
|
|
455
|
-
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
|
456
|
-
/>
|
|
457
|
-
</svg>
|
|
459
|
+
<div className={`flex items-center gap-3 flex-wrap lg:flex-nowrap ${pickerMode ? "w-full" : ""}`}>
|
|
460
|
+
<div className="relative group flex-1 min-w-[200px]">
|
|
461
|
+
<Search className="w-4 h-4" />
|
|
458
462
|
<input
|
|
459
463
|
type="text"
|
|
460
464
|
placeholder="Search assets..."
|
|
@@ -464,117 +468,123 @@ export function MediaGallery({
|
|
|
464
468
|
/>
|
|
465
469
|
</div>
|
|
466
470
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
471
|
+
{!pickerMode && (
|
|
472
|
+
<>
|
|
473
|
+
<div className="flex bg-[var(--kyro-surface-accent)] p-1 rounded-xl border border-[var(--kyro-border)]">
|
|
474
|
+
<button
|
|
475
|
+
onClick={() => setView("grid")}
|
|
476
|
+
className={`p-2 rounded-lg transition-all ${view === "grid" ? "bg-[var(--kyro-surface)] shadow-sm text-[var(--kyro-text-primary)]" : "text-[var(--kyro-text-secondary)] opacity-50 hover:opacity-100"}`}
|
|
477
|
+
>
|
|
478
|
+
<Grid className="w-4 h-4" />
|
|
479
|
+
</button>
|
|
480
|
+
<button
|
|
481
|
+
onClick={() => setView("list")}
|
|
482
|
+
className={`p-2 rounded-lg transition-all ${view === "list" ? "bg-[var(--kyro-surface)] shadow-sm text-[var(--kyro-text-primary)]" : "text-[var(--kyro-text-secondary)] opacity-50 hover:opacity-100"}`}
|
|
483
|
+
>
|
|
484
|
+
<FileIcon className="w-4 h-4" />
|
|
485
|
+
</button>
|
|
486
|
+
</div>
|
|
481
487
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
488
|
+
{canUpload && (
|
|
489
|
+
<button
|
|
490
|
+
onClick={() => fileInputRef.current?.click()}
|
|
491
|
+
className="flex items-center gap-2 px-6 py-2.5 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-xl font-bold text-xs shadow-lg active:scale-95 transition-all"
|
|
492
|
+
>
|
|
493
|
+
<Maximize2 className="w-4 h-4" />
|
|
494
|
+
Upload
|
|
495
|
+
</button>
|
|
496
|
+
)}
|
|
497
|
+
</>
|
|
490
498
|
)}
|
|
491
499
|
</div>
|
|
492
500
|
</div>
|
|
493
501
|
|
|
494
502
|
<div className="flex flex-1 min-h-0 overflow-hidden">
|
|
495
503
|
{/* Folders Sidebar */}
|
|
496
|
-
|
|
497
|
-
<div className="
|
|
498
|
-
<div>
|
|
499
|
-
<
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
504
|
+
{!pickerMode && (
|
|
505
|
+
<div className="w-64 border-r border-[var(--kyro-border)] surface-tile mt-6 overflow-y-auto hidden md:block">
|
|
506
|
+
<div className="p-6 space-y-6">
|
|
507
|
+
<div>
|
|
508
|
+
<span className="text-[10px] font-bold tracking-[0.2em] text-[var(--kyro-text-secondary)] opacity-40 block mb-4">
|
|
509
|
+
Quick Filters
|
|
510
|
+
</span>
|
|
511
|
+
<div className="space-y-1">
|
|
512
|
+
{(
|
|
513
|
+
[
|
|
514
|
+
"all",
|
|
515
|
+
"image",
|
|
516
|
+
"video",
|
|
517
|
+
"audio",
|
|
518
|
+
"document",
|
|
519
|
+
"archive",
|
|
520
|
+
] as const
|
|
521
|
+
).map((t) => (
|
|
522
|
+
<button
|
|
523
|
+
key={t}
|
|
524
|
+
onClick={() => setFilter(t)}
|
|
525
|
+
className={`w-full flex items-center gap-3 px-4 py-2 rounded-xl text-[11px] font-bold capitalize transition-all ${filter === t ? "text-[var(--kyro-text-primary)] bg-[var(--kyro-surface-accent)]" : "text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]/50"}`}
|
|
526
|
+
>
|
|
527
|
+
<span
|
|
528
|
+
className={`w-1.5 h-1.5 rounded-full ${filter === t ? "bg-[var(--kyro-primary)]" : "bg-transparent border border-current opacity-30"}`}
|
|
529
|
+
/>
|
|
530
|
+
{t}
|
|
531
|
+
</button>
|
|
532
|
+
))}
|
|
533
|
+
</div>
|
|
534
|
+
</div>
|
|
535
|
+
|
|
536
|
+
<div className="pt-6 border-t border-[var(--kyro-border)]">
|
|
537
|
+
<div className="flex items-center justify-between mb-4">
|
|
538
|
+
<span className="text-[10px] font-bold tracking-[0.2em] text-[var(--kyro-text-secondary)] opacity-40">
|
|
539
|
+
Folders
|
|
540
|
+
</span>
|
|
513
541
|
<button
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
className={`w-full flex items-center gap-3 px-4 py-2 rounded-xl text-[11px] font-bold capitalize transition-all ${filter === t ? "text-[var(--kyro-text-primary)] bg-[var(--kyro-surface-accent)]" : "text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]/50"}`}
|
|
542
|
+
onClick={() => setShowNewFolderModal(true)}
|
|
543
|
+
className="p-1.5 hover:bg-[var(--kyro-surface-accent)] rounded-lg transition-colors text-[var(--kyro-text-primary)]"
|
|
517
544
|
>
|
|
518
|
-
<
|
|
519
|
-
className={`w-1.5 h-1.5 rounded-full ${filter === t ? "bg-[var(--kyro-primary)]" : "bg-transparent border border-current opacity-30"}`}
|
|
520
|
-
/>
|
|
521
|
-
{t}
|
|
545
|
+
<FolderPlus className="w-4 h-4" />
|
|
522
546
|
</button>
|
|
523
|
-
|
|
524
|
-
</div>
|
|
525
|
-
</div>
|
|
547
|
+
</div>
|
|
526
548
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
549
|
+
<nav className="space-y-1">
|
|
550
|
+
<button
|
|
551
|
+
onClick={() => setCurrentFolder("")}
|
|
552
|
+
className={`w-full flex items-center gap-3 px-4 py-2.5 rounded-xl text-xs font-bold transition-all ${currentFolder === "" ? "bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] shadow-md" : "text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] hover:text-[var(--kyro-text-primary)]"}`}
|
|
553
|
+
>
|
|
554
|
+
<FolderInput className="w-4 h-4 opacity-70" />
|
|
555
|
+
All Assets
|
|
556
|
+
</button>
|
|
557
|
+
{folders.map((folder) => (
|
|
558
|
+
<div key={folder} className="group relative">
|
|
559
|
+
<button
|
|
560
|
+
onClick={() => setCurrentFolder(folder)}
|
|
561
|
+
className={`w-full flex items-center gap-3 px-4 py-2.5 rounded-xl text-xs font-bold transition-all ${currentFolder === folder ? "bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] shadow-md" : "text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] hover:text-[var(--kyro-text-primary)]"}`}
|
|
562
|
+
>
|
|
563
|
+
<div className="w-4 h-4 flex items-center justify-center opacity-70">
|
|
564
|
+
<Folder fill={currentFolder === folder ? "currentColor" : "none"} />
|
|
565
|
+
</div>
|
|
566
|
+
{folder}
|
|
567
|
+
</button>
|
|
568
|
+
<button
|
|
569
|
+
onClick={(e) => {
|
|
570
|
+
e.stopPropagation();
|
|
571
|
+
handleDeleteFolder(folder);
|
|
572
|
+
}}
|
|
573
|
+
className="absolute right-2 top-1/2 -translate-y-1/2 p-1.5 text-red-500 opacity-0 group-hover:opacity-100 transition-all hover:bg-red-50 rounded-lg"
|
|
574
|
+
>
|
|
575
|
+
<Trash2 className="w-3.5 h-3.5" />
|
|
576
|
+
</button>
|
|
577
|
+
</div>
|
|
578
|
+
))}
|
|
579
|
+
</nav>
|
|
538
580
|
</div>
|
|
539
|
-
|
|
540
|
-
<nav className="space-y-1">
|
|
541
|
-
<button
|
|
542
|
-
onClick={() => setCurrentFolder("")}
|
|
543
|
-
className={`w-full flex items-center gap-3 px-4 py-2.5 rounded-xl text-xs font-bold transition-all ${currentFolder === "" ? "bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] shadow-md" : "text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] hover:text-[var(--kyro-text-primary)]"}`}
|
|
544
|
-
>
|
|
545
|
-
<FolderInput className="w-4 h-4 opacity-70" />
|
|
546
|
-
All Assets
|
|
547
|
-
</button>
|
|
548
|
-
{folders.map((folder) => (
|
|
549
|
-
<div key={folder} className="group relative">
|
|
550
|
-
<button
|
|
551
|
-
onClick={() => setCurrentFolder(folder)}
|
|
552
|
-
className={`w-full flex items-center gap-3 px-4 py-2.5 rounded-xl text-xs font-bold transition-all ${currentFolder === folder ? "bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] shadow-md" : "text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] hover:text-[var(--kyro-text-primary)]"}`}
|
|
553
|
-
>
|
|
554
|
-
<div className="w-4 h-4 flex items-center justify-center opacity-70">
|
|
555
|
-
<Folder fill={currentFolder === folder ? "currentColor" : "none"} />
|
|
556
|
-
</div>
|
|
557
|
-
{folder}
|
|
558
|
-
</button>
|
|
559
|
-
<button
|
|
560
|
-
onClick={(e) => {
|
|
561
|
-
e.stopPropagation();
|
|
562
|
-
handleDeleteFolder(folder);
|
|
563
|
-
}}
|
|
564
|
-
className="absolute right-2 top-1/2 -translate-y-1/2 p-1.5 text-red-500 opacity-0 group-hover:opacity-100 transition-all hover:bg-red-50 rounded-lg"
|
|
565
|
-
>
|
|
566
|
-
<Trash2 className="w-3.5 h-3.5" />
|
|
567
|
-
</button>
|
|
568
|
-
</div>
|
|
569
|
-
))}
|
|
570
|
-
</nav>
|
|
571
581
|
</div>
|
|
572
582
|
</div>
|
|
573
|
-
|
|
583
|
+
)}
|
|
574
584
|
|
|
575
585
|
{/* Main Content Area */}
|
|
576
586
|
<div className="flex-1 flex flex-col min-h-0 bg-[var(--kyro-bg)]">
|
|
577
|
-
<div className=
|
|
587
|
+
<div className={`flex-1 overflow-y-auto custom-scrollbar ${pickerMode ? "px-2 py-4" : "py-8 px-4"}`}>
|
|
578
588
|
{loading ? (
|
|
579
589
|
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
|
|
580
590
|
<Shimmer variant="media-card" count={12} />
|
|
@@ -591,7 +601,7 @@ export function MediaGallery({
|
|
|
591
601
|
Upload your first file or create a folder to organize your
|
|
592
602
|
media assets.
|
|
593
603
|
</p>
|
|
594
|
-
{canUpload && (
|
|
604
|
+
{!pickerMode && canUpload && (
|
|
595
605
|
<button
|
|
596
606
|
onClick={() => fileInputRef.current?.click()}
|
|
597
607
|
className="mt-8 px-8 py-3 bg-[var(--kyro-text-primary)] text-[var(--kyro-bg)] rounded-2xl font-bold text-xs hover:scale-105 transition-all shadow-xl"
|
|
@@ -648,21 +658,9 @@ export function MediaGallery({
|
|
|
648
658
|
<div className="flex gap-1">
|
|
649
659
|
<button
|
|
650
660
|
onClick={(e) => handleSelectOne(item.id, e)}
|
|
651
|
-
className={`p-1.5 rounded-lg transition-all ${selectedIds.has(item.id) ? "
|
|
661
|
+
className={`kyro-btn-primary p-1.5 rounded-lg transition-all ${selectedIds.has(item.id) ? "" : "bg-white/10 text-white hover:bg-white/20"}`}
|
|
652
662
|
>
|
|
653
|
-
<
|
|
654
|
-
className="w-3 h-3"
|
|
655
|
-
fill="none"
|
|
656
|
-
stroke="currentColor"
|
|
657
|
-
viewBox="0 0 24 24"
|
|
658
|
-
>
|
|
659
|
-
<path
|
|
660
|
-
strokeLinecap="round"
|
|
661
|
-
strokeLinejoin="round"
|
|
662
|
-
strokeWidth="3"
|
|
663
|
-
d="M5 13l4 4L19 7"
|
|
664
|
-
/>
|
|
665
|
-
</svg>
|
|
663
|
+
<Check className="w-4 h-4" />
|
|
666
664
|
</button>
|
|
667
665
|
</div>
|
|
668
666
|
</div>
|
|
@@ -670,19 +668,7 @@ export function MediaGallery({
|
|
|
670
668
|
|
|
671
669
|
{selectedIds.has(item.id) && (
|
|
672
670
|
<div className="absolute top-3 left-3 w-6 h-6 rounded-lg bg-[var(--kyro-primary)] text-white flex items-center justify-center shadow-lg border-2 border-white/20 animate-in zoom-in duration-300">
|
|
673
|
-
<
|
|
674
|
-
className="w-3 h-3"
|
|
675
|
-
fill="none"
|
|
676
|
-
stroke="currentColor"
|
|
677
|
-
viewBox="0 0 24 24"
|
|
678
|
-
>
|
|
679
|
-
<path
|
|
680
|
-
strokeLinecap="round"
|
|
681
|
-
strokeLinejoin="round"
|
|
682
|
-
strokeWidth="3"
|
|
683
|
-
d="M5 13l4 4L19 7"
|
|
684
|
-
/>
|
|
685
|
-
</svg>
|
|
671
|
+
<Check className="w-4 h-4" />
|
|
686
672
|
</div>
|
|
687
673
|
)}
|
|
688
674
|
</div>
|
|
@@ -770,7 +756,7 @@ export function MediaGallery({
|
|
|
770
756
|
e.stopPropagation();
|
|
771
757
|
handleSelectOne(item.id, e);
|
|
772
758
|
}}
|
|
773
|
-
className={`p-2 rounded-lg transition-all ${selectedIds.has(item.id) ? "
|
|
759
|
+
className={`kyro-btn-primary p-2 rounded-lg transition-all ${selectedIds.has(item.id) ? "" : "text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] opacity-0 group-hover:opacity-100"}`}
|
|
774
760
|
>
|
|
775
761
|
<Grid className="w-4 h-4" />
|
|
776
762
|
</button>
|
|
@@ -783,35 +769,16 @@ export function MediaGallery({
|
|
|
783
769
|
)}
|
|
784
770
|
</div>
|
|
785
771
|
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
</span>
|
|
792
|
-
<div className="flex gap-2">
|
|
793
|
-
<button
|
|
794
|
-
disabled={page === 1}
|
|
795
|
-
onClick={() => setPage(page - 1)}
|
|
796
|
-
className="px-4 py-2 border border-[var(--kyro-border)] rounded-xl text-xs font-bold text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] disabled:opacity-30 transition-all"
|
|
797
|
-
>
|
|
798
|
-
Previous
|
|
799
|
-
</button>
|
|
800
|
-
<button
|
|
801
|
-
disabled={page === totalPages}
|
|
802
|
-
onClick={() => setPage(page + 1)}
|
|
803
|
-
className="px-6 py-2 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-xl text-xs font-bold shadow-lg hover:opacity-90 disabled:opacity-30 transition-all"
|
|
804
|
-
>
|
|
805
|
-
Next
|
|
806
|
-
</button>
|
|
807
|
-
</div>
|
|
808
|
-
</div>
|
|
809
|
-
)}
|
|
772
|
+
<Pagination
|
|
773
|
+
page={page}
|
|
774
|
+
totalPages={totalPages}
|
|
775
|
+
onPageChange={setPage}
|
|
776
|
+
/>
|
|
810
777
|
</div>
|
|
811
778
|
</div>
|
|
812
779
|
|
|
813
780
|
{/* Upload Banner */}
|
|
814
|
-
{uploading && (
|
|
781
|
+
{!pickerMode && uploading && (
|
|
815
782
|
<div className="fixed bottom-12 left-1/2 -translate-x-1/2 z-[60] w-full max-w-lg">
|
|
816
783
|
<div className="bg-[var(--kyro-surface)] border border-[var(--kyro-border)] rounded-[2rem] shadow-2xl p-6 ring-1 ring-white/10 animate-in slide-in-from-bottom-12 duration-700">
|
|
817
784
|
<div className="flex items-center justify-between mb-4">
|
|
@@ -882,7 +849,7 @@ export function MediaGallery({
|
|
|
882
849
|
Confirm Selection
|
|
883
850
|
</button>
|
|
884
851
|
)}
|
|
885
|
-
{canDelete && (
|
|
852
|
+
{!pickerMode && canDelete && (
|
|
886
853
|
<button
|
|
887
854
|
onClick={handleBulkDelete}
|
|
888
855
|
className="p-3 bg-red-500/10 text-red-500 hover:bg-red-500 hover:text-white rounded-full transition-all active:scale-90"
|
|
@@ -974,7 +941,7 @@ export function MediaGallery({
|
|
|
974
941
|
navigator.clipboard.writeText(
|
|
975
942
|
getAbsoluteUrl(panelItem.url),
|
|
976
943
|
);
|
|
977
|
-
|
|
944
|
+
toast.success("URL copied to clipboard");
|
|
978
945
|
}}
|
|
979
946
|
className="p-3 bg-[var(--kyro-surface-accent)] hover:bg-[var(--kyro-border)] border border-[var(--kyro-border)] rounded-xl transition-all"
|
|
980
947
|
>
|
|
@@ -1016,109 +983,119 @@ export function MediaGallery({
|
|
|
1016
983
|
</div>
|
|
1017
984
|
</div>
|
|
1018
985
|
|
|
1019
|
-
|
|
1020
|
-
<
|
|
1021
|
-
onClick={() => {
|
|
1022
|
-
const link = document.createElement("a");
|
|
1023
|
-
link.href = getAbsoluteUrl(panelItem.url);
|
|
1024
|
-
link.download = panelItem.filename;
|
|
1025
|
-
link.click();
|
|
1026
|
-
}}
|
|
1027
|
-
className="flex-1 flex items-center justify-center gap-2 px-6 py-3 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-xl font-bold text-xs shadow-lg hover:opacity-90 transition-all"
|
|
1028
|
-
>
|
|
1029
|
-
<Download className="w-4 h-4" />
|
|
1030
|
-
Download
|
|
1031
|
-
</button>
|
|
1032
|
-
{panelItem.type === "image" && canUpdate && (
|
|
1033
|
-
<button
|
|
1034
|
-
onClick={() => setShowCrop(true)}
|
|
1035
|
-
className="p-3 border border-[var(--kyro-border)] rounded-xl text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)] transition-all"
|
|
1036
|
-
>
|
|
1037
|
-
<CropIcon className="w-4 h-4" />
|
|
1038
|
-
</button>
|
|
1039
|
-
)}
|
|
1040
|
-
{canDelete && (
|
|
986
|
+
{!pickerMode && (
|
|
987
|
+
<div className="pt-8 border-t border-[var(--kyro-border)] mt-8 flex gap-3 pb-8">
|
|
1041
988
|
<button
|
|
1042
989
|
onClick={() => {
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
onConfirm: async () => {
|
|
1048
|
-
try {
|
|
1049
|
-
await apiDelete(`/api/media/${panelItem.id}`);
|
|
1050
|
-
setPanelItem(null);
|
|
1051
|
-
loadMedia();
|
|
1052
|
-
} catch (error) {
|
|
1053
|
-
console.error("Delete failed:", error);
|
|
1054
|
-
alert({ title: "Error", message: "Failed to delete asset" });
|
|
1055
|
-
}
|
|
1056
|
-
}
|
|
1057
|
-
});
|
|
990
|
+
const link = document.createElement("a");
|
|
991
|
+
link.href = getAbsoluteUrl(panelItem.url);
|
|
992
|
+
link.download = panelItem.filename;
|
|
993
|
+
link.click();
|
|
1058
994
|
}}
|
|
1059
|
-
className="
|
|
995
|
+
className="flex-1 flex items-center justify-center gap-2 px-6 py-3 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-xl font-bold text-xs shadow-lg hover:opacity-90 transition-all"
|
|
1060
996
|
>
|
|
1061
|
-
<
|
|
997
|
+
<Download className="w-4 h-4" />
|
|
998
|
+
Download
|
|
1062
999
|
</button>
|
|
1063
|
-
|
|
1064
|
-
|
|
1000
|
+
{panelItem.type === "image" && canUpdate && (
|
|
1001
|
+
<button
|
|
1002
|
+
onClick={() => setShowCrop(true)}
|
|
1003
|
+
className="p-3 border border-[var(--kyro-border)] rounded-xl text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)] transition-all"
|
|
1004
|
+
>
|
|
1005
|
+
<CropIcon className="w-4 h-4" />
|
|
1006
|
+
</button>
|
|
1007
|
+
)}
|
|
1008
|
+
{canDelete && (
|
|
1009
|
+
<button
|
|
1010
|
+
onClick={() => {
|
|
1011
|
+
confirm({
|
|
1012
|
+
title: "Delete Asset",
|
|
1013
|
+
message: `Are you sure you want to delete ${panelItem.filename}? This cannot be undone.`,
|
|
1014
|
+
variant: "danger",
|
|
1015
|
+
onConfirm: async () => {
|
|
1016
|
+
try {
|
|
1017
|
+
await apiDelete(`/api/media/${panelItem.id}`);
|
|
1018
|
+
setPanelItem(null);
|
|
1019
|
+
loadMedia();
|
|
1020
|
+
} catch (error) {
|
|
1021
|
+
console.error("Delete failed:", error);
|
|
1022
|
+
toast.error("Failed to delete asset");
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
});
|
|
1026
|
+
}}
|
|
1027
|
+
className="p-3 border border-red-100 text-red-500 rounded-xl hover:bg-red-50 transition-all"
|
|
1028
|
+
>
|
|
1029
|
+
<Trash2 className="w-4 h-4" />
|
|
1030
|
+
</button>
|
|
1031
|
+
)}
|
|
1032
|
+
</div>
|
|
1033
|
+
)}
|
|
1065
1034
|
</div>
|
|
1066
1035
|
)}
|
|
1067
1036
|
</SlidePanel>
|
|
1068
1037
|
|
|
1069
1038
|
{/* Preview Modal */}
|
|
1070
|
-
{showPreview &&
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
</
|
|
1083
|
-
<
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
>
|
|
1087
|
-
<X className="w-6 h-6" />
|
|
1088
|
-
</button>
|
|
1089
|
-
</div>
|
|
1090
|
-
<div className="flex-1 w-full flex items-center justify-center p-12">
|
|
1091
|
-
{panelItem.type === "image" ? (
|
|
1092
|
-
<img
|
|
1093
|
-
src={getAbsoluteUrl(panelItem.url)}
|
|
1094
|
-
alt=""
|
|
1095
|
-
className="max-h-full max-w-full object-contain shadow-2xl rounded-lg animate-in zoom-in-95 duration-500"
|
|
1096
|
-
/>
|
|
1097
|
-
) : panelItem.type === "video" ? (
|
|
1098
|
-
<video
|
|
1099
|
-
src={getAbsoluteUrl(panelItem.url)}
|
|
1100
|
-
controls
|
|
1101
|
-
autoPlay
|
|
1102
|
-
className="max-h-full max-w-full rounded-lg shadow-2xl"
|
|
1103
|
-
/>
|
|
1104
|
-
) : (
|
|
1105
|
-
<div className="text-white text-center">
|
|
1106
|
-
<FileIcon className="w-24 h-24 mx-auto mb-6 opacity-20" />
|
|
1107
|
-
<p className="text-xl font-bold opacity-50">
|
|
1108
|
-
Preview not available for this file type
|
|
1109
|
-
</p>
|
|
1110
|
-
</div>
|
|
1111
|
-
)}
|
|
1039
|
+
{showPreview && panelItem && (
|
|
1040
|
+
<Modal
|
|
1041
|
+
open={showPreview}
|
|
1042
|
+
onClose={() => setShowPreview(false)}
|
|
1043
|
+
title=""
|
|
1044
|
+
size="full"
|
|
1045
|
+
variant="lightbox"
|
|
1046
|
+
>
|
|
1047
|
+
<div className="flex items-center justify-between p-6">
|
|
1048
|
+
<div className="flex flex-col">
|
|
1049
|
+
<span className="text-white font-bold text-lg tracking-tight">
|
|
1050
|
+
{panelItem.filename}
|
|
1051
|
+
</span>
|
|
1052
|
+
<span className="text-white/40 text-[10px] font-bold tracking-widest mt-1">
|
|
1053
|
+
{formatFileSize(panelItem.fileSize)} · {panelItem.mimeType}
|
|
1054
|
+
</span>
|
|
1112
1055
|
</div>
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1056
|
+
<button
|
|
1057
|
+
onClick={() => setShowPreview(false)}
|
|
1058
|
+
className="p-3 bg-white/10 hover:bg-white/20 text-white rounded-2xl transition-all active:scale-90"
|
|
1059
|
+
>
|
|
1060
|
+
<X className="w-6 h-6" />
|
|
1061
|
+
</button>
|
|
1062
|
+
</div>
|
|
1063
|
+
<div className="flex-1 w-full flex items-center justify-center p-12">
|
|
1064
|
+
{panelItem.type === "image" ? (
|
|
1065
|
+
<img
|
|
1066
|
+
src={getAbsoluteUrl(panelItem.url)}
|
|
1067
|
+
alt=""
|
|
1068
|
+
className="max-h-full max-w-full object-contain shadow-2xl rounded-lg animate-in zoom-in-95 duration-500"
|
|
1069
|
+
/>
|
|
1070
|
+
) : panelItem.type === "video" ? (
|
|
1071
|
+
<video
|
|
1072
|
+
src={getAbsoluteUrl(panelItem.url)}
|
|
1073
|
+
controls
|
|
1074
|
+
autoPlay
|
|
1075
|
+
className="max-h-full max-w-full rounded-lg shadow-2xl"
|
|
1076
|
+
/>
|
|
1077
|
+
) : (
|
|
1078
|
+
<div className="text-white text-center">
|
|
1079
|
+
<FileIcon className="w-24 h-24 mx-auto mb-6 opacity-20" />
|
|
1080
|
+
<p className="text-xl font-bold opacity-50">
|
|
1081
|
+
Preview not available for this file type
|
|
1082
|
+
</p>
|
|
1083
|
+
</div>
|
|
1084
|
+
)}
|
|
1085
|
+
</div>
|
|
1086
|
+
</Modal>
|
|
1087
|
+
)}
|
|
1116
1088
|
|
|
1117
1089
|
{/* Crop Modal */}
|
|
1118
|
-
{showCrop &&
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1090
|
+
{!pickerMode && showCrop && panelItem && (
|
|
1091
|
+
<Modal
|
|
1092
|
+
open={showCrop}
|
|
1093
|
+
onClose={() => setShowCrop(false)}
|
|
1094
|
+
title=""
|
|
1095
|
+
size="full"
|
|
1096
|
+
variant="lightbox"
|
|
1097
|
+
>
|
|
1098
|
+
<div className="flex flex-col h-full p-8">
|
|
1122
1099
|
<div className="flex items-center justify-between mb-8">
|
|
1123
1100
|
<h3 className="text-white font-bold text-2xl tracking-tighter">
|
|
1124
1101
|
Crop Image
|
|
@@ -1152,87 +1129,77 @@ export function MediaGallery({
|
|
|
1152
1129
|
/>
|
|
1153
1130
|
</ReactCrop>
|
|
1154
1131
|
</div>
|
|
1155
|
-
</div
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
<path
|
|
1178
|
-
strokeLinecap="round"
|
|
1179
|
-
strokeLinejoin="round"
|
|
1180
|
-
strokeWidth={2}
|
|
1181
|
-
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2H5a2 2 0 00-2 2v1m2 2a2 2 0 11-4 0 2 2 0 014 0zm2 2h.008v.008H5v-.008z"
|
|
1182
|
-
/>
|
|
1183
|
-
</svg>
|
|
1184
|
-
</div>
|
|
1185
|
-
<h3 className="text-xl font-bold text-[var(--kyro-text-primary)] mb-2">
|
|
1186
|
-
Storage Not Configured
|
|
1187
|
-
</h3>
|
|
1188
|
-
<p className="text-[var(--kyro-text-secondary)] mb-6 text-sm">
|
|
1189
|
-
Before uploading media, you need to configure your storage
|
|
1190
|
-
settings. Choose where files should be stored and how URLs are
|
|
1191
|
-
generated.
|
|
1192
|
-
</p>
|
|
1193
|
-
<div className="flex gap-3">
|
|
1194
|
-
<a
|
|
1195
|
-
href="/settings/storage-settings"
|
|
1196
|
-
className="flex-1 px-4 py-3 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-xl font-bold text-center hover:opacity-90 transition-colors"
|
|
1197
|
-
>
|
|
1198
|
-
Configure Storage
|
|
1199
|
-
</a>
|
|
1200
|
-
<button
|
|
1201
|
-
type="button"
|
|
1202
|
-
onClick={() => {
|
|
1203
|
-
// Set default storage config programmatically
|
|
1204
|
-
apiPost("/api/globals/storage-settings", {
|
|
1205
|
-
provider: "local",
|
|
1206
|
-
local: {
|
|
1207
|
-
uploadDir: "./public/uploads",
|
|
1208
|
-
baseUrl: "/uploads",
|
|
1209
|
-
},
|
|
1210
|
-
}).then(() => {
|
|
1211
|
-
setShowStorageConfigModal(false);
|
|
1212
|
-
setStorageConfigured(true);
|
|
1213
|
-
window.location.reload();
|
|
1214
|
-
});
|
|
1215
|
-
}}
|
|
1216
|
-
className="flex-1 px-4 py-3 border border-[var(--kyro-border)] text-[var(--kyro-text-secondary)] rounded-xl font-bold hover:bg-[var(--kyro-surface-accent)] transition-colors"
|
|
1217
|
-
>
|
|
1218
|
-
Use Defaults
|
|
1219
|
-
</button>
|
|
1220
|
-
</div>
|
|
1221
|
-
</div>
|
|
1132
|
+
</div>
|
|
1133
|
+
</Modal>
|
|
1134
|
+
)}
|
|
1135
|
+
{!pickerMode && (
|
|
1136
|
+
<PromptModal
|
|
1137
|
+
open={showNewFolderModal}
|
|
1138
|
+
onClose={() => setShowNewFolderModal(false)}
|
|
1139
|
+
onSubmit={createFolder}
|
|
1140
|
+
title="Create New Folder"
|
|
1141
|
+
placeholder="Folder name"
|
|
1142
|
+
/>
|
|
1143
|
+
)}
|
|
1144
|
+
{!pickerMode && (
|
|
1145
|
+
<Modal
|
|
1146
|
+
open={showStorageConfigModal}
|
|
1147
|
+
onClose={() => setShowStorageConfigModal(false)}
|
|
1148
|
+
title="Storage Not Configured"
|
|
1149
|
+
size="md"
|
|
1150
|
+
>
|
|
1151
|
+
<div className="text-center">
|
|
1152
|
+
<div className="w-16 h-16 mx-auto mb-4 rounded-full bg-[var(--kyro-sidebar-active)] flex items-center justify-center">
|
|
1153
|
+
<Server className="w-8 h-8 text-[var(--kyro-sidebar-text-active)]" />
|
|
1222
1154
|
</div>
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1155
|
+
<p className="text-[var(--kyro-text-secondary)] mb-6 text-sm">
|
|
1156
|
+
Before uploading media, you need to configure your storage
|
|
1157
|
+
settings. Choose where files should be stored and how URLs are
|
|
1158
|
+
generated.
|
|
1159
|
+
</p>
|
|
1160
|
+
<div className="flex gap-3">
|
|
1161
|
+
<a
|
|
1162
|
+
href="/settings/storage-settings"
|
|
1163
|
+
className="flex-1 px-4 py-3 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-xl font-bold text-center hover:opacity-90 transition-colors"
|
|
1164
|
+
>
|
|
1165
|
+
Configure Storage
|
|
1166
|
+
</a>
|
|
1167
|
+
<button
|
|
1168
|
+
type="button"
|
|
1169
|
+
onClick={() => {
|
|
1170
|
+
// Set default storage config programmatically
|
|
1171
|
+
apiPost("/api/globals/storage-settings", {
|
|
1172
|
+
provider: "local",
|
|
1173
|
+
local: {
|
|
1174
|
+
uploadDir: "./public/uploads",
|
|
1175
|
+
baseUrl: "/uploads",
|
|
1176
|
+
},
|
|
1177
|
+
}).then(() => {
|
|
1178
|
+
setShowStorageConfigModal(false);
|
|
1179
|
+
setStorageConfigured(true);
|
|
1180
|
+
window.location.reload();
|
|
1181
|
+
});
|
|
1182
|
+
}}
|
|
1183
|
+
className="flex-1 px-4 py-3 border border-[var(--kyro-border)] text-[var(--kyro-text-secondary)] rounded-xl font-bold hover:bg-[var(--kyro-surface-accent)] transition-colors"
|
|
1184
|
+
>
|
|
1185
|
+
Use Defaults
|
|
1186
|
+
</button>
|
|
1187
|
+
</div>
|
|
1188
|
+
</div>
|
|
1189
|
+
</Modal>
|
|
1190
|
+
)}
|
|
1191
|
+
{!pickerMode && (
|
|
1192
|
+
<input
|
|
1193
|
+
type="file"
|
|
1194
|
+
ref={fileInputRef}
|
|
1195
|
+
onChange={(e) => {
|
|
1196
|
+
if (e.target.files) handleUpload(e.target.files);
|
|
1197
|
+
}}
|
|
1198
|
+
multiple
|
|
1199
|
+
className="hidden"
|
|
1200
|
+
accept="image/*,video/*,audio/*,.pdf,.doc,.docx,.txt,.zip,.rar,.tar"
|
|
1201
|
+
/>
|
|
1202
|
+
)}
|
|
1236
1203
|
</div>
|
|
1237
1204
|
);
|
|
1238
1205
|
}
|