opacacms 0.1.0 → 0.1.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.
Files changed (247) hide show
  1. package/dist/admin/index.js +49 -0
  2. package/dist/{chunk-2zm8cy1w.js → admin/webcomponent.js} +116 -168
  3. package/dist/{chunk-6dhs73zq.js → chunk-2yz1nsxs.js} +1 -1
  4. package/dist/chunk-fa5mg0hr.js +96 -0
  5. package/dist/{chunk-kwp83w8b.js → chunk-m09hahe2.js} +7 -7
  6. package/dist/{chunk-hmhcense.js → chunk-ry15hke8.js} +253 -4
  7. package/dist/chunk-vtvqfhgy.js +2442 -0
  8. package/dist/{chunk-f3nvxn63.js → chunk-y8hc6nm4.js} +1 -1
  9. package/dist/{src/cli → cli}/index.js +10 -10
  10. package/dist/{src/client.js → client.js} +2 -2
  11. package/dist/{src/db → db}/bun-sqlite.js +10 -10
  12. package/dist/{src/db → db}/d1.js +8 -8
  13. package/dist/db/index.d.ts +2 -0
  14. package/dist/db/index.js +7 -0
  15. package/dist/db/migration.d.ts +39 -0
  16. package/dist/{src/db → db}/postgres.js +10 -10
  17. package/dist/{src/db → db}/sqlite.js +8 -8
  18. package/dist/index.d.ts +0 -2
  19. package/dist/index.js +13 -0
  20. package/dist/{src/runtimes → runtimes}/bun.js +5 -6
  21. package/dist/{src/runtimes → runtimes}/cloudflare-workers.js +5 -6
  22. package/dist/{src/runtimes → runtimes}/next.js +5 -6
  23. package/dist/{src/runtimes → runtimes}/node.js +5 -6
  24. package/dist/{src/server.js → server.js} +7 -8
  25. package/dist/storage/index.d.ts +0 -3
  26. package/dist/storage/index.js +35 -0
  27. package/dist/types.d.ts +5 -2
  28. package/package.json +161 -39
  29. package/bun.lock +0 -34
  30. package/dist/api.d.ts +0 -6
  31. package/dist/chunk-8gkhn1d4.js +0 -309
  32. package/dist/chunk-dy5t83hr.js +0 -261
  33. package/dist/src/admin/index.js +0 -176
  34. package/dist/src/admin/webcomponent.js +0 -19
  35. package/dist/src/api.js +0 -27
  36. package/dist/src/index.js +0 -20
  37. package/dist/src/storage/index.js +0 -355
  38. package/global.d.ts +0 -11
  39. package/src/admin/api-client.ts +0 -63
  40. package/src/admin/auth-client.ts +0 -40
  41. package/src/admin/custom-field.ts +0 -179
  42. package/src/admin/index.ts +0 -15
  43. package/src/admin/react.tsx +0 -72
  44. package/src/admin/router.ts +0 -9
  45. package/src/admin/stores/admin-queries.ts +0 -121
  46. package/src/admin/stores/auth.ts +0 -61
  47. package/src/admin/stores/column-visibility.ts +0 -67
  48. package/src/admin/stores/config.ts +0 -15
  49. package/src/admin/stores/media.ts +0 -95
  50. package/src/admin/stores/query.ts +0 -13
  51. package/src/admin/stores/ui.ts +0 -29
  52. package/src/admin/ui/admin-client.tsx +0 -283
  53. package/src/admin/ui/admin-layout.tsx +0 -276
  54. package/src/admin/ui/components/ColumnVisibilityToggle.tsx +0 -141
  55. package/src/admin/ui/components/DataDetailSheet.tsx +0 -141
  56. package/src/admin/ui/components/DataDetailView.tsx +0 -175
  57. package/src/admin/ui/components/Table.tsx +0 -67
  58. package/src/admin/ui/components/fields/ArrayField.tsx +0 -166
  59. package/src/admin/ui/components/fields/BlocksField.tsx +0 -202
  60. package/src/admin/ui/components/fields/BooleanField.tsx +0 -50
  61. package/src/admin/ui/components/fields/CollapsibleField.tsx +0 -75
  62. package/src/admin/ui/components/fields/DateField.tsx +0 -45
  63. package/src/admin/ui/components/fields/FileField.tsx +0 -322
  64. package/src/admin/ui/components/fields/GroupField.tsx +0 -50
  65. package/src/admin/ui/components/fields/JoinField.tsx +0 -23
  66. package/src/admin/ui/components/fields/NumberField.tsx +0 -46
  67. package/src/admin/ui/components/fields/RadioField.tsx +0 -62
  68. package/src/admin/ui/components/fields/RelationshipField.tsx +0 -278
  69. package/src/admin/ui/components/fields/RowField.tsx +0 -40
  70. package/src/admin/ui/components/fields/SelectField.tsx +0 -59
  71. package/src/admin/ui/components/fields/TabsField.tsx +0 -101
  72. package/src/admin/ui/components/fields/TextAreaField.tsx +0 -54
  73. package/src/admin/ui/components/fields/TextField.tsx +0 -49
  74. package/src/admin/ui/components/fields/VirtualField.tsx +0 -53
  75. package/src/admin/ui/components/fields/index.tsx +0 -371
  76. package/src/admin/ui/components/fields/richtext-editor/index.tsx +0 -211
  77. package/src/admin/ui/components/fields/richtext-editor/nodes/ImageComponent.tsx +0 -142
  78. package/src/admin/ui/components/fields/richtext-editor/nodes/ImageNode.tsx +0 -95
  79. package/src/admin/ui/components/fields/richtext-editor/plugins/ComponentPickerPlugin.tsx +0 -226
  80. package/src/admin/ui/components/fields/richtext-editor/plugins/EditableSyncPlugin.tsx +0 -16
  81. package/src/admin/ui/components/fields/richtext-editor/plugins/NotionToolbarPlugin.tsx +0 -184
  82. package/src/admin/ui/components/fields/richtext-editor/plugins/SimpleToolbarPlugin.tsx +0 -240
  83. package/src/admin/ui/components/fields/richtext-editor/plugins/ValueSyncPlugin.tsx +0 -40
  84. package/src/admin/ui/components/fields/utils.ts +0 -1
  85. package/src/admin/ui/components/link.tsx +0 -41
  86. package/src/admin/ui/components/media/AssetManagerModal.tsx +0 -334
  87. package/src/admin/ui/components/toast.tsx +0 -72
  88. package/src/admin/ui/components/ui/accordion.tsx +0 -51
  89. package/src/admin/ui/components/ui/alert-dialog.tsx +0 -98
  90. package/src/admin/ui/components/ui/blocks.tsx +0 -32
  91. package/src/admin/ui/components/ui/breadcrumbs.tsx +0 -59
  92. package/src/admin/ui/components/ui/button.tsx +0 -26
  93. package/src/admin/ui/components/ui/collapsible.tsx +0 -124
  94. package/src/admin/ui/components/ui/dialog.tsx +0 -79
  95. package/src/admin/ui/components/ui/group.tsx +0 -20
  96. package/src/admin/ui/components/ui/index.ts +0 -17
  97. package/src/admin/ui/components/ui/input.tsx +0 -12
  98. package/src/admin/ui/components/ui/join.tsx +0 -53
  99. package/src/admin/ui/components/ui/label.tsx +0 -11
  100. package/src/admin/ui/components/ui/radio-group.tsx +0 -75
  101. package/src/admin/ui/components/ui/relationship-detail-sheet.tsx +0 -122
  102. package/src/admin/ui/components/ui/relationship.tsx +0 -58
  103. package/src/admin/ui/components/ui/scroll-area.tsx +0 -19
  104. package/src/admin/ui/components/ui/select.tsx +0 -187
  105. package/src/admin/ui/components/ui/separator.tsx +0 -21
  106. package/src/admin/ui/components/ui/sheet.tsx +0 -106
  107. package/src/admin/ui/components/ui/tabs.tsx +0 -116
  108. package/src/admin/ui/components/ui/utils.ts +0 -3
  109. package/src/admin/ui/hooks/use-debounce.ts +0 -15
  110. package/src/admin/ui/styles/_locale-switcher.scss +0 -33
  111. package/src/admin/ui/styles/accordion.scss +0 -60
  112. package/src/admin/ui/styles/animations.scss +0 -41
  113. package/src/admin/ui/styles/asset-manager.scss +0 -547
  114. package/src/admin/ui/styles/badge.scss +0 -13
  115. package/src/admin/ui/styles/base.scss +0 -22
  116. package/src/admin/ui/styles/button.scss +0 -161
  117. package/src/admin/ui/styles/card.scss +0 -13
  118. package/src/admin/ui/styles/collapsible.scss +0 -75
  119. package/src/admin/ui/styles/data-detail.scss +0 -92
  120. package/src/admin/ui/styles/dialog.scss +0 -102
  121. package/src/admin/ui/styles/empty-state.scss +0 -22
  122. package/src/admin/ui/styles/group.scss +0 -19
  123. package/src/admin/ui/styles/index.scss +0 -33
  124. package/src/admin/ui/styles/input.scss +0 -80
  125. package/src/admin/ui/styles/label.scss +0 -12
  126. package/src/admin/ui/styles/layout.scss +0 -56
  127. package/src/admin/ui/styles/lexical.scss +0 -469
  128. package/src/admin/ui/styles/loading.scss +0 -102
  129. package/src/admin/ui/styles/media-registry.scss +0 -597
  130. package/src/admin/ui/styles/pagination.scss +0 -20
  131. package/src/admin/ui/styles/radio-group.scss +0 -66
  132. package/src/admin/ui/styles/row.scss +0 -17
  133. package/src/admin/ui/styles/scrollbar.scss +0 -36
  134. package/src/admin/ui/styles/select.scss +0 -121
  135. package/src/admin/ui/styles/separator.scss +0 -14
  136. package/src/admin/ui/styles/sheet.scss +0 -152
  137. package/src/admin/ui/styles/sidebar.scss +0 -148
  138. package/src/admin/ui/styles/switch.scss +0 -59
  139. package/src/admin/ui/styles/table.scss +0 -207
  140. package/src/admin/ui/styles/tabs.scss +0 -62
  141. package/src/admin/ui/styles/toast.scss +0 -45
  142. package/src/admin/ui/styles/variables.scss +0 -24
  143. package/src/admin/ui/views/collection-list-view.tsx +0 -720
  144. package/src/admin/ui/views/dashboard-view.tsx +0 -263
  145. package/src/admin/ui/views/document-edit-view.tsx +0 -384
  146. package/src/admin/ui/views/global-edit-view.tsx +0 -226
  147. package/src/admin/ui/views/init-view.tsx +0 -182
  148. package/src/admin/ui/views/login-view.tsx +0 -123
  149. package/src/admin/ui/views/media-registry-view.tsx +0 -1104
  150. package/src/admin/ui/views/settings-view.tsx +0 -729
  151. package/src/admin/webcomponent.tsx +0 -15
  152. package/src/api.ts +0 -9
  153. package/src/auth/index.ts +0 -194
  154. package/src/auth/migrations.ts +0 -87
  155. package/src/auth/premissions.ts +0 -46
  156. package/src/cli/commands/generate-types.ts +0 -116
  157. package/src/cli/commands/init.ts +0 -95
  158. package/src/cli/commands/migrate-commands.ts +0 -160
  159. package/src/cli/commands/seed-command.ts +0 -11
  160. package/src/cli/d1-mock.ts +0 -101
  161. package/src/cli/index.test.ts +0 -84
  162. package/src/cli/index.ts +0 -183
  163. package/src/cli/r2-mock.ts +0 -217
  164. package/src/cli/seeding.ts +0 -405
  165. package/src/client.ts +0 -181
  166. package/src/config-utils.ts +0 -102
  167. package/src/config.ts +0 -49
  168. package/src/db/adapter.ts +0 -53
  169. package/src/db/better-sqlite.ts +0 -630
  170. package/src/db/bun-sqlite.ts +0 -646
  171. package/src/db/d1.ts +0 -711
  172. package/src/db/kysely/data-mapper.ts +0 -142
  173. package/src/db/kysely/field-mapper.ts +0 -148
  174. package/src/db/kysely/migration-generator.ts +0 -223
  175. package/src/db/kysely/query-builder.ts +0 -92
  176. package/src/db/kysely/schema-builder.ts +0 -439
  177. package/src/db/kysely/sql-utils.ts +0 -13
  178. package/src/db/postgres.ts +0 -621
  179. package/src/db/sqlite.ts +0 -658
  180. package/src/db/system-schema.ts +0 -121
  181. package/src/index.ts +0 -13
  182. package/src/runtimes/README.md +0 -59
  183. package/src/runtimes/bun.ts +0 -49
  184. package/src/runtimes/cloudflare-workers.ts +0 -38
  185. package/src/runtimes/next.ts +0 -26
  186. package/src/runtimes/node.ts +0 -52
  187. package/src/schema/collection.ts +0 -184
  188. package/src/schema/fields/base.ts +0 -164
  189. package/src/schema/fields/index.ts +0 -427
  190. package/src/schema/global.ts +0 -145
  191. package/src/schema/index.ts +0 -4
  192. package/src/schema/infer.ts +0 -72
  193. package/src/server/admin-router.ts +0 -20
  194. package/src/server/admin.ts +0 -142
  195. package/src/server/assets.ts +0 -306
  196. package/src/server/collection-router.ts +0 -55
  197. package/src/server/handlers.ts +0 -722
  198. package/src/server/middlewares/admin.ts +0 -27
  199. package/src/server/middlewares/auth.ts +0 -89
  200. package/src/server/middlewares/context.ts +0 -17
  201. package/src/server/middlewares/cors.ts +0 -24
  202. package/src/server/middlewares/database-init.ts +0 -74
  203. package/src/server/middlewares/rate-limit.ts +0 -71
  204. package/src/server/router.ts +0 -47
  205. package/src/server/setup-middlewares.ts +0 -58
  206. package/src/server/system-router.ts +0 -35
  207. package/src/server.ts +0 -9
  208. package/src/storage/adapters/cloudflare-r2.ts +0 -136
  209. package/src/storage/adapters/local.ts +0 -146
  210. package/src/storage/adapters/s3.ts +0 -186
  211. package/src/storage/errors.ts +0 -46
  212. package/src/storage/index.ts +0 -5
  213. package/src/storage/types.ts +0 -39
  214. package/src/types.ts +0 -577
  215. package/src/utils/lexical.ts +0 -37
  216. package/src/utils/logger.ts +0 -73
  217. package/src/validation.ts +0 -429
  218. package/src/validator.ts +0 -179
  219. package/test/admin-custom-field.test.ts +0 -162
  220. package/test/admin-react-field.test.tsx +0 -134
  221. package/test/api-features.test.ts +0 -78
  222. package/test/api.test.ts +0 -178
  223. package/test/auth.test.ts +0 -62
  224. package/test/cli-integration.test.ts +0 -146
  225. package/test/cli.test.ts +0 -25
  226. package/test/db/postgres.test.ts +0 -95
  227. package/test/db/sqlite-filter.test.ts +0 -53
  228. package/test/db/sqlite.test.ts +0 -82
  229. package/test/engine-features.test.ts +0 -79
  230. package/test/globals.test.ts +0 -74
  231. package/test/integration-tmp/db-app/opacacms.config.ts +0 -15
  232. package/test/integration-tmp/my-sqlite-app/opacacms.config.ts +0 -25
  233. package/test/integration-tmp/my-test-app/index.ts +0 -8
  234. package/test/integration-tmp/my-test-app/opacacms.config.ts +0 -16
  235. package/test/integration-tmp/my-test-app/package.json +0 -12
  236. package/test/populate.test.ts +0 -79
  237. package/test/runtimes.test.ts +0 -43
  238. package/test/schema-builder.test.ts +0 -107
  239. package/test/schema-features.test.ts +0 -63
  240. package/test/seeding.test.ts +0 -68
  241. package/test/storage/local.test.ts +0 -72
  242. package/test/storage/s3.test.ts +0 -60
  243. package/test/structural-data.test.ts +0 -100
  244. package/test/test-setup.ts +0 -11
  245. package/test/validation.test.ts +0 -162
  246. package/tsconfig.json +0 -42
  247. /package/dist/{src/admin/index.css → admin/webcomponent.css} +0 -0
@@ -1,1104 +0,0 @@
1
- import { useStore } from "@nanostores/react";
2
- import {
3
- Check,
4
- ChevronLeft,
5
- ChevronRight,
6
- Download,
7
- Eye,
8
- File,
9
- FileText,
10
- FolderPlus,
11
- Grid,
12
- Image as ImageIcon,
13
- LayoutList,
14
- Loader2,
15
- Plus,
16
- Save,
17
- Search,
18
- Settings2,
19
- Trash2,
20
- X,
21
- } from "lucide-react";
22
- import { useMemo, useRef, useState } from "react";
23
- import { createPortal } from "react-dom";
24
- import type { SerializableCollection, SerializableConfig } from "../../../types";
25
- import { api } from "../../api-client";
26
- import {
27
- $assets,
28
- $bucketColors,
29
- $mediaCurrentFolder,
30
- $mediaPage,
31
- $mediaSearch,
32
- $mediaSelectedBucket,
33
- $mediaViewMode,
34
- type AssetDoc,
35
- setBucketColor,
36
- setMediaBucket,
37
- setMediaFolder,
38
- setMediaPage,
39
- setMediaSearch,
40
- } from "../../stores/media";
41
- import { notify } from "../../stores/ui";
42
- import {
43
- Button,
44
- Dialog,
45
- DialogContent,
46
- DialogDescription,
47
- DialogFooter,
48
- DialogHeader,
49
- DialogTitle,
50
- Input,
51
- Label,
52
- Select,
53
- SelectContent,
54
- SelectItem,
55
- SelectLabel,
56
- SelectSeparator,
57
- SelectTrigger,
58
- SelectValue,
59
- Sheet,
60
- SheetContent,
61
- SheetDescription,
62
- SheetFooter,
63
- SheetHeader,
64
- SheetTitle,
65
- } from "../components/ui";
66
- import { AlertDialog } from "../components/ui/alert-dialog";
67
- import "../styles/media-registry.scss";
68
-
69
- const formatMediaDate = (date: string | number | Date | undefined | null) => {
70
- if (!date) return "-";
71
- const d = new Date(date);
72
- return Number.isNaN(d.getTime()) ? "-" : d.toLocaleDateString();
73
- };
74
-
75
- export interface MediaRegistryViewProps {
76
- collection: SerializableCollection;
77
- config: SerializableConfig;
78
- }
79
-
80
- // Utility for deterministic bucket colors
81
- const PRESET_COLORS = [
82
- "#7c3aed", // Purple
83
- "#047857", // Emerald
84
- "#b91c1c", // Red
85
- "#0369a1", // Sky
86
- "#a21caf", // Fuchsia
87
- "#c2410c", // Orange
88
- "#eab308", // Yellow
89
- "#10b981", // Green
90
- "#ec4899", // Pink
91
- "#6366f1", // Indigo
92
- ];
93
-
94
- const getBucketColor = (bucket: string, customColors: Record<string, string>) => {
95
- if (customColors && customColors[bucket]) return customColors[bucket];
96
-
97
- let hash = 0;
98
- for (let i = 0; i < bucket.length; i++) {
99
- hash = bucket.charCodeAt(i) + ((hash << 5) - hash);
100
- }
101
- return PRESET_COLORS[Math.abs(hash) % PRESET_COLORS.length];
102
- };
103
-
104
- export function MediaRegistryView({ collection, config }: MediaRegistryViewProps) {
105
- const { data, loading } = useStore($assets);
106
- const viewMode = useStore($mediaViewMode);
107
- const selectedBucket = useStore($mediaSelectedBucket);
108
- const currentFolder = useStore($mediaCurrentFolder);
109
- const search = useStore($mediaSearch);
110
- const page = useStore($mediaPage);
111
- const customColors = useStore($bucketColors);
112
-
113
- const [isUploading, setIsUploading] = useState(false);
114
- const [selectedAsset, setSelectedAsset] = useState<AssetDoc | null>(null);
115
- const [isPreviewOpen, setIsPreviewOpen] = useState(false);
116
- const [newFolderName, setNewFolderName] = useState("");
117
- const [isCreateFolderOpen, setIsCreateFolderOpen] = useState(false);
118
- const [isBucketSettingsOpen, setIsBucketSettingsOpen] = useState(false);
119
-
120
- const fileInputRef = useRef<HTMLInputElement>(null);
121
-
122
- const buckets = useMemo(() => {
123
- const storageKeys = Object.keys(config.storages || {});
124
- return [...storageKeys];
125
- }, [config.storages]);
126
-
127
- const fetchData = () => {
128
- // Now handled by $assets store automatically when dependencies change
129
- // but we can call it manually to refresh if needed (e.g. after upload/delete)
130
- // Actually, $assets is a fetcher store, so it has .revalidate()
131
- $assets.revalidate();
132
- };
133
-
134
- const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
135
- const file = e.target.files?.[0];
136
- if (!file) return;
137
-
138
- setIsUploading(true);
139
- const formData = new FormData();
140
- formData.append("file", file);
141
-
142
- try {
143
- const bucket = selectedBucket === "all" ? "default" : selectedBucket;
144
- await api.post(`api/__system/assets/upload?bucket=${bucket}&folder=${currentFolder}`, {
145
- body: formData,
146
- });
147
- notify("File uploaded successfully", "success");
148
- fetchData();
149
- } catch (err) {
150
- console.error("Upload failed", err);
151
- notify("Upload failed", "error");
152
- } finally {
153
- setIsUploading(false);
154
- if (fileInputRef.current) fileInputRef.current.value = "";
155
- }
156
- };
157
-
158
- const handleCreateFolder = async () => {
159
- if (!newFolderName) return;
160
- try {
161
- const bucket = selectedBucket === "all" ? "default" : selectedBucket;
162
- // We upload a .keep file to "create" the folder in typical object storage
163
- const blob = new Blob([""], { type: "text/plain" });
164
- const formData = new FormData();
165
- const folderPath = currentFolder ? `${currentFolder}/${newFolderName}` : newFolderName;
166
- formData.append("file", blob, ".keep");
167
-
168
- await api.post(`api/__system/assets/upload?bucket=${bucket}&folder=${folderPath}`, {
169
- body: formData,
170
- });
171
-
172
- notify("Folder created", "success");
173
- setNewFolderName("");
174
- setIsCreateFolderOpen(false);
175
- fetchData();
176
- } catch (err) {
177
- console.error("Failed to create folder", err);
178
- notify("Failed to create folder", "error");
179
- }
180
- };
181
-
182
- const handleUpdateMetadata = async () => {
183
- if (!selectedAsset) return;
184
- try {
185
- await api.patch(`api/${collection.slug}/${selectedAsset.id}`, {
186
- json: {
187
- filename: selectedAsset.filename,
188
- altText: selectedAsset.altText || selectedAsset.alt_text,
189
- caption: selectedAsset.caption,
190
- },
191
- });
192
- notify("Metadata updated", "success");
193
- fetchData();
194
- } catch (err) {
195
- console.error("Failed to update metadata", err);
196
- notify("Failed to update metadata", "error");
197
- }
198
- };
199
-
200
- const handleDelete = async (id: string) => {
201
- try {
202
- await api.delete(`api/${collection.slug}/${id}`);
203
- notify("Asset deleted", "success");
204
- if (selectedAsset?.id === id) setSelectedAsset(null);
205
- fetchData();
206
- } catch (err) {
207
- console.error("Delete failed", err);
208
- notify("Failed to delete asset", "error");
209
- }
210
- };
211
-
212
- const docs = data?.docs || [];
213
- const folders = data?.folders || [];
214
-
215
- const formatSize = (bytes: number) => {
216
- if (bytes === 0) return "0 Bytes";
217
- const k = 1024;
218
- const sizes = ["Bytes", "KB", "MB", "GB"];
219
- const i = Math.floor(Math.log(bytes) / Math.log(k));
220
- return parseFloat((bytes / k ** i).toFixed(1)) + " " + sizes[i];
221
- };
222
-
223
- const getFileIcon = (mime: string) => {
224
- if (!mime) return <File size={viewMode === "grid" ? 48 : 20} className="text-gray-400" />;
225
- if (mime.startsWith("image/"))
226
- return <ImageIcon size={viewMode === "grid" ? 48 : 20} className="text-blue-400" />;
227
- if (mime.startsWith("video/"))
228
- return <FileText size={viewMode === "grid" ? 48 : 20} className="text-purple-400" />;
229
- if (mime.includes("pdf"))
230
- return <FileText size={viewMode === "grid" ? 48 : 20} className="text-red-400" />;
231
- return <File size={viewMode === "grid" ? 48 : 20} className="text-gray-400" />;
232
- };
233
-
234
- const filteredDocs = docs.filter((doc) => {
235
- const originalFilename = doc.originalFilename || doc.original_filename;
236
- return (
237
- originalFilename?.toLowerCase().includes(search.toLowerCase()) ||
238
- doc.filename.toLowerCase().includes(search.toLowerCase())
239
- );
240
- });
241
-
242
- const filteredFolders = folders.filter((f) =>
243
- f.name.toLowerCase().includes(search.toLowerCase()),
244
- );
245
-
246
- return (
247
- <div className={`opaca-view-container media-registry-view ${loading ? "loading" : ""}`}>
248
- <input type="file" ref={fileInputRef} style={{ display: "none" }} onChange={handleUpload} />
249
- <div className="opaca-header media-registry-header">
250
- <div>
251
- <h1 className="opaca-title">Media Library</h1>
252
- <p className="opaca-subtitle">Manage your global asset registry across all buckets.</p>
253
- </div>
254
- <div className="media-registry-header-actions">
255
- <Button
256
- variant="outline"
257
- onClick={() => setIsCreateFolderOpen(true)}
258
- disabled={selectedBucket === "all"}
259
- title={selectedBucket === "all" ? "Select a specific bucket to create folders" : ""}
260
- >
261
- <FolderPlus size={16} className="media-registry-icon-mr" />
262
- New Folder
263
- </Button>
264
- <Button
265
- variant="default"
266
- disabled={isUploading || selectedBucket === "all"}
267
- onClick={() => fileInputRef.current?.click()}
268
- title={selectedBucket === "all" ? "Select a specific bucket to upload files" : ""}
269
- >
270
- {isUploading ? (
271
- <Loader2 size={16} className="opaca-spin media-registry-icon-mr" />
272
- ) : (
273
- <Plus size={16} className="media-registry-icon-mr" />
274
- )}
275
- {isUploading ? "Uploading..." : "Upload New"}
276
- </Button>
277
- </div>
278
- </div>
279
-
280
- <div
281
- style={{
282
- display: "flex",
283
- flex: 1,
284
- minHeight: 0,
285
- gap: "1.5rem",
286
- marginBottom: "1.5rem",
287
- }}
288
- >
289
- <div
290
- style={{
291
- flex: 1,
292
- display: "flex",
293
- flexDirection: "column",
294
- minWidth: 0,
295
- }}
296
- >
297
- {/* Breadcrumbs */}
298
- <div
299
- style={{
300
- display: "flex",
301
- alignItems: "center",
302
- gap: "0.5rem",
303
- marginBottom: "1rem",
304
- color: "var(--opaca-text-dim)",
305
- fontSize: "0.875rem",
306
- }}
307
- >
308
- <button
309
- type="button"
310
- onClick={() => setMediaFolder("")}
311
- style={{
312
- background: "none",
313
- border: "none",
314
- color: currentFolder === "" ? "var(--opaca-text)" : "inherit",
315
- cursor: "pointer",
316
- fontWeight: currentFolder === "" ? 600 : 400,
317
- }}
318
- >
319
- Home
320
- </button>
321
- {currentFolder
322
- .split("/")
323
- .filter(Boolean)
324
- .map((part, i, arr) => (
325
- <div
326
- key={part}
327
- style={{
328
- display: "flex",
329
- alignItems: "center",
330
- gap: "0.5rem",
331
- }}
332
- >
333
- <ChevronRight size={14} />
334
- <button
335
- type="button"
336
- onClick={() => setMediaFolder(arr.slice(0, i + 1).join("/"))}
337
- style={{
338
- background: "none",
339
- border: "none",
340
- color: i === arr.length - 1 ? "var(--opaca-text)" : "inherit",
341
- cursor: "pointer",
342
- fontWeight: i === arr.length - 1 ? 600 : 400,
343
- }}
344
- >
345
- {part}
346
- </button>
347
- </div>
348
- ))}
349
- </div>
350
-
351
- <div className="opaca-card media-registry-body">
352
- {/* Toolbar */}
353
- <div className="media-registry-toolbar">
354
- <div className="media-registry-filters">
355
- <div className="media-registry-search">
356
- <Search size={16} className="search-icon" />
357
- <Input
358
- type="text"
359
- placeholder="Search assets..."
360
- value={search}
361
- onChange={(e) => setMediaSearch(e.target.value)}
362
- />
363
- </div>
364
-
365
- <Select value={selectedBucket} onValueChange={(val: string) => setMediaBucket(val)}>
366
- <SelectTrigger className="media-registry-select-trigger">
367
- <div
368
- style={{
369
- display: "flex",
370
- alignItems: "center",
371
- gap: "8px",
372
- }}
373
- >
374
- {selectedBucket !== "all" && (
375
- <div
376
- style={{
377
- width: "8px",
378
- height: "8px",
379
- borderRadius: "50%",
380
- backgroundColor: getBucketColor(selectedBucket, customColors),
381
- }}
382
- />
383
- )}
384
- <SelectValue placeholder="All Buckets" />
385
- </div>
386
- </SelectTrigger>
387
- <SelectContent>
388
- <SelectItem value="all">
389
- <div
390
- style={{
391
- display: "flex",
392
- alignItems: "center",
393
- gap: "8px",
394
- }}
395
- >
396
- <div
397
- style={{
398
- width: "8px",
399
- height: "8px",
400
- borderRadius: "50%",
401
- border: "1px dashed var(--opaca-text-dim)",
402
- }}
403
- />
404
- All Buckets
405
- </div>
406
- </SelectItem>
407
- <SelectSeparator />
408
- <SelectLabel>Storage Buckets</SelectLabel>
409
- {buckets.map((b) => (
410
- <SelectItem key={b} value={b.toLowerCase()}>
411
- <div
412
- style={{
413
- display: "flex",
414
- alignItems: "center",
415
- gap: "8px",
416
- }}
417
- >
418
- <div
419
- style={{
420
- width: "8px",
421
- height: "8px",
422
- borderRadius: "50%",
423
- backgroundColor: getBucketColor(b, customColors),
424
- }}
425
- />
426
- {b.toUpperCase()}
427
- </div>
428
- </SelectItem>
429
- ))}
430
- </SelectContent>
431
- </Select>
432
- </div>
433
-
434
- <div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
435
- <Button
436
- variant="ghost"
437
- size="icon"
438
- onClick={() => setIsBucketSettingsOpen(true)}
439
- title="Bucket Settings"
440
- >
441
- <Settings2 size={16} />
442
- </Button>
443
-
444
- <div className="media-registry-view-toggles">
445
- <button
446
- type="button"
447
- onClick={() => $mediaViewMode.set("grid")}
448
- className={`media-registry-toggle-btn ${viewMode === "grid" ? "active" : ""}`}
449
- >
450
- <Grid size={16} />
451
- </button>
452
- <button
453
- type="button"
454
- onClick={() => $mediaViewMode.set("list")}
455
- className={`media-registry-toggle-btn ${viewMode === "list" ? "active" : ""}`}
456
- >
457
- <LayoutList size={16} />
458
- </button>
459
- </div>
460
- </div>
461
- </div>
462
-
463
- {loading && (
464
- <div
465
- style={{
466
- display: "flex",
467
- justifyContent: "center",
468
- padding: "4rem",
469
- }}
470
- >
471
- <Loader2
472
- size={32}
473
- className="opaca-spin"
474
- style={{ color: "var(--opaca-accent)" }}
475
- />
476
- </div>
477
- )}
478
-
479
- {!loading && filteredDocs.length === 0 && (
480
- <div
481
- style={{
482
- display: "flex",
483
- flexDirection: "column",
484
- alignItems: "center",
485
- justifyContent: "center",
486
- textAlign: "center",
487
- padding: "4rem",
488
- background: "rgba(255,255,255,0.01)",
489
- borderRadius: "12px",
490
- border: "1px dashed var(--opaca-border)",
491
- }}
492
- >
493
- <ImageIcon
494
- size={48}
495
- style={{
496
- color: "var(--opaca-text-dim)",
497
- marginBottom: "1rem",
498
- opacity: 0.3,
499
- }}
500
- />
501
- <h3 style={{ fontSize: "1.25rem", fontWeight: 600 }}>No assets found</h3>
502
- <p style={{ color: "var(--opaca-text-muted)" }}>
503
- Upload some files to get started.
504
- </p>
505
- </div>
506
- )}
507
-
508
- {/* Grid View */}
509
- {!loading &&
510
- viewMode === "grid" &&
511
- (filteredFolders.length > 0 || filteredDocs.length > 0) && (
512
- <div className="media-registry-grid">
513
- {/* Folders first */}
514
- {filteredFolders.map((folder) => (
515
- <div
516
- key={`folder-${folder.name}`}
517
- className="media-registry-card"
518
- onClick={() =>
519
- setMediaFolder(
520
- currentFolder ? `${currentFolder}/${folder.name}` : folder.name,
521
- )
522
- }
523
- >
524
- <div className="media-registry-card-thumb" style={{ position: "relative" }}>
525
- <FolderPlus size={54} className="folder-icon" />
526
- {selectedBucket === "all" && (
527
- <div
528
- style={{
529
- position: "absolute",
530
- bottom: "8px",
531
- right: "8px",
532
- display: "flex",
533
- gap: "2px",
534
- }}
535
- >
536
- {folder.buckets.map((b) => (
537
- <div
538
- key={b}
539
- title={b}
540
- style={{
541
- width: "8px",
542
- height: "8px",
543
- borderRadius: "50%",
544
- backgroundColor: getBucketColor(b, customColors),
545
- }}
546
- />
547
- ))}
548
- </div>
549
- )}
550
- </div>
551
- <div className="media-registry-card-body">
552
- <div className="media-registry-card-title">{folder.name}</div>
553
- <div className="media-registry-card-meta">Folder</div>
554
- </div>
555
- </div>
556
- ))}
557
-
558
- {/* Assets */}
559
- {filteredDocs.map((doc) => (
560
- <div
561
- key={doc.id}
562
- className={`media-registry-card ${selectedAsset?.id === doc.id ? "active" : ""}`}
563
- onClick={() => setSelectedAsset(doc)}
564
- >
565
- <div className="media-registry-card-thumb" style={{ position: "relative" }}>
566
- {(() => {
567
- const mime = doc.mimeType || doc.mime_type;
568
- return mime?.startsWith("image/") ? (
569
- <img
570
- src={`${config.serverURL || ""}/api/assets/${doc.id}/view`}
571
- alt={doc.originalFilename || doc.original_filename || ""}
572
- onError={(e) => (e.currentTarget.style.display = "none")}
573
- />
574
- ) : (
575
- getFileIcon(mime || "")
576
- );
577
- })()}
578
-
579
- {selectedBucket === "all" && (
580
- <div
581
- title={`Bucket: ${doc.bucket}`}
582
- style={{
583
- position: "absolute",
584
- top: "8px",
585
- left: "8px",
586
- width: "12px",
587
- height: "12px",
588
- borderRadius: "50%",
589
- backgroundColor: getBucketColor(doc.bucket, customColors),
590
- border: "2px solid rgba(0,0,0,0.5)",
591
- zIndex: 2,
592
- }}
593
- />
594
- )}
595
-
596
- <div className="media-registry-overlay">
597
- <button
598
- className="media-registry-overlay-btn"
599
- onClick={(e) => {
600
- e.stopPropagation();
601
- setSelectedAsset(doc);
602
- setIsPreviewOpen(true);
603
- }}
604
- >
605
- <Eye size={20} />
606
- </button>
607
- </div>
608
- </div>
609
- <div className="media-registry-card-body">
610
- <div className="media-registry-card-title" title={doc.original_filename}>
611
- {doc.original_filename}
612
- </div>
613
- <div className="media-registry-card-meta">
614
- <span className="meta-type">
615
- {(doc.mimeType || doc.mime_type)?.split("/")[1] || "FILE"}
616
- </span>
617
- <span>{formatSize(doc.filesize)}</span>
618
- </div>
619
- </div>
620
- </div>
621
- ))}
622
- </div>
623
- )}
624
-
625
- {/* List View */}
626
- {!loading &&
627
- viewMode === "list" &&
628
- (filteredFolders.length > 0 || filteredDocs.length > 0) && (
629
- <div className="opaca-table-container">
630
- <table className="opaca-table">
631
- <thead>
632
- <tr>
633
- <th style={{ width: "40px" }}></th>
634
- <th>Name</th>
635
- <th>Type</th>
636
- <th>Size</th>
637
- <th>Date</th>
638
- <th style={{ width: "40px" }}></th>
639
- </tr>
640
- </thead>
641
- <tbody>
642
- {/* Folders first */}
643
- {filteredFolders.map((folder) => (
644
- <tr
645
- key={`folder-${folder.name}`}
646
- onClick={() =>
647
- setMediaFolder(
648
- currentFolder ? `${currentFolder}/${folder.name}` : folder.name,
649
- )
650
- }
651
- style={{ cursor: "pointer" }}
652
- >
653
- <td>
654
- <div
655
- style={{
656
- position: "relative",
657
- display: "inline-block",
658
- }}
659
- >
660
- <FolderPlus size={20} style={{ color: "#eab308" }} />
661
- {selectedBucket === "all" && (
662
- <div
663
- style={{
664
- position: "absolute",
665
- bottom: "-2px",
666
- right: "-2px",
667
- display: "flex",
668
- gap: "1px",
669
- }}
670
- >
671
- {folder.buckets.map((b) => (
672
- <div
673
- key={b}
674
- style={{
675
- width: "6px",
676
- height: "6px",
677
- borderRadius: "50%",
678
- backgroundColor: getBucketColor(b, customColors),
679
- }}
680
- />
681
- ))}
682
- </div>
683
- )}
684
- </div>
685
- </td>
686
- <td style={{ fontWeight: 600 }}>{folder.name}</td>
687
- <td>Folder</td>
688
- <td>-</td>
689
- <td>-</td>
690
- <td></td>
691
- </tr>
692
- ))}
693
-
694
- {filteredDocs.map((doc) => (
695
- <tr
696
- key={doc.id}
697
- onClick={() => setSelectedAsset(doc)}
698
- style={{
699
- cursor: "pointer",
700
- background:
701
- selectedAsset?.id === doc.id
702
- ? "rgba(var(--opaca-accent-rgb), 0.1)"
703
- : "inherit",
704
- }}
705
- >
706
- <td>
707
- <div
708
- style={{
709
- width: "32px",
710
- height: "32px",
711
- borderRadius: "4px",
712
- background: "var(--opaca-bg-alt)",
713
- display: "flex",
714
- alignItems: "center",
715
- justifyContent: "center",
716
- overflow: "hidden",
717
- }}
718
- >
719
- {(() => {
720
- const mime = doc.mimeType || doc.mime_type || "";
721
- return mime.startsWith("image/") ? (
722
- <img
723
- src={`${config.serverURL || ""}/api/assets/${doc.id}/view`}
724
- style={{
725
- width: "100%",
726
- height: "100%",
727
- objectFit: "cover",
728
- }}
729
- alt=""
730
- />
731
- ) : (
732
- getFileIcon(mime)
733
- );
734
- })()}
735
- </div>
736
- </td>
737
- <td>
738
- <div style={{ fontWeight: 500 }}>{doc.original_filename}</div>
739
- <div
740
- style={{
741
- fontSize: "0.75rem",
742
- color: "var(--opaca-text-dim)",
743
- }}
744
- >
745
- {doc.filename}
746
- </div>
747
- </td>
748
- <td style={{ fontSize: "0.875rem" }}>
749
- {doc.mimeType || doc.mime_type || ""}
750
- </td>
751
- <td style={{ fontSize: "0.875rem" }}>{formatSize(doc.filesize)}</td>
752
- <td
753
- style={{
754
- fontSize: "0.75rem",
755
- color: "var(--opaca-text-muted)",
756
- }}
757
- >
758
- {formatMediaDate(doc.createdAt || doc.created_at)}
759
- </td>
760
- <td>
761
- <button
762
- onClick={(e) => {
763
- e.stopPropagation();
764
- setSelectedAsset(doc);
765
- setIsPreviewOpen(true);
766
- }}
767
- style={{
768
- background: "none",
769
- border: "none",
770
- color: "var(--opaca-text-dim)",
771
- cursor: "pointer",
772
- }}
773
- >
774
- <Eye size={18} />
775
- </button>
776
- </td>
777
- </tr>
778
- ))}
779
- </tbody>
780
- </table>
781
- </div>
782
- )}
783
-
784
- {/* Pagination */}
785
- {data && data.totalPages > 1 && (
786
- <div className="media-registry-pagination">
787
- <div className="info">
788
- Showing {(data.page - 1) * data.limit + 1} to{" "}
789
- {Math.min(data.page * data.limit, data.totalDocs)} of {data.totalDocs}
790
- </div>
791
- <div className="actions">
792
- <Button
793
- variant="outline"
794
- size="sm"
795
- disabled={data.page === 1 || loading}
796
- onClick={() => setMediaPage(page - 1)}
797
- >
798
- <ChevronLeft size={16} />
799
- </Button>
800
- <Button
801
- variant="outline"
802
- size="sm"
803
- disabled={data.page === data.totalPages || loading}
804
- onClick={() => setMediaPage(page + 1)}
805
- >
806
- <ChevronRight size={16} />
807
- </Button>
808
- </div>
809
- </div>
810
- )}
811
- </div>
812
- </div>
813
- </div>
814
-
815
- {/* Asset Details Sheet */}
816
- <Sheet
817
- open={!!selectedAsset && !isPreviewOpen}
818
- onOpenChange={(open) => !open && setSelectedAsset(null)}
819
- >
820
- <SheetContent onClose={() => setSelectedAsset(null)}>
821
- <SheetHeader>
822
- <SheetTitle>Asset Details</SheetTitle>
823
- <SheetDescription>View and manage asset metadata.</SheetDescription>
824
- </SheetHeader>
825
-
826
- {selectedAsset && (
827
- <div className="media-sheet-flex">
828
- <div className="media-sheet-body opaca-scrollbar-hidden">
829
- <div className="media-sheet-preview">
830
- {(() => {
831
- const mime = selectedAsset.mimeType || selectedAsset.mime_type || "";
832
- return mime.startsWith("image/") ? (
833
- <img
834
- src={`${config.serverURL || ""}/api/assets/${selectedAsset.id}/view`}
835
- alt={selectedAsset.filename}
836
- />
837
- ) : (
838
- getFileIcon(mime)
839
- );
840
- })()}
841
- <div className="overlay">
842
- <Button variant="secondary" onClick={() => setIsPreviewOpen(true)}>
843
- <Eye size={16} className="media-registry-icon-mr" />
844
- Preview Full
845
- </Button>
846
- </div>
847
- </div>
848
-
849
- <div className="media-sheet-form">
850
- <div className="media-sheet-group">
851
- <Label htmlFor="filename">Display Name</Label>
852
- <Input
853
- id="filename"
854
- value={selectedAsset.filename}
855
- onChange={(e) =>
856
- setSelectedAsset({
857
- ...selectedAsset,
858
- filename: e.target.value,
859
- })
860
- }
861
- />
862
- </div>
863
- <div className="media-sheet-group">
864
- <Label htmlFor="alt_text">Alt Text</Label>
865
- <Input
866
- id="alt_text"
867
- value={selectedAsset.alt_text || ""}
868
- onChange={(e) =>
869
- setSelectedAsset({
870
- ...selectedAsset,
871
- alt_text: e.target.value,
872
- })
873
- }
874
- placeholder="SEO friendly description"
875
- />
876
- </div>
877
- <div className="media-sheet-group">
878
- <Label htmlFor="caption">Caption</Label>
879
- <textarea
880
- id="caption"
881
- value={selectedAsset.caption || ""}
882
- onChange={(e) =>
883
- setSelectedAsset({
884
- ...selectedAsset,
885
- caption: e.target.value,
886
- })
887
- }
888
- className="media-sheet-textarea"
889
- placeholder="Write a short caption..."
890
- />
891
- </div>
892
-
893
- <div className="media-sheet-meta-box">
894
- <div className="media-sheet-meta-row">
895
- <span className="label">File Size</span>
896
- <span className="value">{formatSize(selectedAsset.filesize)}</span>
897
- </div>
898
- <div className="media-sheet-meta-row">
899
- <span className="label">Internal Key</span>
900
- <span className="value value-mono" title={selectedAsset.key}>
901
- {selectedAsset.key}
902
- </span>
903
- </div>
904
- <div className="media-sheet-meta-row">
905
- <span className="label">Bucket</span>
906
- <span className="value">{selectedAsset.bucket}</span>
907
- </div>
908
- </div>
909
- </div>
910
- </div>
911
- </div>
912
- )}
913
-
914
- {selectedAsset && (
915
- <SheetFooter className="media-sheet-actions">
916
- <AlertDialog
917
- title="Delete Asset"
918
- description="Are you sure you want to delete this asset? This action cannot be undone and may break links in your content."
919
- onConfirm={() => handleDelete(selectedAsset.id)}
920
- variant="destructive"
921
- confirmText="Delete Asset"
922
- >
923
- <Button variant="destructive">
924
- <Trash2 size={16} className="media-registry-icon-mr" />
925
- Delete
926
- </Button>
927
- </AlertDialog>
928
- <Button variant="default" onClick={handleUpdateMetadata}>
929
- <Save size={16} className="media-registry-icon-mr" />
930
- Save Changes
931
- </Button>
932
- </SheetFooter>
933
- )}
934
- </SheetContent>
935
- </Sheet>
936
-
937
- {/* CREATE FOLDER DIALOG */}
938
- <Dialog open={isCreateFolderOpen} onOpenChange={setIsCreateFolderOpen}>
939
- <DialogContent className="opaca-dialog-max-w">
940
- <DialogHeader>
941
- <DialogTitle>Create New Folder</DialogTitle>
942
- <DialogDescription>Organize your assets into folders.</DialogDescription>
943
- </DialogHeader>
944
- <div className="media-dialog-create-body">
945
- <Label htmlFor="newFolderName">Folder Name</Label>
946
- <Input
947
- id="newFolderName"
948
- placeholder="e.g. products"
949
- value={newFolderName}
950
- onChange={(e) => setNewFolderName(e.target.value)}
951
- style={{ marginTop: "0.5rem" }}
952
- onKeyDown={(e) => e.key === "Enter" && handleCreateFolder()}
953
- />
954
- </div>
955
- <DialogFooter>
956
- <Button variant="outline" onClick={() => setIsCreateFolderOpen(false)}>
957
- Cancel
958
- </Button>
959
- <Button onClick={handleCreateFolder}>Create</Button>
960
- </DialogFooter>
961
- </DialogContent>
962
- </Dialog>
963
-
964
- {/* ASSET PREVIEW FULL SCREEN PORTAL */}
965
- {isPreviewOpen &&
966
- selectedAsset &&
967
- typeof document !== "undefined" &&
968
- createPortal(
969
- <div className="opaca-ui-dialog-portal">
970
- <div
971
- className="opaca-ui-dialog-overlay"
972
- onClick={() => {
973
- setIsPreviewOpen(false);
974
- setSelectedAsset(null);
975
- }}
976
- />
977
- <div className="opaca-ui-dialog-wrapper">
978
- <div className="media-preview-container">
979
- <div className="media-preview-header">
980
- <div className="media-preview-title-group">
981
- <div
982
- style={{
983
- display: "flex",
984
- alignItems: "center",
985
- gap: "0.75rem",
986
- }}
987
- >
988
- {selectedBucket === "all" && (
989
- <div
990
- title={`Bucket: ${selectedAsset.bucket}`}
991
- style={{
992
- width: "12px",
993
- height: "12px",
994
- borderRadius: "50%",
995
- backgroundColor: getBucketColor(selectedAsset.bucket, customColors),
996
- }}
997
- />
998
- )}
999
- <h2>{selectedAsset.original_filename}</h2>
1000
- </div>
1001
- <span className="badge">
1002
- {selectedAsset.mimeType || selectedAsset.mime_type || ""}
1003
- </span>
1004
- </div>
1005
- <button
1006
- className="media-preview-close-btn"
1007
- onClick={() => {
1008
- setIsPreviewOpen(false);
1009
- setSelectedAsset(null);
1010
- }}
1011
- >
1012
- <X size={24} />
1013
- </button>
1014
- </div>
1015
-
1016
- <div className="media-preview-body">
1017
- {(() => {
1018
- const mime = selectedAsset.mimeType || selectedAsset.mime_type || "";
1019
- return mime.startsWith("image/") ? (
1020
- <img
1021
- src={`${config.serverURL || ""}/api/assets/${selectedAsset.id}/view`}
1022
- alt={
1023
- selectedAsset.altText ||
1024
- selectedAsset.alt_text ||
1025
- selectedAsset.originalFilename ||
1026
- selectedAsset.original_filename ||
1027
- ""
1028
- }
1029
- />
1030
- ) : (
1031
- <div className="media-preview-no-rich">
1032
- <div className="icon-wrapper">{getFileIcon(mime)}</div>
1033
- <p>No rich preview available for this file type.</p>
1034
- <Button
1035
- variant="default"
1036
- onClick={() =>
1037
- (window.location.href = `${config.serverURL || ""}/api/assets/${selectedAsset.id}/view`)
1038
- }
1039
- >
1040
- <Download size={16} className="media-registry-icon-mr" />
1041
- Download Original
1042
- </Button>
1043
- </div>
1044
- );
1045
- })()}
1046
- </div>
1047
-
1048
- <div className="media-preview-footer">
1049
- <div className="media-preview-stat">
1050
- <div className="label">Size</div>
1051
- <div className="value">{formatSize(selectedAsset.filesize)}</div>
1052
- </div>
1053
- <div className="media-preview-stat">
1054
- <div className="label">Created</div>
1055
- <div className="value">
1056
- {formatMediaDate(selectedAsset.createdAt || selectedAsset.created_at)}
1057
- </div>
1058
- </div>
1059
- <div className="media-preview-stat">
1060
- <div className="label">Bucket</div>
1061
- <div className="value">{selectedAsset.bucket}</div>
1062
- </div>
1063
- </div>
1064
- </div>
1065
- </div>
1066
- </div>,
1067
- document.body,
1068
- )}
1069
- {/* Bucket Color Settings Dialog */}
1070
- <Dialog open={isBucketSettingsOpen} onOpenChange={setIsBucketSettingsOpen}>
1071
- <DialogContent className="media-registry-dialog-sm">
1072
- <DialogHeader>
1073
- <DialogTitle>Bucket Colors</DialogTitle>
1074
- </DialogHeader>
1075
- <div className="media-bucket-settings">
1076
- {buckets.map((bucketName) => (
1077
- <div key={bucketName} className="bucket-setting-row">
1078
- <span className="bucket-name">{bucketName}</span>
1079
- <div className="color-presets">
1080
- {PRESET_COLORS.map((color) => {
1081
- const isActive = getBucketColor(bucketName, customColors) === color;
1082
- return (
1083
- <button
1084
- key={color}
1085
- className={`color-bubble ${isActive ? "active" : ""}`}
1086
- style={{ backgroundColor: color }}
1087
- onClick={() => setBucketColor(bucketName, color)}
1088
- >
1089
- {isActive && <Check size={12} color="white" />}
1090
- </button>
1091
- );
1092
- })}
1093
- </div>
1094
- </div>
1095
- ))}
1096
- </div>
1097
- <DialogFooter>
1098
- <Button onClick={() => setIsBucketSettingsOpen(false)}>Close</Button>
1099
- </DialogFooter>
1100
- </DialogContent>
1101
- </Dialog>
1102
- </div>
1103
- );
1104
- }