create-brainerce-store 1.42.0 → 1.43.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 (22) hide show
  1. package/dist/index.js +1 -1
  2. package/package.json +1 -1
  3. package/templates/nextjs/base/scripts/fetch-store-info.mjs +10 -4
  4. package/templates/nextjs/base/src/app/checkout/page.tsx +982 -981
  5. package/templates/nextjs/base/src/app/products/[slug]/page.tsx +118 -117
  6. package/templates/nextjs/base/src/components/account/order-history.tsx +368 -367
  7. package/templates/nextjs/base/src/components/cart/cart-bundle-offer.tsx +111 -112
  8. package/templates/nextjs/base/src/components/cart/cart-item.tsx +152 -153
  9. package/templates/nextjs/base/src/components/cart/cart-summary.tsx +108 -108
  10. package/templates/nextjs/base/src/components/cart/cart-upgrade-banner.tsx +141 -142
  11. package/templates/nextjs/base/src/components/cart/free-shipping-bar.tsx +62 -59
  12. package/templates/nextjs/base/src/components/checkout/order-bump-card.tsx +242 -243
  13. package/templates/nextjs/base/src/components/checkout/pickup-step.tsx +198 -199
  14. package/templates/nextjs/base/src/components/checkout/shipping-step.tsx +109 -110
  15. package/templates/nextjs/base/src/components/checkout/tax-display.tsx +64 -65
  16. package/templates/nextjs/base/src/components/products/frequently-bought-together.tsx +203 -202
  17. package/templates/nextjs/base/src/components/products/product-card.tsx +226 -226
  18. package/templates/nextjs/base/src/components/products/variant-selector.tsx +291 -292
  19. package/templates/nextjs/base/src/components/seo/product-json-ld.tsx +129 -125
  20. package/templates/nextjs/base/src/components/shared/price-display.tsx +61 -65
  21. package/templates/nextjs/base/src/lib/resolve-currency.ts +25 -0
  22. package/templates/nextjs/base/src/lib/use-currency.ts +24 -0
@@ -1,117 +1,118 @@
1
- import type { Metadata } from 'next';
2
- import { notFound } from 'next/navigation';
3
- import { getProductPriceInfo } from 'brainerce';
4
- import { getServerClient, fetchStoreInfo } from '@/lib/brainerce';
5
- import { buildMetaDescription } from '@/lib/seo';
6
- import { decodeSlug } from '@/lib/utils';
7
- import { ProductJsonLd } from '@/components/seo/product-json-ld';
8
- import { ReviewsSection } from '@/components/reviews/reviews-section';
9
- import { ProductClientSection } from './product-client-section';
10
-
11
- type Props = {
12
- params: Promise<{ slug: string; locale?: string }>;
13
- };
14
-
15
- export async function generateMetadata({ params }: Props): Promise<Metadata> {
16
- const { slug: rawSlug, locale } = await params;
17
- const slug = decodeSlug(rawSlug);
18
-
19
- try {
20
- const client = getServerClient(locale);
21
- // Fetch product + store info in parallel; storeInfo gives us the live
22
- // currency for OG price tags (with env-var + USD fallbacks so we never
23
- // silently emit a wrong currency when the API is briefly unreachable).
24
- const [product, storeInfo] = await Promise.all([
25
- client.getProductBySlug(slug),
26
- fetchStoreInfo(locale),
27
- ]);
28
- const imageUrl = product.images?.[0]?.url;
29
- // Prefer merchant-authored SEO copy; fall back to a stripped+truncated
30
- // version of the visible description, then product name. We must NEVER
31
- // emit raw HTML or a mid-word cut into <meta name="description">.
32
- const seoTitle = (product as { seoTitle?: string | null }).seoTitle || product.name;
33
- const seoDescription =
34
- (product as { seoDescription?: string | null }).seoDescription ||
35
- buildMetaDescription(product.description) ||
36
- product.name;
37
-
38
- // OG product meta tags drive WhatsApp / Facebook / X link previews and
39
- // Google Merchant Center product enrichment. Emitting the literal "0"
40
- // here (the previous bug) causes price-zero link cards. Use the real
41
- // effective price from the SDK helper.
42
- const priceInfo = getProductPriceInfo(product);
43
- const currency = storeInfo?.currency || process.env.NEXT_PUBLIC_STORE_CURRENCY || 'USD';
44
- const priceAmount = priceInfo.price > 0 ? priceInfo.price.toFixed(2) : null;
45
- const inStock = product.inventory?.canPurchase !== false;
46
- const brandName = (product as { brand?: { name?: string } | null }).brand?.name;
47
-
48
- return {
49
- title: seoTitle,
50
- description: seoDescription,
51
- alternates: {
52
- canonical: `/products/${slug}`,
53
- },
54
- openGraph: {
55
- title: seoTitle,
56
- description: seoDescription,
57
- images: imageUrl ? [{ url: imageUrl, alt: product.name }] : [],
58
- type: 'website',
59
- },
60
- twitter: {
61
- card: 'summary_large_image',
62
- title: seoTitle,
63
- description: seoDescription,
64
- images: imageUrl ? [imageUrl] : [],
65
- },
66
- // Emit the OG product extension (Facebook / WhatsApp / X link previews,
67
- // Google Merchant Center). Skips the price pair entirely when the SDK
68
- // can't determine a positive amount, rather than shipping "0".
69
- other: {
70
- ...(priceAmount
71
- ? {
72
- 'og:price:amount': priceAmount,
73
- 'og:price:currency': currency,
74
- 'product:price:amount': priceAmount,
75
- 'product:price:currency': currency,
76
- }
77
- : {}),
78
- 'product:availability': inStock ? 'in stock' : 'out of stock',
79
- 'product:condition': 'new',
80
- ...(brandName ? { 'product:brand': brandName } : {}),
81
- },
82
- };
83
- } catch {
84
- return {
85
- title: 'Product not found',
86
- };
87
- }
88
- }
89
-
90
- export default async function ProductDetailPage({ params }: Props) {
91
- const { slug: rawSlug, locale } = await params;
92
- const slug = decodeSlug(rawSlug);
93
-
94
- let product;
95
- try {
96
- const client = getServerClient(locale);
97
- product = await client.getProductBySlug(slug);
98
- } catch {
99
- notFound();
100
- }
101
-
102
- const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || '';
103
- const productUrl = `${baseUrl}/products/${slug}`;
104
- // Reuse the cached storeInfo from generateMetadata's call — React cache()
105
- // collapses both into one backend request per render. Falls back to env
106
- // var then USD if the server fetch returned null.
107
- const storeInfo = await fetchStoreInfo(locale);
108
- const currency = storeInfo?.currency || process.env.NEXT_PUBLIC_STORE_CURRENCY || 'USD';
109
-
110
- return (
111
- <>
112
- <ProductJsonLd product={product} url={productUrl} currency={currency} />
113
- <ProductClientSection product={product} />
114
- <ReviewsSection productId={product.id} />
115
- </>
116
- );
117
- }
1
+ import type { Metadata } from 'next';
2
+ import { notFound } from 'next/navigation';
3
+ import { getProductPriceInfo } from 'brainerce';
4
+ import { getServerClient, fetchStoreInfo } from '@/lib/brainerce';
5
+ import { resolveCurrency } from '@/lib/resolve-currency';
6
+ import { buildMetaDescription } from '@/lib/seo';
7
+ import { decodeSlug } from '@/lib/utils';
8
+ import { ProductJsonLd } from '@/components/seo/product-json-ld';
9
+ import { ReviewsSection } from '@/components/reviews/reviews-section';
10
+ import { ProductClientSection } from './product-client-section';
11
+
12
+ type Props = {
13
+ params: Promise<{ slug: string; locale?: string }>;
14
+ };
15
+
16
+ export async function generateMetadata({ params }: Props): Promise<Metadata> {
17
+ const { slug: rawSlug, locale } = await params;
18
+ const slug = decodeSlug(rawSlug);
19
+
20
+ try {
21
+ const client = getServerClient(locale);
22
+ // Fetch product + store info in parallel; storeInfo gives us the live
23
+ // currency for OG price tags (with env-var + USD fallbacks so we never
24
+ // silently emit a wrong currency when the API is briefly unreachable).
25
+ const [product, storeInfo] = await Promise.all([
26
+ client.getProductBySlug(slug),
27
+ fetchStoreInfo(locale),
28
+ ]);
29
+ const imageUrl = product.images?.[0]?.url;
30
+ // Prefer merchant-authored SEO copy; fall back to a stripped+truncated
31
+ // version of the visible description, then product name. We must NEVER
32
+ // emit raw HTML or a mid-word cut into <meta name="description">.
33
+ const seoTitle = (product as { seoTitle?: string | null }).seoTitle || product.name;
34
+ const seoDescription =
35
+ (product as { seoDescription?: string | null }).seoDescription ||
36
+ buildMetaDescription(product.description) ||
37
+ product.name;
38
+
39
+ // OG product meta tags drive WhatsApp / Facebook / X link previews and
40
+ // Google Merchant Center product enrichment. Emitting the literal "0"
41
+ // here (the previous bug) causes price-zero link cards. Use the real
42
+ // effective price from the SDK helper.
43
+ const priceInfo = getProductPriceInfo(product);
44
+ const currency = resolveCurrency(storeInfo);
45
+ const priceAmount = priceInfo.price > 0 ? priceInfo.price.toFixed(2) : null;
46
+ const inStock = product.inventory?.canPurchase !== false;
47
+ const brandName = (product as { brand?: { name?: string } | null }).brand?.name;
48
+
49
+ return {
50
+ title: seoTitle,
51
+ description: seoDescription,
52
+ alternates: {
53
+ canonical: `/products/${slug}`,
54
+ },
55
+ openGraph: {
56
+ title: seoTitle,
57
+ description: seoDescription,
58
+ images: imageUrl ? [{ url: imageUrl, alt: product.name }] : [],
59
+ type: 'website',
60
+ },
61
+ twitter: {
62
+ card: 'summary_large_image',
63
+ title: seoTitle,
64
+ description: seoDescription,
65
+ images: imageUrl ? [imageUrl] : [],
66
+ },
67
+ // Emit the OG product extension (Facebook / WhatsApp / X link previews,
68
+ // Google Merchant Center). Skips the price pair entirely when the SDK
69
+ // can't determine a positive amount, rather than shipping "0".
70
+ other: {
71
+ ...(priceAmount
72
+ ? {
73
+ 'og:price:amount': priceAmount,
74
+ 'og:price:currency': currency,
75
+ 'product:price:amount': priceAmount,
76
+ 'product:price:currency': currency,
77
+ }
78
+ : {}),
79
+ 'product:availability': inStock ? 'in stock' : 'out of stock',
80
+ 'product:condition': 'new',
81
+ ...(brandName ? { 'product:brand': brandName } : {}),
82
+ },
83
+ };
84
+ } catch {
85
+ return {
86
+ title: 'Product not found',
87
+ };
88
+ }
89
+ }
90
+
91
+ export default async function ProductDetailPage({ params }: Props) {
92
+ const { slug: rawSlug, locale } = await params;
93
+ const slug = decodeSlug(rawSlug);
94
+
95
+ let product;
96
+ try {
97
+ const client = getServerClient(locale);
98
+ product = await client.getProductBySlug(slug);
99
+ } catch {
100
+ notFound();
101
+ }
102
+
103
+ const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || '';
104
+ const productUrl = `${baseUrl}/products/${slug}`;
105
+ // Reuse the cached storeInfo from generateMetadata's call React cache()
106
+ // collapses both into one backend request per render. resolveCurrency owns
107
+ // the env-var + USD fallback chain so this stays a single source of truth.
108
+ const storeInfo = await fetchStoreInfo(locale);
109
+ const currency = resolveCurrency(storeInfo);
110
+
111
+ return (
112
+ <>
113
+ <ProductJsonLd product={product} url={productUrl} currency={currency} />
114
+ <ProductClientSection product={product} />
115
+ <ReviewsSection productId={product.id} />
116
+ </>
117
+ );
118
+ }