@lodashventure/medusa-banner 1.4.9 → 1.4.10
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.
|
@@ -1,898 +0,0 @@
|
|
|
1
|
-
import { jsx, jsxs, Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { defineRouteConfig } from "@medusajs/admin-sdk";
|
|
3
|
-
import { StackPerspective } from "@medusajs/icons";
|
|
4
|
-
import { useSensors, useSensor, PointerSensor, KeyboardSensor, DndContext, closestCenter, DragOverlay } from "@dnd-kit/core";
|
|
5
|
-
import { useSortable, sortableKeyboardCoordinates, SortableContext, rectSortingStrategy, arrayMove } from "@dnd-kit/sortable";
|
|
6
|
-
import { Label, Input, Button, usePrompt, Drawer } from "@medusajs/ui";
|
|
7
|
-
import { Link, Check, Edit, Upload, ImageIcon, Trash, Trash2, MoveHorizontal, X } from "lucide-react";
|
|
8
|
-
import { useState, useEffect, useCallback } from "react";
|
|
9
|
-
import Medusa from "@medusajs/js-sdk";
|
|
10
|
-
import { CSS } from "@dnd-kit/utilities";
|
|
11
|
-
import { useDropzone } from "react-dropzone";
|
|
12
|
-
const sdk = new Medusa({
|
|
13
|
-
baseUrl: "/",
|
|
14
|
-
debug: false,
|
|
15
|
-
auth: {
|
|
16
|
-
type: "session"
|
|
17
|
-
}
|
|
18
|
-
});
|
|
19
|
-
const useBanners = () => {
|
|
20
|
-
const [data, setData] = useState(null);
|
|
21
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
22
|
-
const [error, setError] = useState(null);
|
|
23
|
-
const fetchBanners = async () => {
|
|
24
|
-
setIsLoading(true);
|
|
25
|
-
setError(null);
|
|
26
|
-
try {
|
|
27
|
-
const response = await sdk.client.fetch(
|
|
28
|
-
"/admin/banners",
|
|
29
|
-
{
|
|
30
|
-
method: "GET"
|
|
31
|
-
}
|
|
32
|
-
);
|
|
33
|
-
setData(response ?? { banners: [], count: 0 });
|
|
34
|
-
} catch (err) {
|
|
35
|
-
setError(
|
|
36
|
-
err instanceof Error ? err : new Error("Failed to fetch banners")
|
|
37
|
-
);
|
|
38
|
-
setData({ banners: [], count: 0 });
|
|
39
|
-
} finally {
|
|
40
|
-
setIsLoading(false);
|
|
41
|
-
}
|
|
42
|
-
};
|
|
43
|
-
useEffect(() => {
|
|
44
|
-
fetchBanners();
|
|
45
|
-
}, []);
|
|
46
|
-
return {
|
|
47
|
-
data,
|
|
48
|
-
isLoading,
|
|
49
|
-
error,
|
|
50
|
-
refetch: fetchBanners
|
|
51
|
-
};
|
|
52
|
-
};
|
|
53
|
-
const DraggableItem = ({ banner }) => {
|
|
54
|
-
return /* @__PURE__ */ 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__ */ jsxs("div", { children: [
|
|
55
|
-
/* @__PURE__ */ jsx(
|
|
56
|
-
"img",
|
|
57
|
-
{
|
|
58
|
-
src: banner.url,
|
|
59
|
-
alt: `Banner ${banner.id}`,
|
|
60
|
-
className: "object-cover w-full h-48 rounded-t-lg"
|
|
61
|
-
}
|
|
62
|
-
),
|
|
63
|
-
/* @__PURE__ */ jsx("div", { className: "p-2 text-sm truncate text-gray-900 dark:text-gray-100 bg-white dark:bg-gray-800", children: banner.id })
|
|
64
|
-
] }) });
|
|
65
|
-
};
|
|
66
|
-
const EditBannerLinkDrawerContent = ({
|
|
67
|
-
banner,
|
|
68
|
-
onSuccess,
|
|
69
|
-
onCancel
|
|
70
|
-
}) => {
|
|
71
|
-
const [link, setLink] = useState(banner.link || "");
|
|
72
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
73
|
-
const handleSave = async () => {
|
|
74
|
-
setIsLoading(true);
|
|
75
|
-
try {
|
|
76
|
-
const response = await fetch("/admin/banners/update-link", {
|
|
77
|
-
method: "POST",
|
|
78
|
-
headers: {
|
|
79
|
-
"Content-Type": "application/json"
|
|
80
|
-
},
|
|
81
|
-
credentials: "include",
|
|
82
|
-
body: JSON.stringify({
|
|
83
|
-
id: banner.id,
|
|
84
|
-
link: link.trim() || null
|
|
85
|
-
})
|
|
86
|
-
});
|
|
87
|
-
if (!response.ok) {
|
|
88
|
-
const errorData = await response.json();
|
|
89
|
-
throw new Error(errorData.message || "Failed to update link");
|
|
90
|
-
}
|
|
91
|
-
onSuccess();
|
|
92
|
-
} catch (error) {
|
|
93
|
-
console.error(error);
|
|
94
|
-
alert(
|
|
95
|
-
`Error updating link: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
96
|
-
);
|
|
97
|
-
} finally {
|
|
98
|
-
setIsLoading(false);
|
|
99
|
-
}
|
|
100
|
-
};
|
|
101
|
-
return /* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
|
|
102
|
-
/* @__PURE__ */ jsx("div", { className: "rounded-lg border overflow-hidden", children: /* @__PURE__ */ jsx(
|
|
103
|
-
"img",
|
|
104
|
-
{
|
|
105
|
-
src: banner.url,
|
|
106
|
-
alt: `Banner ${banner.id}`,
|
|
107
|
-
className: "w-full h-48 object-cover"
|
|
108
|
-
}
|
|
109
|
-
) }),
|
|
110
|
-
/* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
|
|
111
|
-
/* @__PURE__ */ jsxs(
|
|
112
|
-
Label,
|
|
113
|
-
{
|
|
114
|
-
htmlFor: "banner-link",
|
|
115
|
-
className: "text-sm font-medium flex items-center gap-2",
|
|
116
|
-
children: [
|
|
117
|
-
/* @__PURE__ */ jsx(Link, { className: "h-4 w-4" }),
|
|
118
|
-
"Click URL"
|
|
119
|
-
]
|
|
120
|
-
}
|
|
121
|
-
),
|
|
122
|
-
/* @__PURE__ */ jsx(
|
|
123
|
-
Input,
|
|
124
|
-
{
|
|
125
|
-
id: "banner-link",
|
|
126
|
-
type: "url",
|
|
127
|
-
placeholder: "https://example.com",
|
|
128
|
-
value: link,
|
|
129
|
-
onChange: (e) => setLink(e.target.value),
|
|
130
|
-
disabled: isLoading,
|
|
131
|
-
className: "w-full"
|
|
132
|
-
}
|
|
133
|
-
),
|
|
134
|
-
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "Add a URL where users will be redirected when clicking this banner. Leave empty to remove the link." })
|
|
135
|
-
] }),
|
|
136
|
-
banner.link && /* @__PURE__ */ jsxs("div", { className: "p-3 bg-muted/50 rounded-lg", children: [
|
|
137
|
-
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground mb-1", children: "Current link:" }),
|
|
138
|
-
/* @__PURE__ */ jsx(
|
|
139
|
-
"a",
|
|
140
|
-
{
|
|
141
|
-
href: banner.link,
|
|
142
|
-
target: "_blank",
|
|
143
|
-
rel: "noopener noreferrer",
|
|
144
|
-
className: "text-sm text-primary hover:underline break-all",
|
|
145
|
-
children: banner.link
|
|
146
|
-
}
|
|
147
|
-
)
|
|
148
|
-
] }),
|
|
149
|
-
/* @__PURE__ */ jsxs("div", { className: "flex gap-3 justify-end", children: [
|
|
150
|
-
/* @__PURE__ */ jsx(
|
|
151
|
-
Button,
|
|
152
|
-
{
|
|
153
|
-
variant: "secondary",
|
|
154
|
-
onClick: onCancel,
|
|
155
|
-
disabled: isLoading,
|
|
156
|
-
children: "Cancel"
|
|
157
|
-
}
|
|
158
|
-
),
|
|
159
|
-
/* @__PURE__ */ jsx(
|
|
160
|
-
Button,
|
|
161
|
-
{
|
|
162
|
-
variant: "primary",
|
|
163
|
-
onClick: handleSave,
|
|
164
|
-
isLoading,
|
|
165
|
-
children: "Save Link"
|
|
166
|
-
}
|
|
167
|
-
)
|
|
168
|
-
] })
|
|
169
|
-
] });
|
|
170
|
-
};
|
|
171
|
-
const GridItem = ({
|
|
172
|
-
banner,
|
|
173
|
-
isSelected,
|
|
174
|
-
onClick,
|
|
175
|
-
onSelect,
|
|
176
|
-
onEditLink
|
|
177
|
-
}) => {
|
|
178
|
-
return /* @__PURE__ */ jsxs(
|
|
179
|
-
"div",
|
|
180
|
-
{
|
|
181
|
-
className: `
|
|
182
|
-
relative overflow-hidden rounded-lg shadow-md cursor-pointer hover:shadow-lg
|
|
183
|
-
transition-all duration-200 ease-in-out
|
|
184
|
-
bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700
|
|
185
|
-
${isSelected ? "ring-2 ring-offset-2 ring-blue-500 dark:ring-offset-gray-900" : ""}
|
|
186
|
-
`,
|
|
187
|
-
onClick,
|
|
188
|
-
children: [
|
|
189
|
-
/* @__PURE__ */ jsxs("div", { children: [
|
|
190
|
-
/* @__PURE__ */ jsx(
|
|
191
|
-
"img",
|
|
192
|
-
{
|
|
193
|
-
src: banner.url || "/placeholder.svg?height=200&width=300",
|
|
194
|
-
alt: `Banner ${banner.id}`,
|
|
195
|
-
className: "object-cover w-full h-48"
|
|
196
|
-
}
|
|
197
|
-
),
|
|
198
|
-
/* @__PURE__ */ jsx("div", { className: "p-2 flex items-center justify-between gap-2 bg-white dark:bg-gray-800", children: /* @__PURE__ */ jsxs("div", { className: "truncate text-sm flex items-center gap-2 text-gray-900 dark:text-gray-100", children: [
|
|
199
|
-
banner.link && /* @__PURE__ */ jsx(Link, { className: "h-3 w-3 text-blue-500 dark:text-blue-400 flex-shrink-0" }),
|
|
200
|
-
/* @__PURE__ */ jsx("span", { className: "truncate", children: banner.id })
|
|
201
|
-
] }) })
|
|
202
|
-
] }),
|
|
203
|
-
/* @__PURE__ */ jsx(
|
|
204
|
-
"div",
|
|
205
|
-
{
|
|
206
|
-
className: `
|
|
207
|
-
absolute top-2 left-2 w-6 h-6 rounded-md flex items-center justify-center
|
|
208
|
-
${isSelected ? "bg-blue-500 text-white" : "bg-white/90 dark:bg-gray-800/90 border border-gray-300 dark:border-gray-600"}
|
|
209
|
-
${isSelected ? "visible" : "invisible group-hover:visible"}
|
|
210
|
-
transition-opacity duration-200
|
|
211
|
-
`,
|
|
212
|
-
onClick: onSelect,
|
|
213
|
-
children: isSelected && /* @__PURE__ */ jsx(Check, { className: "h-4 w-4" })
|
|
214
|
-
}
|
|
215
|
-
),
|
|
216
|
-
/* @__PURE__ */ jsx("div", { className: "absolute top-2 right-2 invisible group-hover:visible", children: /* @__PURE__ */ jsx(
|
|
217
|
-
Button,
|
|
218
|
-
{
|
|
219
|
-
variant: "secondary",
|
|
220
|
-
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",
|
|
221
|
-
onClick: onEditLink,
|
|
222
|
-
children: /* @__PURE__ */ jsx(Edit, { className: "h-4 w-4 text-gray-700 dark:text-gray-300" })
|
|
223
|
-
}
|
|
224
|
-
) })
|
|
225
|
-
]
|
|
226
|
-
}
|
|
227
|
-
);
|
|
228
|
-
};
|
|
229
|
-
const SortableItem = ({ banner }) => {
|
|
230
|
-
const {
|
|
231
|
-
attributes,
|
|
232
|
-
listeners,
|
|
233
|
-
setNodeRef,
|
|
234
|
-
transform,
|
|
235
|
-
transition,
|
|
236
|
-
isDragging
|
|
237
|
-
} = useSortable({ id: banner.id });
|
|
238
|
-
const style = {
|
|
239
|
-
transform: CSS.Transform.toString(transform),
|
|
240
|
-
transition,
|
|
241
|
-
zIndex: isDragging ? 10 : 1,
|
|
242
|
-
opacity: isDragging ? 0.5 : 1
|
|
243
|
-
};
|
|
244
|
-
return /* @__PURE__ */ jsx(
|
|
245
|
-
"div",
|
|
246
|
-
{
|
|
247
|
-
ref: setNodeRef,
|
|
248
|
-
style,
|
|
249
|
-
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",
|
|
250
|
-
...attributes,
|
|
251
|
-
children: /* @__PURE__ */ jsxs("div", { ...listeners, children: [
|
|
252
|
-
/* @__PURE__ */ jsx(
|
|
253
|
-
"img",
|
|
254
|
-
{
|
|
255
|
-
src: banner.url,
|
|
256
|
-
alt: `Banner ${banner.id}`,
|
|
257
|
-
className: "object-cover w-full h-48 rounded-t-lg"
|
|
258
|
-
}
|
|
259
|
-
),
|
|
260
|
-
/* @__PURE__ */ jsx("div", { className: "p-2 text-sm truncate text-gray-900 dark:text-gray-100 bg-white dark:bg-gray-800", children: banner.id })
|
|
261
|
-
] })
|
|
262
|
-
}
|
|
263
|
-
);
|
|
264
|
-
};
|
|
265
|
-
function r(e) {
|
|
266
|
-
var t, f, n = "";
|
|
267
|
-
if ("string" == typeof e || "number" == typeof e) n += e;
|
|
268
|
-
else if ("object" == typeof e) if (Array.isArray(e)) for (t = 0; t < e.length; t++) e[t] && (f = r(e[t])) && (n && (n += " "), n += f);
|
|
269
|
-
else for (t in e) e[t] && (n && (n += " "), n += t);
|
|
270
|
-
return n;
|
|
271
|
-
}
|
|
272
|
-
function clsx() {
|
|
273
|
-
for (var e, t, f = 0, n = ""; f < arguments.length; ) (e = arguments[f++]) && (t = r(e)) && (n && (n += " "), n += t);
|
|
274
|
-
return n;
|
|
275
|
-
}
|
|
276
|
-
const UploadBannerDrawerContent = ({
|
|
277
|
-
maxFileSizeMb = 10,
|
|
278
|
-
onSuccess
|
|
279
|
-
}) => {
|
|
280
|
-
const [files, setFiles] = useState([]);
|
|
281
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
282
|
-
const onDrop = useCallback((acceptedFiles) => {
|
|
283
|
-
const newFiles = acceptedFiles.map((file) => {
|
|
284
|
-
const id = crypto.randomUUID();
|
|
285
|
-
let preview;
|
|
286
|
-
if (file.type.startsWith("image/")) {
|
|
287
|
-
preview = URL.createObjectURL(file);
|
|
288
|
-
}
|
|
289
|
-
return {
|
|
290
|
-
file,
|
|
291
|
-
id,
|
|
292
|
-
preview,
|
|
293
|
-
link: ""
|
|
294
|
-
};
|
|
295
|
-
});
|
|
296
|
-
setFiles((prev) => [...prev, ...newFiles]);
|
|
297
|
-
}, []);
|
|
298
|
-
const removeFile = (id) => {
|
|
299
|
-
setFiles((files2) => {
|
|
300
|
-
const fileToRemove = files2.find((f) => f.id === id);
|
|
301
|
-
if (fileToRemove == null ? void 0 : fileToRemove.preview) {
|
|
302
|
-
URL.revokeObjectURL(fileToRemove.preview);
|
|
303
|
-
}
|
|
304
|
-
return files2.filter((f) => f.id !== id);
|
|
305
|
-
});
|
|
306
|
-
};
|
|
307
|
-
const updateFileLink = (id, link) => {
|
|
308
|
-
setFiles((files2) => files2.map((f) => f.id === id ? { ...f, link } : f));
|
|
309
|
-
};
|
|
310
|
-
const handleResetFiles = () => {
|
|
311
|
-
setFiles([]);
|
|
312
|
-
};
|
|
313
|
-
const handleUploadFiles = async () => {
|
|
314
|
-
setIsLoading(true);
|
|
315
|
-
try {
|
|
316
|
-
const formData = new FormData();
|
|
317
|
-
files.forEach((file) => {
|
|
318
|
-
if (!file.file) {
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
formData.append("files", file.file);
|
|
322
|
-
});
|
|
323
|
-
const response = await fetch("/admin/banners", {
|
|
324
|
-
method: "POST",
|
|
325
|
-
body: formData,
|
|
326
|
-
credentials: "include"
|
|
327
|
-
});
|
|
328
|
-
if (!response.ok) {
|
|
329
|
-
const errorData = await response.json();
|
|
330
|
-
throw new Error(errorData.message || "Upload failed");
|
|
331
|
-
}
|
|
332
|
-
const data = await response.json();
|
|
333
|
-
const uploadedFiles = data.files;
|
|
334
|
-
const updatePromises = uploadedFiles.map(
|
|
335
|
-
async (uploadedFile, index) => {
|
|
336
|
-
var _a;
|
|
337
|
-
const fileLink = (_a = files[index]) == null ? void 0 : _a.link;
|
|
338
|
-
if (fileLink && fileLink.trim()) {
|
|
339
|
-
await fetch("/admin/banners/update-link", {
|
|
340
|
-
method: "POST",
|
|
341
|
-
headers: {
|
|
342
|
-
"Content-Type": "application/json"
|
|
343
|
-
},
|
|
344
|
-
credentials: "include",
|
|
345
|
-
body: JSON.stringify({
|
|
346
|
-
id: uploadedFile.id,
|
|
347
|
-
link: fileLink.trim()
|
|
348
|
-
})
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
|
-
return uploadedFile;
|
|
352
|
-
}
|
|
353
|
-
);
|
|
354
|
-
await Promise.all(updatePromises);
|
|
355
|
-
setFiles([]);
|
|
356
|
-
onSuccess(uploadedFiles);
|
|
357
|
-
return {
|
|
358
|
-
files,
|
|
359
|
-
uploadedFiles
|
|
360
|
-
};
|
|
361
|
-
} catch (error) {
|
|
362
|
-
console.error(error);
|
|
363
|
-
alert(
|
|
364
|
-
`Error uploading files: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
365
|
-
);
|
|
366
|
-
} finally {
|
|
367
|
-
setIsLoading(false);
|
|
368
|
-
}
|
|
369
|
-
};
|
|
370
|
-
const {
|
|
371
|
-
getRootProps,
|
|
372
|
-
getInputProps,
|
|
373
|
-
isDragActive,
|
|
374
|
-
isDragAccept,
|
|
375
|
-
isDragReject,
|
|
376
|
-
isFocused
|
|
377
|
-
} = useDropzone({
|
|
378
|
-
onDrop,
|
|
379
|
-
accept: {
|
|
380
|
-
"image/*": []
|
|
381
|
-
},
|
|
382
|
-
maxSize: maxFileSizeMb * 1024 * 1024
|
|
383
|
-
});
|
|
384
|
-
return /* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
|
|
385
|
-
/* @__PURE__ */ jsxs(
|
|
386
|
-
"div",
|
|
387
|
-
{
|
|
388
|
-
...getRootProps(),
|
|
389
|
-
className: clsx(
|
|
390
|
-
"border-2 border-dashed rounded-xl p-10 transition-all duration-150 ease-in-out cursor-pointer",
|
|
391
|
-
"flex flex-col items-center justify-center gap-4",
|
|
392
|
-
isDragActive ? "bg-primary/5 border-primary/50" : "bg-background hover:bg-muted/50",
|
|
393
|
-
isDragAccept ? "border-green-500 bg-green-50 dark:bg-green-950/20" : "",
|
|
394
|
-
isDragReject ? "border-red-500 bg-red-50 dark:bg-red-950/20" : "",
|
|
395
|
-
isFocused ? "ring-2 ring-ring ring-offset-2" : "",
|
|
396
|
-
"focus-visible:outline-none"
|
|
397
|
-
),
|
|
398
|
-
children: [
|
|
399
|
-
/* @__PURE__ */ jsx("input", { ...getInputProps(), disabled: isLoading }),
|
|
400
|
-
/* @__PURE__ */ jsx("div", { className: "rounded-full bg-primary/10 p-4", children: /* @__PURE__ */ jsx(Upload, { className: "h-8 w-8 text-primary" }) }),
|
|
401
|
-
/* @__PURE__ */ jsxs("div", { className: "text-center space-y-2", children: [
|
|
402
|
-
/* @__PURE__ */ jsx("h3", { className: "font-medium text-lg", children: isDragActive ? isDragAccept ? "Drop files to upload" : "This file type is not supported" : "Drag & drop images here" }),
|
|
403
|
-
/* @__PURE__ */ jsxs("p", { className: "text-sm text-muted-foreground", children: [
|
|
404
|
-
"or ",
|
|
405
|
-
/* @__PURE__ */ jsx("span", { className: "text-primary font-medium", children: "browse images" }),
|
|
406
|
-
" ",
|
|
407
|
-
"from your computer"
|
|
408
|
-
] }),
|
|
409
|
-
/* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
|
|
410
|
-
"(max ",
|
|
411
|
-
maxFileSizeMb,
|
|
412
|
-
"MB)"
|
|
413
|
-
] })
|
|
414
|
-
] })
|
|
415
|
-
]
|
|
416
|
-
}
|
|
417
|
-
),
|
|
418
|
-
files.length > 0 && /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
419
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
420
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-4", children: [
|
|
421
|
-
/* @__PURE__ */ jsxs("h3", { className: "text-lg font-medium", children: [
|
|
422
|
-
"Selected Files (",
|
|
423
|
-
files.length,
|
|
424
|
-
")"
|
|
425
|
-
] }),
|
|
426
|
-
/* @__PURE__ */ jsx(
|
|
427
|
-
Button,
|
|
428
|
-
{
|
|
429
|
-
isLoading,
|
|
430
|
-
variant: "primary",
|
|
431
|
-
onClick: handleUploadFiles,
|
|
432
|
-
children: "Confirm Upload"
|
|
433
|
-
}
|
|
434
|
-
)
|
|
435
|
-
] }),
|
|
436
|
-
/* @__PURE__ */ jsx(
|
|
437
|
-
Button,
|
|
438
|
-
{
|
|
439
|
-
isLoading,
|
|
440
|
-
variant: "secondary",
|
|
441
|
-
onClick: handleResetFiles,
|
|
442
|
-
children: "Clear All"
|
|
443
|
-
}
|
|
444
|
-
)
|
|
445
|
-
] }),
|
|
446
|
-
/* @__PURE__ */ jsx("div", { className: "grid gap-6 grid-cols-1", children: files.map((fileItem) => /* @__PURE__ */ jsx(
|
|
447
|
-
"div",
|
|
448
|
-
{
|
|
449
|
-
className: "rounded-lg border bg-card text-card-foreground shadow-sm overflow-hidden",
|
|
450
|
-
children: /* @__PURE__ */ jsxs("div", { className: "flex gap-4 p-4", children: [
|
|
451
|
-
/* @__PURE__ */ jsx("div", { className: "flex-shrink-0", children: fileItem.preview ? /* @__PURE__ */ jsx("div", { className: "w-32 h-32 overflow-hidden rounded-lg", children: /* @__PURE__ */ jsx(
|
|
452
|
-
"img",
|
|
453
|
-
{
|
|
454
|
-
src: fileItem.preview || "/placeholder.svg",
|
|
455
|
-
alt: fileItem.file.name,
|
|
456
|
-
className: "w-full h-full object-cover"
|
|
457
|
-
}
|
|
458
|
-
) }) : /* @__PURE__ */ jsx("div", { className: "w-32 h-32 flex items-center justify-center bg-muted/50 rounded-lg", children: /* @__PURE__ */ jsx(ImageIcon, { className: "h-8 w-8 text-primary" }) }) }),
|
|
459
|
-
/* @__PURE__ */ jsxs("div", { className: "flex-1 space-y-3", children: [
|
|
460
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between", children: [
|
|
461
|
-
/* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
|
|
462
|
-
/* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: fileItem.file.name }),
|
|
463
|
-
/* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
|
|
464
|
-
(fileItem.file.size / 1024).toFixed(1),
|
|
465
|
-
" KB"
|
|
466
|
-
] })
|
|
467
|
-
] }),
|
|
468
|
-
/* @__PURE__ */ jsxs(
|
|
469
|
-
Button,
|
|
470
|
-
{
|
|
471
|
-
isLoading,
|
|
472
|
-
variant: "secondary",
|
|
473
|
-
className: "size-8 rounded-full p-0.5 text-red-400",
|
|
474
|
-
onClick: (e) => {
|
|
475
|
-
e.stopPropagation();
|
|
476
|
-
removeFile(fileItem.id);
|
|
477
|
-
},
|
|
478
|
-
children: [
|
|
479
|
-
/* @__PURE__ */ jsx(Trash, { className: "h-4 w-4" }),
|
|
480
|
-
/* @__PURE__ */ jsx("span", { className: "sr-only", children: "Remove file" })
|
|
481
|
-
]
|
|
482
|
-
}
|
|
483
|
-
)
|
|
484
|
-
] }),
|
|
485
|
-
/* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
486
|
-
/* @__PURE__ */ jsxs(
|
|
487
|
-
Label,
|
|
488
|
-
{
|
|
489
|
-
htmlFor: `link-${fileItem.id}`,
|
|
490
|
-
className: "text-sm font-medium flex items-center gap-2",
|
|
491
|
-
children: [
|
|
492
|
-
/* @__PURE__ */ jsx(Link, { className: "h-4 w-4" }),
|
|
493
|
-
"Click URL (optional)"
|
|
494
|
-
]
|
|
495
|
-
}
|
|
496
|
-
),
|
|
497
|
-
/* @__PURE__ */ jsx(
|
|
498
|
-
Input,
|
|
499
|
-
{
|
|
500
|
-
id: `link-${fileItem.id}`,
|
|
501
|
-
type: "url",
|
|
502
|
-
placeholder: "https://example.com",
|
|
503
|
-
value: fileItem.link || "",
|
|
504
|
-
onChange: (e) => updateFileLink(fileItem.id, e.target.value),
|
|
505
|
-
disabled: isLoading,
|
|
506
|
-
className: "w-full"
|
|
507
|
-
}
|
|
508
|
-
),
|
|
509
|
-
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "Add a URL where users will be redirected when clicking this banner" })
|
|
510
|
-
] })
|
|
511
|
-
] })
|
|
512
|
-
] })
|
|
513
|
-
},
|
|
514
|
-
fileItem.id
|
|
515
|
-
)) })
|
|
516
|
-
] })
|
|
517
|
-
] });
|
|
518
|
-
};
|
|
519
|
-
const BannerPage = () => {
|
|
520
|
-
const [open, setOpen] = useState(false);
|
|
521
|
-
const [editLinkOpen, setEditLinkOpen] = useState(false);
|
|
522
|
-
const [selectedBannerForEdit, setSelectedBannerForEdit] = useState(null);
|
|
523
|
-
const [selectedImage, setSelectedImage] = useState(null);
|
|
524
|
-
const [selectedIds, setSelectedIds] = useState(/* @__PURE__ */ new Set());
|
|
525
|
-
const [reorderMode, setReorderMode] = useState(false);
|
|
526
|
-
const [orderedBanners, setOrderedBanners] = useState([]);
|
|
527
|
-
const [activeBanner, setActiveBanner] = useState(null);
|
|
528
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
529
|
-
const dialog = usePrompt();
|
|
530
|
-
const { data: bannerData, refetch } = useBanners();
|
|
531
|
-
const { count = 0, banners = [] } = bannerData || {};
|
|
532
|
-
const sensors = useSensors(
|
|
533
|
-
useSensor(PointerSensor, {
|
|
534
|
-
activationConstraint: {
|
|
535
|
-
distance: 8
|
|
536
|
-
// 8px movement required before drag starts
|
|
537
|
-
}
|
|
538
|
-
}),
|
|
539
|
-
useSensor(KeyboardSensor, {
|
|
540
|
-
coordinateGetter: sortableKeyboardCoordinates
|
|
541
|
-
})
|
|
542
|
-
);
|
|
543
|
-
const toggleImageSelection = (e, id) => {
|
|
544
|
-
e.stopPropagation();
|
|
545
|
-
const newSelectedIds = new Set(selectedIds);
|
|
546
|
-
if (newSelectedIds.has(id)) {
|
|
547
|
-
newSelectedIds.delete(id);
|
|
548
|
-
} else {
|
|
549
|
-
newSelectedIds.add(id);
|
|
550
|
-
}
|
|
551
|
-
setSelectedIds(newSelectedIds);
|
|
552
|
-
};
|
|
553
|
-
const selectAll = () => {
|
|
554
|
-
const allIds = banners.map((banner) => banner.id);
|
|
555
|
-
setSelectedIds(new Set(allIds));
|
|
556
|
-
};
|
|
557
|
-
const deselectAll = () => {
|
|
558
|
-
setSelectedIds(/* @__PURE__ */ new Set());
|
|
559
|
-
};
|
|
560
|
-
const openLightbox = (banner) => {
|
|
561
|
-
setSelectedImage(banner);
|
|
562
|
-
};
|
|
563
|
-
const closeLightbox = () => {
|
|
564
|
-
setSelectedImage(null);
|
|
565
|
-
};
|
|
566
|
-
const openEditLinkDrawer = (e, banner) => {
|
|
567
|
-
e.stopPropagation();
|
|
568
|
-
setSelectedBannerForEdit(banner);
|
|
569
|
-
setEditLinkOpen(true);
|
|
570
|
-
};
|
|
571
|
-
const closeEditLinkDrawer = () => {
|
|
572
|
-
setEditLinkOpen(false);
|
|
573
|
-
setSelectedBannerForEdit(null);
|
|
574
|
-
};
|
|
575
|
-
const toggleReorderMode = async () => {
|
|
576
|
-
if (reorderMode) {
|
|
577
|
-
const confirmed = await dialog({
|
|
578
|
-
title: "Save Changes?",
|
|
579
|
-
description: "Do you want to save your reordering changes?",
|
|
580
|
-
variant: "confirmation",
|
|
581
|
-
confirmText: "Save",
|
|
582
|
-
cancelText: "Discard"
|
|
583
|
-
});
|
|
584
|
-
if (confirmed) {
|
|
585
|
-
saveNewOrder();
|
|
586
|
-
} else {
|
|
587
|
-
setOrderedBanners([]);
|
|
588
|
-
setReorderMode(false);
|
|
589
|
-
}
|
|
590
|
-
} else {
|
|
591
|
-
setOrderedBanners([...banners]);
|
|
592
|
-
setReorderMode(true);
|
|
593
|
-
}
|
|
594
|
-
};
|
|
595
|
-
const handleDragStart = (event) => {
|
|
596
|
-
const { active } = event;
|
|
597
|
-
const activeBanner2 = orderedBanners.find(
|
|
598
|
-
(banner) => banner.id === active.id
|
|
599
|
-
);
|
|
600
|
-
if (activeBanner2) {
|
|
601
|
-
setActiveBanner(activeBanner2);
|
|
602
|
-
}
|
|
603
|
-
};
|
|
604
|
-
const handleDragEnd = (event) => {
|
|
605
|
-
const { active, over } = event;
|
|
606
|
-
if (over && active.id !== over.id) {
|
|
607
|
-
setOrderedBanners((items) => {
|
|
608
|
-
const oldIndex = items.findIndex((item) => item.id === active.id);
|
|
609
|
-
const newIndex = items.findIndex((item) => item.id === over.id);
|
|
610
|
-
return arrayMove(items, oldIndex, newIndex);
|
|
611
|
-
});
|
|
612
|
-
}
|
|
613
|
-
setActiveBanner(null);
|
|
614
|
-
};
|
|
615
|
-
const saveNewOrder = async () => {
|
|
616
|
-
try {
|
|
617
|
-
await sdk.client.fetch("/admin/banners/reorder", {
|
|
618
|
-
method: "POST",
|
|
619
|
-
headers: {
|
|
620
|
-
"Content-Type": "application/json"
|
|
621
|
-
},
|
|
622
|
-
body: JSON.stringify({
|
|
623
|
-
ids: orderedBanners.map((banner) => banner.id)
|
|
624
|
-
})
|
|
625
|
-
});
|
|
626
|
-
setReorderMode(false);
|
|
627
|
-
refetch();
|
|
628
|
-
} catch (error) {
|
|
629
|
-
console.error("Failed to save new order:", error);
|
|
630
|
-
}
|
|
631
|
-
};
|
|
632
|
-
const handleDeleteMultiple = async () => {
|
|
633
|
-
const isConfirmed = await dialog({
|
|
634
|
-
title: "Confirm Deletion",
|
|
635
|
-
description: `Are you sure you want to delete ${selectedIds.size} selected images? This action cannot be undone.`,
|
|
636
|
-
confirmText: "Delete",
|
|
637
|
-
cancelText: "Cancel"
|
|
638
|
-
});
|
|
639
|
-
if (!isConfirmed) return;
|
|
640
|
-
const ids = Array.from(selectedIds);
|
|
641
|
-
setIsLoading(true);
|
|
642
|
-
try {
|
|
643
|
-
await fetch("/admin/banners", {
|
|
644
|
-
method: "DELETE",
|
|
645
|
-
headers: {
|
|
646
|
-
"Content-Type": "application/json"
|
|
647
|
-
},
|
|
648
|
-
body: JSON.stringify({ ids })
|
|
649
|
-
});
|
|
650
|
-
setSelectedIds(/* @__PURE__ */ new Set());
|
|
651
|
-
refetch();
|
|
652
|
-
} catch {
|
|
653
|
-
console.error("Failed to delete banners");
|
|
654
|
-
} finally {
|
|
655
|
-
setIsLoading(false);
|
|
656
|
-
}
|
|
657
|
-
};
|
|
658
|
-
return /* @__PURE__ */ jsx("div", { className: "space-y-6", children: /* @__PURE__ */ jsxs("div", { className: "container mx-auto px-4 py-8", children: [
|
|
659
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-6", children: [
|
|
660
|
-
/* @__PURE__ */ jsxs("h2", { className: "text-2xl font-bold", children: [
|
|
661
|
-
"Banners (",
|
|
662
|
-
count,
|
|
663
|
-
")"
|
|
664
|
-
] }),
|
|
665
|
-
/* @__PURE__ */ jsxs(
|
|
666
|
-
Drawer,
|
|
667
|
-
{
|
|
668
|
-
open,
|
|
669
|
-
onOpenChange: (openChanged) => setOpen(openChanged),
|
|
670
|
-
children: [
|
|
671
|
-
/* @__PURE__ */ jsx(
|
|
672
|
-
Drawer.Trigger,
|
|
673
|
-
{
|
|
674
|
-
onClick: () => {
|
|
675
|
-
setOpen(true);
|
|
676
|
-
},
|
|
677
|
-
asChild: true,
|
|
678
|
-
children: /* @__PURE__ */ jsx(Button, { isLoading, children: "Upload new banner" })
|
|
679
|
-
}
|
|
680
|
-
),
|
|
681
|
-
/* @__PURE__ */ jsxs(Drawer.Content, { children: [
|
|
682
|
-
/* @__PURE__ */ jsx(Drawer.Header, { children: /* @__PURE__ */ jsx(Drawer.Title, { children: "Upload new banner" }) }),
|
|
683
|
-
/* @__PURE__ */ jsx(Drawer.Body, { children: /* @__PURE__ */ jsx(
|
|
684
|
-
UploadBannerDrawerContent,
|
|
685
|
-
{
|
|
686
|
-
maxFileSizeMb: 10,
|
|
687
|
-
onSuccess: () => {
|
|
688
|
-
setOpen(false);
|
|
689
|
-
refetch();
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
) })
|
|
693
|
-
] })
|
|
694
|
-
]
|
|
695
|
-
}
|
|
696
|
-
)
|
|
697
|
-
] }),
|
|
698
|
-
/* @__PURE__ */ jsx(
|
|
699
|
-
Drawer,
|
|
700
|
-
{
|
|
701
|
-
open: editLinkOpen,
|
|
702
|
-
onOpenChange: (openChanged) => {
|
|
703
|
-
if (!openChanged) {
|
|
704
|
-
closeEditLinkDrawer();
|
|
705
|
-
}
|
|
706
|
-
},
|
|
707
|
-
children: /* @__PURE__ */ jsxs(Drawer.Content, { children: [
|
|
708
|
-
/* @__PURE__ */ jsx(Drawer.Header, { children: /* @__PURE__ */ jsx(Drawer.Title, { children: "Edit Banner Link" }) }),
|
|
709
|
-
/* @__PURE__ */ jsx(Drawer.Body, { children: selectedBannerForEdit && /* @__PURE__ */ jsx(
|
|
710
|
-
EditBannerLinkDrawerContent,
|
|
711
|
-
{
|
|
712
|
-
banner: selectedBannerForEdit,
|
|
713
|
-
onSuccess: () => {
|
|
714
|
-
closeEditLinkDrawer();
|
|
715
|
-
refetch();
|
|
716
|
-
},
|
|
717
|
-
onCancel: closeEditLinkDrawer
|
|
718
|
-
}
|
|
719
|
-
) })
|
|
720
|
-
] })
|
|
721
|
-
}
|
|
722
|
-
),
|
|
723
|
-
/* @__PURE__ */ jsxs("div", { className: "container mx-auto px-4 py-8", children: [
|
|
724
|
-
/* @__PURE__ */ jsxs("div", { className: "flex justify-between items-center mb-6", children: [
|
|
725
|
-
selectedIds.size > 0 && !reorderMode && /* @__PURE__ */ jsx("div", { className: "flex gap-2", children: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
726
|
-
/* @__PURE__ */ jsx(
|
|
727
|
-
Button,
|
|
728
|
-
{
|
|
729
|
-
isLoading,
|
|
730
|
-
onClick: selectAll,
|
|
731
|
-
disabled: count === 0,
|
|
732
|
-
children: "Select All"
|
|
733
|
-
}
|
|
734
|
-
),
|
|
735
|
-
/* @__PURE__ */ jsx(
|
|
736
|
-
Button,
|
|
737
|
-
{
|
|
738
|
-
isLoading,
|
|
739
|
-
onClick: deselectAll,
|
|
740
|
-
disabled: selectedIds.size === 0,
|
|
741
|
-
children: "Deselect All"
|
|
742
|
-
}
|
|
743
|
-
),
|
|
744
|
-
/* @__PURE__ */ jsxs(
|
|
745
|
-
Button,
|
|
746
|
-
{
|
|
747
|
-
isLoading,
|
|
748
|
-
variant: "danger",
|
|
749
|
-
onClick: () => handleDeleteMultiple(),
|
|
750
|
-
disabled: selectedIds.size === 0,
|
|
751
|
-
children: [
|
|
752
|
-
/* @__PURE__ */ jsx(Trash2, { className: "h-4 w-4" }),
|
|
753
|
-
"Delete (",
|
|
754
|
-
selectedIds.size,
|
|
755
|
-
")"
|
|
756
|
-
]
|
|
757
|
-
}
|
|
758
|
-
)
|
|
759
|
-
] }) }),
|
|
760
|
-
/* @__PURE__ */ jsxs("div", { className: "ml-auto", children: [
|
|
761
|
-
banners.length > 0 && /* @__PURE__ */ jsxs(
|
|
762
|
-
Button,
|
|
763
|
-
{
|
|
764
|
-
isLoading,
|
|
765
|
-
onClick: toggleReorderMode,
|
|
766
|
-
variant: reorderMode ? "secondary" : "primary",
|
|
767
|
-
children: [
|
|
768
|
-
/* @__PURE__ */ jsx(MoveHorizontal, { className: "h-4 w-4 mr-2" }),
|
|
769
|
-
reorderMode ? "Exit Reorder" : "Reorder"
|
|
770
|
-
]
|
|
771
|
-
}
|
|
772
|
-
),
|
|
773
|
-
reorderMode && /* @__PURE__ */ jsxs(
|
|
774
|
-
Button,
|
|
775
|
-
{
|
|
776
|
-
isLoading,
|
|
777
|
-
onClick: saveNewOrder,
|
|
778
|
-
variant: "primary",
|
|
779
|
-
className: "ml-2",
|
|
780
|
-
children: [
|
|
781
|
-
/* @__PURE__ */ jsx(Check, { className: "h-4 w-4 mr-2" }),
|
|
782
|
-
"Save Order"
|
|
783
|
-
]
|
|
784
|
-
}
|
|
785
|
-
)
|
|
786
|
-
] })
|
|
787
|
-
] }),
|
|
788
|
-
reorderMode ? /* @__PURE__ */ jsxs(
|
|
789
|
-
DndContext,
|
|
790
|
-
{
|
|
791
|
-
sensors,
|
|
792
|
-
collisionDetection: closestCenter,
|
|
793
|
-
onDragStart: handleDragStart,
|
|
794
|
-
onDragEnd: handleDragEnd,
|
|
795
|
-
children: [
|
|
796
|
-
/* @__PURE__ */ jsx(
|
|
797
|
-
SortableContext,
|
|
798
|
-
{
|
|
799
|
-
items: orderedBanners.map((banner) => banner.id),
|
|
800
|
-
strategy: rectSortingStrategy,
|
|
801
|
-
children: /* @__PURE__ */ jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 isolate", children: orderedBanners.map((banner) => /* @__PURE__ */ jsx(SortableItem, { banner }, banner.id)) })
|
|
802
|
-
}
|
|
803
|
-
),
|
|
804
|
-
/* @__PURE__ */ jsx(DragOverlay, { children: activeBanner ? /* @__PURE__ */ jsx(DraggableItem, { banner: activeBanner }) : null })
|
|
805
|
-
]
|
|
806
|
-
}
|
|
807
|
-
) : /* @__PURE__ */ jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 group", children: banners.map((banner) => /* @__PURE__ */ jsx(
|
|
808
|
-
GridItem,
|
|
809
|
-
{
|
|
810
|
-
banner,
|
|
811
|
-
isSelected: selectedIds.has(banner.id),
|
|
812
|
-
onClick: () => openLightbox(banner),
|
|
813
|
-
onSelect: (e) => toggleImageSelection(e, banner.id),
|
|
814
|
-
onEditLink: (e) => openEditLinkDrawer(e, banner)
|
|
815
|
-
},
|
|
816
|
-
banner.id
|
|
817
|
-
)) }),
|
|
818
|
-
selectedImage && /* @__PURE__ */ jsxs(
|
|
819
|
-
"div",
|
|
820
|
-
{
|
|
821
|
-
className: "fixed inset-0 bg-black/80 z-50 flex items-center justify-center p-4",
|
|
822
|
-
onClick: closeLightbox,
|
|
823
|
-
children: [
|
|
824
|
-
/* @__PURE__ */ jsx(
|
|
825
|
-
Button,
|
|
826
|
-
{
|
|
827
|
-
isLoading,
|
|
828
|
-
className: "absolute top-4 right-4 text-white bg-black/50 p-2 rounded-full hover:bg-black/70",
|
|
829
|
-
onClick: (e) => {
|
|
830
|
-
e.stopPropagation();
|
|
831
|
-
closeLightbox();
|
|
832
|
-
},
|
|
833
|
-
children: /* @__PURE__ */ jsx(X, { className: "h-6 w-6" })
|
|
834
|
-
}
|
|
835
|
-
),
|
|
836
|
-
/* @__PURE__ */ jsx(
|
|
837
|
-
"div",
|
|
838
|
-
{
|
|
839
|
-
className: "relative max-w-screen-xl max-h-screen overflow-auto",
|
|
840
|
-
onClick: (e) => e.stopPropagation(),
|
|
841
|
-
children: /* @__PURE__ */ jsx(
|
|
842
|
-
"img",
|
|
843
|
-
{
|
|
844
|
-
src: selectedImage.url || "/placeholder.svg",
|
|
845
|
-
alt: `Banner ${selectedImage.id}`,
|
|
846
|
-
className: "max-h-[90vh] max-w-full object-contain mx-auto"
|
|
847
|
-
}
|
|
848
|
-
)
|
|
849
|
-
}
|
|
850
|
-
)
|
|
851
|
-
]
|
|
852
|
-
}
|
|
853
|
-
)
|
|
854
|
-
] })
|
|
855
|
-
] }) });
|
|
856
|
-
};
|
|
857
|
-
const BannerUpload = () => {
|
|
858
|
-
return /* @__PURE__ */ jsx(BannerPage, {});
|
|
859
|
-
};
|
|
860
|
-
const config = defineRouteConfig({
|
|
861
|
-
label: "Banner",
|
|
862
|
-
icon: StackPerspective
|
|
863
|
-
});
|
|
864
|
-
const widgetModule = { widgets: [] };
|
|
865
|
-
const routeModule = {
|
|
866
|
-
routes: [
|
|
867
|
-
{
|
|
868
|
-
Component: BannerUpload,
|
|
869
|
-
path: "/banners"
|
|
870
|
-
}
|
|
871
|
-
]
|
|
872
|
-
};
|
|
873
|
-
const menuItemModule = {
|
|
874
|
-
menuItems: [
|
|
875
|
-
{
|
|
876
|
-
label: config.label,
|
|
877
|
-
icon: config.icon,
|
|
878
|
-
path: "/banners",
|
|
879
|
-
nested: void 0
|
|
880
|
-
}
|
|
881
|
-
]
|
|
882
|
-
};
|
|
883
|
-
const formModule = { customFields: {} };
|
|
884
|
-
const displayModule = {
|
|
885
|
-
displays: {}
|
|
886
|
-
};
|
|
887
|
-
const i18nModule = { resources: {} };
|
|
888
|
-
const plugin = {
|
|
889
|
-
widgetModule,
|
|
890
|
-
routeModule,
|
|
891
|
-
menuItemModule,
|
|
892
|
-
formModule,
|
|
893
|
-
displayModule,
|
|
894
|
-
i18nModule
|
|
895
|
-
};
|
|
896
|
-
export {
|
|
897
|
-
plugin as default
|
|
898
|
-
};
|