@lodashventure/medusa-brand 1.2.20 → 1.2.23
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 +1191 -556
- package/.medusa/server/src/admin/index.mjs +1165 -530
- package/.medusa/server/src/api/admin/brands/[id]/image/route.js +178 -0
- package/.medusa/server/src/api/admin/brands/[id]/logo/route.js +179 -0
- package/.medusa/server/src/api/admin/brands/[id]/products/route.js +55 -0
- package/.medusa/server/src/api/admin/brands/[id]/route.js +251 -0
- package/.medusa/server/src/api/admin/brands/route.js +276 -0
- package/.medusa/server/src/api/admin/products/[id]/brand/route.js +117 -0
- package/.medusa/server/src/api/middlewares/attach-brand-to-products.js +110 -0
- package/.medusa/server/src/api/middlewares.js +53 -0
- package/.medusa/server/src/api/store/brands/[id]/route.js +31 -0
- package/.medusa/server/src/api/store/brands/route.js +99 -0
- package/.medusa/server/{index.js → src/index.js} +1 -1
- package/.medusa/server/{modules → src/modules}/brand/index.js +1 -1
- package/.medusa/server/src/modules/brand/models/brand.js +40 -0
- package/.medusa/server/{modules → src/modules}/brand/service.js +1 -1
- package/.medusa/server/src/services/gcs-direct-upload.js +93 -0
- package/.medusa/server/src/workflows/upload-brand-image.js +66 -0
- package/package.json +11 -10
- package/.medusa/server/api/admin/brands/[id]/image/route.d.ts +0 -5
- package/.medusa/server/api/admin/brands/[id]/image/route.js +0 -119
- package/.medusa/server/api/admin/brands/[id]/logo/route.d.ts +0 -5
- package/.medusa/server/api/admin/brands/[id]/logo/route.js +0 -119
- package/.medusa/server/api/admin/brands/[id]/products/route.d.ts +0 -2
- package/.medusa/server/api/admin/brands/[id]/products/route.js +0 -52
- package/.medusa/server/api/admin/brands/[id]/route.d.ts +0 -5
- package/.medusa/server/api/admin/brands/[id]/route.js +0 -112
- package/.medusa/server/api/admin/brands/route.d.ts +0 -4
- package/.medusa/server/api/admin/brands/route.js +0 -76
- package/.medusa/server/api/admin/products/[id]/brand/route.d.ts +0 -5
- package/.medusa/server/api/admin/products/[id]/brand/route.js +0 -117
- package/.medusa/server/api/middlewares/attach-brand-to-products.d.ts +0 -2
- package/.medusa/server/api/middlewares/attach-brand-to-products.js +0 -105
- package/.medusa/server/api/middlewares.d.ts +0 -6
- package/.medusa/server/api/middlewares.js +0 -27
- package/.medusa/server/api/store/brands/route.d.ts +0 -2
- package/.medusa/server/api/store/brands/route.js +0 -53
- package/.medusa/server/index.d.ts +0 -1
- package/.medusa/server/modules/brand/index.d.ts +0 -35
- package/.medusa/server/modules/brand/migrations/Migration20251021070648.d.ts +0 -5
- package/.medusa/server/modules/brand/migrations/Migration20251021070648.js +0 -28
- package/.medusa/server/modules/brand/models/brand.d.ts +0 -16
- package/.medusa/server/modules/brand/models/brand.js +0 -43
- package/.medusa/server/modules/brand/service.d.ts +0 -21
- package/.medusa/server/services/gcs-direct-upload.d.ts +0 -8
- package/.medusa/server/services/gcs-direct-upload.js +0 -55
- package/.medusa/server/workflows/upload-brand-image.d.ts +0 -15
- package/.medusa/server/workflows/upload-brand-image.js +0 -57
|
@@ -1,17 +1,254 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
+
import React, { useState, useCallback, useEffect, useMemo } from "react";
|
|
2
3
|
import { defineWidgetConfig, defineRouteConfig } from "@medusajs/admin-sdk";
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { useState, useEffect, useCallback } from "react";
|
|
4
|
+
import { Container, Heading, Alert, Text, Button, Label, Input, Select, Badge, Switch, Textarea, clx, toast, usePrompt, DropdownMenu, Table, IconButton } from "@medusajs/ui";
|
|
5
|
+
import { BuildingStorefront, Trash, MagnifyingGlass, Link, XMark, CloudArrowUp, PhotoSolid, Plus, EllipsisHorizontal, PencilSquare, ChevronLeft, ChevronRight } from "@medusajs/icons";
|
|
6
|
+
import { useForm, Controller } from "react-hook-form";
|
|
7
7
|
import "@medusajs/admin-shared";
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
type: "session"
|
|
8
|
+
const DEFAULT_BASE_URL = typeof window !== "undefined" ? window.location.origin : "/";
|
|
9
|
+
const resolveUrl = (input) => {
|
|
10
|
+
if (input instanceof URL) {
|
|
11
|
+
return input;
|
|
13
12
|
}
|
|
14
|
-
|
|
13
|
+
try {
|
|
14
|
+
return new URL(input);
|
|
15
|
+
} catch {
|
|
16
|
+
const sanitizedPath = input.replace(/^[\/]+/, "");
|
|
17
|
+
const base = DEFAULT_BASE_URL.replace(/\/$/, "");
|
|
18
|
+
return new URL(`${base}/${sanitizedPath}`);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
class FetchError extends Error {
|
|
22
|
+
constructor(message, statusText, status) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.statusText = statusText;
|
|
25
|
+
this.status = status;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const buildHeaders = (headers) => {
|
|
29
|
+
const resolved = new Headers();
|
|
30
|
+
if (!headers) {
|
|
31
|
+
return resolved;
|
|
32
|
+
}
|
|
33
|
+
if (headers instanceof Headers) {
|
|
34
|
+
headers.forEach((value, key) => {
|
|
35
|
+
if (value != null) {
|
|
36
|
+
resolved.append(key, value);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
return resolved;
|
|
40
|
+
}
|
|
41
|
+
const appendEntry = (key, value) => {
|
|
42
|
+
if (value === void 0 || value === null) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
resolved.append(key, String(value));
|
|
46
|
+
};
|
|
47
|
+
if (Array.isArray(headers)) {
|
|
48
|
+
headers.forEach(([key, value]) => appendEntry(key, value));
|
|
49
|
+
return resolved;
|
|
50
|
+
}
|
|
51
|
+
Object.entries(headers).forEach(
|
|
52
|
+
([key, value]) => appendEntry(key, value)
|
|
53
|
+
);
|
|
54
|
+
return resolved;
|
|
55
|
+
};
|
|
56
|
+
const normalizeRequestInit = (init, resolvedHeaders) => {
|
|
57
|
+
var _a;
|
|
58
|
+
const requestInit = {
|
|
59
|
+
...init,
|
|
60
|
+
headers: resolvedHeaders,
|
|
61
|
+
credentials: "include"
|
|
62
|
+
};
|
|
63
|
+
const body = init == null ? void 0 : init.body;
|
|
64
|
+
const isFormData = typeof FormData !== "undefined" && body instanceof FormData;
|
|
65
|
+
if (!resolvedHeaders.has("accept")) {
|
|
66
|
+
resolvedHeaders.set("accept", "application/json");
|
|
67
|
+
}
|
|
68
|
+
if (!isFormData && body && typeof body === "object") {
|
|
69
|
+
if (!resolvedHeaders.has("content-type")) {
|
|
70
|
+
resolvedHeaders.set("content-type", "application/json");
|
|
71
|
+
}
|
|
72
|
+
if (((_a = resolvedHeaders.get("content-type")) == null ? void 0 : _a.includes("application/json")) && !(body instanceof ArrayBuffer) && !(body instanceof Blob)) {
|
|
73
|
+
requestInit.body = JSON.stringify(body);
|
|
74
|
+
return requestInit;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
requestInit.body = body;
|
|
78
|
+
return requestInit;
|
|
79
|
+
};
|
|
80
|
+
const parseResponse = async (response, requestHeaders) => {
|
|
81
|
+
var _a;
|
|
82
|
+
if (!response.ok) {
|
|
83
|
+
let message = response.statusText || "Request failed";
|
|
84
|
+
try {
|
|
85
|
+
const errorBody = await response.clone().json();
|
|
86
|
+
if (errorBody == null ? void 0 : errorBody.message) {
|
|
87
|
+
message = errorBody.message;
|
|
88
|
+
}
|
|
89
|
+
} catch {
|
|
90
|
+
}
|
|
91
|
+
throw new FetchError(message, response.statusText, response.status);
|
|
92
|
+
}
|
|
93
|
+
const wantsJson = ((_a = requestHeaders.get("accept")) == null ? void 0 : _a.includes("application/json")) ?? false;
|
|
94
|
+
if (!wantsJson) {
|
|
95
|
+
return response;
|
|
96
|
+
}
|
|
97
|
+
if (response.status === 204) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
return await response.json();
|
|
102
|
+
} catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
const clientFetch = async (input, init) => {
|
|
107
|
+
const url = resolveUrl(input);
|
|
108
|
+
const headers = buildHeaders(init == null ? void 0 : init.headers);
|
|
109
|
+
const requestInit = normalizeRequestInit(init, headers);
|
|
110
|
+
const response = await fetch(url, requestInit);
|
|
111
|
+
return await parseResponse(response, headers);
|
|
112
|
+
};
|
|
113
|
+
const sdk = {
|
|
114
|
+
client: {
|
|
115
|
+
fetch: clientFetch
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
const SLUG_REGEX$1 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
119
|
+
const generateSlug$1 = (value) => value.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
120
|
+
const cleanSlug$1 = (value) => value.toLowerCase().replace(/[^a-z0-9-]/g, "").replace(/--+/g, "-");
|
|
121
|
+
const QuickCreateBrandModal = ({
|
|
122
|
+
onClose,
|
|
123
|
+
onCreated
|
|
124
|
+
}) => {
|
|
125
|
+
const [name, setName] = useState("");
|
|
126
|
+
const [slug, setSlug] = useState("");
|
|
127
|
+
const [isActive, setIsActive] = useState(true);
|
|
128
|
+
const [loading, setLoading] = useState(false);
|
|
129
|
+
const [error, setError] = useState(null);
|
|
130
|
+
const [slugManuallyEdited, setSlugManuallyEdited] = useState(false);
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
if (!slugManuallyEdited) {
|
|
133
|
+
setSlug(generateSlug$1(name));
|
|
134
|
+
}
|
|
135
|
+
}, [name, slugManuallyEdited]);
|
|
136
|
+
const handleSubmit = async (event) => {
|
|
137
|
+
event.preventDefault();
|
|
138
|
+
setError(null);
|
|
139
|
+
if (!name.trim()) {
|
|
140
|
+
setError("Brand name is required");
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (!slug || !SLUG_REGEX$1.test(slug)) {
|
|
144
|
+
setError(
|
|
145
|
+
"Slug must contain only lowercase letters, numbers, and hyphens"
|
|
146
|
+
);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
setLoading(true);
|
|
150
|
+
try {
|
|
151
|
+
const response = await sdk.client.fetch("/admin/brands", {
|
|
152
|
+
method: "POST",
|
|
153
|
+
body: {
|
|
154
|
+
name: name.trim(),
|
|
155
|
+
slug: slug.trim(),
|
|
156
|
+
is_active: isActive
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
if (!(response == null ? void 0 : response.brand)) {
|
|
160
|
+
throw new Error("Unexpected response from server");
|
|
161
|
+
}
|
|
162
|
+
await onCreated(response.brand);
|
|
163
|
+
onClose();
|
|
164
|
+
} catch (err) {
|
|
165
|
+
const message = err instanceof FetchError ? err.message : err instanceof Error ? err.message : "Failed to create brand";
|
|
166
|
+
setError(message);
|
|
167
|
+
} finally {
|
|
168
|
+
setLoading(false);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
return /* @__PURE__ */ jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/40", children: /* @__PURE__ */ jsxs("div", { className: "w-full max-w-lg rounded-lg bg-ui-bg-base p-6 shadow-lg", children: [
|
|
172
|
+
/* @__PURE__ */ jsxs("div", { className: "mb-4 flex items-center justify-between", children: [
|
|
173
|
+
/* @__PURE__ */ jsx(Heading, { level: "h2", children: "Create Brand" }),
|
|
174
|
+
/* @__PURE__ */ jsx(Button, { variant: "secondary", size: "small", onClick: onClose, children: /* @__PURE__ */ jsx(XMark, {}) })
|
|
175
|
+
] }),
|
|
176
|
+
error && /* @__PURE__ */ jsx(Alert, { variant: "error", dismissible: true, onDismiss: () => setError(null), children: error }),
|
|
177
|
+
/* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit, className: "space-y-4", children: [
|
|
178
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
179
|
+
/* @__PURE__ */ jsx(Label, { htmlFor: "quick-brand-name", className: "mb-2", children: "Brand name *" }),
|
|
180
|
+
/* @__PURE__ */ jsx(
|
|
181
|
+
Input,
|
|
182
|
+
{
|
|
183
|
+
id: "quick-brand-name",
|
|
184
|
+
value: name,
|
|
185
|
+
placeholder: "e.g., Nike",
|
|
186
|
+
onChange: (event) => {
|
|
187
|
+
setName(event.target.value);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
)
|
|
191
|
+
] }),
|
|
192
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
193
|
+
/* @__PURE__ */ jsxs("div", { className: "mb-2 flex items-center justify-between gap-2", children: [
|
|
194
|
+
/* @__PURE__ */ jsx(Label, { htmlFor: "quick-brand-slug", children: "Slug *" }),
|
|
195
|
+
/* @__PURE__ */ jsx(
|
|
196
|
+
Button,
|
|
197
|
+
{
|
|
198
|
+
type: "button",
|
|
199
|
+
variant: "secondary",
|
|
200
|
+
size: "small",
|
|
201
|
+
onClick: () => {
|
|
202
|
+
setSlug(generateSlug$1(name));
|
|
203
|
+
setSlugManuallyEdited(false);
|
|
204
|
+
},
|
|
205
|
+
children: "Reset to name"
|
|
206
|
+
}
|
|
207
|
+
)
|
|
208
|
+
] }),
|
|
209
|
+
/* @__PURE__ */ jsx(
|
|
210
|
+
Input,
|
|
211
|
+
{
|
|
212
|
+
id: "quick-brand-slug",
|
|
213
|
+
value: slug,
|
|
214
|
+
placeholder: "e.g., nike",
|
|
215
|
+
onChange: (event) => {
|
|
216
|
+
setSlug(cleanSlug$1(event.target.value));
|
|
217
|
+
setSlugManuallyEdited(true);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
),
|
|
221
|
+
/* @__PURE__ */ jsx(Text, { className: "mt-1 text-xs text-ui-fg-subtle", children: "Used in URLs and must be unique." })
|
|
222
|
+
] }),
|
|
223
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between rounded-lg border p-3", children: [
|
|
224
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
225
|
+
/* @__PURE__ */ jsx(Label, { className: "font-medium", children: "Active" }),
|
|
226
|
+
/* @__PURE__ */ jsx(Text, { className: "text-sm text-ui-fg-subtle", children: "Active brands appear on the storefront." })
|
|
227
|
+
] }),
|
|
228
|
+
/* @__PURE__ */ jsx(
|
|
229
|
+
Switch,
|
|
230
|
+
{
|
|
231
|
+
checked: isActive,
|
|
232
|
+
onCheckedChange: (value) => setIsActive(Boolean(value))
|
|
233
|
+
}
|
|
234
|
+
)
|
|
235
|
+
] }),
|
|
236
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-end gap-2", children: [
|
|
237
|
+
/* @__PURE__ */ jsx(
|
|
238
|
+
Button,
|
|
239
|
+
{
|
|
240
|
+
type: "button",
|
|
241
|
+
variant: "secondary",
|
|
242
|
+
onClick: onClose,
|
|
243
|
+
disabled: loading,
|
|
244
|
+
children: "Cancel"
|
|
245
|
+
}
|
|
246
|
+
),
|
|
247
|
+
/* @__PURE__ */ jsx(Button, { type: "submit", disabled: loading, children: loading ? "Creating…" : "Create brand" })
|
|
248
|
+
] })
|
|
249
|
+
] })
|
|
250
|
+
] }) });
|
|
251
|
+
};
|
|
15
252
|
const ProductBrandWidget = ({ data: product }) => {
|
|
16
253
|
const [brands, setBrands] = useState([]);
|
|
17
254
|
const [currentBrand, setCurrentBrand] = useState(null);
|
|
@@ -19,20 +256,20 @@ const ProductBrandWidget = ({ data: product }) => {
|
|
|
19
256
|
const [loading, setLoading] = useState(false);
|
|
20
257
|
const [error, setError] = useState(null);
|
|
21
258
|
const [success, setSuccess] = useState(null);
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const fetchBrands = async () => {
|
|
259
|
+
const [searchTerm, setSearchTerm] = useState("");
|
|
260
|
+
const [debouncedSearch, setDebouncedSearch] = useState("");
|
|
261
|
+
const [showCreateModal, setShowCreateModal] = useState(false);
|
|
262
|
+
const fetchBrands = useCallback(async (query = "") => {
|
|
27
263
|
try {
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
);
|
|
32
|
-
if (response.ok) {
|
|
33
|
-
const data = await response.json();
|
|
34
|
-
setBrands(data.brands || []);
|
|
264
|
+
const params = new URLSearchParams({ limit: "100" });
|
|
265
|
+
if (query) {
|
|
266
|
+
params.set("q", query);
|
|
35
267
|
}
|
|
268
|
+
const data = await sdk.client.fetch(
|
|
269
|
+
`/admin/brands?${params.toString()}`,
|
|
270
|
+
{ method: "GET" }
|
|
271
|
+
);
|
|
272
|
+
setBrands((data == null ? void 0 : data.brands) ?? []);
|
|
36
273
|
} catch (err) {
|
|
37
274
|
if (err instanceof FetchError && err.status === 404) {
|
|
38
275
|
setBrands([]);
|
|
@@ -40,22 +277,21 @@ const ProductBrandWidget = ({ data: product }) => {
|
|
|
40
277
|
}
|
|
41
278
|
console.error("Error fetching brands:", err);
|
|
42
279
|
}
|
|
43
|
-
};
|
|
44
|
-
const fetchProductBrand = async () => {
|
|
280
|
+
}, []);
|
|
281
|
+
const fetchProductBrand = useCallback(async () => {
|
|
45
282
|
try {
|
|
46
|
-
const
|
|
283
|
+
const data = await sdk.client.fetch(
|
|
47
284
|
`/admin/products/${product.id}/brand`,
|
|
48
|
-
{
|
|
49
|
-
|
|
50
|
-
if (response.ok) {
|
|
51
|
-
const data = await response.json();
|
|
52
|
-
if (data.brand) {
|
|
53
|
-
setCurrentBrand(data.brand);
|
|
54
|
-
setSelectedBrandId(data.brand.id);
|
|
55
|
-
} else {
|
|
56
|
-
setCurrentBrand(null);
|
|
57
|
-
setSelectedBrandId("none");
|
|
285
|
+
{
|
|
286
|
+
method: "GET"
|
|
58
287
|
}
|
|
288
|
+
);
|
|
289
|
+
if (data == null ? void 0 : data.brand) {
|
|
290
|
+
setCurrentBrand(data.brand);
|
|
291
|
+
setSelectedBrandId(data.brand.id);
|
|
292
|
+
} else {
|
|
293
|
+
setCurrentBrand(null);
|
|
294
|
+
setSelectedBrandId("none");
|
|
59
295
|
}
|
|
60
296
|
} catch (err) {
|
|
61
297
|
if (err instanceof FetchError && err.status === 404) {
|
|
@@ -65,372 +301,619 @@ const ProductBrandWidget = ({ data: product }) => {
|
|
|
65
301
|
}
|
|
66
302
|
console.error("Error fetching product brand:", err);
|
|
67
303
|
}
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
304
|
+
}, [product.id]);
|
|
305
|
+
useEffect(() => {
|
|
306
|
+
fetchBrands();
|
|
307
|
+
fetchProductBrand();
|
|
308
|
+
}, [fetchBrands, fetchProductBrand]);
|
|
309
|
+
useEffect(() => {
|
|
310
|
+
const handle = window.setTimeout(() => {
|
|
311
|
+
setDebouncedSearch(searchTerm.trim());
|
|
312
|
+
}, 250);
|
|
313
|
+
return () => window.clearTimeout(handle);
|
|
314
|
+
}, [searchTerm]);
|
|
315
|
+
useEffect(() => {
|
|
316
|
+
fetchBrands(debouncedSearch);
|
|
317
|
+
}, [debouncedSearch, fetchBrands]);
|
|
318
|
+
const assignBrand = useCallback(
|
|
319
|
+
async (brandId, brandData) => {
|
|
320
|
+
setLoading(true);
|
|
321
|
+
setError(null);
|
|
322
|
+
setSuccess(null);
|
|
323
|
+
try {
|
|
324
|
+
if (!brandId) {
|
|
325
|
+
await sdk.client.fetch(`/admin/products/${product.id}/brand`, {
|
|
326
|
+
method: "DELETE"
|
|
327
|
+
});
|
|
328
|
+
setSuccess("Brand removed successfully");
|
|
329
|
+
setCurrentBrand(null);
|
|
330
|
+
setSelectedBrandId("none");
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
await sdk.client.fetch(`/admin/products/${product.id}/brand`, {
|
|
88
334
|
method: "POST",
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
335
|
+
body: { brand_id: brandId }
|
|
336
|
+
});
|
|
337
|
+
const brandToPersist = brandData || brands.find((brand) => brand.id === brandId) || null;
|
|
338
|
+
if (brandToPersist) {
|
|
339
|
+
setCurrentBrand(brandToPersist);
|
|
340
|
+
} else {
|
|
341
|
+
await fetchProductBrand();
|
|
93
342
|
}
|
|
94
|
-
|
|
95
|
-
if (response.ok) {
|
|
343
|
+
setSelectedBrandId(brandId);
|
|
96
344
|
setSuccess("Brand updated successfully");
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
345
|
+
} catch (err) {
|
|
346
|
+
if (err instanceof FetchError) {
|
|
347
|
+
const responseBody = err == null ? void 0 : err.body;
|
|
348
|
+
setError(
|
|
349
|
+
(responseBody == null ? void 0 : responseBody.error) || (responseBody == null ? void 0 : responseBody.message) || err.message || "Failed to update brand"
|
|
350
|
+
);
|
|
351
|
+
} else {
|
|
352
|
+
setError("Failed to update brand");
|
|
353
|
+
}
|
|
354
|
+
console.error("Error assigning brand:", err);
|
|
355
|
+
} finally {
|
|
356
|
+
setLoading(false);
|
|
101
357
|
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
358
|
+
},
|
|
359
|
+
[brands, fetchProductBrand, product.id]
|
|
360
|
+
);
|
|
361
|
+
const handleSaveBrand = async () => {
|
|
362
|
+
if (selectedBrandId === "none") {
|
|
363
|
+
await assignBrand(null);
|
|
364
|
+
return;
|
|
107
365
|
}
|
|
366
|
+
await assignBrand(selectedBrandId);
|
|
108
367
|
};
|
|
109
368
|
const handleRemoveBrand = async () => {
|
|
110
|
-
|
|
369
|
+
const confirmed = window.confirm(
|
|
370
|
+
"Are you sure you want to remove the brand from this product?"
|
|
371
|
+
);
|
|
372
|
+
if (!confirmed) {
|
|
111
373
|
return;
|
|
112
374
|
}
|
|
113
|
-
|
|
114
|
-
setError(null);
|
|
115
|
-
setSuccess(null);
|
|
116
|
-
try {
|
|
117
|
-
const response = await sdk.client.fetch(
|
|
118
|
-
`/admin/products/${product.id}/brand`,
|
|
119
|
-
{
|
|
120
|
-
method: "DELETE"
|
|
121
|
-
}
|
|
122
|
-
);
|
|
123
|
-
if (response.ok) {
|
|
124
|
-
setSuccess("Brand removed successfully");
|
|
125
|
-
setCurrentBrand(null);
|
|
126
|
-
setSelectedBrandId("");
|
|
127
|
-
} else {
|
|
128
|
-
const data = await response.json();
|
|
129
|
-
setError(data.error || "Failed to remove brand");
|
|
130
|
-
}
|
|
131
|
-
} catch (err) {
|
|
132
|
-
setError("Error removing brand");
|
|
133
|
-
console.error("Error removing brand:", err);
|
|
134
|
-
} finally {
|
|
135
|
-
setLoading(false);
|
|
136
|
-
}
|
|
375
|
+
await assignBrand(null);
|
|
137
376
|
};
|
|
377
|
+
const filteredBrands = useMemo(() => {
|
|
378
|
+
if (!debouncedSearch) {
|
|
379
|
+
return brands;
|
|
380
|
+
}
|
|
381
|
+
const query = debouncedSearch.toLowerCase();
|
|
382
|
+
return brands.filter(
|
|
383
|
+
(brand) => brand.name.toLowerCase().includes(query) || brand.slug.toLowerCase().includes(query)
|
|
384
|
+
);
|
|
385
|
+
}, [brands, debouncedSearch]);
|
|
138
386
|
const hasChanges = selectedBrandId !== ((currentBrand == null ? void 0 : currentBrand.id) || "none");
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
"img",
|
|
150
|
-
{
|
|
151
|
-
src: currentBrand.logo,
|
|
152
|
-
alt: currentBrand.name,
|
|
153
|
-
className: "h-10 w-10 object-contain rounded"
|
|
154
|
-
}
|
|
155
|
-
) : /* @__PURE__ */ jsx("div", { className: "h-10 w-10 rounded bg-ui-bg-base flex items-center justify-center", children: /* @__PURE__ */ jsx(BuildingStorefront, { className: "h-5 w-5 text-ui-fg-subtle" }) }),
|
|
156
|
-
/* @__PURE__ */ jsxs("div", { children: [
|
|
157
|
-
/* @__PURE__ */ jsx(Text, { className: "font-medium", children: currentBrand.name }),
|
|
158
|
-
/* @__PURE__ */ jsxs(Text, { className: "text-sm text-ui-fg-subtle", children: [
|
|
159
|
-
"/",
|
|
160
|
-
currentBrand.slug
|
|
161
|
-
] })
|
|
162
|
-
] })
|
|
387
|
+
const handleBrandCreated = async (brand) => {
|
|
388
|
+
setBrands((prev) => [brand, ...prev.filter((b) => b.id !== brand.id)]);
|
|
389
|
+
setSelectedBrandId(brand.id);
|
|
390
|
+
await assignBrand(brand.id, brand);
|
|
391
|
+
};
|
|
392
|
+
return /* @__PURE__ */ jsxs(Container, { className: "divide-y px-0 pb-0 pt-0", children: [
|
|
393
|
+
/* @__PURE__ */ jsxs("div", { className: "px-6 py-6", children: [
|
|
394
|
+
/* @__PURE__ */ jsxs("div", { className: "mb-4 flex items-center gap-3", children: [
|
|
395
|
+
/* @__PURE__ */ jsx(BuildingStorefront, { className: "text-ui-fg-subtle" }),
|
|
396
|
+
/* @__PURE__ */ jsx(Heading, { level: "h2", children: "Brand" })
|
|
163
397
|
] }),
|
|
164
|
-
/* @__PURE__ */
|
|
165
|
-
|
|
398
|
+
error && /* @__PURE__ */ jsx(Alert, { variant: "error", dismissible: true, onDismiss: () => setError(null), children: error }),
|
|
399
|
+
success && /* @__PURE__ */ jsx(
|
|
400
|
+
Alert,
|
|
166
401
|
{
|
|
167
|
-
variant: "
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
children: [
|
|
172
|
-
/* @__PURE__ */ jsx(Trash, { className: "mr-1" }),
|
|
173
|
-
"Remove"
|
|
174
|
-
]
|
|
402
|
+
variant: "success",
|
|
403
|
+
dismissible: true,
|
|
404
|
+
onDismiss: () => setSuccess(null),
|
|
405
|
+
children: success
|
|
175
406
|
}
|
|
176
|
-
)
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
407
|
+
),
|
|
408
|
+
currentBrand && !hasChanges && /* @__PURE__ */ jsx("div", { className: "mb-4 rounded-lg border bg-ui-bg-subtle p-4", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
409
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
410
|
+
currentBrand.logo ? /* @__PURE__ */ jsx(
|
|
411
|
+
"img",
|
|
412
|
+
{
|
|
413
|
+
src: currentBrand.logo,
|
|
414
|
+
alt: currentBrand.name,
|
|
415
|
+
className: "h-10 w-10 rounded object-contain"
|
|
416
|
+
}
|
|
417
|
+
) : /* @__PURE__ */ jsx("div", { className: "flex h-10 w-10 items-center justify-center rounded bg-ui-bg-base", children: /* @__PURE__ */ jsx(BuildingStorefront, { className: "h-5 w-5 text-ui-fg-subtle" }) }),
|
|
418
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
419
|
+
/* @__PURE__ */ jsx(Text, { className: "font-medium", children: currentBrand.name }),
|
|
420
|
+
/* @__PURE__ */ jsxs(Text, { className: "text-sm text-ui-fg-subtle", children: [
|
|
421
|
+
"/",
|
|
422
|
+
currentBrand.slug
|
|
423
|
+
] })
|
|
424
|
+
] })
|
|
425
|
+
] }),
|
|
181
426
|
/* @__PURE__ */ jsxs(
|
|
182
|
-
|
|
427
|
+
Button,
|
|
183
428
|
{
|
|
184
|
-
|
|
185
|
-
|
|
429
|
+
variant: "danger",
|
|
430
|
+
size: "small",
|
|
431
|
+
onClick: handleRemoveBrand,
|
|
186
432
|
disabled: loading,
|
|
187
433
|
children: [
|
|
188
|
-
/* @__PURE__ */ jsx(
|
|
189
|
-
|
|
190
|
-
/* @__PURE__ */ jsx(Select.Item, { value: "none", children: /* @__PURE__ */ jsx("span", { className: "text-ui-fg-muted", children: "No brand" }) }),
|
|
191
|
-
brands.map((brand) => /* @__PURE__ */ jsx(Select.Item, { value: brand.id, children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
192
|
-
brand.logo && /* @__PURE__ */ jsx(
|
|
193
|
-
"img",
|
|
194
|
-
{
|
|
195
|
-
src: brand.logo,
|
|
196
|
-
alt: brand.name,
|
|
197
|
-
className: "h-5 w-5 object-contain rounded"
|
|
198
|
-
}
|
|
199
|
-
),
|
|
200
|
-
/* @__PURE__ */ jsx("span", { children: brand.name }),
|
|
201
|
-
/* @__PURE__ */ jsx(Badge, { size: "xsmall", color: "blue", className: "ml-auto", children: brand.slug })
|
|
202
|
-
] }) }, brand.id))
|
|
203
|
-
] })
|
|
434
|
+
/* @__PURE__ */ jsx(Trash, { className: "mr-1" }),
|
|
435
|
+
" Remove"
|
|
204
436
|
]
|
|
205
437
|
}
|
|
206
|
-
)
|
|
207
|
-
|
|
438
|
+
)
|
|
439
|
+
] }) }),
|
|
440
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
441
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
442
|
+
/* @__PURE__ */ jsx(Label, { htmlFor: "brand-search", className: "mb-2", children: "Find a brand" }),
|
|
443
|
+
/* @__PURE__ */ jsxs("div", { className: "relative", children: [
|
|
444
|
+
/* @__PURE__ */ jsx(
|
|
445
|
+
Input,
|
|
446
|
+
{
|
|
447
|
+
id: "brand-search",
|
|
448
|
+
value: searchTerm,
|
|
449
|
+
placeholder: "Search by name or slug",
|
|
450
|
+
onChange: (event) => setSearchTerm(event.target.value),
|
|
451
|
+
className: "pl-10"
|
|
452
|
+
}
|
|
453
|
+
),
|
|
454
|
+
/* @__PURE__ */ jsx(MagnifyingGlass, { className: "absolute left-3 top-1/2 -translate-y-1/2 text-ui-fg-muted" })
|
|
455
|
+
] })
|
|
456
|
+
] }),
|
|
457
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
458
|
+
/* @__PURE__ */ jsx(Label, { htmlFor: "brand", className: "mb-2", children: "Select brand" }),
|
|
459
|
+
/* @__PURE__ */ jsxs(
|
|
460
|
+
Select,
|
|
461
|
+
{
|
|
462
|
+
value: selectedBrandId,
|
|
463
|
+
onValueChange: (value) => {
|
|
464
|
+
setSelectedBrandId(value);
|
|
465
|
+
setError(null);
|
|
466
|
+
setSuccess(null);
|
|
467
|
+
},
|
|
468
|
+
disabled: loading,
|
|
469
|
+
children: [
|
|
470
|
+
/* @__PURE__ */ jsx(Select.Trigger, { id: "brand", children: /* @__PURE__ */ jsx(Select.Value, { placeholder: "Choose a brand" }) }),
|
|
471
|
+
/* @__PURE__ */ jsxs(Select.Content, { children: [
|
|
472
|
+
/* @__PURE__ */ jsx(Select.Item, { value: "none", children: /* @__PURE__ */ jsx("span", { className: "text-ui-fg-muted", children: "No brand" }) }),
|
|
473
|
+
filteredBrands.map((brand) => /* @__PURE__ */ jsx(Select.Item, { value: brand.id, children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
474
|
+
brand.logo && /* @__PURE__ */ jsx(
|
|
475
|
+
"img",
|
|
476
|
+
{
|
|
477
|
+
src: brand.logo,
|
|
478
|
+
alt: brand.name,
|
|
479
|
+
className: "h-5 w-5 rounded object-contain"
|
|
480
|
+
}
|
|
481
|
+
),
|
|
482
|
+
/* @__PURE__ */ jsx("span", { children: brand.name }),
|
|
483
|
+
/* @__PURE__ */ jsx(Badge, { size: "xsmall", color: "blue", className: "ml-auto", children: brand.slug })
|
|
484
|
+
] }) }, brand.id)),
|
|
485
|
+
filteredBrands.length === 0 && /* @__PURE__ */ jsx(Select.Item, { value: "__no-results", disabled: true, children: /* @__PURE__ */ jsx("span", { className: "text-ui-fg-muted", children: "No brands found" }) })
|
|
486
|
+
] })
|
|
487
|
+
]
|
|
488
|
+
}
|
|
489
|
+
),
|
|
490
|
+
/* @__PURE__ */ jsx(Text, { className: "mt-1 text-xs text-ui-fg-subtle", children: "Assigning a brand helps customers filter products by manufacturer." })
|
|
491
|
+
] }),
|
|
492
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
493
|
+
/* @__PURE__ */ jsx(
|
|
494
|
+
Button,
|
|
495
|
+
{
|
|
496
|
+
variant: "primary",
|
|
497
|
+
size: "small",
|
|
498
|
+
onClick: handleSaveBrand,
|
|
499
|
+
disabled: loading || !hasChanges && selectedBrandId !== "none",
|
|
500
|
+
children: loading ? "Saving…" : "Save brand"
|
|
501
|
+
}
|
|
502
|
+
),
|
|
503
|
+
/* @__PURE__ */ jsx(
|
|
504
|
+
Button,
|
|
505
|
+
{
|
|
506
|
+
variant: "secondary",
|
|
507
|
+
size: "small",
|
|
508
|
+
onClick: () => setShowCreateModal(true),
|
|
509
|
+
disabled: loading,
|
|
510
|
+
children: "Create new brand"
|
|
511
|
+
}
|
|
512
|
+
)
|
|
513
|
+
] })
|
|
208
514
|
] }),
|
|
209
|
-
|
|
515
|
+
/* @__PURE__ */ jsx("div", { className: "mt-6 border-t pt-6", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-sm", children: [
|
|
516
|
+
/* @__PURE__ */ jsx(Link, { className: "text-ui-fg-subtle" }),
|
|
210
517
|
/* @__PURE__ */ jsx(
|
|
211
|
-
|
|
212
|
-
{
|
|
213
|
-
variant: "primary",
|
|
214
|
-
size: "small",
|
|
215
|
-
onClick: handleSaveBrand,
|
|
216
|
-
disabled: loading,
|
|
217
|
-
children: loading ? "Saving..." : "Save Brand"
|
|
218
|
-
}
|
|
219
|
-
),
|
|
220
|
-
/* @__PURE__ */ jsx(
|
|
221
|
-
Button,
|
|
518
|
+
"a",
|
|
222
519
|
{
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
setSelectedBrandId((currentBrand == null ? void 0 : currentBrand.id) || "none");
|
|
227
|
-
setError(null);
|
|
228
|
-
setSuccess(null);
|
|
229
|
-
},
|
|
230
|
-
disabled: loading,
|
|
231
|
-
children: "Cancel"
|
|
520
|
+
href: "/app/brands",
|
|
521
|
+
className: "text-ui-fg-interactive transition-colors hover:text-ui-fg-interactive-hover",
|
|
522
|
+
children: "Manage all brands"
|
|
232
523
|
}
|
|
233
524
|
)
|
|
234
|
-
] })
|
|
525
|
+
] }) })
|
|
235
526
|
] }),
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
}
|
|
245
|
-
)
|
|
246
|
-
] }) })
|
|
247
|
-
] }) });
|
|
527
|
+
showCreateModal && /* @__PURE__ */ jsx(
|
|
528
|
+
QuickCreateBrandModal,
|
|
529
|
+
{
|
|
530
|
+
onClose: () => setShowCreateModal(false),
|
|
531
|
+
onCreated: handleBrandCreated
|
|
532
|
+
}
|
|
533
|
+
)
|
|
534
|
+
] });
|
|
248
535
|
};
|
|
249
536
|
defineWidgetConfig({
|
|
250
537
|
zone: "product.details.side.after"
|
|
251
538
|
});
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
539
|
+
const SLUG_REGEX = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
540
|
+
const generateSlug = (value) => value.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
541
|
+
const cleanSlug = (value) => value.toLowerCase().replace(/[^a-z0-9-]/g, "").replace(/--+/g, "-");
|
|
542
|
+
const formatMetadata = (metadata) => {
|
|
543
|
+
if (!metadata || Object.keys(metadata).length === 0) {
|
|
544
|
+
return "";
|
|
545
|
+
}
|
|
546
|
+
try {
|
|
547
|
+
return JSON.stringify(metadata, null, 2);
|
|
548
|
+
} catch (error) {
|
|
549
|
+
return "";
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
const BrandForm = ({
|
|
553
|
+
brand,
|
|
554
|
+
isCreating,
|
|
555
|
+
onClose
|
|
556
|
+
}) => {
|
|
261
557
|
const [loading, setLoading] = useState(false);
|
|
262
558
|
const [error, setError] = useState(null);
|
|
263
|
-
const [
|
|
559
|
+
const [slugManuallyEdited, setSlugManuallyEdited] = useState(false);
|
|
560
|
+
const [isCheckingSlug, setIsCheckingSlug] = useState(false);
|
|
561
|
+
const [slugFeedback, setSlugFeedback] = useState(null);
|
|
562
|
+
const {
|
|
563
|
+
control,
|
|
564
|
+
handleSubmit,
|
|
565
|
+
watch,
|
|
566
|
+
setValue,
|
|
567
|
+
reset,
|
|
568
|
+
setError: setFormError,
|
|
569
|
+
clearErrors,
|
|
570
|
+
formState: { errors }
|
|
571
|
+
} = useForm({
|
|
572
|
+
defaultValues: {
|
|
573
|
+
name: "",
|
|
574
|
+
slug: "",
|
|
575
|
+
description: "",
|
|
576
|
+
website: "",
|
|
577
|
+
is_active: true,
|
|
578
|
+
metadata: ""
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
const nameValue = watch("name");
|
|
582
|
+
const slugValue = watch("slug");
|
|
264
583
|
useEffect(() => {
|
|
584
|
+
if (isCreating) {
|
|
585
|
+
reset({
|
|
586
|
+
name: "",
|
|
587
|
+
slug: "",
|
|
588
|
+
description: "",
|
|
589
|
+
website: "",
|
|
590
|
+
is_active: true,
|
|
591
|
+
metadata: ""
|
|
592
|
+
});
|
|
593
|
+
setSlugFeedback(null);
|
|
594
|
+
setSlugManuallyEdited(false);
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
265
597
|
if (brand) {
|
|
266
|
-
|
|
267
|
-
name: brand.name
|
|
268
|
-
slug: brand.slug
|
|
269
|
-
description: brand.description
|
|
270
|
-
website: brand.website
|
|
271
|
-
is_active: brand.is_active
|
|
272
|
-
metadata: brand.metadata
|
|
598
|
+
reset({
|
|
599
|
+
name: brand.name ?? "",
|
|
600
|
+
slug: brand.slug ?? "",
|
|
601
|
+
description: brand.description ?? "",
|
|
602
|
+
website: brand.website ?? "",
|
|
603
|
+
is_active: brand.is_active,
|
|
604
|
+
metadata: formatMetadata(brand.metadata)
|
|
273
605
|
});
|
|
606
|
+
setSlugFeedback(null);
|
|
607
|
+
setSlugManuallyEdited(false);
|
|
274
608
|
}
|
|
275
|
-
}, [brand]);
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
const handleNameChange = (value) => {
|
|
280
|
-
setFormData((prev) => ({
|
|
281
|
-
...prev,
|
|
282
|
-
name: value,
|
|
283
|
-
slug: isCreating ? generateSlug(value) : prev.slug
|
|
284
|
-
}));
|
|
285
|
-
};
|
|
286
|
-
const handleSlugChange = (value) => {
|
|
287
|
-
const cleanSlug = value.toLowerCase().replace(/[^a-z0-9-]/g, "").replace(/--+/g, "-");
|
|
288
|
-
setFormData((prev) => ({ ...prev, slug: cleanSlug }));
|
|
289
|
-
if (slugError) setSlugError(null);
|
|
290
|
-
};
|
|
291
|
-
const validateForm = () => {
|
|
292
|
-
if (!formData.name.trim()) {
|
|
293
|
-
setError("Brand name is required");
|
|
294
|
-
return false;
|
|
609
|
+
}, [brand, isCreating, reset]);
|
|
610
|
+
useEffect(() => {
|
|
611
|
+
if (!isCreating) {
|
|
612
|
+
return;
|
|
295
613
|
}
|
|
296
|
-
if (!
|
|
297
|
-
|
|
298
|
-
return
|
|
614
|
+
if (!nameValue) {
|
|
615
|
+
setValue("slug", "");
|
|
616
|
+
return;
|
|
299
617
|
}
|
|
300
|
-
if (
|
|
301
|
-
|
|
302
|
-
"Slug must contain only lowercase letters, numbers, and hyphens"
|
|
303
|
-
);
|
|
304
|
-
return false;
|
|
618
|
+
if (!slugManuallyEdited) {
|
|
619
|
+
setValue("slug", generateSlug(nameValue), { shouldValidate: true });
|
|
305
620
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
);
|
|
310
|
-
|
|
621
|
+
}, [isCreating, nameValue, setValue, slugManuallyEdited]);
|
|
622
|
+
useEffect(() => {
|
|
623
|
+
if (!slugValue) {
|
|
624
|
+
setSlugFeedback(null);
|
|
625
|
+
clearErrors("slug");
|
|
626
|
+
return;
|
|
311
627
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
628
|
+
if (!SLUG_REGEX.test(slugValue)) {
|
|
629
|
+
setSlugFeedback(null);
|
|
630
|
+
setFormError("slug", {
|
|
631
|
+
type: "pattern",
|
|
632
|
+
message: "Slug must contain only lowercase letters, numbers, and hyphens"
|
|
633
|
+
});
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
if (!isCreating && slugValue === (brand == null ? void 0 : brand.slug)) {
|
|
637
|
+
setSlugFeedback(null);
|
|
638
|
+
clearErrors("slug");
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
setSlugFeedback(null);
|
|
642
|
+
clearErrors("slug");
|
|
643
|
+
let cancelled = false;
|
|
644
|
+
setIsCheckingSlug(true);
|
|
645
|
+
const handler = window.setTimeout(async () => {
|
|
646
|
+
var _a;
|
|
647
|
+
try {
|
|
648
|
+
const params = new URLSearchParams({ slug: slugValue, limit: "1" });
|
|
649
|
+
const response = await sdk.client.fetch(
|
|
650
|
+
`/admin/brands?${params.toString()}`,
|
|
651
|
+
{
|
|
652
|
+
method: "GET"
|
|
653
|
+
}
|
|
654
|
+
);
|
|
655
|
+
if (cancelled) {
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
const existing = (_a = response == null ? void 0 : response.brands) == null ? void 0 : _a[0];
|
|
659
|
+
if (existing && existing.id !== (brand == null ? void 0 : brand.id)) {
|
|
660
|
+
setFormError("slug", {
|
|
661
|
+
type: "validate",
|
|
662
|
+
message: "This slug is already in use"
|
|
663
|
+
});
|
|
664
|
+
setSlugFeedback(null);
|
|
665
|
+
} else {
|
|
666
|
+
setSlugFeedback("Slug is available");
|
|
667
|
+
}
|
|
668
|
+
} catch (validationError) {
|
|
669
|
+
if (!cancelled) {
|
|
670
|
+
setSlugFeedback(null);
|
|
671
|
+
}
|
|
672
|
+
} finally {
|
|
673
|
+
if (!cancelled) {
|
|
674
|
+
setIsCheckingSlug(false);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}, 300);
|
|
678
|
+
return () => {
|
|
679
|
+
cancelled = true;
|
|
680
|
+
window.clearTimeout(handler);
|
|
681
|
+
setIsCheckingSlug(false);
|
|
682
|
+
};
|
|
683
|
+
}, [
|
|
684
|
+
slugValue,
|
|
685
|
+
brand == null ? void 0 : brand.id,
|
|
686
|
+
brand == null ? void 0 : brand.slug,
|
|
687
|
+
clearErrors,
|
|
688
|
+
isCreating,
|
|
689
|
+
setFormError
|
|
690
|
+
]);
|
|
691
|
+
const onSubmit = async (data) => {
|
|
315
692
|
var _a;
|
|
316
|
-
e.preventDefault();
|
|
317
|
-
if (!validateForm()) return;
|
|
318
693
|
setLoading(true);
|
|
319
694
|
setError(null);
|
|
695
|
+
clearErrors(["slug", "metadata"]);
|
|
696
|
+
const metadataInput = (_a = data.metadata) == null ? void 0 : _a.trim();
|
|
697
|
+
let parsedMetadata = {};
|
|
698
|
+
if (metadataInput) {
|
|
699
|
+
try {
|
|
700
|
+
const parsed = JSON.parse(metadataInput);
|
|
701
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
702
|
+
throw new Error("Metadata must be a valid JSON object");
|
|
703
|
+
}
|
|
704
|
+
parsedMetadata = parsed;
|
|
705
|
+
} catch (parseError) {
|
|
706
|
+
const message = parseError instanceof Error ? parseError.message : "Metadata must be a valid JSON object";
|
|
707
|
+
setFormError("metadata", {
|
|
708
|
+
type: "manual",
|
|
709
|
+
message
|
|
710
|
+
});
|
|
711
|
+
setError(message);
|
|
712
|
+
setLoading(false);
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
320
716
|
try {
|
|
321
717
|
const url = isCreating ? "/admin/brands" : `/admin/brands/${brand == null ? void 0 : brand.id}`;
|
|
322
718
|
const method = isCreating ? "POST" : "PUT";
|
|
323
|
-
const
|
|
719
|
+
const payload = {
|
|
720
|
+
name: data.name.trim(),
|
|
721
|
+
slug: data.slug.trim(),
|
|
722
|
+
description: data.description.trim() || null,
|
|
723
|
+
website: data.website.trim() || null,
|
|
724
|
+
is_active: data.is_active,
|
|
725
|
+
metadata: parsedMetadata
|
|
726
|
+
};
|
|
727
|
+
await sdk.client.fetch(url, {
|
|
324
728
|
method,
|
|
325
|
-
|
|
326
|
-
"Content-Type": "application/json"
|
|
327
|
-
},
|
|
328
|
-
body: JSON.stringify(formData)
|
|
729
|
+
body: payload
|
|
329
730
|
});
|
|
330
|
-
|
|
331
|
-
if (response.ok) {
|
|
332
|
-
onClose();
|
|
333
|
-
} else {
|
|
334
|
-
setError(data.error || "Failed to save brand");
|
|
335
|
-
if ((_a = data.error) == null ? void 0 : _a.includes("slug")) {
|
|
336
|
-
setSlugError("This slug is already in use");
|
|
337
|
-
}
|
|
338
|
-
}
|
|
731
|
+
onClose();
|
|
339
732
|
} catch (err) {
|
|
340
|
-
|
|
341
|
-
|
|
733
|
+
const message = err instanceof FetchError ? err.message : err instanceof Error ? err.message : "Failed to save brand";
|
|
734
|
+
setError(message);
|
|
735
|
+
if (message.toLowerCase().includes("slug")) {
|
|
736
|
+
setFormError("slug", {
|
|
737
|
+
type: "manual",
|
|
738
|
+
message: "This slug is already in use"
|
|
739
|
+
});
|
|
740
|
+
}
|
|
342
741
|
} finally {
|
|
343
742
|
setLoading(false);
|
|
344
743
|
}
|
|
345
744
|
};
|
|
346
|
-
return /* @__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
|
|
745
|
+
return /* @__PURE__ */ jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/50", children: /* @__PURE__ */ jsxs("div", { className: "max-h-[90vh] w-full max-w-2xl overflow-y-auto rounded-lg bg-ui-bg-base p-6", children: [
|
|
347
746
|
/* @__PURE__ */ jsxs("div", { className: "mb-6 flex items-center justify-between", children: [
|
|
348
747
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
349
|
-
/* @__PURE__ */ jsx(Heading, { level: "h2", children: isCreating ? "Create
|
|
748
|
+
/* @__PURE__ */ jsx(Heading, { level: "h2", children: isCreating ? "Create Brand" : "Edit Brand" }),
|
|
350
749
|
!isCreating && brand && /* @__PURE__ */ jsxs(Text, { className: "text-ui-fg-subtle", children: [
|
|
351
|
-
"Editing
|
|
750
|
+
"Editing ",
|
|
352
751
|
brand.name
|
|
353
752
|
] })
|
|
354
753
|
] }),
|
|
355
|
-
/* @__PURE__ */ jsx(Button, { variant: "secondary", size: "small", onClick: onClose, children: /* @__PURE__ */ jsx(
|
|
754
|
+
/* @__PURE__ */ jsx(Button, { variant: "secondary", size: "small", onClick: onClose, children: /* @__PURE__ */ jsx(XMark, {}) })
|
|
356
755
|
] }),
|
|
357
756
|
error && /* @__PURE__ */ jsx(Alert, { variant: "error", dismissible: true, className: "mb-4", children: error }),
|
|
358
|
-
/* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit, className: "space-y-4", children: [
|
|
757
|
+
/* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit(onSubmit), className: "space-y-4", children: [
|
|
359
758
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
360
|
-
/* @__PURE__ */ jsx(Label, { htmlFor: "name", className: "mb-2", children: "Brand
|
|
759
|
+
/* @__PURE__ */ jsx(Label, { htmlFor: "name", className: "mb-2", children: "Brand name *" }),
|
|
361
760
|
/* @__PURE__ */ jsx(
|
|
362
|
-
|
|
761
|
+
Controller,
|
|
363
762
|
{
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
763
|
+
name: "name",
|
|
764
|
+
control,
|
|
765
|
+
rules: {
|
|
766
|
+
required: "Brand name is required",
|
|
767
|
+
minLength: {
|
|
768
|
+
value: 2,
|
|
769
|
+
message: "Brand name must be at least 2 characters"
|
|
770
|
+
}
|
|
771
|
+
},
|
|
772
|
+
render: ({ field }) => /* @__PURE__ */ jsx(Input, { ...field, id: "name", placeholder: "e.g., Nike" })
|
|
369
773
|
}
|
|
370
|
-
)
|
|
774
|
+
),
|
|
775
|
+
errors.name && /* @__PURE__ */ jsx(Text, { className: "mt-1 text-sm text-ui-fg-error", children: errors.name.message })
|
|
371
776
|
] }),
|
|
372
777
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
373
|
-
/* @__PURE__ */
|
|
778
|
+
/* @__PURE__ */ jsxs("div", { className: "mb-2 flex items-center justify-between gap-2", children: [
|
|
779
|
+
/* @__PURE__ */ jsx(Label, { htmlFor: "slug", children: "Slug *" }),
|
|
780
|
+
/* @__PURE__ */ jsx(
|
|
781
|
+
Button,
|
|
782
|
+
{
|
|
783
|
+
variant: "secondary",
|
|
784
|
+
size: "small",
|
|
785
|
+
type: "button",
|
|
786
|
+
onClick: () => {
|
|
787
|
+
const fallback = generateSlug(nameValue || (brand == null ? void 0 : brand.name) || "");
|
|
788
|
+
setValue("slug", fallback, { shouldValidate: true });
|
|
789
|
+
setSlugManuallyEdited(false);
|
|
790
|
+
},
|
|
791
|
+
children: "Reset to name"
|
|
792
|
+
}
|
|
793
|
+
)
|
|
794
|
+
] }),
|
|
374
795
|
/* @__PURE__ */ jsx(
|
|
375
|
-
|
|
796
|
+
Controller,
|
|
376
797
|
{
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
798
|
+
name: "slug",
|
|
799
|
+
control,
|
|
800
|
+
rules: {
|
|
801
|
+
required: "Slug is required",
|
|
802
|
+
pattern: {
|
|
803
|
+
value: SLUG_REGEX,
|
|
804
|
+
message: "Slug must contain only lowercase letters, numbers, and hyphens"
|
|
805
|
+
}
|
|
806
|
+
},
|
|
807
|
+
render: ({ field }) => /* @__PURE__ */ jsx(
|
|
808
|
+
Input,
|
|
809
|
+
{
|
|
810
|
+
...field,
|
|
811
|
+
id: "slug",
|
|
812
|
+
placeholder: "e.g., nike",
|
|
813
|
+
onChange: (event) => {
|
|
814
|
+
setSlugManuallyEdited(true);
|
|
815
|
+
field.onChange(cleanSlug(event.target.value));
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
)
|
|
382
819
|
}
|
|
383
820
|
),
|
|
384
|
-
|
|
385
|
-
/* @__PURE__ */ jsx(Text, { className: "mt-1 text-
|
|
821
|
+
errors.slug && /* @__PURE__ */ jsx(Text, { className: "mt-1 text-sm text-ui-fg-error", children: errors.slug.message }),
|
|
822
|
+
!errors.slug && slugFeedback && /* @__PURE__ */ jsx(Text, { className: "mt-1 text-sm text-ui-fg-positive", children: isCheckingSlug ? "Checking slug availability…" : slugFeedback }),
|
|
823
|
+
!errors.slug && !slugFeedback && isCheckingSlug && /* @__PURE__ */ jsx(Text, { className: "mt-1 text-sm text-ui-fg-subtle", children: "Checking slug availability…" }),
|
|
824
|
+
/* @__PURE__ */ jsx(Text, { className: "mt-1 text-xs text-ui-fg-subtle", children: "Slug appears in URLs and must be unique." })
|
|
386
825
|
] }),
|
|
387
826
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
388
827
|
/* @__PURE__ */ jsx(Label, { htmlFor: "description", className: "mb-2", children: "Description" }),
|
|
389
828
|
/* @__PURE__ */ jsx(
|
|
390
|
-
|
|
829
|
+
Controller,
|
|
391
830
|
{
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
831
|
+
name: "description",
|
|
832
|
+
control,
|
|
833
|
+
render: ({ field }) => /* @__PURE__ */ jsx(
|
|
834
|
+
Textarea,
|
|
835
|
+
{
|
|
836
|
+
...field,
|
|
837
|
+
id: "description",
|
|
838
|
+
placeholder: "Tell customers about this brand…",
|
|
839
|
+
rows: 4
|
|
840
|
+
}
|
|
841
|
+
)
|
|
400
842
|
}
|
|
401
843
|
)
|
|
402
844
|
] }),
|
|
403
845
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
404
846
|
/* @__PURE__ */ jsx(Label, { htmlFor: "website", className: "mb-2", children: "Website" }),
|
|
405
847
|
/* @__PURE__ */ jsx(
|
|
406
|
-
|
|
848
|
+
Controller,
|
|
407
849
|
{
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
850
|
+
name: "website",
|
|
851
|
+
control,
|
|
852
|
+
rules: {
|
|
853
|
+
pattern: {
|
|
854
|
+
value: /^https?:\/\/.+/,
|
|
855
|
+
message: "Website must start with http:// or https://"
|
|
856
|
+
}
|
|
857
|
+
},
|
|
858
|
+
render: ({ field }) => /* @__PURE__ */ jsx(
|
|
859
|
+
Input,
|
|
860
|
+
{
|
|
861
|
+
...field,
|
|
862
|
+
id: "website",
|
|
863
|
+
type: "url",
|
|
864
|
+
placeholder: "https://example.com"
|
|
865
|
+
}
|
|
866
|
+
)
|
|
413
867
|
}
|
|
414
868
|
),
|
|
415
|
-
/* @__PURE__ */ jsx(Text, { className: "mt-1 text-
|
|
869
|
+
errors.website && /* @__PURE__ */ jsx(Text, { className: "mt-1 text-sm text-ui-fg-error", children: errors.website.message }),
|
|
870
|
+
/* @__PURE__ */ jsx(Text, { className: "mt-1 text-xs text-ui-fg-subtle", children: "Provide the full URL including protocol." })
|
|
871
|
+
] }),
|
|
872
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
873
|
+
/* @__PURE__ */ jsx(Label, { htmlFor: "metadata", className: "mb-2", children: "Metadata (JSON)" }),
|
|
874
|
+
/* @__PURE__ */ jsx(
|
|
875
|
+
Controller,
|
|
876
|
+
{
|
|
877
|
+
name: "metadata",
|
|
878
|
+
control,
|
|
879
|
+
render: ({ field }) => /* @__PURE__ */ jsx(
|
|
880
|
+
Textarea,
|
|
881
|
+
{
|
|
882
|
+
...field,
|
|
883
|
+
id: "metadata",
|
|
884
|
+
placeholder: '{"seo_title": "Premium brand"}',
|
|
885
|
+
rows: 4
|
|
886
|
+
}
|
|
887
|
+
)
|
|
888
|
+
}
|
|
889
|
+
),
|
|
890
|
+
errors.metadata && /* @__PURE__ */ jsx(Text, { className: "mt-1 text-sm text-ui-fg-error", children: errors.metadata.message }),
|
|
891
|
+
/* @__PURE__ */ jsx(Text, { className: "mt-1 text-xs text-ui-fg-subtle", children: "Store custom data as a JSON object. Leave blank if not needed." })
|
|
416
892
|
] }),
|
|
417
893
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between rounded-lg border p-4", children: [
|
|
418
894
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
419
|
-
/* @__PURE__ */ jsx(Label, { htmlFor: "is_active", className: "font-medium", children: "Active
|
|
420
|
-
/* @__PURE__ */ jsx(Text, { className: "text-sm text-ui-fg-subtle", children: "
|
|
895
|
+
/* @__PURE__ */ jsx(Label, { htmlFor: "is_active", className: "font-medium", children: "Active status" }),
|
|
896
|
+
/* @__PURE__ */ jsx(Text, { className: "text-sm text-ui-fg-subtle", children: "Inactive brands are hidden from the storefront." })
|
|
421
897
|
] }),
|
|
422
898
|
/* @__PURE__ */ jsx(
|
|
423
|
-
|
|
899
|
+
Controller,
|
|
424
900
|
{
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
901
|
+
name: "is_active",
|
|
902
|
+
control,
|
|
903
|
+
render: ({ field }) => /* @__PURE__ */ jsx(
|
|
904
|
+
Switch,
|
|
905
|
+
{
|
|
906
|
+
id: "is_active",
|
|
907
|
+
checked: field.value,
|
|
908
|
+
onCheckedChange: field.onChange
|
|
909
|
+
}
|
|
910
|
+
)
|
|
428
911
|
}
|
|
429
912
|
)
|
|
430
913
|
] }),
|
|
431
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-end gap-3 pt-4
|
|
914
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-end gap-3 border-t pt-4", children: [
|
|
432
915
|
/* @__PURE__ */ jsx(Button, { type: "button", variant: "secondary", onClick: onClose, children: "Cancel" }),
|
|
433
|
-
/* @__PURE__ */ jsx(Button, { type: "submit", disabled: loading, children: loading ? "Saving
|
|
916
|
+
/* @__PURE__ */ jsx(Button, { type: "submit", disabled: loading, children: loading ? "Saving…" : isCreating ? "Create brand" : "Update brand" })
|
|
434
917
|
] })
|
|
435
918
|
] })
|
|
436
919
|
] }) });
|
|
@@ -447,14 +930,14 @@ const BrandImageUploader = ({
|
|
|
447
930
|
const [error, setError] = useState(null);
|
|
448
931
|
const [previewUrl, setPreviewUrl] = useState(null);
|
|
449
932
|
const isLogo = imageType === "logo";
|
|
450
|
-
const maxSize = isLogo ?
|
|
451
|
-
const allowedTypes = isLogo ? ["image/jpeg", "image/
|
|
933
|
+
const maxSize = isLogo ? 5 : 10;
|
|
934
|
+
const allowedTypes = isLogo ? ["image/jpeg", "image/jpg", "image/png", "image/webp", "image/svg+xml"] : ["image/jpeg", "image/jpg", "image/png", "image/webp"];
|
|
452
935
|
const formatFileTypes = () => {
|
|
453
|
-
return isLogo ? "
|
|
936
|
+
return isLogo ? "PNG, JPG, JPEG, WebP, or SVG" : "PNG, JPG, JPEG, or WebP";
|
|
454
937
|
};
|
|
455
938
|
const validateFile = (file) => {
|
|
456
939
|
if (!allowedTypes.includes(file.type)) {
|
|
457
|
-
setError(`Please upload a
|
|
940
|
+
setError(`Please upload a supported file type (${formatFileTypes()})`);
|
|
458
941
|
return false;
|
|
459
942
|
}
|
|
460
943
|
if (file.size > maxSize * 1024 * 1024) {
|
|
@@ -463,18 +946,30 @@ const BrandImageUploader = ({
|
|
|
463
946
|
}
|
|
464
947
|
return true;
|
|
465
948
|
};
|
|
466
|
-
const
|
|
467
|
-
if (!validateFile(file)) return;
|
|
468
|
-
setError(null);
|
|
949
|
+
const readFileAsDataUrl = (file) => new Promise((resolve, reject) => {
|
|
469
950
|
const reader = new FileReader();
|
|
470
|
-
reader.
|
|
471
|
-
|
|
951
|
+
reader.onload = () => {
|
|
952
|
+
if (typeof reader.result === "string") {
|
|
953
|
+
resolve(reader.result);
|
|
954
|
+
} else {
|
|
955
|
+
reject(new Error("Failed to read file"));
|
|
956
|
+
}
|
|
957
|
+
};
|
|
958
|
+
reader.onerror = () => {
|
|
959
|
+
reject(reader.error ?? new Error("Failed to read file"));
|
|
472
960
|
};
|
|
473
961
|
reader.readAsDataURL(file);
|
|
962
|
+
});
|
|
963
|
+
const handleFileUpload = async (file) => {
|
|
964
|
+
var _a;
|
|
965
|
+
if (!validateFile(file)) return;
|
|
966
|
+
setError(null);
|
|
474
967
|
setIsUploading(true);
|
|
475
|
-
const formData = new FormData();
|
|
476
|
-
formData.append("file", file);
|
|
477
968
|
try {
|
|
969
|
+
const dataUrl = await readFileAsDataUrl(file);
|
|
970
|
+
setPreviewUrl(dataUrl);
|
|
971
|
+
const formData = new FormData();
|
|
972
|
+
formData.append("file", file);
|
|
478
973
|
const response = await sdk.client.fetch(
|
|
479
974
|
`/admin/brands/${brand.id}/${imageType}`,
|
|
480
975
|
{
|
|
@@ -482,22 +977,25 @@ const BrandImageUploader = ({
|
|
|
482
977
|
body: formData
|
|
483
978
|
}
|
|
484
979
|
);
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
setTimeout(() => {
|
|
491
|
-
onClose();
|
|
492
|
-
}, 1e3);
|
|
493
|
-
} else {
|
|
494
|
-
const errorData = await response.json();
|
|
495
|
-
setError(errorData.error || `Failed to upload ${imageType}`);
|
|
980
|
+
const newImageUrl = ((_a = response == null ? void 0 : response.brand) == null ? void 0 : _a[imageType]) || null;
|
|
981
|
+
if (!newImageUrl) {
|
|
982
|
+
const message = `Failed to upload ${imageType}`;
|
|
983
|
+
setError(message);
|
|
984
|
+
toast.error(message);
|
|
496
985
|
setPreviewUrl(null);
|
|
986
|
+
return;
|
|
497
987
|
}
|
|
988
|
+
setDisplayImage(newImageUrl);
|
|
989
|
+
setPreviewUrl(null);
|
|
990
|
+
toast.success(`${isLogo ? "Logo" : "Image"} uploaded successfully`);
|
|
991
|
+
setTimeout(() => {
|
|
992
|
+
onClose();
|
|
993
|
+
}, 800);
|
|
498
994
|
} catch (err) {
|
|
499
|
-
|
|
995
|
+
const message = err instanceof FetchError ? err.message || `Failed to upload ${imageType}` : err instanceof Error ? err.message : `Failed to upload ${imageType}`;
|
|
996
|
+
setError(message);
|
|
500
997
|
setPreviewUrl(null);
|
|
998
|
+
toast.error(message);
|
|
501
999
|
console.error(`Error uploading ${imageType}:`, err);
|
|
502
1000
|
} finally {
|
|
503
1001
|
setIsUploading(false);
|
|
@@ -508,24 +1006,19 @@ const BrandImageUploader = ({
|
|
|
508
1006
|
setIsUploading(true);
|
|
509
1007
|
setError(null);
|
|
510
1008
|
try {
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
);
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
setTimeout(() => {
|
|
521
|
-
onClose();
|
|
522
|
-
}, 500);
|
|
523
|
-
} else {
|
|
524
|
-
const errorData = await response.json();
|
|
525
|
-
setError(errorData.error || `Failed to delete ${imageType}`);
|
|
526
|
-
}
|
|
1009
|
+
await sdk.client.fetch(`/admin/brands/${brand.id}/${imageType}`, {
|
|
1010
|
+
method: "DELETE"
|
|
1011
|
+
});
|
|
1012
|
+
setDisplayImage(null);
|
|
1013
|
+
setPreviewUrl(null);
|
|
1014
|
+
toast.success(`${isLogo ? "Logo" : "Image"} removed successfully`);
|
|
1015
|
+
setTimeout(() => {
|
|
1016
|
+
onClose();
|
|
1017
|
+
}, 500);
|
|
527
1018
|
} catch (err) {
|
|
528
|
-
|
|
1019
|
+
const message = err instanceof FetchError ? err.message || `Failed to delete ${imageType}` : err instanceof Error ? err.message : `Failed to delete ${imageType}`;
|
|
1020
|
+
setError(message);
|
|
1021
|
+
toast.error(message);
|
|
529
1022
|
console.error(`Error deleting ${imageType}:`, err);
|
|
530
1023
|
} finally {
|
|
531
1024
|
setIsUploading(false);
|
|
@@ -565,7 +1058,7 @@ const BrandImageUploader = ({
|
|
|
565
1058
|
] }),
|
|
566
1059
|
/* @__PURE__ */ jsx(Text, { className: "text-ui-fg-subtle", children: brand.name })
|
|
567
1060
|
] }),
|
|
568
|
-
/* @__PURE__ */ jsx(Button, { variant: "secondary", size: "small", onClick: onClose, children: /* @__PURE__ */ jsx(
|
|
1061
|
+
/* @__PURE__ */ jsx(Button, { variant: "secondary", size: "small", onClick: onClose, children: /* @__PURE__ */ jsx(XMark, {}) })
|
|
569
1062
|
] }),
|
|
570
1063
|
error && /* @__PURE__ */ jsx(Alert, { variant: "error", dismissible: true, className: "mb-4", children: error }),
|
|
571
1064
|
/* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
@@ -640,7 +1133,6 @@ const BrandImageUploader = ({
|
|
|
640
1133
|
maxSize,
|
|
641
1134
|
"MB"
|
|
642
1135
|
] }),
|
|
643
|
-
isLogo && /* @__PURE__ */ jsx(Text, { className: "mt-2 text-xs text-ui-fg-subtle", children: "Recommended: Square image, minimum 200x200px" }),
|
|
644
1136
|
isUploading && /* @__PURE__ */ jsx("div", { className: "mt-4", children: /* @__PURE__ */ jsx(Text, { children: "Uploading..." }) })
|
|
645
1137
|
]
|
|
646
1138
|
}
|
|
@@ -667,49 +1159,112 @@ const BrandImageUploader = ({
|
|
|
667
1159
|
] })
|
|
668
1160
|
] }) });
|
|
669
1161
|
};
|
|
1162
|
+
const PAGE_SIZE = 10;
|
|
1163
|
+
const SORT_OPTIONS = [
|
|
1164
|
+
{
|
|
1165
|
+
id: "created_desc",
|
|
1166
|
+
label: "Created · Newest",
|
|
1167
|
+
sort_by: "created_at",
|
|
1168
|
+
sort_order: "desc"
|
|
1169
|
+
},
|
|
1170
|
+
{
|
|
1171
|
+
id: "created_asc",
|
|
1172
|
+
label: "Created · Oldest",
|
|
1173
|
+
sort_by: "created_at",
|
|
1174
|
+
sort_order: "asc"
|
|
1175
|
+
},
|
|
1176
|
+
{
|
|
1177
|
+
id: "updated_desc",
|
|
1178
|
+
label: "Updated · Recent",
|
|
1179
|
+
sort_by: "updated_at",
|
|
1180
|
+
sort_order: "desc"
|
|
1181
|
+
},
|
|
1182
|
+
{
|
|
1183
|
+
id: "updated_asc",
|
|
1184
|
+
label: "Updated · Oldest",
|
|
1185
|
+
sort_by: "updated_at",
|
|
1186
|
+
sort_order: "asc"
|
|
1187
|
+
},
|
|
1188
|
+
{
|
|
1189
|
+
id: "name_asc",
|
|
1190
|
+
label: "Name · A → Z",
|
|
1191
|
+
sort_by: "name",
|
|
1192
|
+
sort_order: "asc"
|
|
1193
|
+
},
|
|
1194
|
+
{
|
|
1195
|
+
id: "name_desc",
|
|
1196
|
+
label: "Name · Z → A",
|
|
1197
|
+
sort_by: "name",
|
|
1198
|
+
sort_order: "desc"
|
|
1199
|
+
}
|
|
1200
|
+
];
|
|
1201
|
+
const statusFilters = [
|
|
1202
|
+
{ id: "all", label: "All" },
|
|
1203
|
+
{ id: "active", label: "Active" },
|
|
1204
|
+
{ id: "inactive", label: "Inactive" }
|
|
1205
|
+
];
|
|
670
1206
|
const BrandsPage = () => {
|
|
671
1207
|
const [brands, setBrands] = useState([]);
|
|
1208
|
+
const [totalCount, setTotalCount] = useState(0);
|
|
1209
|
+
const [currentPage, setCurrentPage] = useState(1);
|
|
1210
|
+
const [searchTerm, setSearchTerm] = useState("");
|
|
1211
|
+
const [debouncedSearch, setDebouncedSearch] = useState("");
|
|
1212
|
+
const [statusFilter, setStatusFilter] = useState("all");
|
|
1213
|
+
const [sortOption, setSortOption] = useState(SORT_OPTIONS[0]);
|
|
672
1214
|
const [loading, setLoading] = useState(true);
|
|
673
1215
|
const [error, setError] = useState(null);
|
|
674
1216
|
const [selectedBrand, setSelectedBrand] = useState(null);
|
|
675
1217
|
const [showForm, setShowForm] = useState(false);
|
|
676
1218
|
const [showImageUploader, setShowImageUploader] = useState(false);
|
|
677
1219
|
const [imageType, setImageType] = useState("image");
|
|
678
|
-
const [searchQuery, setSearchQuery] = useState("");
|
|
679
1220
|
const [isCreating, setIsCreating] = useState(false);
|
|
1221
|
+
const [isDeleting, setIsDeleting] = useState(false);
|
|
1222
|
+
const dialog = usePrompt();
|
|
680
1223
|
useEffect(() => {
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
1224
|
+
const handle = window.setTimeout(() => {
|
|
1225
|
+
setDebouncedSearch(searchTerm.trim());
|
|
1226
|
+
}, 300);
|
|
1227
|
+
return () => window.clearTimeout(handle);
|
|
1228
|
+
}, [searchTerm]);
|
|
1229
|
+
useEffect(() => {
|
|
1230
|
+
setCurrentPage(1);
|
|
1231
|
+
}, [debouncedSearch, statusFilter, sortOption.id]);
|
|
1232
|
+
const fetchBrands = useCallback(async () => {
|
|
684
1233
|
setLoading(true);
|
|
685
1234
|
setError(null);
|
|
1235
|
+
const params = new URLSearchParams({
|
|
1236
|
+
limit: String(PAGE_SIZE),
|
|
1237
|
+
offset: String((currentPage - 1) * PAGE_SIZE),
|
|
1238
|
+
sort_by: sortOption.sort_by,
|
|
1239
|
+
sort_order: sortOption.sort_order
|
|
1240
|
+
});
|
|
1241
|
+
if (debouncedSearch) {
|
|
1242
|
+
params.set("q", debouncedSearch);
|
|
1243
|
+
}
|
|
1244
|
+
if (statusFilter !== "all") {
|
|
1245
|
+
params.set("status", statusFilter);
|
|
1246
|
+
}
|
|
686
1247
|
try {
|
|
687
|
-
const
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
`/admin/brands?${queryParams}`,
|
|
693
|
-
{}
|
|
1248
|
+
const data = await sdk.client.fetch(
|
|
1249
|
+
`/admin/brands?${params.toString()}`,
|
|
1250
|
+
{
|
|
1251
|
+
method: "GET"
|
|
1252
|
+
}
|
|
694
1253
|
);
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
setBrands(data.brands || []);
|
|
698
|
-
} else {
|
|
699
|
-
setError("Failed to fetch brands");
|
|
700
|
-
}
|
|
1254
|
+
setBrands((data == null ? void 0 : data.brands) ?? []);
|
|
1255
|
+
setTotalCount((data == null ? void 0 : data.count) ?? 0);
|
|
701
1256
|
} catch (err) {
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
setError("Error fetching brands");
|
|
707
|
-
console.error("Error fetching brands:", err);
|
|
708
|
-
}
|
|
1257
|
+
const message = err instanceof FetchError ? err.message : err instanceof Error ? err.message : "Error fetching brands";
|
|
1258
|
+
setError(message || "Error fetching brands");
|
|
1259
|
+
setBrands([]);
|
|
1260
|
+
setTotalCount(0);
|
|
709
1261
|
} finally {
|
|
710
1262
|
setLoading(false);
|
|
711
1263
|
}
|
|
712
|
-
};
|
|
1264
|
+
}, [currentPage, debouncedSearch, statusFilter, sortOption]);
|
|
1265
|
+
useEffect(() => {
|
|
1266
|
+
fetchBrands();
|
|
1267
|
+
}, [fetchBrands]);
|
|
713
1268
|
const handleCreateBrand = () => {
|
|
714
1269
|
setIsCreating(true);
|
|
715
1270
|
setSelectedBrand(null);
|
|
@@ -721,44 +1276,41 @@ const BrandsPage = () => {
|
|
|
721
1276
|
setShowForm(true);
|
|
722
1277
|
};
|
|
723
1278
|
const handleDeleteBrand = async (brandId) => {
|
|
724
|
-
|
|
725
|
-
"
|
|
726
|
-
|
|
1279
|
+
const confirmed = await dialog({
|
|
1280
|
+
title: "Delete brand",
|
|
1281
|
+
description: "This will remove the brand and detach it from any associated products.",
|
|
1282
|
+
confirmText: "Delete",
|
|
1283
|
+
cancelText: "Cancel",
|
|
1284
|
+
variant: "danger"
|
|
1285
|
+
});
|
|
1286
|
+
if (!confirmed) {
|
|
727
1287
|
return;
|
|
728
1288
|
}
|
|
1289
|
+
setIsDeleting(true);
|
|
729
1290
|
try {
|
|
730
|
-
|
|
1291
|
+
await sdk.client.fetch(`/admin/brands/${brandId}`, {
|
|
731
1292
|
method: "DELETE"
|
|
732
1293
|
});
|
|
733
|
-
|
|
734
|
-
fetchBrands();
|
|
735
|
-
} else {
|
|
736
|
-
alert("Failed to delete brand");
|
|
737
|
-
}
|
|
1294
|
+
await fetchBrands();
|
|
738
1295
|
} catch (err) {
|
|
739
|
-
|
|
740
|
-
|
|
1296
|
+
const message = err instanceof FetchError ? err.message : err instanceof Error ? err.message : "Error deleting brand";
|
|
1297
|
+
setError(message || "Error deleting brand");
|
|
1298
|
+
} finally {
|
|
1299
|
+
setIsDeleting(false);
|
|
741
1300
|
}
|
|
742
1301
|
};
|
|
743
1302
|
const handleToggleActive = async (brand) => {
|
|
744
1303
|
try {
|
|
745
|
-
|
|
1304
|
+
await sdk.client.fetch(`/admin/brands/${brand.id}`, {
|
|
746
1305
|
method: "PUT",
|
|
747
|
-
|
|
748
|
-
"Content-Type": "application/json"
|
|
749
|
-
},
|
|
750
|
-
body: JSON.stringify({
|
|
1306
|
+
body: {
|
|
751
1307
|
is_active: !brand.is_active
|
|
752
|
-
}
|
|
1308
|
+
}
|
|
753
1309
|
});
|
|
754
|
-
|
|
755
|
-
fetchBrands();
|
|
756
|
-
} else {
|
|
757
|
-
alert("Failed to update brand status");
|
|
758
|
-
}
|
|
1310
|
+
await fetchBrands();
|
|
759
1311
|
} catch (err) {
|
|
760
|
-
|
|
761
|
-
|
|
1312
|
+
const message = err instanceof FetchError ? err.message : err instanceof Error ? err.message : "Error updating brand status";
|
|
1313
|
+
setError(message || "Error updating brand status");
|
|
762
1314
|
}
|
|
763
1315
|
};
|
|
764
1316
|
const handleImageUpload = (brand, type) => {
|
|
@@ -766,164 +1318,247 @@ const BrandsPage = () => {
|
|
|
766
1318
|
setImageType(type);
|
|
767
1319
|
setShowImageUploader(true);
|
|
768
1320
|
};
|
|
769
|
-
const handleFormClose = () => {
|
|
1321
|
+
const handleFormClose = async () => {
|
|
770
1322
|
setShowForm(false);
|
|
771
1323
|
setSelectedBrand(null);
|
|
772
1324
|
setIsCreating(false);
|
|
773
|
-
fetchBrands();
|
|
1325
|
+
await fetchBrands();
|
|
774
1326
|
};
|
|
775
|
-
const handleImageUploaderClose = () => {
|
|
1327
|
+
const handleImageUploaderClose = async () => {
|
|
776
1328
|
setShowImageUploader(false);
|
|
777
1329
|
setSelectedBrand(null);
|
|
778
|
-
fetchBrands();
|
|
1330
|
+
await fetchBrands();
|
|
779
1331
|
};
|
|
780
|
-
const
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
1332
|
+
const totalPages = Math.max(Math.ceil(totalCount / PAGE_SIZE), 1);
|
|
1333
|
+
const pageNumbers = useMemo(() => {
|
|
1334
|
+
const pages = /* @__PURE__ */ new Set([1, totalPages]);
|
|
1335
|
+
pages.add(currentPage);
|
|
1336
|
+
pages.add(currentPage - 1);
|
|
1337
|
+
pages.add(currentPage + 1);
|
|
1338
|
+
pages.add(currentPage - 2);
|
|
1339
|
+
pages.add(currentPage + 2);
|
|
1340
|
+
const filtered = Array.from(pages).filter((page) => page >= 1 && page <= totalPages).sort((a, b) => a - b);
|
|
1341
|
+
return filtered;
|
|
1342
|
+
}, [currentPage, totalPages]);
|
|
1343
|
+
if (loading && brands.length === 0) {
|
|
1344
|
+
return /* @__PURE__ */ jsx(Container, { children: /* @__PURE__ */ jsx("div", { className: "flex h-64 items-center justify-center", children: /* @__PURE__ */ jsx(Text, { children: "Loading brands…" }) }) });
|
|
791
1345
|
}
|
|
792
1346
|
return /* @__PURE__ */ jsxs(Container, { children: [
|
|
793
|
-
/* @__PURE__ */ jsxs("div", { className: "mb-8", children: [
|
|
794
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between
|
|
1347
|
+
/* @__PURE__ */ jsxs("div", { className: "mb-8 space-y-6", children: [
|
|
1348
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-4", children: [
|
|
795
1349
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
796
|
-
/* @__PURE__ */ jsx(Heading, { level: "h1",
|
|
797
|
-
/* @__PURE__ */ jsx(Text, { className: "text-ui-fg-subtle", children: "
|
|
1350
|
+
/* @__PURE__ */ jsx(Heading, { level: "h1", children: "Brands" }),
|
|
1351
|
+
/* @__PURE__ */ jsx(Text, { className: "text-ui-fg-subtle", children: "Organize products by brand, manage visibility, and upload brand assets." })
|
|
798
1352
|
] }),
|
|
799
1353
|
/* @__PURE__ */ jsxs(Button, { onClick: handleCreateBrand, children: [
|
|
800
1354
|
/* @__PURE__ */ jsx(Plus, { className: "mr-2" }),
|
|
801
1355
|
"Create Brand"
|
|
802
1356
|
] })
|
|
803
1357
|
] }),
|
|
804
|
-
/* @__PURE__ */ jsx(
|
|
805
|
-
|
|
806
|
-
|
|
1358
|
+
error && /* @__PURE__ */ jsx(Alert, { variant: "error", dismissible: true, children: error }),
|
|
1359
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-4", children: [
|
|
1360
|
+
/* @__PURE__ */ jsxs("div", { className: "relative flex-1 min-w-[220px] max-w-md", children: [
|
|
1361
|
+
/* @__PURE__ */ jsx(
|
|
1362
|
+
Input,
|
|
1363
|
+
{
|
|
1364
|
+
value: searchTerm,
|
|
1365
|
+
onChange: (event) => setSearchTerm(event.target.value),
|
|
1366
|
+
placeholder: "Search by name, slug, or description",
|
|
1367
|
+
className: "pl-10"
|
|
1368
|
+
}
|
|
1369
|
+
),
|
|
1370
|
+
/* @__PURE__ */ jsx(MagnifyingGlass, { className: "absolute left-3 top-1/2 -translate-y-1/2 text-ui-fg-muted" })
|
|
1371
|
+
] }),
|
|
1372
|
+
/* @__PURE__ */ jsx("div", { className: "flex items-center gap-2", children: statusFilters.map((filter) => /* @__PURE__ */ jsx(
|
|
1373
|
+
Button,
|
|
807
1374
|
{
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
1375
|
+
size: "small",
|
|
1376
|
+
variant: statusFilter === filter.id ? "primary" : "secondary",
|
|
1377
|
+
onClick: () => setStatusFilter(filter.id),
|
|
1378
|
+
children: filter.label
|
|
1379
|
+
},
|
|
1380
|
+
filter.id
|
|
1381
|
+
)) }),
|
|
1382
|
+
/* @__PURE__ */ jsxs(DropdownMenu, { children: [
|
|
1383
|
+
/* @__PURE__ */ jsx(DropdownMenu.Trigger, { asChild: true, children: /* @__PURE__ */ jsxs(Button, { variant: "secondary", size: "small", children: [
|
|
1384
|
+
"Sort: ",
|
|
1385
|
+
sortOption.label
|
|
1386
|
+
] }) }),
|
|
1387
|
+
/* @__PURE__ */ jsx(DropdownMenu.Content, { children: SORT_OPTIONS.map((option) => /* @__PURE__ */ jsx(
|
|
1388
|
+
DropdownMenu.Item,
|
|
1389
|
+
{
|
|
1390
|
+
onClick: () => setSortOption(option),
|
|
1391
|
+
children: option.label
|
|
1392
|
+
},
|
|
1393
|
+
option.id
|
|
1394
|
+
)) })
|
|
1395
|
+
] })
|
|
1396
|
+
] })
|
|
816
1397
|
] }),
|
|
817
|
-
|
|
1398
|
+
brands.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 text-center", children: [
|
|
818
1399
|
/* @__PURE__ */ jsx(BuildingStorefront, { className: "mb-4 h-12 w-12 text-ui-fg-subtle" }),
|
|
819
|
-
/* @__PURE__ */ jsx(Text, { className: "mb-2 text-lg font-medium", children:
|
|
820
|
-
/* @__PURE__ */ jsx(Text, { className: "mb-4 text-ui-fg-subtle", children:
|
|
821
|
-
!
|
|
1400
|
+
/* @__PURE__ */ jsx(Text, { className: "mb-2 text-lg font-medium", children: debouncedSearch ? "No brands match your filters" : "No brands yet" }),
|
|
1401
|
+
/* @__PURE__ */ jsx(Text, { className: "mb-4 text-ui-fg-subtle max-w-md", children: debouncedSearch ? "Try adjusting your search or filters to see more results." : "Create your first brand to showcase manufacturers and power brand-based merchandising." }),
|
|
1402
|
+
!debouncedSearch && /* @__PURE__ */ jsxs(Button, { onClick: handleCreateBrand, children: [
|
|
822
1403
|
/* @__PURE__ */ jsx(Plus, { className: "mr-2" }),
|
|
823
1404
|
"Create Brand"
|
|
824
1405
|
] })
|
|
825
|
-
] }) : /* @__PURE__ */
|
|
826
|
-
/* @__PURE__ */
|
|
827
|
-
/* @__PURE__ */ jsx(Table.
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
/* @__PURE__ */ jsx(Table.Body, { children: filteredBrands.map((brand) => /* @__PURE__ */ jsxs(Table.Row, { children: [
|
|
835
|
-
/* @__PURE__ */ jsx(Table.Cell, { children: brand.logo ? /* @__PURE__ */ jsx(
|
|
836
|
-
"img",
|
|
837
|
-
{
|
|
838
|
-
src: brand.logo,
|
|
839
|
-
alt: `${brand.name} logo`,
|
|
840
|
-
className: "h-10 w-10 object-contain rounded"
|
|
841
|
-
}
|
|
842
|
-
) : /* @__PURE__ */ jsx("div", { className: "flex h-10 w-10 items-center justify-center rounded bg-ui-bg-subtle", children: /* @__PURE__ */ jsx(BuildingStorefront, { className: "h-5 w-5 text-ui-fg-subtle" }) }) }),
|
|
843
|
-
/* @__PURE__ */ jsx(Table.Cell, { children: /* @__PURE__ */ jsxs("div", { children: [
|
|
844
|
-
/* @__PURE__ */ jsx(Text, { className: "font-medium", children: brand.name }),
|
|
845
|
-
brand.website && /* @__PURE__ */ jsx(Text, { className: "text-xs text-ui-fg-subtle", children: brand.website })
|
|
1406
|
+
] }) : /* @__PURE__ */ jsxs("div", { className: "overflow-hidden rounded-lg border", children: [
|
|
1407
|
+
/* @__PURE__ */ jsxs(Table, { children: [
|
|
1408
|
+
/* @__PURE__ */ jsx(Table.Header, { children: /* @__PURE__ */ jsxs(Table.Row, { children: [
|
|
1409
|
+
/* @__PURE__ */ jsx(Table.HeaderCell, { children: "Logo" }),
|
|
1410
|
+
/* @__PURE__ */ jsx(Table.HeaderCell, { children: "Name" }),
|
|
1411
|
+
/* @__PURE__ */ jsx(Table.HeaderCell, { children: "Slug" }),
|
|
1412
|
+
/* @__PURE__ */ jsx(Table.HeaderCell, { children: "Status" }),
|
|
1413
|
+
/* @__PURE__ */ jsx(Table.HeaderCell, { children: "Assets" }),
|
|
1414
|
+
/* @__PURE__ */ jsx(Table.HeaderCell, { className: "text-right", children: "Actions" })
|
|
846
1415
|
] }) }),
|
|
847
|
-
/* @__PURE__ */ jsx(Table.
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
/* @__PURE__ */ jsx(
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
DropdownMenu.Item,
|
|
865
|
-
{
|
|
866
|
-
onClick: () => handleEditBrand(brand),
|
|
867
|
-
children: [
|
|
868
|
-
/* @__PURE__ */ jsx(PencilSquare, { className: "mr-2" }),
|
|
869
|
-
"Edit Details"
|
|
870
|
-
]
|
|
871
|
-
}
|
|
872
|
-
),
|
|
873
|
-
/* @__PURE__ */ jsx(DropdownMenu.Separator, {}),
|
|
874
|
-
/* @__PURE__ */ jsxs(
|
|
875
|
-
DropdownMenu.Item,
|
|
876
|
-
{
|
|
877
|
-
onClick: () => handleImageUpload(brand, "image"),
|
|
878
|
-
children: [
|
|
879
|
-
/* @__PURE__ */ jsx(CloudArrowUp, { className: "mr-2" }),
|
|
880
|
-
brand.image ? "Replace Image" : "Upload Image"
|
|
881
|
-
]
|
|
882
|
-
}
|
|
883
|
-
),
|
|
884
|
-
/* @__PURE__ */ jsxs(
|
|
885
|
-
DropdownMenu.Item,
|
|
886
|
-
{
|
|
887
|
-
onClick: () => handleImageUpload(brand, "logo"),
|
|
888
|
-
children: [
|
|
889
|
-
/* @__PURE__ */ jsx(CloudArrowUp, { className: "mr-2" }),
|
|
890
|
-
brand.logo ? "Replace Logo" : "Upload Logo"
|
|
891
|
-
]
|
|
892
|
-
}
|
|
893
|
-
),
|
|
894
|
-
/* @__PURE__ */ jsx(DropdownMenu.Separator, {}),
|
|
895
|
-
/* @__PURE__ */ jsxs(
|
|
896
|
-
DropdownMenu.Item,
|
|
1416
|
+
/* @__PURE__ */ jsx(Table.Body, { children: brands.map((brand) => /* @__PURE__ */ jsxs(Table.Row, { children: [
|
|
1417
|
+
/* @__PURE__ */ jsx(Table.Cell, { children: brand.logo ? /* @__PURE__ */ jsx(
|
|
1418
|
+
"img",
|
|
1419
|
+
{
|
|
1420
|
+
src: brand.logo,
|
|
1421
|
+
alt: `${brand.name} logo`,
|
|
1422
|
+
className: "h-10 w-10 rounded object-contain"
|
|
1423
|
+
}
|
|
1424
|
+
) : /* @__PURE__ */ jsx("div", { className: "flex h-10 w-10 items-center justify-center rounded bg-ui-bg-subtle", children: /* @__PURE__ */ jsx(BuildingStorefront, { className: "h-5 w-5 text-ui-fg-subtle" }) }) }),
|
|
1425
|
+
/* @__PURE__ */ jsx(Table.Cell, { children: /* @__PURE__ */ jsxs("div", { className: "space-y-0.5", children: [
|
|
1426
|
+
/* @__PURE__ */ jsx(Text, { className: "font-medium", children: brand.name }),
|
|
1427
|
+
brand.website && /* @__PURE__ */ jsx(Text, { className: "text-xs text-ui-fg-subtle truncate max-w-[220px]", children: brand.website })
|
|
1428
|
+
] }) }),
|
|
1429
|
+
/* @__PURE__ */ jsx(Table.Cell, { children: /* @__PURE__ */ jsx(Badge, { color: "blue", children: brand.slug }) }),
|
|
1430
|
+
/* @__PURE__ */ jsx(Table.Cell, { children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
1431
|
+
/* @__PURE__ */ jsx(
|
|
1432
|
+
Switch,
|
|
897
1433
|
{
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
Switch,
|
|
902
|
-
{
|
|
903
|
-
checked: brand.is_active,
|
|
904
|
-
className: "mr-2 pointer-events-none"
|
|
905
|
-
}
|
|
906
|
-
),
|
|
907
|
-
brand.is_active ? "Deactivate" : "Activate"
|
|
908
|
-
]
|
|
1434
|
+
checked: brand.is_active,
|
|
1435
|
+
onCheckedChange: () => handleToggleActive(brand),
|
|
1436
|
+
"aria-label": `Toggle ${brand.name} visibility`
|
|
909
1437
|
}
|
|
910
1438
|
),
|
|
911
|
-
/* @__PURE__ */ jsx(
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
)
|
|
923
|
-
] })
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
1439
|
+
/* @__PURE__ */ jsx(Text, { className: "text-sm text-ui-fg-subtle", children: brand.is_active ? "Active" : "Inactive" })
|
|
1440
|
+
] }) }),
|
|
1441
|
+
/* @__PURE__ */ jsx(Table.Cell, { children: /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
|
|
1442
|
+
brand.image && /* @__PURE__ */ jsxs(Badge, { color: "green", size: "xsmall", children: [
|
|
1443
|
+
/* @__PURE__ */ jsx(PhotoSolid, { className: "mr-1" }),
|
|
1444
|
+
" Image"
|
|
1445
|
+
] }),
|
|
1446
|
+
brand.logo && /* @__PURE__ */ jsxs(Badge, { color: "green", size: "xsmall", children: [
|
|
1447
|
+
/* @__PURE__ */ jsx(BuildingStorefront, { className: "mr-1" }),
|
|
1448
|
+
" Logo"
|
|
1449
|
+
] }),
|
|
1450
|
+
!brand.image && !brand.logo && /* @__PURE__ */ jsx(Text, { className: "text-sm text-ui-fg-muted", children: "No assets" })
|
|
1451
|
+
] }) }),
|
|
1452
|
+
/* @__PURE__ */ jsx(Table.Cell, { children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-end", children: /* @__PURE__ */ jsxs(DropdownMenu, { children: [
|
|
1453
|
+
/* @__PURE__ */ jsx(DropdownMenu.Trigger, { asChild: true, children: /* @__PURE__ */ jsx(IconButton, { variant: "transparent", size: "small", children: /* @__PURE__ */ jsx(EllipsisHorizontal, {}) }) }),
|
|
1454
|
+
/* @__PURE__ */ jsxs(DropdownMenu.Content, { children: [
|
|
1455
|
+
/* @__PURE__ */ jsxs(
|
|
1456
|
+
DropdownMenu.Item,
|
|
1457
|
+
{
|
|
1458
|
+
onClick: () => handleEditBrand(brand),
|
|
1459
|
+
children: [
|
|
1460
|
+
/* @__PURE__ */ jsx(PencilSquare, { className: "mr-2" }),
|
|
1461
|
+
" Edit details"
|
|
1462
|
+
]
|
|
1463
|
+
}
|
|
1464
|
+
),
|
|
1465
|
+
/* @__PURE__ */ jsx(DropdownMenu.Separator, {}),
|
|
1466
|
+
/* @__PURE__ */ jsxs(
|
|
1467
|
+
DropdownMenu.Item,
|
|
1468
|
+
{
|
|
1469
|
+
onClick: () => handleImageUpload(brand, "image"),
|
|
1470
|
+
children: [
|
|
1471
|
+
/* @__PURE__ */ jsx(CloudArrowUp, { className: "mr-2" }),
|
|
1472
|
+
brand.image ? "Replace image" : "Upload image"
|
|
1473
|
+
]
|
|
1474
|
+
}
|
|
1475
|
+
),
|
|
1476
|
+
/* @__PURE__ */ jsxs(
|
|
1477
|
+
DropdownMenu.Item,
|
|
1478
|
+
{
|
|
1479
|
+
onClick: () => handleImageUpload(brand, "logo"),
|
|
1480
|
+
children: [
|
|
1481
|
+
/* @__PURE__ */ jsx(CloudArrowUp, { className: "mr-2" }),
|
|
1482
|
+
brand.logo ? "Replace logo" : "Upload logo"
|
|
1483
|
+
]
|
|
1484
|
+
}
|
|
1485
|
+
),
|
|
1486
|
+
/* @__PURE__ */ jsx(DropdownMenu.Separator, {}),
|
|
1487
|
+
/* @__PURE__ */ jsxs(
|
|
1488
|
+
DropdownMenu.Item,
|
|
1489
|
+
{
|
|
1490
|
+
className: "text-ui-fg-error",
|
|
1491
|
+
disabled: isDeleting,
|
|
1492
|
+
onClick: () => handleDeleteBrand(brand.id),
|
|
1493
|
+
children: [
|
|
1494
|
+
/* @__PURE__ */ jsx(Trash, { className: "mr-2" }),
|
|
1495
|
+
" Delete brand"
|
|
1496
|
+
]
|
|
1497
|
+
}
|
|
1498
|
+
)
|
|
1499
|
+
] })
|
|
1500
|
+
] }) }) })
|
|
1501
|
+
] }, brand.id)) })
|
|
1502
|
+
] }),
|
|
1503
|
+
totalCount > PAGE_SIZE && /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3 border-t px-4 py-3 text-sm", children: [
|
|
1504
|
+
/* @__PURE__ */ jsxs(Text, { className: "text-ui-fg-subtle", children: [
|
|
1505
|
+
"Showing",
|
|
1506
|
+
" ",
|
|
1507
|
+
Math.min((currentPage - 1) * PAGE_SIZE + 1, totalCount),
|
|
1508
|
+
" to",
|
|
1509
|
+
" ",
|
|
1510
|
+
Math.min(currentPage * PAGE_SIZE, totalCount),
|
|
1511
|
+
" of ",
|
|
1512
|
+
totalCount,
|
|
1513
|
+
" ",
|
|
1514
|
+
"brands"
|
|
1515
|
+
] }),
|
|
1516
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
1517
|
+
/* @__PURE__ */ jsxs(
|
|
1518
|
+
Button,
|
|
1519
|
+
{
|
|
1520
|
+
variant: "secondary",
|
|
1521
|
+
size: "small",
|
|
1522
|
+
disabled: currentPage === 1,
|
|
1523
|
+
onClick: () => setCurrentPage((page) => Math.max(page - 1, 1)),
|
|
1524
|
+
children: [
|
|
1525
|
+
/* @__PURE__ */ jsx(ChevronLeft, {}),
|
|
1526
|
+
" Previous"
|
|
1527
|
+
]
|
|
1528
|
+
}
|
|
1529
|
+
),
|
|
1530
|
+
/* @__PURE__ */ jsx("div", { className: "flex items-center gap-1", children: pageNumbers.map((page, index) => {
|
|
1531
|
+
const previous = pageNumbers[index - 1];
|
|
1532
|
+
const showEllipsis = previous && page - previous > 1;
|
|
1533
|
+
return /* @__PURE__ */ jsxs(React.Fragment, { children: [
|
|
1534
|
+
showEllipsis && /* @__PURE__ */ jsx(Text, { className: "px-1", children: "…" }),
|
|
1535
|
+
/* @__PURE__ */ jsx(
|
|
1536
|
+
Button,
|
|
1537
|
+
{
|
|
1538
|
+
size: "small",
|
|
1539
|
+
variant: page === currentPage ? "primary" : "secondary",
|
|
1540
|
+
onClick: () => setCurrentPage(page),
|
|
1541
|
+
children: page
|
|
1542
|
+
}
|
|
1543
|
+
)
|
|
1544
|
+
] }, page);
|
|
1545
|
+
}) }),
|
|
1546
|
+
/* @__PURE__ */ jsxs(
|
|
1547
|
+
Button,
|
|
1548
|
+
{
|
|
1549
|
+
variant: "secondary",
|
|
1550
|
+
size: "small",
|
|
1551
|
+
disabled: currentPage >= totalPages,
|
|
1552
|
+
onClick: () => setCurrentPage((page) => Math.min(page + 1, totalPages)),
|
|
1553
|
+
children: [
|
|
1554
|
+
"Next ",
|
|
1555
|
+
/* @__PURE__ */ jsx(ChevronRight, {})
|
|
1556
|
+
]
|
|
1557
|
+
}
|
|
1558
|
+
)
|
|
1559
|
+
] })
|
|
1560
|
+
] })
|
|
1561
|
+
] }),
|
|
927
1562
|
showForm && /* @__PURE__ */ jsx(
|
|
928
1563
|
BrandForm,
|
|
929
1564
|
{
|