create-brainerce-store 1.61.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 +10 -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 -564
- 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,80 +1,80 @@
|
|
|
1
|
-
<% if (i18nEnabled) { %>
|
|
2
|
-
'use client';
|
|
3
|
-
|
|
4
|
-
import { usePathname } from 'next/navigation';
|
|
5
|
-
import { supportedLocales } from '@/i18n';
|
|
6
|
-
import {
|
|
7
|
-
Select,
|
|
8
|
-
SelectContent,
|
|
9
|
-
SelectItem,
|
|
10
|
-
SelectTrigger,
|
|
11
|
-
SelectValue,
|
|
12
|
-
} from '@/components/ui/select';
|
|
13
|
-
|
|
14
|
-
const LOCALE_NAMES: Record<string, string> = {
|
|
15
|
-
en: 'English',
|
|
16
|
-
he: 'עברית',
|
|
17
|
-
ar: 'العربية',
|
|
18
|
-
es: 'Español',
|
|
19
|
-
fr: 'Français',
|
|
20
|
-
de: 'Deutsch',
|
|
21
|
-
it: 'Italiano',
|
|
22
|
-
pt: 'Português',
|
|
23
|
-
nl: 'Nederlands',
|
|
24
|
-
ja: '日本語',
|
|
25
|
-
ko: '한국어',
|
|
26
|
-
zh: '中文',
|
|
27
|
-
ru: 'Русский',
|
|
28
|
-
tr: 'Türkçe',
|
|
29
|
-
pl: 'Polski',
|
|
30
|
-
sv: 'Svenska',
|
|
31
|
-
da: 'Dansk',
|
|
32
|
-
no: 'Norsk',
|
|
33
|
-
fi: 'Suomi',
|
|
34
|
-
cs: 'Čeština',
|
|
35
|
-
th: 'ไทย',
|
|
36
|
-
vi: 'Tiếng Việt',
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
const defaultLocale = '<%= defaultLocale %>';
|
|
40
|
-
|
|
41
|
-
export function LanguageSwitcher() {
|
|
42
|
-
const pathname = usePathname();
|
|
43
|
-
const firstSeg = pathname.split('/')[1];
|
|
44
|
-
const currentLocale = supportedLocales.includes(firstSeg as (typeof supportedLocales)[number])
|
|
45
|
-
? firstSeg
|
|
46
|
-
: defaultLocale;
|
|
47
|
-
|
|
48
|
-
function switchLocale(newLocale: string) {
|
|
49
|
-
// Strip any existing locale segment, then re-add only if the new one
|
|
50
|
-
// isn't the default — matches the as-needed prefix scheme in middleware.
|
|
51
|
-
const segments = pathname.split('/');
|
|
52
|
-
if (supportedLocales.includes(segments[1] as (typeof supportedLocales)[number])) {
|
|
53
|
-
segments.splice(1, 1);
|
|
54
|
-
}
|
|
55
|
-
const bare = segments.join('/') || '/';
|
|
56
|
-
window.location.href =
|
|
57
|
-
newLocale === defaultLocale ? bare : `/${newLocale}${bare === '/' ? '' : bare}`;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (supportedLocales.length <= 1) return null;
|
|
61
|
-
|
|
62
|
-
return (
|
|
63
|
-
<Select value={currentLocale} onValueChange={switchLocale}>
|
|
64
|
-
<SelectTrigger
|
|
65
|
-
className="h-8 w-auto gap-1 rounded border bg-transparent px-2 py-1 text-sm"
|
|
66
|
-
aria-label="Select language"
|
|
67
|
-
>
|
|
68
|
-
<SelectValue />
|
|
69
|
-
</SelectTrigger>
|
|
70
|
-
<SelectContent>
|
|
71
|
-
{supportedLocales.map((locale) => (
|
|
72
|
-
<SelectItem key={locale} value={locale}>
|
|
73
|
-
{LOCALE_NAMES[locale] || locale}
|
|
74
|
-
</SelectItem>
|
|
75
|
-
))}
|
|
76
|
-
</SelectContent>
|
|
77
|
-
</Select>
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
<% } %>
|
|
1
|
+
<% if (i18nEnabled) { %>
|
|
2
|
+
'use client';
|
|
3
|
+
|
|
4
|
+
import { usePathname } from 'next/navigation';
|
|
5
|
+
import { supportedLocales } from '@/i18n';
|
|
6
|
+
import {
|
|
7
|
+
Select,
|
|
8
|
+
SelectContent,
|
|
9
|
+
SelectItem,
|
|
10
|
+
SelectTrigger,
|
|
11
|
+
SelectValue,
|
|
12
|
+
} from '@/components/ui/select';
|
|
13
|
+
|
|
14
|
+
const LOCALE_NAMES: Record<string, string> = {
|
|
15
|
+
en: 'English',
|
|
16
|
+
he: 'עברית',
|
|
17
|
+
ar: 'العربية',
|
|
18
|
+
es: 'Español',
|
|
19
|
+
fr: 'Français',
|
|
20
|
+
de: 'Deutsch',
|
|
21
|
+
it: 'Italiano',
|
|
22
|
+
pt: 'Português',
|
|
23
|
+
nl: 'Nederlands',
|
|
24
|
+
ja: '日本語',
|
|
25
|
+
ko: '한국어',
|
|
26
|
+
zh: '中文',
|
|
27
|
+
ru: 'Русский',
|
|
28
|
+
tr: 'Türkçe',
|
|
29
|
+
pl: 'Polski',
|
|
30
|
+
sv: 'Svenska',
|
|
31
|
+
da: 'Dansk',
|
|
32
|
+
no: 'Norsk',
|
|
33
|
+
fi: 'Suomi',
|
|
34
|
+
cs: 'Čeština',
|
|
35
|
+
th: 'ไทย',
|
|
36
|
+
vi: 'Tiếng Việt',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const defaultLocale = '<%= defaultLocale %>';
|
|
40
|
+
|
|
41
|
+
export function LanguageSwitcher() {
|
|
42
|
+
const pathname = usePathname();
|
|
43
|
+
const firstSeg = pathname.split('/')[1];
|
|
44
|
+
const currentLocale = supportedLocales.includes(firstSeg as (typeof supportedLocales)[number])
|
|
45
|
+
? firstSeg
|
|
46
|
+
: defaultLocale;
|
|
47
|
+
|
|
48
|
+
function switchLocale(newLocale: string) {
|
|
49
|
+
// Strip any existing locale segment, then re-add only if the new one
|
|
50
|
+
// isn't the default — matches the as-needed prefix scheme in middleware.
|
|
51
|
+
const segments = pathname.split('/');
|
|
52
|
+
if (supportedLocales.includes(segments[1] as (typeof supportedLocales)[number])) {
|
|
53
|
+
segments.splice(1, 1);
|
|
54
|
+
}
|
|
55
|
+
const bare = segments.join('/') || '/';
|
|
56
|
+
window.location.href =
|
|
57
|
+
newLocale === defaultLocale ? bare : `/${newLocale}${bare === '/' ? '' : bare}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (supportedLocales.length <= 1) return null;
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<Select value={currentLocale} onValueChange={switchLocale}>
|
|
64
|
+
<SelectTrigger
|
|
65
|
+
className="h-8 w-auto gap-1 rounded border bg-transparent px-2 py-1 text-sm"
|
|
66
|
+
aria-label="Select language"
|
|
67
|
+
>
|
|
68
|
+
<SelectValue />
|
|
69
|
+
</SelectTrigger>
|
|
70
|
+
<SelectContent>
|
|
71
|
+
{supportedLocales.map((locale) => (
|
|
72
|
+
<SelectItem key={locale} value={locale}>
|
|
73
|
+
{LOCALE_NAMES[locale] || locale}
|
|
74
|
+
</SelectItem>
|
|
75
|
+
))}
|
|
76
|
+
</SelectContent>
|
|
77
|
+
</Select>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
<% } %>
|
|
@@ -1,144 +1,144 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Storefront footer — fully driven by the merchant's dashboard configuration.
|
|
3
|
-
*
|
|
4
|
-
* The merchant configures the footer in the Brainerce dashboard under
|
|
5
|
-
* Sell → Content → Footer
|
|
6
|
-
*
|
|
7
|
-
* Renders a brand block (store-name + social icons), the merchant's link
|
|
8
|
-
* columns, and a copyright bar. Social icons come from lucide-react where
|
|
9
|
-
* available; X (Twitter) and TikTok have no lucide glyph, so those two stay
|
|
10
|
-
* as local inline SVGs. Unknown platforms fall back to a letter avatar.
|
|
11
|
-
*
|
|
12
|
-
* API contract:
|
|
13
|
-
* GET → client.content.footer.get('main', locale) → Content<'FOOTER'> | null
|
|
14
|
-
* Returns null on 404 — we render a minimal fallback so the layout never
|
|
15
|
-
* crashes when the merchant hasn't seeded the footer yet.
|
|
16
|
-
*/
|
|
17
|
-
import * as React from 'react';
|
|
18
|
-
import { Facebook, Instagram, Linkedin, Youtube } from 'lucide-react';
|
|
19
|
-
import type { Content } from 'brainerce';
|
|
20
|
-
// Locale-aware Link (client component — fine to render from this server
|
|
21
|
-
// component). Merchant-configured link columns keep plain <a> since their
|
|
22
|
-
// URLs are arbitrary (may be external).
|
|
23
|
-
import { Link } from '@/core/lib/navigation';
|
|
24
|
-
|
|
25
|
-
interface SiteFooterProps {
|
|
26
|
-
/** Pre-fetched footer payload (server-side). `null` triggers fallback rendering. */
|
|
27
|
-
footer: Content<'FOOTER'> | null;
|
|
28
|
-
/** Store name shown in the brand block + fallback copyright. */
|
|
29
|
-
storeName?: string;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
type SvgComponent = React.ComponentType<React.SVGProps<SVGSVGElement>>;
|
|
33
|
-
|
|
34
|
-
// No lucide equivalent — keep as local SVGs.
|
|
35
|
-
const XIcon: SvgComponent = (props) => (
|
|
36
|
-
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" {...props}>
|
|
37
|
-
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
|
38
|
-
</svg>
|
|
39
|
-
);
|
|
40
|
-
|
|
41
|
-
const TikTokIcon: SvgComponent = (props) => (
|
|
42
|
-
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" {...props}>
|
|
43
|
-
<path d="M19.59 6.69a4.83 4.83 0 0 1-3.77-4.25V2h-3.45v13.67a2.89 2.89 0 0 1-5.2 1.74 2.89 2.89 0 0 1 2.31-4.64 2.93 2.93 0 0 1 .88.13V9.4a6.84 6.84 0 0 0-1-.05A6.33 6.33 0 0 0 5.8 20.1a6.34 6.34 0 0 0 10.86-4.43v-7a8.16 8.16 0 0 0 4.77 1.52v-3.4a4.85 4.85 0 0 1-1.84-.1z" />
|
|
44
|
-
</svg>
|
|
45
|
-
);
|
|
46
|
-
|
|
47
|
-
const SOCIAL_ICONS: Record<string, SvgComponent> = {
|
|
48
|
-
instagram: Instagram,
|
|
49
|
-
facebook: Facebook,
|
|
50
|
-
x: XIcon,
|
|
51
|
-
twitter: XIcon,
|
|
52
|
-
linkedin: Linkedin,
|
|
53
|
-
youtube: Youtube,
|
|
54
|
-
tiktok: TikTokIcon,
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
export function SiteFooter({ footer, storeName }: SiteFooterProps) {
|
|
58
|
-
const data = footer?.data;
|
|
59
|
-
const columns = data?.columns ?? [];
|
|
60
|
-
const social = data?.social ?? [];
|
|
61
|
-
const year = new Date().getFullYear();
|
|
62
|
-
const brandLabel = storeName ?? 'Store';
|
|
63
|
-
|
|
64
|
-
if (!data || (columns.length === 0 && !data.copyright && social.length === 0)) {
|
|
65
|
-
return (
|
|
66
|
-
<footer className="border-border bg-muted/30 text-muted-foreground mt-16 border-t py-8 text-sm">
|
|
67
|
-
<div className="mx-auto max-w-7xl px-4 text-center sm:px-6 lg:px-8">
|
|
68
|
-
© {year} {brandLabel}. All rights reserved.
|
|
69
|
-
</div>
|
|
70
|
-
</footer>
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return (
|
|
75
|
-
<footer className="border-border bg-muted/30 mt-16 border-t">
|
|
76
|
-
<div className="mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8">
|
|
77
|
-
<div className="grid grid-cols-1 gap-10 md:grid-cols-12">
|
|
78
|
-
<div className="md:col-span-4">
|
|
79
|
-
<Link
|
|
80
|
-
href="/"
|
|
81
|
-
className="text-foreground text-lg font-semibold tracking-tight"
|
|
82
|
-
>
|
|
83
|
-
{brandLabel}
|
|
84
|
-
</Link>
|
|
85
|
-
{social.length > 0 ? (
|
|
86
|
-
<div className="mt-4 flex flex-wrap items-center gap-2">
|
|
87
|
-
{social.map((entry, idx) => {
|
|
88
|
-
const key = entry.platform.toLowerCase();
|
|
89
|
-
const Icon = SOCIAL_ICONS[key];
|
|
90
|
-
return (
|
|
91
|
-
<a
|
|
92
|
-
key={`${entry.platform}-${idx}`}
|
|
93
|
-
href={entry.url}
|
|
94
|
-
target="_blank"
|
|
95
|
-
rel="noopener noreferrer"
|
|
96
|
-
aria-label={entry.platform}
|
|
97
|
-
className="border-border hover:border-foreground/40 hover:text-foreground text-muted-foreground inline-flex h-9 w-9 items-center justify-center rounded-full border transition-colors"
|
|
98
|
-
>
|
|
99
|
-
{Icon ? (
|
|
100
|
-
<Icon className="h-4 w-4" />
|
|
101
|
-
) : (
|
|
102
|
-
<span className="text-xs font-medium uppercase">
|
|
103
|
-
{entry.platform.charAt(0)}
|
|
104
|
-
</span>
|
|
105
|
-
)}
|
|
106
|
-
</a>
|
|
107
|
-
);
|
|
108
|
-
})}
|
|
109
|
-
</div>
|
|
110
|
-
) : null}
|
|
111
|
-
</div>
|
|
112
|
-
|
|
113
|
-
{columns.length > 0 ? (
|
|
114
|
-
<div className="grid grid-cols-2 gap-8 sm:grid-cols-3 md:col-span-8">
|
|
115
|
-
{columns.map((col, idx) => (
|
|
116
|
-
<div key={`${col.title}-${idx}`}>
|
|
117
|
-
<h3 className="text-foreground mb-3 text-sm font-semibold tracking-wide">
|
|
118
|
-
{col.title}
|
|
119
|
-
</h3>
|
|
120
|
-
<ul className="space-y-2">
|
|
121
|
-
{col.links.map((link, linkIdx) => (
|
|
122
|
-
<li key={`${link.url}-${linkIdx}`}>
|
|
123
|
-
<a
|
|
124
|
-
href={link.url}
|
|
125
|
-
className="text-muted-foreground hover:text-foreground text-sm transition-colors"
|
|
126
|
-
>
|
|
127
|
-
{link.label}
|
|
128
|
-
</a>
|
|
129
|
-
</li>
|
|
130
|
-
))}
|
|
131
|
-
</ul>
|
|
132
|
-
</div>
|
|
133
|
-
))}
|
|
134
|
-
</div>
|
|
135
|
-
) : null}
|
|
136
|
-
</div>
|
|
137
|
-
|
|
138
|
-
<div className="border-border text-muted-foreground mt-10 flex flex-col items-center justify-between gap-2 border-t pt-6 text-xs sm:flex-row">
|
|
139
|
-
<span>{data.copyright ?? `© ${year} ${brandLabel}. All rights reserved.`}</span>
|
|
140
|
-
</div>
|
|
141
|
-
</div>
|
|
142
|
-
</footer>
|
|
143
|
-
);
|
|
144
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Storefront footer — fully driven by the merchant's dashboard configuration.
|
|
3
|
+
*
|
|
4
|
+
* The merchant configures the footer in the Brainerce dashboard under
|
|
5
|
+
* Sell → Content → Footer
|
|
6
|
+
*
|
|
7
|
+
* Renders a brand block (store-name + social icons), the merchant's link
|
|
8
|
+
* columns, and a copyright bar. Social icons come from lucide-react where
|
|
9
|
+
* available; X (Twitter) and TikTok have no lucide glyph, so those two stay
|
|
10
|
+
* as local inline SVGs. Unknown platforms fall back to a letter avatar.
|
|
11
|
+
*
|
|
12
|
+
* API contract:
|
|
13
|
+
* GET → client.content.footer.get('main', locale) → Content<'FOOTER'> | null
|
|
14
|
+
* Returns null on 404 — we render a minimal fallback so the layout never
|
|
15
|
+
* crashes when the merchant hasn't seeded the footer yet.
|
|
16
|
+
*/
|
|
17
|
+
import * as React from 'react';
|
|
18
|
+
import { Facebook, Instagram, Linkedin, Youtube } from 'lucide-react';
|
|
19
|
+
import type { Content } from 'brainerce';
|
|
20
|
+
// Locale-aware Link (client component — fine to render from this server
|
|
21
|
+
// component). Merchant-configured link columns keep plain <a> since their
|
|
22
|
+
// URLs are arbitrary (may be external).
|
|
23
|
+
import { Link } from '@/core/lib/navigation';
|
|
24
|
+
|
|
25
|
+
interface SiteFooterProps {
|
|
26
|
+
/** Pre-fetched footer payload (server-side). `null` triggers fallback rendering. */
|
|
27
|
+
footer: Content<'FOOTER'> | null;
|
|
28
|
+
/** Store name shown in the brand block + fallback copyright. */
|
|
29
|
+
storeName?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type SvgComponent = React.ComponentType<React.SVGProps<SVGSVGElement>>;
|
|
33
|
+
|
|
34
|
+
// No lucide equivalent — keep as local SVGs.
|
|
35
|
+
const XIcon: SvgComponent = (props) => (
|
|
36
|
+
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" {...props}>
|
|
37
|
+
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
|
38
|
+
</svg>
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const TikTokIcon: SvgComponent = (props) => (
|
|
42
|
+
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" {...props}>
|
|
43
|
+
<path d="M19.59 6.69a4.83 4.83 0 0 1-3.77-4.25V2h-3.45v13.67a2.89 2.89 0 0 1-5.2 1.74 2.89 2.89 0 0 1 2.31-4.64 2.93 2.93 0 0 1 .88.13V9.4a6.84 6.84 0 0 0-1-.05A6.33 6.33 0 0 0 5.8 20.1a6.34 6.34 0 0 0 10.86-4.43v-7a8.16 8.16 0 0 0 4.77 1.52v-3.4a4.85 4.85 0 0 1-1.84-.1z" />
|
|
44
|
+
</svg>
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const SOCIAL_ICONS: Record<string, SvgComponent> = {
|
|
48
|
+
instagram: Instagram,
|
|
49
|
+
facebook: Facebook,
|
|
50
|
+
x: XIcon,
|
|
51
|
+
twitter: XIcon,
|
|
52
|
+
linkedin: Linkedin,
|
|
53
|
+
youtube: Youtube,
|
|
54
|
+
tiktok: TikTokIcon,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export function SiteFooter({ footer, storeName }: SiteFooterProps) {
|
|
58
|
+
const data = footer?.data;
|
|
59
|
+
const columns = data?.columns ?? [];
|
|
60
|
+
const social = data?.social ?? [];
|
|
61
|
+
const year = new Date().getFullYear();
|
|
62
|
+
const brandLabel = storeName ?? 'Store';
|
|
63
|
+
|
|
64
|
+
if (!data || (columns.length === 0 && !data.copyright && social.length === 0)) {
|
|
65
|
+
return (
|
|
66
|
+
<footer className="border-border bg-muted/30 text-muted-foreground mt-16 border-t py-8 text-sm">
|
|
67
|
+
<div className="mx-auto max-w-7xl px-4 text-center sm:px-6 lg:px-8">
|
|
68
|
+
© {year} {brandLabel}. All rights reserved.
|
|
69
|
+
</div>
|
|
70
|
+
</footer>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<footer className="border-border bg-muted/30 mt-16 border-t">
|
|
76
|
+
<div className="mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8">
|
|
77
|
+
<div className="grid grid-cols-1 gap-10 md:grid-cols-12">
|
|
78
|
+
<div className="md:col-span-4">
|
|
79
|
+
<Link
|
|
80
|
+
href="/"
|
|
81
|
+
className="text-foreground text-lg font-semibold tracking-tight"
|
|
82
|
+
>
|
|
83
|
+
{brandLabel}
|
|
84
|
+
</Link>
|
|
85
|
+
{social.length > 0 ? (
|
|
86
|
+
<div className="mt-4 flex flex-wrap items-center gap-2">
|
|
87
|
+
{social.map((entry, idx) => {
|
|
88
|
+
const key = entry.platform.toLowerCase();
|
|
89
|
+
const Icon = SOCIAL_ICONS[key];
|
|
90
|
+
return (
|
|
91
|
+
<a
|
|
92
|
+
key={`${entry.platform}-${idx}`}
|
|
93
|
+
href={entry.url}
|
|
94
|
+
target="_blank"
|
|
95
|
+
rel="noopener noreferrer"
|
|
96
|
+
aria-label={entry.platform}
|
|
97
|
+
className="border-border hover:border-foreground/40 hover:text-foreground text-muted-foreground inline-flex h-9 w-9 items-center justify-center rounded-full border transition-colors"
|
|
98
|
+
>
|
|
99
|
+
{Icon ? (
|
|
100
|
+
<Icon className="h-4 w-4" />
|
|
101
|
+
) : (
|
|
102
|
+
<span className="text-xs font-medium uppercase">
|
|
103
|
+
{entry.platform.charAt(0)}
|
|
104
|
+
</span>
|
|
105
|
+
)}
|
|
106
|
+
</a>
|
|
107
|
+
);
|
|
108
|
+
})}
|
|
109
|
+
</div>
|
|
110
|
+
) : null}
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
{columns.length > 0 ? (
|
|
114
|
+
<div className="grid grid-cols-2 gap-8 sm:grid-cols-3 md:col-span-8">
|
|
115
|
+
{columns.map((col, idx) => (
|
|
116
|
+
<div key={`${col.title}-${idx}`}>
|
|
117
|
+
<h3 className="text-foreground mb-3 text-sm font-semibold tracking-wide">
|
|
118
|
+
{col.title}
|
|
119
|
+
</h3>
|
|
120
|
+
<ul className="space-y-2">
|
|
121
|
+
{col.links.map((link, linkIdx) => (
|
|
122
|
+
<li key={`${link.url}-${linkIdx}`}>
|
|
123
|
+
<a
|
|
124
|
+
href={link.url}
|
|
125
|
+
className="text-muted-foreground hover:text-foreground text-sm transition-colors"
|
|
126
|
+
>
|
|
127
|
+
{link.label}
|
|
128
|
+
</a>
|
|
129
|
+
</li>
|
|
130
|
+
))}
|
|
131
|
+
</ul>
|
|
132
|
+
</div>
|
|
133
|
+
))}
|
|
134
|
+
</div>
|
|
135
|
+
) : null}
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<div className="border-border text-muted-foreground mt-10 flex flex-col items-center justify-between gap-2 border-t pt-6 text-xs sm:flex-row">
|
|
139
|
+
<span>{data.copyright ?? `© ${year} ${brandLabel}. All rights reserved.`}</span>
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
</footer>
|
|
143
|
+
);
|
|
144
|
+
}
|