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