@lodashventure/medusa-hero 1.0.12 → 1.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.medusa/server/src/admin/index.js +1147 -0
- package/.medusa/server/src/admin/index.mjs +1145 -0
- package/package.json +8 -2
|
@@ -0,0 +1,1147 @@
|
|
|
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 react = require("react");
|
|
6
|
+
const reactDropzone = require("react-dropzone");
|
|
7
|
+
const icons = require("@medusajs/icons");
|
|
8
|
+
const core = require("@dnd-kit/core");
|
|
9
|
+
const sortable = require("@dnd-kit/sortable");
|
|
10
|
+
const lucideReact = require("lucide-react");
|
|
11
|
+
const Medusa = require("@medusajs/js-sdk");
|
|
12
|
+
const utilities = require("@dnd-kit/utilities");
|
|
13
|
+
const clx = require("clsx");
|
|
14
|
+
require("@medusajs/admin-shared");
|
|
15
|
+
const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
|
|
16
|
+
const Medusa__default = /* @__PURE__ */ _interopDefault(Medusa);
|
|
17
|
+
const clx__default = /* @__PURE__ */ _interopDefault(clx);
|
|
18
|
+
const CategoryHeroWidget = ({ data }) => {
|
|
19
|
+
var _a;
|
|
20
|
+
const categoryId = data == null ? void 0 : data.id;
|
|
21
|
+
const [hero, setHero] = react.useState(
|
|
22
|
+
((_a = data == null ? void 0 : data.metadata) == null ? void 0 : _a.hero) || null
|
|
23
|
+
);
|
|
24
|
+
const [uploading, setUploading] = react.useState(false);
|
|
25
|
+
const onDrop = react.useCallback(
|
|
26
|
+
async (acceptedFiles) => {
|
|
27
|
+
if (!acceptedFiles || acceptedFiles.length === 0) return;
|
|
28
|
+
const file = acceptedFiles[0];
|
|
29
|
+
const formData = new FormData();
|
|
30
|
+
formData.append("file", file);
|
|
31
|
+
setUploading(true);
|
|
32
|
+
try {
|
|
33
|
+
const response = await fetch(
|
|
34
|
+
`/admin/categories/${categoryId}/hero`,
|
|
35
|
+
{
|
|
36
|
+
method: "POST",
|
|
37
|
+
body: formData
|
|
38
|
+
}
|
|
39
|
+
);
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
throw new Error("Failed to upload hero image");
|
|
42
|
+
}
|
|
43
|
+
const result = await response.json();
|
|
44
|
+
setHero(result.category.hero);
|
|
45
|
+
ui.toast.success("Hero Image", {
|
|
46
|
+
description: "Hero image uploaded successfully",
|
|
47
|
+
duration: 3e3
|
|
48
|
+
});
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error("Upload error:", error);
|
|
51
|
+
ui.toast.error("Error", {
|
|
52
|
+
description: "Failed to upload hero image",
|
|
53
|
+
duration: 3e3
|
|
54
|
+
});
|
|
55
|
+
} finally {
|
|
56
|
+
setUploading(false);
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
[categoryId]
|
|
60
|
+
);
|
|
61
|
+
const handleDelete = async () => {
|
|
62
|
+
if (!confirm("Are you sure you want to delete this hero image?")) return;
|
|
63
|
+
try {
|
|
64
|
+
const response = await fetch(
|
|
65
|
+
`/admin/categories/${categoryId}/hero`,
|
|
66
|
+
{
|
|
67
|
+
method: "DELETE"
|
|
68
|
+
}
|
|
69
|
+
);
|
|
70
|
+
if (!response.ok) {
|
|
71
|
+
throw new Error("Failed to delete hero image");
|
|
72
|
+
}
|
|
73
|
+
setHero(null);
|
|
74
|
+
ui.toast.success("Hero Image", {
|
|
75
|
+
description: "Hero image deleted successfully",
|
|
76
|
+
duration: 3e3
|
|
77
|
+
});
|
|
78
|
+
} catch (error) {
|
|
79
|
+
console.error("Delete error:", error);
|
|
80
|
+
ui.toast.error("Error", {
|
|
81
|
+
description: "Failed to delete hero image",
|
|
82
|
+
duration: 3e3
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
const { getRootProps, getInputProps, isDragActive } = reactDropzone.useDropzone({
|
|
87
|
+
onDrop,
|
|
88
|
+
accept: {
|
|
89
|
+
"image/*": [".png", ".jpg", ".jpeg", ".gif", ".webp"]
|
|
90
|
+
},
|
|
91
|
+
maxFiles: 1,
|
|
92
|
+
disabled: uploading
|
|
93
|
+
});
|
|
94
|
+
let dropzoneMessage = "Drag & drop a hero image here, or click to select";
|
|
95
|
+
if (uploading) {
|
|
96
|
+
dropzoneMessage = "Uploading...";
|
|
97
|
+
} else if (isDragActive) {
|
|
98
|
+
dropzoneMessage = "Drop the image here";
|
|
99
|
+
}
|
|
100
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
|
|
101
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-between px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: "Category Hero Image" }) }),
|
|
102
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: hero ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
|
|
103
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "relative w-full h-64 border border-ui-border-base rounded-lg overflow-hidden", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
104
|
+
"img",
|
|
105
|
+
{
|
|
106
|
+
src: hero,
|
|
107
|
+
alt: "Category hero",
|
|
108
|
+
className: "w-full h-full object-cover"
|
|
109
|
+
}
|
|
110
|
+
) }),
|
|
111
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
112
|
+
ui.Button,
|
|
113
|
+
{
|
|
114
|
+
variant: "danger",
|
|
115
|
+
onClick: handleDelete,
|
|
116
|
+
className: "w-full",
|
|
117
|
+
children: [
|
|
118
|
+
/* @__PURE__ */ jsxRuntime.jsx(icons.Trash, { className: "mr-2" }),
|
|
119
|
+
"Delete Hero Image"
|
|
120
|
+
]
|
|
121
|
+
}
|
|
122
|
+
)
|
|
123
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs(
|
|
124
|
+
"div",
|
|
125
|
+
{
|
|
126
|
+
...getRootProps(),
|
|
127
|
+
className: `
|
|
128
|
+
border-2 border-dashed rounded-lg p-8 text-center cursor-pointer
|
|
129
|
+
transition-colors
|
|
130
|
+
${isDragActive ? "border-ui-fg-interactive bg-ui-bg-interactive" : "border-ui-border-base hover:border-ui-fg-interactive"}
|
|
131
|
+
${uploading ? "opacity-50 cursor-not-allowed" : ""}
|
|
132
|
+
`,
|
|
133
|
+
children: [
|
|
134
|
+
/* @__PURE__ */ jsxRuntime.jsx("input", { ...getInputProps() }),
|
|
135
|
+
/* @__PURE__ */ jsxRuntime.jsx(icons.CloudArrowUp, { className: "mx-auto mb-4 text-ui-fg-muted" }),
|
|
136
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-ui-fg-subtle", children: dropzoneMessage }),
|
|
137
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-ui-fg-muted mt-2", children: "Supported formats: PNG, JPG, JPEG, GIF, WebP" }),
|
|
138
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-ui-fg-muted", children: "Recommended size: 1920x600 pixels" })
|
|
139
|
+
]
|
|
140
|
+
}
|
|
141
|
+
) })
|
|
142
|
+
] });
|
|
143
|
+
};
|
|
144
|
+
adminSdk.defineWidgetConfig({
|
|
145
|
+
zone: "product_category.details.after"
|
|
146
|
+
});
|
|
147
|
+
const CategoryThumbnailWidget = ({ data }) => {
|
|
148
|
+
var _a;
|
|
149
|
+
const categoryId = data == null ? void 0 : data.id;
|
|
150
|
+
const [thumbnail, setThumbnail] = react.useState(
|
|
151
|
+
((_a = data == null ? void 0 : data.metadata) == null ? void 0 : _a.thumbnail) || null
|
|
152
|
+
);
|
|
153
|
+
const [uploading, setUploading] = react.useState(false);
|
|
154
|
+
const onDrop = react.useCallback(
|
|
155
|
+
async (acceptedFiles) => {
|
|
156
|
+
if (!acceptedFiles || acceptedFiles.length === 0) return;
|
|
157
|
+
const file = acceptedFiles[0];
|
|
158
|
+
const formData = new FormData();
|
|
159
|
+
formData.append("file", file);
|
|
160
|
+
setUploading(true);
|
|
161
|
+
try {
|
|
162
|
+
const response = await fetch(
|
|
163
|
+
`/admin/categories/${categoryId}/thumbnail`,
|
|
164
|
+
{
|
|
165
|
+
method: "POST",
|
|
166
|
+
body: formData
|
|
167
|
+
}
|
|
168
|
+
);
|
|
169
|
+
if (!response.ok) {
|
|
170
|
+
throw new Error("Failed to upload thumbnail");
|
|
171
|
+
}
|
|
172
|
+
const result = await response.json();
|
|
173
|
+
setThumbnail(result.category.thumbnail);
|
|
174
|
+
ui.toast.success("Thumbnail", {
|
|
175
|
+
description: "Thumbnail uploaded successfully",
|
|
176
|
+
duration: 3e3
|
|
177
|
+
});
|
|
178
|
+
} catch (error) {
|
|
179
|
+
console.error("Upload error:", error);
|
|
180
|
+
ui.toast.error("Error", {
|
|
181
|
+
description: "Failed to upload thumbnail",
|
|
182
|
+
duration: 3e3
|
|
183
|
+
});
|
|
184
|
+
} finally {
|
|
185
|
+
setUploading(false);
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
[categoryId]
|
|
189
|
+
);
|
|
190
|
+
const handleDelete = async () => {
|
|
191
|
+
if (!confirm("Are you sure you want to delete this thumbnail?")) return;
|
|
192
|
+
try {
|
|
193
|
+
const response = await fetch(
|
|
194
|
+
`/admin/categories/${categoryId}/thumbnail`,
|
|
195
|
+
{
|
|
196
|
+
method: "DELETE"
|
|
197
|
+
}
|
|
198
|
+
);
|
|
199
|
+
if (!response.ok) {
|
|
200
|
+
throw new Error("Failed to delete thumbnail");
|
|
201
|
+
}
|
|
202
|
+
setThumbnail(null);
|
|
203
|
+
ui.toast.success("Thumbnail", {
|
|
204
|
+
description: "Thumbnail deleted successfully",
|
|
205
|
+
duration: 3e3
|
|
206
|
+
});
|
|
207
|
+
} catch (error) {
|
|
208
|
+
console.error("Delete error:", error);
|
|
209
|
+
ui.toast.error("Error", {
|
|
210
|
+
description: "Failed to delete thumbnail",
|
|
211
|
+
duration: 3e3
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
const { getRootProps, getInputProps, isDragActive } = reactDropzone.useDropzone({
|
|
216
|
+
onDrop,
|
|
217
|
+
accept: {
|
|
218
|
+
"image/*": [".png", ".jpg", ".jpeg", ".gif", ".webp"]
|
|
219
|
+
},
|
|
220
|
+
maxFiles: 1,
|
|
221
|
+
disabled: uploading
|
|
222
|
+
});
|
|
223
|
+
let dropzoneMessage = "Drag & drop an image here, or click to select";
|
|
224
|
+
if (uploading) {
|
|
225
|
+
dropzoneMessage = "Uploading...";
|
|
226
|
+
} else if (isDragActive) {
|
|
227
|
+
dropzoneMessage = "Drop the image here";
|
|
228
|
+
}
|
|
229
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
|
|
230
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-between px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: "Category Thumbnail" }) }),
|
|
231
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: thumbnail ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
|
|
232
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "relative w-full h-48 border border-ui-border-base rounded-lg overflow-hidden", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
233
|
+
"img",
|
|
234
|
+
{
|
|
235
|
+
src: thumbnail,
|
|
236
|
+
alt: "Category thumbnail",
|
|
237
|
+
className: "w-full h-full object-cover"
|
|
238
|
+
}
|
|
239
|
+
) }),
|
|
240
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { variant: "danger", onClick: handleDelete, className: "w-full", children: [
|
|
241
|
+
/* @__PURE__ */ jsxRuntime.jsx(icons.Trash, { className: "mr-2" }),
|
|
242
|
+
"Delete Thumbnail"
|
|
243
|
+
] })
|
|
244
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs(
|
|
245
|
+
"div",
|
|
246
|
+
{
|
|
247
|
+
...getRootProps(),
|
|
248
|
+
className: `
|
|
249
|
+
border-2 border-dashed rounded-lg p-8 text-center cursor-pointer
|
|
250
|
+
transition-colors
|
|
251
|
+
${isDragActive ? "border-ui-fg-interactive bg-ui-bg-interactive" : "border-ui-border-base hover:border-ui-fg-interactive"}
|
|
252
|
+
${uploading ? "opacity-50 cursor-not-allowed" : ""}
|
|
253
|
+
`,
|
|
254
|
+
children: [
|
|
255
|
+
/* @__PURE__ */ jsxRuntime.jsx("input", { ...getInputProps() }),
|
|
256
|
+
/* @__PURE__ */ jsxRuntime.jsx(icons.CloudArrowUp, { className: "mx-auto mb-4 text-ui-fg-muted" }),
|
|
257
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-ui-fg-subtle", children: dropzoneMessage }),
|
|
258
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-ui-fg-muted mt-2", children: "Supported formats: PNG, JPG, JPEG, GIF, WebP" }),
|
|
259
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-ui-fg-muted", children: "Recommended size: 400x400 pixels" })
|
|
260
|
+
]
|
|
261
|
+
}
|
|
262
|
+
) })
|
|
263
|
+
] });
|
|
264
|
+
};
|
|
265
|
+
adminSdk.defineWidgetConfig({
|
|
266
|
+
zone: "product_category.details.after"
|
|
267
|
+
});
|
|
268
|
+
const sdk = new Medusa__default.default({
|
|
269
|
+
baseUrl: "/",
|
|
270
|
+
debug: false,
|
|
271
|
+
auth: {
|
|
272
|
+
type: "session"
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
const useHeroes = () => {
|
|
276
|
+
const [data, setData] = react.useState(null);
|
|
277
|
+
const [isLoading, setIsLoading] = react.useState(true);
|
|
278
|
+
const [error, setError] = react.useState(null);
|
|
279
|
+
const fetchHeroes = async () => {
|
|
280
|
+
setIsLoading(true);
|
|
281
|
+
setError(null);
|
|
282
|
+
try {
|
|
283
|
+
const response = await sdk.client.fetch("/admin/heroes", {
|
|
284
|
+
method: "GET"
|
|
285
|
+
});
|
|
286
|
+
setData(response ?? { heroes: [], count: 0 });
|
|
287
|
+
} catch (err) {
|
|
288
|
+
setError(
|
|
289
|
+
err instanceof Error ? err : new Error("Failed to fetch heroes")
|
|
290
|
+
);
|
|
291
|
+
setData({ heroes: [], count: 0 });
|
|
292
|
+
} finally {
|
|
293
|
+
setIsLoading(false);
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
react.useEffect(() => {
|
|
297
|
+
fetchHeroes();
|
|
298
|
+
}, []);
|
|
299
|
+
return {
|
|
300
|
+
data,
|
|
301
|
+
isLoading,
|
|
302
|
+
error,
|
|
303
|
+
refetch: fetchHeroes
|
|
304
|
+
};
|
|
305
|
+
};
|
|
306
|
+
const DraggableItem = ({ hero }) => {
|
|
307
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rounded-lg shadow-md border border-gray-200 dark:border-gray-700 opacity-80 scale-105 bg-white dark:bg-gray-800", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
308
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
309
|
+
"img",
|
|
310
|
+
{
|
|
311
|
+
src: hero.url,
|
|
312
|
+
alt: `Hero ${hero.id}`,
|
|
313
|
+
className: "object-cover w-full h-48 rounded-t-lg"
|
|
314
|
+
}
|
|
315
|
+
),
|
|
316
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-2 text-sm truncate text-gray-900 dark:text-gray-100 bg-white dark:bg-gray-800", children: hero.id })
|
|
317
|
+
] }) });
|
|
318
|
+
};
|
|
319
|
+
const EditHeroLinkDrawerContent = ({
|
|
320
|
+
hero,
|
|
321
|
+
onSuccess,
|
|
322
|
+
onCancel
|
|
323
|
+
}) => {
|
|
324
|
+
const [link, setLink] = react.useState(hero.link || "");
|
|
325
|
+
const [isLoading, setIsLoading] = react.useState(false);
|
|
326
|
+
const handleSave = async () => {
|
|
327
|
+
setIsLoading(true);
|
|
328
|
+
try {
|
|
329
|
+
const response = await fetch("/admin/heroes/update-link", {
|
|
330
|
+
method: "POST",
|
|
331
|
+
headers: {
|
|
332
|
+
"Content-Type": "application/json"
|
|
333
|
+
},
|
|
334
|
+
credentials: "include",
|
|
335
|
+
body: JSON.stringify({
|
|
336
|
+
id: hero.id,
|
|
337
|
+
link: link.trim() || null
|
|
338
|
+
})
|
|
339
|
+
});
|
|
340
|
+
if (!response.ok) {
|
|
341
|
+
const errorData = await response.json();
|
|
342
|
+
throw new Error(errorData.message || "Failed to update link");
|
|
343
|
+
}
|
|
344
|
+
onSuccess();
|
|
345
|
+
} catch (error) {
|
|
346
|
+
console.error(error);
|
|
347
|
+
alert(
|
|
348
|
+
`Error updating link: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
349
|
+
);
|
|
350
|
+
} finally {
|
|
351
|
+
setIsLoading(false);
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-6", children: [
|
|
355
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "rounded-lg border overflow-hidden", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
356
|
+
"img",
|
|
357
|
+
{
|
|
358
|
+
src: hero.url,
|
|
359
|
+
alt: `Hero ${hero.id}`,
|
|
360
|
+
className: "w-full h-48 object-cover"
|
|
361
|
+
}
|
|
362
|
+
) }),
|
|
363
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
|
|
364
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
365
|
+
ui.Label,
|
|
366
|
+
{
|
|
367
|
+
htmlFor: "hero-link",
|
|
368
|
+
className: "text-sm font-medium flex items-center gap-2",
|
|
369
|
+
children: [
|
|
370
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Link, { className: "h-4 w-4" }),
|
|
371
|
+
"Click URL"
|
|
372
|
+
]
|
|
373
|
+
}
|
|
374
|
+
),
|
|
375
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
376
|
+
ui.Input,
|
|
377
|
+
{
|
|
378
|
+
id: "hero-link",
|
|
379
|
+
type: "url",
|
|
380
|
+
placeholder: "https://example.com",
|
|
381
|
+
value: link,
|
|
382
|
+
onChange: (e) => setLink(e.target.value),
|
|
383
|
+
disabled: isLoading,
|
|
384
|
+
className: "w-full"
|
|
385
|
+
}
|
|
386
|
+
),
|
|
387
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground", children: "Add a URL where users will be redirected when clicking this hero. Leave empty to remove the link." })
|
|
388
|
+
] }),
|
|
389
|
+
hero.link && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-3 bg-muted/50 rounded-lg", children: [
|
|
390
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground mb-1", children: "Current link:" }),
|
|
391
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
392
|
+
"a",
|
|
393
|
+
{
|
|
394
|
+
href: hero.link,
|
|
395
|
+
target: "_blank",
|
|
396
|
+
rel: "noopener noreferrer",
|
|
397
|
+
className: "text-sm text-primary hover:underline break-all",
|
|
398
|
+
children: hero.link
|
|
399
|
+
}
|
|
400
|
+
)
|
|
401
|
+
] }),
|
|
402
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-3 justify-end", children: [
|
|
403
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
404
|
+
ui.Button,
|
|
405
|
+
{
|
|
406
|
+
variant: "secondary",
|
|
407
|
+
onClick: onCancel,
|
|
408
|
+
disabled: isLoading,
|
|
409
|
+
children: "Cancel"
|
|
410
|
+
}
|
|
411
|
+
),
|
|
412
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
413
|
+
ui.Button,
|
|
414
|
+
{
|
|
415
|
+
variant: "primary",
|
|
416
|
+
onClick: handleSave,
|
|
417
|
+
isLoading,
|
|
418
|
+
children: "Save Link"
|
|
419
|
+
}
|
|
420
|
+
)
|
|
421
|
+
] })
|
|
422
|
+
] });
|
|
423
|
+
};
|
|
424
|
+
const GridItem = ({
|
|
425
|
+
hero,
|
|
426
|
+
isSelected,
|
|
427
|
+
onClick,
|
|
428
|
+
onSelect,
|
|
429
|
+
onEditLink
|
|
430
|
+
}) => {
|
|
431
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
432
|
+
"div",
|
|
433
|
+
{
|
|
434
|
+
className: `
|
|
435
|
+
relative overflow-hidden rounded-lg shadow-md cursor-pointer hover:shadow-lg
|
|
436
|
+
transition-all duration-200 ease-in-out
|
|
437
|
+
bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700
|
|
438
|
+
${isSelected ? "ring-2 ring-offset-2 ring-blue-500 dark:ring-offset-gray-900" : ""}
|
|
439
|
+
`,
|
|
440
|
+
onClick,
|
|
441
|
+
children: [
|
|
442
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
443
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
444
|
+
"img",
|
|
445
|
+
{
|
|
446
|
+
src: hero.url || "/placeholder.svg?height=200&width=300",
|
|
447
|
+
alt: `Hero ${hero.id}`,
|
|
448
|
+
className: "object-cover w-full h-48"
|
|
449
|
+
}
|
|
450
|
+
),
|
|
451
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-2 flex items-center justify-between gap-2 bg-white dark:bg-gray-800", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "truncate text-sm flex items-center gap-2 text-gray-900 dark:text-gray-100", children: [
|
|
452
|
+
hero.link && /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Link, { className: "h-3 w-3 text-blue-500 dark:text-blue-400 flex-shrink-0" }),
|
|
453
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: hero.id })
|
|
454
|
+
] }) })
|
|
455
|
+
] }),
|
|
456
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
457
|
+
"div",
|
|
458
|
+
{
|
|
459
|
+
className: `
|
|
460
|
+
absolute top-2 left-2 w-6 h-6 rounded-md flex items-center justify-center
|
|
461
|
+
${isSelected ? "bg-blue-500 text-white" : "bg-white/90 dark:bg-gray-800/90 border border-gray-300 dark:border-gray-600"}
|
|
462
|
+
${isSelected ? "visible" : "invisible group-hover:visible"}
|
|
463
|
+
transition-opacity duration-200
|
|
464
|
+
`,
|
|
465
|
+
onClick: onSelect,
|
|
466
|
+
children: isSelected && /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { className: "h-4 w-4" })
|
|
467
|
+
}
|
|
468
|
+
),
|
|
469
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute top-2 right-2 invisible group-hover:visible", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
470
|
+
ui.Button,
|
|
471
|
+
{
|
|
472
|
+
variant: "secondary",
|
|
473
|
+
className: "size-8 rounded-md p-0 bg-white/90 dark:bg-gray-800/90 hover:bg-white dark:hover:bg-gray-700 border border-gray-200 dark:border-gray-600",
|
|
474
|
+
onClick: onEditLink,
|
|
475
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Edit, { className: "h-4 w-4 text-gray-700 dark:text-gray-300" })
|
|
476
|
+
}
|
|
477
|
+
) })
|
|
478
|
+
]
|
|
479
|
+
}
|
|
480
|
+
);
|
|
481
|
+
};
|
|
482
|
+
const SortableItem = ({ hero }) => {
|
|
483
|
+
const {
|
|
484
|
+
attributes,
|
|
485
|
+
listeners,
|
|
486
|
+
setNodeRef,
|
|
487
|
+
transform,
|
|
488
|
+
transition,
|
|
489
|
+
isDragging
|
|
490
|
+
} = sortable.useSortable({ id: hero.id });
|
|
491
|
+
const style = {
|
|
492
|
+
transform: utilities.CSS.Transform.toString(transform),
|
|
493
|
+
transition,
|
|
494
|
+
zIndex: isDragging ? 10 : 1,
|
|
495
|
+
opacity: isDragging ? 0.5 : 1
|
|
496
|
+
};
|
|
497
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
498
|
+
"div",
|
|
499
|
+
{
|
|
500
|
+
ref: setNodeRef,
|
|
501
|
+
style,
|
|
502
|
+
className: "rounded-lg shadow-md border border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600 transition-all relative bg-white dark:bg-gray-800",
|
|
503
|
+
...attributes,
|
|
504
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs("div", { ...listeners, children: [
|
|
505
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
506
|
+
"img",
|
|
507
|
+
{
|
|
508
|
+
src: hero.url,
|
|
509
|
+
alt: `Hero ${hero.id}`,
|
|
510
|
+
className: "object-cover w-full h-48 rounded-t-lg"
|
|
511
|
+
}
|
|
512
|
+
),
|
|
513
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-2 text-sm truncate text-gray-900 dark:text-gray-100 bg-white dark:bg-gray-800", children: hero.id })
|
|
514
|
+
] })
|
|
515
|
+
}
|
|
516
|
+
);
|
|
517
|
+
};
|
|
518
|
+
const UploadHeroDrawerContent = ({
|
|
519
|
+
maxFileSizeMb = 10,
|
|
520
|
+
onSuccess
|
|
521
|
+
}) => {
|
|
522
|
+
const [files, setFiles] = react.useState([]);
|
|
523
|
+
const [isLoading, setIsLoading] = react.useState(false);
|
|
524
|
+
const onDrop = react.useCallback((acceptedFiles) => {
|
|
525
|
+
const newFiles = acceptedFiles.map((file) => {
|
|
526
|
+
const id = crypto.randomUUID();
|
|
527
|
+
let preview;
|
|
528
|
+
if (file.type.startsWith("image/")) {
|
|
529
|
+
preview = URL.createObjectURL(file);
|
|
530
|
+
}
|
|
531
|
+
return {
|
|
532
|
+
file,
|
|
533
|
+
id,
|
|
534
|
+
preview,
|
|
535
|
+
link: ""
|
|
536
|
+
};
|
|
537
|
+
});
|
|
538
|
+
setFiles((prev) => [...prev, ...newFiles]);
|
|
539
|
+
}, []);
|
|
540
|
+
const removeFile = (id) => {
|
|
541
|
+
setFiles((files2) => {
|
|
542
|
+
const fileToRemove = files2.find((f) => f.id === id);
|
|
543
|
+
if (fileToRemove == null ? void 0 : fileToRemove.preview) {
|
|
544
|
+
URL.revokeObjectURL(fileToRemove.preview);
|
|
545
|
+
}
|
|
546
|
+
return files2.filter((f) => f.id !== id);
|
|
547
|
+
});
|
|
548
|
+
};
|
|
549
|
+
const updateFileLink = (id, link) => {
|
|
550
|
+
setFiles((files2) => files2.map((f) => f.id === id ? { ...f, link } : f));
|
|
551
|
+
};
|
|
552
|
+
const handleResetFiles = () => {
|
|
553
|
+
setFiles([]);
|
|
554
|
+
};
|
|
555
|
+
const handleUploadFiles = async () => {
|
|
556
|
+
setIsLoading(true);
|
|
557
|
+
try {
|
|
558
|
+
const formData = new FormData();
|
|
559
|
+
files.forEach((file) => {
|
|
560
|
+
if (!file.file) {
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
formData.append("files", file.file);
|
|
564
|
+
});
|
|
565
|
+
const response = await fetch("/admin/heroes", {
|
|
566
|
+
method: "POST",
|
|
567
|
+
body: formData,
|
|
568
|
+
credentials: "include"
|
|
569
|
+
});
|
|
570
|
+
if (!response.ok) {
|
|
571
|
+
const errorData = await response.json();
|
|
572
|
+
throw new Error(errorData.message || "Upload failed");
|
|
573
|
+
}
|
|
574
|
+
const data = await response.json();
|
|
575
|
+
const uploadedFiles = data.files;
|
|
576
|
+
const updatePromises = uploadedFiles.map(
|
|
577
|
+
async (uploadedFile, index) => {
|
|
578
|
+
var _a;
|
|
579
|
+
const fileLink = (_a = files[index]) == null ? void 0 : _a.link;
|
|
580
|
+
if (fileLink && fileLink.trim()) {
|
|
581
|
+
await fetch("/admin/heroes/update-link", {
|
|
582
|
+
method: "POST",
|
|
583
|
+
headers: {
|
|
584
|
+
"Content-Type": "application/json"
|
|
585
|
+
},
|
|
586
|
+
credentials: "include",
|
|
587
|
+
body: JSON.stringify({
|
|
588
|
+
id: uploadedFile.id,
|
|
589
|
+
link: fileLink.trim()
|
|
590
|
+
})
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
return uploadedFile;
|
|
594
|
+
}
|
|
595
|
+
);
|
|
596
|
+
await Promise.all(updatePromises);
|
|
597
|
+
setFiles([]);
|
|
598
|
+
onSuccess(uploadedFiles);
|
|
599
|
+
return {
|
|
600
|
+
files,
|
|
601
|
+
uploadedFiles
|
|
602
|
+
};
|
|
603
|
+
} catch (error) {
|
|
604
|
+
console.error(error);
|
|
605
|
+
alert(
|
|
606
|
+
`Error uploading files: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
607
|
+
);
|
|
608
|
+
} finally {
|
|
609
|
+
setIsLoading(false);
|
|
610
|
+
}
|
|
611
|
+
};
|
|
612
|
+
const {
|
|
613
|
+
getRootProps,
|
|
614
|
+
getInputProps,
|
|
615
|
+
isDragActive,
|
|
616
|
+
isDragAccept,
|
|
617
|
+
isDragReject,
|
|
618
|
+
isFocused
|
|
619
|
+
} = reactDropzone.useDropzone({
|
|
620
|
+
onDrop,
|
|
621
|
+
accept: {
|
|
622
|
+
"image/*": []
|
|
623
|
+
},
|
|
624
|
+
maxSize: maxFileSizeMb * 1024 * 1024
|
|
625
|
+
});
|
|
626
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-6", children: [
|
|
627
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
628
|
+
"div",
|
|
629
|
+
{
|
|
630
|
+
...getRootProps(),
|
|
631
|
+
className: clx__default.default(
|
|
632
|
+
"border-2 border-dashed rounded-xl p-10 transition-all duration-150 ease-in-out cursor-pointer",
|
|
633
|
+
"flex flex-col items-center justify-center gap-4",
|
|
634
|
+
isDragActive ? "bg-primary/5 border-primary/50" : "bg-background hover:bg-muted/50",
|
|
635
|
+
isDragAccept ? "border-green-500 bg-green-50 dark:bg-green-950/20" : "",
|
|
636
|
+
isDragReject ? "border-red-500 bg-red-50 dark:bg-red-950/20" : "",
|
|
637
|
+
isFocused ? "ring-2 ring-ring ring-offset-2" : "",
|
|
638
|
+
"focus-visible:outline-none"
|
|
639
|
+
),
|
|
640
|
+
children: [
|
|
641
|
+
/* @__PURE__ */ jsxRuntime.jsx("input", { ...getInputProps(), disabled: isLoading }),
|
|
642
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "rounded-full bg-primary/10 p-4", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Upload, { className: "h-8 w-8 text-primary" }) }),
|
|
643
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center space-y-2", children: [
|
|
644
|
+
/* @__PURE__ */ jsxRuntime.jsx("h3", { className: "font-medium text-lg", children: isDragActive ? isDragAccept ? "Drop files to upload" : "This file type is not supported" : "Drag & drop images here" }),
|
|
645
|
+
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-sm text-muted-foreground", children: [
|
|
646
|
+
"or ",
|
|
647
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-primary font-medium", children: "browse images" }),
|
|
648
|
+
" ",
|
|
649
|
+
"from your computer"
|
|
650
|
+
] }),
|
|
651
|
+
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-xs text-muted-foreground", children: [
|
|
652
|
+
"(max ",
|
|
653
|
+
maxFileSizeMb,
|
|
654
|
+
"MB)"
|
|
655
|
+
] })
|
|
656
|
+
] })
|
|
657
|
+
]
|
|
658
|
+
}
|
|
659
|
+
),
|
|
660
|
+
files.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
|
|
661
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
|
|
662
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-4", children: [
|
|
663
|
+
/* @__PURE__ */ jsxRuntime.jsxs("h3", { className: "text-lg font-medium", children: [
|
|
664
|
+
"Selected Files (",
|
|
665
|
+
files.length,
|
|
666
|
+
")"
|
|
667
|
+
] }),
|
|
668
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
669
|
+
ui.Button,
|
|
670
|
+
{
|
|
671
|
+
isLoading,
|
|
672
|
+
variant: "primary",
|
|
673
|
+
onClick: handleUploadFiles,
|
|
674
|
+
children: "Confirm Upload"
|
|
675
|
+
}
|
|
676
|
+
)
|
|
677
|
+
] }),
|
|
678
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
679
|
+
ui.Button,
|
|
680
|
+
{
|
|
681
|
+
isLoading,
|
|
682
|
+
variant: "secondary",
|
|
683
|
+
onClick: handleResetFiles,
|
|
684
|
+
children: "Clear All"
|
|
685
|
+
}
|
|
686
|
+
)
|
|
687
|
+
] }),
|
|
688
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid gap-6 grid-cols-1", children: files.map((fileItem) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
689
|
+
"div",
|
|
690
|
+
{
|
|
691
|
+
className: "rounded-lg border bg-card text-card-foreground shadow-sm overflow-hidden",
|
|
692
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-4 p-4", children: [
|
|
693
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-shrink-0", children: fileItem.preview ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-32 h-32 overflow-hidden rounded-lg", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
694
|
+
"img",
|
|
695
|
+
{
|
|
696
|
+
src: fileItem.preview || "/placeholder.svg",
|
|
697
|
+
alt: fileItem.file.name,
|
|
698
|
+
className: "w-full h-full object-cover"
|
|
699
|
+
}
|
|
700
|
+
) }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-32 h-32 flex items-center justify-center bg-muted/50 rounded-lg", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ImageIcon, { className: "h-8 w-8 text-primary" }) }) }),
|
|
701
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 space-y-3", children: [
|
|
702
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start justify-between", children: [
|
|
703
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-1", children: [
|
|
704
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-medium", children: fileItem.file.name }),
|
|
705
|
+
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-xs text-muted-foreground", children: [
|
|
706
|
+
(fileItem.file.size / 1024).toFixed(1),
|
|
707
|
+
" KB"
|
|
708
|
+
] })
|
|
709
|
+
] }),
|
|
710
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
711
|
+
ui.Button,
|
|
712
|
+
{
|
|
713
|
+
isLoading,
|
|
714
|
+
variant: "secondary",
|
|
715
|
+
className: "size-8 rounded-full p-0.5 text-red-400",
|
|
716
|
+
onClick: (e) => {
|
|
717
|
+
e.stopPropagation();
|
|
718
|
+
removeFile(fileItem.id);
|
|
719
|
+
},
|
|
720
|
+
children: [
|
|
721
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Trash, { className: "h-4 w-4" }),
|
|
722
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "sr-only", children: "Remove file" })
|
|
723
|
+
]
|
|
724
|
+
}
|
|
725
|
+
)
|
|
726
|
+
] }),
|
|
727
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
|
|
728
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
729
|
+
ui.Label,
|
|
730
|
+
{
|
|
731
|
+
htmlFor: `link-${fileItem.id}`,
|
|
732
|
+
className: "text-sm font-medium flex items-center gap-2",
|
|
733
|
+
children: [
|
|
734
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Link, { className: "h-4 w-4" }),
|
|
735
|
+
"Click URL (optional)"
|
|
736
|
+
]
|
|
737
|
+
}
|
|
738
|
+
),
|
|
739
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
740
|
+
ui.Input,
|
|
741
|
+
{
|
|
742
|
+
id: `link-${fileItem.id}`,
|
|
743
|
+
type: "url",
|
|
744
|
+
placeholder: "https://example.com",
|
|
745
|
+
value: fileItem.link || "",
|
|
746
|
+
onChange: (e) => updateFileLink(fileItem.id, e.target.value),
|
|
747
|
+
disabled: isLoading,
|
|
748
|
+
className: "w-full"
|
|
749
|
+
}
|
|
750
|
+
),
|
|
751
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground", children: "Add a URL where users will be redirected when clicking this hero" })
|
|
752
|
+
] })
|
|
753
|
+
] })
|
|
754
|
+
] })
|
|
755
|
+
},
|
|
756
|
+
fileItem.id
|
|
757
|
+
)) })
|
|
758
|
+
] })
|
|
759
|
+
] });
|
|
760
|
+
};
|
|
761
|
+
const HeroPage = () => {
|
|
762
|
+
const [open, setOpen] = react.useState(false);
|
|
763
|
+
const [editLinkOpen, setEditLinkOpen] = react.useState(false);
|
|
764
|
+
const [selectedHeroForEdit, setSelectedHeroForEdit] = react.useState(
|
|
765
|
+
null
|
|
766
|
+
);
|
|
767
|
+
const [selectedImage, setSelectedImage] = react.useState(null);
|
|
768
|
+
const [selectedIds, setSelectedIds] = react.useState(/* @__PURE__ */ new Set());
|
|
769
|
+
const [reorderMode, setReorderMode] = react.useState(false);
|
|
770
|
+
const [orderedHeroes, setOrderedHeros] = react.useState([]);
|
|
771
|
+
const [activeHero, setActiveHero] = react.useState(null);
|
|
772
|
+
const [isLoading, setIsLoading] = react.useState(false);
|
|
773
|
+
const dialog = ui.usePrompt();
|
|
774
|
+
const { data: heroData, refetch } = useHeroes();
|
|
775
|
+
const { count = 0, heroes = [] } = heroData || {};
|
|
776
|
+
const sensors = core.useSensors(
|
|
777
|
+
core.useSensor(core.PointerSensor, {
|
|
778
|
+
activationConstraint: {
|
|
779
|
+
distance: 8
|
|
780
|
+
// 8px movement required before drag starts
|
|
781
|
+
}
|
|
782
|
+
}),
|
|
783
|
+
core.useSensor(core.KeyboardSensor, {
|
|
784
|
+
coordinateGetter: sortable.sortableKeyboardCoordinates
|
|
785
|
+
})
|
|
786
|
+
);
|
|
787
|
+
const toggleImageSelection = (e, id) => {
|
|
788
|
+
e.stopPropagation();
|
|
789
|
+
const newSelectedIds = new Set(selectedIds);
|
|
790
|
+
if (newSelectedIds.has(id)) {
|
|
791
|
+
newSelectedIds.delete(id);
|
|
792
|
+
} else {
|
|
793
|
+
newSelectedIds.add(id);
|
|
794
|
+
}
|
|
795
|
+
setSelectedIds(newSelectedIds);
|
|
796
|
+
};
|
|
797
|
+
const selectAll = () => {
|
|
798
|
+
const allIds = heroes.map((hero) => hero.id);
|
|
799
|
+
setSelectedIds(new Set(allIds));
|
|
800
|
+
};
|
|
801
|
+
const deselectAll = () => {
|
|
802
|
+
setSelectedIds(/* @__PURE__ */ new Set());
|
|
803
|
+
};
|
|
804
|
+
const openLightbox = (hero) => {
|
|
805
|
+
setSelectedImage(hero);
|
|
806
|
+
};
|
|
807
|
+
const closeLightbox = () => {
|
|
808
|
+
setSelectedImage(null);
|
|
809
|
+
};
|
|
810
|
+
const openEditLinkDrawer = (e, hero) => {
|
|
811
|
+
e.stopPropagation();
|
|
812
|
+
setSelectedHeroForEdit(hero);
|
|
813
|
+
setEditLinkOpen(true);
|
|
814
|
+
};
|
|
815
|
+
const closeEditLinkDrawer = () => {
|
|
816
|
+
setEditLinkOpen(false);
|
|
817
|
+
setSelectedHeroForEdit(null);
|
|
818
|
+
};
|
|
819
|
+
const toggleReorderMode = async () => {
|
|
820
|
+
if (reorderMode) {
|
|
821
|
+
const confirmed = await dialog({
|
|
822
|
+
title: "Save Changes?",
|
|
823
|
+
description: "Do you want to save your reordering changes?",
|
|
824
|
+
variant: "confirmation",
|
|
825
|
+
confirmText: "Save",
|
|
826
|
+
cancelText: "Discard"
|
|
827
|
+
});
|
|
828
|
+
if (confirmed) {
|
|
829
|
+
saveNewOrder();
|
|
830
|
+
} else {
|
|
831
|
+
setOrderedHeros([]);
|
|
832
|
+
setReorderMode(false);
|
|
833
|
+
}
|
|
834
|
+
} else {
|
|
835
|
+
setOrderedHeros([...heroes]);
|
|
836
|
+
setReorderMode(true);
|
|
837
|
+
}
|
|
838
|
+
};
|
|
839
|
+
const handleDragStart = (event) => {
|
|
840
|
+
const { active } = event;
|
|
841
|
+
const activeHero2 = orderedHeroes.find((hero) => hero.id === active.id);
|
|
842
|
+
if (activeHero2) {
|
|
843
|
+
setActiveHero(activeHero2);
|
|
844
|
+
}
|
|
845
|
+
};
|
|
846
|
+
const handleDragEnd = (event) => {
|
|
847
|
+
const { active, over } = event;
|
|
848
|
+
if (over && active.id !== over.id) {
|
|
849
|
+
setOrderedHeros((items) => {
|
|
850
|
+
const oldIndex = items.findIndex((item) => item.id === active.id);
|
|
851
|
+
const newIndex = items.findIndex((item) => item.id === over.id);
|
|
852
|
+
return sortable.arrayMove(items, oldIndex, newIndex);
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
setActiveHero(null);
|
|
856
|
+
};
|
|
857
|
+
const saveNewOrder = async () => {
|
|
858
|
+
try {
|
|
859
|
+
await sdk.client.fetch("/admin/heroes/reorder", {
|
|
860
|
+
method: "POST",
|
|
861
|
+
headers: {
|
|
862
|
+
"Content-Type": "application/json"
|
|
863
|
+
},
|
|
864
|
+
body: {
|
|
865
|
+
ids: orderedHeroes.map((hero) => hero.id)
|
|
866
|
+
}
|
|
867
|
+
});
|
|
868
|
+
setReorderMode(false);
|
|
869
|
+
refetch();
|
|
870
|
+
} catch (error) {
|
|
871
|
+
console.error("Failed to save new order:", error);
|
|
872
|
+
}
|
|
873
|
+
};
|
|
874
|
+
const handleDeleteMultiple = async () => {
|
|
875
|
+
const isConfirmed = await dialog({
|
|
876
|
+
title: "Confirm Deletion",
|
|
877
|
+
description: `Are you sure you want to delete ${selectedIds.size} selected images? This action cannot be undone.`,
|
|
878
|
+
confirmText: "Delete",
|
|
879
|
+
cancelText: "Cancel"
|
|
880
|
+
});
|
|
881
|
+
if (!isConfirmed) return;
|
|
882
|
+
const ids = Array.from(selectedIds);
|
|
883
|
+
setIsLoading(true);
|
|
884
|
+
try {
|
|
885
|
+
await fetch("/admin/heroes", {
|
|
886
|
+
method: "DELETE",
|
|
887
|
+
headers: {
|
|
888
|
+
"Content-Type": "application/json"
|
|
889
|
+
},
|
|
890
|
+
body: JSON.stringify({ ids })
|
|
891
|
+
});
|
|
892
|
+
setSelectedIds(/* @__PURE__ */ new Set());
|
|
893
|
+
refetch();
|
|
894
|
+
} catch {
|
|
895
|
+
console.error("Failed to delete heroes");
|
|
896
|
+
} finally {
|
|
897
|
+
setIsLoading(false);
|
|
898
|
+
}
|
|
899
|
+
};
|
|
900
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-6", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "container mx-auto px-4 py-8", children: [
|
|
901
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between mb-6", children: [
|
|
902
|
+
/* @__PURE__ */ jsxRuntime.jsxs("h2", { className: "text-2xl font-bold", children: [
|
|
903
|
+
"Heros (",
|
|
904
|
+
count,
|
|
905
|
+
")"
|
|
906
|
+
] }),
|
|
907
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
908
|
+
ui.Drawer,
|
|
909
|
+
{
|
|
910
|
+
open,
|
|
911
|
+
onOpenChange: (openChanged) => setOpen(openChanged),
|
|
912
|
+
children: [
|
|
913
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
914
|
+
ui.Drawer.Trigger,
|
|
915
|
+
{
|
|
916
|
+
onClick: () => {
|
|
917
|
+
setOpen(true);
|
|
918
|
+
},
|
|
919
|
+
asChild: true,
|
|
920
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { isLoading, children: "Upload new hero" })
|
|
921
|
+
}
|
|
922
|
+
),
|
|
923
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Content, { children: [
|
|
924
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Header, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Title, { children: "Upload new hero" }) }),
|
|
925
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Body, { children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
926
|
+
UploadHeroDrawerContent,
|
|
927
|
+
{
|
|
928
|
+
maxFileSizeMb: 10,
|
|
929
|
+
onSuccess: () => {
|
|
930
|
+
setOpen(false);
|
|
931
|
+
refetch();
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
) })
|
|
935
|
+
] })
|
|
936
|
+
]
|
|
937
|
+
}
|
|
938
|
+
)
|
|
939
|
+
] }),
|
|
940
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
941
|
+
ui.Drawer,
|
|
942
|
+
{
|
|
943
|
+
open: editLinkOpen,
|
|
944
|
+
onOpenChange: (openChanged) => {
|
|
945
|
+
if (!openChanged) {
|
|
946
|
+
closeEditLinkDrawer();
|
|
947
|
+
}
|
|
948
|
+
},
|
|
949
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Content, { children: [
|
|
950
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Header, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Title, { children: "Edit Hero Link" }) }),
|
|
951
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Body, { children: selectedHeroForEdit && /* @__PURE__ */ jsxRuntime.jsx(
|
|
952
|
+
EditHeroLinkDrawerContent,
|
|
953
|
+
{
|
|
954
|
+
hero: selectedHeroForEdit,
|
|
955
|
+
onSuccess: () => {
|
|
956
|
+
closeEditLinkDrawer();
|
|
957
|
+
refetch();
|
|
958
|
+
},
|
|
959
|
+
onCancel: closeEditLinkDrawer
|
|
960
|
+
}
|
|
961
|
+
) })
|
|
962
|
+
] })
|
|
963
|
+
}
|
|
964
|
+
),
|
|
965
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "container mx-auto px-4 py-8", children: [
|
|
966
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-between items-center mb-6", children: [
|
|
967
|
+
selectedIds.size > 0 && !reorderMode && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex gap-2", children: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
968
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
969
|
+
ui.Button,
|
|
970
|
+
{
|
|
971
|
+
isLoading,
|
|
972
|
+
onClick: selectAll,
|
|
973
|
+
disabled: count === 0,
|
|
974
|
+
children: "Select All"
|
|
975
|
+
}
|
|
976
|
+
),
|
|
977
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
978
|
+
ui.Button,
|
|
979
|
+
{
|
|
980
|
+
isLoading,
|
|
981
|
+
onClick: deselectAll,
|
|
982
|
+
disabled: selectedIds.size === 0,
|
|
983
|
+
children: "Deselect All"
|
|
984
|
+
}
|
|
985
|
+
),
|
|
986
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
987
|
+
ui.Button,
|
|
988
|
+
{
|
|
989
|
+
isLoading,
|
|
990
|
+
variant: "danger",
|
|
991
|
+
onClick: () => handleDeleteMultiple(),
|
|
992
|
+
disabled: selectedIds.size === 0,
|
|
993
|
+
children: [
|
|
994
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Trash2, { className: "h-4 w-4" }),
|
|
995
|
+
"Delete (",
|
|
996
|
+
selectedIds.size,
|
|
997
|
+
")"
|
|
998
|
+
]
|
|
999
|
+
}
|
|
1000
|
+
)
|
|
1001
|
+
] }) }),
|
|
1002
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ml-auto", children: [
|
|
1003
|
+
heroes.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1004
|
+
ui.Button,
|
|
1005
|
+
{
|
|
1006
|
+
isLoading,
|
|
1007
|
+
onClick: toggleReorderMode,
|
|
1008
|
+
variant: reorderMode ? "secondary" : "primary",
|
|
1009
|
+
children: [
|
|
1010
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.MoveHorizontal, { className: "h-4 w-4 mr-2" }),
|
|
1011
|
+
reorderMode ? "Exit Reorder" : "Reorder"
|
|
1012
|
+
]
|
|
1013
|
+
}
|
|
1014
|
+
),
|
|
1015
|
+
reorderMode && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1016
|
+
ui.Button,
|
|
1017
|
+
{
|
|
1018
|
+
isLoading,
|
|
1019
|
+
onClick: saveNewOrder,
|
|
1020
|
+
variant: "primary",
|
|
1021
|
+
className: "ml-2",
|
|
1022
|
+
children: [
|
|
1023
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { className: "h-4 w-4 mr-2" }),
|
|
1024
|
+
"Save Order"
|
|
1025
|
+
]
|
|
1026
|
+
}
|
|
1027
|
+
)
|
|
1028
|
+
] })
|
|
1029
|
+
] }),
|
|
1030
|
+
reorderMode ? /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1031
|
+
core.DndContext,
|
|
1032
|
+
{
|
|
1033
|
+
sensors,
|
|
1034
|
+
collisionDetection: core.closestCenter,
|
|
1035
|
+
onDragStart: handleDragStart,
|
|
1036
|
+
onDragEnd: handleDragEnd,
|
|
1037
|
+
children: [
|
|
1038
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1039
|
+
sortable.SortableContext,
|
|
1040
|
+
{
|
|
1041
|
+
items: orderedHeroes.map((hero) => hero.id),
|
|
1042
|
+
strategy: sortable.rectSortingStrategy,
|
|
1043
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 isolate", children: orderedHeroes.map((hero) => /* @__PURE__ */ jsxRuntime.jsx(SortableItem, { hero }, hero.id)) })
|
|
1044
|
+
}
|
|
1045
|
+
),
|
|
1046
|
+
/* @__PURE__ */ jsxRuntime.jsx(core.DragOverlay, { children: activeHero ? /* @__PURE__ */ jsxRuntime.jsx(DraggableItem, { hero: activeHero }) : null })
|
|
1047
|
+
]
|
|
1048
|
+
}
|
|
1049
|
+
) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 group", children: heroes.map((hero) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
1050
|
+
GridItem,
|
|
1051
|
+
{
|
|
1052
|
+
hero,
|
|
1053
|
+
isSelected: selectedIds.has(hero.id),
|
|
1054
|
+
onClick: () => openLightbox(hero),
|
|
1055
|
+
onSelect: (e) => toggleImageSelection(e, hero.id),
|
|
1056
|
+
onEditLink: (e) => openEditLinkDrawer(e, hero)
|
|
1057
|
+
},
|
|
1058
|
+
hero.id
|
|
1059
|
+
)) }),
|
|
1060
|
+
selectedImage && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1061
|
+
"div",
|
|
1062
|
+
{
|
|
1063
|
+
className: "fixed inset-0 bg-black/80 z-50 flex items-center justify-center p-4",
|
|
1064
|
+
onClick: closeLightbox,
|
|
1065
|
+
children: [
|
|
1066
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1067
|
+
ui.Button,
|
|
1068
|
+
{
|
|
1069
|
+
isLoading,
|
|
1070
|
+
className: "absolute top-4 right-4 text-white bg-black/50 p-2 rounded-full hover:bg-black/70",
|
|
1071
|
+
onClick: (e) => {
|
|
1072
|
+
e.stopPropagation();
|
|
1073
|
+
closeLightbox();
|
|
1074
|
+
},
|
|
1075
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { className: "h-6 w-6" })
|
|
1076
|
+
}
|
|
1077
|
+
),
|
|
1078
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1079
|
+
"div",
|
|
1080
|
+
{
|
|
1081
|
+
className: "relative max-w-screen-xl max-h-screen overflow-auto",
|
|
1082
|
+
onClick: (e) => e.stopPropagation(),
|
|
1083
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1084
|
+
"img",
|
|
1085
|
+
{
|
|
1086
|
+
src: selectedImage.url || "/placeholder.svg",
|
|
1087
|
+
alt: `Hero ${selectedImage.id}`,
|
|
1088
|
+
className: "max-h-[90vh] max-w-full object-contain mx-auto"
|
|
1089
|
+
}
|
|
1090
|
+
)
|
|
1091
|
+
}
|
|
1092
|
+
)
|
|
1093
|
+
]
|
|
1094
|
+
}
|
|
1095
|
+
)
|
|
1096
|
+
] })
|
|
1097
|
+
] }) });
|
|
1098
|
+
};
|
|
1099
|
+
const HeroUpload = () => {
|
|
1100
|
+
return /* @__PURE__ */ jsxRuntime.jsx(HeroPage, {});
|
|
1101
|
+
};
|
|
1102
|
+
const config = adminSdk.defineRouteConfig({
|
|
1103
|
+
label: "Hero",
|
|
1104
|
+
icon: icons.StackPerspective
|
|
1105
|
+
});
|
|
1106
|
+
const widgetModule = { widgets: [
|
|
1107
|
+
{
|
|
1108
|
+
Component: CategoryHeroWidget,
|
|
1109
|
+
zone: ["product_category.details.after"]
|
|
1110
|
+
},
|
|
1111
|
+
{
|
|
1112
|
+
Component: CategoryThumbnailWidget,
|
|
1113
|
+
zone: ["product_category.details.after"]
|
|
1114
|
+
}
|
|
1115
|
+
] };
|
|
1116
|
+
const routeModule = {
|
|
1117
|
+
routes: [
|
|
1118
|
+
{
|
|
1119
|
+
Component: HeroUpload,
|
|
1120
|
+
path: "/heroes"
|
|
1121
|
+
}
|
|
1122
|
+
]
|
|
1123
|
+
};
|
|
1124
|
+
const menuItemModule = {
|
|
1125
|
+
menuItems: [
|
|
1126
|
+
{
|
|
1127
|
+
label: config.label,
|
|
1128
|
+
icon: config.icon,
|
|
1129
|
+
path: "/heroes",
|
|
1130
|
+
nested: void 0
|
|
1131
|
+
}
|
|
1132
|
+
]
|
|
1133
|
+
};
|
|
1134
|
+
const formModule = { customFields: {} };
|
|
1135
|
+
const displayModule = {
|
|
1136
|
+
displays: {}
|
|
1137
|
+
};
|
|
1138
|
+
const i18nModule = { resources: {} };
|
|
1139
|
+
const plugin = {
|
|
1140
|
+
widgetModule,
|
|
1141
|
+
routeModule,
|
|
1142
|
+
menuItemModule,
|
|
1143
|
+
formModule,
|
|
1144
|
+
displayModule,
|
|
1145
|
+
i18nModule
|
|
1146
|
+
};
|
|
1147
|
+
module.exports = plugin;
|