create-brainerce-store 1.11.0 → 1.11.2
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.
- package/dist/index.js +1 -1
- package/messages/en.json +303 -300
- package/messages/he.json +303 -300
- package/package.json +1 -1
- package/templates/nextjs/base/next.config.ts +31 -31
- package/templates/nextjs/base/src/app/api/store/[...path]/route.ts +4 -2
- package/templates/nextjs/base/src/app/cart/page.tsx +137 -134
- package/templates/nextjs/base/src/app/checkout/page.tsx +666 -679
- package/templates/nextjs/base/src/app/globals.css +1 -0
- package/templates/nextjs/base/src/app/order-confirmation/page.tsx +243 -193
- package/templates/nextjs/base/src/app/products/[slug]/page.tsx +527 -509
- package/templates/nextjs/base/src/app/products/page.tsx +430 -434
- package/templates/nextjs/base/src/components/account/order-history.tsx +349 -349
- package/templates/nextjs/base/src/components/checkout/payment-step.tsx +534 -379
- package/templates/nextjs/base/src/components/products/product-card.tsx +102 -102
- package/templates/nextjs/base/src/components/products/recommendation-section.tsx +107 -111
|
@@ -1,31 +1,31 @@
|
|
|
1
|
-
import type { NextConfig } from 'next';
|
|
2
|
-
|
|
3
|
-
const nextConfig: NextConfig = {
|
|
4
|
-
images: {
|
|
5
|
-
remotePatterns: [{ protocol: 'https', hostname: '**' }],
|
|
6
|
-
},
|
|
7
|
-
async headers() {
|
|
8
|
-
return [
|
|
9
|
-
{
|
|
10
|
-
source: '/(.*)',
|
|
11
|
-
headers: [
|
|
12
|
-
{
|
|
13
|
-
key: 'Content-Security-Policy',
|
|
14
|
-
value: [
|
|
15
|
-
"default-src 'self'",
|
|
16
|
-
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.meshulam.co.il https://meshulam.co.il https://*.meshulam.co.il https://grow.link https://*.grow.link https://*.grow.security https://js.stripe.com https://pay.google.com",
|
|
17
|
-
"style-src 'self' 'unsafe-inline'",
|
|
18
|
-
"img-src 'self' data: blob: https:",
|
|
19
|
-
"font-src 'self' data:",
|
|
20
|
-
"frame-src 'self' https://*.meshulam.co.il https://grow.link https://*.grow.link https://*.grow.security https://*.creditguard.co.il https://js.stripe.com https://hooks.stripe.com https://pay.google.com",
|
|
21
|
-
"connect-src 'self' https://*.meshulam.co.il https://grow.link https://*.grow.link https://*.grow.security https://google.com https://pay.google.com https://*.stripe.com https://*.creditguard.co.il",
|
|
22
|
-
"worker-src 'self' blob:",
|
|
23
|
-
].join('; '),
|
|
24
|
-
},
|
|
25
|
-
],
|
|
26
|
-
},
|
|
27
|
-
];
|
|
28
|
-
},
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
export default nextConfig;
|
|
1
|
+
import type { NextConfig } from 'next';
|
|
2
|
+
|
|
3
|
+
const nextConfig: NextConfig = {
|
|
4
|
+
images: {
|
|
5
|
+
remotePatterns: [{ protocol: 'https', hostname: '**' }],
|
|
6
|
+
},
|
|
7
|
+
async headers() {
|
|
8
|
+
return [
|
|
9
|
+
{
|
|
10
|
+
source: '/(.*)',
|
|
11
|
+
headers: [
|
|
12
|
+
{
|
|
13
|
+
key: 'Content-Security-Policy',
|
|
14
|
+
value: [
|
|
15
|
+
"default-src 'self'",
|
|
16
|
+
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.meshulam.co.il https://meshulam.co.il https://*.meshulam.co.il https://grow.link https://*.grow.link https://*.grow.security https://js.stripe.com https://pay.google.com",
|
|
17
|
+
"style-src 'self' 'unsafe-inline'",
|
|
18
|
+
"img-src 'self' data: blob: https:",
|
|
19
|
+
"font-src 'self' data:",
|
|
20
|
+
"frame-src 'self' https://*.meshulam.co.il https://grow.link https://*.grow.link https://*.grow.security https://*.creditguard.co.il https://js.stripe.com https://hooks.stripe.com https://pay.google.com",
|
|
21
|
+
"connect-src 'self' https://*.meshulam.co.il https://grow.link https://*.grow.link https://*.grow.security https://google.com https://pay.google.com https://*.stripe.com https://*.creditguard.co.il",
|
|
22
|
+
"worker-src 'self' blob:",
|
|
23
|
+
].join('; '),
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export default nextConfig;
|
|
@@ -75,9 +75,11 @@ async function proxyRequest(
|
|
|
75
75
|
};
|
|
76
76
|
|
|
77
77
|
// Forward Origin/Referer so backend BrowserOriginGuard accepts proxied requests
|
|
78
|
-
|
|
78
|
+
// Always send Origin — same-origin GET requests may not include it, but the backend
|
|
79
|
+
// uses its presence to distinguish fetch() calls from direct browser navigation
|
|
80
|
+
const origin = request.headers.get('origin') || request.nextUrl.origin;
|
|
79
81
|
const referer = request.headers.get('referer');
|
|
80
|
-
|
|
82
|
+
headers['Origin'] = origin;
|
|
81
83
|
if (referer) headers['Referer'] = referer;
|
|
82
84
|
|
|
83
85
|
// Forward SDK version header if present
|
|
@@ -1,134 +1,137 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useEffect, useState } from 'react';
|
|
4
|
-
import Link from 'next/link';
|
|
5
|
-
import type { CartRecommendationsResponse } from 'brainerce';
|
|
6
|
-
import { getClient } from '@/lib/brainerce';
|
|
7
|
-
import { useCart } from '@/providers/store-provider';
|
|
8
|
-
import { CartItem } from '@/components/cart/cart-item';
|
|
9
|
-
import { CartSummary } from '@/components/cart/cart-summary';
|
|
10
|
-
import { CouponInput } from '@/components/cart/coupon-input';
|
|
11
|
-
import { CartNudges } from '@/components/cart/cart-nudges';
|
|
12
|
-
import { ReservationCountdown } from '@/components/cart/reservation-countdown';
|
|
13
|
-
import { CartRecommendationSection } from '@/components/products/recommendation-section';
|
|
14
|
-
import { LoadingSpinner } from '@/components/shared/loading-spinner';
|
|
15
|
-
import { useTranslations } from '@/lib/translations';
|
|
16
|
-
|
|
17
|
-
export default function CartPage() {
|
|
18
|
-
const { cart, cartLoading, refreshCart, itemCount } = useCart();
|
|
19
|
-
const t = useTranslations('cart');
|
|
20
|
-
const tc = useTranslations('common');
|
|
21
|
-
const [cartRecs, setCartRecs] = useState<CartRecommendationsResponse | null>(null);
|
|
22
|
-
|
|
23
|
-
// Load cross-sell recommendations when cart changes
|
|
24
|
-
useEffect(() => {
|
|
25
|
-
if (!cart?.id || cart.items.length === 0) {
|
|
26
|
-
setCartRecs(null);
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
const client = getClient();
|
|
30
|
-
client
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import Link from 'next/link';
|
|
5
|
+
import type { CartRecommendationsResponse } from 'brainerce';
|
|
6
|
+
import { getClient } from '@/lib/brainerce';
|
|
7
|
+
import { useCart } from '@/providers/store-provider';
|
|
8
|
+
import { CartItem } from '@/components/cart/cart-item';
|
|
9
|
+
import { CartSummary } from '@/components/cart/cart-summary';
|
|
10
|
+
import { CouponInput } from '@/components/cart/coupon-input';
|
|
11
|
+
import { CartNudges } from '@/components/cart/cart-nudges';
|
|
12
|
+
import { ReservationCountdown } from '@/components/cart/reservation-countdown';
|
|
13
|
+
import { CartRecommendationSection } from '@/components/products/recommendation-section';
|
|
14
|
+
import { LoadingSpinner } from '@/components/shared/loading-spinner';
|
|
15
|
+
import { useTranslations } from '@/lib/translations';
|
|
16
|
+
|
|
17
|
+
export default function CartPage() {
|
|
18
|
+
const { cart, cartLoading, refreshCart, itemCount } = useCart();
|
|
19
|
+
const t = useTranslations('cart');
|
|
20
|
+
const tc = useTranslations('common');
|
|
21
|
+
const [cartRecs, setCartRecs] = useState<CartRecommendationsResponse | null>(null);
|
|
22
|
+
|
|
23
|
+
// Load cross-sell recommendations when cart changes
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (!cart?.id || cart.items.length === 0) {
|
|
26
|
+
setCartRecs(null);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const client = getClient();
|
|
30
|
+
client
|
|
31
|
+
.getCartRecommendations(cart.id, 4)
|
|
32
|
+
.then(setCartRecs)
|
|
33
|
+
.catch(() => {});
|
|
34
|
+
}, [cart?.id, cart?.items.length]);
|
|
35
|
+
|
|
36
|
+
if (cartLoading) {
|
|
37
|
+
return (
|
|
38
|
+
<div className="flex min-h-[60vh] items-center justify-center">
|
|
39
|
+
<LoadingSpinner size="lg" />
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Empty cart state
|
|
45
|
+
if (!cart || cart.items.length === 0) {
|
|
46
|
+
return (
|
|
47
|
+
<div className="mx-auto max-w-7xl px-4 py-16 text-center sm:px-6 lg:px-8">
|
|
48
|
+
<svg
|
|
49
|
+
className="text-muted-foreground mx-auto mb-4 h-16 w-16"
|
|
50
|
+
fill="none"
|
|
51
|
+
viewBox="0 0 24 24"
|
|
52
|
+
stroke="currentColor"
|
|
53
|
+
>
|
|
54
|
+
<path
|
|
55
|
+
strokeLinecap="round"
|
|
56
|
+
strokeLinejoin="round"
|
|
57
|
+
strokeWidth={1.5}
|
|
58
|
+
d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z"
|
|
59
|
+
/>
|
|
60
|
+
</svg>
|
|
61
|
+
<h1 className="text-foreground text-2xl font-bold">{t('emptyTitle')}</h1>
|
|
62
|
+
<p className="text-muted-foreground mt-2">{t('emptySubtitle')}</p>
|
|
63
|
+
<Link
|
|
64
|
+
href="/products"
|
|
65
|
+
className="bg-primary text-primary-foreground mt-6 inline-flex items-center rounded px-6 py-3 font-medium transition-opacity hover:opacity-90"
|
|
66
|
+
>
|
|
67
|
+
{tc('continueShopping')}
|
|
68
|
+
</Link>
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
|
|
75
|
+
<h1 className="text-foreground mb-6 text-2xl font-bold">
|
|
76
|
+
{t('title')} ({itemCount} {itemCount === 1 ? tc('item') : tc('items')})
|
|
77
|
+
</h1>
|
|
78
|
+
|
|
79
|
+
{/* Reservation countdown */}
|
|
80
|
+
{cart.reservation?.hasReservation && (
|
|
81
|
+
<ReservationCountdown reservation={cart.reservation} className="mb-6" />
|
|
82
|
+
)}
|
|
83
|
+
|
|
84
|
+
<div className="grid grid-cols-1 gap-8 lg:grid-cols-3">
|
|
85
|
+
{/* Cart Items */}
|
|
86
|
+
<div className="lg:col-span-2">
|
|
87
|
+
{/* Nudges */}
|
|
88
|
+
{cart.nudges && cart.nudges.length > 0 && (
|
|
89
|
+
<CartNudges nudges={cart.nudges} className="mb-4" />
|
|
90
|
+
)}
|
|
91
|
+
|
|
92
|
+
{/* Cart items */}
|
|
93
|
+
<div>
|
|
94
|
+
{cart.items.map((item) => (
|
|
95
|
+
<CartItem key={item.id} item={item} onUpdate={refreshCart} />
|
|
96
|
+
))}
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
{/* Coupon input */}
|
|
100
|
+
<div className="border-border mt-6 border-t pt-4">
|
|
101
|
+
<CouponInput cart={cart} onUpdate={refreshCart} />
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
{/* Summary sidebar */}
|
|
106
|
+
<div className="lg:col-span-1">
|
|
107
|
+
<div className="bg-muted/50 border-border sticky top-24 rounded-lg border p-6">
|
|
108
|
+
<CartSummary />
|
|
109
|
+
|
|
110
|
+
<Link
|
|
111
|
+
href="/checkout"
|
|
112
|
+
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"
|
|
113
|
+
>
|
|
114
|
+
{t('proceedToCheckout')}
|
|
115
|
+
</Link>
|
|
116
|
+
|
|
117
|
+
<Link
|
|
118
|
+
href="/products"
|
|
119
|
+
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"
|
|
120
|
+
>
|
|
121
|
+
{tc('continueShopping')}
|
|
122
|
+
</Link>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
{/* Cross-sell recommendations */}
|
|
128
|
+
{cartRecs?.recommendations && cartRecs.recommendations.length > 0 && (
|
|
129
|
+
<CartRecommendationSection
|
|
130
|
+
title={t('youMightAlsoNeed')}
|
|
131
|
+
items={cartRecs.recommendations}
|
|
132
|
+
className="mt-10"
|
|
133
|
+
/>
|
|
134
|
+
)}
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
}
|