create-brainerce-store 1.62.0 → 1.63.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/LICENSE +21 -21
- package/README.md +71 -71
- package/dist/index.js +5 -2
- package/messages/en.json +502 -502
- package/messages/he.json +502 -502
- package/package.json +1 -1
- package/templates/nextjs/base/.claude/commands/design.md +60 -60
- package/templates/nextjs/base/.env.local.ejs +24 -24
- package/templates/nextjs/base/.eslintrc.json +57 -57
- package/templates/nextjs/base/.mcp.json +8 -8
- package/templates/nextjs/base/AI-GUIDE.md +174 -174
- package/templates/nextjs/base/TRANSLATIONS.md +200 -200
- package/templates/nextjs/base/package.json.ejs +52 -52
- package/templates/nextjs/base/src/app/blog/[slug]/page.tsx.ejs +262 -262
- package/templates/nextjs/base/src/app/blog/page.tsx.ejs +276 -276
- package/templates/nextjs/base/src/app/cart/page.tsx +7 -7
- package/templates/nextjs/base/src/app/category/[slug]/page.tsx +106 -106
- package/templates/nextjs/base/src/app/checkout/page.tsx +12 -0
- package/templates/nextjs/base/src/app/globals.css +36 -36
- package/templates/nextjs/base/src/app/indexnow-key.txt/route.ts +26 -26
- package/templates/nextjs/base/src/app/layout.tsx.ejs +10 -0
- package/templates/nextjs/base/src/app/llms.txt/route.ts +44 -44
- package/templates/nextjs/base/src/app/order-confirmation/page.tsx +295 -278
- package/templates/nextjs/base/src/app/order-status/page.tsx +121 -121
- package/templates/nextjs/base/src/app/page.tsx +64 -64
- package/templates/nextjs/base/src/app/products/[slug]/page.tsx +175 -175
- package/templates/nextjs/base/src/app/products/page.tsx +19 -19
- package/templates/nextjs/base/src/components/account/order-history.tsx +385 -385
- package/templates/nextjs/base/src/components/brainerce-bot.tsx +70 -70
- package/templates/nextjs/base/src/components/checkout/checkout-form.tsx +612 -612
- package/templates/nextjs/base/src/components/checkout/custom-fields-step.tsx +292 -292
- package/templates/nextjs/base/src/components/checkout/date-picker.tsx +309 -309
- package/templates/nextjs/base/src/components/checkout/datetime-picker.tsx +144 -144
- package/templates/nextjs/base/src/components/checkout/order-bump-card.tsx +242 -242
- package/templates/nextjs/base/src/components/checkout/pickup-step.tsx +198 -198
- package/templates/nextjs/base/src/components/checkout/shipping-step.tsx +125 -125
- package/templates/nextjs/base/src/components/checkout/tax-display.tsx +74 -74
- package/templates/nextjs/base/src/components/seo/article-json-ld.tsx +59 -59
- package/templates/nextjs/base/src/components/seo/breadcrumbs.tsx +37 -37
- package/templates/nextjs/base/src/components/seo/category-json-ld.tsx +61 -61
- package/templates/nextjs/base/src/components/seo/organization-json-ld.tsx +97 -97
- package/templates/nextjs/base/src/components/seo/product-json-ld.tsx +179 -179
- package/templates/nextjs/base/src/components/tracking-bootstrap.tsx +62 -0
- package/templates/nextjs/base/src/components/ui/accordion.tsx +53 -53
- package/templates/nextjs/base/src/components/ui/badge.tsx +32 -32
- package/templates/nextjs/base/src/components/ui/button.tsx +49 -49
- package/templates/nextjs/base/src/components/ui/card.tsx +55 -55
- package/templates/nextjs/base/src/components/ui/checkbox.tsx +28 -28
- package/templates/nextjs/base/src/components/ui/dialog.tsx +107 -107
- package/templates/nextjs/base/src/components/ui/input.tsx +22 -22
- package/templates/nextjs/base/src/components/ui/label.tsx +21 -21
- package/templates/nextjs/base/src/components/ui/radio-group.tsx +38 -38
- package/templates/nextjs/base/src/components/ui/select.tsx +153 -153
- package/templates/nextjs/base/src/components/ui/separator.tsx +26 -26
- package/templates/nextjs/base/src/components/ui/sheet.tsx +125 -125
- package/templates/nextjs/base/src/components/ui/skeleton.tsx +7 -7
- package/templates/nextjs/base/src/components/ui/tabs.tsx +55 -55
- package/templates/nextjs/base/src/components/ui/textarea.tsx +21 -21
- package/templates/nextjs/base/src/components/ui/tooltip.tsx +32 -32
- package/templates/nextjs/base/src/core/hooks/use-cart-page.ts +58 -58
- package/templates/nextjs/base/src/core/hooks/use-home-data.ts +51 -51
- package/templates/nextjs/base/src/core/hooks/use-product-listing.ts +295 -295
- package/templates/nextjs/base/src/core/hooks/use-product-page.ts +20 -2
- package/templates/nextjs/base/src/core/lib/image-hosts.ts +7 -7
- package/templates/nextjs/base/src/core/lib/product-options.ts +121 -121
- package/templates/nextjs/base/src/core/lib/resolve-currency.ts +20 -20
- package/templates/nextjs/base/src/core/lib/safe-redirect.ts +38 -38
- package/templates/nextjs/base/src/core/lib/seo.ts +46 -46
- package/templates/nextjs/base/src/core/lib/store-info.ts +9 -0
- package/templates/nextjs/base/src/core/lib/tracking.ts +171 -0
- package/templates/nextjs/base/src/core/lib/use-currency.ts +19 -19
- package/templates/nextjs/base/src/core/lib/utils.ts +21 -21
- package/templates/nextjs/base/src/middleware.ts.ejs +54 -2
- package/templates/nextjs/base/src/ui/cart/cart-bundle-offer.tsx +117 -117
- package/templates/nextjs/base/src/ui/cart/cart-item.tsx +146 -146
- package/templates/nextjs/base/src/ui/cart/cart-nudges.tsx +28 -28
- package/templates/nextjs/base/src/ui/cart/cart-summary.tsx +110 -110
- package/templates/nextjs/base/src/ui/cart/cart-upgrade-banner.tsx +126 -126
- package/templates/nextjs/base/src/ui/cart/cart-view.tsx +140 -140
- package/templates/nextjs/base/src/ui/cart/coupon-input.tsx +134 -134
- package/templates/nextjs/base/src/ui/cart/free-shipping-bar.tsx +61 -61
- package/templates/nextjs/base/src/ui/cart/reservation-countdown.tsx +95 -95
- package/templates/nextjs/base/src/ui/home/discount-banner-strip.tsx +20 -20
- package/templates/nextjs/base/src/ui/home/featured-products-section.tsx +24 -24
- package/templates/nextjs/base/src/ui/home/hero-section.tsx +27 -27
- package/templates/nextjs/base/src/ui/home/home-client.tsx +29 -29
- package/templates/nextjs/base/src/ui/layout/announcement-bar.tsx.ejs +116 -116
- package/templates/nextjs/base/src/ui/layout/faq-section.tsx.ejs +90 -90
- package/templates/nextjs/base/src/ui/layout/header-account.tsx +40 -40
- package/templates/nextjs/base/src/ui/layout/language-switcher.tsx.ejs +80 -80
- package/templates/nextjs/base/src/ui/layout/site-footer.tsx.ejs +144 -144
- package/templates/nextjs/base/src/ui/layout/site-header.tsx.ejs +152 -152
- package/templates/nextjs/base/src/ui/product/customization-fields.tsx +457 -457
- package/templates/nextjs/base/src/ui/product/discount-badge.tsx +18 -18
- package/templates/nextjs/base/src/ui/product/frequently-bought-together.tsx +191 -191
- package/templates/nextjs/base/src/ui/product/modifier-group-selector.tsx +206 -206
- package/templates/nextjs/base/src/ui/product/price-display.tsx +62 -62
- package/templates/nextjs/base/src/ui/product/product-card.tsx +7 -2
- package/templates/nextjs/base/src/ui/product/product-client-section.tsx +455 -455
- package/templates/nextjs/base/src/ui/product/product-listing.tsx +446 -446
- package/templates/nextjs/base/src/ui/product/recommendation-section.tsx +102 -102
- package/templates/nextjs/base/src/ui/product/review-form.tsx +294 -294
- package/templates/nextjs/base/src/ui/product/reviews-section.tsx.ejs +108 -108
- package/templates/nextjs/base/src/ui/product/stock-badge.tsx +61 -61
- package/templates/nextjs/base/src/ui/product/variant-selector.tsx +291 -291
- package/templates/nextjs/base/src/ui/shared/cdn-image.tsx +26 -26
- package/templates/nextjs/base/tailwind.config.ts +69 -69
- package/templates/nextjs/designs/atelier/app-overlay/layout.tsx.ejs +10 -0
- package/templates/nextjs/designs/atelier/globals.css +402 -402
- package/templates/nextjs/designs/atelier/messages-patch/en.json +100 -100
- package/templates/nextjs/designs/atelier/messages-patch/he.json +100 -100
- package/templates/nextjs/designs/atelier/ui/cart/cart-bundle-offer.tsx +110 -110
- package/templates/nextjs/designs/atelier/ui/cart/cart-drawer.tsx +163 -163
- package/templates/nextjs/designs/atelier/ui/cart/cart-item.tsx +140 -140
- package/templates/nextjs/designs/atelier/ui/cart/cart-nudges.tsx +34 -34
- package/templates/nextjs/designs/atelier/ui/cart/cart-summary.tsx +107 -107
- package/templates/nextjs/designs/atelier/ui/cart/cart-upgrade-banner.tsx +123 -123
- package/templates/nextjs/designs/atelier/ui/cart/cart-view.tsx +147 -147
- package/templates/nextjs/designs/atelier/ui/cart/coupon-input.tsx +142 -142
- package/templates/nextjs/designs/atelier/ui/cart/free-shipping-bar.tsx +77 -77
- package/templates/nextjs/designs/atelier/ui/cart/reservation-countdown.tsx +89 -89
- package/templates/nextjs/designs/atelier/ui/home/benefits-band.tsx +34 -34
- package/templates/nextjs/designs/atelier/ui/home/category-tiles.tsx +151 -151
- package/templates/nextjs/designs/atelier/ui/home/discount-banner-strip.tsx +27 -27
- package/templates/nextjs/designs/atelier/ui/home/editorial-band.tsx +75 -75
- package/templates/nextjs/designs/atelier/ui/home/featured-products-section.tsx +36 -36
- package/templates/nextjs/designs/atelier/ui/home/hero-section.tsx +174 -174
- package/templates/nextjs/designs/atelier/ui/home/home-client.tsx +54 -54
- package/templates/nextjs/designs/atelier/ui/home/newsletter-section.tsx +78 -78
- package/templates/nextjs/designs/atelier/ui/home/testimonials-band.tsx +46 -46
- package/templates/nextjs/designs/atelier/ui/layout/announcement-bar.tsx +118 -118
- package/templates/nextjs/designs/atelier/ui/layout/faq-section.tsx +96 -96
- package/templates/nextjs/designs/atelier/ui/layout/header-account.tsx +36 -36
- package/templates/nextjs/designs/atelier/ui/layout/header-cart.tsx +36 -36
- package/templates/nextjs/designs/atelier/ui/layout/language-switcher.tsx.ejs +137 -137
- package/templates/nextjs/designs/atelier/ui/layout/nav-url.ts +14 -14
- package/templates/nextjs/designs/atelier/ui/layout/rich-text-block.tsx +34 -34
- package/templates/nextjs/designs/atelier/ui/layout/site-footer.tsx.ejs +141 -141
- package/templates/nextjs/designs/atelier/ui/layout/site-header.tsx.ejs +138 -138
- package/templates/nextjs/designs/atelier/ui/product/customization-fields.tsx +466 -466
- package/templates/nextjs/designs/atelier/ui/product/discount-badge.tsx +17 -17
- package/templates/nextjs/designs/atelier/ui/product/frequently-bought-together.tsx +196 -196
- package/templates/nextjs/designs/atelier/ui/product/modifier-group-selector.tsx +193 -193
- package/templates/nextjs/designs/atelier/ui/product/price-display.tsx +69 -69
- package/templates/nextjs/designs/atelier/ui/product/product-card.tsx +13 -11
- package/templates/nextjs/designs/atelier/ui/product/product-client-section.tsx +477 -477
- package/templates/nextjs/designs/atelier/ui/product/product-grid.tsx +42 -42
- package/templates/nextjs/designs/atelier/ui/product/product-listing.tsx +260 -260
- package/templates/nextjs/designs/atelier/ui/product/recommendation-section.tsx +95 -95
- package/templates/nextjs/designs/atelier/ui/product/review-form.tsx +267 -267
- package/templates/nextjs/designs/atelier/ui/product/reviews-section.tsx.ejs +148 -148
- package/templates/nextjs/designs/atelier/ui/product/stock-badge.tsx +66 -66
- package/templates/nextjs/designs/atelier/ui/product/variant-selector.tsx +206 -206
- package/templates/nextjs/designs/atelier/ui/shared/cdn-image.tsx +29 -29
- package/templates/nextjs/designs/atelier/ui/shared/fly-to-cart.ts +86 -86
- package/templates/nextjs/designs/atelier/ui/shared/icons.tsx +276 -276
- package/templates/nextjs/designs/atelier/ui/shared/loading-spinner.tsx +63 -63
- package/templates/nextjs/designs/atelier/ui/shared/reveal.tsx +71 -71
- package/templates/nextjs/designs/atelier/ui/shared/select-menu.tsx +125 -125
- package/templates/nextjs/ui-canvas/cart/cart-bundle-offer.tsx +89 -89
- package/templates/nextjs/ui-canvas/cart/cart-item.tsx +123 -123
- package/templates/nextjs/ui-canvas/cart/cart-nudges.tsx +27 -27
- package/templates/nextjs/ui-canvas/cart/cart-summary.tsx +99 -99
- package/templates/nextjs/ui-canvas/cart/cart-upgrade-banner.tsx +105 -105
- package/templates/nextjs/ui-canvas/cart/cart-view.tsx +106 -106
- package/templates/nextjs/ui-canvas/cart/coupon-input.tsx +119 -119
- package/templates/nextjs/ui-canvas/cart/free-shipping-bar.tsx +54 -54
- package/templates/nextjs/ui-canvas/cart/reservation-countdown.tsx +81 -81
- package/templates/nextjs/ui-canvas/home/discount-banner-strip.tsx +19 -19
- package/templates/nextjs/ui-canvas/home/featured-products-section.tsx +23 -23
- package/templates/nextjs/ui-canvas/home/hero-section.tsx +21 -21
- package/templates/nextjs/ui-canvas/home/home-client.tsx +26 -26
- package/templates/nextjs/ui-canvas/layout/announcement-bar.tsx.ejs +101 -101
- package/templates/nextjs/ui-canvas/layout/faq-section.tsx.ejs +71 -71
- package/templates/nextjs/ui-canvas/layout/header-account.tsx +31 -31
- package/templates/nextjs/ui-canvas/layout/language-switcher.tsx.ejs +76 -76
- package/templates/nextjs/ui-canvas/layout/rich-text-block.tsx.ejs +36 -36
- package/templates/nextjs/ui-canvas/layout/site-footer.tsx.ejs +82 -82
- package/templates/nextjs/ui-canvas/layout/site-header.tsx.ejs +119 -119
- package/templates/nextjs/ui-canvas/product/customization-fields.tsx +445 -445
- package/templates/nextjs/ui-canvas/product/discount-badge.tsx +19 -19
- package/templates/nextjs/ui-canvas/product/frequently-bought-together.tsx +168 -168
- package/templates/nextjs/ui-canvas/product/modifier-group-selector.tsx +165 -165
- package/templates/nextjs/ui-canvas/product/price-display.tsx +52 -52
- package/templates/nextjs/ui-canvas/product/product-card.tsx +7 -2
- package/templates/nextjs/ui-canvas/product/product-client-section.tsx +352 -352
- package/templates/nextjs/ui-canvas/product/product-grid.tsx +27 -27
- package/templates/nextjs/ui-canvas/product/product-listing.tsx +224 -224
- package/templates/nextjs/ui-canvas/product/recommendation-section.tsx +99 -99
- package/templates/nextjs/ui-canvas/product/review-form.tsx +266 -266
- package/templates/nextjs/ui-canvas/product/reviews-section.tsx.ejs +96 -96
- package/templates/nextjs/ui-canvas/product/stock-badge.tsx +45 -45
- package/templates/nextjs/ui-canvas/product/variant-selector.tsx +212 -212
- package/templates/nextjs/ui-canvas/shared/cdn-image.tsx +31 -31
- package/templates/nextjs/ui-canvas/shared/loading-spinner.tsx +27 -27
- package/templates/nextjs/base/tsconfig.tsbuildinfo +0 -1
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useStoreInfo } from '@/core/providers/store-provider';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Single source of truth for "which currency code do I format prices with?"
|
|
7
|
-
* in client components. Collapses the historical three-line fallback chain
|
|
8
|
-
* (`storeInfo?.currency || process.env.NEXT_PUBLIC_STORE_CURRENCY || 'USD'`)
|
|
9
|
-
* into one call so the chain lives in exactly one place — and so the day
|
|
10
|
-
* we drop the env var or the USD last-resort, we only touch this file.
|
|
11
|
-
*
|
|
12
|
-
* @param override pass when the caller already has a specific currency
|
|
13
|
-
* (e.g. an `Order` is denominated in its own currency,
|
|
14
|
-
* not the live store currency). Wins over `storeInfo`.
|
|
15
|
-
*/
|
|
16
|
-
export function useCurrency(override?: string | null): string {
|
|
17
|
-
const { storeInfo } = useStoreInfo();
|
|
18
|
-
return override || storeInfo?.currency || process.env.NEXT_PUBLIC_STORE_CURRENCY || 'USD';
|
|
19
|
-
}
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useStoreInfo } from '@/core/providers/store-provider';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Single source of truth for "which currency code do I format prices with?"
|
|
7
|
+
* in client components. Collapses the historical three-line fallback chain
|
|
8
|
+
* (`storeInfo?.currency || process.env.NEXT_PUBLIC_STORE_CURRENCY || 'USD'`)
|
|
9
|
+
* into one call so the chain lives in exactly one place — and so the day
|
|
10
|
+
* we drop the env var or the USD last-resort, we only touch this file.
|
|
11
|
+
*
|
|
12
|
+
* @param override pass when the caller already has a specific currency
|
|
13
|
+
* (e.g. an `Order` is denominated in its own currency,
|
|
14
|
+
* not the live store currency). Wins over `storeInfo`.
|
|
15
|
+
*/
|
|
16
|
+
export function useCurrency(override?: string | null): string {
|
|
17
|
+
const { storeInfo } = useStoreInfo();
|
|
18
|
+
return override || storeInfo?.currency || process.env.NEXT_PUBLIC_STORE_CURRENCY || 'USD';
|
|
19
|
+
}
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
import { clsx, type ClassValue } from 'clsx';
|
|
2
|
-
import { twMerge } from 'tailwind-merge';
|
|
3
|
-
|
|
4
|
-
export function cn(...inputs: ClassValue[]) {
|
|
5
|
-
return twMerge(clsx(inputs));
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Normalize a Next.js dynamic-route param into its decoded human-readable
|
|
10
|
-
* form. App Router usually decodes params automatically, but some runtime
|
|
11
|
-
* + middleware combinations leak percent-encoded values for non-ASCII
|
|
12
|
-
* slugs (e.g. Hebrew `%D7%A7…`), which then surface in canonical URLs and
|
|
13
|
-
* SEO tags. Idempotent on already-decoded input.
|
|
14
|
-
*/
|
|
15
|
-
export function decodeSlug(slug: string): string {
|
|
16
|
-
try {
|
|
17
|
-
return decodeURIComponent(slug);
|
|
18
|
-
} catch {
|
|
19
|
-
return slug;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
1
|
+
import { clsx, type ClassValue } from 'clsx';
|
|
2
|
+
import { twMerge } from 'tailwind-merge';
|
|
3
|
+
|
|
4
|
+
export function cn(...inputs: ClassValue[]) {
|
|
5
|
+
return twMerge(clsx(inputs));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Normalize a Next.js dynamic-route param into its decoded human-readable
|
|
10
|
+
* form. App Router usually decodes params automatically, but some runtime
|
|
11
|
+
* + middleware combinations leak percent-encoded values for non-ASCII
|
|
12
|
+
* slugs (e.g. Hebrew `%D7%A7…`), which then surface in canonical URLs and
|
|
13
|
+
* SEO tags. Idempotent on already-decoded input.
|
|
14
|
+
*/
|
|
15
|
+
export function decodeSlug(slug: string): string {
|
|
16
|
+
try {
|
|
17
|
+
return decodeURIComponent(slug);
|
|
18
|
+
} catch {
|
|
19
|
+
return slug;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -30,6 +30,32 @@ function brainerceApiOrigin(): string {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
// Beacon/XHR endpoints for the merchant's marketing tags (GA4, GTM, Meta,
|
|
34
|
+
// TikTok). Only `connect-src` needs them: `script-src` runs with
|
|
35
|
+
// 'strict-dynamic', under which host allowlists are IGNORED and trust
|
|
36
|
+
// propagates from the nonce'd bundle to the tag scripts it injects — so
|
|
37
|
+
// adding hosts there would be dead config. Without these entries the tags
|
|
38
|
+
// load fine and then every hit is blocked, which fails silently: the
|
|
39
|
+
// dashboard shows a connected pixel and the ad platform records nothing.
|
|
40
|
+
//
|
|
41
|
+
// Fixed list, not read from store config: middleware cannot see which apps a
|
|
42
|
+
// merchant connected without a fetch on every request, and these are the same
|
|
43
|
+
// well-known vendor origins for every store.
|
|
44
|
+
const MARKETING_TAG_ORIGINS = [
|
|
45
|
+
// Google (gtag.js, GTM, GA4 collect, Ads conversion pings)
|
|
46
|
+
'https://www.googletagmanager.com',
|
|
47
|
+
'https://www.google-analytics.com',
|
|
48
|
+
'https://*.google-analytics.com',
|
|
49
|
+
'https://*.analytics.google.com',
|
|
50
|
+
'https://stats.g.doubleclick.net',
|
|
51
|
+
'https://www.google.com',
|
|
52
|
+
// Meta pixel
|
|
53
|
+
'https://connect.facebook.net',
|
|
54
|
+
'https://www.facebook.com',
|
|
55
|
+
// TikTok pixel
|
|
56
|
+
'https://analytics.tiktok.com',
|
|
57
|
+
].join(' ');
|
|
58
|
+
|
|
33
59
|
function buildCsp(nonce: string): string {
|
|
34
60
|
const isDev = process.env.NODE_ENV === 'development';
|
|
35
61
|
// Next dev uses webpack's eval-source-map devtool, which requires 'unsafe-eval'
|
|
@@ -44,7 +70,7 @@ function buildCsp(nonce: string): string {
|
|
|
44
70
|
"img-src 'self' data: blob: https:",
|
|
45
71
|
"font-src 'self' data:",
|
|
46
72
|
"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
|
-
`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`,
|
|
73
|
+
`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 ${MARKETING_TAG_ORIGINS}`,
|
|
48
74
|
"worker-src 'self' blob:",
|
|
49
75
|
// 'self' (not 'none') so iframe-based payment providers (e.g. Cardcom)
|
|
50
76
|
// can redirect the iframe back to /payment-complete on the storefront
|
|
@@ -158,6 +184,32 @@ function brainerceApiOrigin(): string {
|
|
|
158
184
|
}
|
|
159
185
|
}
|
|
160
186
|
|
|
187
|
+
// Beacon/XHR endpoints for the merchant's marketing tags (GA4, GTM, Meta,
|
|
188
|
+
// TikTok). Only `connect-src` needs them: `script-src` runs with
|
|
189
|
+
// 'strict-dynamic', under which host allowlists are IGNORED and trust
|
|
190
|
+
// propagates from the nonce'd bundle to the tag scripts it injects — so
|
|
191
|
+
// adding hosts there would be dead config. Without these entries the tags
|
|
192
|
+
// load fine and then every hit is blocked, which fails silently: the
|
|
193
|
+
// dashboard shows a connected pixel and the ad platform records nothing.
|
|
194
|
+
//
|
|
195
|
+
// Fixed list, not read from store config: middleware cannot see which apps a
|
|
196
|
+
// merchant connected without a fetch on every request, and these are the same
|
|
197
|
+
// well-known vendor origins for every store.
|
|
198
|
+
const MARKETING_TAG_ORIGINS = [
|
|
199
|
+
// Google (gtag.js, GTM, GA4 collect, Ads conversion pings)
|
|
200
|
+
'https://www.googletagmanager.com',
|
|
201
|
+
'https://www.google-analytics.com',
|
|
202
|
+
'https://*.google-analytics.com',
|
|
203
|
+
'https://*.analytics.google.com',
|
|
204
|
+
'https://stats.g.doubleclick.net',
|
|
205
|
+
'https://www.google.com',
|
|
206
|
+
// Meta pixel
|
|
207
|
+
'https://connect.facebook.net',
|
|
208
|
+
'https://www.facebook.com',
|
|
209
|
+
// TikTok pixel
|
|
210
|
+
'https://analytics.tiktok.com',
|
|
211
|
+
].join(' ');
|
|
212
|
+
|
|
161
213
|
function buildCsp(nonce: string): string {
|
|
162
214
|
const isDev = process.env.NODE_ENV === 'development';
|
|
163
215
|
// Next dev uses webpack's eval-source-map devtool, which requires 'unsafe-eval'
|
|
@@ -172,7 +224,7 @@ function buildCsp(nonce: string): string {
|
|
|
172
224
|
"img-src 'self' data: blob: https:",
|
|
173
225
|
"font-src 'self' data:",
|
|
174
226
|
"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
|
-
`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`,
|
|
227
|
+
`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 ${MARKETING_TAG_ORIGINS}`,
|
|
176
228
|
"worker-src 'self' blob:",
|
|
177
229
|
// 'self' (not 'none') so iframe-based payment providers (e.g. Cardcom)
|
|
178
230
|
// can redirect the iframe back to /payment-complete on the storefront
|
|
@@ -1,117 +1,117 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useState } from 'react';
|
|
4
|
-
import { CdnImage as Image } from '@/ui/shared/cdn-image';
|
|
5
|
-
import type { CartBundleOffer as CartBundleOfferType } from 'brainerce';
|
|
6
|
-
import { formatPrice } from 'brainerce';
|
|
7
|
-
import { useCurrency } from '@/core/lib/use-currency';
|
|
8
|
-
import { useTranslations } from '@/core/lib/translations';
|
|
9
|
-
import { Badge } from '@/components/ui/badge';
|
|
10
|
-
import { Button } from '@/components/ui/button';
|
|
11
|
-
import { Card } from '@/components/ui/card';
|
|
12
|
-
import { Separator } from '@/components/ui/separator';
|
|
13
|
-
import { cn } from '@/core/lib/utils';
|
|
14
|
-
|
|
15
|
-
interface CartBundleOfferCardProps {
|
|
16
|
-
offer: CartBundleOfferType;
|
|
17
|
-
cartId: string;
|
|
18
|
-
onAdd: () => void;
|
|
19
|
-
className?: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function CartBundleOfferCard({ offer, cartId, onAdd, className }: CartBundleOfferCardProps) {
|
|
23
|
-
const t = useTranslations('cart');
|
|
24
|
-
const currency = useCurrency();
|
|
25
|
-
const [adding, setAdding] = useState(false);
|
|
26
|
-
|
|
27
|
-
const offered = offer.offeredProducts;
|
|
28
|
-
|
|
29
|
-
const totalOriginal = parseFloat(offer.totalOriginalPrice);
|
|
30
|
-
const totalDiscounted = parseFloat(offer.totalDiscountedPrice);
|
|
31
|
-
const discountLabel =
|
|
32
|
-
offer.discountType === 'PERCENTAGE'
|
|
33
|
-
? `${offer.discountValue}%`
|
|
34
|
-
: (formatPrice(parseFloat(offer.discountValue), { currency }) as string);
|
|
35
|
-
|
|
36
|
-
async function handleAdd() {
|
|
37
|
-
if (adding) return;
|
|
38
|
-
try {
|
|
39
|
-
setAdding(true);
|
|
40
|
-
const { getClient } = await import('@/core/lib/brainerce');
|
|
41
|
-
const client = getClient();
|
|
42
|
-
// No variant selections in the default template — products with
|
|
43
|
-
// variants are best handled with a dedicated picker per offered item.
|
|
44
|
-
await client.addBundleToCart(cartId, offer.id);
|
|
45
|
-
onAdd();
|
|
46
|
-
} catch (err) {
|
|
47
|
-
console.error('Failed to add bundle:', err);
|
|
48
|
-
} finally {
|
|
49
|
-
setAdding(false);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return (
|
|
54
|
-
<Card className={cn('p-4 shadow-none', className)}>
|
|
55
|
-
<div className="mb-3">
|
|
56
|
-
<p className="text-foreground text-sm font-medium">{offer.name}</p>
|
|
57
|
-
{offer.description && (
|
|
58
|
-
<p className="text-muted-foreground mt-0.5 text-xs">{offer.description}</p>
|
|
59
|
-
)}
|
|
60
|
-
</div>
|
|
61
|
-
|
|
62
|
-
<ul className="space-y-2">
|
|
63
|
-
{offered.map((p) => {
|
|
64
|
-
const firstImage = p.images?.[0];
|
|
65
|
-
const imageUrl = firstImage?.url ?? null;
|
|
66
|
-
return (
|
|
67
|
-
<li key={p.id} className="flex items-center gap-3">
|
|
68
|
-
<div className="bg-muted relative h-12 w-12 flex-shrink-0 overflow-hidden rounded">
|
|
69
|
-
{imageUrl ? (
|
|
70
|
-
<Image src={imageUrl} alt={p.name} fill sizes="48px" className="object-cover" />
|
|
71
|
-
) : null}
|
|
72
|
-
</div>
|
|
73
|
-
<div className="min-w-0 flex-1">
|
|
74
|
-
<p className="text-foreground truncate text-sm">{p.name}</p>
|
|
75
|
-
<div className="mt-0.5 flex items-center gap-2">
|
|
76
|
-
<span className="text-muted-foreground text-xs line-through">
|
|
77
|
-
{formatPrice(parseFloat(p.originalPrice), { currency }) as string}
|
|
78
|
-
</span>
|
|
79
|
-
<span className="text-foreground text-xs font-semibold">
|
|
80
|
-
{formatPrice(parseFloat(p.discountedPrice), { currency }) as string}
|
|
81
|
-
</span>
|
|
82
|
-
</div>
|
|
83
|
-
</div>
|
|
84
|
-
</li>
|
|
85
|
-
);
|
|
86
|
-
})}
|
|
87
|
-
</ul>
|
|
88
|
-
|
|
89
|
-
<Separator className="mt-3" />
|
|
90
|
-
<div className="flex items-center justify-between pt-3">
|
|
91
|
-
<div className="flex items-center gap-2">
|
|
92
|
-
<span className="text-muted-foreground text-sm line-through">
|
|
93
|
-
{formatPrice(totalOriginal, { currency }) as string}
|
|
94
|
-
</span>
|
|
95
|
-
<span className="text-foreground text-sm font-semibold">
|
|
96
|
-
{formatPrice(totalDiscounted, { currency }) as string}
|
|
97
|
-
</span>
|
|
98
|
-
<Badge
|
|
99
|
-
variant="outline"
|
|
100
|
-
className="bg-destructive/10 text-destructive rounded border-transparent px-1.5 py-0.5 font-medium"
|
|
101
|
-
>
|
|
102
|
-
-{discountLabel}
|
|
103
|
-
</Badge>
|
|
104
|
-
</div>
|
|
105
|
-
<Button
|
|
106
|
-
type="button"
|
|
107
|
-
size="sm"
|
|
108
|
-
onClick={handleAdd}
|
|
109
|
-
disabled={adding}
|
|
110
|
-
className="h-auto flex-shrink-0 rounded px-4 py-2 text-xs"
|
|
111
|
-
>
|
|
112
|
-
{adding ? t('addingBundle') : t('addBundleItem')}
|
|
113
|
-
</Button>
|
|
114
|
-
</div>
|
|
115
|
-
</Card>
|
|
116
|
-
);
|
|
117
|
-
}
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { CdnImage as Image } from '@/ui/shared/cdn-image';
|
|
5
|
+
import type { CartBundleOffer as CartBundleOfferType } from 'brainerce';
|
|
6
|
+
import { formatPrice } from 'brainerce';
|
|
7
|
+
import { useCurrency } from '@/core/lib/use-currency';
|
|
8
|
+
import { useTranslations } from '@/core/lib/translations';
|
|
9
|
+
import { Badge } from '@/components/ui/badge';
|
|
10
|
+
import { Button } from '@/components/ui/button';
|
|
11
|
+
import { Card } from '@/components/ui/card';
|
|
12
|
+
import { Separator } from '@/components/ui/separator';
|
|
13
|
+
import { cn } from '@/core/lib/utils';
|
|
14
|
+
|
|
15
|
+
interface CartBundleOfferCardProps {
|
|
16
|
+
offer: CartBundleOfferType;
|
|
17
|
+
cartId: string;
|
|
18
|
+
onAdd: () => void;
|
|
19
|
+
className?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function CartBundleOfferCard({ offer, cartId, onAdd, className }: CartBundleOfferCardProps) {
|
|
23
|
+
const t = useTranslations('cart');
|
|
24
|
+
const currency = useCurrency();
|
|
25
|
+
const [adding, setAdding] = useState(false);
|
|
26
|
+
|
|
27
|
+
const offered = offer.offeredProducts;
|
|
28
|
+
|
|
29
|
+
const totalOriginal = parseFloat(offer.totalOriginalPrice);
|
|
30
|
+
const totalDiscounted = parseFloat(offer.totalDiscountedPrice);
|
|
31
|
+
const discountLabel =
|
|
32
|
+
offer.discountType === 'PERCENTAGE'
|
|
33
|
+
? `${offer.discountValue}%`
|
|
34
|
+
: (formatPrice(parseFloat(offer.discountValue), { currency }) as string);
|
|
35
|
+
|
|
36
|
+
async function handleAdd() {
|
|
37
|
+
if (adding) return;
|
|
38
|
+
try {
|
|
39
|
+
setAdding(true);
|
|
40
|
+
const { getClient } = await import('@/core/lib/brainerce');
|
|
41
|
+
const client = getClient();
|
|
42
|
+
// No variant selections in the default template — products with
|
|
43
|
+
// variants are best handled with a dedicated picker per offered item.
|
|
44
|
+
await client.addBundleToCart(cartId, offer.id);
|
|
45
|
+
onAdd();
|
|
46
|
+
} catch (err) {
|
|
47
|
+
console.error('Failed to add bundle:', err);
|
|
48
|
+
} finally {
|
|
49
|
+
setAdding(false);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<Card className={cn('p-4 shadow-none', className)}>
|
|
55
|
+
<div className="mb-3">
|
|
56
|
+
<p className="text-foreground text-sm font-medium">{offer.name}</p>
|
|
57
|
+
{offer.description && (
|
|
58
|
+
<p className="text-muted-foreground mt-0.5 text-xs">{offer.description}</p>
|
|
59
|
+
)}
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<ul className="space-y-2">
|
|
63
|
+
{offered.map((p) => {
|
|
64
|
+
const firstImage = p.images?.[0];
|
|
65
|
+
const imageUrl = firstImage?.url ?? null;
|
|
66
|
+
return (
|
|
67
|
+
<li key={p.id} className="flex items-center gap-3">
|
|
68
|
+
<div className="bg-muted relative h-12 w-12 flex-shrink-0 overflow-hidden rounded">
|
|
69
|
+
{imageUrl ? (
|
|
70
|
+
<Image src={imageUrl} alt={p.name} fill sizes="48px" className="object-cover" />
|
|
71
|
+
) : null}
|
|
72
|
+
</div>
|
|
73
|
+
<div className="min-w-0 flex-1">
|
|
74
|
+
<p className="text-foreground truncate text-sm">{p.name}</p>
|
|
75
|
+
<div className="mt-0.5 flex items-center gap-2">
|
|
76
|
+
<span className="text-muted-foreground text-xs line-through">
|
|
77
|
+
{formatPrice(parseFloat(p.originalPrice), { currency }) as string}
|
|
78
|
+
</span>
|
|
79
|
+
<span className="text-foreground text-xs font-semibold">
|
|
80
|
+
{formatPrice(parseFloat(p.discountedPrice), { currency }) as string}
|
|
81
|
+
</span>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
</li>
|
|
85
|
+
);
|
|
86
|
+
})}
|
|
87
|
+
</ul>
|
|
88
|
+
|
|
89
|
+
<Separator className="mt-3" />
|
|
90
|
+
<div className="flex items-center justify-between pt-3">
|
|
91
|
+
<div className="flex items-center gap-2">
|
|
92
|
+
<span className="text-muted-foreground text-sm line-through">
|
|
93
|
+
{formatPrice(totalOriginal, { currency }) as string}
|
|
94
|
+
</span>
|
|
95
|
+
<span className="text-foreground text-sm font-semibold">
|
|
96
|
+
{formatPrice(totalDiscounted, { currency }) as string}
|
|
97
|
+
</span>
|
|
98
|
+
<Badge
|
|
99
|
+
variant="outline"
|
|
100
|
+
className="bg-destructive/10 text-destructive rounded border-transparent px-1.5 py-0.5 font-medium"
|
|
101
|
+
>
|
|
102
|
+
-{discountLabel}
|
|
103
|
+
</Badge>
|
|
104
|
+
</div>
|
|
105
|
+
<Button
|
|
106
|
+
type="button"
|
|
107
|
+
size="sm"
|
|
108
|
+
onClick={handleAdd}
|
|
109
|
+
disabled={adding}
|
|
110
|
+
className="h-auto flex-shrink-0 rounded px-4 py-2 text-xs"
|
|
111
|
+
>
|
|
112
|
+
{adding ? t('addingBundle') : t('addBundleItem')}
|
|
113
|
+
</Button>
|
|
114
|
+
</div>
|
|
115
|
+
</Card>
|
|
116
|
+
);
|
|
117
|
+
}
|