create-brainerce-store 1.77.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 -2
  3. package/messages/he.json +8 -2
  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 +43 -31
  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.77.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
@@ -11,7 +11,6 @@
11
11
  "no": "No",
12
12
  "total": "Total",
13
13
  "subtotal": "Subtotal",
14
- "subtotalExclTax": "Subtotal (excl. tax)",
15
14
  "discount": "Discount",
16
15
  "generalDiscount": "General Discount",
17
16
  "couponDiscount": "Coupon",
@@ -123,6 +122,7 @@
123
122
  "addSelectedToCart": "Add Selected to Cart",
124
123
  "totalPrice": "Total: {price}",
125
124
  "addingAll": "Adding...",
125
+ "whatsInTheBox": "What's in the box",
126
126
  "by": "By"
127
127
  },
128
128
  "reviews": {
@@ -244,6 +244,7 @@
244
244
  "paymentNotConfigured": "Payment Not Configured",
245
245
  "paymentNotConfiguredDesc": "Payment has not been set up for this store yet. Please contact the store owner.",
246
246
  "paymentError": "Payment Error",
247
+ "paymentUnavailable": "Payment is unavailable for this store right now. Please contact the store.",
247
248
  "paymentRedirectBlocked": "Payment redirect blocked for security reasons. Please contact support.",
248
249
  "sandboxTitle": "Sandbox Mode",
249
250
  "sandboxDescription": "This is a test order. No real payment will be processed.",
@@ -434,7 +435,7 @@
434
435
  "orderPrefix": "Order",
435
436
  "productFallback": "Product",
436
437
  "statusDraft": "Draft",
437
- "statusPending": "Pending",
438
+ "statusPending": "Awaiting processing",
438
439
  "statusProcessing": "Processing",
439
440
  "statusOnHold": "On hold",
440
441
  "statusPaid": "Paid",
@@ -516,6 +517,8 @@
516
517
  "loading": "Loading your order...",
517
518
  "errorTitle": "Order not found",
518
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.",
519
522
  "missingCheckoutInfo": "Missing order information.",
520
523
  "returnHome": "Return Home"
521
524
  },
@@ -602,6 +605,9 @@
602
605
  "giftCard": {
603
606
  "placeholder": "Gift card code",
604
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.",
605
611
  "removeFailed": "We could not remove that gift card.",
606
612
  "applied": "Gift card applied — {amount}"
607
613
  }
package/messages/he.json CHANGED
@@ -11,7 +11,6 @@
11
11
  "no": "לא",
12
12
  "total": "סה\"כ",
13
13
  "subtotal": "סיכום ביניים",
14
- "subtotalExclTax": "סיכום ביניים (לפני מע\"מ)",
15
14
  "discount": "הנחה",
16
15
  "generalDiscount": "הנחה כללית",
17
16
  "couponDiscount": "קופון",
@@ -123,6 +122,7 @@
123
122
  "addSelectedToCart": "הוסף הנבחרים לעגלה",
124
123
  "totalPrice": "סה\"כ: {price}",
125
124
  "addingAll": "מוסיף...",
125
+ "whatsInTheBox": "מה יש בקופסה",
126
126
  "by": "מאת"
127
127
  },
128
128
  "reviews": {
@@ -244,6 +244,7 @@
244
244
  "paymentNotConfigured": "תשלום לא מוגדר",
245
245
  "paymentNotConfiguredDesc": "התשלום עדיין לא הוגדר לחנות זו. אנא פנו לבעל החנות.",
246
246
  "paymentError": "שגיאת תשלום",
247
+ "paymentUnavailable": "לא ניתן לשלם בחנות זו כרגע. ניתן לפנות לחנות.",
247
248
  "paymentRedirectBlocked": "הפניית התשלום נחסמה מטעמי אבטחה. אנא פנו לתמיכה.",
248
249
  "sandboxTitle": "מצב בדיקה",
249
250
  "sandboxDescription": "זוהי הזמנת בדיקה. לא יבוצע תשלום אמיתי.",
@@ -434,7 +435,7 @@
434
435
  "orderPrefix": "הזמנה",
435
436
  "productFallback": "מוצר",
436
437
  "statusDraft": "טיוטה",
437
- "statusPending": "ממתין",
438
+ "statusPending": "ממתין לטיפול",
438
439
  "statusProcessing": "בטיפול",
439
440
  "statusOnHold": "מושהה",
440
441
  "statusPaid": "שולם",
@@ -516,6 +517,8 @@
516
517
  "loading": "טוען את ההזמנה שלך...",
517
518
  "errorTitle": "ההזמנה לא נמצאה",
518
519
  "notFound": "לא הצלחנו למצוא הזמנה שתואמת לקישור הזה.",
520
+ "unavailableTitle": "לא הצלחנו לטעון את ההזמנה",
521
+ "unavailableBody": "משהו השתבש אצלנו. ההזמנה שלכם שמורה. נסו שוב בעוד רגע.",
519
522
  "missingCheckoutInfo": "חסר מידע הזמנה.",
520
523
  "returnHome": "חזרה לדף הבית"
521
524
  },
@@ -602,6 +605,9 @@
602
605
  "giftCard": {
603
606
  "placeholder": "קוד כרטיס מתנה",
604
607
  "invalidCode": "לא ניתן להשתמש בקוד הזה בהזמנה הזאת.",
608
+ "applyFailed": "לא הצלחנו לממש את כרטיס המתנה כרגע. נסו שוב.",
609
+ "locked": "התשלום כבר התחיל, אז אי אפשר להוסיף כרטיס מתנה להזמנה הזאת.",
610
+ "lockedHint": "כרטיס מתנה מוסיפים לפני שלב התשלום.",
605
611
  "removeFailed": "לא הצלחנו להסיר את כרטיס המתנה.",
606
612
  "applied": "כרטיס מתנה — {amount}"
607
613
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-brainerce-store",
3
- "version": "1.77.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
  }
@@ -1068,36 +1085,31 @@ function CheckoutContent() {
1068
1085
  {/* Totals */}
1069
1086
  {checkout &&
1070
1087
  (() => {
1071
- // When the store prices include tax (VAT-style), the on-row
1072
- // `checkout.subtotal` is GROSS it already contains the tax.
1073
- // Show the net (tax-excluded) value here, then a separate VAT
1074
- // line below, so the customer sees the breakdown the merchant
1075
- // asked for. Falls back to the raw subtotal when no breakdown
1076
- // is available yet (e.g. shipping address not entered).
1077
- const isInclusive = checkout.taxBreakdown?.pricesIncludeTax === true;
1078
- // `taxBreakdown.subtotal` is the NET of every taxed line, and
1079
- // shipping is one of those lines so it already contains the
1080
- // shipping net. Rendering it and then adding `shippingAmount`
1081
- // below counts shipping twice: a real checkout read
1082
- // 54.06 + 9.99 + 7.93 = 71.98 beside a 61.99 total, three rows
1083
- // that visibly refuse to add up next to the figure being charged.
1088
+ // Every row in this column is GROSS, and the tax row below is
1089
+ // informational. That is how a VAT receipt reads, it is what
1090
+ // `TaxDisplay` already says on its own line ("VAT 18% included"),
1091
+ // and it is exactly what the order-history summary renders one
1092
+ // convention across all three, so a shopper who compares the
1093
+ // checkout against the order they later open sees the same
1094
+ // arithmetic twice.
1095
+ //
1096
+ // This deliberately drops an earlier attempt to show a NET
1097
+ // subtotal for inclusive stores. Two things were wrong with it.
1098
+ // It read `taxBreakdown.subtotal`, which is the net of every
1099
+ // TAXED LINE shipping included and then added
1100
+ // `shippingAmount` below, counting shipping twice: a real
1101
+ // checkout printed 54.06 + 9.99 + 7.93 beside a 61.99 total.
1102
+ // And a net subtotal cannot coexist with a tax row labelled
1103
+ // "included": either the shopper adds the tax, contradicting the
1104
+ // label, or they do not, and the column falls short of the total
1105
+ // by the whole VAT. Subtracting the shipping net back out fixes
1106
+ // the arithmetic and leaves the contradiction, so the row that
1107
+ // has to go is the special case itself.
1084
1108
  //
1085
- // Subtract the shipping net back out so the column reconciles.
1086
- // The shipping row keeps showing the GROSS amount, which is what
1087
- // a shopper recognises from the rate they picked.
1088
- // `shippingNet` comes from the server, never from
1089
- // `shippingAmount` minus a guess: subtracting the GROSS is only
1090
- // correct when shipping is untaxed, and a store that taxes
1091
- // shipping would silently show a short subtotal.
1092
- const shippingNet =
1093
- typeof checkout.taxBreakdown?.shippingNet === 'number'
1094
- ? checkout.taxBreakdown.shippingNet
1095
- : parseFloat(checkout.shippingAmount) || 0;
1096
- const displayedSubtotal =
1097
- isInclusive && typeof checkout.taxBreakdown?.subtotal === 'number'
1098
- ? checkout.taxBreakdown.subtotal - shippingNet
1099
- : parseFloat(checkout.subtotal);
1100
- const subtotalLabel = isInclusive ? tc('subtotalExclTax') : tc('subtotal');
1109
+ // Gross + gross = total, with the VAT stated beneath it:
1110
+ // 52.00 + 9.99 = 61.99, "VAT 18% included 7.93".
1111
+ const displayedSubtotal = parseFloat(checkout.subtotal);
1112
+ const subtotalLabel = tc('subtotal');
1101
1113
  return (
1102
1114
  <div className="border-border space-y-2 border-t pt-4 text-sm">
1103
1115
  <div className="flex items-center justify-between">
@@ -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="/"