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.
@@ -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 { cn } from '@/core/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 CURRENT: Record<NonNullable<PriceDisplayProps['size']>, string> = {
16
- sm: 'text-sm font-semibold',
17
- md: 'text-base font-semibold',
18
- lg: 'text-2xl font-semibold',
19
- };
20
- const STRUCK: Record<NonNullable<PriceDisplayProps['size']>, string> = {
21
- sm: 'text-xs',
22
- md: 'text-sm',
23
- lg: 'text-base',
24
- };
25
-
26
- /**
27
- * Price renderer used by cards, product page and recommendations; computes
28
- * sale state + discount % from props (currency via useCurrency()).
29
- * Never hardcode currency symbols — formatPrice handles them.
30
- */
31
- export function PriceDisplay({
32
- price,
33
- salePrice,
34
- currency,
35
- className,
36
- size = 'md',
37
- }: PriceDisplayProps) {
38
- const currencyCode = useCurrency(currency);
39
-
40
- const basePrice = typeof price === 'string' ? parseFloat(price) : price;
41
- const sale =
42
- salePrice != null ? (typeof salePrice === 'string' ? parseFloat(salePrice) : salePrice) : null;
43
- const isOnSale = sale !== null && sale < basePrice;
44
-
45
- const discountPercent =
46
- isOnSale && basePrice > 0 ? Math.round(((basePrice - sale!) / basePrice) * 100) : 0;
47
-
48
- return (
49
- <span data-size={size} className={cn('inline-flex flex-wrap items-baseline gap-2', className)}>
50
- {isOnSale ? (
51
- <>
52
- <span className={cn(CURRENT[size], 'text-foreground')}>
53
- {formatPrice(sale!, { currency: currencyCode }) as string}
54
- </span>
55
- <s className={cn(STRUCK[size], 'text-muted-foreground')}>
56
- {formatPrice(basePrice, { currency: currencyCode }) as string}
57
- </s>
58
- {discountPercent > 0 && (
59
- <span className="badge-destructive self-center">-{discountPercent}%</span>
60
- )}
61
- </>
62
- ) : (
63
- <span className={cn(CURRENT[size], 'text-foreground')}>
64
- {formatPrice(basePrice, { currency: currencyCode }) as string}
65
- </span>
66
- )}
67
- </span>
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 { cn } from '@/core/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
- /**
16
- * DESIGN ME — price renderer used by cards, product page and recommendations;
17
- * computes sale state + discount % from props (currency via useCurrency()).
18
- * Never hardcode currency symbols — formatPrice handles them.
19
- *
20
- * 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.
21
- */
22
- export function PriceDisplay({
23
- price,
24
- salePrice,
25
- currency,
26
- className,
27
- size = 'md',
28
- }: PriceDisplayProps) {
29
- const currencyCode = useCurrency(currency);
30
-
31
- const basePrice = typeof price === 'string' ? parseFloat(price) : price;
32
- const sale =
33
- salePrice != null ? (typeof salePrice === 'string' ? parseFloat(salePrice) : salePrice) : null;
34
- const isOnSale = sale !== null && sale < basePrice;
35
-
36
- const discountPercent =
37
- isOnSale && basePrice > 0 ? Math.round(((basePrice - sale!) / basePrice) * 100) : 0;
38
-
39
- return (
40
- <span data-size={size} className={cn('inline-flex items-center gap-2', className)}>
41
- {isOnSale ? (
42
- <>
43
- <span>{formatPrice(sale!, { currency: currencyCode }) as string}</span>
44
- <s>{formatPrice(basePrice, { currency: currencyCode }) as string}</s>
45
- {discountPercent > 0 && <span>-{discountPercent}%</span>}
46
- </>
47
- ) : (
48
- <span>{formatPrice(basePrice, { currency: currencyCode }) as string}</span>
49
- )}
50
- </span>
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
+ }