@lodashventure/medusa-brand 1.2.19 → 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.
Files changed (48) hide show
  1. package/.medusa/server/src/admin/index.js +1191 -556
  2. package/.medusa/server/src/admin/index.mjs +1165 -530
  3. package/.medusa/server/src/api/admin/brands/[id]/image/route.js +178 -0
  4. package/.medusa/server/src/api/admin/brands/[id]/logo/route.js +179 -0
  5. package/.medusa/server/src/api/admin/brands/[id]/products/route.js +55 -0
  6. package/.medusa/server/src/api/admin/brands/[id]/route.js +251 -0
  7. package/.medusa/server/src/api/admin/brands/route.js +276 -0
  8. package/.medusa/server/src/api/admin/products/[id]/brand/route.js +117 -0
  9. package/.medusa/server/src/api/middlewares/attach-brand-to-products.js +110 -0
  10. package/.medusa/server/src/api/middlewares.js +53 -0
  11. package/.medusa/server/src/api/store/brands/[id]/route.js +31 -0
  12. package/.medusa/server/src/api/store/brands/route.js +99 -0
  13. package/.medusa/server/{index.js → src/index.js} +1 -1
  14. package/.medusa/server/{modules → src/modules}/brand/index.js +1 -1
  15. package/.medusa/server/src/modules/brand/models/brand.js +40 -0
  16. package/.medusa/server/{modules → src/modules}/brand/service.js +1 -1
  17. package/.medusa/server/src/services/gcs-direct-upload.js +93 -0
  18. package/.medusa/server/src/workflows/upload-brand-image.js +66 -0
  19. package/package.json +11 -10
  20. package/.medusa/server/api/admin/brands/[id]/image/route.d.ts +0 -5
  21. package/.medusa/server/api/admin/brands/[id]/image/route.js +0 -119
  22. package/.medusa/server/api/admin/brands/[id]/logo/route.d.ts +0 -5
  23. package/.medusa/server/api/admin/brands/[id]/logo/route.js +0 -119
  24. package/.medusa/server/api/admin/brands/[id]/products/route.d.ts +0 -2
  25. package/.medusa/server/api/admin/brands/[id]/products/route.js +0 -52
  26. package/.medusa/server/api/admin/brands/[id]/route.d.ts +0 -5
  27. package/.medusa/server/api/admin/brands/[id]/route.js +0 -112
  28. package/.medusa/server/api/admin/brands/route.d.ts +0 -4
  29. package/.medusa/server/api/admin/brands/route.js +0 -76
  30. package/.medusa/server/api/admin/products/[id]/brand/route.d.ts +0 -5
  31. package/.medusa/server/api/admin/products/[id]/brand/route.js +0 -117
  32. package/.medusa/server/api/middlewares/attach-brand-to-products.d.ts +0 -2
  33. package/.medusa/server/api/middlewares/attach-brand-to-products.js +0 -105
  34. package/.medusa/server/api/middlewares.d.ts +0 -6
  35. package/.medusa/server/api/middlewares.js +0 -27
  36. package/.medusa/server/api/store/brands/route.d.ts +0 -2
  37. package/.medusa/server/api/store/brands/route.js +0 -53
  38. package/.medusa/server/index.d.ts +0 -1
  39. package/.medusa/server/modules/brand/index.d.ts +0 -35
  40. package/.medusa/server/modules/brand/migrations/Migration20251021070648.d.ts +0 -5
  41. package/.medusa/server/modules/brand/migrations/Migration20251021070648.js +0 -28
  42. package/.medusa/server/modules/brand/models/brand.d.ts +0 -16
  43. package/.medusa/server/modules/brand/models/brand.js +0 -43
  44. package/.medusa/server/modules/brand/service.d.ts +0 -21
  45. package/.medusa/server/services/gcs-direct-upload.d.ts +0 -8
  46. package/.medusa/server/services/gcs-direct-upload.js +0 -55
  47. package/.medusa/server/workflows/upload-brand-image.d.ts +0 -15
  48. 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 react = require("react");
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 Medusa__default = /* @__PURE__ */ _interopDefault(Medusa);
11
- const sdk = new Medusa__default.default({
12
- baseUrl: "/",
13
- debug: false,
14
- auth: {
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
- const ProductBrandWidget = ({ data: product }) => {
19
- const [brands, setBrands] = react.useState([]);
20
- const [currentBrand, setCurrentBrand] = react.useState(null);
21
- const [selectedBrandId, setSelectedBrandId] = react.useState("none");
22
- const [loading, setLoading] = react.useState(false);
23
- const [error, setError] = react.useState(null);
24
- const [success, setSuccess] = react.useState(null);
25
- react.useEffect(() => {
26
- fetchBrands();
27
- fetchProductBrand();
28
- }, [product.id]);
29
- const fetchBrands = async () => {
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 response = await sdk.client.fetch(
32
- "/admin/brands?is_active=true&limit=100",
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
- if (response.ok) {
36
- const data = await response.json();
37
- setBrands(data.brands || []);
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
- if (err instanceof Medusa.FetchError && err.status === 404) {
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 response = await sdk.client.fetch(
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 Medusa.FetchError && err.status === 404) {
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
- const handleBrandChange = (brandId) => {
73
- setSelectedBrandId(brandId);
74
- setError(null);
75
- setSuccess(null);
76
- };
77
- const handleSaveBrand = async () => {
78
- if (!selectedBrandId || selectedBrandId === "none") {
79
- if (currentBrand) {
80
- await handleRemoveBrand();
81
- }
82
- return;
83
- }
84
- setLoading(true);
85
- setError(null);
86
- setSuccess(null);
87
- try {
88
- const response = await sdk.client.fetch(
89
- `/admin/products/${product.id}/brand`,
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
- headers: {
93
- "Content-Type": "application/json"
94
- },
95
- body: JSON.stringify({ brand_id: selectedBrandId })
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
- fetchProductBrand();
101
- } else {
102
- const data = await response.json();
103
- setError(data.error || "Failed to update brand");
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
- } catch (err) {
106
- setError("Error updating brand");
107
- console.error("Error updating brand:", err);
108
- } finally {
109
- setLoading(false);
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
- if (!confirm("Are you sure you want to remove the brand from this product?")) {
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
- setLoading(true);
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
- return /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { className: "divide-y px-0 pb-0 pt-0", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-6", children: [
143
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 mb-4", children: [
144
- /* @__PURE__ */ jsxRuntime.jsx(icons.BuildingStorefront, { className: "text-ui-fg-subtle" }),
145
- /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: "Brand" })
146
- ] }),
147
- error && /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "error", dismissible: true, className: "mb-4", children: error }),
148
- success && /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "success", dismissible: true, className: "mb-4", children: success }),
149
- currentBrand && !hasChanges && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-4 p-4 rounded-lg border bg-ui-bg-subtle", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
150
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
151
- currentBrand.logo ? /* @__PURE__ */ jsxRuntime.jsx(
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.jsxs(
168
- ui.Button,
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: "danger",
171
- size: "small",
172
- onClick: handleRemoveBrand,
173
- disabled: loading,
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
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
182
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
183
- /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "brand", className: "mb-2", children: "Select Brand" }),
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.Select,
430
+ ui.Button,
186
431
  {
187
- value: selectedBrandId,
188
- onValueChange: handleBrandChange,
432
+ variant: "danger",
433
+ size: "small",
434
+ onClick: handleRemoveBrand,
189
435
  disabled: loading,
190
436
  children: [
191
- /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Trigger, { id: "brand", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Value, { placeholder: "Choose a brand..." }) }),
192
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Select.Content, { children: [
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
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "mt-1 text-xs text-ui-fg-subtle", children: "Assign a brand to help customers find products by their favorite brands" })
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
- hasChanges && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2", children: [
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
- ui.Button,
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
- variant: "secondary",
227
- size: "small",
228
- onClick: () => {
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
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-6 pt-6 border-t", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 text-sm", children: [
240
- /* @__PURE__ */ jsxRuntime.jsx(icons.Link, { className: "text-ui-fg-subtle" }),
241
- /* @__PURE__ */ jsxRuntime.jsx(
242
- "a",
243
- {
244
- href: "/app/brands",
245
- className: "text-ui-fg-interactive hover:text-ui-fg-interactive-hover transition-colors",
246
- children: "Manage all brands"
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 BrandForm = ({ brand, isCreating, onClose }) => {
256
- const [formData, setFormData] = react.useState({
257
- name: "",
258
- slug: "",
259
- description: "",
260
- website: "",
261
- is_active: true,
262
- metadata: {}
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 [loading, setLoading] = react.useState(false);
265
- const [error, setError] = react.useState(null);
266
- const [slugError, setSlugError] = react.useState(null);
267
- react.useEffect(() => {
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
- setFormData({
270
- name: brand.name || "",
271
- slug: brand.slug || "",
272
- description: brand.description || "",
273
- website: brand.website || "",
274
- is_active: brand.is_active !== false,
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
- const generateSlug = (name) => {
280
- return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
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 (!formData.slug.trim()) {
300
- setError("Brand slug is required");
301
- return false;
617
+ if (!nameValue) {
618
+ setValue("slug", "");
619
+ return;
302
620
  }
303
- if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(formData.slug)) {
304
- setSlugError(
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
- if (formData.website && !formData.website.match(/^https?:\/\/.+/)) {
310
- setError(
311
- "Website must be a valid URL (starting with http:// or https://)"
312
- );
313
- return false;
624
+ }, [isCreating, nameValue, setValue, slugManuallyEdited]);
625
+ React.useEffect(() => {
626
+ if (!slugValue) {
627
+ setSlugFeedback(null);
628
+ clearErrors("slug");
629
+ return;
314
630
  }
315
- return true;
316
- };
317
- const handleSubmit = async (e) => {
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 response = await sdk.client.fetch(url, {
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
- headers: {
329
- "Content-Type": "application/json"
330
- },
331
- body: JSON.stringify(formData)
732
+ body: payload
332
733
  });
333
- const data = await response.json();
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
- setError("An error occurred while saving the brand");
344
- console.error("Error saving brand:", err);
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 max-h-[90vh] overflow-y-auto", children: [
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 New Brand" : "Edit Brand" }),
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.X, {}) })
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 Name *" }),
762
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "name", className: "mb-2", children: "Brand name *" }),
364
763
  /* @__PURE__ */ jsxRuntime.jsx(
365
- ui.Input,
764
+ reactHookForm.Controller,
366
765
  {
367
- id: "name",
368
- placeholder: "e.g., Nike",
369
- value: formData.name,
370
- onChange: (e) => handleNameChange(e.target.value),
371
- required: true
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.jsx(ui.Label, { htmlFor: "slug", className: "mb-2", children: "Slug *" }),
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
- ui.Input,
799
+ reactHookForm.Controller,
379
800
  {
380
- id: "slug",
381
- placeholder: "e.g., nike",
382
- value: formData.slug,
383
- onChange: (e) => handleSlugChange(e.target.value),
384
- required: true
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
- slugError && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "mt-1 text-sm text-ui-fg-error", children: slugError }),
388
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "mt-1 text-xs text-ui-fg-subtle", children: "Used in URLs and must be unique" })
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
- ui.Textarea,
832
+ reactHookForm.Controller,
394
833
  {
395
- id: "description",
396
- placeholder: "Enter brand description...",
397
- value: formData.description,
398
- onChange: (e) => setFormData((prev) => ({
399
- ...prev,
400
- description: e.target.value
401
- })),
402
- rows: 4
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
- ui.Input,
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
- id: "website",
412
- type: "url",
413
- placeholder: "https://example.com",
414
- value: formData.website,
415
- onChange: (e) => setFormData((prev) => ({ ...prev, website: e.target.value }))
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-xs text-ui-fg-subtle", children: "Include the full URL with http:// or https://" })
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 Status" }),
423
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-sm text-ui-fg-subtle", children: "Active brands are visible in your store" })
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
- ui.Switch,
902
+ reactHookForm.Controller,
427
903
  {
428
- id: "is_active",
429
- checked: formData.is_active,
430
- onCheckedChange: (checked) => setFormData((prev) => ({ ...prev, is_active: checked }))
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 border-t", children: [
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..." : isCreating ? "Create Brand" : "Update Brand" })
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] = react.useState(currentImage || null);
448
- const [isUploading, setIsUploading] = react.useState(false);
449
- const [isDragging, setIsDragging] = react.useState(false);
450
- const [error, setError] = react.useState(null);
451
- const [previewUrl, setPreviewUrl] = react.useState(null);
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 ? 2 : 5;
454
- const allowedTypes = isLogo ? ["image/jpeg", "image/png", "image/gif", "image/webp", "image/svg+xml"] : ["image/jpeg", "image/png", "image/gif", "image/webp"];
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 ? "JPEG, PNG, GIF, WebP, or SVG" : "JPEG, PNG, GIF, or WebP";
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 valid image file (${formatFileTypes()})`);
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 handleFileUpload = async (file) => {
470
- if (!validateFile(file)) return;
471
- setError(null);
952
+ const readFileAsDataUrl = (file) => new Promise((resolve, reject) => {
472
953
  const reader = new FileReader();
473
- reader.onloadend = () => {
474
- setPreviewUrl(reader.result);
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
- if (response.ok) {
489
- const result = await response.json();
490
- const newImageUrl = result.brand[imageType];
491
- setDisplayImage(newImageUrl);
492
- setPreviewUrl(null);
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
- setError(`Error uploading ${imageType}`);
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
- const response = await sdk.client.fetch(
515
- `/admin/brands/${brand.id}/${imageType}`,
516
- {
517
- method: "DELETE"
518
- }
519
- );
520
- if (response.ok) {
521
- setDisplayImage(null);
522
- setPreviewUrl(null);
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
- setError(`Error deleting ${imageType}`);
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 = react.useCallback((e) => {
1030
+ const handleDragEnter = React.useCallback((e) => {
538
1031
  e.preventDefault();
539
1032
  e.stopPropagation();
540
1033
  setIsDragging(true);
541
1034
  }, []);
542
- const handleDragLeave = react.useCallback((e) => {
1035
+ const handleDragLeave = React.useCallback((e) => {
543
1036
  e.preventDefault();
544
1037
  e.stopPropagation();
545
1038
  setIsDragging(false);
546
1039
  }, []);
547
- const handleDragOver = react.useCallback((e) => {
1040
+ const handleDragOver = React.useCallback((e) => {
548
1041
  e.preventDefault();
549
1042
  e.stopPropagation();
550
1043
  }, []);
551
- const handleDrop = react.useCallback((e) => {
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.X, {}) })
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] = react.useState([]);
675
- const [loading, setLoading] = react.useState(true);
676
- const [error, setError] = react.useState(null);
677
- const [selectedBrand, setSelectedBrand] = react.useState(null);
678
- const [showForm, setShowForm] = react.useState(false);
679
- const [showImageUploader, setShowImageUploader] = react.useState(false);
680
- const [imageType, setImageType] = react.useState("image");
681
- const [searchQuery, setSearchQuery] = react.useState("");
682
- const [isCreating, setIsCreating] = react.useState(false);
683
- react.useEffect(() => {
684
- fetchBrands();
685
- }, []);
686
- const fetchBrands = async () => {
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 queryParams = new URLSearchParams({ limit: "100" });
691
- if (searchQuery) {
692
- queryParams.append("q", searchQuery);
693
- }
694
- const response = await sdk.client.fetch(
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
- if (response.ok) {
699
- const data = await response.json();
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
- if (err instanceof Medusa.FetchError && err.status === 404) {
706
- setBrands([]);
707
- setError(null);
708
- } else {
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
- if (!confirm(
728
- "Are you sure you want to delete this brand? This action cannot be undone."
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
- const response = await sdk.client.fetch(`/admin/brands/${brandId}`, {
1294
+ await sdk.client.fetch(`/admin/brands/${brandId}`, {
734
1295
  method: "DELETE"
735
1296
  });
736
- if (response.ok) {
737
- fetchBrands();
738
- } else {
739
- alert("Failed to delete brand");
740
- }
1297
+ await fetchBrands();
741
1298
  } catch (err) {
742
- alert("Error deleting brand");
743
- console.error("Error deleting brand:", err);
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
- const response = await sdk.client.fetch(`/admin/brands/${brand.id}`, {
1307
+ await sdk.client.fetch(`/admin/brands/${brand.id}`, {
749
1308
  method: "PUT",
750
- headers: {
751
- "Content-Type": "application/json"
752
- },
753
- body: JSON.stringify({
1309
+ body: {
754
1310
  is_active: !brand.is_active
755
- })
1311
+ }
756
1312
  });
757
- if (response.ok) {
758
- fetchBrands();
759
- } else {
760
- alert("Failed to update brand status");
761
- }
1313
+ await fetchBrands();
762
1314
  } catch (err) {
763
- alert("Error updating brand status");
764
- console.error("Error updating brand status:", err);
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 filteredBrands = brands.filter((brand) => {
784
- var _a;
785
- if (!searchQuery) return true;
786
- const query = searchQuery.toLowerCase();
787
- return brand.name.toLowerCase().includes(query) || brand.slug.toLowerCase().includes(query) || ((_a = brand.description) == null ? void 0 : _a.toLowerCase().includes(query));
788
- });
789
- if (loading) {
790
- 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..." }) }) });
791
- }
792
- if (error) {
793
- return /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "error", dismissible: true, children: error }) });
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 mb-4", children: [
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", className: "mb-2", children: "Brands" }),
800
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle", children: "Manage your product brands and their images" })
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("div", { className: "flex items-center gap-4", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative flex-1 max-w-md", children: [
808
- /* @__PURE__ */ jsxRuntime.jsx(
809
- ui.Input,
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
- placeholder: "Search brands...",
812
- value: searchQuery,
813
- onChange: (e) => setSearchQuery(e.target.value),
814
- className: "pl-10"
815
- }
816
- ),
817
- /* @__PURE__ */ jsxRuntime.jsx(icons.MagnifyingGlass, { className: "absolute left-3 top-1/2 -translate-y-1/2 text-ui-fg-muted" })
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
- filteredBrands.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex h-64 flex-col items-center justify-center rounded-lg border-2 border-dashed border-ui-border-base", children: [
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: searchQuery ? "No brands found" : "No brands yet" }),
823
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "mb-4 text-ui-fg-subtle", children: searchQuery ? "Try adjusting your search query" : "Create your first brand to get started" }),
824
- !searchQuery && /* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { onClick: handleCreateBrand, children: [
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.jsx("div", { className: "overflow-hidden rounded-lg border", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table, { children: [
829
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
830
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Logo" }),
831
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Name" }),
832
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Slug" }),
833
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Status" }),
834
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Images" }),
835
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { className: "text-right", children: "Actions" })
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.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { color: "blue", children: brand.slug }) }),
851
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { color: brand.is_active ? "green" : "grey", children: brand.is_active ? "Active" : "Inactive" }) }),
852
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2", children: [
853
- brand.image && /* @__PURE__ */ jsxRuntime.jsxs(ui.Badge, { color: "green", size: "xsmall", children: [
854
- /* @__PURE__ */ jsxRuntime.jsx(icons.PhotoSolid, { className: "mr-1" }),
855
- "Image"
856
- ] }),
857
- brand.logo && /* @__PURE__ */ jsxRuntime.jsxs(ui.Badge, { color: "green", size: "xsmall", children: [
858
- /* @__PURE__ */ jsxRuntime.jsx(icons.BuildingStorefront, { className: "mr-1" }),
859
- "Logo"
860
- ] }),
861
- !brand.image && !brand.logo && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-sm text-ui-fg-muted", children: "No images" })
862
- ] }) }),
863
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-end gap-2", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.DropdownMenu, { children: [
864
- /* @__PURE__ */ jsxRuntime.jsx(ui.DropdownMenu.Trigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx(ui.IconButton, { variant: "transparent", size: "small", children: /* @__PURE__ */ jsxRuntime.jsx(icons.EllipsisHorizontal, {}) }) }),
865
- /* @__PURE__ */ jsxRuntime.jsxs(ui.DropdownMenu.Content, { children: [
866
- /* @__PURE__ */ jsxRuntime.jsxs(
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
- onClick: () => handleToggleActive(brand),
902
- children: [
903
- /* @__PURE__ */ jsxRuntime.jsx(
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.DropdownMenu.Separator, {}),
915
- /* @__PURE__ */ jsxRuntime.jsxs(
916
- ui.DropdownMenu.Item,
917
- {
918
- onClick: () => handleDeleteBrand(brand.id),
919
- className: "text-ui-fg-error",
920
- children: [
921
- /* @__PURE__ */ jsxRuntime.jsx(icons.Trash, { className: "mr-2" }),
922
- "Delete Brand"
923
- ]
924
- }
925
- )
926
- ] })
927
- ] }) }) })
928
- ] }, brand.id)) })
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
  {