@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
@@ -3,6 +3,7 @@
3
3
  import { useStoreContext } from "@86d-app/core/client";
4
4
  import { useEffect, useRef, useState } from "react";
5
5
  import {
6
+ normalizeCartQueryData,
6
7
  useCartMutation,
7
8
  useProductsApi,
8
9
  useReviewsApi,
@@ -13,11 +14,13 @@ import type {
13
14
  ProductWithVariants,
14
15
  ReviewsResponse,
15
16
  } from "./_types";
16
- import { formatPrice } from "./_utils";
17
+ import { formatPrice, imageUrl } from "./_utils";
17
18
  import { BackInStockNotify } from "./back-in-stock-notify";
18
19
  import ProductDetailTemplate from "./product-detail.mdx";
20
+ import { ProductQASection } from "./product-qa-section";
19
21
  import { ProductReviewsSection } from "./product-reviews-section";
20
22
  import { RecentlyViewedProducts } from "./recently-viewed";
23
+ import { RecommendedProducts } from "./recommended-products";
21
24
  import { RelatedProducts } from "./related-products";
22
25
  import { StarDisplay } from "./star-display";
23
26
  import { StockBadge } from "./stock-badge";
@@ -36,12 +39,13 @@ export function ProductDetail(props: ProductDetailProps) {
36
39
  // biome-ignore lint/suspicious/noExplicitAny: store context shape varies per app
37
40
  const store = useStoreContext<{ cart: any }>();
38
41
 
39
- const { data, isLoading } = api.getProduct.useQuery(
42
+ const { data, isLoading, isError } = api.getProduct.useQuery(
40
43
  { params: { id: slug ?? "" } },
41
44
  { enabled: !!slug },
42
45
  ) as {
43
46
  data: { product: ProductWithVariants } | undefined;
44
47
  isLoading: boolean;
48
+ isError: boolean;
45
49
  };
46
50
 
47
51
  const product = data?.product ?? null;
@@ -63,6 +67,24 @@ export function ProductDetail(props: ProductDetailProps) {
63
67
  const [qty, setQty] = useState(1);
64
68
  const [added, setAdded] = useState(false);
65
69
 
70
+ type AddToCartResponse = {
71
+ cart: { id: string };
72
+ items: {
73
+ id: string;
74
+ productId: string;
75
+ variantId?: string | null;
76
+ quantity: number;
77
+ price: number;
78
+ productName: string;
79
+ productSlug: string;
80
+ productImage?: string | null;
81
+ variantName?: string | null;
82
+ variantOptions?: Record<string, string> | null;
83
+ }[];
84
+ itemCount: number;
85
+ subtotal: number;
86
+ };
87
+
66
88
  const trackedRef = useRef<string | null>(null);
67
89
  useEffect(() => {
68
90
  if (product && trackedRef.current !== product.id) {
@@ -74,7 +96,7 @@ export function ProductDetail(props: ProductDetailProps) {
74
96
  name: product.name,
75
97
  slug: product.slug,
76
98
  price: product.price,
77
- image: product.images[0],
99
+ image: imageUrl(product.images[0]),
78
100
  },
79
101
  });
80
102
  }
@@ -88,9 +110,13 @@ export function ProductDetail(props: ProductDetailProps) {
88
110
  }, [product?.id]);
89
111
 
90
112
  const addToCartMutation = cartApi.addToCart.useMutation({
91
- onSuccess: () => {
92
- void cartApi.getCart.invalidate();
113
+ onSuccess: (data: AddToCartResponse) => {
114
+ store.cart.setItemCount(data.itemCount);
93
115
  store.cart.openDrawer();
116
+ cartApi.queryClient.setQueryData(
117
+ cartApi.getCart.getQueryKey(),
118
+ normalizeCartQueryData(data),
119
+ );
94
120
  setAdded(true);
95
121
  setTimeout(() => setAdded(false), 2000);
96
122
  if (product) {
@@ -136,6 +162,25 @@ export function ProductDetail(props: ProductDetailProps) {
136
162
  );
137
163
  }
138
164
 
165
+ if (isError) {
166
+ return (
167
+ <div className="flex flex-col items-center justify-center py-24 text-center">
168
+ <p className="font-medium text-foreground text-sm">
169
+ Something went wrong
170
+ </p>
171
+ <p className="mt-1 text-muted-foreground text-sm">
172
+ We couldn&apos;t load this product. Please try again.
173
+ </p>
174
+ <a
175
+ href="/products"
176
+ className="mt-3 text-muted-foreground text-sm transition-colors hover:text-foreground"
177
+ >
178
+ Back to products
179
+ </a>
180
+ </div>
181
+ );
182
+ }
183
+
139
184
  if (!product) {
140
185
  return (
141
186
  <div className="flex flex-col items-center justify-center py-24 text-center">
@@ -195,7 +240,7 @@ export function ProductDetail(props: ProductDetailProps) {
195
240
  price: displayPrice,
196
241
  productName: product.name,
197
242
  productSlug: product.slug,
198
- productImage: product.images[0],
243
+ productImage: imageUrl(product.images[0]),
199
244
  variantName: selectedVariant?.name,
200
245
  variantOptions: selectedVariant?.options,
201
246
  });
@@ -222,7 +267,7 @@ export function ProductDetail(props: ProductDetailProps) {
222
267
  <div className="aspect-square overflow-hidden rounded-lg bg-muted">
223
268
  {product.images[selectedImage] ? (
224
269
  <img
225
- src={product.images[selectedImage]}
270
+ src={imageUrl(product.images[selectedImage])}
226
271
  alt={product.name}
227
272
  className="h-full w-full object-cover object-center"
228
273
  />
@@ -261,7 +306,7 @@ export function ProductDetail(props: ProductDetailProps) {
261
306
  }`}
262
307
  >
263
308
  <img
264
- src={img}
309
+ src={imageUrl(img)}
265
310
  alt={`${product.name} view ${i + 1}`}
266
311
  className="h-full w-full object-cover"
267
312
  />
@@ -481,6 +526,8 @@ export function ProductDetail(props: ProductDetailProps) {
481
526
  descriptionBlock={descriptionBlock}
482
527
  tagsBlock={tagsBlock}
483
528
  reviewsSection={<ProductReviewsSection productId={product.id} />}
529
+ questionsSection={<ProductQASection productId={product.id} />}
530
+ recommendedProducts={<RecommendedProducts productId={product.id} />}
484
531
  relatedProducts={<RelatedProducts productId={product.id} />}
485
532
  recentlyViewed={<RecentlyViewedProducts excludeProductId={product.id} />}
486
533
  />
@@ -44,11 +44,14 @@ export function ProductListing({
44
44
  if (inStock) queryInput.inStock = "true";
45
45
  if (tag) queryInput.tag = tag;
46
46
 
47
- const { data: productsData, isLoading } = api.listProducts.useQuery(
48
- queryInput,
49
- ) as {
47
+ const {
48
+ data: productsData,
49
+ isLoading,
50
+ isError,
51
+ } = api.listProducts.useQuery(queryInput) as {
50
52
  data: { products: Product[]; total: number } | undefined;
51
53
  isLoading: boolean;
54
+ isError: boolean;
52
55
  };
53
56
 
54
57
  const { data: categoriesData } = api.listCategories.useQuery() as {
@@ -116,6 +119,7 @@ export function ProductListing({
116
119
 
117
120
  {categories.length > 0 && (
118
121
  <select
122
+ aria-label="Filter by category"
119
123
  value={category}
120
124
  onChange={(e) => {
121
125
  setCategory(e.target.value);
@@ -133,6 +137,7 @@ export function ProductListing({
133
137
  )}
134
138
 
135
139
  <select
140
+ aria-label="Sort products"
136
141
  value={`${sort}:${order}`}
137
142
  onChange={(e) => {
138
143
  const [s, o] = e.target.value.split(":");
@@ -350,7 +355,23 @@ export function ProductListing({
350
355
  </div>
351
356
  ) : null;
352
357
 
353
- const gridContent = isLoading ? (
358
+ const gridContent = isError ? (
359
+ <div className="flex flex-col items-center justify-center py-20 text-center">
360
+ <p className="font-medium text-foreground text-sm">
361
+ Something went wrong
362
+ </p>
363
+ <p className="mt-1 text-muted-foreground text-sm">
364
+ We couldn&apos;t load products right now. Please try again.
365
+ </p>
366
+ <button
367
+ type="button"
368
+ onClick={() => window.location.reload()}
369
+ className="mt-4 rounded-full border border-border px-4 py-1.5 text-foreground text-xs transition-colors hover:bg-muted"
370
+ >
371
+ Refresh page
372
+ </button>
373
+ </div>
374
+ ) : isLoading ? (
354
375
  <div className="grid grid-cols-2 gap-x-4 gap-y-8 sm:grid-cols-3 lg:grid-cols-4">
355
376
  {Array.from({ length: pageSize }).map((_, i) => (
356
377
  <div key={i}>
@@ -0,0 +1,21 @@
1
+ <section id="questions" className="border-border/50 border-t py-10">
2
+ {/* Header */}
3
+ <div className="mb-6 flex flex-wrap items-center justify-between gap-4">
4
+ <div>
5
+ <h2 className="font-display font-semibold text-foreground text-lg tracking-tight sm:text-xl">
6
+ Questions & Answers
7
+ </h2>
8
+ {props.summaryDisplay}
9
+ </div>
10
+ {props.toggleFormButton}
11
+ </div>
12
+
13
+ {/* Question form */}
14
+ {props.formContent}
15
+
16
+ {/* Empty state */}
17
+ {props.emptyState}
18
+
19
+ {/* Questions list */}
20
+ {props.questionsContent}
21
+ </section>