create-brainerce-store 1.78.0 → 1.79.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 (31) hide show
  1. package/dist/index.js +12 -2
  2. package/messages/en.json +8 -1
  3. package/messages/he.json +8 -1
  4. package/package.json +1 -1
  5. package/templates/nextjs/base/.eslintrc.json +2 -0
  6. package/templates/nextjs/base/AGENTS.md.ejs +23 -5
  7. package/templates/nextjs/base/CLAUDE.md.ejs +23 -5
  8. package/templates/nextjs/base/src/app/checkout/page.tsx +19 -2
  9. package/templates/nextjs/base/src/app/order-status/page.tsx +18 -3
  10. package/templates/nextjs/base/src/app/products/[slug]/page.tsx +220 -214
  11. package/templates/nextjs/base/src/components/account/order-history.tsx +11 -4
  12. package/templates/nextjs/base/src/components/checkout/payment-step.tsx +14 -2
  13. package/templates/nextjs/base/src/core/hooks/use-product-page.ts +343 -328
  14. package/templates/nextjs/base/src/core/lib/kit.ts +88 -0
  15. package/templates/nextjs/base/src/ui/cart/gift-card-input.tsx +87 -12
  16. package/templates/nextjs/base/src/ui/product/frequently-bought-together.tsx +205 -197
  17. package/templates/nextjs/base/src/ui/product/product-card.tsx +230 -221
  18. package/templates/nextjs/base/src/ui/product/product-client-section.tsx +524 -493
  19. package/templates/nextjs/base/src/ui/product/recommendation-section.tsx +117 -108
  20. package/templates/nextjs/base/src/ui/product/stock-badge.tsx +23 -3
  21. package/templates/nextjs/designs/atelier/ui/product/frequently-bought-together.tsx +210 -202
  22. package/templates/nextjs/designs/atelier/ui/product/product-card.tsx +251 -242
  23. package/templates/nextjs/designs/atelier/ui/product/product-client-section.tsx +540 -509
  24. package/templates/nextjs/designs/atelier/ui/product/recommendation-section.tsx +110 -101
  25. package/templates/nextjs/designs/atelier/ui/product/stock-badge.tsx +22 -2
  26. package/templates/nextjs/ui-canvas/cart/gift-card-input.tsx +41 -11
  27. package/templates/nextjs/ui-canvas/product/frequently-bought-together.tsx +182 -174
  28. package/templates/nextjs/ui-canvas/product/product-card.tsx +174 -165
  29. package/templates/nextjs/ui-canvas/product/product-client-section.tsx +31 -0
  30. package/templates/nextjs/ui-canvas/product/recommendation-section.tsx +114 -105
  31. package/templates/nextjs/ui-canvas/product/stock-badge.tsx +22 -2
package/dist/index.js CHANGED
@@ -31,7 +31,7 @@ var require_package = __commonJS({
31
31
  "package.json"(exports2, module2) {
32
32
  module2.exports = {
33
33
  name: "create-brainerce-store",
34
- version: "1.78.0",
34
+ version: "1.79.0",
35
35
  description: "Scaffold a production-ready e-commerce storefront connected to Brainerce",
36
36
  bin: {
37
37
  "create-brainerce-store": "dist/index.js"
@@ -305,7 +305,17 @@ var BRAINERCE_RUNTIME_DEPS = Object.freeze({
305
305
  // bare 0, so an older SDK fails type-check at scaffold time. This floor is
306
306
  // load-bearing now; it is no longer only about provably getting the
307
307
  // corrected README.
308
- brainerce: "^2.4.0",
308
+ //
309
+ // 2.5.0 is the first SDK that carries the KIT fields, and this floor is
310
+ // load-bearing for exactly the reason the ones above are. `2.4.0` already
311
+ // typed `Product.type` as `'SIMPLE' | 'VARIABLE' | 'KIT'` — so
312
+ // `product.type === 'KIT'` compiles against it and looks fine — but it has no
313
+ // `kitComponents` and no `kitAvailable`. The scaffolded product card reads
314
+ // `product.kitAvailable` and the product page reads `product.kitComponents`
315
+ // off an SDK-typed `Product`, so on an older SDK a scaffolded store fails
316
+ // type-check at creation. Verified against the published 2.4.0 tarball: zero
317
+ // files mention either field.
318
+ brainerce: "^2.5.0",
309
319
  "isomorphic-dompurify": "^3.8.0"
310
320
  });
311
321
 
package/messages/en.json CHANGED
@@ -122,6 +122,7 @@
122
122
  "addSelectedToCart": "Add Selected to Cart",
123
123
  "totalPrice": "Total: {price}",
124
124
  "addingAll": "Adding...",
125
+ "whatsInTheBox": "What's in the box",
125
126
  "by": "By"
126
127
  },
127
128
  "reviews": {
@@ -243,6 +244,7 @@
243
244
  "paymentNotConfigured": "Payment Not Configured",
244
245
  "paymentNotConfiguredDesc": "Payment has not been set up for this store yet. Please contact the store owner.",
245
246
  "paymentError": "Payment Error",
247
+ "paymentUnavailable": "Payment is unavailable for this store right now. Please contact the store.",
246
248
  "paymentRedirectBlocked": "Payment redirect blocked for security reasons. Please contact support.",
247
249
  "sandboxTitle": "Sandbox Mode",
248
250
  "sandboxDescription": "This is a test order. No real payment will be processed.",
@@ -433,7 +435,7 @@
433
435
  "orderPrefix": "Order",
434
436
  "productFallback": "Product",
435
437
  "statusDraft": "Draft",
436
- "statusPending": "Pending",
438
+ "statusPending": "Awaiting processing",
437
439
  "statusProcessing": "Processing",
438
440
  "statusOnHold": "On hold",
439
441
  "statusPaid": "Paid",
@@ -515,6 +517,8 @@
515
517
  "loading": "Loading your order...",
516
518
  "errorTitle": "Order not found",
517
519
  "notFound": "We couldn't find an order matching this link.",
520
+ "unavailableTitle": "We could not load your order",
521
+ "unavailableBody": "Something went wrong on our side. Your order is safe. Please try again in a moment.",
518
522
  "missingCheckoutInfo": "Missing order information.",
519
523
  "returnHome": "Return Home"
520
524
  },
@@ -601,6 +605,9 @@
601
605
  "giftCard": {
602
606
  "placeholder": "Gift card code",
603
607
  "invalidCode": "That gift card code cannot be used on this order.",
608
+ "applyFailed": "We could not apply that gift card right now. Please try again.",
609
+ "locked": "Payment has already started, so this order can no longer take a gift card.",
610
+ "lockedHint": "A gift card is added before the payment step.",
604
611
  "removeFailed": "We could not remove that gift card.",
605
612
  "applied": "Gift card applied — {amount}"
606
613
  }
package/messages/he.json CHANGED
@@ -122,6 +122,7 @@
122
122
  "addSelectedToCart": "הוסף הנבחרים לעגלה",
123
123
  "totalPrice": "סה\"כ: {price}",
124
124
  "addingAll": "מוסיף...",
125
+ "whatsInTheBox": "מה יש בקופסה",
125
126
  "by": "מאת"
126
127
  },
127
128
  "reviews": {
@@ -243,6 +244,7 @@
243
244
  "paymentNotConfigured": "תשלום לא מוגדר",
244
245
  "paymentNotConfiguredDesc": "התשלום עדיין לא הוגדר לחנות זו. אנא פנו לבעל החנות.",
245
246
  "paymentError": "שגיאת תשלום",
247
+ "paymentUnavailable": "לא ניתן לשלם בחנות זו כרגע. ניתן לפנות לחנות.",
246
248
  "paymentRedirectBlocked": "הפניית התשלום נחסמה מטעמי אבטחה. אנא פנו לתמיכה.",
247
249
  "sandboxTitle": "מצב בדיקה",
248
250
  "sandboxDescription": "זוהי הזמנת בדיקה. לא יבוצע תשלום אמיתי.",
@@ -433,7 +435,7 @@
433
435
  "orderPrefix": "הזמנה",
434
436
  "productFallback": "מוצר",
435
437
  "statusDraft": "טיוטה",
436
- "statusPending": "ממתין",
438
+ "statusPending": "ממתין לטיפול",
437
439
  "statusProcessing": "בטיפול",
438
440
  "statusOnHold": "מושהה",
439
441
  "statusPaid": "שולם",
@@ -515,6 +517,8 @@
515
517
  "loading": "טוען את ההזמנה שלך...",
516
518
  "errorTitle": "ההזמנה לא נמצאה",
517
519
  "notFound": "לא הצלחנו למצוא הזמנה שתואמת לקישור הזה.",
520
+ "unavailableTitle": "לא הצלחנו לטעון את ההזמנה",
521
+ "unavailableBody": "משהו השתבש אצלנו. ההזמנה שלכם שמורה. נסו שוב בעוד רגע.",
518
522
  "missingCheckoutInfo": "חסר מידע הזמנה.",
519
523
  "returnHome": "חזרה לדף הבית"
520
524
  },
@@ -601,6 +605,9 @@
601
605
  "giftCard": {
602
606
  "placeholder": "קוד כרטיס מתנה",
603
607
  "invalidCode": "לא ניתן להשתמש בקוד הזה בהזמנה הזאת.",
608
+ "applyFailed": "לא הצלחנו לממש את כרטיס המתנה כרגע. נסו שוב.",
609
+ "locked": "התשלום כבר התחיל, אז אי אפשר להוסיף כרטיס מתנה להזמנה הזאת.",
610
+ "lockedHint": "כרטיס מתנה מוסיפים לפני שלב התשלום.",
604
611
  "removeFailed": "לא הצלחנו להסיר את כרטיס המתנה.",
605
612
  "applied": "כרטיס מתנה — {amount}"
606
613
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-brainerce-store",
3
- "version": "1.78.0",
3
+ "version": "1.79.0",
4
4
  "description": "Scaffold a production-ready e-commerce storefront connected to Brainerce",
5
5
  "bin": {
6
6
  "create-brainerce-store": "dist/index.js"
@@ -14,6 +14,8 @@
14
14
  "!@/core/lib/auth",
15
15
  "!@/core/lib/brainerce",
16
16
  "!@/core/lib/brainerce.server",
17
+ "!@/core/lib/capabilities",
18
+ "!@/core/lib/display-price",
17
19
  "!@/core/lib/image-hosts",
18
20
  "!@/core/lib/navigation",
19
21
  "!@/core/lib/product-options",
@@ -91,11 +91,12 @@ and handlers, never JSX. Never hardcode catalog content.
91
91
 
92
92
  ### Rebuilding the look is free. Dropping a feature is not.
93
93
 
94
- Some files under `src/ui/` are the ONLY place a mandatory checklist entry
95
- exists in this project. Delete one and the capability leaves the store with
96
- nothing to notice it by: no type error, no console warning, and no visual hole
97
- either, because these components auto-hide while the merchant has the feature
98
- switched off, so a deleted one and an idle one look identical on the page.
94
+ Some files under `src/ui/` (and one under `src/core/`) are the ONLY place a
95
+ mandatory checklist entry exists in this project. Delete one and the capability
96
+ leaves the store with nothing to notice it by: no type error, no console
97
+ warning, and no visual hole either, because these components auto-hide while
98
+ the merchant has the feature switched off, so a deleted one and an idle one
99
+ look identical on the page.
99
100
  These are the usual casualties of a redesign, because no art-direction brief
100
101
  asks for them by name:
101
102
 
@@ -103,6 +104,8 @@ asks for them by name:
103
104
  product/ back-in-stock-form · customization-fields · modifier-group-selector
104
105
  review-form · reviews-section · frequently-bought-together
105
106
  discount-badge · stock-badge
107
+ product-client-section → the KIT "what's in the box" block
108
+ core/ lib/kit.ts → the KIT stock resolver every badge and button reads
106
109
  cart/ reservation-countdown · coupon-input · cart-upgrade-banner
107
110
  cart-bundle-offer · tax-estimate-line
108
111
  home/ discount-banner-strip
@@ -115,6 +118,21 @@ split them in half: all fine. What has to survive a rebuild is the SDK call
115
118
  each one makes and the states it handles (loading, empty, failed, and the
116
119
  merchant-has-it-off state that renders nothing).
117
120
 
121
+ ⛔ **The two KIT entries are the ones a redesign silently breaks.** A `KIT` is
122
+ one product assembled from other catalog products, and three things have to
123
+ survive whatever you do to the markup. (1) It is added to the cart as ONE line
124
+ using the kit's own `productId`, with no `variantId` and no modifier
125
+ selections; never loop `product.kitComponents` into `addToCart`, which charges
126
+ the shopper twice and reserves the stock twice. (2) A kit has NO `inventory`
127
+ object, so every badge and buy button must go through `resolveStockInfo` /
128
+ `canPurchaseProduct` from `@/core/lib/kit`; reading `product.inventory`
129
+ directly gives a kit a red "out of stock" badge beside an ENABLED buy button,
130
+ or worse, renders a sold-out kit as buyable. (3) `kitComponents` is display
131
+ only and arrives on the by-slug read alone, never on list responses. And do not
132
+ assume a kit price is fixed: `kitPricingMode` may be `SUM` or
133
+ `SUM_MINUS_PERCENT`, in which case the price is recomputed from the components
134
+ on every read, so never cache one.
135
+
118
136
  The list is short on purpose and it is NOT the specification. It names the
119
137
  files people lose, not every mandatory entry. `get-required-features` is the
120
138
  specification, and step 1 of "Verify before declaring done" is what actually
@@ -93,11 +93,12 @@ return state and handlers, never JSX. Never hardcode catalog content.
93
93
 
94
94
  ### Rebuilding the look is free. Dropping a feature is not.
95
95
 
96
- Some files under `src/ui/` are the ONLY place a mandatory checklist entry
97
- exists in this project. Delete one and the capability leaves the store with
98
- nothing to notice it by: no type error, no console warning, and no visual hole
99
- either, because these components auto-hide while the merchant has the feature
100
- switched off, so a deleted one and an idle one look identical on the page.
96
+ Some files under `src/ui/` (and one under `src/core/`) are the ONLY place a
97
+ mandatory checklist entry exists in this project. Delete one and the capability
98
+ leaves the store with nothing to notice it by: no type error, no console
99
+ warning, and no visual hole either, because these components auto-hide while
100
+ the merchant has the feature switched off, so a deleted one and an idle one
101
+ look identical on the page.
101
102
  These are the usual casualties of a redesign, because no art-direction brief
102
103
  asks for them by name:
103
104
 
@@ -105,6 +106,8 @@ asks for them by name:
105
106
  product/ back-in-stock-form · customization-fields · modifier-group-selector
106
107
  review-form · reviews-section · frequently-bought-together
107
108
  discount-badge · stock-badge
109
+ product-client-section → the KIT "what's in the box" block
110
+ core/ lib/kit.ts → the KIT stock resolver every badge and button reads
108
111
  cart/ reservation-countdown · coupon-input · cart-upgrade-banner
109
112
  cart-bundle-offer · tax-estimate-line
110
113
  home/ discount-banner-strip
@@ -117,6 +120,21 @@ split them in half: all fine. What has to survive a rebuild is the SDK call
117
120
  each one makes and the states it handles (loading, empty, failed, and the
118
121
  merchant-has-it-off state that renders nothing).
119
122
 
123
+ ⛔ **The two KIT entries are the ones a redesign silently breaks.** A `KIT` is
124
+ one product assembled from other catalog products, and three things have to
125
+ survive whatever you do to the markup. (1) It is added to the cart as ONE line
126
+ using the kit's own `productId`, with no `variantId` and no modifier
127
+ selections; never loop `product.kitComponents` into `addToCart`, which charges
128
+ the shopper twice and reserves the stock twice. (2) A kit has NO `inventory`
129
+ object, so every badge and buy button must go through `resolveStockInfo` /
130
+ `canPurchaseProduct` from `@/core/lib/kit`; reading `product.inventory`
131
+ directly gives a kit a red "out of stock" badge beside an ENABLED buy button,
132
+ or worse, renders a sold-out kit as buyable. (3) `kitComponents` is display
133
+ only and arrives on the by-slug read alone, never on list responses. And do not
134
+ assume a kit price is fixed: `kitPricingMode` may be `SUM` or
135
+ `SUM_MINUS_PERCENT`, in which case the price is recomputed from the components
136
+ on every read, so never cache one.
137
+
120
138
  The list is short on purpose and it is NOT the specification. It names the
121
139
  files people lose, not every mandatory entry. `get-required-features` is the
122
140
  specification, and step 1 of "Verify before declaring done" is what actually
@@ -132,8 +132,18 @@ function CheckoutContent() {
132
132
  try {
133
133
  setPlacing(true);
134
134
  setPlaceError(null);
135
- const { orderId } = await getClient().completeCheckout(checkout.id);
136
- router.push(`/order-confirmation?orderId=${encodeURIComponent(orderId)}`);
135
+ await getClient().completeCheckout(checkout.id);
136
+ // `checkout_id`, NOT the order id. The confirmation page reads
137
+ // `checkout_id` and every other caller (payment-step, all three of its
138
+ // redirect paths) sends that. Sending the order id instead meant the page
139
+ // found no parameter it recognised, made zero requests, and rendered its
140
+ // "missing payment information" state over an order that had just been
141
+ // placed and paid for — with the cart still full.
142
+ //
143
+ // The order id `completeCheckout` returns is deliberately not used: the
144
+ // page resolves the order from the checkout, which is also what makes a
145
+ // reload of the confirmation URL work.
146
+ router.push(`/order-confirmation?checkout_id=${encodeURIComponent(checkout.id)}`);
137
147
  } catch (err) {
138
148
  // Said out loud. A silent failure here leaves someone staring at a button
139
149
  // that did nothing, holding a card they believe they have already spent.
@@ -1056,6 +1066,13 @@ function CheckoutContent() {
1056
1066
  className="mt-3"
1057
1067
  checkoutId={checkout.id}
1058
1068
  tenders={checkout.tenders ?? []}
1069
+ // Mirrors the server's own guard. Once an intent exists
1070
+ // the checkout takes no more tender changes, so the field
1071
+ // must stop offering them.
1072
+ locked={
1073
+ checkout.status === 'PAYMENT_PENDING' ||
1074
+ checkout.status === 'PAYMENT_PROCESSING'
1075
+ }
1059
1076
  formatAmount={(value) =>
1060
1077
  formatPrice(parseFloat(value), { currency }) as string
1061
1078
  }
@@ -4,6 +4,7 @@ import { Suspense, useEffect, useState } from 'react';
4
4
  import { useSearchParams } from 'next/navigation';
5
5
  import { Link } from '@/core/lib/navigation';
6
6
  import type { Order } from 'brainerce';
7
+ import { BrainerceError } from 'brainerce';
7
8
  import { getClient } from '@/core/lib/brainerce';
8
9
  import { LoadingSpinner } from '@/ui/shared/loading-spinner';
9
10
  import { OrderHistory } from '@/components/account/order-history';
@@ -23,6 +24,8 @@ function OrderStatusContent() {
23
24
  const [order, setOrder] = useState<Order | null>(null);
24
25
  const [loading, setLoading] = useState(true);
25
26
  const [error, setError] = useState<string | null>(null);
27
+ /** Separates our own outage from a link that names no order. */
28
+ const [unavailable, setUnavailable] = useState(false);
26
29
 
27
30
  useEffect(() => {
28
31
  if (!checkoutId) {
@@ -39,8 +42,18 @@ function OrderStatusContent() {
39
42
  if (!cancelled) setOrder(result);
40
43
  } catch (err) {
41
44
  if (!cancelled) {
42
- const message = err instanceof Error ? err.message : t('notFound');
43
- setError(message);
45
+ // "We could not find your order" and "we could not reach the server"
46
+ // are different facts, and only one of them is about the customer.
47
+ // A backend outage rendered as ORDER NOT FOUND on the page people
48
+ // open from their receipt email -- seen for real during a redeploy.
49
+ // Telling someone the order they just paid for does not exist is
50
+ // worse than telling them nothing.
51
+ //
52
+ // A 4xx really is "no order for this link". Anything else is us.
53
+ const missing = err instanceof BrainerceError && err.statusCode < 500;
54
+ if (!missing) console.error('[order-status] could not load order', err);
55
+ setUnavailable(!missing);
56
+ setError(missing ? t('notFound') : t('unavailableBody'));
44
57
  }
45
58
  } finally {
46
59
  if (!cancelled) setLoading(false);
@@ -78,7 +91,9 @@ function OrderStatusContent() {
78
91
  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
92
  />
80
93
  </svg>
81
- <h1 className="text-foreground text-2xl font-bold">{t('errorTitle')}</h1>
94
+ <h1 className="text-foreground text-2xl font-bold">
95
+ {unavailable ? t('unavailableTitle') : t('errorTitle')}
96
+ </h1>
82
97
  <p className="text-muted-foreground mt-2">{error || t('notFound')}</p>
83
98
  <Link
84
99
  href="/"