create-brainerce-store 1.28.0 → 1.28.4
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 +35 -16
- package/package.json +1 -1
- package/templates/nextjs/base/src/app/checkout/page.tsx +975 -975
- package/templates/nextjs/base/src/app/layout.tsx.ejs +2 -0
- package/templates/nextjs/base/src/components/checkout/payment-step.tsx +2 -2
- package/templates/nextjs/base/src/components/layout/language-switcher.tsx.ejs +12 -5
- package/templates/nextjs/base/src/components/seo/product-json-ld.tsx +2 -0
- package/templates/nextjs/base/src/lib/navigation.tsx.ejs +17 -4
- package/templates/nextjs/base/src/middleware.ts.ejs +49 -14
|
@@ -60,6 +60,7 @@ export default async function RootLayout({
|
|
|
60
60
|
<script
|
|
61
61
|
type="application/ld+json"
|
|
62
62
|
nonce={nonce}
|
|
63
|
+
suppressHydrationWarning
|
|
63
64
|
dangerouslySetInnerHTML={{
|
|
64
65
|
__html: JSON.stringify(organizationJsonLd)
|
|
65
66
|
.replace(/</g, '\\u003c')
|
|
@@ -133,6 +134,7 @@ export default async function RootLayout({
|
|
|
133
134
|
<script
|
|
134
135
|
type="application/ld+json"
|
|
135
136
|
nonce={nonce}
|
|
137
|
+
suppressHydrationWarning
|
|
136
138
|
dangerouslySetInnerHTML={{
|
|
137
139
|
__html: JSON.stringify(organizationJsonLd)
|
|
138
140
|
.replace(/</g, '\\u003c')
|
|
@@ -76,8 +76,8 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
|
|
|
76
76
|
// but we re-check here so the component is safe to render in any context.
|
|
77
77
|
if (!isValidCheckoutId(checkoutId)) {
|
|
78
78
|
return (
|
|
79
|
-
<div className={cn('rounded-md border
|
|
80
|
-
<p className="text-
|
|
79
|
+
<div className={cn('border-destructive/50 rounded-md border p-4', className)}>
|
|
80
|
+
<p className="text-destructive text-sm">{t('paymentError')}</p>
|
|
81
81
|
</div>
|
|
82
82
|
);
|
|
83
83
|
}
|
|
@@ -29,18 +29,25 @@ const LOCALE_NAMES: Record<string, string> = {
|
|
|
29
29
|
vi: 'Tiếng Việt',
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
+
const defaultLocale = '<%= defaultLocale %>';
|
|
33
|
+
|
|
32
34
|
export function LanguageSwitcher() {
|
|
33
35
|
const pathname = usePathname();
|
|
34
|
-
const
|
|
36
|
+
const firstSeg = pathname.split('/')[1];
|
|
37
|
+
const currentLocale = supportedLocales.includes(firstSeg as (typeof supportedLocales)[number])
|
|
38
|
+
? firstSeg
|
|
39
|
+
: defaultLocale;
|
|
35
40
|
|
|
36
41
|
function switchLocale(newLocale: string) {
|
|
42
|
+
// Strip any existing locale segment, then re-add only if the new one
|
|
43
|
+
// isn't the default — matches the as-needed prefix scheme in middleware.
|
|
37
44
|
const segments = pathname.split('/');
|
|
38
45
|
if (supportedLocales.includes(segments[1] as (typeof supportedLocales)[number])) {
|
|
39
|
-
segments
|
|
40
|
-
} else {
|
|
41
|
-
segments.splice(1, 0, newLocale);
|
|
46
|
+
segments.splice(1, 1);
|
|
42
47
|
}
|
|
43
|
-
|
|
48
|
+
const bare = segments.join('/') || '/';
|
|
49
|
+
window.location.href =
|
|
50
|
+
newLocale === defaultLocale ? bare : `/${newLocale}${bare === '/' ? '' : bare}`;
|
|
44
51
|
}
|
|
45
52
|
|
|
46
53
|
if (supportedLocales.length <= 1) return null;
|
|
@@ -64,11 +64,13 @@ export async function ProductJsonLd({ product, url, currency = 'USD' }: ProductJ
|
|
|
64
64
|
<script
|
|
65
65
|
type="application/ld+json"
|
|
66
66
|
nonce={nonce}
|
|
67
|
+
suppressHydrationWarning
|
|
67
68
|
dangerouslySetInnerHTML={{ __html: serializeJsonLd(productJsonLd) }}
|
|
68
69
|
/>
|
|
69
70
|
<script
|
|
70
71
|
type="application/ld+json"
|
|
71
72
|
nonce={nonce}
|
|
73
|
+
suppressHydrationWarning
|
|
72
74
|
dangerouslySetInnerHTML={{ __html: serializeJsonLd(breadcrumbJsonLd) }}
|
|
73
75
|
/>
|
|
74
76
|
</>
|
|
@@ -14,12 +14,25 @@ function getLocale(pathname: string): string {
|
|
|
14
14
|
return supportedLocales.includes(segment) ? segment : defaultLocale;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
function
|
|
17
|
+
function stripLocale(path: string): string {
|
|
18
18
|
if (!path.startsWith('/')) return path;
|
|
19
|
-
// Don't double-prefix
|
|
20
19
|
const firstSeg = path.split('/')[1];
|
|
21
|
-
if (supportedLocales.includes(firstSeg))
|
|
22
|
-
|
|
20
|
+
if (supportedLocales.includes(firstSeg)) {
|
|
21
|
+
return path.slice(`/${firstSeg}`.length) || '/';
|
|
22
|
+
}
|
|
23
|
+
return path;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// As-needed locale prefix:
|
|
27
|
+
// defaultLocale → no prefix (/foo)
|
|
28
|
+
// other locale → /{locale}/foo
|
|
29
|
+
// Any incoming path is first stripped of an existing locale segment so we
|
|
30
|
+
// never double-prefix even if callers already localized.
|
|
31
|
+
function localizePath(path: string, locale: string): string {
|
|
32
|
+
if (!path.startsWith('/')) return path;
|
|
33
|
+
const bare = stripLocale(path);
|
|
34
|
+
if (locale === defaultLocale) return bare;
|
|
35
|
+
return bare === '/' ? `/${locale}` : `/${locale}${bare}`;
|
|
23
36
|
}
|
|
24
37
|
|
|
25
38
|
/**
|
|
@@ -20,9 +20,15 @@ function generateNonce(): string {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
function buildCsp(nonce: string): string {
|
|
23
|
+
const isDev = process.env.NODE_ENV === 'development';
|
|
24
|
+
// Next dev uses webpack's eval-source-map devtool, which requires 'unsafe-eval'
|
|
25
|
+
// to execute module code. Prod builds never eval, so this only loosens dev.
|
|
26
|
+
const scriptSrc = isDev
|
|
27
|
+
? `script-src 'self' 'nonce-${nonce}' 'strict-dynamic' 'unsafe-eval'`
|
|
28
|
+
: `script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`;
|
|
23
29
|
return [
|
|
24
30
|
"default-src 'self'",
|
|
25
|
-
|
|
31
|
+
scriptSrc,
|
|
26
32
|
"style-src 'self' 'unsafe-inline' https://cdn.meshulam.co.il",
|
|
27
33
|
"img-src 'self' data: blob: https:",
|
|
28
34
|
"font-src 'self' data:",
|
|
@@ -61,25 +67,48 @@ export function middleware(request: NextRequest) {
|
|
|
61
67
|
|
|
62
68
|
const pathnameLocale = getLocaleFromPath(pathname);
|
|
63
69
|
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const pathWithoutLocale =
|
|
70
|
+
// As-needed locale routing:
|
|
71
|
+
// /foo → rewrite to /{defaultLocale}/foo (URL stays clean)
|
|
72
|
+
// /{default}/foo → 308-redirect to /foo (canonical, avoids duplicate content)
|
|
73
|
+
// /{other}/foo → proceed as-is
|
|
74
|
+
//
|
|
75
|
+
// Effective locale used for auth/header is `defaultLocale` for clean URLs
|
|
76
|
+
// and the explicit prefix otherwise.
|
|
77
|
+
const effectiveLocale = pathnameLocale || defaultLocale;
|
|
78
|
+
const pathWithoutLocale = pathnameLocale
|
|
79
|
+
? pathname.slice(`/${pathnameLocale}`.length) || '/'
|
|
80
|
+
: pathname;
|
|
81
|
+
|
|
82
|
+
// Auth protection — check before any rewrite/redirect so the login
|
|
83
|
+
// redirect lands on the correct canonical URL.
|
|
73
84
|
const isProtected = PROTECTED_PATHS.some((p) => pathWithoutLocale.startsWith(p));
|
|
74
85
|
if (isProtected) {
|
|
75
86
|
const token = request.cookies.get(TOKEN_COOKIE);
|
|
76
87
|
if (!token?.value) {
|
|
77
|
-
const
|
|
78
|
-
|
|
88
|
+
const loginPath =
|
|
89
|
+
effectiveLocale === defaultLocale ? '/login' : `/${effectiveLocale}/login`;
|
|
90
|
+
return applyCspHeaders(NextResponse.redirect(new URL(loginPath, request.url)), nonce);
|
|
79
91
|
}
|
|
80
92
|
}
|
|
81
93
|
|
|
82
|
-
//
|
|
94
|
+
// Canonicalize: strip the default-locale prefix so /en/foo → /foo.
|
|
95
|
+
if (pathnameLocale === defaultLocale) {
|
|
96
|
+
const url = request.nextUrl.clone();
|
|
97
|
+
url.pathname = pathWithoutLocale;
|
|
98
|
+
return applyCspHeaders(NextResponse.redirect(url, 308), nonce);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// No locale prefix → rewrite internally to /{defaultLocale}/...
|
|
102
|
+
// so the [locale] segment resolves; URL in the bar stays unprefixed.
|
|
103
|
+
if (!pathnameLocale) {
|
|
104
|
+
const url = request.nextUrl.clone();
|
|
105
|
+
url.pathname = `/${defaultLocale}${pathname === '/' ? '' : pathname}`;
|
|
106
|
+
const response = NextResponse.rewrite(url, { request: { headers: requestHeaders } });
|
|
107
|
+
response.headers.set('x-locale', defaultLocale);
|
|
108
|
+
return applyCspHeaders(response, nonce);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Non-default locale prefix → proceed as-is.
|
|
83
112
|
const response = NextResponse.next({ request: { headers: requestHeaders } });
|
|
84
113
|
response.headers.set('x-locale', pathnameLocale);
|
|
85
114
|
return applyCspHeaders(response, nonce);
|
|
@@ -105,9 +134,15 @@ function generateNonce(): string {
|
|
|
105
134
|
}
|
|
106
135
|
|
|
107
136
|
function buildCsp(nonce: string): string {
|
|
137
|
+
const isDev = process.env.NODE_ENV === 'development';
|
|
138
|
+
// Next dev uses webpack's eval-source-map devtool, which requires 'unsafe-eval'
|
|
139
|
+
// to execute module code. Prod builds never eval, so this only loosens dev.
|
|
140
|
+
const scriptSrc = isDev
|
|
141
|
+
? `script-src 'self' 'nonce-${nonce}' 'strict-dynamic' 'unsafe-eval'`
|
|
142
|
+
: `script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`;
|
|
108
143
|
return [
|
|
109
144
|
"default-src 'self'",
|
|
110
|
-
|
|
145
|
+
scriptSrc,
|
|
111
146
|
"style-src 'self' 'unsafe-inline' https://cdn.meshulam.co.il",
|
|
112
147
|
"img-src 'self' data: blob: https:",
|
|
113
148
|
"font-src 'self' data:",
|