@lodashventure/medusa-collection-thumbnail 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/.medusa/server/src/admin/index.js +342 -0
  2. package/.medusa/server/src/admin/index.mjs +343 -0
  3. package/.medusa/server/src/api/admin/collections/[id]/thumbnail/route.js +211 -0
  4. package/.medusa/server/{api → src/api}/middlewares.js +1 -1
  5. package/.medusa/server/src/api/store/collections/[id]/route.js +43 -0
  6. package/.medusa/server/src/api/store/collections/route.js +71 -0
  7. package/.medusa/server/src/index.js +15 -0
  8. package/.medusa/server/{modules → src/modules}/collection/index.js +1 -1
  9. package/.medusa/server/{modules/collection/migrations/Migration20250921062957.js → src/modules/collection/migrations/Migration20251108000000.js} +4 -4
  10. package/.medusa/server/{modules → src/modules}/collection/models/collection.js +1 -1
  11. package/.medusa/server/{modules → src/modules}/collection/service.js +1 -1
  12. package/.medusa/server/src/services/gcs-direct-upload.js +56 -0
  13. package/.medusa/server/src/workflows/upload-collection-thumbnail.js +59 -0
  14. package/README.md +33 -35
  15. package/package.json +28 -21
  16. package/.medusa/server/api/admin/collections/[id]/thumbnail/route.d.ts +0 -4
  17. package/.medusa/server/api/admin/collections/[id]/thumbnail/route.js +0 -101
  18. package/.medusa/server/api/middlewares.d.ts +0 -2
  19. package/.medusa/server/api/store/collections/[id]/route.d.ts +0 -2
  20. package/.medusa/server/api/store/collections/[id]/route.js +0 -46
  21. package/.medusa/server/api/store/collections/route.d.ts +0 -2
  22. package/.medusa/server/api/store/collections/route.js +0 -71
  23. package/.medusa/server/index.d.ts +0 -2
  24. package/.medusa/server/index.js +0 -8
  25. package/.medusa/server/links/collection-thumbnail.d.ts +0 -2
  26. package/.medusa/server/links/collection-thumbnail.js +0 -16
  27. package/.medusa/server/modules/collection/index.d.ts +0 -21
  28. package/.medusa/server/modules/collection/migrations/Migration20250921062957.d.ts +0 -5
  29. package/.medusa/server/modules/collection/models/collection.d.ts +0 -5
  30. package/.medusa/server/modules/collection/service.d.ts +0 -10
  31. package/.medusa/server/services/gcs-direct-upload.d.ts +0 -8
  32. package/.medusa/server/services/gcs-direct-upload.js +0 -55
  33. package/.medusa/server/workflows/upload-collection-thumbnail.d.ts +0 -13
  34. package/.medusa/server/workflows/upload-collection-thumbnail.js +0 -57
@@ -0,0 +1,342 @@
1
+ "use strict";
2
+ const jsxRuntime = require("react/jsx-runtime");
3
+ const adminSdk = require("@medusajs/admin-sdk");
4
+ const ui = require("@medusajs/ui");
5
+ const icons = require("@medusajs/icons");
6
+ const react = require("react");
7
+ require("@medusajs/admin-shared");
8
+ const CollectionThumbnailUploader = ({
9
+ collectionId,
10
+ initialThumbnail,
11
+ onThumbnailChange,
12
+ className
13
+ }) => {
14
+ const [thumbnail, setThumbnail] = react.useState(initialThumbnail || null);
15
+ const [isUploading, setIsUploading] = react.useState(false);
16
+ const [isDragging, setIsDragging] = react.useState(false);
17
+ const [error, setError] = react.useState(null);
18
+ const [previewUrl, setPreviewUrl] = react.useState(null);
19
+ const validateFile = (file) => {
20
+ const allowedTypes = ["image/jpeg", "image/png", "image/gif", "image/webp"];
21
+ if (!allowedTypes.includes(file.type)) {
22
+ setError("Please upload a valid image file (JPEG, PNG, GIF, or WebP)");
23
+ return false;
24
+ }
25
+ if (file.size > 5 * 1024 * 1024) {
26
+ setError("File size must be less than 5MB");
27
+ return false;
28
+ }
29
+ return true;
30
+ };
31
+ const handleFileUpload = async (file) => {
32
+ if (!validateFile(file)) return;
33
+ setError(null);
34
+ const reader = new FileReader();
35
+ reader.onloadend = () => {
36
+ setPreviewUrl(reader.result);
37
+ };
38
+ reader.readAsDataURL(file);
39
+ if (collectionId) {
40
+ setIsUploading(true);
41
+ const formData = new FormData();
42
+ formData.append("thumbnail", file);
43
+ try {
44
+ const response = await fetch(`/admin/collections/${collectionId}/thumbnail`, {
45
+ method: "POST",
46
+ body: formData,
47
+ credentials: "include"
48
+ });
49
+ if (response.ok) {
50
+ const result = await response.json();
51
+ setThumbnail(result.collection.thumbnail);
52
+ setPreviewUrl(null);
53
+ onThumbnailChange == null ? void 0 : onThumbnailChange(result.collection.thumbnail);
54
+ } else {
55
+ const errorData = await response.json();
56
+ setError(errorData.error || "Failed to upload thumbnail");
57
+ setPreviewUrl(null);
58
+ }
59
+ } catch (err) {
60
+ setError("Error uploading thumbnail");
61
+ setPreviewUrl(null);
62
+ } finally {
63
+ setIsUploading(false);
64
+ }
65
+ } else {
66
+ onThumbnailChange == null ? void 0 : onThumbnailChange(previewUrl);
67
+ }
68
+ };
69
+ const handleDelete = async () => {
70
+ if (!confirm("Are you sure you want to delete the thumbnail?")) return;
71
+ if (collectionId) {
72
+ setIsUploading(true);
73
+ setError(null);
74
+ try {
75
+ const response = await fetch(`/admin/collections/${collectionId}/thumbnail`, {
76
+ method: "DELETE",
77
+ credentials: "include"
78
+ });
79
+ if (response.ok) {
80
+ setThumbnail(null);
81
+ setPreviewUrl(null);
82
+ onThumbnailChange == null ? void 0 : onThumbnailChange(null);
83
+ } else {
84
+ const errorData = await response.json();
85
+ setError(errorData.error || "Failed to delete thumbnail");
86
+ }
87
+ } catch (err) {
88
+ setError("Error deleting thumbnail");
89
+ } finally {
90
+ setIsUploading(false);
91
+ }
92
+ } else {
93
+ setPreviewUrl(null);
94
+ onThumbnailChange == null ? void 0 : onThumbnailChange(null);
95
+ }
96
+ };
97
+ const handleDragEnter = react.useCallback((e) => {
98
+ e.preventDefault();
99
+ e.stopPropagation();
100
+ setIsDragging(true);
101
+ }, []);
102
+ const handleDragLeave = react.useCallback((e) => {
103
+ e.preventDefault();
104
+ e.stopPropagation();
105
+ setIsDragging(false);
106
+ }, []);
107
+ const handleDragOver = react.useCallback((e) => {
108
+ e.preventDefault();
109
+ e.stopPropagation();
110
+ }, []);
111
+ const handleDrop = react.useCallback((e) => {
112
+ var _a;
113
+ e.preventDefault();
114
+ e.stopPropagation();
115
+ setIsDragging(false);
116
+ const file = (_a = e.dataTransfer.files) == null ? void 0 : _a[0];
117
+ if (file) {
118
+ handleFileUpload(file);
119
+ }
120
+ }, []);
121
+ const displayImage = thumbnail || previewUrl;
122
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: ui.clx("space-y-4", className), children: [
123
+ displayImage ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
124
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative overflow-hidden rounded-lg border border-ui-border-base", children: [
125
+ /* @__PURE__ */ jsxRuntime.jsx(
126
+ "img",
127
+ {
128
+ src: (displayImage == null ? void 0 : displayImage.startsWith("http")) || (displayImage == null ? void 0 : displayImage.startsWith("data:")) ? displayImage : `http://localhost:9000${displayImage}`,
129
+ alt: "Collection thumbnail",
130
+ className: "h-64 w-full object-cover"
131
+ }
132
+ ),
133
+ isUploading && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 flex items-center justify-center bg-black/50", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-white", children: "Uploading..." }) })
134
+ ] }),
135
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2", children: [
136
+ /* @__PURE__ */ jsxRuntime.jsxs(
137
+ ui.Button,
138
+ {
139
+ variant: "secondary",
140
+ disabled: isUploading,
141
+ onClick: () => {
142
+ var _a;
143
+ return (_a = document.getElementById("thumbnail-file-input")) == null ? void 0 : _a.click();
144
+ },
145
+ children: [
146
+ /* @__PURE__ */ jsxRuntime.jsx(icons.CloudArrowUp, { className: "mr-2" }),
147
+ "Replace"
148
+ ]
149
+ }
150
+ ),
151
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { variant: "danger", disabled: isUploading, onClick: handleDelete, children: [
152
+ /* @__PURE__ */ jsxRuntime.jsx(icons.Trash, { className: "mr-2" }),
153
+ "Delete"
154
+ ] })
155
+ ] })
156
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(
157
+ "div",
158
+ {
159
+ className: ui.clx(
160
+ "flex h-64 flex-col items-center justify-center rounded-lg border-2 border-dashed transition-colors",
161
+ isDragging ? "border-ui-border-interactive bg-ui-bg-highlight" : "border-ui-border-base bg-ui-bg-subtle",
162
+ !isUploading && "cursor-pointer"
163
+ ),
164
+ onDragEnter: handleDragEnter,
165
+ onDragLeave: handleDragLeave,
166
+ onDragOver: handleDragOver,
167
+ onDrop: handleDrop,
168
+ onClick: () => {
169
+ var _a;
170
+ return !isUploading && ((_a = document.getElementById("thumbnail-file-input")) == null ? void 0 : _a.click());
171
+ },
172
+ children: [
173
+ /* @__PURE__ */ jsxRuntime.jsx(icons.PhotoSolid, { className: "mb-4 h-12 w-12 text-ui-fg-subtle" }),
174
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "mb-2 text-lg font-medium", children: isDragging ? "Drop image here" : "Upload thumbnail" }),
175
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "mb-4 text-center text-ui-fg-subtle", children: "Drag and drop an image here, or click to select" }),
176
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-sm text-ui-fg-subtle", children: "JPEG, PNG, GIF, or WebP • Max 5MB" }),
177
+ isUploading && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: "Uploading..." }) })
178
+ ]
179
+ }
180
+ ),
181
+ /* @__PURE__ */ jsxRuntime.jsx(
182
+ "input",
183
+ {
184
+ id: "thumbnail-file-input",
185
+ type: "file",
186
+ accept: "image/jpeg,image/png,image/gif,image/webp",
187
+ onChange: (e) => {
188
+ var _a;
189
+ const file = (_a = e.target.files) == null ? void 0 : _a[0];
190
+ if (file) handleFileUpload(file);
191
+ },
192
+ className: "hidden"
193
+ }
194
+ ),
195
+ error && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error", children: error })
196
+ ] });
197
+ };
198
+ const CollectionThumbnailWidget = ({ data }) => {
199
+ const [thumbnail, setThumbnail] = react.useState(null);
200
+ const [loading, setLoading] = react.useState(true);
201
+ const [showUploader, setShowUploader] = react.useState(false);
202
+ react.useEffect(() => {
203
+ fetchThumbnail();
204
+ }, [data.id]);
205
+ const fetchThumbnail = async () => {
206
+ setLoading(true);
207
+ try {
208
+ const response = await fetch(`/admin/collections/${data.id}/thumbnail`, {
209
+ credentials: "include"
210
+ });
211
+ if (response.ok) {
212
+ const result = await response.json();
213
+ setThumbnail(result.thumbnail || null);
214
+ }
215
+ } catch (error) {
216
+ console.error("Failed to fetch thumbnail:", error);
217
+ } finally {
218
+ setLoading(false);
219
+ }
220
+ };
221
+ const handleDeleteThumbnail = async () => {
222
+ if (!confirm("Are you sure you want to delete this thumbnail?")) return;
223
+ try {
224
+ const response = await fetch(`/admin/collections/${data.id}/thumbnail`, {
225
+ method: "DELETE",
226
+ credentials: "include"
227
+ });
228
+ if (response.ok) {
229
+ setThumbnail(null);
230
+ } else {
231
+ alert("Failed to delete thumbnail");
232
+ }
233
+ } catch (error) {
234
+ alert("Error deleting thumbnail");
235
+ }
236
+ };
237
+ const handleUploaderClose = () => {
238
+ setShowUploader(false);
239
+ fetchThumbnail();
240
+ };
241
+ if (loading) {
242
+ return /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { className: "p-0", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: "Loading thumbnail..." }) });
243
+ }
244
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
245
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "p-0", children: [
246
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-between mb-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: "Collection Thumbnail" }) }),
247
+ thumbnail ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-4", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative group", children: [
248
+ /* @__PURE__ */ jsxRuntime.jsx(
249
+ "img",
250
+ {
251
+ src: thumbnail.startsWith("http") ? thumbnail : `http://localhost:9000${thumbnail}`,
252
+ alt: data.title,
253
+ className: "w-full h-48 object-cover rounded-lg"
254
+ }
255
+ ),
256
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity rounded-lg flex items-center justify-center gap-2", children: [
257
+ /* @__PURE__ */ jsxRuntime.jsxs(
258
+ ui.Button,
259
+ {
260
+ variant: "secondary",
261
+ size: "small",
262
+ onClick: () => setShowUploader(true),
263
+ children: [
264
+ /* @__PURE__ */ jsxRuntime.jsx(icons.CloudArrowUp, {}),
265
+ "Replace"
266
+ ]
267
+ }
268
+ ),
269
+ /* @__PURE__ */ jsxRuntime.jsxs(
270
+ ui.Button,
271
+ {
272
+ variant: "danger",
273
+ size: "small",
274
+ onClick: handleDeleteThumbnail,
275
+ children: [
276
+ /* @__PURE__ */ jsxRuntime.jsx(icons.Trash, {}),
277
+ "Delete"
278
+ ]
279
+ }
280
+ )
281
+ ] })
282
+ ] }) }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "border-2 border-dashed border-ui-border-base rounded-lg p-8", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center justify-center text-center", children: [
283
+ /* @__PURE__ */ jsxRuntime.jsx(icons.PhotoSolid, { className: "h-12 w-12 text-ui-fg-subtle mb-4" }),
284
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle mb-4", children: "No thumbnail uploaded for this collection" }),
285
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { onClick: () => setShowUploader(true), children: [
286
+ /* @__PURE__ */ jsxRuntime.jsx(icons.CloudArrowUp, { className: "mr-2" }),
287
+ "Upload Thumbnail"
288
+ ] })
289
+ ] }) })
290
+ ] }),
291
+ showUploader && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/50", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-full max-w-2xl rounded-lg bg-ui-bg-base p-6", children: [
292
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-4 flex items-center justify-between", children: [
293
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
294
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: "Upload Thumbnail" }),
295
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle", children: data.title })
296
+ ] }),
297
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", onClick: handleUploaderClose, children: "Close" })
298
+ ] }),
299
+ /* @__PURE__ */ jsxRuntime.jsx(
300
+ CollectionThumbnailUploader,
301
+ {
302
+ collectionId: data.id,
303
+ initialThumbnail: thumbnail || void 0,
304
+ onThumbnailChange: (url) => {
305
+ if (!url) {
306
+ handleUploaderClose();
307
+ }
308
+ }
309
+ }
310
+ )
311
+ ] }) })
312
+ ] });
313
+ };
314
+ adminSdk.defineWidgetConfig({
315
+ zone: "product_collection.details.after"
316
+ });
317
+ const widgetModule = { widgets: [
318
+ {
319
+ Component: CollectionThumbnailWidget,
320
+ zone: ["product_collection.details.after"]
321
+ }
322
+ ] };
323
+ const routeModule = {
324
+ routes: []
325
+ };
326
+ const menuItemModule = {
327
+ menuItems: []
328
+ };
329
+ const formModule = { customFields: {} };
330
+ const displayModule = {
331
+ displays: {}
332
+ };
333
+ const i18nModule = { resources: {} };
334
+ const plugin = {
335
+ widgetModule,
336
+ routeModule,
337
+ menuItemModule,
338
+ formModule,
339
+ displayModule,
340
+ i18nModule
341
+ };
342
+ module.exports = plugin;
@@ -0,0 +1,343 @@
1
+ import { jsxs, jsx, Fragment } from "react/jsx-runtime";
2
+ import { defineWidgetConfig } from "@medusajs/admin-sdk";
3
+ import { clx, Button, Text, Container, Heading } from "@medusajs/ui";
4
+ import { CloudArrowUp, Trash, PhotoSolid } from "@medusajs/icons";
5
+ import { useState, useCallback, useEffect } from "react";
6
+ import "@medusajs/admin-shared";
7
+ const CollectionThumbnailUploader = ({
8
+ collectionId,
9
+ initialThumbnail,
10
+ onThumbnailChange,
11
+ className
12
+ }) => {
13
+ const [thumbnail, setThumbnail] = useState(initialThumbnail || null);
14
+ const [isUploading, setIsUploading] = useState(false);
15
+ const [isDragging, setIsDragging] = useState(false);
16
+ const [error, setError] = useState(null);
17
+ const [previewUrl, setPreviewUrl] = useState(null);
18
+ const validateFile = (file) => {
19
+ const allowedTypes = ["image/jpeg", "image/png", "image/gif", "image/webp"];
20
+ if (!allowedTypes.includes(file.type)) {
21
+ setError("Please upload a valid image file (JPEG, PNG, GIF, or WebP)");
22
+ return false;
23
+ }
24
+ if (file.size > 5 * 1024 * 1024) {
25
+ setError("File size must be less than 5MB");
26
+ return false;
27
+ }
28
+ return true;
29
+ };
30
+ const handleFileUpload = async (file) => {
31
+ if (!validateFile(file)) return;
32
+ setError(null);
33
+ const reader = new FileReader();
34
+ reader.onloadend = () => {
35
+ setPreviewUrl(reader.result);
36
+ };
37
+ reader.readAsDataURL(file);
38
+ if (collectionId) {
39
+ setIsUploading(true);
40
+ const formData = new FormData();
41
+ formData.append("thumbnail", file);
42
+ try {
43
+ const response = await fetch(`/admin/collections/${collectionId}/thumbnail`, {
44
+ method: "POST",
45
+ body: formData,
46
+ credentials: "include"
47
+ });
48
+ if (response.ok) {
49
+ const result = await response.json();
50
+ setThumbnail(result.collection.thumbnail);
51
+ setPreviewUrl(null);
52
+ onThumbnailChange == null ? void 0 : onThumbnailChange(result.collection.thumbnail);
53
+ } else {
54
+ const errorData = await response.json();
55
+ setError(errorData.error || "Failed to upload thumbnail");
56
+ setPreviewUrl(null);
57
+ }
58
+ } catch (err) {
59
+ setError("Error uploading thumbnail");
60
+ setPreviewUrl(null);
61
+ } finally {
62
+ setIsUploading(false);
63
+ }
64
+ } else {
65
+ onThumbnailChange == null ? void 0 : onThumbnailChange(previewUrl);
66
+ }
67
+ };
68
+ const handleDelete = async () => {
69
+ if (!confirm("Are you sure you want to delete the thumbnail?")) return;
70
+ if (collectionId) {
71
+ setIsUploading(true);
72
+ setError(null);
73
+ try {
74
+ const response = await fetch(`/admin/collections/${collectionId}/thumbnail`, {
75
+ method: "DELETE",
76
+ credentials: "include"
77
+ });
78
+ if (response.ok) {
79
+ setThumbnail(null);
80
+ setPreviewUrl(null);
81
+ onThumbnailChange == null ? void 0 : onThumbnailChange(null);
82
+ } else {
83
+ const errorData = await response.json();
84
+ setError(errorData.error || "Failed to delete thumbnail");
85
+ }
86
+ } catch (err) {
87
+ setError("Error deleting thumbnail");
88
+ } finally {
89
+ setIsUploading(false);
90
+ }
91
+ } else {
92
+ setPreviewUrl(null);
93
+ onThumbnailChange == null ? void 0 : onThumbnailChange(null);
94
+ }
95
+ };
96
+ const handleDragEnter = useCallback((e) => {
97
+ e.preventDefault();
98
+ e.stopPropagation();
99
+ setIsDragging(true);
100
+ }, []);
101
+ const handleDragLeave = useCallback((e) => {
102
+ e.preventDefault();
103
+ e.stopPropagation();
104
+ setIsDragging(false);
105
+ }, []);
106
+ const handleDragOver = useCallback((e) => {
107
+ e.preventDefault();
108
+ e.stopPropagation();
109
+ }, []);
110
+ const handleDrop = useCallback((e) => {
111
+ var _a;
112
+ e.preventDefault();
113
+ e.stopPropagation();
114
+ setIsDragging(false);
115
+ const file = (_a = e.dataTransfer.files) == null ? void 0 : _a[0];
116
+ if (file) {
117
+ handleFileUpload(file);
118
+ }
119
+ }, []);
120
+ const displayImage = thumbnail || previewUrl;
121
+ return /* @__PURE__ */ jsxs("div", { className: clx("space-y-4", className), children: [
122
+ displayImage ? /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
123
+ /* @__PURE__ */ jsxs("div", { className: "relative overflow-hidden rounded-lg border border-ui-border-base", children: [
124
+ /* @__PURE__ */ jsx(
125
+ "img",
126
+ {
127
+ src: (displayImage == null ? void 0 : displayImage.startsWith("http")) || (displayImage == null ? void 0 : displayImage.startsWith("data:")) ? displayImage : `http://localhost:9000${displayImage}`,
128
+ alt: "Collection thumbnail",
129
+ className: "h-64 w-full object-cover"
130
+ }
131
+ ),
132
+ isUploading && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 flex items-center justify-center bg-black/50", children: /* @__PURE__ */ jsx("div", { className: "text-white", children: "Uploading..." }) })
133
+ ] }),
134
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
135
+ /* @__PURE__ */ jsxs(
136
+ Button,
137
+ {
138
+ variant: "secondary",
139
+ disabled: isUploading,
140
+ onClick: () => {
141
+ var _a;
142
+ return (_a = document.getElementById("thumbnail-file-input")) == null ? void 0 : _a.click();
143
+ },
144
+ children: [
145
+ /* @__PURE__ */ jsx(CloudArrowUp, { className: "mr-2" }),
146
+ "Replace"
147
+ ]
148
+ }
149
+ ),
150
+ /* @__PURE__ */ jsxs(Button, { variant: "danger", disabled: isUploading, onClick: handleDelete, children: [
151
+ /* @__PURE__ */ jsx(Trash, { className: "mr-2" }),
152
+ "Delete"
153
+ ] })
154
+ ] })
155
+ ] }) : /* @__PURE__ */ jsxs(
156
+ "div",
157
+ {
158
+ className: clx(
159
+ "flex h-64 flex-col items-center justify-center rounded-lg border-2 border-dashed transition-colors",
160
+ isDragging ? "border-ui-border-interactive bg-ui-bg-highlight" : "border-ui-border-base bg-ui-bg-subtle",
161
+ !isUploading && "cursor-pointer"
162
+ ),
163
+ onDragEnter: handleDragEnter,
164
+ onDragLeave: handleDragLeave,
165
+ onDragOver: handleDragOver,
166
+ onDrop: handleDrop,
167
+ onClick: () => {
168
+ var _a;
169
+ return !isUploading && ((_a = document.getElementById("thumbnail-file-input")) == null ? void 0 : _a.click());
170
+ },
171
+ children: [
172
+ /* @__PURE__ */ jsx(PhotoSolid, { className: "mb-4 h-12 w-12 text-ui-fg-subtle" }),
173
+ /* @__PURE__ */ jsx(Text, { className: "mb-2 text-lg font-medium", children: isDragging ? "Drop image here" : "Upload thumbnail" }),
174
+ /* @__PURE__ */ jsx(Text, { className: "mb-4 text-center text-ui-fg-subtle", children: "Drag and drop an image here, or click to select" }),
175
+ /* @__PURE__ */ jsx(Text, { className: "text-sm text-ui-fg-subtle", children: "JPEG, PNG, GIF, or WebP • Max 5MB" }),
176
+ isUploading && /* @__PURE__ */ jsx("div", { className: "mt-4", children: /* @__PURE__ */ jsx(Text, { children: "Uploading..." }) })
177
+ ]
178
+ }
179
+ ),
180
+ /* @__PURE__ */ jsx(
181
+ "input",
182
+ {
183
+ id: "thumbnail-file-input",
184
+ type: "file",
185
+ accept: "image/jpeg,image/png,image/gif,image/webp",
186
+ onChange: (e) => {
187
+ var _a;
188
+ const file = (_a = e.target.files) == null ? void 0 : _a[0];
189
+ if (file) handleFileUpload(file);
190
+ },
191
+ className: "hidden"
192
+ }
193
+ ),
194
+ error && /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-error", children: error })
195
+ ] });
196
+ };
197
+ const CollectionThumbnailWidget = ({ data }) => {
198
+ const [thumbnail, setThumbnail] = useState(null);
199
+ const [loading, setLoading] = useState(true);
200
+ const [showUploader, setShowUploader] = useState(false);
201
+ useEffect(() => {
202
+ fetchThumbnail();
203
+ }, [data.id]);
204
+ const fetchThumbnail = async () => {
205
+ setLoading(true);
206
+ try {
207
+ const response = await fetch(`/admin/collections/${data.id}/thumbnail`, {
208
+ credentials: "include"
209
+ });
210
+ if (response.ok) {
211
+ const result = await response.json();
212
+ setThumbnail(result.thumbnail || null);
213
+ }
214
+ } catch (error) {
215
+ console.error("Failed to fetch thumbnail:", error);
216
+ } finally {
217
+ setLoading(false);
218
+ }
219
+ };
220
+ const handleDeleteThumbnail = async () => {
221
+ if (!confirm("Are you sure you want to delete this thumbnail?")) return;
222
+ try {
223
+ const response = await fetch(`/admin/collections/${data.id}/thumbnail`, {
224
+ method: "DELETE",
225
+ credentials: "include"
226
+ });
227
+ if (response.ok) {
228
+ setThumbnail(null);
229
+ } else {
230
+ alert("Failed to delete thumbnail");
231
+ }
232
+ } catch (error) {
233
+ alert("Error deleting thumbnail");
234
+ }
235
+ };
236
+ const handleUploaderClose = () => {
237
+ setShowUploader(false);
238
+ fetchThumbnail();
239
+ };
240
+ if (loading) {
241
+ return /* @__PURE__ */ jsx(Container, { className: "p-0", children: /* @__PURE__ */ jsx(Text, { children: "Loading thumbnail..." }) });
242
+ }
243
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
244
+ /* @__PURE__ */ jsxs(Container, { className: "p-0", children: [
245
+ /* @__PURE__ */ jsx("div", { className: "flex items-center justify-between mb-4", children: /* @__PURE__ */ jsx(Heading, { level: "h2", children: "Collection Thumbnail" }) }),
246
+ thumbnail ? /* @__PURE__ */ jsx("div", { className: "space-y-4", children: /* @__PURE__ */ jsxs("div", { className: "relative group", children: [
247
+ /* @__PURE__ */ jsx(
248
+ "img",
249
+ {
250
+ src: thumbnail.startsWith("http") ? thumbnail : `http://localhost:9000${thumbnail}`,
251
+ alt: data.title,
252
+ className: "w-full h-48 object-cover rounded-lg"
253
+ }
254
+ ),
255
+ /* @__PURE__ */ jsxs("div", { className: "absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity rounded-lg flex items-center justify-center gap-2", children: [
256
+ /* @__PURE__ */ jsxs(
257
+ Button,
258
+ {
259
+ variant: "secondary",
260
+ size: "small",
261
+ onClick: () => setShowUploader(true),
262
+ children: [
263
+ /* @__PURE__ */ jsx(CloudArrowUp, {}),
264
+ "Replace"
265
+ ]
266
+ }
267
+ ),
268
+ /* @__PURE__ */ jsxs(
269
+ Button,
270
+ {
271
+ variant: "danger",
272
+ size: "small",
273
+ onClick: handleDeleteThumbnail,
274
+ children: [
275
+ /* @__PURE__ */ jsx(Trash, {}),
276
+ "Delete"
277
+ ]
278
+ }
279
+ )
280
+ ] })
281
+ ] }) }) : /* @__PURE__ */ jsx("div", { className: "border-2 border-dashed border-ui-border-base rounded-lg p-8", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center text-center", children: [
282
+ /* @__PURE__ */ jsx(PhotoSolid, { className: "h-12 w-12 text-ui-fg-subtle mb-4" }),
283
+ /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-subtle mb-4", children: "No thumbnail uploaded for this collection" }),
284
+ /* @__PURE__ */ jsxs(Button, { onClick: () => setShowUploader(true), children: [
285
+ /* @__PURE__ */ jsx(CloudArrowUp, { className: "mr-2" }),
286
+ "Upload Thumbnail"
287
+ ] })
288
+ ] }) })
289
+ ] }),
290
+ showUploader && /* @__PURE__ */ jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/50", children: /* @__PURE__ */ jsxs("div", { className: "w-full max-w-2xl rounded-lg bg-ui-bg-base p-6", children: [
291
+ /* @__PURE__ */ jsxs("div", { className: "mb-4 flex items-center justify-between", children: [
292
+ /* @__PURE__ */ jsxs("div", { children: [
293
+ /* @__PURE__ */ jsx(Heading, { level: "h2", children: "Upload Thumbnail" }),
294
+ /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-subtle", children: data.title })
295
+ ] }),
296
+ /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: handleUploaderClose, children: "Close" })
297
+ ] }),
298
+ /* @__PURE__ */ jsx(
299
+ CollectionThumbnailUploader,
300
+ {
301
+ collectionId: data.id,
302
+ initialThumbnail: thumbnail || void 0,
303
+ onThumbnailChange: (url) => {
304
+ if (!url) {
305
+ handleUploaderClose();
306
+ }
307
+ }
308
+ }
309
+ )
310
+ ] }) })
311
+ ] });
312
+ };
313
+ defineWidgetConfig({
314
+ zone: "product_collection.details.after"
315
+ });
316
+ const widgetModule = { widgets: [
317
+ {
318
+ Component: CollectionThumbnailWidget,
319
+ zone: ["product_collection.details.after"]
320
+ }
321
+ ] };
322
+ const routeModule = {
323
+ routes: []
324
+ };
325
+ const menuItemModule = {
326
+ menuItems: []
327
+ };
328
+ const formModule = { customFields: {} };
329
+ const displayModule = {
330
+ displays: {}
331
+ };
332
+ const i18nModule = { resources: {} };
333
+ const plugin = {
334
+ widgetModule,
335
+ routeModule,
336
+ menuItemModule,
337
+ formModule,
338
+ displayModule,
339
+ i18nModule
340
+ };
341
+ export {
342
+ plugin as default
343
+ };