@lodashventure/medusa-media-manager 0.1.1

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 (25) hide show
  1. package/.medusa/server/src/admin/index.js +1376 -0
  2. package/.medusa/server/src/admin/index.mjs +1377 -0
  3. package/.medusa/server/src/api/admin/media/assets/[id]/route.js +50 -0
  4. package/.medusa/server/src/api/admin/media/assets/[id]/url/route.js +24 -0
  5. package/.medusa/server/src/api/admin/media/assets/route.js +53 -0
  6. package/.medusa/server/src/api/admin/media/assets/upload/route.js +67 -0
  7. package/.medusa/server/src/api/middlewares.js +53 -0
  8. package/.medusa/server/src/index.js +46 -0
  9. package/.medusa/server/src/modules/media-manager/index.js +13 -0
  10. package/.medusa/server/src/modules/media-manager/migrations/Migration20251104115419.js +83 -0
  11. package/.medusa/server/src/modules/media-manager/migrations/Migration20251104160000.js +15 -0
  12. package/.medusa/server/src/modules/media-manager/models/index.js +14 -0
  13. package/.medusa/server/src/modules/media-manager/models/media-models.js +201 -0
  14. package/.medusa/server/src/modules/media-manager/service.js +778 -0
  15. package/.medusa/server/src/providers/index.js +23 -0
  16. package/.medusa/server/src/providers/storage/azure.js +100 -0
  17. package/.medusa/server/src/providers/storage/factory.js +32 -0
  18. package/.medusa/server/src/providers/storage/gcs.js +91 -0
  19. package/.medusa/server/src/providers/storage/local.js +73 -0
  20. package/.medusa/server/src/providers/storage/s3.js +96 -0
  21. package/.medusa/server/src/providers/storage/types.js +3 -0
  22. package/.medusa/server/src/types/index.js +3 -0
  23. package/.medusa/server/src/workflows/index.js +4 -0
  24. package/README.md +85 -0
  25. package/package.json +82 -0
@@ -0,0 +1,1376 @@
1
+ "use strict";
2
+ const jsxRuntime = require("react/jsx-runtime");
3
+ const react = require("react");
4
+ const adminSdk = require("@medusajs/admin-sdk");
5
+ const icons = require("@medusajs/icons");
6
+ const ui = require("@medusajs/ui");
7
+ require("@medusajs/admin-shared");
8
+ const ALL_OPTION_VALUE = "all";
9
+ const normalizeSelectValue = (value) => {
10
+ if (value === "" || value === void 0) {
11
+ return ALL_OPTION_VALUE;
12
+ }
13
+ return value;
14
+ };
15
+ const resolveSelection = (value) => {
16
+ if (value === ALL_OPTION_VALUE || value === "") {
17
+ return void 0;
18
+ }
19
+ return value;
20
+ };
21
+ const typeOptions = [
22
+ { label: "All types", value: ALL_OPTION_VALUE },
23
+ { label: "Images", value: "image" },
24
+ { label: "SVG", value: "svg" },
25
+ { label: "Videos", value: "video" },
26
+ { label: "Documents", value: "document" }
27
+ ];
28
+ const statusOptions = [
29
+ { label: "All statuses", value: ALL_OPTION_VALUE },
30
+ { label: "Draft", value: "draft" },
31
+ { label: "Published", value: "published" },
32
+ { label: "Archived", value: "archived" }
33
+ ];
34
+ const visibilityOptions = [
35
+ { label: "All visibility", value: ALL_OPTION_VALUE },
36
+ { label: "Public", value: "public" },
37
+ { label: "Private", value: "private" }
38
+ ];
39
+ const FiltersBar = ({
40
+ search,
41
+ onSearchChange,
42
+ type,
43
+ onTypeChange,
44
+ status,
45
+ onStatusChange,
46
+ visibility,
47
+ onVisibilityChange,
48
+ tags,
49
+ onTagsChange,
50
+ viewMode,
51
+ onViewModeChange,
52
+ onOpenUpload,
53
+ onRefresh,
54
+ totalCount
55
+ }) => {
56
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-4", children: [
57
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-3 lg:flex-row lg:items-center lg:justify-between lg:gap-y-0 lg:gap-x-4", children: [
58
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-1 flex-col gap-y-3 sm:flex-row sm:items-center sm:gap-x-3", children: [
59
+ /* @__PURE__ */ jsxRuntime.jsx(
60
+ ui.Input,
61
+ {
62
+ type: "search",
63
+ placeholder: "Search filename, tags, or captions...",
64
+ value: search,
65
+ onChange: (e) => onSearchChange(e.target.value),
66
+ className: "w-full sm:max-w-xs"
67
+ }
68
+ ),
69
+ /* @__PURE__ */ jsxRuntime.jsxs(
70
+ ui.Select,
71
+ {
72
+ value: normalizeSelectValue(type),
73
+ onValueChange: (value) => onTypeChange(resolveSelection(value)),
74
+ children: [
75
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Trigger, { className: "w-full sm:w-[160px]", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Value, { placeholder: "Type" }) }),
76
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Content, { children: typeOptions.map((option) => /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: option.value, children: option.label }, option.value)) })
77
+ ]
78
+ }
79
+ ),
80
+ /* @__PURE__ */ jsxRuntime.jsxs(
81
+ ui.Select,
82
+ {
83
+ value: normalizeSelectValue(status),
84
+ onValueChange: (value) => onStatusChange(resolveSelection(value)),
85
+ children: [
86
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Trigger, { className: "w-full sm:w-[160px]", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Value, { placeholder: "Status" }) }),
87
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Content, { children: statusOptions.map((option) => /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: option.value, children: option.label }, option.value)) })
88
+ ]
89
+ }
90
+ ),
91
+ /* @__PURE__ */ jsxRuntime.jsxs(
92
+ ui.Select,
93
+ {
94
+ value: normalizeSelectValue(visibility),
95
+ onValueChange: (value) => onVisibilityChange(resolveSelection(value)),
96
+ children: [
97
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Trigger, { className: "w-full sm:w-[160px]", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Value, { placeholder: "Visibility" }) }),
98
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Content, { children: visibilityOptions.map((option) => /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: option.value, children: option.label }, option.value)) })
99
+ ]
100
+ }
101
+ )
102
+ ] }),
103
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-x-3 sm:justify-end", children: [
104
+ /* @__PURE__ */ jsxRuntime.jsx(
105
+ ui.Input,
106
+ {
107
+ placeholder: "Tags (comma separated)",
108
+ value: tags,
109
+ onChange: (e) => onTagsChange(e.target.value),
110
+ className: "hidden md:block md:max-w-sm"
111
+ }
112
+ ),
113
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.TooltipProvider, { children: [
114
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Tooltip, { content: "Grid view", children: /* @__PURE__ */ jsxRuntime.jsx(
115
+ ui.IconButton,
116
+ {
117
+ variant: "secondary",
118
+ className: ui.clx(viewMode === "grid" && "bg-ui-bg-subtle"),
119
+ onClick: () => onViewModeChange("grid"),
120
+ children: /* @__PURE__ */ jsxRuntime.jsx(icons.SquaresPlus, {})
121
+ }
122
+ ) }),
123
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Tooltip, { content: "List view", children: /* @__PURE__ */ jsxRuntime.jsx(
124
+ ui.IconButton,
125
+ {
126
+ variant: "secondary",
127
+ className: ui.clx(viewMode === "table" && "bg-ui-bg-subtle"),
128
+ onClick: () => onViewModeChange("table"),
129
+ children: /* @__PURE__ */ jsxRuntime.jsx(icons.ListBullet, {})
130
+ }
131
+ ) }),
132
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Tooltip, { content: "Refresh", children: /* @__PURE__ */ jsxRuntime.jsx(ui.IconButton, { variant: "secondary", onClick: onRefresh, children: /* @__PURE__ */ jsxRuntime.jsx(icons.ArrowPath, {}) }) })
133
+ ] }),
134
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { onClick: onOpenUpload, className: "whitespace-nowrap", children: [
135
+ /* @__PURE__ */ jsxRuntime.jsx(icons.CloudArrowUp, { className: "mr-1 size-4" }),
136
+ "Upload"
137
+ ] })
138
+ ] })
139
+ ] }),
140
+ /* @__PURE__ */ jsxRuntime.jsx(
141
+ ui.Input,
142
+ {
143
+ placeholder: "Tags (comma separated)",
144
+ value: tags,
145
+ onChange: (e) => onTagsChange(e.target.value),
146
+ className: "md:hidden"
147
+ }
148
+ ),
149
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Badge, { size: "small", variant: "neutral", children: [
150
+ totalCount,
151
+ " assets"
152
+ ] })
153
+ ] });
154
+ };
155
+ function formatFileSize(bytes) {
156
+ if (!bytes || bytes <= 0) {
157
+ return "0 B";
158
+ }
159
+ const units = ["B", "KB", "MB", "GB", "TB"];
160
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
161
+ const size = bytes / Math.pow(1024, i);
162
+ return `${size.toFixed(size < 10 && i > 0 ? 1 : 0)} ${units[i]}`;
163
+ }
164
+ function formatDateTime(value) {
165
+ if (!value) {
166
+ return "-";
167
+ }
168
+ try {
169
+ return new Date(value).toLocaleString();
170
+ } catch {
171
+ return value;
172
+ }
173
+ }
174
+ function parseTags(value) {
175
+ return value.split(",").map((tag) => tag.trim()).filter(Boolean);
176
+ }
177
+ function getLocaleText(record, fallbackLocale = "default") {
178
+ if (!record) {
179
+ return "";
180
+ }
181
+ if (record[fallbackLocale]) {
182
+ return record[fallbackLocale] ?? "";
183
+ }
184
+ const first = Object.values(record).find((value) => value);
185
+ return first ?? "";
186
+ }
187
+ function setLocaleText(value, locale = "default") {
188
+ const trimmed = value.trim();
189
+ if (!trimmed) {
190
+ return null;
191
+ }
192
+ return { [locale]: trimmed };
193
+ }
194
+ const typeIconMap = {
195
+ image: /* @__PURE__ */ jsxRuntime.jsx(icons.Photo, { className: "size-4" }),
196
+ svg: /* @__PURE__ */ jsxRuntime.jsx(icons.Photo, { className: "size-4" }),
197
+ document: /* @__PURE__ */ jsxRuntime.jsx(icons.DocumentText, { className: "size-4" }),
198
+ video: /* @__PURE__ */ jsxRuntime.jsx(icons.PlaySolid, { className: "size-4" })
199
+ };
200
+ const statusColorMap = {
201
+ published: "success",
202
+ draft: "default",
203
+ archived: "warning"
204
+ };
205
+ const MediaGrid = ({
206
+ assets,
207
+ previews,
208
+ viewMode,
209
+ isLoading,
210
+ onSelectAsset,
211
+ selectedAssetId
212
+ }) => {
213
+ if (isLoading) {
214
+ return /* @__PURE__ */ jsxRuntime.jsx(
215
+ "div",
216
+ {
217
+ className: ui.clx(
218
+ "grid gap-4",
219
+ viewMode === "grid" ? "grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4" : "grid-cols-1"
220
+ ),
221
+ children: Array.from({ length: viewMode === "grid" ? 8 : 4 }).map((_, index) => /* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-48 rounded-md" }, index))
222
+ }
223
+ );
224
+ }
225
+ if (!assets.length) {
226
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex h-64 flex-col items-center justify-center gap-y-2 rounded-md border border-dashed border-ui-border-base bg-ui-bg-subtle", children: [
227
+ /* @__PURE__ */ jsxRuntime.jsx(icons.Photo, { className: "size-6 text-ui-fg-subtle" }),
228
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-ui-fg-subtle", children: "No media assets found. Try adjusting filters or upload new assets." })
229
+ ] });
230
+ }
231
+ if (viewMode === "table") {
232
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Table, { children: [
233
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
234
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Preview" }),
235
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Name" }),
236
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Type" }),
237
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Size" }),
238
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Status" }),
239
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Visibility" }),
240
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Updated" })
241
+ ] }) }),
242
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Body, { children: assets.map((asset) => /* @__PURE__ */ jsxRuntime.jsxs(
243
+ ui.Table.Row,
244
+ {
245
+ className: "cursor-pointer",
246
+ onClick: () => onSelectAsset(asset),
247
+ children: [
248
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-x-3", children: /* @__PURE__ */ jsxRuntime.jsx(AssetPreview, { asset, previewUrl: previews[asset.id] }) }) }),
249
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "max-w-xs truncate", children: asset.original_filename }),
250
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "capitalize", children: asset.type }),
251
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: formatFileSize(asset.size_bytes) }),
252
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(
253
+ ui.StatusBadge,
254
+ {
255
+ color: statusColorMap[asset.status] ?? "default",
256
+ size: "small",
257
+ children: asset.status
258
+ }
259
+ ) }),
260
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(
261
+ ui.Badge,
262
+ {
263
+ size: "small",
264
+ variant: asset.visibility === "public" ? "neutral" : "warning",
265
+ className: "capitalize",
266
+ children: asset.visibility
267
+ }
268
+ ) }),
269
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: formatDateTime(asset.updated_at) })
270
+ ]
271
+ },
272
+ asset.id
273
+ )) })
274
+ ] });
275
+ }
276
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4", children: assets.map((asset) => {
277
+ var _a;
278
+ return /* @__PURE__ */ jsxRuntime.jsxs(
279
+ "button",
280
+ {
281
+ type: "button",
282
+ onClick: () => onSelectAsset(asset),
283
+ className: ui.clx(
284
+ "flex h-full flex-col overflow-hidden rounded-lg border border-ui-border-base bg-ui-bg-base text-left shadow-sm transition hover:border-ui-border-strong",
285
+ selectedAssetId === asset.id && "border-ui-fg-interactive"
286
+ ),
287
+ children: [
288
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative aspect-video w-full overflow-hidden bg-ui-bg-subtle", children: [
289
+ /* @__PURE__ */ jsxRuntime.jsx(AssetPreview, { asset, previewUrl: previews[asset.id] }),
290
+ asset.visibility === "private" && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute right-2 top-2 rounded-full bg-ui-bg-base/80 p-1", children: /* @__PURE__ */ jsxRuntime.jsx(icons.ShieldCheck, { className: "size-4 text-ui-fg-muted" }) })
291
+ ] }),
292
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-1 flex-col gap-y-2 p-4", children: [
293
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-2", children: [
294
+ /* @__PURE__ */ jsxRuntime.jsx(ui.TooltipProvider, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Tooltip, { content: asset.mime, children: /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-1 text-xs text-ui-fg-subtle", children: [
295
+ typeIconMap[asset.type] ?? /* @__PURE__ */ jsxRuntime.jsx(icons.DocumentText, { className: "size-4" }),
296
+ asset.mime
297
+ ] }) }) }),
298
+ /* @__PURE__ */ jsxRuntime.jsx(
299
+ ui.StatusBadge,
300
+ {
301
+ size: "small",
302
+ color: statusColorMap[asset.status] ?? "default",
303
+ children: asset.status
304
+ }
305
+ )
306
+ ] }),
307
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
308
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "line-clamp-1 text-sm font-medium", children: asset.original_filename }),
309
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-xs text-ui-fg-subtle", children: [
310
+ formatFileSize(asset.size_bytes),
311
+ " ·",
312
+ " ",
313
+ formatDateTime(asset.updated_at)
314
+ ] })
315
+ ] }),
316
+ ((_a = asset.tag_links) == null ? void 0 : _a.length) ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap gap-1.5", children: [
317
+ asset.tag_links.slice(0, 4).map((link) => /* @__PURE__ */ jsxRuntime.jsxs(ui.Badge, { size: "small", variant: "neutral", children: [
318
+ "#",
319
+ link.tag.name
320
+ ] }, link.id)),
321
+ asset.tag_links.length > 4 && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs text-ui-fg-muted", children: [
322
+ "+",
323
+ asset.tag_links.length - 4
324
+ ] })
325
+ ] }) : null
326
+ ] })
327
+ ]
328
+ },
329
+ asset.id
330
+ );
331
+ }) });
332
+ };
333
+ const AssetPreview = ({
334
+ asset,
335
+ previewUrl
336
+ }) => {
337
+ if (previewUrl && asset.type !== "document") {
338
+ return (
339
+ // eslint-disable-next-line @next/next/no-img-element
340
+ /* @__PURE__ */ jsxRuntime.jsx(
341
+ "img",
342
+ {
343
+ src: previewUrl,
344
+ alt: asset.original_filename,
345
+ className: "h-full w-full object-cover"
346
+ }
347
+ )
348
+ );
349
+ }
350
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-full w-full items-center justify-center bg-ui-bg-subtle text-ui-fg-muted", children: typeIconMap[asset.type] ?? /* @__PURE__ */ jsxRuntime.jsx(icons.DocumentText, { className: "size-5" }) });
351
+ };
352
+ const API_BASE = "/admin/media/assets";
353
+ function buildQuery(params) {
354
+ const search = new URLSearchParams();
355
+ Object.entries(params).forEach(([key, value]) => {
356
+ if (value === void 0 || value === null || value === "") {
357
+ return;
358
+ }
359
+ if (Array.isArray(value)) {
360
+ if (value.length) {
361
+ search.set(key, value.join(","));
362
+ }
363
+ return;
364
+ }
365
+ search.set(key, String(value));
366
+ });
367
+ const query = search.toString();
368
+ return query.length ? `?${query}` : "";
369
+ }
370
+ async function parseJson(res) {
371
+ if (!res.ok) {
372
+ let message = "Unexpected server error";
373
+ try {
374
+ const body = await res.json();
375
+ if (body == null ? void 0 : body.message) {
376
+ message = body.message;
377
+ }
378
+ } catch {
379
+ message = res.statusText || message;
380
+ }
381
+ throw new Error(message);
382
+ }
383
+ return res.json();
384
+ }
385
+ async function listMediaAssets(params) {
386
+ const query = buildQuery(params);
387
+ const res = await fetch(`${API_BASE}${query}`, {
388
+ credentials: "include"
389
+ });
390
+ return parseJson(res);
391
+ }
392
+ async function uploadMediaAssets(input) {
393
+ var _a;
394
+ const formData = new FormData();
395
+ input.files.forEach((file) => {
396
+ formData.append("files", file);
397
+ });
398
+ if (input.folderId) {
399
+ formData.append("folderId", input.folderId);
400
+ }
401
+ if (input.visibility) {
402
+ formData.append("visibility", input.visibility);
403
+ }
404
+ if (input.status) {
405
+ formData.append("status", input.status);
406
+ }
407
+ if ((_a = input.tags) == null ? void 0 : _a.length) {
408
+ formData.append("tags", JSON.stringify(input.tags));
409
+ }
410
+ const res = await fetch(`${API_BASE}/upload`, {
411
+ method: "POST",
412
+ body: formData,
413
+ credentials: "include"
414
+ });
415
+ return parseJson(res);
416
+ }
417
+ async function getMediaAsset(id) {
418
+ const res = await fetch(`${API_BASE}/${id}`, {
419
+ credentials: "include"
420
+ });
421
+ return parseJson(res);
422
+ }
423
+ async function updateMediaAsset(id, payload) {
424
+ const res = await fetch(`${API_BASE}/${id}`, {
425
+ method: "PATCH",
426
+ credentials: "include",
427
+ headers: {
428
+ "Content-Type": "application/json"
429
+ },
430
+ body: JSON.stringify(payload)
431
+ });
432
+ return parseJson(res);
433
+ }
434
+ async function deleteMediaAsset(id, force) {
435
+ const search = force ? "?force=true" : "";
436
+ const res = await fetch(`${API_BASE}/${id}${search}`, {
437
+ method: "DELETE",
438
+ credentials: "include"
439
+ });
440
+ if (!res.ok) {
441
+ throw new Error("Failed to delete media asset");
442
+ }
443
+ }
444
+ async function replaceMediaAsset(id, file) {
445
+ const formData = new FormData();
446
+ formData.append("file", file);
447
+ const res = await fetch(`${API_BASE}/${id}/replace`, {
448
+ method: "POST",
449
+ body: formData,
450
+ credentials: "include"
451
+ });
452
+ return parseJson(res);
453
+ }
454
+ async function getMediaAssetUrl(id, options = {}) {
455
+ const params = new URLSearchParams();
456
+ if (options.variant) {
457
+ params.set("variant", options.variant);
458
+ }
459
+ if (options.signed !== void 0) {
460
+ params.set("signed", String(options.signed));
461
+ }
462
+ if (options.ttl) {
463
+ params.set("ttl", String(options.ttl));
464
+ }
465
+ const query = params.toString();
466
+ const res = await fetch(
467
+ `${API_BASE}/${id}/url${query ? `?${query}` : ""}`,
468
+ {
469
+ credentials: "include"
470
+ }
471
+ );
472
+ const data = await parseJson(res);
473
+ return data.url;
474
+ }
475
+ const UploadDrawer = ({
476
+ open,
477
+ onOpenChange,
478
+ onUploaded
479
+ }) => {
480
+ const [files, setFiles] = react.useState([]);
481
+ const [tagsInput, setTagsInput] = react.useState("");
482
+ const [status, setStatus] = react.useState("draft");
483
+ const [visibility, setVisibility] = react.useState("public");
484
+ const [isUploading, setIsUploading] = react.useState(false);
485
+ const [error, setError] = react.useState(null);
486
+ const tags = react.useMemo(() => parseTags(tagsInput), [tagsInput]);
487
+ const handleFiles = react.useCallback((selected) => {
488
+ if (!selected) {
489
+ setFiles([]);
490
+ return;
491
+ }
492
+ setFiles(Array.from(selected));
493
+ }, []);
494
+ const handleDrop = react.useCallback((event) => {
495
+ event.preventDefault();
496
+ if (event.dataTransfer.items) {
497
+ const droppedFiles = Array.from(event.dataTransfer.items).filter((item) => item.kind === "file").map((item) => item.getAsFile()).filter(Boolean);
498
+ if (droppedFiles.length) {
499
+ setFiles(droppedFiles);
500
+ }
501
+ } else if (event.dataTransfer.files) {
502
+ setFiles(Array.from(event.dataTransfer.files));
503
+ }
504
+ }, []);
505
+ const handleUpload = react.useCallback(async () => {
506
+ if (!files.length) {
507
+ return;
508
+ }
509
+ setIsUploading(true);
510
+ setError(null);
511
+ try {
512
+ await uploadMediaAssets({
513
+ files,
514
+ tags,
515
+ visibility,
516
+ status
517
+ });
518
+ ui.toast.success("Upload completed", {
519
+ description: `${files.length} file${files.length > 1 ? "s" : ""} uploaded successfully.`
520
+ });
521
+ setFiles([]);
522
+ setTagsInput("");
523
+ onUploaded();
524
+ onOpenChange(false);
525
+ } catch (err) {
526
+ setError((err == null ? void 0 : err.message) ?? "Failed to upload media assets");
527
+ ui.toast.error("Upload failed", {
528
+ description: (err == null ? void 0 : err.message) ?? "Unable to upload assets. Try again."
529
+ });
530
+ } finally {
531
+ setIsUploading(false);
532
+ }
533
+ }, [files, tags, visibility, status, onUploaded, onOpenChange]);
534
+ const clearFile = (file) => {
535
+ setFiles((prev) => prev.filter((item) => item !== file));
536
+ };
537
+ return /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer, { open, onOpenChange, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Content, { className: "flex w-full flex-col gap-y-6 p-6 sm:max-w-xl", children: [
538
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Header, { children: [
539
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Title, { children: "Upload media" }),
540
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Description, { children: "Drag & drop or pick files to upload. Supported formats include images, SVG, videos, and PDFs." })
541
+ ] }),
542
+ /* @__PURE__ */ jsxRuntime.jsxs(
543
+ "div",
544
+ {
545
+ onDragOver: (event) => event.preventDefault(),
546
+ onDrop: handleDrop,
547
+ className: "flex cursor-pointer flex-col items-center justify-center gap-y-2 rounded-lg border border-dashed border-ui-border-base bg-ui-bg-subtle p-8 text-center transition hover:border-ui-border-strong",
548
+ children: [
549
+ /* @__PURE__ */ jsxRuntime.jsx(icons.CloudArrowUp, { className: "size-6 text-ui-fg-subtle" }),
550
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-medium text-ui-fg-base", children: "Drag & drop files here, or click to browse" }),
551
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-ui-fg-subtle", children: "JPG, PNG, WebP, AVIF, SVG, MP4, WebM, PDF up to 200MB" }),
552
+ /* @__PURE__ */ jsxRuntime.jsx(
553
+ "input",
554
+ {
555
+ type: "file",
556
+ multiple: true,
557
+ className: "hidden",
558
+ id: "media-manager-upload-input",
559
+ onChange: (event) => handleFiles(event.target.files)
560
+ }
561
+ ),
562
+ /* @__PURE__ */ jsxRuntime.jsx(
563
+ "label",
564
+ {
565
+ htmlFor: "media-manager-upload-input",
566
+ className: "mt-2 inline-flex cursor-pointer rounded-md border border-ui-border-base bg-ui-bg-base px-3 py-1 text-xs font-medium text-ui-fg-base transition hover:border-ui-border-strong",
567
+ children: "Select files"
568
+ }
569
+ )
570
+ ]
571
+ }
572
+ ),
573
+ files.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "max-h-48 space-y-2 overflow-y-auto rounded-md border border-ui-border-base p-3", children: files.map((file) => /* @__PURE__ */ jsxRuntime.jsxs(
574
+ "div",
575
+ {
576
+ className: "flex items-center justify-between gap-x-2 rounded bg-ui-bg-subtle px-3 py-2",
577
+ children: [
578
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0", children: [
579
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "truncate text-sm font-medium", children: file.name }),
580
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-xs text-ui-fg-subtle", children: [
581
+ (file.size / 1024 / 1024).toFixed(2),
582
+ " MB · ",
583
+ file.type
584
+ ] })
585
+ ] }),
586
+ /* @__PURE__ */ jsxRuntime.jsx(IconButtonSmall, { onClick: () => clearFile(file) })
587
+ ]
588
+ },
589
+ `${file.name}-${file.size}-${file.lastModified}`
590
+ )) }),
591
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
592
+ /* @__PURE__ */ jsxRuntime.jsx(
593
+ ui.Input,
594
+ {
595
+ label: "Tags",
596
+ placeholder: "summer, lookbook, homepage",
597
+ value: tagsInput,
598
+ onChange: (event) => setTagsInput(event.target.value),
599
+ helperText: "Separate tags with commas."
600
+ }
601
+ ),
602
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2", children: [
603
+ /* @__PURE__ */ jsxRuntime.jsxs(
604
+ ui.Select,
605
+ {
606
+ value: status,
607
+ onValueChange: (value) => setStatus(value),
608
+ children: [
609
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Trigger, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Value, { placeholder: "Status" }) }),
610
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Select.Content, { children: [
611
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "draft", children: "Draft" }),
612
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "published", children: "Published" }),
613
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "archived", children: "Archived" })
614
+ ] })
615
+ ]
616
+ }
617
+ ),
618
+ /* @__PURE__ */ jsxRuntime.jsxs(
619
+ ui.Select,
620
+ {
621
+ value: visibility,
622
+ onValueChange: (value) => setVisibility(value),
623
+ children: [
624
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Trigger, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Value, { placeholder: "Visibility" }) }),
625
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Select.Content, { children: [
626
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "public", children: "Public" }),
627
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "private", children: "Private" })
628
+ ] })
629
+ ]
630
+ }
631
+ )
632
+ ] })
633
+ ] }),
634
+ error && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-ui-fg-error", children: error }),
635
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Footer, { className: "flex items-center justify-between gap-x-2", children: [
636
+ /* @__PURE__ */ jsxRuntime.jsx(
637
+ ui.Button,
638
+ {
639
+ variant: "secondary",
640
+ onClick: () => {
641
+ setFiles([]);
642
+ setTagsInput("");
643
+ setError(null);
644
+ onOpenChange(false);
645
+ },
646
+ children: "Cancel"
647
+ }
648
+ ),
649
+ /* @__PURE__ */ jsxRuntime.jsxs(
650
+ ui.Button,
651
+ {
652
+ onClick: handleUpload,
653
+ disabled: !files.length || isUploading,
654
+ isLoading: isUploading,
655
+ children: [
656
+ "Upload ",
657
+ files.length ? `(${files.length})` : ""
658
+ ]
659
+ }
660
+ )
661
+ ] })
662
+ ] }) });
663
+ };
664
+ const IconButtonSmall = ({ onClick }) => {
665
+ return /* @__PURE__ */ jsxRuntime.jsx(
666
+ "button",
667
+ {
668
+ type: "button",
669
+ onClick: (event) => {
670
+ event.stopPropagation();
671
+ onClick();
672
+ },
673
+ className: "rounded-full p-1 text-ui-fg-muted transition hover:bg-ui-bg-subtle hover:text-ui-fg-base",
674
+ children: /* @__PURE__ */ jsxRuntime.jsx(icons.X, { className: "size-4" })
675
+ }
676
+ );
677
+ };
678
+ const STATUS_COLORS = {
679
+ published: "success",
680
+ draft: "default",
681
+ archived: "warning"
682
+ };
683
+ const DEFAULT_LOCALE = "default";
684
+ const MediaAssetDrawer = ({
685
+ assetId,
686
+ onClose,
687
+ onUpdated,
688
+ onDeleted
689
+ }) => {
690
+ var _a;
691
+ const fileInputRef = react.useRef(null);
692
+ const [asset, setAsset] = react.useState(null);
693
+ const [isLoading, setIsLoading] = react.useState(false);
694
+ const [isSaving, setIsSaving] = react.useState(false);
695
+ const [isReplacing, setIsReplacing] = react.useState(false);
696
+ const [previewUrl, setPreviewUrl] = react.useState(null);
697
+ const [variantUrls, setVariantUrls] = react.useState({});
698
+ const [title, setTitle] = react.useState("");
699
+ const [altText, setAltText] = react.useState("");
700
+ const [caption, setCaption] = react.useState("");
701
+ const [tagsInput, setTagsInput] = react.useState("");
702
+ const [status, setStatus] = react.useState("draft");
703
+ const [visibility, setVisibility] = react.useState("public");
704
+ const [deletePromptOpen, setDeletePromptOpen] = react.useState(false);
705
+ const [deleteAttempted, setDeleteAttempted] = react.useState(false);
706
+ react.useEffect(() => {
707
+ if (!assetId) {
708
+ setAsset(null);
709
+ setPreviewUrl(null);
710
+ setDeleteAttempted(false);
711
+ return;
712
+ }
713
+ let isMounted = true;
714
+ const loadAsset = async () => {
715
+ var _a2;
716
+ try {
717
+ setIsLoading(true);
718
+ const response = await getMediaAsset(assetId);
719
+ if (!isMounted) {
720
+ return;
721
+ }
722
+ const currentAsset = response.asset;
723
+ setAsset(currentAsset);
724
+ setDeleteAttempted(false);
725
+ setTitle(getLocaleText(currentAsset.title, DEFAULT_LOCALE));
726
+ setAltText(getLocaleText(currentAsset.alt_text, DEFAULT_LOCALE));
727
+ setCaption(getLocaleText(currentAsset.caption, DEFAULT_LOCALE));
728
+ setStatus(currentAsset.status);
729
+ setVisibility(currentAsset.visibility);
730
+ setTagsInput(
731
+ ((_a2 = currentAsset.tag_links) == null ? void 0 : _a2.map((link) => link.tag.name).join(", ")) ?? ""
732
+ );
733
+ try {
734
+ const url = await getMediaAssetUrl(currentAsset.id, {
735
+ variant: "medium",
736
+ signed: currentAsset.visibility === "private"
737
+ });
738
+ if (isMounted) {
739
+ setPreviewUrl(url);
740
+ }
741
+ } catch {
742
+ try {
743
+ const fallback = await getMediaAssetUrl(currentAsset.id, {
744
+ signed: currentAsset.visibility === "private"
745
+ });
746
+ if (isMounted) {
747
+ setPreviewUrl(fallback);
748
+ }
749
+ } catch {
750
+ setPreviewUrl(null);
751
+ }
752
+ }
753
+ } catch (err) {
754
+ ui.toast.error("Failed to load asset", {
755
+ description: (err == null ? void 0 : err.message) ?? "Unable to load asset details."
756
+ });
757
+ } finally {
758
+ if (isMounted) {
759
+ setIsLoading(false);
760
+ }
761
+ }
762
+ };
763
+ loadAsset();
764
+ return () => {
765
+ isMounted = false;
766
+ };
767
+ }, [assetId]);
768
+ const tags = react.useMemo(() => parseTags(tagsInput), [tagsInput]);
769
+ const handleSave = async () => {
770
+ if (!asset || !assetId) {
771
+ return;
772
+ }
773
+ setIsSaving(true);
774
+ try {
775
+ await updateMediaAsset(assetId, {
776
+ title: setLocaleText(title, DEFAULT_LOCALE),
777
+ alt_text: setLocaleText(altText, DEFAULT_LOCALE),
778
+ caption: setLocaleText(caption, DEFAULT_LOCALE),
779
+ visibility,
780
+ status,
781
+ tags
782
+ });
783
+ ui.toast.success("Asset updated", {
784
+ description: "Metadata has been saved successfully."
785
+ });
786
+ onUpdated();
787
+ } catch (err) {
788
+ ui.toast.error("Failed to update", {
789
+ description: (err == null ? void 0 : err.message) ?? "Unable to update asset metadata."
790
+ });
791
+ } finally {
792
+ setIsSaving(false);
793
+ }
794
+ };
795
+ const handleDelete = async (force) => {
796
+ if (!assetId) {
797
+ return;
798
+ }
799
+ try {
800
+ await deleteMediaAsset(assetId, force);
801
+ ui.toast.success("Asset deleted");
802
+ setDeletePromptOpen(false);
803
+ setDeleteAttempted(false);
804
+ onDeleted();
805
+ onClose();
806
+ } catch (err) {
807
+ setDeleteAttempted(true);
808
+ ui.toast.error("Failed to delete", {
809
+ description: (err == null ? void 0 : err.message) ?? "Unable to delete asset. If it's in use, try forcing deletion."
810
+ });
811
+ }
812
+ };
813
+ const handleVariantCopy = async (preset) => {
814
+ if (!asset) {
815
+ return;
816
+ }
817
+ try {
818
+ const url = variantUrls[preset] ?? await getMediaAssetUrl(asset.id, {
819
+ variant: preset,
820
+ signed: asset.visibility === "private"
821
+ });
822
+ setVariantUrls((prev) => ({ ...prev, [preset]: url }));
823
+ await navigator.clipboard.writeText(url);
824
+ ui.toast.success("Variant URL copied", {
825
+ description: preset
826
+ });
827
+ } catch (err) {
828
+ ui.toast.error("Failed to copy URL", {
829
+ description: (err == null ? void 0 : err.message) ?? "Unable to copy variant URL."
830
+ });
831
+ }
832
+ };
833
+ const handleReplace = async (file) => {
834
+ if (!file || !assetId) {
835
+ return;
836
+ }
837
+ setIsReplacing(true);
838
+ try {
839
+ await replaceMediaAsset(assetId, file);
840
+ ui.toast.success("Asset replaced", {
841
+ description: file.name
842
+ });
843
+ setVariantUrls({});
844
+ onUpdated();
845
+ const response = await getMediaAsset(assetId);
846
+ setAsset(response.asset);
847
+ } catch (err) {
848
+ ui.toast.error("Failed to replace", {
849
+ description: (err == null ? void 0 : err.message) ?? "Unable to replace asset."
850
+ });
851
+ } finally {
852
+ setIsReplacing(false);
853
+ }
854
+ };
855
+ const assetRelations = (asset == null ? void 0 : asset.relations) ?? [];
856
+ return /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer, { open: Boolean(assetId), onOpenChange: (open) => !open && onClose(), children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Content, { className: "flex h-full w-full max-w-3xl flex-col overflow-y-auto p-6", children: [
857
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Header, { children: [
858
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Title, { className: "text-lg font-semibold", children: "Asset details" }),
859
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Description, { children: "Review and update metadata, copy delivery URLs, or replace the original file." })
860
+ ] }),
861
+ isLoading ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-1 items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm text-ui-fg-subtle", children: "Loading asset..." }) }) : asset ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-1 flex-col gap-y-6", children: [
862
+ /* @__PURE__ */ jsxRuntime.jsxs("section", { className: "flex flex-col gap-y-4", children: [
863
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "aspect-video w-full overflow-hidden rounded-lg border border-ui-border-base bg-ui-bg-subtle", children: previewUrl ? /* @__PURE__ */ jsxRuntime.jsx(
864
+ "img",
865
+ {
866
+ src: previewUrl,
867
+ alt: asset.original_filename,
868
+ className: "h-full w-full object-contain"
869
+ }
870
+ ) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-full w-full items-center justify-center text-ui-fg-muted", children: /* @__PURE__ */ jsxRuntime.jsx(icons.Photo, { className: "size-8" }) }) }),
871
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center gap-2 text-xs text-ui-fg-subtle", children: [
872
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: asset.mime }),
873
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "·" }),
874
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: formatFileSize(asset.size_bytes) }),
875
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "·" }),
876
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
877
+ "Updated ",
878
+ formatDateTime(asset.updated_at)
879
+ ] }),
880
+ /* @__PURE__ */ jsxRuntime.jsx(
881
+ ui.StatusBadge,
882
+ {
883
+ size: "small",
884
+ color: STATUS_COLORS[asset.status] ?? "default",
885
+ children: asset.status
886
+ }
887
+ ),
888
+ /* @__PURE__ */ jsxRuntime.jsx(
889
+ ui.Badge,
890
+ {
891
+ size: "small",
892
+ variant: asset.visibility === "public" ? "neutral" : "warning",
893
+ className: "capitalize",
894
+ children: asset.visibility
895
+ }
896
+ )
897
+ ] }),
898
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap gap-2", children: [
899
+ /* @__PURE__ */ jsxRuntime.jsxs(
900
+ ui.Button,
901
+ {
902
+ type: "button",
903
+ variant: "secondary",
904
+ onClick: () => {
905
+ var _a2;
906
+ return (_a2 = fileInputRef.current) == null ? void 0 : _a2.click();
907
+ },
908
+ isLoading: isReplacing,
909
+ children: [
910
+ /* @__PURE__ */ jsxRuntime.jsx(icons.CircleArrowUp, { className: "mr-1 size-4" }),
911
+ "Replace file"
912
+ ]
913
+ }
914
+ ),
915
+ /* @__PURE__ */ jsxRuntime.jsxs(
916
+ ui.Button,
917
+ {
918
+ type: "button",
919
+ variant: "secondary",
920
+ onClick: () => handleVariantCopy("medium"),
921
+ children: [
922
+ /* @__PURE__ */ jsxRuntime.jsx(icons.ArrowDownTray, { className: "mr-1 size-4" }),
923
+ "Get URL"
924
+ ]
925
+ }
926
+ )
927
+ ] }),
928
+ /* @__PURE__ */ jsxRuntime.jsx(
929
+ "input",
930
+ {
931
+ type: "file",
932
+ ref: fileInputRef,
933
+ className: "hidden",
934
+ onChange: (event) => {
935
+ var _a2;
936
+ return handleReplace(((_a2 = event.target.files) == null ? void 0 : _a2[0]) ?? null);
937
+ }
938
+ }
939
+ )
940
+ ] }),
941
+ /* @__PURE__ */ jsxRuntime.jsxs("section", { className: "space-y-4", children: [
942
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-1 gap-4 md:grid-cols-2", children: [
943
+ /* @__PURE__ */ jsxRuntime.jsx(
944
+ ui.Input,
945
+ {
946
+ label: "Title",
947
+ placeholder: "Hero banner",
948
+ value: title,
949
+ onChange: (event) => setTitle(event.target.value)
950
+ }
951
+ ),
952
+ /* @__PURE__ */ jsxRuntime.jsx(
953
+ ui.Input,
954
+ {
955
+ label: "Alt text",
956
+ placeholder: "Describe the media for accessibility",
957
+ value: altText,
958
+ onChange: (event) => setAltText(event.target.value)
959
+ }
960
+ )
961
+ ] }),
962
+ /* @__PURE__ */ jsxRuntime.jsx(
963
+ ui.Textarea,
964
+ {
965
+ label: "Caption",
966
+ placeholder: "Optional caption or notes",
967
+ value: caption,
968
+ onChange: (event) => setCaption(event.target.value)
969
+ }
970
+ ),
971
+ /* @__PURE__ */ jsxRuntime.jsx(
972
+ ui.Input,
973
+ {
974
+ label: "Tags",
975
+ placeholder: "summer, lookbook, hero",
976
+ value: tagsInput,
977
+ onChange: (event) => setTagsInput(event.target.value),
978
+ helperText: "Separate tags with commas"
979
+ }
980
+ ),
981
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2", children: [
982
+ /* @__PURE__ */ jsxRuntime.jsxs(
983
+ ui.Select,
984
+ {
985
+ value: status,
986
+ onValueChange: (value) => setStatus(value),
987
+ children: [
988
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Trigger, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Value, { placeholder: "Status" }) }),
989
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Select.Content, { children: [
990
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "draft", children: "Draft" }),
991
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "published", children: "Published" }),
992
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "archived", children: "Archived" })
993
+ ] })
994
+ ]
995
+ }
996
+ ),
997
+ /* @__PURE__ */ jsxRuntime.jsxs(
998
+ ui.Select,
999
+ {
1000
+ value: visibility,
1001
+ onValueChange: (value) => setVisibility(value),
1002
+ children: [
1003
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Trigger, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Value, { placeholder: "Visibility" }) }),
1004
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Select.Content, { children: [
1005
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "public", children: "Public" }),
1006
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "private", children: "Private" })
1007
+ ] })
1008
+ ]
1009
+ }
1010
+ )
1011
+ ] })
1012
+ ] }),
1013
+ ((_a = asset.variants) == null ? void 0 : _a.length) ? /* @__PURE__ */ jsxRuntime.jsxs("section", { className: "space-y-3", children: [
1014
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-sm font-medium", children: "Variants" }),
1015
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Table, { children: [
1016
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
1017
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Name" }),
1018
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Format" }),
1019
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Dimensions" }),
1020
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Size" }),
1021
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { className: "text-right", children: "Actions" })
1022
+ ] }) }),
1023
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Body, { children: asset.variants.map((variant) => /* @__PURE__ */ jsxRuntime.jsxs(
1024
+ ui.Table.Row,
1025
+ {
1026
+ children: [
1027
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "capitalize", children: variant.preset }),
1028
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: variant.format }),
1029
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: variant.width && variant.height ? `${variant.width}×${variant.height}` : "-" }),
1030
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: formatFileSize(variant.size_bytes) }),
1031
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "flex justify-end gap-2", children: /* @__PURE__ */ jsxRuntime.jsxs(
1032
+ ui.Button,
1033
+ {
1034
+ size: "small",
1035
+ variant: "secondary",
1036
+ onClick: () => handleVariantCopy(variant.preset),
1037
+ children: [
1038
+ /* @__PURE__ */ jsxRuntime.jsx(icons.SquareTwoStack, { className: "mr-1 size-4" }),
1039
+ " Copy URL"
1040
+ ]
1041
+ }
1042
+ ) })
1043
+ ]
1044
+ },
1045
+ variant.id ?? `${variant.preset}-${variant.format}`
1046
+ )) })
1047
+ ] })
1048
+ ] }) : null,
1049
+ /* @__PURE__ */ jsxRuntime.jsxs("section", { className: "space-y-3", children: [
1050
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-sm font-medium", children: "Usage" }),
1051
+ assetRelations.length ? /* @__PURE__ */ jsxRuntime.jsxs(ui.Table, { children: [
1052
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
1053
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Entity" }),
1054
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Reference" }),
1055
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Role" })
1056
+ ] }) }),
1057
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Body, { children: assetRelations.map((relation) => /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
1058
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "capitalize", children: relation.entity_type }),
1059
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "font-mono text-xs", children: relation.entity_id }),
1060
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: relation.relation_role })
1061
+ ] }, relation.id)) })
1062
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-ui-fg-subtle", children: "This asset is not linked to any entities." })
1063
+ ] })
1064
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-1 items-center justify-center text-sm text-ui-fg-subtle", children: "Select an asset to view details." }),
1065
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Footer, { className: "mt-6 flex flex-wrap items-center justify-between gap-3", children: [
1066
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2", children: [
1067
+ /* @__PURE__ */ jsxRuntime.jsxs(
1068
+ ui.Button,
1069
+ {
1070
+ type: "button",
1071
+ variant: "danger",
1072
+ onClick: () => {
1073
+ if (!asset) {
1074
+ return;
1075
+ }
1076
+ const confirmDelete = window.confirm(
1077
+ "Delete this asset and all generated variants?"
1078
+ );
1079
+ if (confirmDelete) {
1080
+ handleDelete(false);
1081
+ }
1082
+ },
1083
+ disabled: !asset,
1084
+ children: [
1085
+ /* @__PURE__ */ jsxRuntime.jsx(icons.Trash, { className: "mr-1 size-4" }),
1086
+ " Delete"
1087
+ ]
1088
+ }
1089
+ ),
1090
+ /* @__PURE__ */ jsxRuntime.jsxs(
1091
+ ui.Button,
1092
+ {
1093
+ type: "button",
1094
+ variant: "secondary",
1095
+ onClick: () => asset && handleVariantCopy("medium"),
1096
+ disabled: !asset,
1097
+ children: [
1098
+ /* @__PURE__ */ jsxRuntime.jsx(icons.ArrowPath, { className: "mr-1 size-4" }),
1099
+ " Copy URL"
1100
+ ]
1101
+ }
1102
+ )
1103
+ ] }),
1104
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2", children: [
1105
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", onClick: onClose, children: "Close" }),
1106
+ /* @__PURE__ */ jsxRuntime.jsx(
1107
+ ui.Button,
1108
+ {
1109
+ onClick: handleSave,
1110
+ disabled: !asset || isSaving,
1111
+ isLoading: isSaving,
1112
+ children: "Save changes"
1113
+ }
1114
+ )
1115
+ ] })
1116
+ ] }),
1117
+ deleteAttempted && asset && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-4 rounded-md border border-ui-border-base bg-ui-bg-subtle p-4 text-sm text-ui-fg-subtle", children: [
1118
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "font-medium text-ui-fg-base", children: "Having trouble deleting?" }),
1119
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-1", children: "If the asset is still referenced, remove those relations first or attempt a force delete." }),
1120
+ /* @__PURE__ */ jsxRuntime.jsx(
1121
+ ui.Button,
1122
+ {
1123
+ className: "mt-3",
1124
+ variant: "danger",
1125
+ size: "small",
1126
+ onClick: () => {
1127
+ const confirmForce = window.confirm(
1128
+ "Force delete will remove the asset even if it is still referenced. Continue?"
1129
+ );
1130
+ if (confirmForce) {
1131
+ handleDelete(true);
1132
+ }
1133
+ },
1134
+ children: "Force delete"
1135
+ }
1136
+ )
1137
+ ] })
1138
+ ] }) });
1139
+ };
1140
+ const PAGE_SIZE = 24;
1141
+ const MediaLibraryPage = () => {
1142
+ const [search, setSearch] = react.useState("");
1143
+ const [type, setType] = react.useState("");
1144
+ const [status, setStatus] = react.useState("");
1145
+ const [visibility, setVisibility] = react.useState("");
1146
+ const [tagsInput, setTagsInput] = react.useState("");
1147
+ const [viewMode, setViewMode] = react.useState("grid");
1148
+ const [page, setPage] = react.useState(0);
1149
+ const [isUploadOpen, setIsUploadOpen] = react.useState(false);
1150
+ const [selectedAsset, setSelectedAsset] = react.useState(null);
1151
+ const [assetsResult, setAssetsResult] = react.useState(null);
1152
+ const [isLoading, setIsLoading] = react.useState(true);
1153
+ const [isFetching, setIsFetching] = react.useState(false);
1154
+ const [error, setError] = react.useState(null);
1155
+ const tags = react.useMemo(() => parseTags(tagsInput), [tagsInput]);
1156
+ const queryParams = react.useMemo(
1157
+ () => ({
1158
+ q: search || void 0,
1159
+ type: type || void 0,
1160
+ status: status || void 0,
1161
+ visibility: visibility || void 0,
1162
+ tags: tags.length ? tags : void 0,
1163
+ limit: PAGE_SIZE,
1164
+ offset: page * PAGE_SIZE
1165
+ }),
1166
+ [search, type, status, visibility, tags, page]
1167
+ );
1168
+ const fetchAssets = react.useCallback(
1169
+ async (showSpinner) => {
1170
+ setError(null);
1171
+ if (showSpinner) {
1172
+ setIsLoading(true);
1173
+ } else {
1174
+ setIsFetching(true);
1175
+ }
1176
+ try {
1177
+ const result = await listMediaAssets(queryParams);
1178
+ setAssetsResult(result);
1179
+ } catch (err) {
1180
+ const message = err instanceof Error ? err.message : "Failed to load media assets";
1181
+ setError(message);
1182
+ } finally {
1183
+ setIsLoading(false);
1184
+ setIsFetching(false);
1185
+ }
1186
+ },
1187
+ [queryParams]
1188
+ );
1189
+ react.useEffect(() => {
1190
+ void fetchAssets(true);
1191
+ }, [fetchAssets]);
1192
+ const refetch = react.useCallback(() => {
1193
+ void fetchAssets(false);
1194
+ }, [fetchAssets]);
1195
+ const assets = (assetsResult == null ? void 0 : assetsResult.assets) ?? [];
1196
+ const total = (assetsResult == null ? void 0 : assetsResult.count) ?? 0;
1197
+ const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
1198
+ react.useEffect(() => {
1199
+ setPage(0);
1200
+ }, [search, type, status, visibility, tagsInput]);
1201
+ const previewCache = react.useRef({});
1202
+ const [previews, setPreviews] = react.useState({});
1203
+ react.useEffect(() => {
1204
+ let cancelled = false;
1205
+ const loadPreviews = async () => {
1206
+ const entries = [];
1207
+ for (const asset of assets) {
1208
+ if (asset.type === "document") {
1209
+ previewCache.current[asset.id] = null;
1210
+ continue;
1211
+ }
1212
+ if (previewCache.current[asset.id] !== void 0) {
1213
+ entries.push([asset.id, previewCache.current[asset.id]]);
1214
+ continue;
1215
+ }
1216
+ try {
1217
+ const url = await getMediaAssetUrl(asset.id, {
1218
+ variant: "thumbnail",
1219
+ signed: asset.visibility === "private"
1220
+ });
1221
+ previewCache.current[asset.id] = url;
1222
+ entries.push([asset.id, url]);
1223
+ } catch {
1224
+ previewCache.current[asset.id] = null;
1225
+ entries.push([asset.id, null]);
1226
+ }
1227
+ }
1228
+ if (!cancelled && entries.length) {
1229
+ setPreviews((prev) => ({ ...prev, ...Object.fromEntries(entries) }));
1230
+ }
1231
+ };
1232
+ if (assets.length) {
1233
+ loadPreviews();
1234
+ }
1235
+ return () => {
1236
+ cancelled = true;
1237
+ };
1238
+ }, [assets]);
1239
+ const handleSelectAsset = (asset) => {
1240
+ setSelectedAsset(asset);
1241
+ };
1242
+ const handleCloseDrawer = () => {
1243
+ setSelectedAsset(null);
1244
+ };
1245
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "flex h-full w-full flex-1 flex-col gap-y-6", children: [
1246
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", children: [
1247
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", children: "Media library" }),
1248
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-ui-fg-subtle", children: "Upload, organize, and manage media assets across the storefront and admin experiences." })
1249
+ ] }),
1250
+ /* @__PURE__ */ jsxRuntime.jsx(
1251
+ FiltersBar,
1252
+ {
1253
+ search,
1254
+ onSearchChange: setSearch,
1255
+ type,
1256
+ onTypeChange: setType,
1257
+ status,
1258
+ onStatusChange: setStatus,
1259
+ visibility,
1260
+ onVisibilityChange: setVisibility,
1261
+ tags: tagsInput,
1262
+ onTagsChange: setTagsInput,
1263
+ viewMode,
1264
+ onViewModeChange: setViewMode,
1265
+ onOpenUpload: () => setIsUploadOpen(true),
1266
+ onRefresh: () => refetch(),
1267
+ totalCount: total
1268
+ }
1269
+ ),
1270
+ error && /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "error", children: error }),
1271
+ /* @__PURE__ */ jsxRuntime.jsx(
1272
+ MediaGrid,
1273
+ {
1274
+ assets,
1275
+ previews,
1276
+ viewMode,
1277
+ isLoading: isLoading || isFetching,
1278
+ onSelectAsset: handleSelectAsset,
1279
+ selectedAssetId: selectedAsset == null ? void 0 : selectedAsset.id
1280
+ }
1281
+ ),
1282
+ totalPages > 1 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-end gap-x-3", children: [
1283
+ /* @__PURE__ */ jsxRuntime.jsx(
1284
+ ui.Button,
1285
+ {
1286
+ variant: "secondary",
1287
+ size: "small",
1288
+ onClick: () => setPage((prev) => Math.max(prev - 1, 0)),
1289
+ disabled: page === 0,
1290
+ children: "Previous"
1291
+ }
1292
+ ),
1293
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-sm text-ui-fg-muted", children: [
1294
+ "Page ",
1295
+ page + 1,
1296
+ " of ",
1297
+ totalPages
1298
+ ] }),
1299
+ /* @__PURE__ */ jsxRuntime.jsx(
1300
+ ui.Button,
1301
+ {
1302
+ variant: "secondary",
1303
+ size: "small",
1304
+ onClick: () => setPage((prev) => Math.min(prev + 1, totalPages - 1)),
1305
+ disabled: page + 1 >= totalPages,
1306
+ children: "Next"
1307
+ }
1308
+ )
1309
+ ] }),
1310
+ /* @__PURE__ */ jsxRuntime.jsx(
1311
+ UploadDrawer,
1312
+ {
1313
+ open: isUploadOpen,
1314
+ onOpenChange: setIsUploadOpen,
1315
+ onUploaded: () => {
1316
+ previewCache.current = {};
1317
+ refetch();
1318
+ }
1319
+ }
1320
+ ),
1321
+ /* @__PURE__ */ jsxRuntime.jsx(
1322
+ MediaAssetDrawer,
1323
+ {
1324
+ assetId: (selectedAsset == null ? void 0 : selectedAsset.id) ?? null,
1325
+ onClose: handleCloseDrawer,
1326
+ onUpdated: () => {
1327
+ previewCache.current = {};
1328
+ refetch();
1329
+ },
1330
+ onDeleted: () => {
1331
+ previewCache.current = {};
1332
+ setSelectedAsset(null);
1333
+ refetch();
1334
+ }
1335
+ }
1336
+ )
1337
+ ] });
1338
+ };
1339
+ const config = adminSdk.defineRouteConfig({
1340
+ icon: icons.Photo,
1341
+ label: "Media Library",
1342
+ path: "/media-library"
1343
+ });
1344
+ const widgetModule = { widgets: [] };
1345
+ const routeModule = {
1346
+ routes: [
1347
+ {
1348
+ Component: MediaLibraryPage,
1349
+ path: "/library"
1350
+ }
1351
+ ]
1352
+ };
1353
+ const menuItemModule = {
1354
+ menuItems: [
1355
+ {
1356
+ label: config.label,
1357
+ icon: config.icon,
1358
+ path: "/library",
1359
+ nested: void 0
1360
+ }
1361
+ ]
1362
+ };
1363
+ const formModule = { customFields: {} };
1364
+ const displayModule = {
1365
+ displays: {}
1366
+ };
1367
+ const i18nModule = { resources: {} };
1368
+ const plugin = {
1369
+ widgetModule,
1370
+ routeModule,
1371
+ menuItemModule,
1372
+ formModule,
1373
+ displayModule,
1374
+ i18nModule
1375
+ };
1376
+ module.exports = plugin;