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.
Files changed (40) hide show
  1. package/dist/index.js +22 -2
  2. package/messages/en.json +52 -2
  3. package/messages/he.json +52 -2
  4. package/package.json +1 -1
  5. package/templates/nextjs/base/.env.local.ejs +7 -0
  6. package/templates/nextjs/base/AGENTS.md.ejs +7 -0
  7. package/templates/nextjs/base/CLAUDE.md.ejs +7 -0
  8. package/templates/nextjs/base/src/app/blog/[slug]/page.tsx.ejs +8 -2
  9. package/templates/nextjs/base/src/app/category/[slug]/page.tsx +16 -7
  10. package/templates/nextjs/base/src/app/checkout/page.tsx +1018 -1017
  11. package/templates/nextjs/base/src/app/error.tsx.ejs +53 -0
  12. package/templates/nextjs/base/src/app/pages/[slug]/page.tsx.ejs +8 -2
  13. package/templates/nextjs/base/src/app/products/[slug]/page.tsx +17 -7
  14. package/templates/nextjs/base/src/app/register/page.tsx +67 -64
  15. package/templates/nextjs/base/src/components/account/profile-section.tsx +303 -226
  16. package/templates/nextjs/base/src/components/auth/register-form.tsx +326 -245
  17. package/templates/nextjs/base/src/components/checkout/custom-fields-step.tsx +306 -294
  18. package/templates/nextjs/base/src/components/checkout/date-picker.tsx +13 -1
  19. package/templates/nextjs/base/src/components/checkout/datetime-picker.tsx +61 -21
  20. package/templates/nextjs/base/src/components/shared/birthday-picker.tsx +258 -0
  21. package/templates/nextjs/base/src/core/lib/auth.ts +162 -154
  22. package/templates/nextjs/base/src/core/lib/birthday.ts +74 -0
  23. package/templates/nextjs/base/src/core/lib/site-url.ts +42 -9
  24. package/templates/nextjs/base/src/core/lib/store-info.ts +10 -0
  25. package/templates/nextjs/base/src/ui/layout/newsletter-signup.tsx +143 -0
  26. package/templates/nextjs/base/src/ui/layout/site-footer.tsx.ejs +18 -2
  27. package/templates/nextjs/base/src/ui/product/back-in-stock-form.tsx +173 -0
  28. package/templates/nextjs/base/src/ui/product/product-client-section.tsx +484 -455
  29. package/templates/nextjs/base/src/ui/product/review-form.tsx +136 -12
  30. package/templates/nextjs/base/src/ui/product/reviews-section.tsx.ejs +139 -108
  31. package/templates/nextjs/designs/atelier/ui/layout/site-footer.tsx.ejs +155 -142
  32. package/templates/nextjs/designs/atelier/ui/product/product-client-section.tsx +500 -477
  33. package/templates/nextjs/designs/atelier/ui/product/review-form.tsx +135 -11
  34. package/templates/nextjs/designs/atelier/ui/product/reviews-section.tsx.ejs +179 -148
  35. package/templates/nextjs/ui-canvas/layout/newsletter-signup.tsx +122 -0
  36. package/templates/nextjs/ui-canvas/layout/site-footer.tsx.ejs +87 -83
  37. package/templates/nextjs/ui-canvas/product/back-in-stock-form.tsx +151 -0
  38. package/templates/nextjs/ui-canvas/product/product-client-section.tsx +373 -352
  39. package/templates/nextjs/ui-canvas/product/review-form.tsx +129 -11
  40. 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
- const page = await (await getServerClient(locale)).content.page.getBySlug(slug, locale).catch(() => null);
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
- const page = await (await getServerClient()).content.page.getBySlug(slug).catch(() => null);
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
- return {
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
- try {
25
- setError(null);
26
- const result = await proxyRegister(data);
27
-
28
- if (result.requiresVerification) {
29
- // Cookie already set by proxy; verify-email uses it for auth
30
- router.push('/verify-email');
31
- return;
32
- }
33
-
34
- // Cookie was set by the proxy; refresh auth state
35
- await auth.login();
36
- router.push('/');
37
- } catch (err) {
38
- const message = err instanceof Error ? err.message : 'Registration failed. Please try again.';
39
- setError(message);
40
- }
41
- }
42
-
43
- return (
44
- <div className="flex min-h-[60vh] items-center justify-center px-4 py-12">
45
- <div className="w-full max-w-md space-y-6">
46
- <div className="text-center">
47
- <h1 className="text-foreground text-2xl font-bold">{t('createAccountTitle')}</h1>
48
- <p className="text-muted-foreground mt-1 text-sm">{t('joinSubtitle')}</p>
49
- </div>
50
-
51
- <RegisterForm onSubmit={handleRegister} error={error} />
52
-
53
- <OAuthButtons />
54
-
55
- <p className="text-muted-foreground text-center text-sm">
56
- {t('alreadyHaveAccount')}{' '}
57
- <Link href="/login" className="text-primary font-medium hover:underline">
58
- {t('signIn')}
59
- </Link>
60
- </p>
61
- </div>
62
- </div>
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
+ }