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