@86d-app/products 0.0.3

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 (94) hide show
  1. package/AGENTS.md +65 -0
  2. package/COMPONENTS.md +231 -0
  3. package/README.md +201 -0
  4. package/package.json +46 -0
  5. package/src/__tests__/controllers.test.ts +2227 -0
  6. package/src/__tests__/state.test.ts +138 -0
  7. package/src/admin/components/categories-admin.mdx +3 -0
  8. package/src/admin/components/categories-admin.tsx +449 -0
  9. package/src/admin/components/category-form.mdx +9 -0
  10. package/src/admin/components/category-form.tsx +490 -0
  11. package/src/admin/components/category-list.mdx +75 -0
  12. package/src/admin/components/category-list.tsx +168 -0
  13. package/src/admin/components/collections-admin.mdx +3 -0
  14. package/src/admin/components/collections-admin.tsx +771 -0
  15. package/src/admin/components/index.tsx +8 -0
  16. package/src/admin/components/product-detail.mdx +12 -0
  17. package/src/admin/components/product-detail.tsx +790 -0
  18. package/src/admin/components/product-edit.tsx +60 -0
  19. package/src/admin/components/product-form.tsx +793 -0
  20. package/src/admin/components/product-list.mdx +3 -0
  21. package/src/admin/components/product-list.tsx +1125 -0
  22. package/src/admin/components/product-new.tsx +38 -0
  23. package/src/admin/endpoints/add-collection-product.ts +17 -0
  24. package/src/admin/endpoints/bulk-action.ts +43 -0
  25. package/src/admin/endpoints/create-category.ts +52 -0
  26. package/src/admin/endpoints/create-collection.ts +35 -0
  27. package/src/admin/endpoints/create-product.ts +50 -0
  28. package/src/admin/endpoints/create-variant.ts +45 -0
  29. package/src/admin/endpoints/delete-category.ts +27 -0
  30. package/src/admin/endpoints/delete-collection.ts +12 -0
  31. package/src/admin/endpoints/delete-product.ts +27 -0
  32. package/src/admin/endpoints/delete-variant.ts +27 -0
  33. package/src/admin/endpoints/get-product.ts +23 -0
  34. package/src/admin/endpoints/import-products.ts +47 -0
  35. package/src/admin/endpoints/index.ts +43 -0
  36. package/src/admin/endpoints/list-categories.ts +21 -0
  37. package/src/admin/endpoints/list-collections.ts +20 -0
  38. package/src/admin/endpoints/list-products.ts +25 -0
  39. package/src/admin/endpoints/remove-collection-product.ts +15 -0
  40. package/src/admin/endpoints/update-category.ts +82 -0
  41. package/src/admin/endpoints/update-collection.ts +22 -0
  42. package/src/admin/endpoints/update-product.ts +67 -0
  43. package/src/admin/endpoints/update-variant.ts +41 -0
  44. package/src/controllers.ts +1410 -0
  45. package/src/index.ts +120 -0
  46. package/src/markdown.ts +150 -0
  47. package/src/mdx.d.ts +5 -0
  48. package/src/schema.ts +352 -0
  49. package/src/state.ts +84 -0
  50. package/src/store/components/_hooks.ts +78 -0
  51. package/src/store/components/_types.ts +73 -0
  52. package/src/store/components/_utils.ts +14 -0
  53. package/src/store/components/back-in-stock-notify.tsx +97 -0
  54. package/src/store/components/collection-card.mdx +42 -0
  55. package/src/store/components/collection-card.tsx +12 -0
  56. package/src/store/components/collection-detail.mdx +12 -0
  57. package/src/store/components/collection-detail.tsx +149 -0
  58. package/src/store/components/collection-grid.mdx +9 -0
  59. package/src/store/components/collection-grid.tsx +80 -0
  60. package/src/store/components/featured-products.mdx +9 -0
  61. package/src/store/components/featured-products.tsx +75 -0
  62. package/src/store/components/filter-chip.mdx +25 -0
  63. package/src/store/components/filter-chip.tsx +12 -0
  64. package/src/store/components/index.tsx +39 -0
  65. package/src/store/components/product-card.mdx +69 -0
  66. package/src/store/components/product-card.tsx +71 -0
  67. package/src/store/components/product-detail.mdx +30 -0
  68. package/src/store/components/product-detail.tsx +488 -0
  69. package/src/store/components/product-listing.mdx +7 -0
  70. package/src/store/components/product-listing.tsx +423 -0
  71. package/src/store/components/product-reviews-section.mdx +21 -0
  72. package/src/store/components/product-reviews-section.tsx +372 -0
  73. package/src/store/components/recently-viewed.tsx +100 -0
  74. package/src/store/components/related-products.mdx +6 -0
  75. package/src/store/components/related-products.tsx +62 -0
  76. package/src/store/components/star-display.mdx +18 -0
  77. package/src/store/components/star-display.tsx +27 -0
  78. package/src/store/components/star-picker.mdx +21 -0
  79. package/src/store/components/star-picker.tsx +21 -0
  80. package/src/store/components/stock-badge.mdx +12 -0
  81. package/src/store/components/stock-badge.tsx +19 -0
  82. package/src/store/endpoints/get-category.ts +61 -0
  83. package/src/store/endpoints/get-collection.ts +46 -0
  84. package/src/store/endpoints/get-featured.ts +18 -0
  85. package/src/store/endpoints/get-product.ts +52 -0
  86. package/src/store/endpoints/get-related.ts +20 -0
  87. package/src/store/endpoints/index.ts +23 -0
  88. package/src/store/endpoints/list-categories.ts +13 -0
  89. package/src/store/endpoints/list-collections.ts +22 -0
  90. package/src/store/endpoints/list-products.ts +28 -0
  91. package/src/store/endpoints/search-products.ts +18 -0
  92. package/src/store/endpoints/store-search.ts +111 -0
  93. package/tsconfig.json +9 -0
  94. package/vitest.config.ts +7 -0
package/src/index.ts ADDED
@@ -0,0 +1,120 @@
1
+ import type { Module, ModuleConfig } from "@86d-app/core";
2
+ import { adminEndpoints } from "./admin/endpoints";
3
+ import { controllers } from "./controllers";
4
+ import {
5
+ toMarkdownCollectionDetail,
6
+ toMarkdownCollectionListing,
7
+ toMarkdownProductDetail,
8
+ toMarkdownProductListing,
9
+ } from "./markdown";
10
+ import { productsSchema } from "./schema";
11
+ import { storeEndpoints } from "./store/endpoints";
12
+
13
+ export interface ProductsOptions extends ModuleConfig {
14
+ /**
15
+ * Default number of products per page
16
+ * @default 20
17
+ */
18
+ defaultPageSize?: number;
19
+ /**
20
+ * Maximum number of products per page
21
+ * @default 100
22
+ */
23
+ maxPageSize?: number;
24
+ /**
25
+ * Enable inventory tracking by default
26
+ * @default true
27
+ */
28
+ trackInventory?: boolean;
29
+ }
30
+
31
+ export default function products(options?: ProductsOptions): Module {
32
+ return {
33
+ id: "products",
34
+ version: "0.0.1",
35
+ schema: productsSchema,
36
+ exports: {
37
+ read: [
38
+ "productTitle",
39
+ "productPrice",
40
+ "productSlug",
41
+ "productStatus",
42
+ "categoryName",
43
+ "categorySlug",
44
+ "collectionName",
45
+ "collectionSlug",
46
+ ],
47
+ },
48
+ events: {
49
+ emits: ["product.created", "product.updated", "product.deleted"],
50
+ },
51
+ controllers,
52
+ options,
53
+ endpoints: {
54
+ store: storeEndpoints,
55
+ admin: adminEndpoints,
56
+ },
57
+ search: { store: "/products/store-search" },
58
+ admin: {
59
+ pages: [
60
+ {
61
+ path: "/admin/products",
62
+ component: "ProductList",
63
+ label: "Products",
64
+ icon: "Package",
65
+ group: "Catalog",
66
+ },
67
+ {
68
+ path: "/admin/products/new",
69
+ component: "ProductNew",
70
+ },
71
+ {
72
+ path: "/admin/products/:id/edit",
73
+ component: "ProductEdit",
74
+ },
75
+ {
76
+ path: "/admin/products/:id",
77
+ component: "ProductDetail",
78
+ },
79
+ {
80
+ path: "/admin/categories",
81
+ component: "CategoriesAdmin",
82
+ label: "Categories",
83
+ icon: "SquaresFour",
84
+ group: "Catalog",
85
+ },
86
+ {
87
+ path: "/admin/collections",
88
+ component: "CollectionsAdmin",
89
+ label: "Collections",
90
+ icon: "Stack",
91
+ group: "Catalog",
92
+ },
93
+ ],
94
+ },
95
+ store: {
96
+ pages: [
97
+ {
98
+ path: "/products",
99
+ component: "ProductListing",
100
+ toMarkdown: toMarkdownProductListing,
101
+ },
102
+ {
103
+ path: "/products/:slug",
104
+ component: "ProductDetail",
105
+ toMarkdown: toMarkdownProductDetail,
106
+ },
107
+ {
108
+ path: "/collections",
109
+ component: "CollectionGrid",
110
+ toMarkdown: toMarkdownCollectionListing,
111
+ },
112
+ {
113
+ path: "/collections/:slug",
114
+ component: "CollectionDetail",
115
+ toMarkdown: toMarkdownCollectionDetail,
116
+ },
117
+ ],
118
+ },
119
+ };
120
+ }
@@ -0,0 +1,150 @@
1
+ import type { ModuleContext } from "@86d-app/core";
2
+ import type {
3
+ Collection,
4
+ CollectionWithProducts,
5
+ Product,
6
+ ProductWithVariants,
7
+ } from "./controllers";
8
+
9
+ function formatPrice(cents: number): string {
10
+ return new Intl.NumberFormat("en-US", {
11
+ style: "currency",
12
+ currency: "USD",
13
+ }).format(cents / 100);
14
+ }
15
+
16
+ /** Build minimal endpoint-like ctx for controller calls. */
17
+ function withQuery(ctx: ModuleContext, query: Record<string, string>) {
18
+ return { context: ctx, query, params: {} };
19
+ }
20
+
21
+ function withParams(ctx: ModuleContext, params: Record<string, string>) {
22
+ return { context: ctx, query: {}, params };
23
+ }
24
+
25
+ export async function toMarkdownProductListing(
26
+ ctx: ModuleContext,
27
+ _params: Record<string, string>,
28
+ ): Promise<string | null> {
29
+ const result = await (
30
+ ctx.controllers as {
31
+ product: {
32
+ list: (ctx: unknown) => Promise<{ products?: ProductWithVariants[] }>;
33
+ };
34
+ }
35
+ ).product.list(withQuery(ctx, { limit: "100", status: "active" }));
36
+ const products = (result?.products ?? []) as ProductWithVariants[];
37
+ let md = `# Products\n\n`;
38
+ if (products.length === 0) {
39
+ md += "No products yet.\n";
40
+ return md;
41
+ }
42
+ for (const p of products) {
43
+ md += `- [${p.name}](/products/${p.slug}) — ${formatPrice(p.price)}\n`;
44
+ }
45
+ return md;
46
+ }
47
+
48
+ export async function toMarkdownProductDetail(
49
+ ctx: ModuleContext,
50
+ params: Record<string, string>,
51
+ ): Promise<string | null> {
52
+ const slug = params.slug;
53
+ if (!slug) return null;
54
+
55
+ const bySlug = (await (
56
+ ctx.controllers as {
57
+ product: { getBySlug: (ctx: unknown) => Promise<Product | null> };
58
+ }
59
+ ).product.getBySlug(withQuery(ctx, { slug }))) as Product | null;
60
+ if (!bySlug || bySlug.status !== "active") return null;
61
+
62
+ const product = (await (
63
+ ctx.controllers as {
64
+ product: {
65
+ getWithVariants: (ctx: unknown) => Promise<ProductWithVariants | null>;
66
+ };
67
+ }
68
+ ).product.getWithVariants(
69
+ withParams(ctx, { id: bySlug.id }),
70
+ )) as ProductWithVariants | null;
71
+ if (!product) return null;
72
+
73
+ let md = `# ${product.name}\n\n`;
74
+ if (product.shortDescription) {
75
+ md += `${product.shortDescription}\n\n`;
76
+ }
77
+ if (product.description) {
78
+ md += `${product.description}\n\n`;
79
+ }
80
+ md += `## Price\n\n${formatPrice(product.price)}\n\n`;
81
+ if (product.images?.length > 0) {
82
+ md += `## Images\n\n`;
83
+ for (const img of product.images) {
84
+ md += `![${product.name}](${img})\n`;
85
+ }
86
+ }
87
+ md += `\n[View product](/products/${product.slug})\n`;
88
+ return md;
89
+ }
90
+
91
+ export async function toMarkdownCollectionListing(
92
+ ctx: ModuleContext,
93
+ _params: Record<string, string>,
94
+ ): Promise<string | null> {
95
+ const result = await (
96
+ ctx.controllers as {
97
+ collection: {
98
+ list: (ctx: unknown) => Promise<{ collections?: Collection[] }>;
99
+ };
100
+ }
101
+ ).collection.list(withQuery(ctx, { limit: "100", visible: "true" }));
102
+ const collections = (result?.collections ?? []) as Collection[];
103
+ let md = `# Collections\n\n`;
104
+ if (collections.length === 0) {
105
+ md += "No collections yet.\n";
106
+ return md;
107
+ }
108
+ for (const c of collections) {
109
+ md += `- [${c.name}](/collections/${c.slug})\n`;
110
+ }
111
+ return md;
112
+ }
113
+
114
+ export async function toMarkdownCollectionDetail(
115
+ ctx: ModuleContext,
116
+ params: Record<string, string>,
117
+ ): Promise<string | null> {
118
+ const slug = params.slug;
119
+ if (!slug) return null;
120
+
121
+ const bySlug = (await (
122
+ ctx.controllers as {
123
+ collection: { getBySlug: (ctx: unknown) => Promise<Collection | null> };
124
+ }
125
+ ).collection.getBySlug(withQuery(ctx, { slug }))) as Collection | null;
126
+ if (!bySlug || !bySlug.isVisible) return null;
127
+
128
+ const collection = (await (
129
+ ctx.controllers as {
130
+ collection: {
131
+ getWithProducts: (
132
+ ctx: unknown,
133
+ ) => Promise<CollectionWithProducts | null>;
134
+ };
135
+ }
136
+ ).collection.getWithProducts(
137
+ withParams(ctx, { id: bySlug.id }),
138
+ )) as CollectionWithProducts | null;
139
+ if (!collection) return null;
140
+
141
+ let md = `# ${collection.name}\n\n`;
142
+ if (collection.description) {
143
+ md += `${collection.description}\n\n`;
144
+ }
145
+ if (collection.image) {
146
+ md += `![${collection.name}](${collection.image})\n\n`;
147
+ }
148
+ md += `[View collection](/collections/${collection.slug})\n`;
149
+ return md;
150
+ }
package/src/mdx.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ declare module "*.mdx" {
2
+ import type { ComponentType } from "react";
3
+ const Component: ComponentType<Record<string, unknown>>;
4
+ export default Component;
5
+ }
package/src/schema.ts ADDED
@@ -0,0 +1,352 @@
1
+ import type { ModuleSchema } from "@86d-app/core";
2
+
3
+ export const productsSchema = {
4
+ product: {
5
+ fields: {
6
+ id: {
7
+ type: "string",
8
+ required: true,
9
+ },
10
+ name: {
11
+ type: "string",
12
+ required: true,
13
+ },
14
+ slug: {
15
+ type: "string",
16
+ required: true,
17
+ unique: true,
18
+ },
19
+ description: {
20
+ type: "string",
21
+ required: false,
22
+ },
23
+ shortDescription: {
24
+ type: "string",
25
+ required: false,
26
+ },
27
+ price: {
28
+ type: "number",
29
+ required: true,
30
+ },
31
+ compareAtPrice: {
32
+ type: "number",
33
+ required: false,
34
+ },
35
+ costPrice: {
36
+ type: "number",
37
+ required: false,
38
+ },
39
+ sku: {
40
+ type: "string",
41
+ required: false,
42
+ unique: true,
43
+ },
44
+ barcode: {
45
+ type: "string",
46
+ required: false,
47
+ },
48
+ inventory: {
49
+ type: "number",
50
+ required: true,
51
+ defaultValue: 0,
52
+ },
53
+ trackInventory: {
54
+ type: "boolean",
55
+ required: true,
56
+ defaultValue: true,
57
+ },
58
+ allowBackorder: {
59
+ type: "boolean",
60
+ required: true,
61
+ defaultValue: false,
62
+ },
63
+ status: {
64
+ type: ["draft", "active", "archived"],
65
+ required: true,
66
+ defaultValue: "draft",
67
+ },
68
+ categoryId: {
69
+ type: "string",
70
+ required: false,
71
+ references: {
72
+ model: "category",
73
+ field: "id",
74
+ onDelete: "set null",
75
+ },
76
+ },
77
+ images: {
78
+ type: "json",
79
+ required: false,
80
+ defaultValue: [],
81
+ },
82
+ tags: {
83
+ type: "json",
84
+ required: false,
85
+ defaultValue: [],
86
+ },
87
+ metadata: {
88
+ type: "json",
89
+ required: false,
90
+ defaultValue: {},
91
+ },
92
+ weight: {
93
+ type: "number",
94
+ required: false,
95
+ },
96
+ weightUnit: {
97
+ type: ["kg", "lb", "oz", "g"],
98
+ required: false,
99
+ defaultValue: "kg",
100
+ },
101
+ isFeatured: {
102
+ type: "boolean",
103
+ required: true,
104
+ defaultValue: false,
105
+ },
106
+ createdAt: {
107
+ type: "date",
108
+ required: true,
109
+ defaultValue: () => new Date(),
110
+ },
111
+ updatedAt: {
112
+ type: "date",
113
+ required: true,
114
+ defaultValue: () => new Date(),
115
+ onUpdate: () => new Date(),
116
+ },
117
+ },
118
+ },
119
+ productVariant: {
120
+ fields: {
121
+ id: {
122
+ type: "string",
123
+ required: true,
124
+ },
125
+ productId: {
126
+ type: "string",
127
+ required: true,
128
+ references: {
129
+ model: "product",
130
+ field: "id",
131
+ onDelete: "cascade",
132
+ },
133
+ },
134
+ name: {
135
+ type: "string",
136
+ required: true,
137
+ },
138
+ sku: {
139
+ type: "string",
140
+ required: false,
141
+ unique: true,
142
+ },
143
+ barcode: {
144
+ type: "string",
145
+ required: false,
146
+ },
147
+ price: {
148
+ type: "number",
149
+ required: true,
150
+ },
151
+ compareAtPrice: {
152
+ type: "number",
153
+ required: false,
154
+ },
155
+ costPrice: {
156
+ type: "number",
157
+ required: false,
158
+ },
159
+ inventory: {
160
+ type: "number",
161
+ required: true,
162
+ defaultValue: 0,
163
+ },
164
+ options: {
165
+ type: "json",
166
+ required: true,
167
+ defaultValue: {},
168
+ },
169
+ images: {
170
+ type: "json",
171
+ required: false,
172
+ defaultValue: [],
173
+ },
174
+ weight: {
175
+ type: "number",
176
+ required: false,
177
+ },
178
+ weightUnit: {
179
+ type: ["kg", "lb", "oz", "g"],
180
+ required: false,
181
+ },
182
+ position: {
183
+ type: "number",
184
+ required: true,
185
+ defaultValue: 0,
186
+ },
187
+ createdAt: {
188
+ type: "date",
189
+ required: true,
190
+ defaultValue: () => new Date(),
191
+ },
192
+ updatedAt: {
193
+ type: "date",
194
+ required: true,
195
+ defaultValue: () => new Date(),
196
+ onUpdate: () => new Date(),
197
+ },
198
+ },
199
+ },
200
+ category: {
201
+ fields: {
202
+ id: {
203
+ type: "string",
204
+ required: true,
205
+ },
206
+ name: {
207
+ type: "string",
208
+ required: true,
209
+ },
210
+ slug: {
211
+ type: "string",
212
+ required: true,
213
+ unique: true,
214
+ },
215
+ description: {
216
+ type: "string",
217
+ required: false,
218
+ },
219
+ parentId: {
220
+ type: "string",
221
+ required: false,
222
+ references: {
223
+ model: "category",
224
+ field: "id",
225
+ onDelete: "set null",
226
+ },
227
+ },
228
+ image: {
229
+ type: "string",
230
+ required: false,
231
+ },
232
+ position: {
233
+ type: "number",
234
+ required: true,
235
+ defaultValue: 0,
236
+ },
237
+ isVisible: {
238
+ type: "boolean",
239
+ required: true,
240
+ defaultValue: true,
241
+ },
242
+ metadata: {
243
+ type: "json",
244
+ required: false,
245
+ defaultValue: {},
246
+ },
247
+ createdAt: {
248
+ type: "date",
249
+ required: true,
250
+ defaultValue: () => new Date(),
251
+ },
252
+ updatedAt: {
253
+ type: "date",
254
+ required: true,
255
+ defaultValue: () => new Date(),
256
+ onUpdate: () => new Date(),
257
+ },
258
+ },
259
+ },
260
+ collection: {
261
+ fields: {
262
+ id: {
263
+ type: "string",
264
+ required: true,
265
+ },
266
+ name: {
267
+ type: "string",
268
+ required: true,
269
+ },
270
+ slug: {
271
+ type: "string",
272
+ required: true,
273
+ unique: true,
274
+ },
275
+ description: {
276
+ type: "string",
277
+ required: false,
278
+ },
279
+ image: {
280
+ type: "string",
281
+ required: false,
282
+ },
283
+ isFeatured: {
284
+ type: "boolean",
285
+ required: true,
286
+ defaultValue: false,
287
+ },
288
+ isVisible: {
289
+ type: "boolean",
290
+ required: true,
291
+ defaultValue: true,
292
+ },
293
+ position: {
294
+ type: "number",
295
+ required: true,
296
+ defaultValue: 0,
297
+ },
298
+ metadata: {
299
+ type: "json",
300
+ required: false,
301
+ defaultValue: {},
302
+ },
303
+ createdAt: {
304
+ type: "date",
305
+ required: true,
306
+ defaultValue: () => new Date(),
307
+ },
308
+ updatedAt: {
309
+ type: "date",
310
+ required: true,
311
+ defaultValue: () => new Date(),
312
+ onUpdate: () => new Date(),
313
+ },
314
+ },
315
+ },
316
+ collectionProduct: {
317
+ fields: {
318
+ id: {
319
+ type: "string",
320
+ required: true,
321
+ },
322
+ collectionId: {
323
+ type: "string",
324
+ required: true,
325
+ references: {
326
+ model: "collection",
327
+ field: "id",
328
+ onDelete: "cascade",
329
+ },
330
+ },
331
+ productId: {
332
+ type: "string",
333
+ required: true,
334
+ references: {
335
+ model: "product",
336
+ field: "id",
337
+ onDelete: "cascade",
338
+ },
339
+ },
340
+ position: {
341
+ type: "number",
342
+ required: true,
343
+ defaultValue: 0,
344
+ },
345
+ createdAt: {
346
+ type: "date",
347
+ required: true,
348
+ defaultValue: () => new Date(),
349
+ },
350
+ },
351
+ },
352
+ } satisfies ModuleSchema;
package/src/state.ts ADDED
@@ -0,0 +1,84 @@
1
+ import { makeAutoObservable } from "@86d-app/core/state";
2
+
3
+ export type SortField = "name" | "price" | "createdAt";
4
+ export type SortOrder = "asc" | "desc";
5
+ export type ViewMode = "grid" | "list";
6
+
7
+ /**
8
+ * Products UI state — shared across components via MobX.
9
+ * Persists active filters, sort order, and view mode across navigations.
10
+ */
11
+ export const productsState = makeAutoObservable({
12
+ /** Active category filter (empty = all categories) */
13
+ activeCategory: "",
14
+ /** Search query */
15
+ searchQuery: "",
16
+ /** Sort field */
17
+ sortField: "createdAt" as SortField,
18
+ /** Sort direction */
19
+ sortOrder: "desc" as SortOrder,
20
+ /** Grid vs list view */
21
+ viewMode: "grid" as ViewMode,
22
+ /** Price range filter (in display dollars, not cents) */
23
+ minPrice: "",
24
+ maxPrice: "",
25
+ /** Stock filter */
26
+ inStockOnly: false,
27
+ /** Tag filter */
28
+ activeTag: "",
29
+
30
+ setCategory(category: string) {
31
+ this.activeCategory = category;
32
+ },
33
+
34
+ setSearchQuery(query: string) {
35
+ this.searchQuery = query;
36
+ },
37
+
38
+ setSortField(field: SortField) {
39
+ this.sortField = field;
40
+ },
41
+
42
+ setSortOrder(order: SortOrder) {
43
+ this.sortOrder = order;
44
+ },
45
+
46
+ setViewMode(mode: ViewMode) {
47
+ this.viewMode = mode;
48
+ },
49
+
50
+ setPriceRange(min: string, max: string) {
51
+ this.minPrice = min;
52
+ this.maxPrice = max;
53
+ },
54
+
55
+ setInStockOnly(v: boolean) {
56
+ this.inStockOnly = v;
57
+ },
58
+
59
+ setActiveTag(tag: string) {
60
+ this.activeTag = tag;
61
+ },
62
+
63
+ clearFilters() {
64
+ this.activeCategory = "";
65
+ this.searchQuery = "";
66
+ this.minPrice = "";
67
+ this.maxPrice = "";
68
+ this.inStockOnly = false;
69
+ this.activeTag = "";
70
+ },
71
+
72
+ get hasActiveFilters(): boolean {
73
+ return (
74
+ !!this.searchQuery ||
75
+ !!this.activeCategory ||
76
+ !!this.minPrice ||
77
+ !!this.maxPrice ||
78
+ this.inStockOnly ||
79
+ !!this.activeTag
80
+ );
81
+ },
82
+ });
83
+
84
+ export type ProductsState = typeof productsState;