create-brainerce-store 1.62.0 → 1.64.0

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 (203) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +71 -71
  3. package/dist/index.js +26 -3
  4. package/messages/en.json +502 -502
  5. package/messages/he.json +502 -502
  6. package/package.json +1 -1
  7. package/templates/nextjs/base/.claude/commands/design.md +60 -60
  8. package/templates/nextjs/base/.env.local.ejs +24 -24
  9. package/templates/nextjs/base/.eslintrc.json +57 -57
  10. package/templates/nextjs/base/.mcp.json +8 -8
  11. package/templates/nextjs/base/AI-GUIDE.md +174 -174
  12. package/templates/nextjs/base/TRANSLATIONS.md +200 -200
  13. package/templates/nextjs/base/package.json.ejs +52 -52
  14. package/templates/nextjs/base/src/app/agents.md/route.ts +87 -0
  15. package/templates/nextjs/base/src/app/blog/[slug]/page.tsx.ejs +308 -262
  16. package/templates/nextjs/base/src/app/blog/page.tsx.ejs +276 -276
  17. package/templates/nextjs/base/src/app/blog/rss.xml/route.ts +70 -0
  18. package/templates/nextjs/base/src/app/cart/page.tsx +7 -7
  19. package/templates/nextjs/base/src/app/category/[slug]/page.tsx +29 -3
  20. package/templates/nextjs/base/src/app/checkout/page.tsx +12 -0
  21. package/templates/nextjs/base/src/app/contact/layout.tsx.ejs +14 -0
  22. package/templates/nextjs/base/src/app/globals.css +36 -36
  23. package/templates/nextjs/base/src/app/indexnow-key.txt/route.ts +26 -26
  24. package/templates/nextjs/base/src/app/layout.tsx.ejs +30 -0
  25. package/templates/nextjs/base/src/app/llms.txt/route.ts +28 -5
  26. package/templates/nextjs/base/src/app/not-found.tsx.ejs +35 -0
  27. package/templates/nextjs/base/src/app/opengraph-image.tsx +109 -0
  28. package/templates/nextjs/base/src/app/order-confirmation/page.tsx +295 -278
  29. package/templates/nextjs/base/src/app/order-status/page.tsx +121 -121
  30. package/templates/nextjs/base/src/app/page.tsx +64 -64
  31. package/templates/nextjs/base/src/app/products/[slug]/page.tsx +14 -2
  32. package/templates/nextjs/base/src/app/products/page.tsx +19 -19
  33. package/templates/nextjs/base/src/app/robots.ts +73 -14
  34. package/templates/nextjs/base/src/app/sitemap.ts +89 -66
  35. package/templates/nextjs/base/src/components/account/order-history.tsx +385 -385
  36. package/templates/nextjs/base/src/components/brainerce-bot.tsx +70 -70
  37. package/templates/nextjs/base/src/components/checkout/checkout-form.tsx +612 -612
  38. package/templates/nextjs/base/src/components/checkout/custom-fields-step.tsx +292 -292
  39. package/templates/nextjs/base/src/components/checkout/date-picker.tsx +309 -309
  40. package/templates/nextjs/base/src/components/checkout/datetime-picker.tsx +144 -144
  41. package/templates/nextjs/base/src/components/checkout/order-bump-card.tsx +242 -242
  42. package/templates/nextjs/base/src/components/checkout/pickup-step.tsx +198 -198
  43. package/templates/nextjs/base/src/components/checkout/shipping-step.tsx +125 -125
  44. package/templates/nextjs/base/src/components/checkout/tax-display.tsx +74 -74
  45. package/templates/nextjs/base/src/components/seo/article-json-ld.tsx +59 -59
  46. package/templates/nextjs/base/src/components/seo/breadcrumbs.tsx +37 -37
  47. package/templates/nextjs/base/src/components/seo/category-json-ld.tsx +61 -61
  48. package/templates/nextjs/base/src/components/seo/organization-json-ld.tsx +97 -97
  49. package/templates/nextjs/base/src/components/seo/product-json-ld.tsx +23 -131
  50. package/templates/nextjs/base/src/components/tracking-bootstrap.tsx +62 -0
  51. package/templates/nextjs/base/src/components/ui/accordion.tsx +53 -53
  52. package/templates/nextjs/base/src/components/ui/badge.tsx +32 -32
  53. package/templates/nextjs/base/src/components/ui/button.tsx +49 -49
  54. package/templates/nextjs/base/src/components/ui/card.tsx +55 -55
  55. package/templates/nextjs/base/src/components/ui/checkbox.tsx +28 -28
  56. package/templates/nextjs/base/src/components/ui/dialog.tsx +107 -107
  57. package/templates/nextjs/base/src/components/ui/input.tsx +22 -22
  58. package/templates/nextjs/base/src/components/ui/label.tsx +21 -21
  59. package/templates/nextjs/base/src/components/ui/radio-group.tsx +38 -38
  60. package/templates/nextjs/base/src/components/ui/select.tsx +153 -153
  61. package/templates/nextjs/base/src/components/ui/separator.tsx +26 -26
  62. package/templates/nextjs/base/src/components/ui/sheet.tsx +125 -125
  63. package/templates/nextjs/base/src/components/ui/skeleton.tsx +7 -7
  64. package/templates/nextjs/base/src/components/ui/tabs.tsx +55 -55
  65. package/templates/nextjs/base/src/components/ui/textarea.tsx +21 -21
  66. package/templates/nextjs/base/src/components/ui/tooltip.tsx +32 -32
  67. package/templates/nextjs/base/src/core/hooks/use-cart-page.ts +58 -58
  68. package/templates/nextjs/base/src/core/hooks/use-home-data.ts +51 -51
  69. package/templates/nextjs/base/src/core/hooks/use-product-listing.ts +295 -295
  70. package/templates/nextjs/base/src/core/hooks/use-product-page.ts +20 -2
  71. package/templates/nextjs/base/src/core/lib/image-hosts.ts +7 -7
  72. package/templates/nextjs/base/src/core/lib/product-options.ts +121 -121
  73. package/templates/nextjs/base/src/core/lib/resolve-currency.ts +20 -20
  74. package/templates/nextjs/base/src/core/lib/safe-redirect.ts +38 -38
  75. package/templates/nextjs/base/src/core/lib/seo.ts +46 -46
  76. package/templates/nextjs/base/src/core/lib/store-info.ts +17 -0
  77. package/templates/nextjs/base/src/core/lib/tracking.ts +171 -0
  78. package/templates/nextjs/base/src/core/lib/use-currency.ts +19 -19
  79. package/templates/nextjs/base/src/core/lib/utils.ts +21 -21
  80. package/templates/nextjs/base/src/middleware.ts.ejs +54 -2
  81. package/templates/nextjs/base/src/ui/cart/cart-bundle-offer.tsx +117 -117
  82. package/templates/nextjs/base/src/ui/cart/cart-item.tsx +146 -146
  83. package/templates/nextjs/base/src/ui/cart/cart-nudges.tsx +28 -28
  84. package/templates/nextjs/base/src/ui/cart/cart-summary.tsx +110 -110
  85. package/templates/nextjs/base/src/ui/cart/cart-upgrade-banner.tsx +126 -126
  86. package/templates/nextjs/base/src/ui/cart/cart-view.tsx +140 -140
  87. package/templates/nextjs/base/src/ui/cart/coupon-input.tsx +134 -134
  88. package/templates/nextjs/base/src/ui/cart/free-shipping-bar.tsx +61 -61
  89. package/templates/nextjs/base/src/ui/cart/reservation-countdown.tsx +95 -95
  90. package/templates/nextjs/base/src/ui/home/discount-banner-strip.tsx +22 -20
  91. package/templates/nextjs/base/src/ui/home/featured-products-section.tsx +24 -24
  92. package/templates/nextjs/base/src/ui/home/hero-section.tsx +27 -27
  93. package/templates/nextjs/base/src/ui/home/home-client.tsx +29 -29
  94. package/templates/nextjs/base/src/ui/layout/announcement-bar.tsx.ejs +116 -116
  95. package/templates/nextjs/base/src/ui/layout/faq-section.tsx.ejs +90 -90
  96. package/templates/nextjs/base/src/ui/layout/header-account.tsx +40 -40
  97. package/templates/nextjs/base/src/ui/layout/language-switcher.tsx.ejs +80 -80
  98. package/templates/nextjs/base/src/ui/layout/site-footer.tsx.ejs +144 -144
  99. package/templates/nextjs/base/src/ui/layout/site-header.tsx.ejs +152 -152
  100. package/templates/nextjs/base/src/ui/product/customization-fields.tsx +457 -457
  101. package/templates/nextjs/base/src/ui/product/discount-badge.tsx +18 -18
  102. package/templates/nextjs/base/src/ui/product/frequently-bought-together.tsx +191 -191
  103. package/templates/nextjs/base/src/ui/product/modifier-group-selector.tsx +206 -206
  104. package/templates/nextjs/base/src/ui/product/price-display.tsx +62 -62
  105. package/templates/nextjs/base/src/ui/product/product-card.tsx +7 -2
  106. package/templates/nextjs/base/src/ui/product/product-client-section.tsx +455 -455
  107. package/templates/nextjs/base/src/ui/product/product-listing.tsx +446 -446
  108. package/templates/nextjs/base/src/ui/product/recommendation-section.tsx +102 -102
  109. package/templates/nextjs/base/src/ui/product/review-form.tsx +294 -294
  110. package/templates/nextjs/base/src/ui/product/reviews-section.tsx.ejs +108 -108
  111. package/templates/nextjs/base/src/ui/product/stock-badge.tsx +61 -61
  112. package/templates/nextjs/base/src/ui/product/variant-selector.tsx +291 -291
  113. package/templates/nextjs/base/src/ui/shared/cdn-image.tsx +26 -26
  114. package/templates/nextjs/base/tailwind.config.ts +69 -69
  115. package/templates/nextjs/designs/atelier/app-overlay/layout.tsx.ejs +30 -0
  116. package/templates/nextjs/designs/atelier/globals.css +402 -402
  117. package/templates/nextjs/designs/atelier/messages-patch/en.json +100 -100
  118. package/templates/nextjs/designs/atelier/messages-patch/he.json +100 -100
  119. package/templates/nextjs/designs/atelier/ui/cart/cart-bundle-offer.tsx +110 -110
  120. package/templates/nextjs/designs/atelier/ui/cart/cart-drawer.tsx +163 -163
  121. package/templates/nextjs/designs/atelier/ui/cart/cart-item.tsx +140 -140
  122. package/templates/nextjs/designs/atelier/ui/cart/cart-nudges.tsx +34 -34
  123. package/templates/nextjs/designs/atelier/ui/cart/cart-summary.tsx +107 -107
  124. package/templates/nextjs/designs/atelier/ui/cart/cart-upgrade-banner.tsx +123 -123
  125. package/templates/nextjs/designs/atelier/ui/cart/cart-view.tsx +147 -147
  126. package/templates/nextjs/designs/atelier/ui/cart/coupon-input.tsx +142 -142
  127. package/templates/nextjs/designs/atelier/ui/cart/free-shipping-bar.tsx +77 -77
  128. package/templates/nextjs/designs/atelier/ui/cart/reservation-countdown.tsx +89 -89
  129. package/templates/nextjs/designs/atelier/ui/home/benefits-band.tsx +34 -34
  130. package/templates/nextjs/designs/atelier/ui/home/category-tiles.tsx +151 -151
  131. package/templates/nextjs/designs/atelier/ui/home/discount-banner-strip.tsx +27 -27
  132. package/templates/nextjs/designs/atelier/ui/home/editorial-band.tsx +75 -75
  133. package/templates/nextjs/designs/atelier/ui/home/featured-products-section.tsx +36 -36
  134. package/templates/nextjs/designs/atelier/ui/home/hero-section.tsx +174 -174
  135. package/templates/nextjs/designs/atelier/ui/home/home-client.tsx +54 -54
  136. package/templates/nextjs/designs/atelier/ui/home/newsletter-section.tsx +78 -78
  137. package/templates/nextjs/designs/atelier/ui/home/testimonials-band.tsx +46 -46
  138. package/templates/nextjs/designs/atelier/ui/layout/announcement-bar.tsx +118 -118
  139. package/templates/nextjs/designs/atelier/ui/layout/faq-section.tsx +96 -96
  140. package/templates/nextjs/designs/atelier/ui/layout/header-account.tsx +36 -36
  141. package/templates/nextjs/designs/atelier/ui/layout/header-cart.tsx +36 -36
  142. package/templates/nextjs/designs/atelier/ui/layout/language-switcher.tsx.ejs +137 -137
  143. package/templates/nextjs/designs/atelier/ui/layout/nav-url.ts +14 -14
  144. package/templates/nextjs/designs/atelier/ui/layout/rich-text-block.tsx +34 -34
  145. package/templates/nextjs/designs/atelier/ui/layout/site-footer.tsx.ejs +141 -141
  146. package/templates/nextjs/designs/atelier/ui/layout/site-header.tsx.ejs +138 -138
  147. package/templates/nextjs/designs/atelier/ui/product/customization-fields.tsx +466 -466
  148. package/templates/nextjs/designs/atelier/ui/product/discount-badge.tsx +17 -17
  149. package/templates/nextjs/designs/atelier/ui/product/frequently-bought-together.tsx +196 -196
  150. package/templates/nextjs/designs/atelier/ui/product/modifier-group-selector.tsx +193 -193
  151. package/templates/nextjs/designs/atelier/ui/product/price-display.tsx +69 -69
  152. package/templates/nextjs/designs/atelier/ui/product/product-card.tsx +13 -11
  153. package/templates/nextjs/designs/atelier/ui/product/product-client-section.tsx +477 -477
  154. package/templates/nextjs/designs/atelier/ui/product/product-grid.tsx +42 -42
  155. package/templates/nextjs/designs/atelier/ui/product/product-listing.tsx +260 -260
  156. package/templates/nextjs/designs/atelier/ui/product/recommendation-section.tsx +95 -95
  157. package/templates/nextjs/designs/atelier/ui/product/review-form.tsx +267 -267
  158. package/templates/nextjs/designs/atelier/ui/product/reviews-section.tsx.ejs +148 -148
  159. package/templates/nextjs/designs/atelier/ui/product/stock-badge.tsx +66 -66
  160. package/templates/nextjs/designs/atelier/ui/product/variant-selector.tsx +206 -206
  161. package/templates/nextjs/designs/atelier/ui/shared/cdn-image.tsx +29 -29
  162. package/templates/nextjs/designs/atelier/ui/shared/fly-to-cart.ts +86 -86
  163. package/templates/nextjs/designs/atelier/ui/shared/icons.tsx +276 -276
  164. package/templates/nextjs/designs/atelier/ui/shared/loading-spinner.tsx +63 -63
  165. package/templates/nextjs/designs/atelier/ui/shared/reveal.tsx +71 -71
  166. package/templates/nextjs/designs/atelier/ui/shared/select-menu.tsx +125 -125
  167. package/templates/nextjs/ui-canvas/cart/cart-bundle-offer.tsx +89 -89
  168. package/templates/nextjs/ui-canvas/cart/cart-item.tsx +123 -123
  169. package/templates/nextjs/ui-canvas/cart/cart-nudges.tsx +29 -27
  170. package/templates/nextjs/ui-canvas/cart/cart-summary.tsx +99 -99
  171. package/templates/nextjs/ui-canvas/cart/cart-upgrade-banner.tsx +105 -105
  172. package/templates/nextjs/ui-canvas/cart/cart-view.tsx +106 -106
  173. package/templates/nextjs/ui-canvas/cart/coupon-input.tsx +119 -119
  174. package/templates/nextjs/ui-canvas/cart/free-shipping-bar.tsx +54 -54
  175. package/templates/nextjs/ui-canvas/cart/reservation-countdown.tsx +81 -81
  176. package/templates/nextjs/ui-canvas/home/discount-banner-strip.tsx +21 -19
  177. package/templates/nextjs/ui-canvas/home/featured-products-section.tsx +23 -23
  178. package/templates/nextjs/ui-canvas/home/hero-section.tsx +21 -21
  179. package/templates/nextjs/ui-canvas/home/home-client.tsx +26 -26
  180. package/templates/nextjs/ui-canvas/layout/announcement-bar.tsx.ejs +101 -101
  181. package/templates/nextjs/ui-canvas/layout/faq-section.tsx.ejs +71 -71
  182. package/templates/nextjs/ui-canvas/layout/header-account.tsx +31 -31
  183. package/templates/nextjs/ui-canvas/layout/language-switcher.tsx.ejs +76 -76
  184. package/templates/nextjs/ui-canvas/layout/rich-text-block.tsx.ejs +36 -36
  185. package/templates/nextjs/ui-canvas/layout/site-footer.tsx.ejs +82 -82
  186. package/templates/nextjs/ui-canvas/layout/site-header.tsx.ejs +119 -119
  187. package/templates/nextjs/ui-canvas/product/customization-fields.tsx +445 -445
  188. package/templates/nextjs/ui-canvas/product/discount-badge.tsx +19 -19
  189. package/templates/nextjs/ui-canvas/product/frequently-bought-together.tsx +168 -168
  190. package/templates/nextjs/ui-canvas/product/modifier-group-selector.tsx +165 -165
  191. package/templates/nextjs/ui-canvas/product/price-display.tsx +52 -52
  192. package/templates/nextjs/ui-canvas/product/product-card.tsx +7 -2
  193. package/templates/nextjs/ui-canvas/product/product-client-section.tsx +352 -352
  194. package/templates/nextjs/ui-canvas/product/product-grid.tsx +27 -27
  195. package/templates/nextjs/ui-canvas/product/product-listing.tsx +224 -224
  196. package/templates/nextjs/ui-canvas/product/recommendation-section.tsx +99 -99
  197. package/templates/nextjs/ui-canvas/product/review-form.tsx +266 -266
  198. package/templates/nextjs/ui-canvas/product/reviews-section.tsx.ejs +96 -96
  199. package/templates/nextjs/ui-canvas/product/stock-badge.tsx +45 -45
  200. package/templates/nextjs/ui-canvas/product/variant-selector.tsx +212 -212
  201. package/templates/nextjs/ui-canvas/shared/cdn-image.tsx +31 -31
  202. package/templates/nextjs/ui-canvas/shared/loading-spinner.tsx +27 -27
  203. package/templates/nextjs/base/tsconfig.tsbuildinfo +0 -1
@@ -1,8 +1,7 @@
1
1
  import type { Product, ShippingSummaryEntry } from 'brainerce';
2
- import { getProductPriceInfo } from 'brainerce';
2
+ import { buildProductJsonLd, buildBreadcrumbJsonLd } from 'brainerce';
3
3
  import { getNonce } from '@/core/lib/nonce';
4
4
  import { resolveCurrency } from '@/core/lib/resolve-currency';
5
- import { stripHtmlForSeo } from '@/core/lib/seo';
6
5
 
7
6
  interface ProductJsonLdProps {
8
7
  product: Product;
@@ -12,143 +11,36 @@ interface ProductJsonLdProps {
12
11
  shipping?: ShippingSummaryEntry[];
13
12
  }
14
13
 
14
+ /**
15
+ * Product + BreadcrumbList structured data for a product detail page (PDP).
16
+ *
17
+ * Uses the SDK's `buildProductJsonLd` — NEVER hand-roll the schema object
18
+ * here. The builder owns the availability mapping (InStock / BackOrder /
19
+ * OutOfStock from the backend's pre-computed inventory flags), the
20
+ * Offer/AggregateOffer split, identifier emission (gtin/mpn/sku), shipping
21
+ * details, and the aggregateRating gating; a local copy WILL drift from it.
22
+ * The <script> is rendered locally only to attach the CSP nonce (the SDK's
23
+ * jsonLdScriptProps can't).
24
+ */
15
25
  export async function ProductJsonLd({ product, url, currency, shipping }: ProductJsonLdProps) {
16
26
  // Defer to the shared server helper so the env-var + USD fallback chain
17
27
  // lives in exactly one place across all server-side currency reads.
18
28
  const resolvedCurrency = resolveCurrency(null, currency);
19
29
  const nonce = await getNonce();
20
- const priceInfo = getProductPriceInfo(product);
21
- const imageUrl = product.images?.[0]?.url;
22
30
  const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || '';
23
31
 
24
- const availability =
25
- product.inventory?.canPurchase !== false
26
- ? 'https://schema.org/InStock'
27
- : 'https://schema.org/OutOfStock';
32
+ const productJsonLd = buildProductJsonLd(product, {
33
+ siteUrl: baseUrl,
34
+ path: url,
35
+ currency: resolvedCurrency,
36
+ shipping,
37
+ });
28
38
 
29
- // All Brainerce catalog listings are first-party new-goods sales — no
30
- // used/refurbished condition data exists anywhere in the product model, so
31
- // this is a deliberate always-true assumption, not a per-product field.
32
- const itemCondition = 'https://schema.org/NewCondition';
33
-
34
- const shippingDetails = (shipping ?? [])
35
- .filter((z) => z.amount !== null)
36
- .map((z) => ({
37
- '@type': 'OfferShippingDetails',
38
- shippingRate: { '@type': 'MonetaryAmount', value: z.amount, currency: resolvedCurrency },
39
- shippingDestination: { '@type': 'DefinedRegion', addressCountry: z.countries },
40
- ...(z.handlingTime != null || z.minDeliveryDays != null || z.maxDeliveryDays != null
41
- ? {
42
- deliveryTime: {
43
- '@type': 'ShippingDeliveryTime',
44
- ...(z.handlingTime != null
45
- ? {
46
- handlingTime: {
47
- '@type': 'QuantitativeValue',
48
- minValue: 0,
49
- maxValue: z.handlingTime,
50
- },
51
- }
52
- : {}),
53
- ...(z.minDeliveryDays != null || z.maxDeliveryDays != null
54
- ? {
55
- transitTime: {
56
- '@type': 'QuantitativeValue',
57
- minValue: z.minDeliveryDays ?? z.maxDeliveryDays,
58
- maxValue: z.maxDeliveryDays ?? z.minDeliveryDays,
59
- },
60
- }
61
- : {}),
62
- },
63
- }
64
- : {}),
65
- }));
66
-
67
- const isVariable = product.type === 'VARIABLE';
68
- const offers =
69
- isVariable && product.priceMin
70
- ? {
71
- '@type': 'AggregateOffer',
72
- lowPrice: product.priceMin,
73
- highPrice: product.priceMax ?? product.priceMin,
74
- offerCount: product.variants?.length ?? 1,
75
- priceCurrency: resolvedCurrency,
76
- availability,
77
- itemCondition,
78
- ...(shippingDetails.length > 0 ? { shippingDetails } : {}),
79
- url,
80
- }
81
- : {
82
- '@type': 'Offer',
83
- price: priceInfo.price,
84
- priceCurrency: resolvedCurrency,
85
- availability,
86
- itemCondition,
87
- ...(product.salePrice && product.salePriceEndsAt
88
- ? { priceValidUntil: product.salePriceEndsAt }
89
- : {}),
90
- ...(shippingDetails.length > 0 ? { shippingDetails } : {}),
91
- url,
92
- };
93
-
94
- // schema.org/Product.description must be plain text — Google's Rich Results
95
- // and AI Overviews ingest this field directly and reject markup. Strip HTML
96
- // first; if the description is empty after stripping, fall back to the name.
97
- const cleanDescription = stripHtmlForSeo(product.description) || product.name;
98
- const brandName = product.brands?.[0]?.name;
99
- const productJsonLd: Record<string, unknown> = {
100
- '@context': 'https://schema.org',
101
- '@type': 'Product',
102
- name: product.name,
103
- description: cleanDescription,
104
- image: imageUrl,
105
- url,
106
- sku: product.sku || product.id,
107
- // Brand + identifiers qualify the PDP for Google's free merchant listings
108
- // (no Merchant Center feed needed). Emitted only when present — Google
109
- // penalizes fabricated identifiers.
110
- ...(brandName ? { brand: { '@type': 'Brand', name: brandName } } : {}),
111
- ...(product.gtin ? { gtin: product.gtin } : {}),
112
- ...(product.mpn ? { mpn: product.mpn } : {}),
113
- offers,
114
- };
115
-
116
- // Emit aggregateRating only when there is real review data — Google rejects
117
- // self-serving / faked ratings and stores with 0 reviews shouldn't claim any.
118
- if (product.reviewCount && product.reviewCount > 0) {
119
- productJsonLd.aggregateRating = {
120
- '@type': 'AggregateRating',
121
- ratingValue: product.avgRating,
122
- reviewCount: product.reviewCount,
123
- bestRating: 5,
124
- worstRating: 1,
125
- };
126
- }
127
-
128
- const breadcrumbJsonLd = {
129
- '@context': 'https://schema.org',
130
- '@type': 'BreadcrumbList',
131
- itemListElement: [
132
- {
133
- '@type': 'ListItem',
134
- position: 1,
135
- name: 'Home',
136
- item: baseUrl || '/',
137
- },
138
- {
139
- '@type': 'ListItem',
140
- position: 2,
141
- name: 'Products',
142
- item: `${baseUrl}/products`,
143
- },
144
- {
145
- '@type': 'ListItem',
146
- position: 3,
147
- name: product.name,
148
- item: url,
149
- },
150
- ],
151
- };
39
+ const breadcrumbJsonLd = buildBreadcrumbJsonLd([
40
+ { name: 'Home', url: baseUrl || '/' },
41
+ { name: 'Products', url: `${baseUrl}/products` },
42
+ { name: product.name, url },
43
+ ]);
152
44
 
153
45
  return (
154
46
  <>
@@ -0,0 +1,62 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useRef } from 'react';
4
+ import { usePathname } from 'next/navigation';
5
+ import { getClient } from '@/core/lib/brainerce';
6
+ import { useStoreInfo } from '@/core/providers/store-provider';
7
+
8
+ /**
9
+ * Boots the merchant's marketing tags — GA4, Google Tag Manager, the Meta
10
+ * pixel, the TikTok pixel — from whatever they connected in the Brainerce
11
+ * dashboard.
12
+ *
13
+ * There is nothing to configure here and no environment variable to set. The
14
+ * tag ids arrive inside `storeInfo.tracking`, which the backend resolves from
15
+ * the merchant's connected marketplace apps: connecting the Google & YouTube
16
+ * app runs GA4 discovery and the measurement id shows up on its own, likewise
17
+ * the Meta and TikTok pixels. Connect an app in the dashboard and this file
18
+ * starts loading its tag within a minute — no redeploy, no code change.
19
+ *
20
+ * Loading GA4 also switches on Brainerce's server-side purchase conversion:
21
+ * the SDK captures gtag's `client_id`/`session_id` and attaches them to cart
22
+ * and checkout calls, which is what lets the backend send a purchase event
23
+ * that lands in the right GA4 session instead of orphaning.
24
+ */
25
+ export function TrackingBootstrap() {
26
+ const { storeInfo } = useStoreInfo();
27
+ const pathname = usePathname();
28
+ const tracking = storeInfo?.tracking;
29
+ const bootedRef = useRef(false);
30
+
31
+ useEffect(() => {
32
+ if (!tracking) return;
33
+ getClient().initTracking(tracking);
34
+ bootedRef.current = true;
35
+ }, [tracking]);
36
+
37
+ // App Router client-side navigation never reloads the document, so the
38
+ // pixels' one-shot boot pageview is the only one they would ever record —
39
+ // every page after the landing page would be invisible. Re-fire on each
40
+ // path change, skipping the first run so the landing page isn't counted
41
+ // twice.
42
+ const lastPathRef = useRef<string | null>(null);
43
+ useEffect(() => {
44
+ if (!bootedRef.current) return;
45
+ if (lastPathRef.current === null) {
46
+ lastPathRef.current = pathname;
47
+ return;
48
+ }
49
+ if (lastPathRef.current === pathname) return;
50
+ lastPathRef.current = pathname;
51
+
52
+ if (typeof window === 'undefined') return;
53
+ try {
54
+ window.fbq?.('track', 'PageView');
55
+ (window.ttq as { page?: () => void } | undefined)?.page?.();
56
+ } catch {
57
+ // Analytics must never break navigation.
58
+ }
59
+ }, [pathname]);
60
+
61
+ return null;
62
+ }
@@ -1,53 +1,53 @@
1
- 'use client';
2
-
3
- import * as React from 'react';
4
- import * as AccordionPrimitive from '@radix-ui/react-accordion';
5
- import { ChevronDown } from 'lucide-react';
6
-
7
- import { cn } from '@/core/lib/utils';
8
-
9
- const Accordion = AccordionPrimitive.Root;
10
-
11
- const AccordionItem = React.forwardRef<
12
- React.ElementRef<typeof AccordionPrimitive.Item>,
13
- React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
14
- >(({ className, ...props }, ref) => (
15
- <AccordionPrimitive.Item ref={ref} className={cn('border-b', className)} {...props} />
16
- ));
17
- AccordionItem.displayName = 'AccordionItem';
18
-
19
- const AccordionTrigger = React.forwardRef<
20
- React.ElementRef<typeof AccordionPrimitive.Trigger>,
21
- React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
22
- >(({ className, children, ...props }, ref) => (
23
- <AccordionPrimitive.Header className="flex">
24
- <AccordionPrimitive.Trigger
25
- ref={ref}
26
- className={cn(
27
- 'flex flex-1 items-center justify-between py-4 text-start font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180',
28
- className
29
- )}
30
- {...props}
31
- >
32
- {children}
33
- <ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
34
- </AccordionPrimitive.Trigger>
35
- </AccordionPrimitive.Header>
36
- ));
37
- AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
38
-
39
- const AccordionContent = React.forwardRef<
40
- React.ElementRef<typeof AccordionPrimitive.Content>,
41
- React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
42
- >(({ className, children, ...props }, ref) => (
43
- <AccordionPrimitive.Content
44
- ref={ref}
45
- className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
46
- {...props}
47
- >
48
- <div className={cn('pb-4 pt-0', className)}>{children}</div>
49
- </AccordionPrimitive.Content>
50
- ));
51
- AccordionContent.displayName = AccordionPrimitive.Content.displayName;
52
-
53
- export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import * as AccordionPrimitive from '@radix-ui/react-accordion';
5
+ import { ChevronDown } from 'lucide-react';
6
+
7
+ import { cn } from '@/core/lib/utils';
8
+
9
+ const Accordion = AccordionPrimitive.Root;
10
+
11
+ const AccordionItem = React.forwardRef<
12
+ React.ElementRef<typeof AccordionPrimitive.Item>,
13
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
14
+ >(({ className, ...props }, ref) => (
15
+ <AccordionPrimitive.Item ref={ref} className={cn('border-b', className)} {...props} />
16
+ ));
17
+ AccordionItem.displayName = 'AccordionItem';
18
+
19
+ const AccordionTrigger = React.forwardRef<
20
+ React.ElementRef<typeof AccordionPrimitive.Trigger>,
21
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
22
+ >(({ className, children, ...props }, ref) => (
23
+ <AccordionPrimitive.Header className="flex">
24
+ <AccordionPrimitive.Trigger
25
+ ref={ref}
26
+ className={cn(
27
+ 'flex flex-1 items-center justify-between py-4 text-start font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180',
28
+ className
29
+ )}
30
+ {...props}
31
+ >
32
+ {children}
33
+ <ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
34
+ </AccordionPrimitive.Trigger>
35
+ </AccordionPrimitive.Header>
36
+ ));
37
+ AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
38
+
39
+ const AccordionContent = React.forwardRef<
40
+ React.ElementRef<typeof AccordionPrimitive.Content>,
41
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
42
+ >(({ className, children, ...props }, ref) => (
43
+ <AccordionPrimitive.Content
44
+ ref={ref}
45
+ className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
46
+ {...props}
47
+ >
48
+ <div className={cn('pb-4 pt-0', className)}>{children}</div>
49
+ </AccordionPrimitive.Content>
50
+ ));
51
+ AccordionContent.displayName = AccordionPrimitive.Content.displayName;
52
+
53
+ export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
@@ -1,32 +1,32 @@
1
- import * as React from 'react';
2
- import { cva, type VariantProps } from 'class-variance-authority';
3
-
4
- import { cn } from '@/core/lib/utils';
5
-
6
- const badgeVariants = cva(
7
- 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
8
- {
9
- variants: {
10
- variant: {
11
- default: 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
12
- secondary: 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
13
- destructive:
14
- 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
15
- outline: 'text-foreground',
16
- },
17
- },
18
- defaultVariants: {
19
- variant: 'default',
20
- },
21
- }
22
- );
23
-
24
- export interface BadgeProps
25
- extends React.HTMLAttributes<HTMLDivElement>,
26
- VariantProps<typeof badgeVariants> {}
27
-
28
- function Badge({ className, variant, ...props }: BadgeProps) {
29
- return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
30
- }
31
-
32
- export { Badge, badgeVariants };
1
+ import * as React from 'react';
2
+ import { cva, type VariantProps } from 'class-variance-authority';
3
+
4
+ import { cn } from '@/core/lib/utils';
5
+
6
+ const badgeVariants = cva(
7
+ 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
12
+ secondary: 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
13
+ destructive:
14
+ 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
15
+ outline: 'text-foreground',
16
+ },
17
+ },
18
+ defaultVariants: {
19
+ variant: 'default',
20
+ },
21
+ }
22
+ );
23
+
24
+ export interface BadgeProps
25
+ extends React.HTMLAttributes<HTMLDivElement>,
26
+ VariantProps<typeof badgeVariants> {}
27
+
28
+ function Badge({ className, variant, ...props }: BadgeProps) {
29
+ return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
30
+ }
31
+
32
+ export { Badge, badgeVariants };
@@ -1,49 +1,49 @@
1
- import * as React from 'react';
2
- import { Slot } from '@radix-ui/react-slot';
3
- import { cva, type VariantProps } from 'class-variance-authority';
4
-
5
- import { cn } from '@/core/lib/utils';
6
-
7
- const buttonVariants = cva(
8
- 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
9
- {
10
- variants: {
11
- variant: {
12
- default: 'bg-primary text-primary-foreground hover:bg-primary/90',
13
- destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
14
- outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
15
- secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
16
- ghost: 'hover:bg-accent hover:text-accent-foreground',
17
- link: 'text-primary underline-offset-4 hover:underline',
18
- },
19
- size: {
20
- default: 'h-10 px-4 py-2',
21
- sm: 'h-9 rounded-md px-3',
22
- lg: 'h-11 rounded-md px-8',
23
- icon: 'h-10 w-10',
24
- },
25
- },
26
- defaultVariants: {
27
- variant: 'default',
28
- size: 'default',
29
- },
30
- }
31
- );
32
-
33
- export interface ButtonProps
34
- extends React.ButtonHTMLAttributes<HTMLButtonElement>,
35
- VariantProps<typeof buttonVariants> {
36
- asChild?: boolean;
37
- }
38
-
39
- const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
40
- ({ className, variant, size, asChild = false, ...props }, ref) => {
41
- const Comp = asChild ? Slot : 'button';
42
- return (
43
- <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
44
- );
45
- }
46
- );
47
- Button.displayName = 'Button';
48
-
49
- export { Button, buttonVariants };
1
+ import * as React from 'react';
2
+ import { Slot } from '@radix-ui/react-slot';
3
+ import { cva, type VariantProps } from 'class-variance-authority';
4
+
5
+ import { cn } from '@/core/lib/utils';
6
+
7
+ const buttonVariants = cva(
8
+ 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: 'bg-primary text-primary-foreground hover:bg-primary/90',
13
+ destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
14
+ outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
15
+ secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
16
+ ghost: 'hover:bg-accent hover:text-accent-foreground',
17
+ link: 'text-primary underline-offset-4 hover:underline',
18
+ },
19
+ size: {
20
+ default: 'h-10 px-4 py-2',
21
+ sm: 'h-9 rounded-md px-3',
22
+ lg: 'h-11 rounded-md px-8',
23
+ icon: 'h-10 w-10',
24
+ },
25
+ },
26
+ defaultVariants: {
27
+ variant: 'default',
28
+ size: 'default',
29
+ },
30
+ }
31
+ );
32
+
33
+ export interface ButtonProps
34
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
35
+ VariantProps<typeof buttonVariants> {
36
+ asChild?: boolean;
37
+ }
38
+
39
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
40
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
41
+ const Comp = asChild ? Slot : 'button';
42
+ return (
43
+ <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
44
+ );
45
+ }
46
+ );
47
+ Button.displayName = 'Button';
48
+
49
+ export { Button, buttonVariants };
@@ -1,55 +1,55 @@
1
- import * as React from 'react';
2
-
3
- import { cn } from '@/core/lib/utils';
4
-
5
- const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
6
- ({ className, ...props }, ref) => (
7
- <div
8
- ref={ref}
9
- className={cn('rounded-lg border bg-card text-card-foreground shadow-sm', className)}
10
- {...props}
11
- />
12
- )
13
- );
14
- Card.displayName = 'Card';
15
-
16
- const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
17
- ({ className, ...props }, ref) => (
18
- <div ref={ref} className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} />
19
- )
20
- );
21
- CardHeader.displayName = 'CardHeader';
22
-
23
- const CardTitle = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
24
- ({ className, ...props }, ref) => (
25
- <div
26
- ref={ref}
27
- className={cn('text-2xl font-semibold leading-none tracking-tight', className)}
28
- {...props}
29
- />
30
- )
31
- );
32
- CardTitle.displayName = 'CardTitle';
33
-
34
- const CardDescription = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
35
- ({ className, ...props }, ref) => (
36
- <div ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />
37
- )
38
- );
39
- CardDescription.displayName = 'CardDescription';
40
-
41
- const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
42
- ({ className, ...props }, ref) => (
43
- <div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
44
- )
45
- );
46
- CardContent.displayName = 'CardContent';
47
-
48
- const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
49
- ({ className, ...props }, ref) => (
50
- <div ref={ref} className={cn('flex items-center p-6 pt-0', className)} {...props} />
51
- )
52
- );
53
- CardFooter.displayName = 'CardFooter';
54
-
55
- export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
1
+ import * as React from 'react';
2
+
3
+ import { cn } from '@/core/lib/utils';
4
+
5
+ const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
6
+ ({ className, ...props }, ref) => (
7
+ <div
8
+ ref={ref}
9
+ className={cn('rounded-lg border bg-card text-card-foreground shadow-sm', className)}
10
+ {...props}
11
+ />
12
+ )
13
+ );
14
+ Card.displayName = 'Card';
15
+
16
+ const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
17
+ ({ className, ...props }, ref) => (
18
+ <div ref={ref} className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} />
19
+ )
20
+ );
21
+ CardHeader.displayName = 'CardHeader';
22
+
23
+ const CardTitle = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
24
+ ({ className, ...props }, ref) => (
25
+ <div
26
+ ref={ref}
27
+ className={cn('text-2xl font-semibold leading-none tracking-tight', className)}
28
+ {...props}
29
+ />
30
+ )
31
+ );
32
+ CardTitle.displayName = 'CardTitle';
33
+
34
+ const CardDescription = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
35
+ ({ className, ...props }, ref) => (
36
+ <div ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />
37
+ )
38
+ );
39
+ CardDescription.displayName = 'CardDescription';
40
+
41
+ const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
42
+ ({ className, ...props }, ref) => (
43
+ <div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
44
+ )
45
+ );
46
+ CardContent.displayName = 'CardContent';
47
+
48
+ const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
49
+ ({ className, ...props }, ref) => (
50
+ <div ref={ref} className={cn('flex items-center p-6 pt-0', className)} {...props} />
51
+ )
52
+ );
53
+ CardFooter.displayName = 'CardFooter';
54
+
55
+ export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };