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,121 +1,121 @@
1
- 'use client';
2
-
3
- import { Suspense, useEffect, useState } from 'react';
4
- import { useSearchParams } from 'next/navigation';
5
- import { Link } from '@/core/lib/navigation';
6
- import type { Order } from 'brainerce';
7
- import { getClient } from '@/core/lib/brainerce';
8
- import { LoadingSpinner } from '@/ui/shared/loading-spinner';
9
- import { OrderHistory } from '@/components/account/order-history';
10
- import { useTranslations } from '@/core/lib/translations';
11
-
12
- // Read-only order lookup for links from transactional emails (order confirmation,
13
- // shipped, refunded, etc). Unlike /order-confirmation, this page never calls
14
- // handlePaymentSuccess() or touches cart state — it only fetches and displays,
15
- // so it's safe to revisit at any time without side effects on an unrelated
16
- // active cart. Works for guests too: the checkout id itself is the credential.
17
- function OrderStatusContent() {
18
- const searchParams = useSearchParams();
19
- const checkoutId = searchParams.get('checkout_id');
20
-
21
- const t = useTranslations('orderStatus');
22
- const tc = useTranslations('common');
23
- const [order, setOrder] = useState<Order | null>(null);
24
- const [loading, setLoading] = useState(true);
25
- const [error, setError] = useState<string | null>(null);
26
-
27
- useEffect(() => {
28
- if (!checkoutId) {
29
- setError(t('missingCheckoutInfo'));
30
- setLoading(false);
31
- return;
32
- }
33
-
34
- let cancelled = false;
35
- async function loadOrder() {
36
- try {
37
- const client = getClient();
38
- const result = await client.getOrderByCheckout(checkoutId!);
39
- if (!cancelled) setOrder(result);
40
- } catch (err) {
41
- if (!cancelled) {
42
- const message = err instanceof Error ? err.message : t('notFound');
43
- setError(message);
44
- }
45
- } finally {
46
- if (!cancelled) setLoading(false);
47
- }
48
- }
49
-
50
- loadOrder();
51
- return () => {
52
- cancelled = true;
53
- };
54
- }, [checkoutId, t]);
55
-
56
- if (loading) {
57
- return (
58
- <div className="flex min-h-[60vh] flex-col items-center justify-center">
59
- <LoadingSpinner size="lg" />
60
- <p className="text-muted-foreground mt-4">{t('loading')}</p>
61
- </div>
62
- );
63
- }
64
-
65
- if (error || !order) {
66
- return (
67
- <div className="mx-auto max-w-2xl px-4 py-16 text-center sm:px-6 lg:px-8">
68
- <svg
69
- className="text-destructive mx-auto mb-4 h-16 w-16"
70
- fill="none"
71
- viewBox="0 0 24 24"
72
- stroke="currentColor"
73
- >
74
- <path
75
- strokeLinecap="round"
76
- strokeLinejoin="round"
77
- strokeWidth={1.5}
78
- d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.834-2.694-.834-3.464 0L3.34 16.5c-.77.833.192 2.5 1.732 2.5z"
79
- />
80
- </svg>
81
- <h1 className="text-foreground text-2xl font-bold">{t('errorTitle')}</h1>
82
- <p className="text-muted-foreground mt-2">{error || t('notFound')}</p>
83
- <Link
84
- href="/"
85
- className="bg-primary text-primary-foreground mt-6 inline-flex items-center rounded px-6 py-3 font-medium transition-opacity hover:opacity-90"
86
- >
87
- {t('returnHome')}
88
- </Link>
89
- </div>
90
- );
91
- }
92
-
93
- return (
94
- <div className="mx-auto max-w-3xl px-4 py-8 sm:px-6 lg:px-8">
95
- <h1 className="text-foreground mb-6 text-2xl font-bold">{t('pageTitle')}</h1>
96
- <OrderHistory orders={[order]} />
97
- <div className="mt-8 flex items-center justify-center">
98
- <Link
99
- href="/products"
100
- className="bg-primary text-primary-foreground inline-flex items-center rounded px-6 py-3 font-medium transition-opacity hover:opacity-90"
101
- >
102
- {tc('continueShopping')}
103
- </Link>
104
- </div>
105
- </div>
106
- );
107
- }
108
-
109
- export default function OrderStatusPage() {
110
- return (
111
- <Suspense
112
- fallback={
113
- <div className="flex min-h-[60vh] items-center justify-center">
114
- <LoadingSpinner size="lg" />
115
- </div>
116
- }
117
- >
118
- <OrderStatusContent />
119
- </Suspense>
120
- );
121
- }
1
+ 'use client';
2
+
3
+ import { Suspense, useEffect, useState } from 'react';
4
+ import { useSearchParams } from 'next/navigation';
5
+ import { Link } from '@/core/lib/navigation';
6
+ import type { Order } from 'brainerce';
7
+ import { getClient } from '@/core/lib/brainerce';
8
+ import { LoadingSpinner } from '@/ui/shared/loading-spinner';
9
+ import { OrderHistory } from '@/components/account/order-history';
10
+ import { useTranslations } from '@/core/lib/translations';
11
+
12
+ // Read-only order lookup for links from transactional emails (order confirmation,
13
+ // shipped, refunded, etc). Unlike /order-confirmation, this page never calls
14
+ // handlePaymentSuccess() or touches cart state — it only fetches and displays,
15
+ // so it's safe to revisit at any time without side effects on an unrelated
16
+ // active cart. Works for guests too: the checkout id itself is the credential.
17
+ function OrderStatusContent() {
18
+ const searchParams = useSearchParams();
19
+ const checkoutId = searchParams.get('checkout_id');
20
+
21
+ const t = useTranslations('orderStatus');
22
+ const tc = useTranslations('common');
23
+ const [order, setOrder] = useState<Order | null>(null);
24
+ const [loading, setLoading] = useState(true);
25
+ const [error, setError] = useState<string | null>(null);
26
+
27
+ useEffect(() => {
28
+ if (!checkoutId) {
29
+ setError(t('missingCheckoutInfo'));
30
+ setLoading(false);
31
+ return;
32
+ }
33
+
34
+ let cancelled = false;
35
+ async function loadOrder() {
36
+ try {
37
+ const client = getClient();
38
+ const result = await client.getOrderByCheckout(checkoutId!);
39
+ if (!cancelled) setOrder(result);
40
+ } catch (err) {
41
+ if (!cancelled) {
42
+ const message = err instanceof Error ? err.message : t('notFound');
43
+ setError(message);
44
+ }
45
+ } finally {
46
+ if (!cancelled) setLoading(false);
47
+ }
48
+ }
49
+
50
+ loadOrder();
51
+ return () => {
52
+ cancelled = true;
53
+ };
54
+ }, [checkoutId, t]);
55
+
56
+ if (loading) {
57
+ return (
58
+ <div className="flex min-h-[60vh] flex-col items-center justify-center">
59
+ <LoadingSpinner size="lg" />
60
+ <p className="text-muted-foreground mt-4">{t('loading')}</p>
61
+ </div>
62
+ );
63
+ }
64
+
65
+ if (error || !order) {
66
+ return (
67
+ <div className="mx-auto max-w-2xl px-4 py-16 text-center sm:px-6 lg:px-8">
68
+ <svg
69
+ className="text-destructive mx-auto mb-4 h-16 w-16"
70
+ fill="none"
71
+ viewBox="0 0 24 24"
72
+ stroke="currentColor"
73
+ >
74
+ <path
75
+ strokeLinecap="round"
76
+ strokeLinejoin="round"
77
+ strokeWidth={1.5}
78
+ d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.834-2.694-.834-3.464 0L3.34 16.5c-.77.833.192 2.5 1.732 2.5z"
79
+ />
80
+ </svg>
81
+ <h1 className="text-foreground text-2xl font-bold">{t('errorTitle')}</h1>
82
+ <p className="text-muted-foreground mt-2">{error || t('notFound')}</p>
83
+ <Link
84
+ href="/"
85
+ className="bg-primary text-primary-foreground mt-6 inline-flex items-center rounded px-6 py-3 font-medium transition-opacity hover:opacity-90"
86
+ >
87
+ {t('returnHome')}
88
+ </Link>
89
+ </div>
90
+ );
91
+ }
92
+
93
+ return (
94
+ <div className="mx-auto max-w-3xl px-4 py-8 sm:px-6 lg:px-8">
95
+ <h1 className="text-foreground mb-6 text-2xl font-bold">{t('pageTitle')}</h1>
96
+ <OrderHistory orders={[order]} />
97
+ <div className="mt-8 flex items-center justify-center">
98
+ <Link
99
+ href="/products"
100
+ className="bg-primary text-primary-foreground inline-flex items-center rounded px-6 py-3 font-medium transition-opacity hover:opacity-90"
101
+ >
102
+ {tc('continueShopping')}
103
+ </Link>
104
+ </div>
105
+ </div>
106
+ );
107
+ }
108
+
109
+ export default function OrderStatusPage() {
110
+ return (
111
+ <Suspense
112
+ fallback={
113
+ <div className="flex min-h-[60vh] items-center justify-center">
114
+ <LoadingSpinner size="lg" />
115
+ </div>
116
+ }
117
+ >
118
+ <OrderStatusContent />
119
+ </Suspense>
120
+ );
121
+ }
@@ -1,64 +1,64 @@
1
- import type { Metadata } from 'next';
2
- import { getServerClient } from '@/core/lib/brainerce';
3
- import { buildMetaDescription } from '@/core/lib/seo';
4
- import { OrganizationJsonLd } from '@/components/seo/organization-json-ld';
5
- import { RichTextBlock } from '@/ui/layout/rich-text-block';
6
- import { HomeClient } from '@/ui/home/home-client';
7
-
8
- // Build homepage metadata server-side so Google, Facebook, WhatsApp etc.
9
- // see a real <meta name="description"> on first request. Falls back to the
10
- // store name when the merchant hasn't authored a meta description yet.
11
- export async function generateMetadata(): Promise<Metadata> {
12
- try {
13
- const storeInfo = await getServerClient().getStoreInfo();
14
- const description = buildMetaDescription(storeInfo.metaDescription) || undefined;
15
- return {
16
- title: storeInfo.name,
17
- description,
18
- openGraph: {
19
- title: storeInfo.name,
20
- description,
21
- type: 'website',
22
- },
23
- twitter: {
24
- card: 'summary_large_image',
25
- title: storeInfo.name,
26
- description,
27
- },
28
- };
29
- } catch {
30
- return {};
31
- }
32
- }
33
-
34
- export default async function HomePage() {
35
- // Fetch store info server-side so the JSON-LD is in the initial HTML —
36
- // crawlers and AI Overviews see it without needing to execute JavaScript.
37
- // Failure here is non-fatal: the client component re-fetches via the
38
- // StoreProvider so the page still renders.
39
- //
40
- // Also fetch the merchant's homepage intro (RICH_TEXT key='main'). When
41
- // present, it renders as a sanitized HTML block above the product grid —
42
- // typical use is brand introduction, value-prop copy, or a seasonal
43
- // announcement that needs more formatting than the AnnouncementBar.
44
- // When absent, RichTextBlock returns null cleanly so the layout collapses
45
- // to just the product grid.
46
- const client = getServerClient();
47
- const [storeInfo, intro] = await Promise.all([
48
- client.getStoreInfo().catch(() => null),
49
- client.content.richText.get('main').catch(() => null),
50
- ]);
51
-
52
- const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || '';
53
-
54
- return (
55
- <>
56
- {storeInfo && baseUrl ? <OrganizationJsonLd storeInfo={storeInfo} baseUrl={baseUrl} /> : null}
57
- <RichTextBlock
58
- block={intro}
59
- className="prose prose-sm dark:prose-invert mx-auto max-w-3xl px-4 py-8 sm:px-6 lg:px-8"
60
- />
61
- <HomeClient />
62
- </>
63
- );
64
- }
1
+ import type { Metadata } from 'next';
2
+ import { getServerClient } from '@/core/lib/brainerce';
3
+ import { buildMetaDescription } from '@/core/lib/seo';
4
+ import { OrganizationJsonLd } from '@/components/seo/organization-json-ld';
5
+ import { RichTextBlock } from '@/ui/layout/rich-text-block';
6
+ import { HomeClient } from '@/ui/home/home-client';
7
+
8
+ // Build homepage metadata server-side so Google, Facebook, WhatsApp etc.
9
+ // see a real <meta name="description"> on first request. Falls back to the
10
+ // store name when the merchant hasn't authored a meta description yet.
11
+ export async function generateMetadata(): Promise<Metadata> {
12
+ try {
13
+ const storeInfo = await getServerClient().getStoreInfo();
14
+ const description = buildMetaDescription(storeInfo.metaDescription) || undefined;
15
+ return {
16
+ title: storeInfo.name,
17
+ description,
18
+ openGraph: {
19
+ title: storeInfo.name,
20
+ description,
21
+ type: 'website',
22
+ },
23
+ twitter: {
24
+ card: 'summary_large_image',
25
+ title: storeInfo.name,
26
+ description,
27
+ },
28
+ };
29
+ } catch {
30
+ return {};
31
+ }
32
+ }
33
+
34
+ export default async function HomePage() {
35
+ // Fetch store info server-side so the JSON-LD is in the initial HTML —
36
+ // crawlers and AI Overviews see it without needing to execute JavaScript.
37
+ // Failure here is non-fatal: the client component re-fetches via the
38
+ // StoreProvider so the page still renders.
39
+ //
40
+ // Also fetch the merchant's homepage intro (RICH_TEXT key='main'). When
41
+ // present, it renders as a sanitized HTML block above the product grid —
42
+ // typical use is brand introduction, value-prop copy, or a seasonal
43
+ // announcement that needs more formatting than the AnnouncementBar.
44
+ // When absent, RichTextBlock returns null cleanly so the layout collapses
45
+ // to just the product grid.
46
+ const client = getServerClient();
47
+ const [storeInfo, intro] = await Promise.all([
48
+ client.getStoreInfo().catch(() => null),
49
+ client.content.richText.get('main').catch(() => null),
50
+ ]);
51
+
52
+ const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || '';
53
+
54
+ return (
55
+ <>
56
+ {storeInfo && baseUrl ? <OrganizationJsonLd storeInfo={storeInfo} baseUrl={baseUrl} /> : null}
57
+ <RichTextBlock
58
+ block={intro}
59
+ className="prose prose-sm dark:prose-invert mx-auto max-w-3xl px-4 py-8 sm:px-6 lg:px-8"
60
+ />
61
+ <HomeClient />
62
+ </>
63
+ );
64
+ }
@@ -1,5 +1,5 @@
1
1
  import type { Metadata } from 'next';
2
- import { notFound } from 'next/navigation';
2
+ import { notFound, permanentRedirect } from 'next/navigation';
3
3
  import { getProductPriceInfo } from 'brainerce';
4
4
  import { getServerClient, fetchStoreInfo } from '@/core/lib/brainerce';
5
5
  import { resolveCurrency } from '@/core/lib/resolve-currency';
@@ -132,11 +132,23 @@ export default async function ProductDetailPage({ params }: Props) {
132
132
  const { slug: rawSlug, locale } = await params;
133
133
  const slug = decodeSlug(rawSlug);
134
134
 
135
+ const client = getServerClient(locale);
135
136
  let product;
136
137
  try {
137
- const client = getServerClient(locale);
138
138
  product = await client.getProductBySlug(slug);
139
139
  } catch {
140
+ // The slug may have been RENAMED — the platform records every rename.
141
+ // 301 the old URL to the current slug so its ranking and inbound links
142
+ // carry over; genuine unknowns fall through to the 404 page. Default
143
+ // locale stays unprefixed (matches the sitemap/hreflang convention); the
144
+ // destination page's canonical then points crawlers at the localized URL.
145
+ const redirect = await client.resolveSlugRedirect('product', slug);
146
+ if (redirect) {
147
+ const info = await fetchStoreInfo(locale);
148
+ const defaultLoc = info?.i18n?.defaultLocale ?? info?.language ?? '';
149
+ const prefix = locale && locale !== defaultLoc ? `/${locale}` : '';
150
+ permanentRedirect(`${prefix}/products/${redirect.currentSlug}`);
151
+ }
140
152
  notFound();
141
153
  }
142
154
 
@@ -1,19 +1,19 @@
1
- 'use client';
2
-
3
- import { Suspense } from 'react';
4
- import { ProductListing } from '@/ui/product/product-listing';
5
- import { LoadingSpinner } from '@/ui/shared/loading-spinner';
6
-
7
- export default function ProductsPage() {
8
- return (
9
- <Suspense
10
- fallback={
11
- <div className="flex min-h-[60vh] items-center justify-center">
12
- <LoadingSpinner size="lg" />
13
- </div>
14
- }
15
- >
16
- <ProductListing />
17
- </Suspense>
18
- );
19
- }
1
+ 'use client';
2
+
3
+ import { Suspense } from 'react';
4
+ import { ProductListing } from '@/ui/product/product-listing';
5
+ import { LoadingSpinner } from '@/ui/shared/loading-spinner';
6
+
7
+ export default function ProductsPage() {
8
+ return (
9
+ <Suspense
10
+ fallback={
11
+ <div className="flex min-h-[60vh] items-center justify-center">
12
+ <LoadingSpinner size="lg" />
13
+ </div>
14
+ }
15
+ >
16
+ <ProductListing />
17
+ </Suspense>
18
+ );
19
+ }
@@ -1,14 +1,73 @@
1
- import type { MetadataRoute } from 'next';
2
-
3
- export default function robots(): MetadataRoute.Robots {
4
- const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://example.com';
5
-
6
- return {
7
- rules: {
8
- userAgent: '*',
9
- allow: '/',
10
- disallow: ['/api/', '/auth/', '/checkout/', '/account/'],
11
- },
12
- sitemap: `${baseUrl}/sitemap.xml`,
13
- };
14
- }
1
+ import type { MetadataRoute } from 'next';
2
+
3
+ /**
4
+ * robots.txt with an explicit AI-crawler policy.
5
+ *
6
+ * Merchants WANT to be recommended by AI assistants, so the bots that power
7
+ * AI search/answers are allowed by name — an explicit allow survives any
8
+ * future tightening of the `*` rule and documents intent to auditors:
9
+ *
10
+ * - OAI-SearchBot / ChatGPT-User → ChatGPT search + shopping visibility
11
+ * - Claude-SearchBot / Claude-User → Claude search + user-requested reads
12
+ * - PerplexityBot / Perplexity-User → Perplexity shopping visibility
13
+ * - Bingbot → also feeds Microsoft Copilot answers
14
+ * - Applebot → Siri / Apple search surfaces
15
+ * - Amazonbot → Alexa / Amazon shopping surfaces
16
+ *
17
+ * TRAINING bots (GPTBot, ClaudeBot, CCBot, Google-Extended, Meta-
18
+ * ExternalAgent, Applebot-Extended) are also allowed by default: model-weight
19
+ * familiarity with the store feeds zero-shot recommendations, which is what a
20
+ * store wants. A merchant who objects to AI training on their content can
21
+ * move those user-agents to a `disallow: '/'` rule — that does NOT affect
22
+ * search visibility (Google-Extended, for example, has no effect on Google
23
+ * Search or AI Overviews; it only gates Gemini training).
24
+ *
25
+ * Checkout/account/API paths stay disallowed for every agent.
26
+ */
27
+ const DISALLOWED_PATHS = ['/api/', '/auth/', '/checkout/', '/account/'];
28
+
29
+ const AI_SEARCH_BOTS = [
30
+ 'OAI-SearchBot',
31
+ 'ChatGPT-User',
32
+ 'Claude-SearchBot',
33
+ 'Claude-User',
34
+ 'PerplexityBot',
35
+ 'Perplexity-User',
36
+ 'Bingbot',
37
+ 'Applebot',
38
+ 'Amazonbot',
39
+ ];
40
+
41
+ const AI_TRAINING_BOTS = [
42
+ 'GPTBot',
43
+ 'ClaudeBot',
44
+ 'CCBot',
45
+ 'Google-Extended',
46
+ 'Meta-ExternalAgent',
47
+ 'Applebot-Extended',
48
+ ];
49
+
50
+ export default function robots(): MetadataRoute.Robots {
51
+ const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://example.com';
52
+
53
+ return {
54
+ rules: [
55
+ {
56
+ userAgent: '*',
57
+ allow: '/',
58
+ disallow: DISALLOWED_PATHS,
59
+ },
60
+ {
61
+ userAgent: AI_SEARCH_BOTS,
62
+ allow: '/',
63
+ disallow: DISALLOWED_PATHS,
64
+ },
65
+ {
66
+ userAgent: AI_TRAINING_BOTS,
67
+ allow: '/',
68
+ disallow: DISALLOWED_PATHS,
69
+ },
70
+ ],
71
+ sitemap: `${baseUrl}/sitemap.xml`,
72
+ };
73
+ }