create-brainerce-store 1.85.0 → 1.86.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 +1 -1
- package/messages/en.json +6 -2
- package/messages/he.json +6 -2
- package/package.json +1 -1
- package/templates/nextjs/base/src/app/category/[slug]/page.tsx +2 -0
- package/templates/nextjs/base/src/app/checkout/page.tsx +1325 -1302
- package/templates/nextjs/base/src/app/icon.svg +11 -0
- package/templates/nextjs/base/src/components/account/order-payment-block.tsx +17 -2
- package/templates/nextjs/base/src/components/checkout/payment-step.tsx +51 -4
- package/templates/nextjs/base/src/core/lib/tracking.ts +25 -14
- package/templates/nextjs/base/src/ui/product/price-display.tsx +85 -62
- package/templates/nextjs/base/src/ui/product/product-client-section.tsx +539 -537
- package/templates/nextjs/designs/atelier/app-overlay/layout.tsx.ejs +2 -2
- package/templates/nextjs/designs/atelier/ui/product/price-display.tsx +90 -69
- package/templates/nextjs/designs/atelier/ui/product/product-client-section.tsx +3 -1
- package/templates/nextjs/ui-canvas/product/price-display.tsx +73 -52
- package/templates/nextjs/ui-canvas/product/product-client-section.tsx +428 -426
|
@@ -131,7 +131,7 @@ export default async function RootLayout({
|
|
|
131
131
|
const storeName = resolveStoreName(storeInfo);
|
|
132
132
|
|
|
133
133
|
return (
|
|
134
|
-
<html lang={locale} dir={dir}>
|
|
134
|
+
<html lang={locale} dir={dir} data-scroll-behavior="smooth">
|
|
135
135
|
<head>
|
|
136
136
|
<script
|
|
137
137
|
type="application/ld+json"
|
|
@@ -310,7 +310,7 @@ export default async function RootLayout({
|
|
|
310
310
|
const storeName = resolveStoreName(storeInfo);
|
|
311
311
|
|
|
312
312
|
return (
|
|
313
|
-
<html lang="<%= language %>" dir="<%= direction %>">
|
|
313
|
+
<html lang="<%= language %>" dir="<%= direction %>" data-scroll-behavior="smooth">
|
|
314
314
|
<head>
|
|
315
315
|
<script
|
|
316
316
|
type="application/ld+json"
|
|
@@ -1,69 +1,90 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { formatPrice } from 'brainerce';
|
|
4
|
-
import { useCurrency } from '@/core/lib/use-currency';
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
*
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { formatPrice } from 'brainerce';
|
|
4
|
+
import { useCurrency } from '@/core/lib/use-currency';
|
|
5
|
+
import { useTranslations } from '@/core/lib/translations';
|
|
6
|
+
import { useStoreInfo } from '@/core/providers/store-provider';
|
|
7
|
+
import { cn } from '@/core/lib/utils';
|
|
8
|
+
|
|
9
|
+
interface PriceDisplayProps {
|
|
10
|
+
price: string | number;
|
|
11
|
+
salePrice?: string | number | null;
|
|
12
|
+
currency?: string;
|
|
13
|
+
className?: string;
|
|
14
|
+
size?: 'sm' | 'md' | 'lg';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const CURRENT: Record<NonNullable<PriceDisplayProps['size']>, string> = {
|
|
18
|
+
sm: 'text-sm font-semibold',
|
|
19
|
+
md: 'text-base font-semibold',
|
|
20
|
+
lg: 'text-2xl font-semibold',
|
|
21
|
+
};
|
|
22
|
+
const STRUCK: Record<NonNullable<PriceDisplayProps['size']>, string> = {
|
|
23
|
+
sm: 'text-xs',
|
|
24
|
+
md: 'text-sm',
|
|
25
|
+
lg: 'text-base',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Price renderer used by cards, product page and recommendations; computes
|
|
30
|
+
* sale state + discount % from props (currency via useCurrency()).
|
|
31
|
+
* Never hardcode currency symbols — formatPrice handles them.
|
|
32
|
+
*/
|
|
33
|
+
export function PriceDisplay({
|
|
34
|
+
price,
|
|
35
|
+
salePrice,
|
|
36
|
+
currency,
|
|
37
|
+
className,
|
|
38
|
+
size = 'md',
|
|
39
|
+
}: PriceDisplayProps) {
|
|
40
|
+
const currencyCode = useCurrency(currency);
|
|
41
|
+
const { storeInfo } = useStoreInfo();
|
|
42
|
+
const t = useTranslations('common');
|
|
43
|
+
|
|
44
|
+
const basePrice = typeof price === 'string' ? parseFloat(price) : price;
|
|
45
|
+
const sale =
|
|
46
|
+
salePrice != null ? (typeof salePrice === 'string' ? parseFloat(salePrice) : salePrice) : null;
|
|
47
|
+
const isOnSale = sale !== null && sale < basePrice;
|
|
48
|
+
|
|
49
|
+
const discountPercent =
|
|
50
|
+
isOnSale && basePrice > 0 ? Math.round(((basePrice - sale!) / basePrice) * 100) : 0;
|
|
51
|
+
|
|
52
|
+
// FX overlay disclosure (see core/lib/display-price.ts): `currency` is the
|
|
53
|
+
// region-converted DISPLAY currency when it differs from the store's own
|
|
54
|
+
// base currency. The shopper is still charged in the base currency at
|
|
55
|
+
// checkout, so every converted price needs a visible "estimated" cue.
|
|
56
|
+
const isConverted = !!currency && !!storeInfo?.currency && currency !== storeInfo.currency;
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<span data-size={size} className={cn('inline-flex flex-wrap items-baseline gap-2', className)}>
|
|
60
|
+
{isOnSale ? (
|
|
61
|
+
<>
|
|
62
|
+
<span className={cn(CURRENT[size], 'text-foreground')}>
|
|
63
|
+
{formatPrice(sale!, { currency: currencyCode }) as string}
|
|
64
|
+
</span>
|
|
65
|
+
<s className={cn(STRUCK[size], 'text-muted-foreground')}>
|
|
66
|
+
{formatPrice(basePrice, { currency: currencyCode }) as string}
|
|
67
|
+
</s>
|
|
68
|
+
{discountPercent > 0 && (
|
|
69
|
+
<span className="badge-destructive self-center">-{discountPercent}%</span>
|
|
70
|
+
)}
|
|
71
|
+
</>
|
|
72
|
+
) : (
|
|
73
|
+
<span className={cn(CURRENT[size], 'text-foreground')}>
|
|
74
|
+
{formatPrice(basePrice, { currency: currencyCode }) as string}
|
|
75
|
+
</span>
|
|
76
|
+
)}
|
|
77
|
+
{isConverted && (
|
|
78
|
+
<span
|
|
79
|
+
className="text-muted-foreground cursor-help self-center text-xs font-normal underline decoration-dotted"
|
|
80
|
+
title={t('estimatedPriceTooltip', {
|
|
81
|
+
currency: currencyCode,
|
|
82
|
+
baseCurrency: storeInfo!.currency,
|
|
83
|
+
})}
|
|
84
|
+
>
|
|
85
|
+
{t('estimatedPrice')}
|
|
86
|
+
</span>
|
|
87
|
+
)}
|
|
88
|
+
</span>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
@@ -330,7 +330,6 @@ export function ProductClientSection({
|
|
|
330
330
|
className="flex items-center gap-3 p-3"
|
|
331
331
|
>
|
|
332
332
|
{c.image ? (
|
|
333
|
-
// eslint-disable-next-line @next/next/no-img-element
|
|
334
333
|
<img
|
|
335
334
|
src={c.image}
|
|
336
335
|
alt={c.name}
|
|
@@ -491,6 +490,9 @@ export function ProductClientSection({
|
|
|
491
490
|
<section className="mt-8">
|
|
492
491
|
<h2 className="mb-3 text-xl">{t('description')}</h2>
|
|
493
492
|
{'html' in description ? (
|
|
493
|
+
// Security: merchant-authored product description HTML,
|
|
494
|
+
// sanitized via `sanitizeProductHtml` (isomorphic-dompurify,
|
|
495
|
+
// allowlisted tags) before injecting.
|
|
494
496
|
<div
|
|
495
497
|
className="prose-store"
|
|
496
498
|
dangerouslySetInnerHTML={{ __html: sanitizeProductHtml(description.html) }}
|
|
@@ -1,52 +1,73 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { formatPrice } from 'brainerce';
|
|
4
|
-
import { useCurrency } from '@/core/lib/use-currency';
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { formatPrice } from 'brainerce';
|
|
4
|
+
import { useCurrency } from '@/core/lib/use-currency';
|
|
5
|
+
import { useTranslations } from '@/core/lib/translations';
|
|
6
|
+
import { useStoreInfo } from '@/core/providers/store-provider';
|
|
7
|
+
import { cn } from '@/core/lib/utils';
|
|
8
|
+
|
|
9
|
+
interface PriceDisplayProps {
|
|
10
|
+
price: string | number;
|
|
11
|
+
salePrice?: string | number | null;
|
|
12
|
+
currency?: string;
|
|
13
|
+
className?: string;
|
|
14
|
+
size?: 'sm' | 'md' | 'lg';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* DESIGN ME — price renderer used by cards, product page and recommendations;
|
|
19
|
+
* computes sale state + discount % from props (currency via useCurrency()).
|
|
20
|
+
* Never hardcode currency symbols — formatPrice handles them.
|
|
21
|
+
*
|
|
22
|
+
* Building blocks: shadcn/ui primitives in src/components/ui (Button, Card, Badge, Input, Select, Accordion, Dialog, Skeleton, ...) + lucide-react icons are installed and ready to compose with.
|
|
23
|
+
*/
|
|
24
|
+
export function PriceDisplay({
|
|
25
|
+
price,
|
|
26
|
+
salePrice,
|
|
27
|
+
currency,
|
|
28
|
+
className,
|
|
29
|
+
size = 'md',
|
|
30
|
+
}: PriceDisplayProps) {
|
|
31
|
+
const currencyCode = useCurrency(currency);
|
|
32
|
+
const { storeInfo } = useStoreInfo();
|
|
33
|
+
const t = useTranslations('common');
|
|
34
|
+
|
|
35
|
+
const basePrice = typeof price === 'string' ? parseFloat(price) : price;
|
|
36
|
+
const sale =
|
|
37
|
+
salePrice != null ? (typeof salePrice === 'string' ? parseFloat(salePrice) : salePrice) : null;
|
|
38
|
+
const isOnSale = sale !== null && sale < basePrice;
|
|
39
|
+
|
|
40
|
+
const discountPercent =
|
|
41
|
+
isOnSale && basePrice > 0 ? Math.round(((basePrice - sale!) / basePrice) * 100) : 0;
|
|
42
|
+
|
|
43
|
+
// FX overlay disclosure (see core/lib/display-price.ts): `currency` is the
|
|
44
|
+
// region-converted DISPLAY currency when it differs from the store's own
|
|
45
|
+
// base currency. The shopper is still charged in the base currency at
|
|
46
|
+
// checkout, so every converted price needs a visible "estimated" cue.
|
|
47
|
+
const isConverted = !!currency && !!storeInfo?.currency && currency !== storeInfo.currency;
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<span data-size={size} className={cn('inline-flex items-center gap-2', className)}>
|
|
51
|
+
{isOnSale ? (
|
|
52
|
+
<>
|
|
53
|
+
<span>{formatPrice(sale!, { currency: currencyCode }) as string}</span>
|
|
54
|
+
<s>{formatPrice(basePrice, { currency: currencyCode }) as string}</s>
|
|
55
|
+
{discountPercent > 0 && <span>-{discountPercent}%</span>}
|
|
56
|
+
</>
|
|
57
|
+
) : (
|
|
58
|
+
<span>{formatPrice(basePrice, { currency: currencyCode }) as string}</span>
|
|
59
|
+
)}
|
|
60
|
+
{isConverted && (
|
|
61
|
+
<span
|
|
62
|
+
className="text-muted-foreground cursor-help text-xs"
|
|
63
|
+
title={t('estimatedPriceTooltip', {
|
|
64
|
+
currency: currencyCode,
|
|
65
|
+
baseCurrency: storeInfo!.currency,
|
|
66
|
+
})}
|
|
67
|
+
>
|
|
68
|
+
{t('estimatedPrice')}
|
|
69
|
+
</span>
|
|
70
|
+
)}
|
|
71
|
+
</span>
|
|
72
|
+
);
|
|
73
|
+
}
|