create-brainerce-store 1.17.0 → 1.19.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 (65) hide show
  1. package/dist/index.js +31 -9
  2. package/messages/en.json +366 -359
  3. package/messages/he.json +366 -359
  4. package/package.json +45 -45
  5. package/templates/nextjs/base/next.config.ts +31 -31
  6. package/templates/nextjs/base/scripts/fetch-store-info.mjs +81 -81
  7. package/templates/nextjs/base/src/app/.well-known/apple-developer-merchantid-domain-association/route.ts +26 -26
  8. package/templates/nextjs/base/src/app/account/layout.tsx +9 -9
  9. package/templates/nextjs/base/src/app/account/page.tsx +122 -122
  10. package/templates/nextjs/base/src/app/api/auth/logout/route.ts +14 -14
  11. package/templates/nextjs/base/src/app/api/auth/me/route.ts +56 -56
  12. package/templates/nextjs/base/src/app/api/auth/oauth-callback/route.ts +59 -59
  13. package/templates/nextjs/base/src/app/api/auth/reset-callback/route.ts +41 -41
  14. package/templates/nextjs/base/src/app/api/auth/reset-password/route.ts +77 -77
  15. package/templates/nextjs/base/src/app/api/store/[...path]/route.ts +198 -198
  16. package/templates/nextjs/base/src/app/auth/callback/page.tsx +92 -92
  17. package/templates/nextjs/base/src/app/cart/layout.tsx +9 -9
  18. package/templates/nextjs/base/src/app/cart/page.tsx +204 -199
  19. package/templates/nextjs/base/src/app/checkout/layout.tsx +9 -9
  20. package/templates/nextjs/base/src/app/checkout/page.tsx +860 -860
  21. package/templates/nextjs/base/src/app/forgot-password/page.tsx +112 -112
  22. package/templates/nextjs/base/src/app/layout.tsx.ejs +75 -0
  23. package/templates/nextjs/base/src/app/login/layout.tsx +9 -9
  24. package/templates/nextjs/base/src/app/login/page.tsx +59 -59
  25. package/templates/nextjs/base/src/app/order-confirmation/layout.tsx +9 -9
  26. package/templates/nextjs/base/src/app/order-confirmation/page.tsx +254 -254
  27. package/templates/nextjs/base/src/app/products/[slug]/page.tsx +67 -67
  28. package/templates/nextjs/base/src/app/products/[slug]/product-client-section.tsx +486 -486
  29. package/templates/nextjs/base/src/app/products/layout.tsx +18 -18
  30. package/templates/nextjs/base/src/app/products/page.tsx +431 -431
  31. package/templates/nextjs/base/src/app/register/layout.tsx +9 -9
  32. package/templates/nextjs/base/src/app/register/page.tsx +65 -65
  33. package/templates/nextjs/base/src/app/reset-password/page.tsx +132 -132
  34. package/templates/nextjs/base/src/app/robots.ts +14 -14
  35. package/templates/nextjs/base/src/app/sitemap.ts +25 -25
  36. package/templates/nextjs/base/src/app/verify-email/page.tsx +258 -258
  37. package/templates/nextjs/base/src/components/account/address-book.tsx +432 -432
  38. package/templates/nextjs/base/src/components/account/order-history.tsx +350 -350
  39. package/templates/nextjs/base/src/components/auth/oauth-buttons.tsx +137 -137
  40. package/templates/nextjs/base/src/components/auth/register-form.tsx +232 -232
  41. package/templates/nextjs/base/src/components/cart/cart-bundle-offer.tsx +247 -111
  42. package/templates/nextjs/base/src/components/cart/cart-item.tsx +153 -153
  43. package/templates/nextjs/base/src/components/cart/cart-upgrade-banner.tsx +142 -142
  44. package/templates/nextjs/base/src/components/cart/free-shipping-bar.tsx +59 -59
  45. package/templates/nextjs/base/src/components/checkout/checkout-form.tsx +415 -415
  46. package/templates/nextjs/base/src/components/checkout/order-bump-card.tsx +243 -83
  47. package/templates/nextjs/base/src/components/checkout/payment-step.tsx +519 -473
  48. package/templates/nextjs/base/src/components/layout/footer.tsx +41 -41
  49. package/templates/nextjs/base/src/components/layout/header.tsx +336 -336
  50. package/templates/nextjs/base/src/components/layout/language-switcher.tsx.ejs +63 -0
  51. package/templates/nextjs/base/src/components/products/discount-badge.tsx +22 -22
  52. package/templates/nextjs/base/src/components/products/frequently-bought-together.tsx +202 -202
  53. package/templates/nextjs/base/src/components/products/product-card.tsx +218 -218
  54. package/templates/nextjs/base/src/components/products/recommendation-section.tsx +107 -107
  55. package/templates/nextjs/base/src/components/products/stock-badge.tsx +63 -63
  56. package/templates/nextjs/base/src/components/products/variant-selector.tsx +292 -292
  57. package/templates/nextjs/base/src/components/seo/product-json-ld.tsx +72 -72
  58. package/templates/nextjs/base/src/i18n.ts.ejs +21 -0
  59. package/templates/nextjs/base/src/lib/auth.ts +149 -149
  60. package/templates/nextjs/base/src/lib/brainerce.ts.ejs +9 -0
  61. package/templates/nextjs/base/src/lib/translations.ts.ejs +31 -0
  62. package/templates/nextjs/base/src/middleware.ts.ejs +81 -0
  63. package/templates/nextjs/base/src/providers/store-provider.tsx.ejs +41 -0
  64. package/templates/nextjs/base/src/lib/translations.ts +0 -11
  65. package/templates/nextjs/base/src/middleware.ts +0 -25
@@ -1,254 +1,254 @@
1
- 'use client';
2
-
3
- import { Suspense, useEffect, useState } from 'react';
4
- import { useSearchParams } from 'next/navigation';
5
- import Link from 'next/link';
6
- import type { WaitForOrderResult, OrderDownloadLink } from 'brainerce';
7
- import { getClient } from '@/lib/brainerce';
8
- import { useCart } from '@/providers/store-provider';
9
- import { LoadingSpinner } from '@/components/shared/loading-spinner';
10
- import { useTranslations } from '@/lib/translations';
11
-
12
- function OrderConfirmationContent() {
13
- const searchParams = useSearchParams();
14
- const checkoutId = searchParams.get('checkout_id');
15
-
16
- const { refreshCart } = useCart();
17
- const t = useTranslations('orderConfirmation');
18
- const tc = useTranslations('common');
19
- const [result, setResult] = useState<WaitForOrderResult | null>(null);
20
- const [loading, setLoading] = useState(true);
21
- const [error, setError] = useState<string | null>(null);
22
-
23
- useEffect(() => {
24
- if (!checkoutId) {
25
- setError(t('missingCheckoutInfo'));
26
- setLoading(false);
27
- return;
28
- }
29
-
30
- async function waitForOrder() {
31
- try {
32
- const client = getClient();
33
-
34
- // Clear cart state after successful payment
35
- client.handlePaymentSuccess(checkoutId!);
36
- await refreshCart();
37
-
38
- const orderResult = await client.waitForOrder(checkoutId!, {
39
- maxWaitMs: 30000,
40
- });
41
- setResult(orderResult);
42
- } catch (err) {
43
- const message = err instanceof Error ? err.message : 'Failed to confirm order';
44
- setError(message);
45
- } finally {
46
- setLoading(false);
47
- }
48
- }
49
-
50
- waitForOrder();
51
- }, [checkoutId, refreshCart]);
52
-
53
- if (loading) {
54
- return (
55
- <div className="flex min-h-[60vh] flex-col items-center justify-center">
56
- <LoadingSpinner size="lg" />
57
- <p className="text-muted-foreground mt-4">{t('confirming')}</p>
58
- <p className="text-muted-foreground mt-1 text-xs">{t('confirmingHint')}</p>
59
- </div>
60
- );
61
- }
62
-
63
- if (error) {
64
- return (
65
- <div className="mx-auto max-w-2xl px-4 py-16 text-center sm:px-6 lg:px-8">
66
- <svg
67
- className="text-destructive mx-auto mb-4 h-16 w-16"
68
- fill="none"
69
- viewBox="0 0 24 24"
70
- stroke="currentColor"
71
- >
72
- <path
73
- strokeLinecap="round"
74
- strokeLinejoin="round"
75
- strokeWidth={1.5}
76
- 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"
77
- />
78
- </svg>
79
- <h1 className="text-foreground text-2xl font-bold">{t('errorTitle')}</h1>
80
- <p className="text-muted-foreground mt-2">{error}</p>
81
- <p className="text-muted-foreground mt-1 text-sm">{t('errorChargedHint')}</p>
82
- <Link
83
- href="/"
84
- className="bg-primary text-primary-foreground mt-6 inline-flex items-center rounded px-6 py-3 font-medium transition-opacity hover:opacity-90"
85
- >
86
- {t('returnHome')}
87
- </Link>
88
- </div>
89
- );
90
- }
91
-
92
- // Order was created successfully
93
- if (result?.success) {
94
- const orderNumber = result.status.orderNumber;
95
-
96
- return (
97
- <div className="mx-auto max-w-2xl px-4 py-16 text-center sm:px-6 lg:px-8">
98
- <svg
99
- className="text-primary mx-auto mb-4 h-16 w-16"
100
- fill="none"
101
- viewBox="0 0 24 24"
102
- stroke="currentColor"
103
- >
104
- <path
105
- strokeLinecap="round"
106
- strokeLinejoin="round"
107
- strokeWidth={1.5}
108
- d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
109
- />
110
- </svg>
111
-
112
- <h1 className="text-foreground text-2xl font-bold">{t('thankYou')}</h1>
113
-
114
- {orderNumber && (
115
- <p className="text-foreground mt-3 text-lg">
116
- {t('orderNumber')} <span className="font-semibold">{orderNumber}</span>
117
- </p>
118
- )}
119
-
120
- <p className="text-muted-foreground mt-2">{t('confirmationEmail')}</p>
121
-
122
- {result.status.orderId && (
123
- <ConfirmationDownloads orderId={result.status.orderId} checkoutId={checkoutId!} />
124
- )}
125
-
126
- <div className="mt-8 flex flex-col items-center justify-center gap-3 sm:flex-row">
127
- <Link
128
- href="/products"
129
- className="bg-primary text-primary-foreground inline-flex items-center rounded px-6 py-3 font-medium transition-opacity hover:opacity-90"
130
- >
131
- {tc('continueShopping')}
132
- </Link>
133
-
134
- <Link
135
- href="/account"
136
- className="border-border text-foreground hover:bg-muted inline-flex items-center rounded border px-6 py-3 font-medium transition-colors"
137
- >
138
- {t('viewOrders')}
139
- </Link>
140
- </div>
141
- </div>
142
- );
143
- }
144
-
145
- // Order not yet confirmed (polling timed out) - still show success
146
- return (
147
- <div className="mx-auto max-w-2xl px-4 py-16 text-center sm:px-6 lg:px-8">
148
- <svg
149
- className="text-primary mx-auto mb-4 h-16 w-16"
150
- fill="none"
151
- viewBox="0 0 24 24"
152
- stroke="currentColor"
153
- >
154
- <path
155
- strokeLinecap="round"
156
- strokeLinejoin="round"
157
- strokeWidth={1.5}
158
- d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
159
- />
160
- </svg>
161
-
162
- <h1 className="text-foreground text-2xl font-bold">{t('paymentReceived')}</h1>
163
-
164
- <p className="text-muted-foreground mt-2">{t('orderProcessing')}</p>
165
-
166
- <div className="mt-8 flex flex-col items-center justify-center gap-3 sm:flex-row">
167
- <Link
168
- href="/products"
169
- className="bg-primary text-primary-foreground inline-flex items-center rounded px-6 py-3 font-medium transition-opacity hover:opacity-90"
170
- >
171
- {tc('continueShopping')}
172
- </Link>
173
-
174
- <Link
175
- href="/account"
176
- className="border-border text-foreground hover:bg-muted inline-flex items-center rounded border px-6 py-3 font-medium transition-colors"
177
- >
178
- {t('viewOrders')}
179
- </Link>
180
- </div>
181
- </div>
182
- );
183
- }
184
-
185
- function ConfirmationDownloads({ orderId, checkoutId }: { orderId: string; checkoutId: string }) {
186
- const t = useTranslations('orderConfirmation');
187
- const [downloads, setDownloads] = useState<OrderDownloadLink[] | null>(null);
188
-
189
- useEffect(() => {
190
- let cancelled = false;
191
- async function fetchDownloads() {
192
- const client = getClient();
193
- // Retry a few times — the worker may still be writing downloadMeta
194
- for (let attempt = 0; attempt < 3; attempt++) {
195
- try {
196
- const links = await client.getOrderDownloads(orderId, { checkoutId });
197
- if (!cancelled && links.length > 0) {
198
- setDownloads(links);
199
- return;
200
- }
201
- } catch {
202
- // Not all orders have downloads
203
- }
204
- if (attempt < 2 && !cancelled) {
205
- await new Promise((r) => setTimeout(r, 1500));
206
- }
207
- }
208
- }
209
- fetchDownloads();
210
- return () => {
211
- cancelled = true;
212
- };
213
- }, [orderId, checkoutId]);
214
-
215
- if (!downloads || downloads.length === 0) return null;
216
-
217
- return (
218
- <div className="border-border bg-muted/30 mx-auto mt-8 max-w-md rounded-lg border p-6 text-start">
219
- <h3 className="text-foreground mb-3 text-sm font-semibold">{t('yourDownloads')}</h3>
220
- <div className="space-y-2">
221
- {downloads.map((link, idx) => (
222
- <div key={idx} className="flex items-center justify-between gap-3">
223
- <div className="min-w-0 flex-1">
224
- <p className="text-foreground truncate text-sm">{link.fileName}</p>
225
- <p className="text-muted-foreground truncate text-xs">{link.productName}</p>
226
- </div>
227
- <a
228
- href={link.downloadUrl}
229
- target="_blank"
230
- rel="noopener noreferrer"
231
- className="bg-primary text-primary-foreground flex-shrink-0 rounded px-3 py-1.5 text-xs font-medium hover:opacity-90"
232
- >
233
- {t('download')}
234
- </a>
235
- </div>
236
- ))}
237
- </div>
238
- </div>
239
- );
240
- }
241
-
242
- export default function OrderConfirmationPage() {
243
- return (
244
- <Suspense
245
- fallback={
246
- <div className="flex min-h-[60vh] items-center justify-center">
247
- <LoadingSpinner size="lg" />
248
- </div>
249
- }
250
- >
251
- <OrderConfirmationContent />
252
- </Suspense>
253
- );
254
- }
1
+ 'use client';
2
+
3
+ import { Suspense, useEffect, useState } from 'react';
4
+ import { useSearchParams } from 'next/navigation';
5
+ import Link from 'next/link';
6
+ import type { WaitForOrderResult, OrderDownloadLink } from 'brainerce';
7
+ import { getClient } from '@/lib/brainerce';
8
+ import { useCart } from '@/providers/store-provider';
9
+ import { LoadingSpinner } from '@/components/shared/loading-spinner';
10
+ import { useTranslations } from '@/lib/translations';
11
+
12
+ function OrderConfirmationContent() {
13
+ const searchParams = useSearchParams();
14
+ const checkoutId = searchParams.get('checkout_id');
15
+
16
+ const { refreshCart } = useCart();
17
+ const t = useTranslations('orderConfirmation');
18
+ const tc = useTranslations('common');
19
+ const [result, setResult] = useState<WaitForOrderResult | null>(null);
20
+ const [loading, setLoading] = useState(true);
21
+ const [error, setError] = useState<string | null>(null);
22
+
23
+ useEffect(() => {
24
+ if (!checkoutId) {
25
+ setError(t('missingCheckoutInfo'));
26
+ setLoading(false);
27
+ return;
28
+ }
29
+
30
+ async function waitForOrder() {
31
+ try {
32
+ const client = getClient();
33
+
34
+ // Clear cart state after successful payment
35
+ client.handlePaymentSuccess(checkoutId!);
36
+ await refreshCart();
37
+
38
+ const orderResult = await client.waitForOrder(checkoutId!, {
39
+ maxWaitMs: 30000,
40
+ });
41
+ setResult(orderResult);
42
+ } catch (err) {
43
+ const message = err instanceof Error ? err.message : 'Failed to confirm order';
44
+ setError(message);
45
+ } finally {
46
+ setLoading(false);
47
+ }
48
+ }
49
+
50
+ waitForOrder();
51
+ }, [checkoutId, refreshCart]);
52
+
53
+ if (loading) {
54
+ return (
55
+ <div className="flex min-h-[60vh] flex-col items-center justify-center">
56
+ <LoadingSpinner size="lg" />
57
+ <p className="text-muted-foreground mt-4">{t('confirming')}</p>
58
+ <p className="text-muted-foreground mt-1 text-xs">{t('confirmingHint')}</p>
59
+ </div>
60
+ );
61
+ }
62
+
63
+ if (error) {
64
+ return (
65
+ <div className="mx-auto max-w-2xl px-4 py-16 text-center sm:px-6 lg:px-8">
66
+ <svg
67
+ className="text-destructive mx-auto mb-4 h-16 w-16"
68
+ fill="none"
69
+ viewBox="0 0 24 24"
70
+ stroke="currentColor"
71
+ >
72
+ <path
73
+ strokeLinecap="round"
74
+ strokeLinejoin="round"
75
+ strokeWidth={1.5}
76
+ 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"
77
+ />
78
+ </svg>
79
+ <h1 className="text-foreground text-2xl font-bold">{t('errorTitle')}</h1>
80
+ <p className="text-muted-foreground mt-2">{error}</p>
81
+ <p className="text-muted-foreground mt-1 text-sm">{t('errorChargedHint')}</p>
82
+ <Link
83
+ href="/"
84
+ className="bg-primary text-primary-foreground mt-6 inline-flex items-center rounded px-6 py-3 font-medium transition-opacity hover:opacity-90"
85
+ >
86
+ {t('returnHome')}
87
+ </Link>
88
+ </div>
89
+ );
90
+ }
91
+
92
+ // Order was created successfully
93
+ if (result?.success) {
94
+ const orderNumber = result.status.orderNumber;
95
+
96
+ return (
97
+ <div className="mx-auto max-w-2xl px-4 py-16 text-center sm:px-6 lg:px-8">
98
+ <svg
99
+ className="text-primary mx-auto mb-4 h-16 w-16"
100
+ fill="none"
101
+ viewBox="0 0 24 24"
102
+ stroke="currentColor"
103
+ >
104
+ <path
105
+ strokeLinecap="round"
106
+ strokeLinejoin="round"
107
+ strokeWidth={1.5}
108
+ d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
109
+ />
110
+ </svg>
111
+
112
+ <h1 className="text-foreground text-2xl font-bold">{t('thankYou')}</h1>
113
+
114
+ {orderNumber && (
115
+ <p className="text-foreground mt-3 text-lg">
116
+ {t('orderNumber')} <span className="font-semibold">{orderNumber}</span>
117
+ </p>
118
+ )}
119
+
120
+ <p className="text-muted-foreground mt-2">{t('confirmationEmail')}</p>
121
+
122
+ {result.status.orderId && (
123
+ <ConfirmationDownloads orderId={result.status.orderId} checkoutId={checkoutId!} />
124
+ )}
125
+
126
+ <div className="mt-8 flex flex-col items-center justify-center gap-3 sm:flex-row">
127
+ <Link
128
+ href="/products"
129
+ className="bg-primary text-primary-foreground inline-flex items-center rounded px-6 py-3 font-medium transition-opacity hover:opacity-90"
130
+ >
131
+ {tc('continueShopping')}
132
+ </Link>
133
+
134
+ <Link
135
+ href="/account"
136
+ className="border-border text-foreground hover:bg-muted inline-flex items-center rounded border px-6 py-3 font-medium transition-colors"
137
+ >
138
+ {t('viewOrders')}
139
+ </Link>
140
+ </div>
141
+ </div>
142
+ );
143
+ }
144
+
145
+ // Order not yet confirmed (polling timed out) - still show success
146
+ return (
147
+ <div className="mx-auto max-w-2xl px-4 py-16 text-center sm:px-6 lg:px-8">
148
+ <svg
149
+ className="text-primary mx-auto mb-4 h-16 w-16"
150
+ fill="none"
151
+ viewBox="0 0 24 24"
152
+ stroke="currentColor"
153
+ >
154
+ <path
155
+ strokeLinecap="round"
156
+ strokeLinejoin="round"
157
+ strokeWidth={1.5}
158
+ d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
159
+ />
160
+ </svg>
161
+
162
+ <h1 className="text-foreground text-2xl font-bold">{t('paymentReceived')}</h1>
163
+
164
+ <p className="text-muted-foreground mt-2">{t('orderProcessing')}</p>
165
+
166
+ <div className="mt-8 flex flex-col items-center justify-center gap-3 sm:flex-row">
167
+ <Link
168
+ href="/products"
169
+ className="bg-primary text-primary-foreground inline-flex items-center rounded px-6 py-3 font-medium transition-opacity hover:opacity-90"
170
+ >
171
+ {tc('continueShopping')}
172
+ </Link>
173
+
174
+ <Link
175
+ href="/account"
176
+ className="border-border text-foreground hover:bg-muted inline-flex items-center rounded border px-6 py-3 font-medium transition-colors"
177
+ >
178
+ {t('viewOrders')}
179
+ </Link>
180
+ </div>
181
+ </div>
182
+ );
183
+ }
184
+
185
+ function ConfirmationDownloads({ orderId, checkoutId }: { orderId: string; checkoutId: string }) {
186
+ const t = useTranslations('orderConfirmation');
187
+ const [downloads, setDownloads] = useState<OrderDownloadLink[] | null>(null);
188
+
189
+ useEffect(() => {
190
+ let cancelled = false;
191
+ async function fetchDownloads() {
192
+ const client = getClient();
193
+ // Retry a few times — the worker may still be writing downloadMeta
194
+ for (let attempt = 0; attempt < 3; attempt++) {
195
+ try {
196
+ const links = await client.getOrderDownloads(orderId, { checkoutId });
197
+ if (!cancelled && links.length > 0) {
198
+ setDownloads(links);
199
+ return;
200
+ }
201
+ } catch {
202
+ // Not all orders have downloads
203
+ }
204
+ if (attempt < 2 && !cancelled) {
205
+ await new Promise((r) => setTimeout(r, 1500));
206
+ }
207
+ }
208
+ }
209
+ fetchDownloads();
210
+ return () => {
211
+ cancelled = true;
212
+ };
213
+ }, [orderId, checkoutId]);
214
+
215
+ if (!downloads || downloads.length === 0) return null;
216
+
217
+ return (
218
+ <div className="border-border bg-muted/30 mx-auto mt-8 max-w-md rounded-lg border p-6 text-start">
219
+ <h3 className="text-foreground mb-3 text-sm font-semibold">{t('yourDownloads')}</h3>
220
+ <div className="space-y-2">
221
+ {downloads.map((link, idx) => (
222
+ <div key={idx} className="flex items-center justify-between gap-3">
223
+ <div className="min-w-0 flex-1">
224
+ <p className="text-foreground truncate text-sm">{link.fileName}</p>
225
+ <p className="text-muted-foreground truncate text-xs">{link.productName}</p>
226
+ </div>
227
+ <a
228
+ href={link.downloadUrl}
229
+ target="_blank"
230
+ rel="noopener noreferrer"
231
+ className="bg-primary text-primary-foreground flex-shrink-0 rounded px-3 py-1.5 text-xs font-medium hover:opacity-90"
232
+ >
233
+ {t('download')}
234
+ </a>
235
+ </div>
236
+ ))}
237
+ </div>
238
+ </div>
239
+ );
240
+ }
241
+
242
+ export default function OrderConfirmationPage() {
243
+ return (
244
+ <Suspense
245
+ fallback={
246
+ <div className="flex min-h-[60vh] items-center justify-center">
247
+ <LoadingSpinner size="lg" />
248
+ </div>
249
+ }
250
+ >
251
+ <OrderConfirmationContent />
252
+ </Suspense>
253
+ );
254
+ }
@@ -1,67 +1,67 @@
1
- import type { Metadata } from 'next';
2
- import { notFound } from 'next/navigation';
3
- import { getServerClient } from '@/lib/brainerce';
4
- import { ProductJsonLd } from '@/components/seo/product-json-ld';
5
- import { ProductClientSection } from './product-client-section';
6
-
7
- type Props = {
8
- params: Promise<{ slug: string }>;
9
- };
10
-
11
- export async function generateMetadata({ params }: Props): Promise<Metadata> {
12
- const { slug } = await params;
13
-
14
- try {
15
- const client = getServerClient();
16
- const product = await client.getProductBySlug(slug);
17
- const imageUrl = product.images?.[0]?.url;
18
- const description = product.description?.substring(0, 160) || product.name;
19
-
20
- return {
21
- title: product.name,
22
- description,
23
- alternates: {
24
- canonical: `/products/${slug}`,
25
- },
26
- openGraph: {
27
- title: product.name,
28
- description,
29
- images: imageUrl ? [{ url: imageUrl, alt: product.name }] : [],
30
- type: 'website',
31
- },
32
- twitter: {
33
- card: 'summary_large_image',
34
- title: product.name,
35
- description,
36
- images: imageUrl ? [imageUrl] : [],
37
- },
38
- };
39
- } catch {
40
- return {
41
- title: 'Product not found',
42
- };
43
- }
44
- }
45
-
46
- export default async function ProductDetailPage({ params }: Props) {
47
- const { slug } = await params;
48
-
49
- let product;
50
- try {
51
- const client = getServerClient();
52
- product = await client.getProductBySlug(slug);
53
- } catch {
54
- notFound();
55
- }
56
-
57
- const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || '';
58
- const productUrl = `${baseUrl}/products/${slug}`;
59
- const currency = process.env.NEXT_PUBLIC_STORE_CURRENCY || 'USD';
60
-
61
- return (
62
- <>
63
- <ProductJsonLd product={product} url={productUrl} currency={currency} />
64
- <ProductClientSection product={product} />
65
- </>
66
- );
67
- }
1
+ import type { Metadata } from 'next';
2
+ import { notFound } from 'next/navigation';
3
+ import { getServerClient } from '@/lib/brainerce';
4
+ import { ProductJsonLd } from '@/components/seo/product-json-ld';
5
+ import { ProductClientSection } from './product-client-section';
6
+
7
+ type Props = {
8
+ params: Promise<{ slug: string }>;
9
+ };
10
+
11
+ export async function generateMetadata({ params }: Props): Promise<Metadata> {
12
+ const { slug } = await params;
13
+
14
+ try {
15
+ const client = getServerClient();
16
+ const product = await client.getProductBySlug(slug);
17
+ const imageUrl = product.images?.[0]?.url;
18
+ const description = product.description?.substring(0, 160) || product.name;
19
+
20
+ return {
21
+ title: product.name,
22
+ description,
23
+ alternates: {
24
+ canonical: `/products/${slug}`,
25
+ },
26
+ openGraph: {
27
+ title: product.name,
28
+ description,
29
+ images: imageUrl ? [{ url: imageUrl, alt: product.name }] : [],
30
+ type: 'website',
31
+ },
32
+ twitter: {
33
+ card: 'summary_large_image',
34
+ title: product.name,
35
+ description,
36
+ images: imageUrl ? [imageUrl] : [],
37
+ },
38
+ };
39
+ } catch {
40
+ return {
41
+ title: 'Product not found',
42
+ };
43
+ }
44
+ }
45
+
46
+ export default async function ProductDetailPage({ params }: Props) {
47
+ const { slug } = await params;
48
+
49
+ let product;
50
+ try {
51
+ const client = getServerClient();
52
+ product = await client.getProductBySlug(slug);
53
+ } catch {
54
+ notFound();
55
+ }
56
+
57
+ const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || '';
58
+ const productUrl = `${baseUrl}/products/${slug}`;
59
+ const currency = process.env.NEXT_PUBLIC_STORE_CURRENCY || 'USD';
60
+
61
+ return (
62
+ <>
63
+ <ProductJsonLd product={product} url={productUrl} currency={currency} />
64
+ <ProductClientSection product={product} />
65
+ </>
66
+ );
67
+ }