create-brainerce-store 1.18.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 -362
  3. package/messages/he.json +366 -362
  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 -204
  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 -519
  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,72 +1,72 @@
1
- import type { Product } from 'brainerce';
2
- import { getProductPriceInfo } from 'brainerce';
3
-
4
- interface ProductJsonLdProps {
5
- product: Product;
6
- url: string;
7
- currency?: string;
8
- }
9
-
10
- export function ProductJsonLd({ product, url, currency = 'USD' }: ProductJsonLdProps) {
11
- const priceInfo = getProductPriceInfo(product);
12
- const imageUrl = product.images?.[0]?.url;
13
- const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || '';
14
-
15
- const productJsonLd = {
16
- '@context': 'https://schema.org',
17
- '@type': 'Product',
18
- name: product.name,
19
- description: product.description || product.name,
20
- image: imageUrl,
21
- url,
22
- sku: product.sku || product.id,
23
- offers: {
24
- '@type': 'Offer',
25
- price: priceInfo.price,
26
- priceCurrency: currency,
27
- availability:
28
- product.inventory?.canPurchase !== false
29
- ? 'https://schema.org/InStock'
30
- : 'https://schema.org/OutOfStock',
31
- url,
32
- },
33
- };
34
-
35
- const breadcrumbJsonLd = {
36
- '@context': 'https://schema.org',
37
- '@type': 'BreadcrumbList',
38
- itemListElement: [
39
- {
40
- '@type': 'ListItem',
41
- position: 1,
42
- name: 'Home',
43
- item: baseUrl || '/',
44
- },
45
- {
46
- '@type': 'ListItem',
47
- position: 2,
48
- name: 'Products',
49
- item: `${baseUrl}/products`,
50
- },
51
- {
52
- '@type': 'ListItem',
53
- position: 3,
54
- name: product.name,
55
- item: url,
56
- },
57
- ],
58
- };
59
-
60
- return (
61
- <>
62
- <script
63
- type="application/ld+json"
64
- dangerouslySetInnerHTML={{ __html: JSON.stringify(productJsonLd) }}
65
- />
66
- <script
67
- type="application/ld+json"
68
- dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbJsonLd) }}
69
- />
70
- </>
71
- );
72
- }
1
+ import type { Product } from 'brainerce';
2
+ import { getProductPriceInfo } from 'brainerce';
3
+
4
+ interface ProductJsonLdProps {
5
+ product: Product;
6
+ url: string;
7
+ currency?: string;
8
+ }
9
+
10
+ export function ProductJsonLd({ product, url, currency = 'USD' }: ProductJsonLdProps) {
11
+ const priceInfo = getProductPriceInfo(product);
12
+ const imageUrl = product.images?.[0]?.url;
13
+ const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || '';
14
+
15
+ const productJsonLd = {
16
+ '@context': 'https://schema.org',
17
+ '@type': 'Product',
18
+ name: product.name,
19
+ description: product.description || product.name,
20
+ image: imageUrl,
21
+ url,
22
+ sku: product.sku || product.id,
23
+ offers: {
24
+ '@type': 'Offer',
25
+ price: priceInfo.price,
26
+ priceCurrency: currency,
27
+ availability:
28
+ product.inventory?.canPurchase !== false
29
+ ? 'https://schema.org/InStock'
30
+ : 'https://schema.org/OutOfStock',
31
+ url,
32
+ },
33
+ };
34
+
35
+ const breadcrumbJsonLd = {
36
+ '@context': 'https://schema.org',
37
+ '@type': 'BreadcrumbList',
38
+ itemListElement: [
39
+ {
40
+ '@type': 'ListItem',
41
+ position: 1,
42
+ name: 'Home',
43
+ item: baseUrl || '/',
44
+ },
45
+ {
46
+ '@type': 'ListItem',
47
+ position: 2,
48
+ name: 'Products',
49
+ item: `${baseUrl}/products`,
50
+ },
51
+ {
52
+ '@type': 'ListItem',
53
+ position: 3,
54
+ name: product.name,
55
+ item: url,
56
+ },
57
+ ],
58
+ };
59
+
60
+ return (
61
+ <>
62
+ <script
63
+ type="application/ld+json"
64
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(productJsonLd) }}
65
+ />
66
+ <script
67
+ type="application/ld+json"
68
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbJsonLd) }}
69
+ />
70
+ </>
71
+ );
72
+ }
@@ -1,5 +1,26 @@
1
+ <% if (i18nEnabled) { %>
2
+ // Multi-language store — locales loaded dynamically
3
+ export const defaultLocale = '<%= defaultLocale %>';
4
+ export const supportedLocales = <%- supportedLocales %> as const;
5
+ export type Locale = (typeof supportedLocales)[number];
6
+
7
+ const RTL_LOCALES = new Set(['he', 'ar']);
8
+
9
+ export function getDirection(locale: string): 'ltr' | 'rtl' {
10
+ return RTL_LOCALES.has(locale) ? 'rtl' : 'ltr';
11
+ }
12
+
13
+ export async function getMessages(locale: string): Promise<Record<string, Record<string, string>>> {
14
+ try {
15
+ return (await import(`../messages/${locale}.json`)).default;
16
+ } catch {
17
+ return (await import(`../messages/<%= defaultLocale %>.json`)).default;
18
+ }
19
+ }
20
+ <% } else { %>
1
21
  import messages from '../messages/<%= language %>.json';
2
22
 
3
23
  export const defaultLocale = '<%= language %>';
4
24
  export const direction = '<%= direction %>' as 'ltr' | 'rtl';
5
25
  export { messages };
26
+ <% } %>
@@ -1,149 +1,149 @@
1
- /**
2
- * Client-side auth helpers that call the BFF proxy API routes.
3
- * All mutating requests include the CSRF header.
4
- * The token is managed server-side via httpOnly cookies — never exposed to JS.
5
- */
6
-
7
- const CONNECTION_ID = process.env.NEXT_PUBLIC_BRAINERCE_CONNECTION_ID || '';
8
-
9
- const CSRF_HEADERS: Record<string, string> = {
10
- 'Content-Type': 'application/json',
11
- 'X-Requested-With': 'brainerce',
12
- };
13
-
14
- interface LoginResult {
15
- customer: {
16
- id: string;
17
- email: string;
18
- firstName?: string;
19
- lastName?: string;
20
- emailVerified: boolean;
21
- };
22
- expiresAt: string;
23
- requiresVerification?: boolean;
24
- }
25
-
26
- interface RegisterResult {
27
- customer: {
28
- id: string;
29
- email: string;
30
- firstName?: string;
31
- lastName?: string;
32
- emailVerified: boolean;
33
- };
34
- expiresAt: string;
35
- requiresVerification?: boolean;
36
- }
37
-
38
- interface AuthStatus {
39
- isLoggedIn: boolean;
40
- customer?: {
41
- id: string;
42
- email: string;
43
- firstName?: string;
44
- lastName?: string;
45
- phone?: string;
46
- emailVerified: boolean;
47
- };
48
- error?: string;
49
- }
50
-
51
- interface VerifyEmailResult {
52
- verified: boolean;
53
- message?: string;
54
- }
55
-
56
- async function handleResponse<T>(response: Response): Promise<T> {
57
- const data = await response.json();
58
- if (!response.ok) {
59
- throw new Error(data.message || data.error || `Request failed (${response.status})`);
60
- }
61
- return data as T;
62
- }
63
-
64
- /**
65
- * Login via BFF proxy. The proxy sets the httpOnly cookie on success.
66
- */
67
- export async function proxyLogin(email: string, password: string): Promise<LoginResult> {
68
- const response = await fetch(`/api/store/api/vc/${CONNECTION_ID}/customers/login`, {
69
- method: 'POST',
70
- headers: CSRF_HEADERS,
71
- body: JSON.stringify({ email, password }),
72
- });
73
- return handleResponse<LoginResult>(response);
74
- }
75
-
76
- /**
77
- * Register via BFF proxy. The proxy sets the httpOnly cookie on success.
78
- */
79
- export async function proxyRegister(data: {
80
- firstName: string;
81
- lastName: string;
82
- email: string;
83
- password: string;
84
- acceptsMarketing?: boolean;
85
- }): Promise<RegisterResult> {
86
- const response = await fetch(`/api/store/api/vc/${CONNECTION_ID}/customers/register`, {
87
- method: 'POST',
88
- headers: CSRF_HEADERS,
89
- body: JSON.stringify(data),
90
- });
91
- return handleResponse<RegisterResult>(response);
92
- }
93
-
94
- /**
95
- * Check auth status. Reads httpOnly cookie server-side and validates with backend.
96
- */
97
- export async function checkAuthStatus(): Promise<AuthStatus> {
98
- const response = await fetch('/api/auth/me');
99
- return response.json();
100
- }
101
-
102
- /**
103
- * Logout. Clears httpOnly auth cookies server-side.
104
- */
105
- export async function proxyLogout(): Promise<void> {
106
- await fetch('/api/auth/logout', {
107
- method: 'POST',
108
- headers: { 'X-Requested-With': 'brainerce' },
109
- });
110
- }
111
-
112
- /**
113
- * Verify email via BFF proxy. The auth token is in the httpOnly cookie (set during login/register).
114
- * The proxy adds the Authorization header automatically.
115
- */
116
- export async function proxyVerifyEmail(code: string): Promise<VerifyEmailResult> {
117
- const response = await fetch(`/api/store/api/vc/${CONNECTION_ID}/customers/verify-email`, {
118
- method: 'POST',
119
- headers: CSRF_HEADERS,
120
- body: JSON.stringify({ code }),
121
- });
122
- return handleResponse<VerifyEmailResult>(response);
123
- }
124
-
125
- /**
126
- * Resend verification email via BFF proxy.
127
- * Uses the auth token from the httpOnly cookie.
128
- */
129
- export async function proxyResendVerification(): Promise<{ message: string }> {
130
- const response = await fetch(`/api/store/api/vc/${CONNECTION_ID}/customers/resend-verification`, {
131
- method: 'POST',
132
- headers: CSRF_HEADERS,
133
- });
134
- return handleResponse<{ message: string }>(response);
135
- }
136
-
137
- /**
138
- * Reset password via BFF proxy.
139
- * The reset token is in an httpOnly cookie (set by /api/auth/reset-callback when the user
140
- * clicked the email link). The proxy reads it server-side — the token never reaches client JS.
141
- */
142
- export async function proxyResetPassword(newPassword: string): Promise<{ message: string }> {
143
- const response = await fetch('/api/auth/reset-password', {
144
- method: 'POST',
145
- headers: CSRF_HEADERS,
146
- body: JSON.stringify({ newPassword }),
147
- });
148
- return handleResponse<{ message: string }>(response);
149
- }
1
+ /**
2
+ * Client-side auth helpers that call the BFF proxy API routes.
3
+ * All mutating requests include the CSRF header.
4
+ * The token is managed server-side via httpOnly cookies — never exposed to JS.
5
+ */
6
+
7
+ const CONNECTION_ID = process.env.NEXT_PUBLIC_BRAINERCE_CONNECTION_ID || '';
8
+
9
+ const CSRF_HEADERS: Record<string, string> = {
10
+ 'Content-Type': 'application/json',
11
+ 'X-Requested-With': 'brainerce',
12
+ };
13
+
14
+ interface LoginResult {
15
+ customer: {
16
+ id: string;
17
+ email: string;
18
+ firstName?: string;
19
+ lastName?: string;
20
+ emailVerified: boolean;
21
+ };
22
+ expiresAt: string;
23
+ requiresVerification?: boolean;
24
+ }
25
+
26
+ interface RegisterResult {
27
+ customer: {
28
+ id: string;
29
+ email: string;
30
+ firstName?: string;
31
+ lastName?: string;
32
+ emailVerified: boolean;
33
+ };
34
+ expiresAt: string;
35
+ requiresVerification?: boolean;
36
+ }
37
+
38
+ interface AuthStatus {
39
+ isLoggedIn: boolean;
40
+ customer?: {
41
+ id: string;
42
+ email: string;
43
+ firstName?: string;
44
+ lastName?: string;
45
+ phone?: string;
46
+ emailVerified: boolean;
47
+ };
48
+ error?: string;
49
+ }
50
+
51
+ interface VerifyEmailResult {
52
+ verified: boolean;
53
+ message?: string;
54
+ }
55
+
56
+ async function handleResponse<T>(response: Response): Promise<T> {
57
+ const data = await response.json();
58
+ if (!response.ok) {
59
+ throw new Error(data.message || data.error || `Request failed (${response.status})`);
60
+ }
61
+ return data as T;
62
+ }
63
+
64
+ /**
65
+ * Login via BFF proxy. The proxy sets the httpOnly cookie on success.
66
+ */
67
+ export async function proxyLogin(email: string, password: string): Promise<LoginResult> {
68
+ const response = await fetch(`/api/store/api/vc/${CONNECTION_ID}/customers/login`, {
69
+ method: 'POST',
70
+ headers: CSRF_HEADERS,
71
+ body: JSON.stringify({ email, password }),
72
+ });
73
+ return handleResponse<LoginResult>(response);
74
+ }
75
+
76
+ /**
77
+ * Register via BFF proxy. The proxy sets the httpOnly cookie on success.
78
+ */
79
+ export async function proxyRegister(data: {
80
+ firstName: string;
81
+ lastName: string;
82
+ email: string;
83
+ password: string;
84
+ acceptsMarketing?: boolean;
85
+ }): Promise<RegisterResult> {
86
+ const response = await fetch(`/api/store/api/vc/${CONNECTION_ID}/customers/register`, {
87
+ method: 'POST',
88
+ headers: CSRF_HEADERS,
89
+ body: JSON.stringify(data),
90
+ });
91
+ return handleResponse<RegisterResult>(response);
92
+ }
93
+
94
+ /**
95
+ * Check auth status. Reads httpOnly cookie server-side and validates with backend.
96
+ */
97
+ export async function checkAuthStatus(): Promise<AuthStatus> {
98
+ const response = await fetch('/api/auth/me');
99
+ return response.json();
100
+ }
101
+
102
+ /**
103
+ * Logout. Clears httpOnly auth cookies server-side.
104
+ */
105
+ export async function proxyLogout(): Promise<void> {
106
+ await fetch('/api/auth/logout', {
107
+ method: 'POST',
108
+ headers: { 'X-Requested-With': 'brainerce' },
109
+ });
110
+ }
111
+
112
+ /**
113
+ * Verify email via BFF proxy. The auth token is in the httpOnly cookie (set during login/register).
114
+ * The proxy adds the Authorization header automatically.
115
+ */
116
+ export async function proxyVerifyEmail(code: string): Promise<VerifyEmailResult> {
117
+ const response = await fetch(`/api/store/api/vc/${CONNECTION_ID}/customers/verify-email`, {
118
+ method: 'POST',
119
+ headers: CSRF_HEADERS,
120
+ body: JSON.stringify({ code }),
121
+ });
122
+ return handleResponse<VerifyEmailResult>(response);
123
+ }
124
+
125
+ /**
126
+ * Resend verification email via BFF proxy.
127
+ * Uses the auth token from the httpOnly cookie.
128
+ */
129
+ export async function proxyResendVerification(): Promise<{ message: string }> {
130
+ const response = await fetch(`/api/store/api/vc/${CONNECTION_ID}/customers/resend-verification`, {
131
+ method: 'POST',
132
+ headers: CSRF_HEADERS,
133
+ });
134
+ return handleResponse<{ message: string }>(response);
135
+ }
136
+
137
+ /**
138
+ * Reset password via BFF proxy.
139
+ * The reset token is in an httpOnly cookie (set by /api/auth/reset-callback when the user
140
+ * clicked the email link). The proxy reads it server-side — the token never reaches client JS.
141
+ */
142
+ export async function proxyResetPassword(newPassword: string): Promise<{ message: string }> {
143
+ const response = await fetch('/api/auth/reset-password', {
144
+ method: 'POST',
145
+ headers: CSRF_HEADERS,
146
+ body: JSON.stringify({ newPassword }),
147
+ });
148
+ return handleResponse<{ message: string }>(response);
149
+ }
@@ -15,6 +15,15 @@ export function getClient(): BrainerceClient {
15
15
  }
16
16
  return clientInstance;
17
17
  }
18
+ <% if (i18nEnabled) { %>
19
+
20
+ /** Initialize client with a specific locale for translated content */
21
+ export function initClientWithLocale(locale: string): BrainerceClient {
22
+ const client = getClient();
23
+ client.setLocale(locale);
24
+ return client;
25
+ }
26
+ <% } %>
18
27
 
19
28
  // Cart ID helpers (not a security token — safe in localStorage)
20
29
  const CART_ID_KEY = 'brainerce_cart_id';
@@ -0,0 +1,31 @@
1
+ <% if (i18nEnabled) { %>
2
+ 'use client';
3
+
4
+ import { createContext, useContext } from 'react';
5
+
6
+ type Messages = Record<string, Record<string, string>>;
7
+
8
+ const MessagesContext = createContext<Messages>({});
9
+
10
+ export { MessagesContext };
11
+
12
+ export function useTranslations(namespace: string) {
13
+ const messages = useContext(MessagesContext);
14
+ const ns = (messages[namespace] || {}) as Record<string, string>;
15
+ return function t(key: string): string {
16
+ return ns[key] || `${namespace}.${key}`;
17
+ };
18
+ }
19
+ <% } else { %>
20
+ import { messages } from '@/i18n';
21
+
22
+ type Messages = typeof messages;
23
+ type Namespace = keyof Messages;
24
+
25
+ export function useTranslations<N extends Namespace>(namespace: N) {
26
+ const ns = messages[namespace] as Record<string, string>;
27
+ return function t(key: keyof Messages[N]): string {
28
+ return ns[key as string] || `${String(namespace)}.${key as string}`;
29
+ };
30
+ }
31
+ <% } %>
@@ -0,0 +1,81 @@
1
+ <% if (i18nEnabled) { %>
2
+ import { NextRequest, NextResponse } from 'next/server';
3
+
4
+ const TOKEN_COOKIE = 'brainerce_customer_token';
5
+ const PROTECTED_PATHS = ['/account'];
6
+ const supportedLocales = <%- supportedLocales %>;
7
+ const defaultLocale = '<%= defaultLocale %>';
8
+
9
+ function getLocaleFromPath(pathname: string): string | null {
10
+ const segment = pathname.split('/')[1];
11
+ return supportedLocales.includes(segment) ? segment : null;
12
+ }
13
+
14
+ export function middleware(request: NextRequest) {
15
+ const { pathname } = request.nextUrl;
16
+
17
+ // Skip static files and API routes
18
+ if (
19
+ pathname.startsWith('/api/') ||
20
+ pathname.startsWith('/_next/') ||
21
+ pathname.includes('.')
22
+ ) {
23
+ return NextResponse.next();
24
+ }
25
+
26
+ const pathnameLocale = getLocaleFromPath(pathname);
27
+
28
+ // Redirect to default locale if no locale prefix
29
+ if (!pathnameLocale) {
30
+ const url = request.nextUrl.clone();
31
+ url.pathname = `/${defaultLocale}${pathname}`;
32
+ return NextResponse.redirect(url);
33
+ }
34
+
35
+ // Auth protection (with locale prefix)
36
+ const pathWithoutLocale = pathname.replace(`/${pathnameLocale}`, '') || '/';
37
+ const isProtected = PROTECTED_PATHS.some((p) => pathWithoutLocale.startsWith(p));
38
+ if (isProtected) {
39
+ const token = request.cookies.get(TOKEN_COOKIE);
40
+ if (!token?.value) {
41
+ const loginUrl = new URL(`/${pathnameLocale}/login`, request.url);
42
+ return NextResponse.redirect(loginUrl);
43
+ }
44
+ }
45
+
46
+ // Set locale header for server components
47
+ const response = NextResponse.next();
48
+ response.headers.set('x-locale', pathnameLocale);
49
+ return response;
50
+ }
51
+
52
+ export const config = {
53
+ matcher: ['/((?!_next|api|.*\\..*).*)'],
54
+ };
55
+ <% } else { %>
56
+ import { NextRequest, NextResponse } from 'next/server';
57
+
58
+ const TOKEN_COOKIE = 'brainerce_customer_token';
59
+
60
+ /** Routes that require customer authentication */
61
+ const PROTECTED_PATHS = ['/account'];
62
+
63
+ export function middleware(request: NextRequest) {
64
+ const { pathname } = request.nextUrl;
65
+ const isProtected = PROTECTED_PATHS.some((p) => pathname.startsWith(p));
66
+
67
+ if (isProtected) {
68
+ const token = request.cookies.get(TOKEN_COOKIE);
69
+ if (!token?.value) {
70
+ const loginUrl = new URL('/login', request.url);
71
+ return NextResponse.redirect(loginUrl);
72
+ }
73
+ }
74
+
75
+ return NextResponse.next();
76
+ }
77
+
78
+ export const config = {
79
+ matcher: ['/account/:path*'],
80
+ };
81
+ <% } %>