create-brainerce-store 1.67.0 → 1.71.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.
- package/dist/index.js +22 -2
- package/messages/en.json +52 -2
- package/messages/he.json +52 -2
- package/package.json +1 -1
- package/templates/nextjs/base/.env.local.ejs +7 -0
- package/templates/nextjs/base/AGENTS.md.ejs +7 -0
- package/templates/nextjs/base/CLAUDE.md.ejs +7 -0
- package/templates/nextjs/base/src/app/blog/[slug]/page.tsx.ejs +8 -2
- package/templates/nextjs/base/src/app/category/[slug]/page.tsx +16 -7
- package/templates/nextjs/base/src/app/checkout/page.tsx +1018 -1017
- package/templates/nextjs/base/src/app/error.tsx.ejs +53 -0
- package/templates/nextjs/base/src/app/pages/[slug]/page.tsx.ejs +8 -2
- package/templates/nextjs/base/src/app/products/[slug]/page.tsx +17 -7
- package/templates/nextjs/base/src/app/register/page.tsx +67 -64
- package/templates/nextjs/base/src/components/account/profile-section.tsx +303 -226
- package/templates/nextjs/base/src/components/auth/register-form.tsx +326 -245
- package/templates/nextjs/base/src/components/checkout/custom-fields-step.tsx +306 -294
- package/templates/nextjs/base/src/components/checkout/date-picker.tsx +13 -1
- package/templates/nextjs/base/src/components/checkout/datetime-picker.tsx +61 -21
- package/templates/nextjs/base/src/components/shared/birthday-picker.tsx +258 -0
- package/templates/nextjs/base/src/core/lib/auth.ts +162 -154
- package/templates/nextjs/base/src/core/lib/birthday.ts +74 -0
- package/templates/nextjs/base/src/core/lib/site-url.ts +42 -9
- package/templates/nextjs/base/src/core/lib/store-info.ts +10 -0
- package/templates/nextjs/base/src/ui/layout/newsletter-signup.tsx +143 -0
- package/templates/nextjs/base/src/ui/layout/site-footer.tsx.ejs +18 -2
- package/templates/nextjs/base/src/ui/product/back-in-stock-form.tsx +173 -0
- package/templates/nextjs/base/src/ui/product/product-client-section.tsx +484 -455
- package/templates/nextjs/base/src/ui/product/review-form.tsx +136 -12
- package/templates/nextjs/base/src/ui/product/reviews-section.tsx.ejs +139 -108
- package/templates/nextjs/designs/atelier/ui/layout/site-footer.tsx.ejs +155 -142
- package/templates/nextjs/designs/atelier/ui/product/product-client-section.tsx +500 -477
- package/templates/nextjs/designs/atelier/ui/product/review-form.tsx +135 -11
- package/templates/nextjs/designs/atelier/ui/product/reviews-section.tsx.ejs +179 -148
- package/templates/nextjs/ui-canvas/layout/newsletter-signup.tsx +122 -0
- package/templates/nextjs/ui-canvas/layout/site-footer.tsx.ejs +87 -83
- package/templates/nextjs/ui-canvas/product/back-in-stock-form.tsx +151 -0
- package/templates/nextjs/ui-canvas/product/product-client-section.tsx +373 -352
- package/templates/nextjs/ui-canvas/product/review-form.tsx +129 -11
- package/templates/nextjs/ui-canvas/product/reviews-section.tsx.ejs +127 -96
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect } from 'react';
|
|
4
|
+
import Link from 'next/link';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Error page — rendered when a page render throws. Kept deliberately separate
|
|
8
|
+
* from the 404 page: an API failure (a rejected Origin, a network error, a
|
|
9
|
+
* 5xx) is NOT "product not found", and dressing it up as a 404 sends the
|
|
10
|
+
* merchant hunting for a slug bug that isn't there. The full error is in the
|
|
11
|
+
* server log; this page only needs to say "temporary" and offer a retry.
|
|
12
|
+
*/
|
|
13
|
+
export default function ErrorPage({
|
|
14
|
+
error,
|
|
15
|
+
reset,
|
|
16
|
+
}: {
|
|
17
|
+
error: Error & { digest?: string };
|
|
18
|
+
reset: () => void;
|
|
19
|
+
}) {
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
// The server log has the full error; the digest printed here links the two.
|
|
22
|
+
console.error(error);
|
|
23
|
+
}, [error]);
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div className="mx-auto flex max-w-2xl flex-col items-center justify-center px-4 py-24 text-center">
|
|
27
|
+
<p className="text-sm font-medium text-muted-foreground">
|
|
28
|
+
<%- language === 'he' ? 'שגיאה' : 'Error' %>
|
|
29
|
+
</p>
|
|
30
|
+
<h1 className="mt-2 text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
|
|
31
|
+
<%- language === 'he' ? 'משהו השתבש' : 'Something went wrong' %>
|
|
32
|
+
</h1>
|
|
33
|
+
<p className="mt-4 text-muted-foreground">
|
|
34
|
+
<%- language === 'he' ? 'זו תקלה זמנית בטעינת העמוד — לא עמוד חסר. נסו שוב בעוד רגע.' : 'This is a temporary problem loading the page — not a missing page. Please try again in a moment.' %>
|
|
35
|
+
</p>
|
|
36
|
+
<div className="mt-8 flex gap-4">
|
|
37
|
+
<button
|
|
38
|
+
type="button"
|
|
39
|
+
onClick={reset}
|
|
40
|
+
className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90"
|
|
41
|
+
>
|
|
42
|
+
<%- language === 'he' ? 'לנסות שוב' : 'Try again' %>
|
|
43
|
+
</button>
|
|
44
|
+
<Link
|
|
45
|
+
href="/"
|
|
46
|
+
className="rounded-md border border-border px-4 py-2 text-sm font-medium text-foreground hover:bg-muted"
|
|
47
|
+
>
|
|
48
|
+
<%- language === 'he' ? 'לדף הבית' : 'Go home' %>
|
|
49
|
+
</Link>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -40,7 +40,10 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
|
|
|
40
40
|
export default async function StaticPage({ params }: PageProps) {
|
|
41
41
|
const { locale, slug: rawSlug } = await params;
|
|
42
42
|
const slug = decodeSlug(rawSlug);
|
|
43
|
-
|
|
43
|
+
// getBySlug returns null only on a genuine 404; any other API failure (a
|
|
44
|
+
// 403 from the channel's origin check, a network error) throws and surfaces
|
|
45
|
+
// as the error it is instead of masquerading as a missing page.
|
|
46
|
+
const page = await (await getServerClient(locale)).content.page.getBySlug(slug, locale);
|
|
44
47
|
if (!page) notFound();
|
|
45
48
|
return (
|
|
46
49
|
<article className="mx-auto max-w-3xl px-4 py-10 sm:px-6 lg:px-8">
|
|
@@ -75,7 +78,10 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
|
|
|
75
78
|
export default async function StaticPage({ params }: PageProps) {
|
|
76
79
|
const { slug: rawSlug } = await params;
|
|
77
80
|
const slug = decodeSlug(rawSlug);
|
|
78
|
-
|
|
81
|
+
// getBySlug returns null only on a genuine 404; any other API failure (a
|
|
82
|
+
// 403 from the channel's origin check, a network error) throws and surfaces
|
|
83
|
+
// as the error it is instead of masquerading as a missing page.
|
|
84
|
+
const page = await (await getServerClient()).content.page.getBySlug(slug);
|
|
79
85
|
if (!page) notFound();
|
|
80
86
|
return (
|
|
81
87
|
<article className="mx-auto max-w-3xl px-4 py-10 sm:px-6 lg:px-8">
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Metadata } from 'next';
|
|
2
2
|
import { notFound, permanentRedirect } from 'next/navigation';
|
|
3
|
-
import { getProductPriceInfo } from 'brainerce';
|
|
3
|
+
import { BrainerceError, getProductPriceInfo } from 'brainerce';
|
|
4
4
|
import { getServerClient, fetchStoreInfo } from '@/core/lib/brainerce.server';
|
|
5
5
|
import { resolveCurrency } from '@/core/lib/resolve-currency';
|
|
6
6
|
import { buildMetaDescription } from '@/core/lib/seo';
|
|
@@ -123,10 +123,13 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|
|
123
123
|
...(brandName ? { 'product:brand': brandName } : {}),
|
|
124
124
|
},
|
|
125
125
|
};
|
|
126
|
-
} catch {
|
|
127
|
-
|
|
128
|
-
title: 'Product not found'
|
|
129
|
-
}
|
|
126
|
+
} catch (err) {
|
|
127
|
+
if (err instanceof BrainerceError && err.statusCode === 404) {
|
|
128
|
+
return { title: 'Product not found' };
|
|
129
|
+
}
|
|
130
|
+
// Anything else (channel origin rejection, network failure) hits the page
|
|
131
|
+
// render below too and surfaces there — don't stamp a misleading title.
|
|
132
|
+
return {};
|
|
130
133
|
}
|
|
131
134
|
}
|
|
132
135
|
|
|
@@ -138,7 +141,14 @@ export default async function ProductDetailPage({ params }: Props) {
|
|
|
138
141
|
let product;
|
|
139
142
|
try {
|
|
140
143
|
product = await client.getProductBySlug(slug);
|
|
141
|
-
} catch {
|
|
144
|
+
} catch (err) {
|
|
145
|
+
// Only a genuine 404 may become a storefront 404. Anything else — a 403
|
|
146
|
+
// from the channel's origin check, a network failure, a 5xx — is a
|
|
147
|
+
// configuration or availability problem; disguising it as "product not
|
|
148
|
+
// found" sends the merchant hunting for a slug bug that isn't there.
|
|
149
|
+
if (!(err instanceof BrainerceError && err.statusCode === 404)) {
|
|
150
|
+
throw err;
|
|
151
|
+
}
|
|
142
152
|
// The slug may have been RENAMED — the platform records every rename.
|
|
143
153
|
// 301 the old URL to the current slug so its ranking and inbound links
|
|
144
154
|
// carry over; genuine unknowns fall through to the 404 page. Default
|
|
@@ -182,7 +192,7 @@ export default async function ProductDetailPage({ params }: Props) {
|
|
|
182
192
|
<div className="mx-auto max-w-7xl px-4 pt-6 sm:px-6 lg:px-8">
|
|
183
193
|
<Breadcrumbs items={breadcrumbItems} />
|
|
184
194
|
</div>
|
|
185
|
-
<ProductClientSection product={product} />
|
|
195
|
+
<ProductClientSection product={product} stockAlertsEnabled={storeInfo?.stockAlertsEnabled} />
|
|
186
196
|
{/* Visible Q&A + the FAQPage JSON-LD above read the same product.faq —
|
|
187
197
|
the extractable format AI answer engines cite. */}
|
|
188
198
|
<ProductFaqSection product={product} locale={locale} />
|
|
@@ -1,64 +1,67 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useState } from 'react';
|
|
4
|
-
import { useRouter, Link } from '@/core/lib/navigation';
|
|
5
|
-
import { useAuth } from '@/core/providers/store-provider';
|
|
6
|
-
import { proxyRegister } from '@/core/lib/auth';
|
|
7
|
-
import { RegisterForm } from '@/components/auth/register-form';
|
|
8
|
-
import { OAuthButtons } from '@/components/auth/oauth-buttons';
|
|
9
|
-
import { useTranslations } from '@/core/lib/translations';
|
|
10
|
-
|
|
11
|
-
export default function RegisterPage() {
|
|
12
|
-
const router = useRouter();
|
|
13
|
-
const auth = useAuth();
|
|
14
|
-
const t = useTranslations('auth');
|
|
15
|
-
const [error, setError] = useState<string | null>(null);
|
|
16
|
-
|
|
17
|
-
async function handleRegister(data: {
|
|
18
|
-
firstName: string;
|
|
19
|
-
lastName: string;
|
|
20
|
-
email: string;
|
|
21
|
-
password: string;
|
|
22
|
-
acceptsMarketing: boolean;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { useRouter, Link } from '@/core/lib/navigation';
|
|
5
|
+
import { useAuth } from '@/core/providers/store-provider';
|
|
6
|
+
import { proxyRegister } from '@/core/lib/auth';
|
|
7
|
+
import { RegisterForm } from '@/components/auth/register-form';
|
|
8
|
+
import { OAuthButtons } from '@/components/auth/oauth-buttons';
|
|
9
|
+
import { useTranslations } from '@/core/lib/translations';
|
|
10
|
+
|
|
11
|
+
export default function RegisterPage() {
|
|
12
|
+
const router = useRouter();
|
|
13
|
+
const auth = useAuth();
|
|
14
|
+
const t = useTranslations('auth');
|
|
15
|
+
const [error, setError] = useState<string | null>(null);
|
|
16
|
+
|
|
17
|
+
async function handleRegister(data: {
|
|
18
|
+
firstName: string;
|
|
19
|
+
lastName: string;
|
|
20
|
+
email: string;
|
|
21
|
+
password: string;
|
|
22
|
+
acceptsMarketing: boolean;
|
|
23
|
+
/** Month and day only, never a year. Present together or not at all. */
|
|
24
|
+
birthMonth?: number;
|
|
25
|
+
birthDay?: number;
|
|
26
|
+
}) {
|
|
27
|
+
try {
|
|
28
|
+
setError(null);
|
|
29
|
+
const result = await proxyRegister(data);
|
|
30
|
+
|
|
31
|
+
if (result.requiresVerification) {
|
|
32
|
+
// Cookie already set by proxy; verify-email uses it for auth
|
|
33
|
+
router.push('/verify-email');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Cookie was set by the proxy; refresh auth state
|
|
38
|
+
await auth.login();
|
|
39
|
+
router.push('/');
|
|
40
|
+
} catch (err) {
|
|
41
|
+
const message = err instanceof Error ? err.message : 'Registration failed. Please try again.';
|
|
42
|
+
setError(message);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div className="flex min-h-[60vh] items-center justify-center px-4 py-12">
|
|
48
|
+
<div className="w-full max-w-md space-y-6">
|
|
49
|
+
<div className="text-center">
|
|
50
|
+
<h1 className="text-foreground text-2xl font-bold">{t('createAccountTitle')}</h1>
|
|
51
|
+
<p className="text-muted-foreground mt-1 text-sm">{t('joinSubtitle')}</p>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<RegisterForm onSubmit={handleRegister} error={error} />
|
|
55
|
+
|
|
56
|
+
<OAuthButtons />
|
|
57
|
+
|
|
58
|
+
<p className="text-muted-foreground text-center text-sm">
|
|
59
|
+
{t('alreadyHaveAccount')}{' '}
|
|
60
|
+
<Link href="/login" className="text-primary font-medium hover:underline">
|
|
61
|
+
{t('signIn')}
|
|
62
|
+
</Link>
|
|
63
|
+
</p>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|