create-brainerce-store 1.49.0 → 1.50.5
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 +1 -0
- package/messages/he.json +1 -0
- package/package.json +1 -1
- package/templates/nextjs/base/next.config.ts +18 -10
- package/templates/nextjs/base/package.json.ejs +5 -1
- package/templates/nextjs/base/src/app/blog/[slug]/page.tsx.ejs +1 -1
- package/templates/nextjs/base/src/app/blog/page.tsx.ejs +1 -1
- package/templates/nextjs/base/src/app/checkout/page.tsx +1 -1
- package/templates/nextjs/base/src/app/layout.tsx.ejs +24 -0
- package/templates/nextjs/base/src/app/products/[slug]/product-client-section.tsx +1 -1
- package/templates/nextjs/base/src/components/account/order-customizations.tsx +1 -1
- package/templates/nextjs/base/src/components/account/order-history.tsx +12 -3
- package/templates/nextjs/base/src/components/cart/cart-bundle-offer.tsx +1 -1
- package/templates/nextjs/base/src/components/cart/cart-item.tsx +1 -1
- package/templates/nextjs/base/src/components/cart/cart-upgrade-banner.tsx +1 -1
- package/templates/nextjs/base/src/components/checkout/order-bump-card.tsx +1 -1
- package/templates/nextjs/base/src/components/content/site-header.tsx.ejs +10 -0
- package/templates/nextjs/base/src/components/products/frequently-bought-together.tsx +1 -1
- package/templates/nextjs/base/src/components/products/modifier-group-selector.tsx +0 -4
- package/templates/nextjs/base/src/components/products/product-card.tsx +1 -1
- package/templates/nextjs/base/src/components/products/recommendation-section.tsx +1 -1
- package/templates/nextjs/base/src/components/shared/cdn-image.tsx +26 -0
- package/templates/nextjs/base/src/lib/image-hosts.ts +7 -0
- package/templates/nextjs/base/src/lib/sanitize-html.ts +12 -0
- package/templates/nextjs/base/src/middleware.ts.ejs +2 -2
- package/templates/nextjs/base/src/components/products/allergen-chips.tsx +0 -78
package/dist/index.js
CHANGED
|
@@ -31,7 +31,7 @@ var require_package = __commonJS({
|
|
|
31
31
|
"package.json"(exports2, module2) {
|
|
32
32
|
module2.exports = {
|
|
33
33
|
name: "create-brainerce-store",
|
|
34
|
-
version: "1.
|
|
34
|
+
version: "1.50.5",
|
|
35
35
|
description: "Scaffold a production-ready e-commerce storefront connected to Brainerce",
|
|
36
36
|
bin: {
|
|
37
37
|
"create-brainerce-store": "dist/index.js"
|
package/messages/en.json
CHANGED
package/messages/he.json
CHANGED
package/package.json
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { NextConfig } from 'next';
|
|
2
|
+
import { OPTIMIZED_IMAGE_HOSTS } from './src/lib/image-hosts';
|
|
2
3
|
|
|
3
4
|
// Build-time invariant — fail loud if NEXT_PUBLIC_STORE_CURRENCY is missing
|
|
4
5
|
// for a production build. Next.js inlines NEXT_PUBLIC_* into the client
|
|
@@ -28,16 +29,23 @@ const nextConfig: NextConfig = {
|
|
|
28
29
|
serverExternalPackages: ['isomorphic-dompurify'],
|
|
29
30
|
images: {
|
|
30
31
|
// The storefront is a consumer of the Brainerce API — it has to render
|
|
31
|
-
// whatever image URLs the API returns. In practice those URLs
|
|
32
|
-
// cdn.brainerce.com
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
|
|
32
|
+
// whatever image URLs the API returns. In practice those URLs are
|
|
33
|
+
// usually on cdn.brainerce.com, but a product/variant can still carry a
|
|
34
|
+
// raw upstream-merchant URL (WooCommerce, Shopify, self-hosted) while
|
|
35
|
+
// its image-import job is pending or failed. Rather than allowlist every
|
|
36
|
+
// possible merchant host (or hard-fail on them), only cdn.brainerce.com
|
|
37
|
+
// goes through the server-side optimizer; components render product/blog
|
|
38
|
+
// images via `CdnImage` (src/components/shared/cdn-image.tsx), which
|
|
39
|
+
// detects any other host and renders it `unoptimized` — an unresized
|
|
40
|
+
// direct-from-origin fetch, same as this template's prior behavior —
|
|
41
|
+
// instead of next/image throwing on an unconfigured hostname. No
|
|
42
|
+
// server-side fetching of unknown hosts → no SSRF/DoS surface on this
|
|
43
|
+
// Next server.
|
|
44
|
+
remotePatterns: OPTIMIZED_IMAGE_HOSTS.map((hostname) => ({
|
|
45
|
+
protocol: 'https' as const,
|
|
46
|
+
hostname,
|
|
47
|
+
pathname: '/**',
|
|
48
|
+
})),
|
|
41
49
|
},
|
|
42
50
|
async headers() {
|
|
43
51
|
return [
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"react-dom": "^19.0.0",
|
|
17
17
|
"clsx": "^2.1.0",
|
|
18
18
|
"tailwind-merge": "^2.2.0",
|
|
19
|
-
"isomorphic-dompurify": "<%= isomorphicDompurifyVersion %>"
|
|
19
|
+
"isomorphic-dompurify": "<%= isomorphicDompurifyVersion %>",
|
|
20
|
+
"sharp": "^0.35.3"
|
|
20
21
|
},
|
|
21
22
|
"devDependencies": {
|
|
22
23
|
"@types/node": "^20.0.0",
|
|
@@ -31,5 +32,8 @@
|
|
|
31
32
|
},
|
|
32
33
|
"engines": {
|
|
33
34
|
"node": "^20.19.0 || ^22.13.0 || >=24.0.0"
|
|
35
|
+
},
|
|
36
|
+
"pnpm": {
|
|
37
|
+
"onlyBuiltDependencies": ["sharp"]
|
|
34
38
|
}
|
|
35
39
|
}
|
|
@@ -12,7 +12,7 @@ import * as React from 'react';
|
|
|
12
12
|
import type { Metadata } from 'next';
|
|
13
13
|
import { notFound } from 'next/navigation';
|
|
14
14
|
import Link from 'next/link';
|
|
15
|
-
import Image from '
|
|
15
|
+
import { CdnImage as Image } from '@/components/shared/cdn-image';
|
|
16
16
|
|
|
17
17
|
import { getServerClient } from '@/lib/brainerce';
|
|
18
18
|
import { sanitizeHtml } from '@/lib/sanitize';
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import * as React from 'react';
|
|
11
11
|
import type { Metadata } from 'next';
|
|
12
12
|
import Link from 'next/link';
|
|
13
|
-
import Image from '
|
|
13
|
+
import { CdnImage as Image } from '@/components/shared/cdn-image';
|
|
14
14
|
|
|
15
15
|
import { getServerClient } from '@/lib/brainerce';
|
|
16
16
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { Suspense, useEffect, useState, useCallback, useRef } from 'react';
|
|
4
4
|
import { useSearchParams } from 'next/navigation';
|
|
5
|
-
import Image from '
|
|
5
|
+
import { CdnImage as Image } from '@/components/shared/cdn-image';
|
|
6
6
|
import { Link } from '@/lib/navigation';
|
|
7
7
|
import type {
|
|
8
8
|
Checkout,
|
|
@@ -86,6 +86,18 @@ export default async function RootLayout({
|
|
|
86
86
|
.replace(/&/g, '\\u0026'),
|
|
87
87
|
}}
|
|
88
88
|
/>
|
|
89
|
+
{/* Brainerce cookieless traffic analytics — auto-tracks pageviews +
|
|
90
|
+
SPA route changes. Cookieless (no consent banner needed); the
|
|
91
|
+
merchant can toggle it per sales channel in the dashboard. */}
|
|
92
|
+
<script
|
|
93
|
+
defer
|
|
94
|
+
src={`${process.env.NEXT_PUBLIC_BRAINERCE_PIXEL_URL || 'https://api.brainerce.com'}/t.js`}
|
|
95
|
+
data-channel={
|
|
96
|
+
process.env.NEXT_PUBLIC_BRAINERCE_SALES_CHANNEL_ID || '<%= connectionId %>'
|
|
97
|
+
}
|
|
98
|
+
nonce={nonce}
|
|
99
|
+
suppressHydrationWarning
|
|
100
|
+
/>
|
|
89
101
|
</head>
|
|
90
102
|
<body className={font.className}>
|
|
91
103
|
<StoreProvider locale={locale} initialStoreInfo={storeInfo}>
|
|
@@ -181,6 +193,18 @@ export default async function RootLayout({
|
|
|
181
193
|
.replace(/&/g, '\\u0026'),
|
|
182
194
|
}}
|
|
183
195
|
/>
|
|
196
|
+
{/* Brainerce cookieless traffic analytics — auto-tracks pageviews +
|
|
197
|
+
SPA route changes. Cookieless (no consent banner needed); the
|
|
198
|
+
merchant can toggle it per sales channel in the dashboard. */}
|
|
199
|
+
<script
|
|
200
|
+
defer
|
|
201
|
+
src={`${process.env.NEXT_PUBLIC_BRAINERCE_PIXEL_URL || 'https://api.brainerce.com'}/t.js`}
|
|
202
|
+
data-channel={
|
|
203
|
+
process.env.NEXT_PUBLIC_BRAINERCE_SALES_CHANNEL_ID || '<%= connectionId %>'
|
|
204
|
+
}
|
|
205
|
+
nonce={nonce}
|
|
206
|
+
suppressHydrationWarning
|
|
207
|
+
/>
|
|
184
208
|
</head>
|
|
185
209
|
<body className={font.className}>
|
|
186
210
|
<StoreProvider initialStoreInfo={storeInfo}>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect } from 'react';
|
|
4
|
-
import Image from '
|
|
4
|
+
import { CdnImage as Image } from '@/components/shared/cdn-image';
|
|
5
5
|
import type { Order, OrderStatus, OrderDownloadLink } from 'brainerce';
|
|
6
6
|
import { formatPrice } from 'brainerce';
|
|
7
7
|
import { getClient } from '@/lib/brainerce';
|
|
@@ -291,7 +291,16 @@ function OrderFinancialSummary({ order, currency }: { order: Order; currency: st
|
|
|
291
291
|
const ruleAmt = order.ruleDiscountAmount ? parseFloat(order.ruleDiscountAmount) : 0;
|
|
292
292
|
const couponAmt = order.couponDiscount ? parseFloat(order.couponDiscount) : 0;
|
|
293
293
|
const shipping = order.shippingAmount ? parseFloat(order.shippingAmount) : 0;
|
|
294
|
-
|
|
294
|
+
// Inclusive (VAT) orders persist taxAmount=0; the real VAT lives on the
|
|
295
|
+
// breakdown. Surface whichever is present (mirror of TaxDisplay).
|
|
296
|
+
const explicitTax = order.taxAmount ? parseFloat(order.taxAmount) : 0;
|
|
297
|
+
const taxIncluded = !!order.taxBreakdown?.pricesIncludeTax;
|
|
298
|
+
const tax =
|
|
299
|
+
explicitTax > 0
|
|
300
|
+
? explicitTax
|
|
301
|
+
: typeof order.taxBreakdown?.totalTax === 'number'
|
|
302
|
+
? order.taxBreakdown.totalTax
|
|
303
|
+
: 0;
|
|
295
304
|
const rules = order.appliedDiscounts;
|
|
296
305
|
|
|
297
306
|
const hasBreakdown = subtotal !== null && subtotal > 0;
|
|
@@ -352,7 +361,7 @@ function OrderFinancialSummary({ order, currency }: { order: Order; currency: st
|
|
|
352
361
|
|
|
353
362
|
{tax > 0 && (
|
|
354
363
|
<div className="flex items-center justify-between">
|
|
355
|
-
<span className="text-muted-foreground">{tc('tax')}</span>
|
|
364
|
+
<span className="text-muted-foreground">{taxIncluded ? tc('taxIncl') : tc('tax')}</span>
|
|
356
365
|
<span className="text-foreground">{formatPrice(tax, { currency }) as string}</span>
|
|
357
366
|
</div>
|
|
358
367
|
)}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useState } from 'react';
|
|
4
|
-
import Image from '
|
|
4
|
+
import { CdnImage as Image } from '@/components/shared/cdn-image';
|
|
5
5
|
import type { CartBundleOffer as CartBundleOfferType } from 'brainerce';
|
|
6
6
|
import { formatPrice } from 'brainerce';
|
|
7
7
|
import { useCurrency } from '@/lib/use-currency';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useState } from 'react';
|
|
4
|
-
import Image from '
|
|
4
|
+
import { CdnImage as Image } from '@/components/shared/cdn-image';
|
|
5
5
|
import type { CartItem as CartItemType } from 'brainerce';
|
|
6
6
|
import { getCartItemImage, formatPrice } from 'brainerce';
|
|
7
7
|
import { getClient } from '@/lib/brainerce';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect } from 'react';
|
|
4
|
-
import Image from '
|
|
4
|
+
import { CdnImage as Image } from '@/components/shared/cdn-image';
|
|
5
5
|
import type { CartUpgradeSuggestion, CartItem as CartItemType } from 'brainerce';
|
|
6
6
|
import { formatPrice } from 'brainerce';
|
|
7
7
|
import { getClient } from '@/lib/brainerce';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useState, useMemo } from 'react';
|
|
4
|
-
import Image from '
|
|
4
|
+
import { CdnImage as Image } from '@/components/shared/cdn-image';
|
|
5
5
|
import type { OrderBump, RecommendationVariant } from 'brainerce';
|
|
6
6
|
import { formatPrice, getVariantOptions } from 'brainerce';
|
|
7
7
|
import { useCurrency } from '@/lib/use-currency';
|
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
import * as React from 'react';
|
|
19
19
|
import type { Content } from 'brainerce';
|
|
20
20
|
import { HeaderAccount } from './header-account';
|
|
21
|
+
<% if (i18nEnabled) { %>
|
|
22
|
+
import { LanguageSwitcher } from '@/components/layout/language-switcher';
|
|
23
|
+
<% } %>
|
|
21
24
|
|
|
22
25
|
interface SiteHeaderProps {
|
|
23
26
|
/** Pre-fetched header payload (server-side). `null` triggers static fallback. */
|
|
@@ -79,6 +82,9 @@ export function SiteHeader({ header, storeName }: SiteHeaderProps) {
|
|
|
79
82
|
{brandLabel}
|
|
80
83
|
</a>
|
|
81
84
|
<div className="flex items-center gap-2">
|
|
85
|
+
<% if (i18nEnabled) { %>
|
|
86
|
+
<LanguageSwitcher />
|
|
87
|
+
<% } %>
|
|
82
88
|
<HeaderAccount />
|
|
83
89
|
<a
|
|
84
90
|
href="/cart"
|
|
@@ -133,6 +139,10 @@ export function SiteHeader({ header, storeName }: SiteHeaderProps) {
|
|
|
133
139
|
</a>
|
|
134
140
|
) : null}
|
|
135
141
|
|
|
142
|
+
<% if (i18nEnabled) { %>
|
|
143
|
+
<LanguageSwitcher />
|
|
144
|
+
<% } %>
|
|
145
|
+
|
|
136
146
|
<HeaderAccount />
|
|
137
147
|
|
|
138
148
|
<a
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useState } from 'react';
|
|
4
|
-
import Image from '
|
|
4
|
+
import { CdnImage as Image } from '@/components/shared/cdn-image';
|
|
5
5
|
import type { Product, ProductRecommendation } from 'brainerce';
|
|
6
6
|
import { formatPrice } from 'brainerce';
|
|
7
7
|
import { useCart, useStoreInfo } from '@/providers/store-provider';
|
|
@@ -24,7 +24,6 @@
|
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
import type { Modifier, ModifierGroup, ModifierSelection } from 'brainerce';
|
|
27
|
-
import { AllergenChips } from './allergen-chips';
|
|
28
27
|
|
|
29
28
|
interface ModifierGroupSelectorProps {
|
|
30
29
|
/**
|
|
@@ -121,9 +120,6 @@ export function ModifierGroupSelector({
|
|
|
121
120
|
Sold out
|
|
122
121
|
</span>
|
|
123
122
|
)}
|
|
124
|
-
{modifier.allergens && modifier.allergens.length > 0 && (
|
|
125
|
-
<AllergenChips allergens={modifier.allergens} />
|
|
126
|
-
)}
|
|
127
123
|
</label>
|
|
128
124
|
{modifier.description && (
|
|
129
125
|
<p className="modifier-option__description">{modifier.description}</p>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState } from 'react';
|
|
4
4
|
import { Link, useRouter } from '@/lib/navigation';
|
|
5
|
-
import Image from '
|
|
5
|
+
import { CdnImage as Image } from '@/components/shared/cdn-image';
|
|
6
6
|
import type { Product } from 'brainerce';
|
|
7
7
|
import { getProductPriceInfo, getVariantPrice, formatPrice } from 'brainerce';
|
|
8
8
|
import { useTranslations } from '@/lib/translations';
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect, useState } from 'react';
|
|
4
4
|
import { Link } from '@/lib/navigation';
|
|
5
|
-
import Image from '
|
|
5
|
+
import { CdnImage as Image } from '@/components/shared/cdn-image';
|
|
6
6
|
import type { ProductRecommendation } from 'brainerce';
|
|
7
7
|
import { PriceDisplay } from '@/components/shared/price-display';
|
|
8
8
|
import { cn } from '@/lib/utils';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import Image, { type ImageProps } from 'next/image';
|
|
2
|
+
import { OPTIMIZED_IMAGE_HOSTS } from '@/lib/image-hosts';
|
|
3
|
+
|
|
4
|
+
const OPTIMIZED_HOSTS = new Set(OPTIMIZED_IMAGE_HOSTS);
|
|
5
|
+
|
|
6
|
+
function isOptimizedHost(src: ImageProps['src']): boolean {
|
|
7
|
+
if (typeof src !== 'string') return true; // StaticImageData — always local
|
|
8
|
+
try {
|
|
9
|
+
return OPTIMIZED_HOSTS.has(new URL(src).hostname);
|
|
10
|
+
} catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Drop-in `next/image` replacement for Brainerce-sourced images (product
|
|
17
|
+
* photos, blog covers). Uploaded images get re-hosted onto cdn.brainerce.com
|
|
18
|
+
* by the backend's async image-import pipeline, but a product/variant can
|
|
19
|
+
* still carry a raw upstream-merchant URL while mid-import, or if that job
|
|
20
|
+
* failed. next/image throws if `src`'s host isn't in `images.remotePatterns`,
|
|
21
|
+
* so any host outside our known CDN falls back to `unoptimized` — an
|
|
22
|
+
* unresized, direct-from-origin fetch — instead of crashing the page.
|
|
23
|
+
*/
|
|
24
|
+
export function CdnImage({ unoptimized, ...props }: ImageProps) {
|
|
25
|
+
return <Image {...props} unoptimized={unoptimized ?? !isOptimizedHost(props.src)} />;
|
|
26
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for which hostnames Brainerce serves optimized
|
|
3
|
+
* (CDN-hosted, WebP) images from. Shared between `next.config.ts`
|
|
4
|
+
* (`images.remotePatterns`) and `CdnImage` (per-instance `unoptimized`
|
|
5
|
+
* fallback) so the two lists can't drift apart.
|
|
6
|
+
*/
|
|
7
|
+
export const OPTIMIZED_IMAGE_HOSTS = ['cdn.brainerce.com'];
|
|
@@ -71,6 +71,9 @@ export function sanitizeProductHtml(html: string): string {
|
|
|
71
71
|
'colgroup',
|
|
72
72
|
'col',
|
|
73
73
|
'iframe',
|
|
74
|
+
// Self-hosted product videos (uploaded via the dashboard media library).
|
|
75
|
+
'video',
|
|
76
|
+
'source',
|
|
74
77
|
],
|
|
75
78
|
ALLOWED_ATTR: [
|
|
76
79
|
'href',
|
|
@@ -87,6 +90,15 @@ export function sanitizeProductHtml(html: string): string {
|
|
|
87
90
|
'allowfullscreen',
|
|
88
91
|
'frameborder',
|
|
89
92
|
'loading',
|
|
93
|
+
'title',
|
|
94
|
+
// <video> / <source> playback attributes.
|
|
95
|
+
'controls',
|
|
96
|
+
'poster',
|
|
97
|
+
'preload',
|
|
98
|
+
'muted',
|
|
99
|
+
'loop',
|
|
100
|
+
'playsinline',
|
|
101
|
+
'type',
|
|
90
102
|
],
|
|
91
103
|
ALLOW_DATA_ATTR: false,
|
|
92
104
|
});
|
|
@@ -43,7 +43,7 @@ function buildCsp(nonce: string): string {
|
|
|
43
43
|
"style-src 'self' 'unsafe-inline' https://cdn.meshulam.co.il",
|
|
44
44
|
"img-src 'self' data: blob: https:",
|
|
45
45
|
"font-src 'self' data:",
|
|
46
|
-
"frame-src 'self' https://meshulam.co.il https://*.meshulam.co.il https://grow.link https://*.grow.link https://grow.security https://*.grow.security https://creditguard.co.il https://*.creditguard.co.il https://js.stripe.com https://hooks.stripe.com https://pay.google.com https://secure.cardcom.solutions https://checkout.stripe.com https://www.paypal.com https://www.sandbox.paypal.com https://*.brainerce.com",
|
|
46
|
+
"frame-src 'self' https://meshulam.co.il https://*.meshulam.co.il https://grow.link https://*.grow.link https://grow.security https://*.grow.security https://creditguard.co.il https://*.creditguard.co.il https://js.stripe.com https://hooks.stripe.com https://pay.google.com https://secure.cardcom.solutions https://checkout.stripe.com https://www.paypal.com https://www.sandbox.paypal.com https://www.youtube.com https://www.youtube-nocookie.com https://player.vimeo.com https://*.brainerce.com",
|
|
47
47
|
`connect-src 'self' ${brainerceApiOrigin()} https://api.brainerce.com https://*.meshulam.co.il https://grow.link https://*.grow.link https://*.grow.security https://pay.google.com https://*.stripe.com https://*.creditguard.co.il`,
|
|
48
48
|
"worker-src 'self' blob:",
|
|
49
49
|
// 'self' (not 'none') so iframe-based payment providers (e.g. Cardcom)
|
|
@@ -171,7 +171,7 @@ function buildCsp(nonce: string): string {
|
|
|
171
171
|
"style-src 'self' 'unsafe-inline' https://cdn.meshulam.co.il",
|
|
172
172
|
"img-src 'self' data: blob: https:",
|
|
173
173
|
"font-src 'self' data:",
|
|
174
|
-
"frame-src 'self' https://meshulam.co.il https://*.meshulam.co.il https://grow.link https://*.grow.link https://grow.security https://*.grow.security https://creditguard.co.il https://*.creditguard.co.il https://js.stripe.com https://hooks.stripe.com https://pay.google.com https://secure.cardcom.solutions https://checkout.stripe.com https://www.paypal.com https://www.sandbox.paypal.com https://*.brainerce.com",
|
|
174
|
+
"frame-src 'self' https://meshulam.co.il https://*.meshulam.co.il https://grow.link https://*.grow.link https://grow.security https://*.grow.security https://creditguard.co.il https://*.creditguard.co.il https://js.stripe.com https://hooks.stripe.com https://pay.google.com https://secure.cardcom.solutions https://checkout.stripe.com https://www.paypal.com https://www.sandbox.paypal.com https://www.youtube.com https://www.youtube-nocookie.com https://player.vimeo.com https://*.brainerce.com",
|
|
175
175
|
`connect-src 'self' ${brainerceApiOrigin()} https://api.brainerce.com https://*.meshulam.co.il https://grow.link https://*.grow.link https://*.grow.security https://pay.google.com https://*.stripe.com https://*.creditguard.co.il`,
|
|
176
176
|
"worker-src 'self' blob:",
|
|
177
177
|
// 'self' (not 'none') so iframe-based payment providers (e.g. Cardcom)
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* <AllergenChips> — informational allergen markers for storefront PDPs.
|
|
5
|
-
* Pure presentation; no interactivity. Pairs with `<ModifierGroupSelector>`
|
|
6
|
-
* but works standalone next to a product as well.
|
|
7
|
-
*
|
|
8
|
-
* Allergens are descriptive only: the system surfaces them but does not
|
|
9
|
-
* auto-block purchase. Customer-side allergen filtering is a deferred SDK
|
|
10
|
-
* feature (PRD §16).
|
|
11
|
-
*
|
|
12
|
-
* Icon resolution (PRD §15 Q4): when `allergen.iconUrl` is set, use it.
|
|
13
|
-
* Otherwise fall back to a built-in emoji keyed by `allergen.code` for the
|
|
14
|
-
* 8 common allergens listed in PRD §5.7. Stores that want branded icons
|
|
15
|
-
* upload via `iconUrl` per allergen; the rest get a sensible default
|
|
16
|
-
* without any setup.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import type { Allergen } from 'brainerce';
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Default emoji per canonical allergen code. The schema's @@unique on
|
|
23
|
-
* `(accountId, storeId, code)` means a store may have at most one of each;
|
|
24
|
-
* any code not in this map renders without an icon (the chip still shows
|
|
25
|
-
* the localized name, so the customer is informed regardless).
|
|
26
|
-
*/
|
|
27
|
-
const DEFAULT_EMOJI_BY_CODE: Record<string, string> = {
|
|
28
|
-
gluten: '🌾',
|
|
29
|
-
dairy: '🥛',
|
|
30
|
-
nuts: '🥜',
|
|
31
|
-
eggs: '🥚',
|
|
32
|
-
soy: '🫘',
|
|
33
|
-
fish: '🐟',
|
|
34
|
-
shellfish: '🦐',
|
|
35
|
-
sesame: '🌱',
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
interface AllergenChipsProps {
|
|
39
|
-
allergens: Allergen[];
|
|
40
|
-
/**
|
|
41
|
-
* When the storefront has more allergen icons than fit nicely, condense
|
|
42
|
-
* to "Contains: X, Y, +N more" instead of rendering every chip.
|
|
43
|
-
*/
|
|
44
|
-
maxVisible?: number;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function AllergenChips({ allergens, maxVisible = 6 }: AllergenChipsProps) {
|
|
48
|
-
if (!allergens || allergens.length === 0) return null;
|
|
49
|
-
|
|
50
|
-
const visible = allergens.slice(0, maxVisible);
|
|
51
|
-
const overflow = Math.max(0, allergens.length - visible.length);
|
|
52
|
-
|
|
53
|
-
return (
|
|
54
|
-
<span className="allergen-chips" role="list" aria-label="Contains">
|
|
55
|
-
{visible.map((allergen) => {
|
|
56
|
-
const fallbackEmoji = DEFAULT_EMOJI_BY_CODE[allergen.code];
|
|
57
|
-
return (
|
|
58
|
-
<span key={allergen.id} className="allergen-chip" role="listitem" title={allergen.name}>
|
|
59
|
-
{allergen.iconUrl ? (
|
|
60
|
-
<img
|
|
61
|
-
src={allergen.iconUrl}
|
|
62
|
-
alt=""
|
|
63
|
-
aria-hidden="true"
|
|
64
|
-
className="allergen-chip__icon"
|
|
65
|
-
/>
|
|
66
|
-
) : fallbackEmoji ? (
|
|
67
|
-
<span aria-hidden="true" className="allergen-chip__emoji">
|
|
68
|
-
{fallbackEmoji}
|
|
69
|
-
</span>
|
|
70
|
-
) : null}
|
|
71
|
-
<span className="allergen-chip__name">{allergen.name}</span>
|
|
72
|
-
</span>
|
|
73
|
-
);
|
|
74
|
-
})}
|
|
75
|
-
{overflow > 0 && <span className="allergen-chip allergen-chip--more">+{overflow} more</span>}
|
|
76
|
-
</span>
|
|
77
|
-
);
|
|
78
|
-
}
|