create-brainerce-store 1.18.0 → 1.20.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 (67) hide show
  1. package/LICENSE +0 -0
  2. package/dist/index.js +31 -9
  3. package/messages/en.json +366 -362
  4. package/messages/he.json +366 -362
  5. package/package.json +8 -8
  6. package/templates/nextjs/base/next.config.ts +31 -31
  7. package/templates/nextjs/base/scripts/fetch-store-info.mjs +81 -81
  8. package/templates/nextjs/base/src/app/.well-known/apple-developer-merchantid-domain-association/route.ts +26 -26
  9. package/templates/nextjs/base/src/app/account/layout.tsx +9 -9
  10. package/templates/nextjs/base/src/app/account/page.tsx +122 -122
  11. package/templates/nextjs/base/src/app/api/auth/logout/route.ts +14 -14
  12. package/templates/nextjs/base/src/app/api/auth/me/route.ts +56 -56
  13. package/templates/nextjs/base/src/app/api/auth/oauth-callback/route.ts +59 -59
  14. package/templates/nextjs/base/src/app/api/auth/reset-callback/route.ts +41 -41
  15. package/templates/nextjs/base/src/app/api/auth/reset-password/route.ts +77 -77
  16. package/templates/nextjs/base/src/app/api/store/[...path]/route.ts +198 -198
  17. package/templates/nextjs/base/src/app/auth/callback/page.tsx +92 -92
  18. package/templates/nextjs/base/src/app/cart/layout.tsx +9 -9
  19. package/templates/nextjs/base/src/app/cart/page.tsx +204 -204
  20. package/templates/nextjs/base/src/app/checkout/layout.tsx +9 -9
  21. package/templates/nextjs/base/src/app/checkout/page.tsx +860 -860
  22. package/templates/nextjs/base/src/app/forgot-password/page.tsx +112 -112
  23. package/templates/nextjs/base/src/app/layout.tsx.ejs +75 -0
  24. package/templates/nextjs/base/src/app/login/layout.tsx +9 -9
  25. package/templates/nextjs/base/src/app/login/page.tsx +59 -59
  26. package/templates/nextjs/base/src/app/order-confirmation/layout.tsx +9 -9
  27. package/templates/nextjs/base/src/app/order-confirmation/page.tsx +17 -0
  28. package/templates/nextjs/base/src/app/payment-complete/page.tsx +59 -0
  29. package/templates/nextjs/base/src/app/products/[slug]/page.tsx +67 -67
  30. package/templates/nextjs/base/src/app/products/[slug]/product-client-section.tsx +486 -486
  31. package/templates/nextjs/base/src/app/products/layout.tsx +18 -18
  32. package/templates/nextjs/base/src/app/products/page.tsx +431 -431
  33. package/templates/nextjs/base/src/app/register/layout.tsx +9 -9
  34. package/templates/nextjs/base/src/app/register/page.tsx +65 -65
  35. package/templates/nextjs/base/src/app/reset-password/page.tsx +132 -132
  36. package/templates/nextjs/base/src/app/robots.ts +14 -14
  37. package/templates/nextjs/base/src/app/sitemap.ts +25 -25
  38. package/templates/nextjs/base/src/app/verify-email/page.tsx +258 -258
  39. package/templates/nextjs/base/src/components/account/address-book.tsx +432 -432
  40. package/templates/nextjs/base/src/components/account/order-history.tsx +350 -350
  41. package/templates/nextjs/base/src/components/auth/oauth-buttons.tsx +137 -137
  42. package/templates/nextjs/base/src/components/auth/register-form.tsx +232 -232
  43. package/templates/nextjs/base/src/components/cart/cart-bundle-offer.tsx +247 -111
  44. package/templates/nextjs/base/src/components/cart/cart-item.tsx +153 -153
  45. package/templates/nextjs/base/src/components/cart/cart-upgrade-banner.tsx +142 -142
  46. package/templates/nextjs/base/src/components/cart/free-shipping-bar.tsx +59 -59
  47. package/templates/nextjs/base/src/components/checkout/checkout-form.tsx +415 -415
  48. package/templates/nextjs/base/src/components/checkout/order-bump-card.tsx +243 -83
  49. package/templates/nextjs/base/src/components/checkout/payment-step.tsx +49 -3
  50. package/templates/nextjs/base/src/components/layout/footer.tsx +41 -41
  51. package/templates/nextjs/base/src/components/layout/header.tsx +336 -336
  52. package/templates/nextjs/base/src/components/layout/language-switcher.tsx.ejs +63 -0
  53. package/templates/nextjs/base/src/components/products/discount-badge.tsx +22 -22
  54. package/templates/nextjs/base/src/components/products/frequently-bought-together.tsx +202 -202
  55. package/templates/nextjs/base/src/components/products/product-card.tsx +218 -218
  56. package/templates/nextjs/base/src/components/products/recommendation-section.tsx +107 -107
  57. package/templates/nextjs/base/src/components/products/stock-badge.tsx +63 -63
  58. package/templates/nextjs/base/src/components/products/variant-selector.tsx +292 -292
  59. package/templates/nextjs/base/src/components/seo/product-json-ld.tsx +72 -72
  60. package/templates/nextjs/base/src/i18n.ts.ejs +21 -0
  61. package/templates/nextjs/base/src/lib/auth.ts +149 -149
  62. package/templates/nextjs/base/src/lib/brainerce.ts.ejs +9 -0
  63. package/templates/nextjs/base/src/lib/translations.ts.ejs +31 -0
  64. package/templates/nextjs/base/src/middleware.ts.ejs +81 -0
  65. package/templates/nextjs/base/src/providers/store-provider.tsx.ejs +41 -0
  66. package/templates/nextjs/base/src/lib/translations.ts +0 -11
  67. package/templates/nextjs/base/src/middleware.ts +0 -25
@@ -1,92 +1,92 @@
1
- 'use client';
2
-
3
- import { Suspense, useEffect, useState, useRef } from 'react';
4
- import { useRouter, useSearchParams } from 'next/navigation';
5
- import { useAuth } from '@/providers/store-provider';
6
- import { LoadingSpinner } from '@/components/shared/loading-spinner';
7
- import { useTranslations } from '@/lib/translations';
8
-
9
- function OAuthCallbackContent() {
10
- const router = useRouter();
11
- const searchParams = useSearchParams();
12
- const auth = useAuth();
13
- const [error, setError] = useState<string | null>(null);
14
- const processedRef = useRef(false);
15
- const t = useTranslations('auth');
16
-
17
- const oauthSuccess = searchParams.get('oauth_success');
18
- const oauthError = searchParams.get('oauth_error');
19
- // Token is no longer in URL — it was set as httpOnly cookie by /api/auth/oauth-callback
20
-
21
- useEffect(() => {
22
- // Prevent double-processing in React StrictMode
23
- if (processedRef.current) return;
24
- processedRef.current = true;
25
-
26
- if (oauthError) {
27
- setError(oauthError);
28
- return;
29
- }
30
-
31
- if (oauthSuccess === 'true') {
32
- // Cookie was already set by the API route; refresh auth state
33
- auth.login().then(() => {
34
- router.push('/');
35
- });
36
- } else {
37
- setError(t('authFailedDesc'));
38
- }
39
- }, [oauthSuccess, oauthError, auth, router, t]);
40
-
41
- if (error) {
42
- return (
43
- <div className="flex min-h-[60vh] items-center justify-center px-4 py-12">
44
- <div className="w-full max-w-md space-y-4 text-center">
45
- <svg
46
- className="text-destructive mx-auto h-12 w-12"
47
- fill="none"
48
- viewBox="0 0 24 24"
49
- stroke="currentColor"
50
- >
51
- <path
52
- strokeLinecap="round"
53
- strokeLinejoin="round"
54
- strokeWidth={1.5}
55
- 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"
56
- />
57
- </svg>
58
- <h1 className="text-foreground text-2xl font-bold">{t('authFailed')}</h1>
59
- <p className="text-muted-foreground text-sm">{error}</p>
60
- <button
61
- type="button"
62
- onClick={() => router.push('/login')}
63
- className="bg-primary text-primary-foreground inline-flex items-center rounded px-6 py-3 font-medium transition-opacity hover:opacity-90"
64
- >
65
- {t('backToLogin')}
66
- </button>
67
- </div>
68
- </div>
69
- );
70
- }
71
-
72
- return (
73
- <div className="flex min-h-[60vh] flex-col items-center justify-center px-4 py-12">
74
- <LoadingSpinner size="lg" />
75
- <p className="text-muted-foreground mt-4">{t('completingSignIn')}</p>
76
- </div>
77
- );
78
- }
79
-
80
- export default function OAuthCallbackPage() {
81
- return (
82
- <Suspense
83
- fallback={
84
- <div className="flex min-h-[60vh] items-center justify-center">
85
- <LoadingSpinner size="lg" />
86
- </div>
87
- }
88
- >
89
- <OAuthCallbackContent />
90
- </Suspense>
91
- );
92
- }
1
+ 'use client';
2
+
3
+ import { Suspense, useEffect, useState, useRef } from 'react';
4
+ import { useRouter, useSearchParams } from 'next/navigation';
5
+ import { useAuth } from '@/providers/store-provider';
6
+ import { LoadingSpinner } from '@/components/shared/loading-spinner';
7
+ import { useTranslations } from '@/lib/translations';
8
+
9
+ function OAuthCallbackContent() {
10
+ const router = useRouter();
11
+ const searchParams = useSearchParams();
12
+ const auth = useAuth();
13
+ const [error, setError] = useState<string | null>(null);
14
+ const processedRef = useRef(false);
15
+ const t = useTranslations('auth');
16
+
17
+ const oauthSuccess = searchParams.get('oauth_success');
18
+ const oauthError = searchParams.get('oauth_error');
19
+ // Token is no longer in URL — it was set as httpOnly cookie by /api/auth/oauth-callback
20
+
21
+ useEffect(() => {
22
+ // Prevent double-processing in React StrictMode
23
+ if (processedRef.current) return;
24
+ processedRef.current = true;
25
+
26
+ if (oauthError) {
27
+ setError(oauthError);
28
+ return;
29
+ }
30
+
31
+ if (oauthSuccess === 'true') {
32
+ // Cookie was already set by the API route; refresh auth state
33
+ auth.login().then(() => {
34
+ router.push('/');
35
+ });
36
+ } else {
37
+ setError(t('authFailedDesc'));
38
+ }
39
+ }, [oauthSuccess, oauthError, auth, router, t]);
40
+
41
+ if (error) {
42
+ return (
43
+ <div className="flex min-h-[60vh] items-center justify-center px-4 py-12">
44
+ <div className="w-full max-w-md space-y-4 text-center">
45
+ <svg
46
+ className="text-destructive mx-auto h-12 w-12"
47
+ fill="none"
48
+ viewBox="0 0 24 24"
49
+ stroke="currentColor"
50
+ >
51
+ <path
52
+ strokeLinecap="round"
53
+ strokeLinejoin="round"
54
+ strokeWidth={1.5}
55
+ 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"
56
+ />
57
+ </svg>
58
+ <h1 className="text-foreground text-2xl font-bold">{t('authFailed')}</h1>
59
+ <p className="text-muted-foreground text-sm">{error}</p>
60
+ <button
61
+ type="button"
62
+ onClick={() => router.push('/login')}
63
+ className="bg-primary text-primary-foreground inline-flex items-center rounded px-6 py-3 font-medium transition-opacity hover:opacity-90"
64
+ >
65
+ {t('backToLogin')}
66
+ </button>
67
+ </div>
68
+ </div>
69
+ );
70
+ }
71
+
72
+ return (
73
+ <div className="flex min-h-[60vh] flex-col items-center justify-center px-4 py-12">
74
+ <LoadingSpinner size="lg" />
75
+ <p className="text-muted-foreground mt-4">{t('completingSignIn')}</p>
76
+ </div>
77
+ );
78
+ }
79
+
80
+ export default function OAuthCallbackPage() {
81
+ return (
82
+ <Suspense
83
+ fallback={
84
+ <div className="flex min-h-[60vh] items-center justify-center">
85
+ <LoadingSpinner size="lg" />
86
+ </div>
87
+ }
88
+ >
89
+ <OAuthCallbackContent />
90
+ </Suspense>
91
+ );
92
+ }
@@ -1,9 +1,9 @@
1
- import type { Metadata } from 'next';
2
-
3
- export const metadata: Metadata = {
4
- robots: { index: false, follow: false },
5
- };
6
-
7
- export default function Layout({ children }: { children: React.ReactNode }) {
8
- return <>{children}</>;
9
- }
1
+ import type { Metadata } from 'next';
2
+
3
+ export const metadata: Metadata = {
4
+ robots: { index: false, follow: false },
5
+ };
6
+
7
+ export default function Layout({ children }: { children: React.ReactNode }) {
8
+ return <>{children}</>;
9
+ }
@@ -1,204 +1,204 @@
1
- 'use client';
2
-
3
- import { useEffect, useState } from 'react';
4
- import Link from 'next/link';
5
- import type {
6
- CartRecommendationsResponse,
7
- CartUpgradesResponse,
8
- CartBundlesResponse,
9
- } from 'brainerce';
10
- import { getClient } from '@/lib/brainerce';
11
- import { useCart } from '@/providers/store-provider';
12
- import { useStoreInfo } from '@/providers/store-provider';
13
- import { CartItem } from '@/components/cart/cart-item';
14
- import { CartUpgradeBanner } from '@/components/cart/cart-upgrade-banner';
15
- import { CartBundleOfferCard } from '@/components/cart/cart-bundle-offer';
16
- import { CartSummary } from '@/components/cart/cart-summary';
17
- import { CouponInput } from '@/components/cart/coupon-input';
18
- import { CartNudges } from '@/components/cart/cart-nudges';
19
- import { FreeShippingBar } from '@/components/cart/free-shipping-bar';
20
- import { ReservationCountdown } from '@/components/cart/reservation-countdown';
21
- import { CartRecommendationSection } from '@/components/products/recommendation-section';
22
- import { LoadingSpinner } from '@/components/shared/loading-spinner';
23
- import { useTranslations } from '@/lib/translations';
24
-
25
- export default function CartPage() {
26
- const { cart, cartLoading, refreshCart, itemCount } = useCart();
27
- const { storeInfo } = useStoreInfo();
28
- const t = useTranslations('cart');
29
- const tc = useTranslations('common');
30
- const [cartRecs, setCartRecs] = useState<CartRecommendationsResponse | null>(null);
31
- const [upgrades, setUpgrades] = useState<CartUpgradesResponse | null>(null);
32
- const [bundles, setBundles] = useState<CartBundlesResponse | null>(null);
33
-
34
- // Load cross-sell recommendations when cart changes
35
- useEffect(() => {
36
- if (!cart?.id || cart.items.length === 0) {
37
- setCartRecs(null);
38
- return;
39
- }
40
- const client = getClient();
41
- client
42
- .getCartRecommendations(cart.id, 4)
43
- .then(setCartRecs)
44
- .catch(() => {});
45
- }, [cart?.id, cart?.items.length]);
46
-
47
- // Load upgrade suggestions when cart changes
48
- useEffect(() => {
49
- if (
50
- !cart?.id ||
51
- cart.items.length === 0 ||
52
- storeInfo?.upsell?.cartUpgradeBannerEnabled === false
53
- ) {
54
- setUpgrades(null);
55
- return;
56
- }
57
- const client = getClient();
58
- client
59
- .getCartUpgrades(cart.id)
60
- .then(setUpgrades)
61
- .catch(() => {});
62
- }, [cart?.id, cart?.items.length, storeInfo?.upsell?.cartUpgradeBannerEnabled]);
63
-
64
- // Load bundle offers when cart changes
65
- useEffect(() => {
66
- if (!cart?.id || cart.items.length === 0 || storeInfo?.upsell?.cartBundleEnabled === false) {
67
- setBundles(null);
68
- return;
69
- }
70
- const client = getClient();
71
- client
72
- .getCartBundles(cart.id)
73
- .then(setBundles)
74
- .catch(() => {});
75
- }, [cart?.id, cart?.items.length, storeInfo?.upsell?.cartBundleEnabled]);
76
-
77
- if (cartLoading) {
78
- return (
79
- <div className="flex min-h-[60vh] items-center justify-center">
80
- <LoadingSpinner size="lg" />
81
- </div>
82
- );
83
- }
84
-
85
- // Empty cart state
86
- if (!cart || cart.items.length === 0) {
87
- return (
88
- <div className="mx-auto max-w-7xl px-4 py-16 text-center sm:px-6 lg:px-8">
89
- <svg
90
- className="text-muted-foreground mx-auto mb-4 h-16 w-16"
91
- fill="none"
92
- viewBox="0 0 24 24"
93
- stroke="currentColor"
94
- >
95
- <path
96
- strokeLinecap="round"
97
- strokeLinejoin="round"
98
- strokeWidth={1.5}
99
- d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z"
100
- />
101
- </svg>
102
- <h1 className="text-foreground text-2xl font-bold">{t('emptyTitle')}</h1>
103
- <p className="text-muted-foreground mt-2">{t('emptySubtitle')}</p>
104
- <Link
105
- href="/products"
106
- className="bg-primary text-primary-foreground mt-6 inline-flex items-center rounded px-6 py-3 font-medium transition-opacity hover:opacity-90"
107
- >
108
- {tc('continueShopping')}
109
- </Link>
110
- </div>
111
- );
112
- }
113
-
114
- return (
115
- <div className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
116
- <h1 className="text-foreground mb-6 text-2xl font-bold">
117
- {t('title')} ({itemCount} {itemCount === 1 ? tc('item') : tc('items')})
118
- </h1>
119
-
120
- {/* Reservation countdown */}
121
- {cart.reservation?.hasReservation && (
122
- <ReservationCountdown reservation={cart.reservation} className="mb-6" />
123
- )}
124
-
125
- <div className="grid grid-cols-1 gap-8 lg:grid-cols-3">
126
- {/* Cart Items */}
127
- <div className="lg:col-span-2">
128
- {/* Nudges */}
129
- {cart.nudges && cart.nudges.length > 0 && (
130
- <CartNudges nudges={cart.nudges} className="mb-4" />
131
- )}
132
-
133
- {/* Cart items */}
134
- <div>
135
- {cart.items.map((item) => (
136
- <div key={item.id}>
137
- <CartItem item={item} onUpdate={refreshCart} />
138
- {upgrades?.upgrades?.[item.productId] && (
139
- <CartUpgradeBanner
140
- suggestion={upgrades.upgrades[item.productId]}
141
- cartItem={item}
142
- onUpgrade={refreshCart}
143
- className="mb-2 ms-24"
144
- />
145
- )}
146
- </div>
147
- ))}
148
- </div>
149
-
150
- {/* Bundle offers */}
151
- {bundles?.bundles && bundles.bundles.length > 0 && (
152
- <div className="mt-6 space-y-3">
153
- <h3 className="text-foreground text-sm font-semibold">{t('bundleOffers')}</h3>
154
- {bundles.bundles.map((offer) => (
155
- <CartBundleOfferCard
156
- key={offer.id}
157
- offer={offer}
158
- cartId={cart.id}
159
- onAdd={refreshCart}
160
- />
161
- ))}
162
- </div>
163
- )}
164
-
165
- {/* Coupon input */}
166
- <div className="border-border mt-6 border-t pt-4">
167
- <CouponInput cart={cart} onUpdate={refreshCart} />
168
- </div>
169
- </div>
170
-
171
- {/* Summary sidebar */}
172
- <div className="lg:col-span-1">
173
- <div className="bg-muted/50 border-border sticky top-24 rounded-lg border p-6">
174
- <FreeShippingBar className="mb-4" />
175
- <CartSummary />
176
-
177
- <Link
178
- href="/checkout"
179
- className="bg-primary text-primary-foreground mt-6 inline-flex w-full items-center justify-center rounded px-6 py-3 text-sm font-medium transition-opacity hover:opacity-90"
180
- >
181
- {t('proceedToCheckout')}
182
- </Link>
183
-
184
- <Link
185
- href="/products"
186
- className="text-muted-foreground hover:text-foreground mt-3 inline-flex w-full items-center justify-center px-6 py-2 text-sm transition-colors"
187
- >
188
- {tc('continueShopping')}
189
- </Link>
190
- </div>
191
- </div>
192
- </div>
193
-
194
- {/* Cross-sell recommendations */}
195
- {cartRecs?.recommendations && cartRecs.recommendations.length > 0 && (
196
- <CartRecommendationSection
197
- title={t('youMightAlsoNeed')}
198
- items={cartRecs.recommendations}
199
- className="mt-10"
200
- />
201
- )}
202
- </div>
203
- );
204
- }
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+ import Link from 'next/link';
5
+ import type {
6
+ CartRecommendationsResponse,
7
+ CartUpgradesResponse,
8
+ CartBundlesResponse,
9
+ } from 'brainerce';
10
+ import { getClient } from '@/lib/brainerce';
11
+ import { useCart } from '@/providers/store-provider';
12
+ import { useStoreInfo } from '@/providers/store-provider';
13
+ import { CartItem } from '@/components/cart/cart-item';
14
+ import { CartUpgradeBanner } from '@/components/cart/cart-upgrade-banner';
15
+ import { CartBundleOfferCard } from '@/components/cart/cart-bundle-offer';
16
+ import { CartSummary } from '@/components/cart/cart-summary';
17
+ import { CouponInput } from '@/components/cart/coupon-input';
18
+ import { CartNudges } from '@/components/cart/cart-nudges';
19
+ import { FreeShippingBar } from '@/components/cart/free-shipping-bar';
20
+ import { ReservationCountdown } from '@/components/cart/reservation-countdown';
21
+ import { CartRecommendationSection } from '@/components/products/recommendation-section';
22
+ import { LoadingSpinner } from '@/components/shared/loading-spinner';
23
+ import { useTranslations } from '@/lib/translations';
24
+
25
+ export default function CartPage() {
26
+ const { cart, cartLoading, refreshCart, itemCount } = useCart();
27
+ const { storeInfo } = useStoreInfo();
28
+ const t = useTranslations('cart');
29
+ const tc = useTranslations('common');
30
+ const [cartRecs, setCartRecs] = useState<CartRecommendationsResponse | null>(null);
31
+ const [upgrades, setUpgrades] = useState<CartUpgradesResponse | null>(null);
32
+ const [bundles, setBundles] = useState<CartBundlesResponse | null>(null);
33
+
34
+ // Load cross-sell recommendations when cart changes
35
+ useEffect(() => {
36
+ if (!cart?.id || cart.items.length === 0) {
37
+ setCartRecs(null);
38
+ return;
39
+ }
40
+ const client = getClient();
41
+ client
42
+ .getCartRecommendations(cart.id, 4)
43
+ .then(setCartRecs)
44
+ .catch(() => {});
45
+ }, [cart?.id, cart?.items.length]);
46
+
47
+ // Load upgrade suggestions when cart changes
48
+ useEffect(() => {
49
+ if (
50
+ !cart?.id ||
51
+ cart.items.length === 0 ||
52
+ storeInfo?.upsell?.cartUpgradeBannerEnabled === false
53
+ ) {
54
+ setUpgrades(null);
55
+ return;
56
+ }
57
+ const client = getClient();
58
+ client
59
+ .getCartUpgrades(cart.id)
60
+ .then(setUpgrades)
61
+ .catch(() => {});
62
+ }, [cart?.id, cart?.items.length, storeInfo?.upsell?.cartUpgradeBannerEnabled]);
63
+
64
+ // Load bundle offers when cart changes
65
+ useEffect(() => {
66
+ if (!cart?.id || cart.items.length === 0 || storeInfo?.upsell?.cartBundleEnabled === false) {
67
+ setBundles(null);
68
+ return;
69
+ }
70
+ const client = getClient();
71
+ client
72
+ .getCartBundles(cart.id)
73
+ .then(setBundles)
74
+ .catch(() => {});
75
+ }, [cart?.id, cart?.items.length, storeInfo?.upsell?.cartBundleEnabled]);
76
+
77
+ if (cartLoading) {
78
+ return (
79
+ <div className="flex min-h-[60vh] items-center justify-center">
80
+ <LoadingSpinner size="lg" />
81
+ </div>
82
+ );
83
+ }
84
+
85
+ // Empty cart state
86
+ if (!cart || cart.items.length === 0) {
87
+ return (
88
+ <div className="mx-auto max-w-7xl px-4 py-16 text-center sm:px-6 lg:px-8">
89
+ <svg
90
+ className="text-muted-foreground mx-auto mb-4 h-16 w-16"
91
+ fill="none"
92
+ viewBox="0 0 24 24"
93
+ stroke="currentColor"
94
+ >
95
+ <path
96
+ strokeLinecap="round"
97
+ strokeLinejoin="round"
98
+ strokeWidth={1.5}
99
+ d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z"
100
+ />
101
+ </svg>
102
+ <h1 className="text-foreground text-2xl font-bold">{t('emptyTitle')}</h1>
103
+ <p className="text-muted-foreground mt-2">{t('emptySubtitle')}</p>
104
+ <Link
105
+ href="/products"
106
+ className="bg-primary text-primary-foreground mt-6 inline-flex items-center rounded px-6 py-3 font-medium transition-opacity hover:opacity-90"
107
+ >
108
+ {tc('continueShopping')}
109
+ </Link>
110
+ </div>
111
+ );
112
+ }
113
+
114
+ return (
115
+ <div className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
116
+ <h1 className="text-foreground mb-6 text-2xl font-bold">
117
+ {t('title')} ({itemCount} {itemCount === 1 ? tc('item') : tc('items')})
118
+ </h1>
119
+
120
+ {/* Reservation countdown */}
121
+ {cart.reservation?.hasReservation && (
122
+ <ReservationCountdown reservation={cart.reservation} className="mb-6" />
123
+ )}
124
+
125
+ <div className="grid grid-cols-1 gap-8 lg:grid-cols-3">
126
+ {/* Cart Items */}
127
+ <div className="lg:col-span-2">
128
+ {/* Nudges */}
129
+ {cart.nudges && cart.nudges.length > 0 && (
130
+ <CartNudges nudges={cart.nudges} className="mb-4" />
131
+ )}
132
+
133
+ {/* Cart items */}
134
+ <div>
135
+ {cart.items.map((item) => (
136
+ <div key={item.id}>
137
+ <CartItem item={item} onUpdate={refreshCart} />
138
+ {upgrades?.upgrades?.[item.productId] && (
139
+ <CartUpgradeBanner
140
+ suggestion={upgrades.upgrades[item.productId]}
141
+ cartItem={item}
142
+ onUpgrade={refreshCart}
143
+ className="mb-2 ms-24"
144
+ />
145
+ )}
146
+ </div>
147
+ ))}
148
+ </div>
149
+
150
+ {/* Bundle offers */}
151
+ {bundles?.bundles && bundles.bundles.length > 0 && (
152
+ <div className="mt-6 space-y-3">
153
+ <h3 className="text-foreground text-sm font-semibold">{t('bundleOffers')}</h3>
154
+ {bundles.bundles.map((offer) => (
155
+ <CartBundleOfferCard
156
+ key={offer.id}
157
+ offer={offer}
158
+ cartId={cart.id}
159
+ onAdd={refreshCart}
160
+ />
161
+ ))}
162
+ </div>
163
+ )}
164
+
165
+ {/* Coupon input */}
166
+ <div className="border-border mt-6 border-t pt-4">
167
+ <CouponInput cart={cart} onUpdate={refreshCart} />
168
+ </div>
169
+ </div>
170
+
171
+ {/* Summary sidebar */}
172
+ <div className="lg:col-span-1">
173
+ <div className="bg-muted/50 border-border sticky top-24 rounded-lg border p-6">
174
+ <FreeShippingBar className="mb-4" />
175
+ <CartSummary />
176
+
177
+ <Link
178
+ href="/checkout"
179
+ className="bg-primary text-primary-foreground mt-6 inline-flex w-full items-center justify-center rounded px-6 py-3 text-sm font-medium transition-opacity hover:opacity-90"
180
+ >
181
+ {t('proceedToCheckout')}
182
+ </Link>
183
+
184
+ <Link
185
+ href="/products"
186
+ className="text-muted-foreground hover:text-foreground mt-3 inline-flex w-full items-center justify-center px-6 py-2 text-sm transition-colors"
187
+ >
188
+ {tc('continueShopping')}
189
+ </Link>
190
+ </div>
191
+ </div>
192
+ </div>
193
+
194
+ {/* Cross-sell recommendations */}
195
+ {cartRecs?.recommendations && cartRecs.recommendations.length > 0 && (
196
+ <CartRecommendationSection
197
+ title={t('youMightAlsoNeed')}
198
+ items={cartRecs.recommendations}
199
+ className="mt-10"
200
+ />
201
+ )}
202
+ </div>
203
+ );
204
+ }
@@ -1,9 +1,9 @@
1
- import type { Metadata } from 'next';
2
-
3
- export const metadata: Metadata = {
4
- robots: { index: false, follow: false },
5
- };
6
-
7
- export default function Layout({ children }: { children: React.ReactNode }) {
8
- return <>{children}</>;
9
- }
1
+ import type { Metadata } from 'next';
2
+
3
+ export const metadata: Metadata = {
4
+ robots: { index: false, follow: false },
5
+ };
6
+
7
+ export default function Layout({ children }: { children: React.ReactNode }) {
8
+ return <>{children}</>;
9
+ }