@lodashventure/medusa-collection-thumbnail 1.0.11 → 1.0.13
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.
|
@@ -0,0 +1,517 @@
|
|
|
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 CollectionThumbnailsPage = () => {
|
|
318
|
+
const [collections, setCollections] = react.useState([]);
|
|
319
|
+
const [loading, setLoading] = react.useState(true);
|
|
320
|
+
const [error, setError] = react.useState(null);
|
|
321
|
+
const [selectedCollection, setSelectedCollection] = react.useState(null);
|
|
322
|
+
const [showUploader, setShowUploader] = react.useState(false);
|
|
323
|
+
react.useEffect(() => {
|
|
324
|
+
fetchCollections();
|
|
325
|
+
}, []);
|
|
326
|
+
const fetchCollections = async () => {
|
|
327
|
+
setLoading(true);
|
|
328
|
+
setError(null);
|
|
329
|
+
try {
|
|
330
|
+
const response = await fetch("/admin/collections?limit=100", {
|
|
331
|
+
credentials: "include"
|
|
332
|
+
});
|
|
333
|
+
if (response.ok) {
|
|
334
|
+
const data = await response.json();
|
|
335
|
+
const collectionsData = data.collections || [];
|
|
336
|
+
const collectionsWithThumbnails = await Promise.all(
|
|
337
|
+
collectionsData.map(async (collection) => {
|
|
338
|
+
try {
|
|
339
|
+
const thumbResponse = await fetch(
|
|
340
|
+
`/admin/collections/${collection.id}/thumbnail`,
|
|
341
|
+
{
|
|
342
|
+
credentials: "include"
|
|
343
|
+
}
|
|
344
|
+
);
|
|
345
|
+
if (thumbResponse.ok) {
|
|
346
|
+
const thumbData = await thumbResponse.json();
|
|
347
|
+
return { ...collection, thumbnail: thumbData.thumbnail };
|
|
348
|
+
}
|
|
349
|
+
} catch (err) {
|
|
350
|
+
}
|
|
351
|
+
return collection;
|
|
352
|
+
})
|
|
353
|
+
);
|
|
354
|
+
setCollections(collectionsWithThumbnails);
|
|
355
|
+
} else {
|
|
356
|
+
setCollections([]);
|
|
357
|
+
}
|
|
358
|
+
} catch (err) {
|
|
359
|
+
setCollections([]);
|
|
360
|
+
console.log("Collections API not available, showing demo mode");
|
|
361
|
+
} finally {
|
|
362
|
+
setLoading(false);
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
const handleUploadClick = (collection) => {
|
|
366
|
+
setSelectedCollection(collection);
|
|
367
|
+
setShowUploader(true);
|
|
368
|
+
};
|
|
369
|
+
const handleDeleteThumbnail = async (collectionId) => {
|
|
370
|
+
if (!confirm("Are you sure you want to delete this thumbnail?")) return;
|
|
371
|
+
try {
|
|
372
|
+
const response = await fetch(
|
|
373
|
+
`/admin/collections/${collectionId}/thumbnail`,
|
|
374
|
+
{
|
|
375
|
+
method: "DELETE",
|
|
376
|
+
credentials: "include"
|
|
377
|
+
}
|
|
378
|
+
);
|
|
379
|
+
if (response.ok) {
|
|
380
|
+
fetchCollections();
|
|
381
|
+
} else {
|
|
382
|
+
alert("Failed to delete thumbnail");
|
|
383
|
+
}
|
|
384
|
+
} catch (err) {
|
|
385
|
+
alert("Error deleting thumbnail");
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
const handleUploaderClose = () => {
|
|
389
|
+
setShowUploader(false);
|
|
390
|
+
setSelectedCollection(null);
|
|
391
|
+
fetchCollections();
|
|
392
|
+
};
|
|
393
|
+
if (loading) {
|
|
394
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-64 items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: "Loading collections..." }) }) });
|
|
395
|
+
}
|
|
396
|
+
if (error) {
|
|
397
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "error", dismissible: true, children: error }) });
|
|
398
|
+
}
|
|
399
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { children: [
|
|
400
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-8", children: [
|
|
401
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", className: "mb-2", children: "Collection Thumbnails" }),
|
|
402
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle", children: "Manage thumbnail images for your product collections" })
|
|
403
|
+
] }),
|
|
404
|
+
collections.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex h-64 flex-col items-center justify-center rounded-lg border-2 border-dashed border-ui-border-base", children: [
|
|
405
|
+
/* @__PURE__ */ jsxRuntime.jsx(icons.PhotoSolid, { className: "mb-4 h-12 w-12 text-ui-fg-subtle" }),
|
|
406
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "mb-2 text-lg font-medium", children: "No collections found" }),
|
|
407
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle", children: "Create collections first to manage their thumbnails" })
|
|
408
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-hidden rounded-lg border", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table, { children: [
|
|
409
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
|
|
410
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Thumbnail" }),
|
|
411
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Collection" }),
|
|
412
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Handle" }),
|
|
413
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Status" }),
|
|
414
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { className: "text-right", children: "Actions" })
|
|
415
|
+
] }) }),
|
|
416
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Body, { children: collections.map((collection) => /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
|
|
417
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: collection.thumbnail ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
418
|
+
"img",
|
|
419
|
+
{
|
|
420
|
+
src: collection.thumbnail.startsWith("http") ? collection.thumbnail : `http://localhost:9000${collection.thumbnail}`,
|
|
421
|
+
alt: collection.title,
|
|
422
|
+
className: "h-12 w-12 object-cover rounded"
|
|
423
|
+
}
|
|
424
|
+
) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-12 w-12 items-center justify-center rounded bg-ui-bg-subtle", children: /* @__PURE__ */ jsxRuntime.jsx(icons.PhotoSolid, { className: "h-6 w-6 text-ui-fg-subtle" }) }) }),
|
|
425
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "font-medium", children: collection.title }) }),
|
|
426
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { color: "blue", children: collection.handle }) }),
|
|
427
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: collection.thumbnail ? /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { color: "green", children: "Has Thumbnail" }) : /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { color: "grey", children: "No Thumbnail" }) }),
|
|
428
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-end gap-2", children: [
|
|
429
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
430
|
+
ui.Button,
|
|
431
|
+
{
|
|
432
|
+
variant: "secondary",
|
|
433
|
+
size: "small",
|
|
434
|
+
onClick: () => handleUploadClick(collection),
|
|
435
|
+
children: [
|
|
436
|
+
/* @__PURE__ */ jsxRuntime.jsx(icons.CloudArrowUp, { className: "mr-1" }),
|
|
437
|
+
collection.thumbnail ? "Replace" : "Upload"
|
|
438
|
+
]
|
|
439
|
+
}
|
|
440
|
+
),
|
|
441
|
+
collection.thumbnail && /* @__PURE__ */ jsxRuntime.jsx(
|
|
442
|
+
ui.Button,
|
|
443
|
+
{
|
|
444
|
+
variant: "danger",
|
|
445
|
+
size: "small",
|
|
446
|
+
onClick: () => handleDeleteThumbnail(collection.id),
|
|
447
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(icons.Trash, {})
|
|
448
|
+
}
|
|
449
|
+
)
|
|
450
|
+
] }) })
|
|
451
|
+
] }, collection.id)) })
|
|
452
|
+
] }) }),
|
|
453
|
+
showUploader && selectedCollection && /* @__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: [
|
|
454
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-4 flex items-center justify-between", children: [
|
|
455
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
456
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: "Upload Thumbnail" }),
|
|
457
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle", children: selectedCollection.title })
|
|
458
|
+
] }),
|
|
459
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", onClick: handleUploaderClose, children: "Close" })
|
|
460
|
+
] }),
|
|
461
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
462
|
+
CollectionThumbnailUploader,
|
|
463
|
+
{
|
|
464
|
+
collectionId: selectedCollection.id,
|
|
465
|
+
initialThumbnail: selectedCollection.thumbnail,
|
|
466
|
+
onThumbnailChange: (url) => {
|
|
467
|
+
if (!url) {
|
|
468
|
+
handleUploaderClose();
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
)
|
|
473
|
+
] }) })
|
|
474
|
+
] });
|
|
475
|
+
};
|
|
476
|
+
const config = adminSdk.defineRouteConfig({
|
|
477
|
+
label: "Collection Thumbnails",
|
|
478
|
+
icon: icons.PhotoSolid
|
|
479
|
+
});
|
|
480
|
+
const widgetModule = { widgets: [
|
|
481
|
+
{
|
|
482
|
+
Component: CollectionThumbnailWidget,
|
|
483
|
+
zone: ["product_collection.details.after"]
|
|
484
|
+
}
|
|
485
|
+
] };
|
|
486
|
+
const routeModule = {
|
|
487
|
+
routes: [
|
|
488
|
+
{
|
|
489
|
+
Component: CollectionThumbnailsPage,
|
|
490
|
+
path: "/collection-thumbnails"
|
|
491
|
+
}
|
|
492
|
+
]
|
|
493
|
+
};
|
|
494
|
+
const menuItemModule = {
|
|
495
|
+
menuItems: [
|
|
496
|
+
{
|
|
497
|
+
label: config.label,
|
|
498
|
+
icon: config.icon,
|
|
499
|
+
path: "/collection-thumbnails",
|
|
500
|
+
nested: void 0
|
|
501
|
+
}
|
|
502
|
+
]
|
|
503
|
+
};
|
|
504
|
+
const formModule = { customFields: {} };
|
|
505
|
+
const displayModule = {
|
|
506
|
+
displays: {}
|
|
507
|
+
};
|
|
508
|
+
const i18nModule = { resources: {} };
|
|
509
|
+
const plugin = {
|
|
510
|
+
widgetModule,
|
|
511
|
+
routeModule,
|
|
512
|
+
menuItemModule,
|
|
513
|
+
formModule,
|
|
514
|
+
displayModule,
|
|
515
|
+
i18nModule
|
|
516
|
+
};
|
|
517
|
+
module.exports = plugin;
|
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
import { jsxs, jsx, Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { defineWidgetConfig, defineRouteConfig } from "@medusajs/admin-sdk";
|
|
3
|
+
import { clx, Button, Text, Container, Heading, Alert, Table, Badge } 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 CollectionThumbnailsPage = () => {
|
|
317
|
+
const [collections, setCollections] = useState([]);
|
|
318
|
+
const [loading, setLoading] = useState(true);
|
|
319
|
+
const [error, setError] = useState(null);
|
|
320
|
+
const [selectedCollection, setSelectedCollection] = useState(null);
|
|
321
|
+
const [showUploader, setShowUploader] = useState(false);
|
|
322
|
+
useEffect(() => {
|
|
323
|
+
fetchCollections();
|
|
324
|
+
}, []);
|
|
325
|
+
const fetchCollections = async () => {
|
|
326
|
+
setLoading(true);
|
|
327
|
+
setError(null);
|
|
328
|
+
try {
|
|
329
|
+
const response = await fetch("/admin/collections?limit=100", {
|
|
330
|
+
credentials: "include"
|
|
331
|
+
});
|
|
332
|
+
if (response.ok) {
|
|
333
|
+
const data = await response.json();
|
|
334
|
+
const collectionsData = data.collections || [];
|
|
335
|
+
const collectionsWithThumbnails = await Promise.all(
|
|
336
|
+
collectionsData.map(async (collection) => {
|
|
337
|
+
try {
|
|
338
|
+
const thumbResponse = await fetch(
|
|
339
|
+
`/admin/collections/${collection.id}/thumbnail`,
|
|
340
|
+
{
|
|
341
|
+
credentials: "include"
|
|
342
|
+
}
|
|
343
|
+
);
|
|
344
|
+
if (thumbResponse.ok) {
|
|
345
|
+
const thumbData = await thumbResponse.json();
|
|
346
|
+
return { ...collection, thumbnail: thumbData.thumbnail };
|
|
347
|
+
}
|
|
348
|
+
} catch (err) {
|
|
349
|
+
}
|
|
350
|
+
return collection;
|
|
351
|
+
})
|
|
352
|
+
);
|
|
353
|
+
setCollections(collectionsWithThumbnails);
|
|
354
|
+
} else {
|
|
355
|
+
setCollections([]);
|
|
356
|
+
}
|
|
357
|
+
} catch (err) {
|
|
358
|
+
setCollections([]);
|
|
359
|
+
console.log("Collections API not available, showing demo mode");
|
|
360
|
+
} finally {
|
|
361
|
+
setLoading(false);
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
const handleUploadClick = (collection) => {
|
|
365
|
+
setSelectedCollection(collection);
|
|
366
|
+
setShowUploader(true);
|
|
367
|
+
};
|
|
368
|
+
const handleDeleteThumbnail = async (collectionId) => {
|
|
369
|
+
if (!confirm("Are you sure you want to delete this thumbnail?")) return;
|
|
370
|
+
try {
|
|
371
|
+
const response = await fetch(
|
|
372
|
+
`/admin/collections/${collectionId}/thumbnail`,
|
|
373
|
+
{
|
|
374
|
+
method: "DELETE",
|
|
375
|
+
credentials: "include"
|
|
376
|
+
}
|
|
377
|
+
);
|
|
378
|
+
if (response.ok) {
|
|
379
|
+
fetchCollections();
|
|
380
|
+
} else {
|
|
381
|
+
alert("Failed to delete thumbnail");
|
|
382
|
+
}
|
|
383
|
+
} catch (err) {
|
|
384
|
+
alert("Error deleting thumbnail");
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
const handleUploaderClose = () => {
|
|
388
|
+
setShowUploader(false);
|
|
389
|
+
setSelectedCollection(null);
|
|
390
|
+
fetchCollections();
|
|
391
|
+
};
|
|
392
|
+
if (loading) {
|
|
393
|
+
return /* @__PURE__ */ jsx(Container, { children: /* @__PURE__ */ jsx("div", { className: "flex h-64 items-center justify-center", children: /* @__PURE__ */ jsx(Text, { children: "Loading collections..." }) }) });
|
|
394
|
+
}
|
|
395
|
+
if (error) {
|
|
396
|
+
return /* @__PURE__ */ jsx(Container, { children: /* @__PURE__ */ jsx(Alert, { variant: "error", dismissible: true, children: error }) });
|
|
397
|
+
}
|
|
398
|
+
return /* @__PURE__ */ jsxs(Container, { children: [
|
|
399
|
+
/* @__PURE__ */ jsxs("div", { className: "mb-8", children: [
|
|
400
|
+
/* @__PURE__ */ jsx(Heading, { level: "h1", className: "mb-2", children: "Collection Thumbnails" }),
|
|
401
|
+
/* @__PURE__ */ jsx(Text, { className: "text-ui-fg-subtle", children: "Manage thumbnail images for your product collections" })
|
|
402
|
+
] }),
|
|
403
|
+
collections.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "flex h-64 flex-col items-center justify-center rounded-lg border-2 border-dashed border-ui-border-base", children: [
|
|
404
|
+
/* @__PURE__ */ jsx(PhotoSolid, { className: "mb-4 h-12 w-12 text-ui-fg-subtle" }),
|
|
405
|
+
/* @__PURE__ */ jsx(Text, { className: "mb-2 text-lg font-medium", children: "No collections found" }),
|
|
406
|
+
/* @__PURE__ */ jsx(Text, { className: "text-ui-fg-subtle", children: "Create collections first to manage their thumbnails" })
|
|
407
|
+
] }) : /* @__PURE__ */ jsx("div", { className: "overflow-hidden rounded-lg border", children: /* @__PURE__ */ jsxs(Table, { children: [
|
|
408
|
+
/* @__PURE__ */ jsx(Table.Header, { children: /* @__PURE__ */ jsxs(Table.Row, { children: [
|
|
409
|
+
/* @__PURE__ */ jsx(Table.HeaderCell, { children: "Thumbnail" }),
|
|
410
|
+
/* @__PURE__ */ jsx(Table.HeaderCell, { children: "Collection" }),
|
|
411
|
+
/* @__PURE__ */ jsx(Table.HeaderCell, { children: "Handle" }),
|
|
412
|
+
/* @__PURE__ */ jsx(Table.HeaderCell, { children: "Status" }),
|
|
413
|
+
/* @__PURE__ */ jsx(Table.HeaderCell, { className: "text-right", children: "Actions" })
|
|
414
|
+
] }) }),
|
|
415
|
+
/* @__PURE__ */ jsx(Table.Body, { children: collections.map((collection) => /* @__PURE__ */ jsxs(Table.Row, { children: [
|
|
416
|
+
/* @__PURE__ */ jsx(Table.Cell, { children: collection.thumbnail ? /* @__PURE__ */ jsx(
|
|
417
|
+
"img",
|
|
418
|
+
{
|
|
419
|
+
src: collection.thumbnail.startsWith("http") ? collection.thumbnail : `http://localhost:9000${collection.thumbnail}`,
|
|
420
|
+
alt: collection.title,
|
|
421
|
+
className: "h-12 w-12 object-cover rounded"
|
|
422
|
+
}
|
|
423
|
+
) : /* @__PURE__ */ jsx("div", { className: "flex h-12 w-12 items-center justify-center rounded bg-ui-bg-subtle", children: /* @__PURE__ */ jsx(PhotoSolid, { className: "h-6 w-6 text-ui-fg-subtle" }) }) }),
|
|
424
|
+
/* @__PURE__ */ jsx(Table.Cell, { children: /* @__PURE__ */ jsx(Text, { className: "font-medium", children: collection.title }) }),
|
|
425
|
+
/* @__PURE__ */ jsx(Table.Cell, { children: /* @__PURE__ */ jsx(Badge, { color: "blue", children: collection.handle }) }),
|
|
426
|
+
/* @__PURE__ */ jsx(Table.Cell, { children: collection.thumbnail ? /* @__PURE__ */ jsx(Badge, { color: "green", children: "Has Thumbnail" }) : /* @__PURE__ */ jsx(Badge, { color: "grey", children: "No Thumbnail" }) }),
|
|
427
|
+
/* @__PURE__ */ jsx(Table.Cell, { children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-end gap-2", children: [
|
|
428
|
+
/* @__PURE__ */ jsxs(
|
|
429
|
+
Button,
|
|
430
|
+
{
|
|
431
|
+
variant: "secondary",
|
|
432
|
+
size: "small",
|
|
433
|
+
onClick: () => handleUploadClick(collection),
|
|
434
|
+
children: [
|
|
435
|
+
/* @__PURE__ */ jsx(CloudArrowUp, { className: "mr-1" }),
|
|
436
|
+
collection.thumbnail ? "Replace" : "Upload"
|
|
437
|
+
]
|
|
438
|
+
}
|
|
439
|
+
),
|
|
440
|
+
collection.thumbnail && /* @__PURE__ */ jsx(
|
|
441
|
+
Button,
|
|
442
|
+
{
|
|
443
|
+
variant: "danger",
|
|
444
|
+
size: "small",
|
|
445
|
+
onClick: () => handleDeleteThumbnail(collection.id),
|
|
446
|
+
children: /* @__PURE__ */ jsx(Trash, {})
|
|
447
|
+
}
|
|
448
|
+
)
|
|
449
|
+
] }) })
|
|
450
|
+
] }, collection.id)) })
|
|
451
|
+
] }) }),
|
|
452
|
+
showUploader && selectedCollection && /* @__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: [
|
|
453
|
+
/* @__PURE__ */ jsxs("div", { className: "mb-4 flex items-center justify-between", children: [
|
|
454
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
455
|
+
/* @__PURE__ */ jsx(Heading, { level: "h2", children: "Upload Thumbnail" }),
|
|
456
|
+
/* @__PURE__ */ jsx(Text, { className: "text-ui-fg-subtle", children: selectedCollection.title })
|
|
457
|
+
] }),
|
|
458
|
+
/* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: handleUploaderClose, children: "Close" })
|
|
459
|
+
] }),
|
|
460
|
+
/* @__PURE__ */ jsx(
|
|
461
|
+
CollectionThumbnailUploader,
|
|
462
|
+
{
|
|
463
|
+
collectionId: selectedCollection.id,
|
|
464
|
+
initialThumbnail: selectedCollection.thumbnail,
|
|
465
|
+
onThumbnailChange: (url) => {
|
|
466
|
+
if (!url) {
|
|
467
|
+
handleUploaderClose();
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
)
|
|
472
|
+
] }) })
|
|
473
|
+
] });
|
|
474
|
+
};
|
|
475
|
+
const config = defineRouteConfig({
|
|
476
|
+
label: "Collection Thumbnails",
|
|
477
|
+
icon: PhotoSolid
|
|
478
|
+
});
|
|
479
|
+
const widgetModule = { widgets: [
|
|
480
|
+
{
|
|
481
|
+
Component: CollectionThumbnailWidget,
|
|
482
|
+
zone: ["product_collection.details.after"]
|
|
483
|
+
}
|
|
484
|
+
] };
|
|
485
|
+
const routeModule = {
|
|
486
|
+
routes: [
|
|
487
|
+
{
|
|
488
|
+
Component: CollectionThumbnailsPage,
|
|
489
|
+
path: "/collection-thumbnails"
|
|
490
|
+
}
|
|
491
|
+
]
|
|
492
|
+
};
|
|
493
|
+
const menuItemModule = {
|
|
494
|
+
menuItems: [
|
|
495
|
+
{
|
|
496
|
+
label: config.label,
|
|
497
|
+
icon: config.icon,
|
|
498
|
+
path: "/collection-thumbnails",
|
|
499
|
+
nested: void 0
|
|
500
|
+
}
|
|
501
|
+
]
|
|
502
|
+
};
|
|
503
|
+
const formModule = { customFields: {} };
|
|
504
|
+
const displayModule = {
|
|
505
|
+
displays: {}
|
|
506
|
+
};
|
|
507
|
+
const i18nModule = { resources: {} };
|
|
508
|
+
const plugin = {
|
|
509
|
+
widgetModule,
|
|
510
|
+
routeModule,
|
|
511
|
+
menuItemModule,
|
|
512
|
+
formModule,
|
|
513
|
+
displayModule,
|
|
514
|
+
i18nModule
|
|
515
|
+
};
|
|
516
|
+
export {
|
|
517
|
+
plugin as default
|
|
518
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lodashventure/medusa-collection-thumbnail",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.13",
|
|
4
4
|
"description": "Collection thumbnail management plugin for Medusa v2",
|
|
5
5
|
"author": "Lodashventure",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
},
|
|
29
29
|
"scripts": {
|
|
30
30
|
"build": "medusa plugin:build",
|
|
31
|
-
"dev": "medusa plugin:develop"
|
|
31
|
+
"dev": "medusa plugin:develop",
|
|
32
|
+
"prepublishOnly": "medusa plugin:build"
|
|
32
33
|
},
|
|
33
34
|
"dependencies": {
|
|
34
35
|
"@google-cloud/storage": "^7.17.2",
|
|
@@ -47,6 +48,7 @@
|
|
|
47
48
|
"react-dom": "^18.0.0"
|
|
48
49
|
},
|
|
49
50
|
"devDependencies": {
|
|
51
|
+
"@medusajs/admin-shared": "2.11.2",
|
|
50
52
|
"@medusajs/types": "2.11.2",
|
|
51
53
|
"@types/multer": "^2.0.0",
|
|
52
54
|
"@types/node": "^20.19.23",
|