@86d-app/products 0.0.4 → 0.0.13

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 (196) hide show
  1. package/.turbo/turbo-build.log +1 -0
  2. package/AGENTS.md +41 -41
  3. package/README.md +266 -5
  4. package/dist/__tests__/controllers.test.d.ts +2 -0
  5. package/dist/__tests__/controllers.test.d.ts.map +1 -0
  6. package/dist/__tests__/endpoint-security.test.d.ts +2 -0
  7. package/dist/__tests__/endpoint-security.test.d.ts.map +1 -0
  8. package/dist/__tests__/service-impl.test.d.ts +2 -0
  9. package/dist/__tests__/service-impl.test.d.ts.map +1 -0
  10. package/dist/__tests__/state.test.d.ts +2 -0
  11. package/dist/__tests__/state.test.d.ts.map +1 -0
  12. package/dist/admin/components/categories-admin.d.ts +2 -0
  13. package/dist/admin/components/categories-admin.d.ts.map +1 -0
  14. package/dist/admin/components/category-form.d.ts +7 -0
  15. package/dist/admin/components/category-form.d.ts.map +1 -0
  16. package/dist/admin/components/category-list.d.ts +7 -0
  17. package/dist/admin/components/category-list.d.ts.map +1 -0
  18. package/dist/admin/components/collections-admin.d.ts +2 -0
  19. package/dist/admin/components/collections-admin.d.ts.map +1 -0
  20. package/dist/admin/components/index.d.ts +9 -0
  21. package/dist/admin/components/index.d.ts.map +1 -0
  22. package/dist/admin/components/product-detail.d.ts +7 -0
  23. package/dist/admin/components/product-detail.d.ts.map +1 -0
  24. package/dist/admin/components/product-edit.d.ts +6 -0
  25. package/dist/admin/components/product-edit.d.ts.map +1 -0
  26. package/dist/admin/components/product-form.d.ts +7 -0
  27. package/dist/admin/components/product-form.d.ts.map +1 -0
  28. package/dist/admin/components/product-list.d.ts +2 -0
  29. package/dist/admin/components/product-list.d.ts.map +1 -0
  30. package/dist/admin/components/product-new.d.ts +2 -0
  31. package/dist/admin/components/product-new.d.ts.map +1 -0
  32. package/dist/admin/endpoints/add-collection-product.d.ts +15 -0
  33. package/dist/admin/endpoints/add-collection-product.d.ts.map +1 -0
  34. package/dist/admin/endpoints/bulk-action.d.ts +17 -0
  35. package/dist/admin/endpoints/bulk-action.d.ts.map +1 -0
  36. package/dist/admin/endpoints/create-category.d.ts +23 -0
  37. package/dist/admin/endpoints/create-category.d.ts.map +1 -0
  38. package/dist/admin/endpoints/create-collection.d.ts +22 -0
  39. package/dist/admin/endpoints/create-collection.d.ts.map +1 -0
  40. package/dist/admin/endpoints/create-product.d.ts +44 -0
  41. package/dist/admin/endpoints/create-product.d.ts.map +1 -0
  42. package/dist/admin/endpoints/create-variant.d.ts +35 -0
  43. package/dist/admin/endpoints/create-variant.d.ts.map +1 -0
  44. package/dist/admin/endpoints/delete-category.d.ts +18 -0
  45. package/dist/admin/endpoints/delete-category.d.ts.map +1 -0
  46. package/dist/admin/endpoints/delete-collection.d.ts +8 -0
  47. package/dist/admin/endpoints/delete-collection.d.ts.map +1 -0
  48. package/dist/admin/endpoints/delete-product.d.ts +18 -0
  49. package/dist/admin/endpoints/delete-product.d.ts.map +1 -0
  50. package/dist/admin/endpoints/delete-variant.d.ts +18 -0
  51. package/dist/admin/endpoints/delete-variant.d.ts.map +1 -0
  52. package/dist/admin/endpoints/get-product.d.ts +16 -0
  53. package/dist/admin/endpoints/get-product.d.ts.map +1 -0
  54. package/dist/admin/endpoints/import-products.d.ts +36 -0
  55. package/dist/admin/endpoints/import-products.d.ts.map +1 -0
  56. package/dist/admin/endpoints/index.d.ts +418 -0
  57. package/dist/admin/endpoints/index.d.ts.map +1 -0
  58. package/dist/admin/endpoints/list-categories.d.ts +11 -0
  59. package/dist/admin/endpoints/list-categories.d.ts.map +1 -0
  60. package/dist/admin/endpoints/list-collections.d.ts +11 -0
  61. package/dist/admin/endpoints/list-collections.d.ts.map +1 -0
  62. package/dist/admin/endpoints/list-products.d.ts +27 -0
  63. package/dist/admin/endpoints/list-products.d.ts.map +1 -0
  64. package/dist/admin/endpoints/remove-collection-product.d.ts +9 -0
  65. package/dist/admin/endpoints/remove-collection-product.d.ts.map +1 -0
  66. package/dist/admin/endpoints/update-category.d.ts +26 -0
  67. package/dist/admin/endpoints/update-category.d.ts.map +1 -0
  68. package/dist/admin/endpoints/update-collection.d.ts +19 -0
  69. package/dist/admin/endpoints/update-collection.d.ts.map +1 -0
  70. package/dist/admin/endpoints/update-product.d.ts +47 -0
  71. package/dist/admin/endpoints/update-product.d.ts.map +1 -0
  72. package/dist/admin/endpoints/update-variant.d.ts +35 -0
  73. package/dist/admin/endpoints/update-variant.d.ts.map +1 -0
  74. package/dist/controllers.d.ts +130 -0
  75. package/dist/controllers.d.ts.map +1 -0
  76. package/dist/index.d.ts +20 -0
  77. package/dist/index.d.ts.map +1 -0
  78. package/dist/markdown.d.ts +6 -0
  79. package/dist/markdown.d.ts.map +1 -0
  80. package/dist/schema.d.ts +351 -0
  81. package/dist/schema.d.ts.map +1 -0
  82. package/dist/service-impl.d.ts +4 -0
  83. package/dist/service-impl.d.ts.map +1 -0
  84. package/dist/service.d.ts +280 -0
  85. package/dist/service.d.ts.map +1 -0
  86. package/dist/state.d.ts +38 -0
  87. package/dist/state.d.ts.map +1 -0
  88. package/dist/store/components/_hooks.d.ts +88 -0
  89. package/dist/store/components/_hooks.d.ts.map +1 -0
  90. package/dist/store/components/_types.d.ts +70 -0
  91. package/dist/store/components/_types.d.ts.map +1 -0
  92. package/dist/store/components/_utils.d.ts +5 -0
  93. package/dist/store/components/_utils.d.ts.map +1 -0
  94. package/dist/store/components/back-in-stock-notify.d.ts +8 -0
  95. package/dist/store/components/back-in-stock-notify.d.ts.map +1 -0
  96. package/dist/store/components/collection-card.d.ts +6 -0
  97. package/dist/store/components/collection-card.d.ts.map +1 -0
  98. package/dist/store/components/collection-detail.d.ts +6 -0
  99. package/dist/store/components/collection-detail.d.ts.map +1 -0
  100. package/dist/store/components/collection-grid.d.ts +6 -0
  101. package/dist/store/components/collection-grid.d.ts.map +1 -0
  102. package/dist/store/components/featured-products.d.ts +6 -0
  103. package/dist/store/components/featured-products.d.ts.map +1 -0
  104. package/dist/store/components/filter-chip.d.ts +6 -0
  105. package/dist/store/components/filter-chip.d.ts.map +1 -0
  106. package/dist/store/components/index.d.ts +37 -0
  107. package/dist/store/components/index.d.ts.map +1 -0
  108. package/dist/store/components/product-card.d.ts +7 -0
  109. package/dist/store/components/product-card.d.ts.map +1 -0
  110. package/dist/store/components/product-detail.d.ts +6 -0
  111. package/dist/store/components/product-detail.d.ts.map +1 -0
  112. package/dist/store/components/product-listing.d.ts +7 -0
  113. package/dist/store/components/product-listing.d.ts.map +1 -0
  114. package/dist/store/components/product-qa-section.d.ts +5 -0
  115. package/dist/store/components/product-qa-section.d.ts.map +1 -0
  116. package/dist/store/components/product-reviews-section.d.ts +5 -0
  117. package/dist/store/components/product-reviews-section.d.ts.map +1 -0
  118. package/dist/store/components/recently-viewed.d.ts +12 -0
  119. package/dist/store/components/recently-viewed.d.ts.map +1 -0
  120. package/dist/store/components/recommended-products.d.ts +7 -0
  121. package/dist/store/components/recommended-products.d.ts.map +1 -0
  122. package/dist/store/components/related-products.d.ts +7 -0
  123. package/dist/store/components/related-products.d.ts.map +1 -0
  124. package/dist/store/components/star-display.d.ts +6 -0
  125. package/dist/store/components/star-display.d.ts.map +1 -0
  126. package/dist/store/components/star-picker.d.ts +6 -0
  127. package/dist/store/components/star-picker.d.ts.map +1 -0
  128. package/dist/store/components/stock-badge.d.ts +5 -0
  129. package/dist/store/components/stock-badge.d.ts.map +1 -0
  130. package/dist/store/endpoints/get-category.d.ts +22 -0
  131. package/dist/store/endpoints/get-category.d.ts.map +1 -0
  132. package/dist/store/endpoints/get-collection.d.ts +17 -0
  133. package/dist/store/endpoints/get-collection.d.ts.map +1 -0
  134. package/dist/store/endpoints/get-featured.d.ts +10 -0
  135. package/dist/store/endpoints/get-featured.d.ts.map +1 -0
  136. package/dist/store/endpoints/get-product.d.ts +17 -0
  137. package/dist/store/endpoints/get-product.d.ts.map +1 -0
  138. package/dist/store/endpoints/get-related.d.ts +11 -0
  139. package/dist/store/endpoints/get-related.d.ts.map +1 -0
  140. package/dist/store/endpoints/index.d.ts +129 -0
  141. package/dist/store/endpoints/index.d.ts.map +1 -0
  142. package/dist/store/endpoints/list-categories.d.ts +6 -0
  143. package/dist/store/endpoints/list-categories.d.ts.map +1 -0
  144. package/dist/store/endpoints/list-collections.d.ts +10 -0
  145. package/dist/store/endpoints/list-collections.d.ts.map +1 -0
  146. package/dist/store/endpoints/list-products.d.ts +26 -0
  147. package/dist/store/endpoints/list-products.d.ts.map +1 -0
  148. package/dist/store/endpoints/search-products.d.ts +11 -0
  149. package/dist/store/endpoints/search-products.d.ts.map +1 -0
  150. package/dist/store/endpoints/store-search.d.ts +18 -0
  151. package/dist/store/endpoints/store-search.d.ts.map +1 -0
  152. package/package.json +3 -3
  153. package/src/__tests__/endpoint-security.test.ts +457 -0
  154. package/src/__tests__/service-impl.test.ts +1745 -0
  155. package/src/admin/endpoints/create-category.ts +5 -2
  156. package/src/admin/endpoints/create-collection.ts +1 -1
  157. package/src/admin/endpoints/create-product.ts +5 -2
  158. package/src/admin/endpoints/delete-category.ts +1 -1
  159. package/src/admin/endpoints/delete-collection.ts +1 -1
  160. package/src/admin/endpoints/delete-product.ts +1 -1
  161. package/src/admin/endpoints/delete-variant.ts +1 -1
  162. package/src/admin/endpoints/list-categories.ts +1 -1
  163. package/src/admin/endpoints/list-collections.ts +1 -1
  164. package/src/admin/endpoints/list-products.ts +1 -1
  165. package/src/admin/endpoints/remove-collection-product.ts +1 -1
  166. package/src/admin/endpoints/update-category.ts +5 -2
  167. package/src/admin/endpoints/update-collection.ts +1 -1
  168. package/src/admin/endpoints/update-product.ts +5 -2
  169. package/src/admin/endpoints/update-variant.ts +1 -1
  170. package/src/service-impl.ts +1139 -0
  171. package/src/service.ts +312 -0
  172. package/src/store/components/_hooks.ts +81 -0
  173. package/src/store/components/_utils.ts +8 -0
  174. package/src/store/components/collection-detail.tsx +21 -1
  175. package/src/store/components/collection-grid.tsx +5 -1
  176. package/src/store/components/featured-products.tsx +5 -1
  177. package/src/store/components/index.tsx +2 -0
  178. package/src/store/components/product-card.mdx +1 -1
  179. package/src/store/components/product-card.tsx +25 -5
  180. package/src/store/components/product-detail.mdx +2 -0
  181. package/src/store/components/product-detail.tsx +55 -8
  182. package/src/store/components/product-listing.tsx +25 -4
  183. package/src/store/components/product-qa-section.mdx +21 -0
  184. package/src/store/components/product-qa-section.tsx +503 -0
  185. package/src/store/components/recommended-products.mdx +6 -0
  186. package/src/store/components/recommended-products.tsx +119 -0
  187. package/src/store/endpoints/get-category.ts +2 -2
  188. package/src/store/endpoints/get-collection.ts +1 -1
  189. package/src/store/endpoints/get-featured.ts +1 -1
  190. package/src/store/endpoints/get-product.ts +1 -1
  191. package/src/store/endpoints/get-related.ts +2 -2
  192. package/src/store/endpoints/list-collections.ts +3 -3
  193. package/src/store/endpoints/list-products.ts +9 -9
  194. package/src/store/endpoints/search-products.ts +4 -6
  195. package/src/store/endpoints/store-search.ts +1 -1
  196. package/COMPONENTS.md +0 -231
package/src/service.ts ADDED
@@ -0,0 +1,312 @@
1
+ import type { ModuleController } from "@86d-app/core";
2
+
3
+ export interface Product {
4
+ id: string;
5
+ name: string;
6
+ slug: string;
7
+ description?: string | undefined;
8
+ shortDescription?: string | undefined;
9
+ price: number;
10
+ compareAtPrice?: number | undefined;
11
+ costPrice?: number | undefined;
12
+ sku?: string | undefined;
13
+ barcode?: string | undefined;
14
+ inventory: number;
15
+ trackInventory: boolean;
16
+ allowBackorder: boolean;
17
+ status: "draft" | "active" | "archived";
18
+ categoryId?: string | undefined;
19
+ images: string[];
20
+ tags: string[];
21
+ metadata?: Record<string, unknown> | undefined;
22
+ weight?: number | undefined;
23
+ weightUnit?: "kg" | "lb" | "oz" | "g" | undefined;
24
+ isFeatured: boolean;
25
+ createdAt: Date;
26
+ updatedAt: Date;
27
+ }
28
+
29
+ export interface ProductVariant {
30
+ id: string;
31
+ productId: string;
32
+ name: string;
33
+ sku?: string | undefined;
34
+ barcode?: string | undefined;
35
+ price: number;
36
+ compareAtPrice?: number | undefined;
37
+ costPrice?: number | undefined;
38
+ inventory: number;
39
+ options: Record<string, string>;
40
+ images: string[];
41
+ weight?: number | undefined;
42
+ weightUnit?: "kg" | "lb" | "oz" | "g" | undefined;
43
+ position: number;
44
+ createdAt: Date;
45
+ updatedAt: Date;
46
+ }
47
+
48
+ export interface Category {
49
+ id: string;
50
+ name: string;
51
+ slug: string;
52
+ description?: string | undefined;
53
+ parentId?: string | undefined;
54
+ image?: string | undefined;
55
+ position: number;
56
+ isVisible: boolean;
57
+ metadata?: Record<string, unknown> | undefined;
58
+ createdAt: Date;
59
+ updatedAt: Date;
60
+ }
61
+
62
+ export interface CategoryWithChildren extends Category {
63
+ children: CategoryWithChildren[];
64
+ }
65
+
66
+ export interface ProductWithVariants extends Product {
67
+ variants: ProductVariant[];
68
+ category?: Category | undefined;
69
+ }
70
+
71
+ export interface Collection {
72
+ id: string;
73
+ name: string;
74
+ slug: string;
75
+ description?: string | undefined;
76
+ image?: string | undefined;
77
+ isFeatured: boolean;
78
+ isVisible: boolean;
79
+ position: number;
80
+ metadata?: Record<string, unknown> | undefined;
81
+ createdAt: Date;
82
+ updatedAt: Date;
83
+ }
84
+
85
+ export interface CollectionProduct {
86
+ id: string;
87
+ collectionId: string;
88
+ productId: string;
89
+ position: number;
90
+ createdAt: Date;
91
+ }
92
+
93
+ export interface CollectionWithProducts extends Collection {
94
+ products: Product[];
95
+ }
96
+
97
+ export interface ImportProductRow {
98
+ name: string;
99
+ slug?: string | undefined;
100
+ price: number | string;
101
+ sku?: string | undefined;
102
+ barcode?: string | undefined;
103
+ description?: string | undefined;
104
+ shortDescription?: string | undefined;
105
+ compareAtPrice?: number | string | undefined;
106
+ costPrice?: number | string | undefined;
107
+ inventory?: number | string | undefined;
108
+ status?: string | undefined;
109
+ category?: string | undefined;
110
+ tags?: string[] | undefined;
111
+ weight?: number | string | undefined;
112
+ weightUnit?: string | undefined;
113
+ featured?: boolean | undefined;
114
+ trackInventory?: boolean | undefined;
115
+ allowBackorder?: boolean | undefined;
116
+ }
117
+
118
+ export interface ImportError {
119
+ row: number;
120
+ field: string;
121
+ message: string;
122
+ }
123
+
124
+ export interface ImportResult {
125
+ created: number;
126
+ updated: number;
127
+ errors: ImportError[];
128
+ }
129
+
130
+ export interface CreateProductParams {
131
+ name: string;
132
+ slug: string;
133
+ price: number;
134
+ description?: string | undefined;
135
+ shortDescription?: string | undefined;
136
+ compareAtPrice?: number | undefined;
137
+ costPrice?: number | undefined;
138
+ sku?: string | undefined;
139
+ barcode?: string | undefined;
140
+ inventory?: number | undefined;
141
+ trackInventory?: boolean | undefined;
142
+ allowBackorder?: boolean | undefined;
143
+ status?: "draft" | "active" | "archived" | undefined;
144
+ categoryId?: string | undefined;
145
+ images?: string[] | undefined;
146
+ tags?: string[] | undefined;
147
+ metadata?: Record<string, unknown> | undefined;
148
+ weight?: number | undefined;
149
+ weightUnit?: "kg" | "lb" | "oz" | "g" | undefined;
150
+ isFeatured?: boolean | undefined;
151
+ }
152
+
153
+ export interface CreateVariantParams {
154
+ productId: string;
155
+ name: string;
156
+ price: number;
157
+ options: Record<string, string>;
158
+ sku?: string | undefined;
159
+ barcode?: string | undefined;
160
+ compareAtPrice?: number | undefined;
161
+ costPrice?: number | undefined;
162
+ inventory?: number | undefined;
163
+ images?: string[] | undefined;
164
+ weight?: number | undefined;
165
+ weightUnit?: "kg" | "lb" | "oz" | "g" | undefined;
166
+ position?: number | undefined;
167
+ }
168
+
169
+ export interface CreateCategoryParams {
170
+ name: string;
171
+ slug: string;
172
+ description?: string | undefined;
173
+ parentId?: string | undefined;
174
+ image?: string | undefined;
175
+ position?: number | undefined;
176
+ isVisible?: boolean | undefined;
177
+ metadata?: Record<string, unknown> | undefined;
178
+ }
179
+
180
+ export interface CreateCollectionParams {
181
+ name: string;
182
+ slug: string;
183
+ description?: string | undefined;
184
+ image?: string | undefined;
185
+ isFeatured?: boolean | undefined;
186
+ isVisible?: boolean | undefined;
187
+ position?: number | undefined;
188
+ metadata?: Record<string, unknown> | undefined;
189
+ }
190
+
191
+ export interface ListProductsParams {
192
+ page?: number | undefined;
193
+ limit?: number | undefined;
194
+ category?: string | undefined;
195
+ status?: string | undefined;
196
+ featured?: boolean | undefined;
197
+ search?: string | undefined;
198
+ sort?: string | undefined;
199
+ order?: string | undefined;
200
+ minPrice?: number | undefined;
201
+ maxPrice?: number | undefined;
202
+ inStock?: boolean | undefined;
203
+ tag?: string | undefined;
204
+ }
205
+
206
+ export interface ProductController extends ModuleController {
207
+ // ── Products ──
208
+ createProduct(params: CreateProductParams): Promise<Product>;
209
+ getProduct(id: string): Promise<Product | null>;
210
+ getProductBySlug(slug: string): Promise<Product | null>;
211
+ getProductWithVariants(id: string): Promise<ProductWithVariants | null>;
212
+ listProducts(params?: ListProductsParams): Promise<{
213
+ products: ProductWithVariants[];
214
+ total: number;
215
+ page: number;
216
+ limit: number;
217
+ }>;
218
+ searchProducts(q: string, limit?: number): Promise<Product[]>;
219
+ getFeaturedProducts(limit?: number): Promise<Product[]>;
220
+ getProductsByCategory(categoryId: string): Promise<Product[]>;
221
+ getRelatedProducts(
222
+ id: string,
223
+ limit?: number,
224
+ ): Promise<{ products: Product[] }>;
225
+ updateProduct(id: string, params: Partial<Product>): Promise<Product>;
226
+ deleteProduct(id: string): Promise<{ success: boolean }>;
227
+
228
+ // ── Inventory ──
229
+ checkAvailability(
230
+ productId: string,
231
+ variantId?: string,
232
+ quantity?: number,
233
+ ): Promise<{
234
+ available: boolean;
235
+ inventory: number;
236
+ allowBackorder: boolean;
237
+ }>;
238
+ decrementInventory(
239
+ productId: string,
240
+ quantity: number,
241
+ variantId?: string,
242
+ ): Promise<{ success: boolean }>;
243
+ incrementInventory(
244
+ productId: string,
245
+ quantity: number,
246
+ variantId?: string,
247
+ ): Promise<{ success: boolean }>;
248
+
249
+ // ── Variants ──
250
+ getVariant(id: string): Promise<ProductVariant | null>;
251
+ getVariantsByProduct(productId: string): Promise<ProductVariant[]>;
252
+ createVariant(params: CreateVariantParams): Promise<ProductVariant>;
253
+ updateVariant(
254
+ id: string,
255
+ params: Partial<ProductVariant>,
256
+ ): Promise<ProductVariant>;
257
+ deleteVariant(id: string): Promise<{ success: boolean }>;
258
+
259
+ // ── Categories ──
260
+ getCategory(id: string): Promise<Category | null>;
261
+ getCategoryBySlug(slug: string): Promise<Category | null>;
262
+ listCategories(params?: {
263
+ page?: number;
264
+ limit?: number;
265
+ parentId?: string;
266
+ visible?: boolean;
267
+ }): Promise<{ categories: Category[]; page: number; limit: number }>;
268
+ getCategoryTree(): Promise<CategoryWithChildren[]>;
269
+ createCategory(params: CreateCategoryParams): Promise<Category>;
270
+ updateCategory(id: string, params: Partial<Category>): Promise<Category>;
271
+ deleteCategory(id: string): Promise<{ success: boolean }>;
272
+
273
+ // ── Bulk ──
274
+ bulkUpdateStatus(
275
+ ids: string[],
276
+ status: "draft" | "active" | "archived",
277
+ ): Promise<{ updated: number }>;
278
+ bulkDelete(ids: string[]): Promise<{ deleted: number }>;
279
+
280
+ // ── Import ──
281
+ importProducts(rows: ImportProductRow[]): Promise<ImportResult>;
282
+
283
+ // ── Collections ──
284
+ getCollection(id: string): Promise<Collection | null>;
285
+ getCollectionBySlug(slug: string): Promise<Collection | null>;
286
+ listCollections(params?: {
287
+ page?: number;
288
+ limit?: number;
289
+ featured?: boolean;
290
+ visible?: boolean;
291
+ }): Promise<{ collections: Collection[]; page: number; limit: number }>;
292
+ searchCollections(q: string, limit?: number): Promise<Collection[]>;
293
+ getCollectionWithProducts(id: string): Promise<CollectionWithProducts | null>;
294
+ createCollection(params: CreateCollectionParams): Promise<Collection>;
295
+ updateCollection(
296
+ id: string,
297
+ params: Partial<Collection>,
298
+ ): Promise<Collection>;
299
+ deleteCollection(id: string): Promise<{ success: boolean }>;
300
+ addProductToCollection(
301
+ collectionId: string,
302
+ productId: string,
303
+ position?: number,
304
+ ): Promise<CollectionProduct>;
305
+ removeProductFromCollection(
306
+ collectionId: string,
307
+ productId: string,
308
+ ): Promise<{ success: boolean }>;
309
+ listCollectionProducts(
310
+ collectionId: string,
311
+ ): Promise<{ products: Product[] }>;
312
+ }
@@ -3,6 +3,53 @@
3
3
  import { useModuleClient } from "@86d-app/core/client";
4
4
  import { useCallback, useRef } from "react";
5
5
 
6
+ type RawCartItem = {
7
+ id: string;
8
+ productId: string;
9
+ variantId?: string | null;
10
+ quantity: number;
11
+ price: number;
12
+ productName: string;
13
+ productSlug: string;
14
+ productImage?: string | null;
15
+ variantName?: string | null;
16
+ variantOptions?: Record<string, string> | null;
17
+ };
18
+
19
+ type AddToCartResponse = {
20
+ cart: { id: string };
21
+ items: RawCartItem[];
22
+ itemCount: number;
23
+ subtotal: number;
24
+ };
25
+
26
+ export function normalizeCartQueryData(data: AddToCartResponse) {
27
+ return {
28
+ id: data.cart.id,
29
+ items: data.items.map((item) => ({
30
+ id: item.id,
31
+ productId: item.productId,
32
+ variantId: item.variantId ?? null,
33
+ quantity: item.quantity,
34
+ price: item.price,
35
+ product: {
36
+ name: item.productName,
37
+ price: item.price,
38
+ images: item.productImage ? [item.productImage] : [],
39
+ slug: item.productSlug,
40
+ },
41
+ variant: item.variantName
42
+ ? {
43
+ name: item.variantName,
44
+ options: item.variantOptions ?? {},
45
+ }
46
+ : null,
47
+ })),
48
+ itemCount: data.itemCount,
49
+ subtotal: data.subtotal,
50
+ };
51
+ }
52
+
6
53
  export function useProductsApi() {
7
54
  const client = useModuleClient();
8
55
  return {
@@ -20,6 +67,7 @@ export function useCartMutation() {
20
67
  return {
21
68
  addToCart: client.module("cart").store["/cart"],
22
69
  getCart: client.module("cart").store["/cart/get"],
70
+ queryClient: client.queryClient,
23
71
  };
24
72
  }
25
73
 
@@ -53,6 +101,39 @@ export function useAnalyticsApi() {
53
101
  };
54
102
  }
55
103
 
104
+ export function useProductQaApi() {
105
+ const client = useModuleClient();
106
+ return {
107
+ listProductQuestions:
108
+ client.module("product-qa").store[
109
+ "/product-qa/products/:productId/questions"
110
+ ],
111
+ productQaSummary:
112
+ client.module("product-qa").store[
113
+ "/product-qa/products/:productId/summary"
114
+ ],
115
+ submitQuestion: client.module("product-qa").store["/product-qa/questions"],
116
+ listAnswers:
117
+ client.module("product-qa").store[
118
+ "/product-qa/questions/:questionId/answers"
119
+ ],
120
+ upvoteQuestion:
121
+ client.module("product-qa").store["/product-qa/questions/:id/upvote"],
122
+ upvoteAnswer:
123
+ client.module("product-qa").store["/product-qa/answers/:id/upvote"],
124
+ };
125
+ }
126
+
127
+ export function useRecommendationsApi() {
128
+ const client = useModuleClient();
129
+ return {
130
+ getForProduct:
131
+ client.module("recommendations").store["/recommendations/:productId"],
132
+ getTrending:
133
+ client.module("recommendations").store["/recommendations/trending"],
134
+ };
135
+ }
136
+
56
137
  /** Fire-and-forget analytics event via the analytics module endpoint. */
57
138
  export function useTrack() {
58
139
  const client = useModuleClient();
@@ -1,3 +1,11 @@
1
+ /** Extract a URL string from an image entry (handles both string and {url,alt} formats). */
2
+ export function imageUrl(img: unknown): string {
3
+ if (typeof img === "string") return img;
4
+ if (img && typeof img === "object" && "url" in img)
5
+ return String((img as { url: string }).url);
6
+ return "";
7
+ }
8
+
1
9
  export function formatPrice(cents: number): string {
2
10
  return new Intl.NumberFormat("en-US", {
3
11
  style: "currency",
@@ -15,7 +15,7 @@ export function CollectionDetail(props: CollectionDetailProps) {
15
15
  const client = useModuleClient();
16
16
  const getCollection = client.module("products").store["/collections/:id"];
17
17
 
18
- const { data, isLoading } = getCollection.useQuery(
18
+ const { data, isLoading, isError } = getCollection.useQuery(
19
19
  { params: { id: slug ?? "" } },
20
20
  { enabled: !!slug },
21
21
  ) as {
@@ -25,6 +25,7 @@ export function CollectionDetail(props: CollectionDetailProps) {
25
25
  }
26
26
  | undefined;
27
27
  isLoading: boolean;
28
+ isError: boolean;
28
29
  };
29
30
 
30
31
  const collection = data?.collection ?? null;
@@ -63,6 +64,25 @@ export function CollectionDetail(props: CollectionDetailProps) {
63
64
  );
64
65
  }
65
66
 
67
+ if (isError) {
68
+ return (
69
+ <div className="flex flex-col items-center justify-center py-24 text-center">
70
+ <p className="font-medium text-foreground text-sm">
71
+ Something went wrong
72
+ </p>
73
+ <p className="mt-1 text-muted-foreground text-sm">
74
+ We couldn&apos;t load this collection. Please try again.
75
+ </p>
76
+ <a
77
+ href="/collections"
78
+ className="mt-3 text-muted-foreground text-sm transition-colors hover:text-foreground"
79
+ >
80
+ Back to collections
81
+ </a>
82
+ </div>
83
+ );
84
+ }
85
+
66
86
  if (!collection) {
67
87
  return (
68
88
  <div className="flex flex-col items-center justify-center py-24 text-center">
@@ -21,13 +21,17 @@ export function CollectionGrid({
21
21
  const queryInput: Record<string, any> = {};
22
22
  if (featured) queryInput.featured = "true";
23
23
 
24
- const { data, isLoading } = listCollections.useQuery(queryInput) as {
24
+ const { data, isLoading, isError } = listCollections.useQuery(queryInput) as {
25
25
  data: { collections: CollectionCardData[] } | undefined;
26
26
  isLoading: boolean;
27
+ isError: boolean;
27
28
  };
28
29
 
29
30
  const collections = data?.collections ?? [];
30
31
 
32
+ // Silently hide on error — homepage sections are non-critical
33
+ if (isError) return null;
34
+
31
35
  if (isLoading) {
32
36
  return (
33
37
  <section className="py-12 sm:py-14">
@@ -14,15 +14,19 @@ export function FeaturedProducts({
14
14
  title = "Featured Products",
15
15
  }: FeaturedProductsProps) {
16
16
  const api = useProductsApi();
17
- const { data, isLoading } = api.getFeaturedProducts.useQuery({
17
+ const { data, isLoading, isError } = api.getFeaturedProducts.useQuery({
18
18
  limit: String(limit),
19
19
  }) as {
20
20
  data: { products: import("./_types").Product[] } | undefined;
21
21
  isLoading: boolean;
22
+ isError: boolean;
22
23
  };
23
24
 
24
25
  const products = data?.products ?? [];
25
26
 
27
+ // Silently hide on error — homepage sections are non-critical
28
+ if (isError) return null;
29
+
26
30
  if (isLoading) {
27
31
  return (
28
32
  <section className="py-12 sm:py-14">
@@ -12,6 +12,7 @@ import { ProductDetail } from "./product-detail";
12
12
  import { ProductListing } from "./product-listing";
13
13
  import { ProductReviewsSection } from "./product-reviews-section";
14
14
  import { RecentlyViewedProducts } from "./recently-viewed";
15
+ import { RecommendedProducts } from "./recommended-products";
15
16
  import { RelatedProducts } from "./related-products";
16
17
  import { StarDisplay } from "./star-display";
17
18
  import { StarPicker } from "./star-picker";
@@ -36,4 +37,5 @@ export default {
36
37
  StockBadge,
37
38
  ProductReviewsSection,
38
39
  RecentlyViewedProducts,
40
+ RecommendedProducts,
39
41
  } satisfies MDXComponents;
@@ -60,7 +60,7 @@
60
60
  {props.priceFormatted}
61
61
  </span>
62
62
  {props.compareAtPriceFormatted && (
63
- <span className="text-muted-foreground/60 text-xs tabular-nums line-through">
63
+ <span className="text-muted-foreground text-xs tabular-nums line-through">
64
64
  {props.compareAtPriceFormatted}
65
65
  </span>
66
66
  )}
@@ -2,9 +2,9 @@
2
2
 
3
3
  import { useStoreContext } from "@86d-app/core/client";
4
4
  import { memo } from "react";
5
- import { useCartMutation, useTrack } from "./_hooks";
5
+ import { normalizeCartQueryData, useCartMutation, useTrack } from "./_hooks";
6
6
  import type { Product } from "./_types";
7
- import { formatPrice } from "./_utils";
7
+ import { formatPrice, imageUrl } from "./_utils";
8
8
  import ProductCardTemplate from "./product-card.mdx";
9
9
 
10
10
  export interface ProductCardProps {
@@ -22,9 +22,29 @@ export const ProductCard = memo(function ProductCard({
22
22
  const store = useStoreContext<{ cart: any }>();
23
23
 
24
24
  const addToCartMutation = cartApi.addToCart.useMutation({
25
- onSuccess: () => {
26
- void cartApi.getCart.invalidate();
25
+ onSuccess: (data: {
26
+ cart: { id: string };
27
+ items: {
28
+ id: string;
29
+ productId: string;
30
+ variantId?: string | null;
31
+ quantity: number;
32
+ price: number;
33
+ productName: string;
34
+ productSlug: string;
35
+ productImage?: string | null;
36
+ variantName?: string | null;
37
+ variantOptions?: Record<string, string> | null;
38
+ }[];
39
+ itemCount: number;
40
+ subtotal: number;
41
+ }) => {
42
+ store.cart.setItemCount(data.itemCount);
27
43
  store.cart.openDrawer();
44
+ cartApi.queryClient.setQueryData(
45
+ cartApi.getCart.getQueryKey(),
46
+ normalizeCartQueryData(data),
47
+ );
28
48
  track({
29
49
  type: "addToCart",
30
50
  productId: product.id,
@@ -34,7 +54,7 @@ export const ProductCard = memo(function ProductCard({
34
54
  },
35
55
  });
36
56
 
37
- const image = product.images[0];
57
+ const image = imageUrl(product.images[0]);
38
58
  const hasDiscount =
39
59
  product.compareAtPrice != null && product.compareAtPrice > product.price;
40
60
  const discountPct = hasDiscount
@@ -25,6 +25,8 @@
25
25
  </div>
26
26
 
27
27
  {props.reviewsSection}
28
+ {props.questionsSection}
29
+ {props.recommendedProducts}
28
30
  {props.relatedProducts}
29
31
  {props.recentlyViewed}
30
32
  </div>