create-brainerce-store 1.43.0 → 1.43.1

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 (24) hide show
  1. package/dist/index.js +1 -1
  2. package/messages/en.json +1 -0
  3. package/messages/he.json +1 -0
  4. package/package.json +1 -1
  5. package/templates/nextjs/base/next.config.ts +68 -69
  6. package/templates/nextjs/base/scripts/fetch-store-info.mjs +91 -93
  7. package/templates/nextjs/base/src/app/checkout/page.tsx +1004 -982
  8. package/templates/nextjs/base/src/app/products/[slug]/page.tsx +118 -118
  9. package/templates/nextjs/base/src/components/account/order-history.tsx +368 -368
  10. package/templates/nextjs/base/src/components/cart/cart-bundle-offer.tsx +111 -111
  11. package/templates/nextjs/base/src/components/cart/cart-item.tsx +152 -152
  12. package/templates/nextjs/base/src/components/cart/cart-summary.tsx +108 -108
  13. package/templates/nextjs/base/src/components/cart/cart-upgrade-banner.tsx +141 -141
  14. package/templates/nextjs/base/src/components/cart/free-shipping-bar.tsx +62 -62
  15. package/templates/nextjs/base/src/components/checkout/order-bump-card.tsx +242 -242
  16. package/templates/nextjs/base/src/components/checkout/pickup-step.tsx +198 -198
  17. package/templates/nextjs/base/src/components/checkout/shipping-step.tsx +109 -109
  18. package/templates/nextjs/base/src/components/checkout/tax-display.tsx +74 -64
  19. package/templates/nextjs/base/src/components/products/frequently-bought-together.tsx +203 -203
  20. package/templates/nextjs/base/src/components/products/variant-selector.tsx +291 -291
  21. package/templates/nextjs/base/src/components/seo/product-json-ld.tsx +125 -129
  22. package/templates/nextjs/base/src/components/shared/price-display.tsx +61 -61
  23. package/templates/nextjs/base/src/lib/resolve-currency.ts +1 -6
  24. package/templates/nextjs/base/src/lib/use-currency.ts +1 -6
@@ -1,129 +1,125 @@
1
- import type { Product } from 'brainerce';
2
- import { getProductPriceInfo } from 'brainerce';
3
- import { getNonce } from '@/lib/nonce';
4
- import { resolveCurrency } from '@/lib/resolve-currency';
5
- import { stripHtmlForSeo } from '@/lib/seo';
6
-
7
- interface ProductJsonLdProps {
8
- product: Product;
9
- url: string;
10
- currency?: string;
11
- }
12
-
13
- export async function ProductJsonLd({
14
- product,
15
- url,
16
- currency,
17
- }: ProductJsonLdProps) {
18
- // Defer to the shared server helper so the env-var + USD fallback chain
19
- // lives in exactly one place across all server-side currency reads.
20
- const resolvedCurrency = resolveCurrency(null, currency);
21
- const nonce = await getNonce();
22
- const priceInfo = getProductPriceInfo(product);
23
- const imageUrl = product.images?.[0]?.url;
24
- const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || '';
25
-
26
- const availability =
27
- product.inventory?.canPurchase !== false
28
- ? 'https://schema.org/InStock'
29
- : 'https://schema.org/OutOfStock';
30
-
31
- const isVariable = product.type === 'VARIABLE';
32
- const offers =
33
- isVariable && product.priceMin
34
- ? {
35
- '@type': 'AggregateOffer',
36
- lowPrice: product.priceMin,
37
- highPrice: product.priceMax ?? product.priceMin,
38
- offerCount: product.variants?.length ?? 1,
39
- priceCurrency: resolvedCurrency,
40
- availability,
41
- url,
42
- }
43
- : {
44
- '@type': 'Offer',
45
- price: priceInfo.price,
46
- priceCurrency: resolvedCurrency,
47
- availability,
48
- url,
49
- };
50
-
51
- // schema.org/Product.description must be plain text — Google's Rich Results
52
- // and AI Overviews ingest this field directly and reject markup. Strip HTML
53
- // first; if the description is empty after stripping, fall back to the name.
54
- const cleanDescription = stripHtmlForSeo(product.description) || product.name;
55
- const productJsonLd: Record<string, unknown> = {
56
- '@context': 'https://schema.org',
57
- '@type': 'Product',
58
- name: product.name,
59
- description: cleanDescription,
60
- image: imageUrl,
61
- url,
62
- sku: product.sku || product.id,
63
- offers,
64
- };
65
-
66
- // Emit aggregateRating only when there is real review data — Google rejects
67
- // self-serving / faked ratings and stores with 0 reviews shouldn't claim any.
68
- if (product.reviewCount && product.reviewCount > 0) {
69
- productJsonLd.aggregateRating = {
70
- '@type': 'AggregateRating',
71
- ratingValue: product.avgRating,
72
- reviewCount: product.reviewCount,
73
- bestRating: 5,
74
- worstRating: 1,
75
- };
76
- }
77
-
78
- const breadcrumbJsonLd = {
79
- '@context': 'https://schema.org',
80
- '@type': 'BreadcrumbList',
81
- itemListElement: [
82
- {
83
- '@type': 'ListItem',
84
- position: 1,
85
- name: 'Home',
86
- item: baseUrl || '/',
87
- },
88
- {
89
- '@type': 'ListItem',
90
- position: 2,
91
- name: 'Products',
92
- item: `${baseUrl}/products`,
93
- },
94
- {
95
- '@type': 'ListItem',
96
- position: 3,
97
- name: product.name,
98
- item: url,
99
- },
100
- ],
101
- };
102
-
103
- return (
104
- <>
105
- <script
106
- type="application/ld+json"
107
- nonce={nonce}
108
- suppressHydrationWarning
109
- dangerouslySetInnerHTML={{ __html: serializeJsonLd(productJsonLd) }}
110
- />
111
- <script
112
- type="application/ld+json"
113
- nonce={nonce}
114
- suppressHydrationWarning
115
- dangerouslySetInnerHTML={{ __html: serializeJsonLd(breadcrumbJsonLd) }}
116
- />
117
- </>
118
- );
119
- }
120
-
121
- // Serialize a JSON-LD object for embedding in a <script> tag. Escapes
122
- // `<`, `>`, and `&` to \uXXXX so seller-controlled product fields cannot
123
- // break out of the script element with `</script>` or inject HTML.
124
- function serializeJsonLd(value: unknown): string {
125
- return JSON.stringify(value)
126
- .replace(/</g, '\\u003c')
127
- .replace(/>/g, '\\u003e')
128
- .replace(/&/g, '\\u0026');
129
- }
1
+ import type { Product } from 'brainerce';
2
+ import { getProductPriceInfo } from 'brainerce';
3
+ import { getNonce } from '@/lib/nonce';
4
+ import { resolveCurrency } from '@/lib/resolve-currency';
5
+ import { stripHtmlForSeo } from '@/lib/seo';
6
+
7
+ interface ProductJsonLdProps {
8
+ product: Product;
9
+ url: string;
10
+ currency?: string;
11
+ }
12
+
13
+ export async function ProductJsonLd({ product, url, currency }: ProductJsonLdProps) {
14
+ // Defer to the shared server helper so the env-var + USD fallback chain
15
+ // lives in exactly one place across all server-side currency reads.
16
+ const resolvedCurrency = resolveCurrency(null, currency);
17
+ const nonce = await getNonce();
18
+ const priceInfo = getProductPriceInfo(product);
19
+ const imageUrl = product.images?.[0]?.url;
20
+ const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || '';
21
+
22
+ const availability =
23
+ product.inventory?.canPurchase !== false
24
+ ? 'https://schema.org/InStock'
25
+ : 'https://schema.org/OutOfStock';
26
+
27
+ const isVariable = product.type === 'VARIABLE';
28
+ const offers =
29
+ isVariable && product.priceMin
30
+ ? {
31
+ '@type': 'AggregateOffer',
32
+ lowPrice: product.priceMin,
33
+ highPrice: product.priceMax ?? product.priceMin,
34
+ offerCount: product.variants?.length ?? 1,
35
+ priceCurrency: resolvedCurrency,
36
+ availability,
37
+ url,
38
+ }
39
+ : {
40
+ '@type': 'Offer',
41
+ price: priceInfo.price,
42
+ priceCurrency: resolvedCurrency,
43
+ availability,
44
+ url,
45
+ };
46
+
47
+ // schema.org/Product.description must be plain text — Google's Rich Results
48
+ // and AI Overviews ingest this field directly and reject markup. Strip HTML
49
+ // first; if the description is empty after stripping, fall back to the name.
50
+ const cleanDescription = stripHtmlForSeo(product.description) || product.name;
51
+ const productJsonLd: Record<string, unknown> = {
52
+ '@context': 'https://schema.org',
53
+ '@type': 'Product',
54
+ name: product.name,
55
+ description: cleanDescription,
56
+ image: imageUrl,
57
+ url,
58
+ sku: product.sku || product.id,
59
+ offers,
60
+ };
61
+
62
+ // Emit aggregateRating only when there is real review data — Google rejects
63
+ // self-serving / faked ratings and stores with 0 reviews shouldn't claim any.
64
+ if (product.reviewCount && product.reviewCount > 0) {
65
+ productJsonLd.aggregateRating = {
66
+ '@type': 'AggregateRating',
67
+ ratingValue: product.avgRating,
68
+ reviewCount: product.reviewCount,
69
+ bestRating: 5,
70
+ worstRating: 1,
71
+ };
72
+ }
73
+
74
+ const breadcrumbJsonLd = {
75
+ '@context': 'https://schema.org',
76
+ '@type': 'BreadcrumbList',
77
+ itemListElement: [
78
+ {
79
+ '@type': 'ListItem',
80
+ position: 1,
81
+ name: 'Home',
82
+ item: baseUrl || '/',
83
+ },
84
+ {
85
+ '@type': 'ListItem',
86
+ position: 2,
87
+ name: 'Products',
88
+ item: `${baseUrl}/products`,
89
+ },
90
+ {
91
+ '@type': 'ListItem',
92
+ position: 3,
93
+ name: product.name,
94
+ item: url,
95
+ },
96
+ ],
97
+ };
98
+
99
+ return (
100
+ <>
101
+ <script
102
+ type="application/ld+json"
103
+ nonce={nonce}
104
+ suppressHydrationWarning
105
+ dangerouslySetInnerHTML={{ __html: serializeJsonLd(productJsonLd) }}
106
+ />
107
+ <script
108
+ type="application/ld+json"
109
+ nonce={nonce}
110
+ suppressHydrationWarning
111
+ dangerouslySetInnerHTML={{ __html: serializeJsonLd(breadcrumbJsonLd) }}
112
+ />
113
+ </>
114
+ );
115
+ }
116
+
117
+ // Serialize a JSON-LD object for embedding in a <script> tag. Escapes
118
+ // `<`, `>`, and `&` to \uXXXX so seller-controlled product fields cannot
119
+ // break out of the script element with `</script>` or inject HTML.
120
+ function serializeJsonLd(value: unknown): string {
121
+ return JSON.stringify(value)
122
+ .replace(/</g, '\\u003c')
123
+ .replace(/>/g, '\\u003e')
124
+ .replace(/&/g, '\\u0026');
125
+ }
@@ -1,61 +1,61 @@
1
- 'use client';
2
-
3
- import { formatPrice } from 'brainerce';
4
- import { useCurrency } from '@/lib/use-currency';
5
- import { cn } from '@/lib/utils';
6
-
7
- interface PriceDisplayProps {
8
- price: string | number;
9
- salePrice?: string | number | null;
10
- currency?: string;
11
- className?: string;
12
- size?: 'sm' | 'md' | 'lg';
13
- }
14
-
15
- const sizeClasses = {
16
- sm: 'text-sm',
17
- md: 'text-base',
18
- lg: 'text-xl font-semibold',
19
- };
20
-
21
- export function PriceDisplay({
22
- price,
23
- salePrice,
24
- currency,
25
- className,
26
- size = 'md',
27
- }: PriceDisplayProps) {
28
- const currencyCode = useCurrency(currency);
29
-
30
- const basePrice = typeof price === 'string' ? parseFloat(price) : price;
31
- const sale =
32
- salePrice != null ? (typeof salePrice === 'string' ? parseFloat(salePrice) : salePrice) : null;
33
- const isOnSale = sale !== null && sale < basePrice;
34
-
35
- const discountPercent =
36
- isOnSale && basePrice > 0 ? Math.round(((basePrice - sale!) / basePrice) * 100) : 0;
37
-
38
- return (
39
- <span className={cn('inline-flex items-center gap-2', sizeClasses[size], className)}>
40
- {isOnSale ? (
41
- <>
42
- <span className="text-destructive font-medium">
43
- {formatPrice(sale!, { currency: currencyCode }) as string}
44
- </span>
45
- <span className="text-muted-foreground text-[0.85em] line-through">
46
- {formatPrice(basePrice, { currency: currencyCode }) as string}
47
- </span>
48
- {discountPercent > 0 && (
49
- <span className="bg-destructive text-destructive-foreground rounded px-1.5 py-0.5 text-xs font-medium">
50
- -{discountPercent}%
51
- </span>
52
- )}
53
- </>
54
- ) : (
55
- <span className="text-foreground font-medium">
56
- {formatPrice(basePrice, { currency: currencyCode }) as string}
57
- </span>
58
- )}
59
- </span>
60
- );
61
- }
1
+ 'use client';
2
+
3
+ import { formatPrice } from 'brainerce';
4
+ import { useCurrency } from '@/lib/use-currency';
5
+ import { cn } from '@/lib/utils';
6
+
7
+ interface PriceDisplayProps {
8
+ price: string | number;
9
+ salePrice?: string | number | null;
10
+ currency?: string;
11
+ className?: string;
12
+ size?: 'sm' | 'md' | 'lg';
13
+ }
14
+
15
+ const sizeClasses = {
16
+ sm: 'text-sm',
17
+ md: 'text-base',
18
+ lg: 'text-xl font-semibold',
19
+ };
20
+
21
+ export function PriceDisplay({
22
+ price,
23
+ salePrice,
24
+ currency,
25
+ className,
26
+ size = 'md',
27
+ }: PriceDisplayProps) {
28
+ const currencyCode = useCurrency(currency);
29
+
30
+ const basePrice = typeof price === 'string' ? parseFloat(price) : price;
31
+ const sale =
32
+ salePrice != null ? (typeof salePrice === 'string' ? parseFloat(salePrice) : salePrice) : null;
33
+ const isOnSale = sale !== null && sale < basePrice;
34
+
35
+ const discountPercent =
36
+ isOnSale && basePrice > 0 ? Math.round(((basePrice - sale!) / basePrice) * 100) : 0;
37
+
38
+ return (
39
+ <span className={cn('inline-flex items-center gap-2', sizeClasses[size], className)}>
40
+ {isOnSale ? (
41
+ <>
42
+ <span className="text-destructive font-medium">
43
+ {formatPrice(sale!, { currency: currencyCode }) as string}
44
+ </span>
45
+ <span className="text-muted-foreground text-[0.85em] line-through">
46
+ {formatPrice(basePrice, { currency: currencyCode }) as string}
47
+ </span>
48
+ {discountPercent > 0 && (
49
+ <span className="bg-destructive text-destructive-foreground rounded px-1.5 py-0.5 text-xs font-medium">
50
+ -{discountPercent}%
51
+ </span>
52
+ )}
53
+ </>
54
+ ) : (
55
+ <span className="text-foreground font-medium">
56
+ {formatPrice(basePrice, { currency: currencyCode }) as string}
57
+ </span>
58
+ )}
59
+ </span>
60
+ );
61
+ }
@@ -16,10 +16,5 @@ export function resolveCurrency(
16
16
  storeInfo: PublicStoreInfo | null | undefined,
17
17
  override?: string | null
18
18
  ): string {
19
- return (
20
- override ||
21
- storeInfo?.currency ||
22
- process.env.NEXT_PUBLIC_STORE_CURRENCY ||
23
- 'USD'
24
- );
19
+ return override || storeInfo?.currency || process.env.NEXT_PUBLIC_STORE_CURRENCY || 'USD';
25
20
  }
@@ -15,10 +15,5 @@ import { useStoreInfo } from '@/providers/store-provider';
15
15
  */
16
16
  export function useCurrency(override?: string | null): string {
17
17
  const { storeInfo } = useStoreInfo();
18
- return (
19
- override ||
20
- storeInfo?.currency ||
21
- process.env.NEXT_PUBLIC_STORE_CURRENCY ||
22
- 'USD'
23
- );
18
+ return override || storeInfo?.currency || process.env.NEXT_PUBLIC_STORE_CURRENCY || 'USD';
24
19
  }