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,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,65 +1,65 @@
1
- 'use client';
2
-
3
- import { useState } from 'react';
4
- import { useRouter } from 'next/navigation';
5
- import Link from 'next/link';
6
- import { useAuth } from '@/providers/store-provider';
7
- import { proxyRegister } from '@/lib/auth';
8
- import { RegisterForm } from '@/components/auth/register-form';
9
- import { OAuthButtons } from '@/components/auth/oauth-buttons';
10
- import { useTranslations } from '@/lib/translations';
11
-
12
- export default function RegisterPage() {
13
- const router = useRouter();
14
- const auth = useAuth();
15
- const t = useTranslations('auth');
16
- const [error, setError] = useState<string | null>(null);
17
-
18
- async function handleRegister(data: {
19
- firstName: string;
20
- lastName: string;
21
- email: string;
22
- password: string;
23
- acceptsMarketing: boolean;
24
- }) {
25
- try {
26
- setError(null);
27
- const result = await proxyRegister(data);
28
-
29
- if (result.requiresVerification) {
30
- // Cookie already set by proxy; verify-email uses it for auth
31
- router.push('/verify-email');
32
- return;
33
- }
34
-
35
- // Cookie was set by the proxy; refresh auth state
36
- await auth.login();
37
- router.push('/');
38
- } catch (err) {
39
- const message = err instanceof Error ? err.message : 'Registration failed. Please try again.';
40
- setError(message);
41
- }
42
- }
43
-
44
- return (
45
- <div className="flex min-h-[60vh] items-center justify-center px-4 py-12">
46
- <div className="w-full max-w-md space-y-6">
47
- <div className="text-center">
48
- <h1 className="text-foreground text-2xl font-bold">{t('createAccountTitle')}</h1>
49
- <p className="text-muted-foreground mt-1 text-sm">{t('joinSubtitle')}</p>
50
- </div>
51
-
52
- <RegisterForm onSubmit={handleRegister} error={error} />
53
-
54
- <OAuthButtons />
55
-
56
- <p className="text-muted-foreground text-center text-sm">
57
- {t('alreadyHaveAccount')}{' '}
58
- <Link href="/login" className="text-primary font-medium hover:underline">
59
- {t('signIn')}
60
- </Link>
61
- </p>
62
- </div>
63
- </div>
64
- );
65
- }
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { useRouter } from 'next/navigation';
5
+ import Link from 'next/link';
6
+ import { useAuth } from '@/providers/store-provider';
7
+ import { proxyRegister } from '@/lib/auth';
8
+ import { RegisterForm } from '@/components/auth/register-form';
9
+ import { OAuthButtons } from '@/components/auth/oauth-buttons';
10
+ import { useTranslations } from '@/lib/translations';
11
+
12
+ export default function RegisterPage() {
13
+ const router = useRouter();
14
+ const auth = useAuth();
15
+ const t = useTranslations('auth');
16
+ const [error, setError] = useState<string | null>(null);
17
+
18
+ async function handleRegister(data: {
19
+ firstName: string;
20
+ lastName: string;
21
+ email: string;
22
+ password: string;
23
+ acceptsMarketing: boolean;
24
+ }) {
25
+ try {
26
+ setError(null);
27
+ const result = await proxyRegister(data);
28
+
29
+ if (result.requiresVerification) {
30
+ // Cookie already set by proxy; verify-email uses it for auth
31
+ router.push('/verify-email');
32
+ return;
33
+ }
34
+
35
+ // Cookie was set by the proxy; refresh auth state
36
+ await auth.login();
37
+ router.push('/');
38
+ } catch (err) {
39
+ const message = err instanceof Error ? err.message : 'Registration failed. Please try again.';
40
+ setError(message);
41
+ }
42
+ }
43
+
44
+ return (
45
+ <div className="flex min-h-[60vh] items-center justify-center px-4 py-12">
46
+ <div className="w-full max-w-md space-y-6">
47
+ <div className="text-center">
48
+ <h1 className="text-foreground text-2xl font-bold">{t('createAccountTitle')}</h1>
49
+ <p className="text-muted-foreground mt-1 text-sm">{t('joinSubtitle')}</p>
50
+ </div>
51
+
52
+ <RegisterForm onSubmit={handleRegister} error={error} />
53
+
54
+ <OAuthButtons />
55
+
56
+ <p className="text-muted-foreground text-center text-sm">
57
+ {t('alreadyHaveAccount')}{' '}
58
+ <Link href="/login" className="text-primary font-medium hover:underline">
59
+ {t('signIn')}
60
+ </Link>
61
+ </p>
62
+ </div>
63
+ </div>
64
+ );
65
+ }
@@ -1,132 +1,132 @@
1
- 'use client';
2
-
3
- import { useState } from 'react';
4
- import { useRouter } from 'next/navigation';
5
- import Link from 'next/link';
6
- import { proxyResetPassword } from '@/lib/auth';
7
- import { LoadingSpinner } from '@/components/shared/loading-spinner';
8
- import { useTranslations } from '@/lib/translations';
9
-
10
- export default function ResetPasswordPage() {
11
- const router = useRouter();
12
- const t = useTranslations('auth');
13
-
14
- const [newPassword, setNewPassword] = useState('');
15
- const [confirmPassword, setConfirmPassword] = useState('');
16
- const [loading, setLoading] = useState(false);
17
- const [error, setError] = useState<string | null>(null);
18
- const [success, setSuccess] = useState(false);
19
-
20
- async function handleSubmit(e: React.FormEvent) {
21
- e.preventDefault();
22
- if (loading) return;
23
-
24
- if (newPassword !== confirmPassword) {
25
- setError(t('passwordsMustMatch'));
26
- return;
27
- }
28
-
29
- try {
30
- setLoading(true);
31
- setError(null);
32
- await proxyResetPassword(newPassword);
33
- setSuccess(true);
34
- setTimeout(() => router.push('/login'), 2000);
35
- } catch (err) {
36
- const message =
37
- err instanceof Error ? err.message : 'Something went wrong. Please try again.';
38
- setError(message);
39
- } finally {
40
- setLoading(false);
41
- }
42
- }
43
-
44
- return (
45
- <div className="flex min-h-[60vh] items-center justify-center px-4 py-12">
46
- <div className="w-full max-w-md space-y-6">
47
- <div className="text-center">
48
- <h1 className="text-foreground text-2xl font-bold">{t('resetPasswordTitle')}</h1>
49
- <p className="text-muted-foreground mt-1 text-sm">{t('resetPasswordSubtitle')}</p>
50
- </div>
51
-
52
- {error && (
53
- <div className="bg-destructive/10 border-destructive/20 text-destructive space-y-2 rounded-lg border px-4 py-3 text-sm">
54
- <p>{error}</p>
55
- <Link
56
- href="/forgot-password"
57
- className="text-primary block font-medium hover:underline"
58
- >
59
- {t('sendResetLink')}
60
- </Link>
61
- </div>
62
- )}
63
-
64
- {success ? (
65
- <div className="rounded-lg border border-green-200 bg-green-50 px-4 py-3 text-sm text-green-800 dark:border-green-800 dark:bg-green-950/30 dark:text-green-300">
66
- {t('passwordResetSuccess')}
67
- </div>
68
- ) : (
69
- <form onSubmit={handleSubmit} className="space-y-4">
70
- <div>
71
- <label
72
- htmlFor="new-password"
73
- className="text-foreground mb-1.5 block text-sm font-medium"
74
- >
75
- {t('newPassword')}
76
- </label>
77
- <input
78
- id="new-password"
79
- type="password"
80
- required
81
- minLength={8}
82
- value={newPassword}
83
- onChange={(e) => setNewPassword(e.target.value)}
84
- placeholder={t('newPasswordPlaceholder')}
85
- autoComplete="new-password"
86
- className="border-border bg-background text-foreground placeholder:text-muted-foreground focus:ring-primary/20 focus:border-primary h-10 w-full rounded border px-3 text-sm focus:outline-none focus:ring-2"
87
- />
88
- </div>
89
-
90
- <div>
91
- <label
92
- htmlFor="confirm-password"
93
- className="text-foreground mb-1.5 block text-sm font-medium"
94
- >
95
- {t('confirmPassword')}
96
- </label>
97
- <input
98
- id="confirm-password"
99
- type="password"
100
- required
101
- minLength={8}
102
- value={confirmPassword}
103
- onChange={(e) => setConfirmPassword(e.target.value)}
104
- placeholder={t('confirmPasswordPlaceholder')}
105
- autoComplete="new-password"
106
- className="border-border bg-background text-foreground placeholder:text-muted-foreground focus:ring-primary/20 focus:border-primary h-10 w-full rounded border px-3 text-sm focus:outline-none focus:ring-2"
107
- />
108
- </div>
109
-
110
- <button
111
- type="submit"
112
- disabled={loading}
113
- className="bg-primary text-primary-foreground flex h-10 w-full items-center justify-center gap-2 rounded text-sm font-medium transition-opacity hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50"
114
- >
115
- {loading ? (
116
- <>
117
- <LoadingSpinner
118
- size="sm"
119
- className="border-primary-foreground/30 border-t-primary-foreground"
120
- />
121
- {t('resettingPassword')}
122
- </>
123
- ) : (
124
- t('resetPassword')
125
- )}
126
- </button>
127
- </form>
128
- )}
129
- </div>
130
- </div>
131
- );
132
- }
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { useRouter } from 'next/navigation';
5
+ import Link from 'next/link';
6
+ import { proxyResetPassword } from '@/lib/auth';
7
+ import { LoadingSpinner } from '@/components/shared/loading-spinner';
8
+ import { useTranslations } from '@/lib/translations';
9
+
10
+ export default function ResetPasswordPage() {
11
+ const router = useRouter();
12
+ const t = useTranslations('auth');
13
+
14
+ const [newPassword, setNewPassword] = useState('');
15
+ const [confirmPassword, setConfirmPassword] = useState('');
16
+ const [loading, setLoading] = useState(false);
17
+ const [error, setError] = useState<string | null>(null);
18
+ const [success, setSuccess] = useState(false);
19
+
20
+ async function handleSubmit(e: React.FormEvent) {
21
+ e.preventDefault();
22
+ if (loading) return;
23
+
24
+ if (newPassword !== confirmPassword) {
25
+ setError(t('passwordsMustMatch'));
26
+ return;
27
+ }
28
+
29
+ try {
30
+ setLoading(true);
31
+ setError(null);
32
+ await proxyResetPassword(newPassword);
33
+ setSuccess(true);
34
+ setTimeout(() => router.push('/login'), 2000);
35
+ } catch (err) {
36
+ const message =
37
+ err instanceof Error ? err.message : 'Something went wrong. Please try again.';
38
+ setError(message);
39
+ } finally {
40
+ setLoading(false);
41
+ }
42
+ }
43
+
44
+ return (
45
+ <div className="flex min-h-[60vh] items-center justify-center px-4 py-12">
46
+ <div className="w-full max-w-md space-y-6">
47
+ <div className="text-center">
48
+ <h1 className="text-foreground text-2xl font-bold">{t('resetPasswordTitle')}</h1>
49
+ <p className="text-muted-foreground mt-1 text-sm">{t('resetPasswordSubtitle')}</p>
50
+ </div>
51
+
52
+ {error && (
53
+ <div className="bg-destructive/10 border-destructive/20 text-destructive space-y-2 rounded-lg border px-4 py-3 text-sm">
54
+ <p>{error}</p>
55
+ <Link
56
+ href="/forgot-password"
57
+ className="text-primary block font-medium hover:underline"
58
+ >
59
+ {t('sendResetLink')}
60
+ </Link>
61
+ </div>
62
+ )}
63
+
64
+ {success ? (
65
+ <div className="rounded-lg border border-green-200 bg-green-50 px-4 py-3 text-sm text-green-800 dark:border-green-800 dark:bg-green-950/30 dark:text-green-300">
66
+ {t('passwordResetSuccess')}
67
+ </div>
68
+ ) : (
69
+ <form onSubmit={handleSubmit} className="space-y-4">
70
+ <div>
71
+ <label
72
+ htmlFor="new-password"
73
+ className="text-foreground mb-1.5 block text-sm font-medium"
74
+ >
75
+ {t('newPassword')}
76
+ </label>
77
+ <input
78
+ id="new-password"
79
+ type="password"
80
+ required
81
+ minLength={8}
82
+ value={newPassword}
83
+ onChange={(e) => setNewPassword(e.target.value)}
84
+ placeholder={t('newPasswordPlaceholder')}
85
+ autoComplete="new-password"
86
+ className="border-border bg-background text-foreground placeholder:text-muted-foreground focus:ring-primary/20 focus:border-primary h-10 w-full rounded border px-3 text-sm focus:outline-none focus:ring-2"
87
+ />
88
+ </div>
89
+
90
+ <div>
91
+ <label
92
+ htmlFor="confirm-password"
93
+ className="text-foreground mb-1.5 block text-sm font-medium"
94
+ >
95
+ {t('confirmPassword')}
96
+ </label>
97
+ <input
98
+ id="confirm-password"
99
+ type="password"
100
+ required
101
+ minLength={8}
102
+ value={confirmPassword}
103
+ onChange={(e) => setConfirmPassword(e.target.value)}
104
+ placeholder={t('confirmPasswordPlaceholder')}
105
+ autoComplete="new-password"
106
+ className="border-border bg-background text-foreground placeholder:text-muted-foreground focus:ring-primary/20 focus:border-primary h-10 w-full rounded border px-3 text-sm focus:outline-none focus:ring-2"
107
+ />
108
+ </div>
109
+
110
+ <button
111
+ type="submit"
112
+ disabled={loading}
113
+ className="bg-primary text-primary-foreground flex h-10 w-full items-center justify-center gap-2 rounded text-sm font-medium transition-opacity hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50"
114
+ >
115
+ {loading ? (
116
+ <>
117
+ <LoadingSpinner
118
+ size="sm"
119
+ className="border-primary-foreground/30 border-t-primary-foreground"
120
+ />
121
+ {t('resettingPassword')}
122
+ </>
123
+ ) : (
124
+ t('resetPassword')
125
+ )}
126
+ </button>
127
+ </form>
128
+ )}
129
+ </div>
130
+ </div>
131
+ );
132
+ }
@@ -1,14 +1,14 @@
1
- import type { MetadataRoute } from 'next';
2
-
3
- export default function robots(): MetadataRoute.Robots {
4
- const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://example.com';
5
-
6
- return {
7
- rules: {
8
- userAgent: '*',
9
- allow: '/',
10
- disallow: ['/api/', '/auth/', '/checkout/', '/account/'],
11
- },
12
- sitemap: `${baseUrl}/sitemap.xml`,
13
- };
14
- }
1
+ import type { MetadataRoute } from 'next';
2
+
3
+ export default function robots(): MetadataRoute.Robots {
4
+ const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://example.com';
5
+
6
+ return {
7
+ rules: {
8
+ userAgent: '*',
9
+ allow: '/',
10
+ disallow: ['/api/', '/auth/', '/checkout/', '/account/'],
11
+ },
12
+ sitemap: `${baseUrl}/sitemap.xml`,
13
+ };
14
+ }
@@ -1,25 +1,25 @@
1
- import type { MetadataRoute } from 'next';
2
- import { getServerClient } from '@/lib/brainerce';
3
-
4
- export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
5
- const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://example.com';
6
-
7
- const staticPages: MetadataRoute.Sitemap = [
8
- { url: baseUrl, lastModified: new Date(), priority: 1 },
9
- { url: `${baseUrl}/products`, lastModified: new Date(), priority: 0.9 },
10
- ];
11
-
12
- try {
13
- const client = getServerClient();
14
- const { data: products } = await client.getProducts({ limit: 1000 });
15
- const productPages: MetadataRoute.Sitemap = products.map((product) => ({
16
- url: `${baseUrl}/products/${product.slug}`,
17
- lastModified: product.updatedAt ? new Date(product.updatedAt) : new Date(),
18
- priority: 0.8,
19
- }));
20
-
21
- return [...staticPages, ...productPages];
22
- } catch {
23
- return staticPages;
24
- }
25
- }
1
+ import type { MetadataRoute } from 'next';
2
+ import { getServerClient } from '@/lib/brainerce';
3
+
4
+ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
5
+ const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://example.com';
6
+
7
+ const staticPages: MetadataRoute.Sitemap = [
8
+ { url: baseUrl, lastModified: new Date(), priority: 1 },
9
+ { url: `${baseUrl}/products`, lastModified: new Date(), priority: 0.9 },
10
+ ];
11
+
12
+ try {
13
+ const client = getServerClient();
14
+ const { data: products } = await client.getProducts({ limit: 1000 });
15
+ const productPages: MetadataRoute.Sitemap = products.map((product) => ({
16
+ url: `${baseUrl}/products/${product.slug}`,
17
+ lastModified: product.updatedAt ? new Date(product.updatedAt) : new Date(),
18
+ priority: 0.8,
19
+ }));
20
+
21
+ return [...staticPages, ...productPages];
22
+ } catch {
23
+ return staticPages;
24
+ }
25
+ }