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,174 +1,174 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import type { Product } from 'brainerce';
|
|
4
|
-
import { Link } from '@/core/lib/navigation';
|
|
5
|
-
import { CdnImage as Image } from '@/ui/shared/cdn-image';
|
|
6
|
-
import { useTranslations } from '@/core/lib/translations';
|
|
7
|
-
import { PriceDisplay } from '@/ui/product/price-display';
|
|
8
|
-
import { getProductPriceInfo } from 'brainerce';
|
|
9
|
-
import { IconArrowEnd, IconStar, IconSparkle } from '@/ui/shared/icons';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Homepage hero — grid-breaking composition: oversized serif headline over a
|
|
13
|
-
* soft forest/sand radial wash, live-catalog photography as overlapping,
|
|
14
|
-
* slightly rotated floating cards with a glassy price chip. All imagery comes
|
|
15
|
-
* from useHomeData() in the parent; motion is reduced-motion guarded.
|
|
16
|
-
*/
|
|
17
|
-
export function HeroSection({
|
|
18
|
-
storeName,
|
|
19
|
-
products = [],
|
|
20
|
-
}: {
|
|
21
|
-
storeName?: string | null;
|
|
22
|
-
products?: Product[];
|
|
23
|
-
}) {
|
|
24
|
-
const t = useTranslations('home');
|
|
25
|
-
const tc = useTranslations('common');
|
|
26
|
-
|
|
27
|
-
const heroProduct = products[0];
|
|
28
|
-
const sideProduct = products[1];
|
|
29
|
-
const heroImage = heroProduct?.images?.[0]?.url;
|
|
30
|
-
const sideImage = sideProduct?.images?.[0]?.url;
|
|
31
|
-
|
|
32
|
-
return (
|
|
33
|
-
<section className="wash-hero relative overflow-hidden">
|
|
34
|
-
{/* Drifting glow blob behind the collage */}
|
|
35
|
-
<div aria-hidden="true" className="glow-blob blob-drift end-[8%] top-[18%] h-80 w-80" />
|
|
36
|
-
|
|
37
|
-
<div className="container-page relative grid items-center gap-12 pb-20 pt-12 lg:grid-cols-12 lg:gap-8 lg:pb-28 lg:pt-20">
|
|
38
|
-
{/* Copy */}
|
|
39
|
-
<div className="reveal relative z-10 max-w-2xl lg:col-span-6">
|
|
40
|
-
<span className="glass inline-flex items-center gap-2 rounded-full px-4 py-1.5 text-xs font-semibold text-primary">
|
|
41
|
-
<IconSparkle size={16} />
|
|
42
|
-
{t('heroEyebrow')}
|
|
43
|
-
</span>
|
|
44
|
-
<h1 className="mt-6 text-[clamp(2.75rem,6.5vw,5.5rem)] leading-[1.04]">
|
|
45
|
-
{t('heroTitle')}
|
|
46
|
-
</h1>
|
|
47
|
-
<p className="mt-6 max-w-lg text-lg leading-8 text-muted-foreground">{t('heroLead')}</p>
|
|
48
|
-
|
|
49
|
-
<div className="mt-9 flex flex-wrap items-center gap-3">
|
|
50
|
-
<Link href="/products" className="btn-primary btn-lg">
|
|
51
|
-
{tc('shopNow')}
|
|
52
|
-
<IconArrowEnd size={20} className="rtl-flip" />
|
|
53
|
-
</Link>
|
|
54
|
-
<a href="#categories" className="btn-outline btn-lg bg-background/60 backdrop-blur-sm">
|
|
55
|
-
{t('heroCtaSecondary')}
|
|
56
|
-
</a>
|
|
57
|
-
</div>
|
|
58
|
-
|
|
59
|
-
{/* Soft social proof */}
|
|
60
|
-
<p className="mt-9 inline-flex items-center gap-2 text-sm text-muted-foreground">
|
|
61
|
-
<span className="inline-flex text-primary" aria-hidden="true">
|
|
62
|
-
{[1, 2, 3, 4, 5].map((n) => (
|
|
63
|
-
<IconStar key={n} size={16} filled />
|
|
64
|
-
))}
|
|
65
|
-
</span>
|
|
66
|
-
{t('heroTrust')}
|
|
67
|
-
</p>
|
|
68
|
-
</div>
|
|
69
|
-
|
|
70
|
-
{/* Overlapping floating collage — real catalog photography */}
|
|
71
|
-
{heroImage && (
|
|
72
|
-
<div className="reveal reveal-delay-1 relative mx-auto h-[26rem] w-full max-w-md sm:h-[30rem] lg:col-span-6 lg:h-[34rem] lg:max-w-none">
|
|
73
|
-
{/* Main card — tilted, floating */}
|
|
74
|
-
<Link
|
|
75
|
-
href={`/products/${heroProduct.slug || heroProduct.id}`}
|
|
76
|
-
className="group float-slow absolute inset-y-0 start-[4%] block w-[72%] -rotate-2 overflow-hidden rounded-3xl border-4 border-background shadow-[0_36px_80px_-30px_hsl(var(--primary)/0.45)]"
|
|
77
|
-
style={{ '--float-rotate': '-2deg' } as React.CSSProperties}
|
|
78
|
-
>
|
|
79
|
-
<Image
|
|
80
|
-
src={heroImage}
|
|
81
|
-
alt={heroProduct.images?.[0]?.alt || heroProduct.name}
|
|
82
|
-
fill
|
|
83
|
-
priority
|
|
84
|
-
sizes="(max-width: 1024px) 80vw, 36vw"
|
|
85
|
-
className="img-zoom object-cover"
|
|
86
|
-
/>
|
|
87
|
-
{/* Glassy shop-this chip */}
|
|
88
|
-
<span className="glass absolute bottom-5 end-5 start-5 flex items-center justify-between gap-3 px-4 py-3 sm:end-auto sm:min-w-[15rem]">
|
|
89
|
-
<span className="min-w-0">
|
|
90
|
-
<span className="block truncate text-sm font-semibold text-foreground">
|
|
91
|
-
{heroProduct.name}
|
|
92
|
-
</span>
|
|
93
|
-
<HeroPrice product={heroProduct} />
|
|
94
|
-
</span>
|
|
95
|
-
<span
|
|
96
|
-
aria-hidden="true"
|
|
97
|
-
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground shadow-[0_8px_20px_-6px_hsl(var(--primary)/0.6)]"
|
|
98
|
-
>
|
|
99
|
-
<IconArrowEnd size={18} className="rtl-flip" />
|
|
100
|
-
</span>
|
|
101
|
-
</span>
|
|
102
|
-
</Link>
|
|
103
|
-
|
|
104
|
-
{/* Secondary card — overlaps the main one, counter-tilted */}
|
|
105
|
-
{sideImage && (
|
|
106
|
-
<Link
|
|
107
|
-
href={`/products/${sideProduct.slug || sideProduct.id}`}
|
|
108
|
-
className="group float-slower absolute -end-2 top-[54%] block aspect-[3/4] w-[38%] rotate-3 overflow-hidden rounded-3xl border-4 border-background shadow-[0_28px_60px_-24px_hsl(var(--primary)/0.4)] sm:end-0"
|
|
109
|
-
style={{ '--float-rotate': '3deg' } as React.CSSProperties}
|
|
110
|
-
>
|
|
111
|
-
<Image
|
|
112
|
-
src={sideImage}
|
|
113
|
-
alt={sideProduct.images?.[0]?.alt || sideProduct.name}
|
|
114
|
-
fill
|
|
115
|
-
sizes="(max-width: 1024px) 40vw, 16vw"
|
|
116
|
-
className="img-zoom object-cover"
|
|
117
|
-
/>
|
|
118
|
-
<span className="sr-only">{sideProduct.name}</span>
|
|
119
|
-
</Link>
|
|
120
|
-
)}
|
|
121
|
-
|
|
122
|
-
{/* Floating accent chip */}
|
|
123
|
-
<span
|
|
124
|
-
aria-hidden="true"
|
|
125
|
-
className="glass float-slower absolute -start-2 top-[12%] hidden -rotate-[4deg] items-center gap-2 px-4 py-2 text-sm font-semibold text-primary sm:inline-flex"
|
|
126
|
-
style={{ '--float-rotate': '-4deg' } as React.CSSProperties}
|
|
127
|
-
>
|
|
128
|
-
<IconSparkle size={18} />
|
|
129
|
-
{t('storyStat1Value')}
|
|
130
|
-
</span>
|
|
131
|
-
</div>
|
|
132
|
-
)}
|
|
133
|
-
|
|
134
|
-
{/* Empty catalog — the same floating composition as designed washes,
|
|
135
|
-
so a brand-new store still opens rich instead of half-blank. The
|
|
136
|
-
first two published products replace these automatically. */}
|
|
137
|
-
{!heroImage && (
|
|
138
|
-
<div
|
|
139
|
-
aria-hidden="true"
|
|
140
|
-
className="reveal reveal-delay-1 relative mx-auto h-[26rem] w-full max-w-md sm:h-[30rem] lg:col-span-6 lg:h-[34rem] lg:max-w-none"
|
|
141
|
-
>
|
|
142
|
-
<div
|
|
143
|
-
className="float-slow wash-forest absolute inset-y-0 start-[4%] flex w-[72%] -rotate-2 items-center justify-center overflow-hidden rounded-3xl border-4 border-background shadow-[0_36px_80px_-30px_hsl(var(--primary)/0.45)]"
|
|
144
|
-
style={{ '--float-rotate': '-2deg' } as React.CSSProperties}
|
|
145
|
-
>
|
|
146
|
-
<IconSparkle size={56} className="text-primary/25" />
|
|
147
|
-
<span className="glass absolute bottom-5 end-5 start-5 px-4 py-3 text-center text-sm font-medium text-muted-foreground">
|
|
148
|
-
{t('heroEmptyHint')}
|
|
149
|
-
</span>
|
|
150
|
-
</div>
|
|
151
|
-
<div
|
|
152
|
-
className="float-slower wash-sand absolute -end-2 top-[54%] flex aspect-[3/4] w-[38%] rotate-3 items-center justify-center rounded-3xl border-4 border-background shadow-[0_28px_60px_-24px_hsl(var(--primary)/0.4)] sm:end-0"
|
|
153
|
-
style={{ '--float-rotate': '3deg' } as React.CSSProperties}
|
|
154
|
-
>
|
|
155
|
-
<IconStar size={32} filled className="text-primary/20" />
|
|
156
|
-
</div>
|
|
157
|
-
</div>
|
|
158
|
-
)}
|
|
159
|
-
</div>
|
|
160
|
-
</section>
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function HeroPrice({ product }: { product: Product }) {
|
|
165
|
-
const { price, originalPrice, isOnSale } = getProductPriceInfo(product);
|
|
166
|
-
return (
|
|
167
|
-
<PriceDisplay
|
|
168
|
-
price={originalPrice}
|
|
169
|
-
salePrice={isOnSale ? price : undefined}
|
|
170
|
-
size="sm"
|
|
171
|
-
className="mt-0.5"
|
|
172
|
-
/>
|
|
173
|
-
);
|
|
174
|
-
}
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { Product } from 'brainerce';
|
|
4
|
+
import { Link } from '@/core/lib/navigation';
|
|
5
|
+
import { CdnImage as Image } from '@/ui/shared/cdn-image';
|
|
6
|
+
import { useTranslations } from '@/core/lib/translations';
|
|
7
|
+
import { PriceDisplay } from '@/ui/product/price-display';
|
|
8
|
+
import { getProductPriceInfo } from 'brainerce';
|
|
9
|
+
import { IconArrowEnd, IconStar, IconSparkle } from '@/ui/shared/icons';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Homepage hero — grid-breaking composition: oversized serif headline over a
|
|
13
|
+
* soft forest/sand radial wash, live-catalog photography as overlapping,
|
|
14
|
+
* slightly rotated floating cards with a glassy price chip. All imagery comes
|
|
15
|
+
* from useHomeData() in the parent; motion is reduced-motion guarded.
|
|
16
|
+
*/
|
|
17
|
+
export function HeroSection({
|
|
18
|
+
storeName,
|
|
19
|
+
products = [],
|
|
20
|
+
}: {
|
|
21
|
+
storeName?: string | null;
|
|
22
|
+
products?: Product[];
|
|
23
|
+
}) {
|
|
24
|
+
const t = useTranslations('home');
|
|
25
|
+
const tc = useTranslations('common');
|
|
26
|
+
|
|
27
|
+
const heroProduct = products[0];
|
|
28
|
+
const sideProduct = products[1];
|
|
29
|
+
const heroImage = heroProduct?.images?.[0]?.url;
|
|
30
|
+
const sideImage = sideProduct?.images?.[0]?.url;
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<section className="wash-hero relative overflow-hidden">
|
|
34
|
+
{/* Drifting glow blob behind the collage */}
|
|
35
|
+
<div aria-hidden="true" className="glow-blob blob-drift end-[8%] top-[18%] h-80 w-80" />
|
|
36
|
+
|
|
37
|
+
<div className="container-page relative grid items-center gap-12 pb-20 pt-12 lg:grid-cols-12 lg:gap-8 lg:pb-28 lg:pt-20">
|
|
38
|
+
{/* Copy */}
|
|
39
|
+
<div className="reveal relative z-10 max-w-2xl lg:col-span-6">
|
|
40
|
+
<span className="glass inline-flex items-center gap-2 rounded-full px-4 py-1.5 text-xs font-semibold text-primary">
|
|
41
|
+
<IconSparkle size={16} />
|
|
42
|
+
{t('heroEyebrow')}
|
|
43
|
+
</span>
|
|
44
|
+
<h1 className="mt-6 text-[clamp(2.75rem,6.5vw,5.5rem)] leading-[1.04]">
|
|
45
|
+
{t('heroTitle')}
|
|
46
|
+
</h1>
|
|
47
|
+
<p className="mt-6 max-w-lg text-lg leading-8 text-muted-foreground">{t('heroLead')}</p>
|
|
48
|
+
|
|
49
|
+
<div className="mt-9 flex flex-wrap items-center gap-3">
|
|
50
|
+
<Link href="/products" className="btn-primary btn-lg">
|
|
51
|
+
{tc('shopNow')}
|
|
52
|
+
<IconArrowEnd size={20} className="rtl-flip" />
|
|
53
|
+
</Link>
|
|
54
|
+
<a href="#categories" className="btn-outline btn-lg bg-background/60 backdrop-blur-sm">
|
|
55
|
+
{t('heroCtaSecondary')}
|
|
56
|
+
</a>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
{/* Soft social proof */}
|
|
60
|
+
<p className="mt-9 inline-flex items-center gap-2 text-sm text-muted-foreground">
|
|
61
|
+
<span className="inline-flex text-primary" aria-hidden="true">
|
|
62
|
+
{[1, 2, 3, 4, 5].map((n) => (
|
|
63
|
+
<IconStar key={n} size={16} filled />
|
|
64
|
+
))}
|
|
65
|
+
</span>
|
|
66
|
+
{t('heroTrust')}
|
|
67
|
+
</p>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
{/* Overlapping floating collage — real catalog photography */}
|
|
71
|
+
{heroImage && (
|
|
72
|
+
<div className="reveal reveal-delay-1 relative mx-auto h-[26rem] w-full max-w-md sm:h-[30rem] lg:col-span-6 lg:h-[34rem] lg:max-w-none">
|
|
73
|
+
{/* Main card — tilted, floating */}
|
|
74
|
+
<Link
|
|
75
|
+
href={`/products/${heroProduct.slug || heroProduct.id}`}
|
|
76
|
+
className="group float-slow absolute inset-y-0 start-[4%] block w-[72%] -rotate-2 overflow-hidden rounded-3xl border-4 border-background shadow-[0_36px_80px_-30px_hsl(var(--primary)/0.45)]"
|
|
77
|
+
style={{ '--float-rotate': '-2deg' } as React.CSSProperties}
|
|
78
|
+
>
|
|
79
|
+
<Image
|
|
80
|
+
src={heroImage}
|
|
81
|
+
alt={heroProduct.images?.[0]?.alt || heroProduct.name}
|
|
82
|
+
fill
|
|
83
|
+
priority
|
|
84
|
+
sizes="(max-width: 1024px) 80vw, 36vw"
|
|
85
|
+
className="img-zoom object-cover"
|
|
86
|
+
/>
|
|
87
|
+
{/* Glassy shop-this chip */}
|
|
88
|
+
<span className="glass absolute bottom-5 end-5 start-5 flex items-center justify-between gap-3 px-4 py-3 sm:end-auto sm:min-w-[15rem]">
|
|
89
|
+
<span className="min-w-0">
|
|
90
|
+
<span className="block truncate text-sm font-semibold text-foreground">
|
|
91
|
+
{heroProduct.name}
|
|
92
|
+
</span>
|
|
93
|
+
<HeroPrice product={heroProduct} />
|
|
94
|
+
</span>
|
|
95
|
+
<span
|
|
96
|
+
aria-hidden="true"
|
|
97
|
+
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground shadow-[0_8px_20px_-6px_hsl(var(--primary)/0.6)]"
|
|
98
|
+
>
|
|
99
|
+
<IconArrowEnd size={18} className="rtl-flip" />
|
|
100
|
+
</span>
|
|
101
|
+
</span>
|
|
102
|
+
</Link>
|
|
103
|
+
|
|
104
|
+
{/* Secondary card — overlaps the main one, counter-tilted */}
|
|
105
|
+
{sideImage && (
|
|
106
|
+
<Link
|
|
107
|
+
href={`/products/${sideProduct.slug || sideProduct.id}`}
|
|
108
|
+
className="group float-slower absolute -end-2 top-[54%] block aspect-[3/4] w-[38%] rotate-3 overflow-hidden rounded-3xl border-4 border-background shadow-[0_28px_60px_-24px_hsl(var(--primary)/0.4)] sm:end-0"
|
|
109
|
+
style={{ '--float-rotate': '3deg' } as React.CSSProperties}
|
|
110
|
+
>
|
|
111
|
+
<Image
|
|
112
|
+
src={sideImage}
|
|
113
|
+
alt={sideProduct.images?.[0]?.alt || sideProduct.name}
|
|
114
|
+
fill
|
|
115
|
+
sizes="(max-width: 1024px) 40vw, 16vw"
|
|
116
|
+
className="img-zoom object-cover"
|
|
117
|
+
/>
|
|
118
|
+
<span className="sr-only">{sideProduct.name}</span>
|
|
119
|
+
</Link>
|
|
120
|
+
)}
|
|
121
|
+
|
|
122
|
+
{/* Floating accent chip */}
|
|
123
|
+
<span
|
|
124
|
+
aria-hidden="true"
|
|
125
|
+
className="glass float-slower absolute -start-2 top-[12%] hidden -rotate-[4deg] items-center gap-2 px-4 py-2 text-sm font-semibold text-primary sm:inline-flex"
|
|
126
|
+
style={{ '--float-rotate': '-4deg' } as React.CSSProperties}
|
|
127
|
+
>
|
|
128
|
+
<IconSparkle size={18} />
|
|
129
|
+
{t('storyStat1Value')}
|
|
130
|
+
</span>
|
|
131
|
+
</div>
|
|
132
|
+
)}
|
|
133
|
+
|
|
134
|
+
{/* Empty catalog — the same floating composition as designed washes,
|
|
135
|
+
so a brand-new store still opens rich instead of half-blank. The
|
|
136
|
+
first two published products replace these automatically. */}
|
|
137
|
+
{!heroImage && (
|
|
138
|
+
<div
|
|
139
|
+
aria-hidden="true"
|
|
140
|
+
className="reveal reveal-delay-1 relative mx-auto h-[26rem] w-full max-w-md sm:h-[30rem] lg:col-span-6 lg:h-[34rem] lg:max-w-none"
|
|
141
|
+
>
|
|
142
|
+
<div
|
|
143
|
+
className="float-slow wash-forest absolute inset-y-0 start-[4%] flex w-[72%] -rotate-2 items-center justify-center overflow-hidden rounded-3xl border-4 border-background shadow-[0_36px_80px_-30px_hsl(var(--primary)/0.45)]"
|
|
144
|
+
style={{ '--float-rotate': '-2deg' } as React.CSSProperties}
|
|
145
|
+
>
|
|
146
|
+
<IconSparkle size={56} className="text-primary/25" />
|
|
147
|
+
<span className="glass absolute bottom-5 end-5 start-5 px-4 py-3 text-center text-sm font-medium text-muted-foreground">
|
|
148
|
+
{t('heroEmptyHint')}
|
|
149
|
+
</span>
|
|
150
|
+
</div>
|
|
151
|
+
<div
|
|
152
|
+
className="float-slower wash-sand absolute -end-2 top-[54%] flex aspect-[3/4] w-[38%] rotate-3 items-center justify-center rounded-3xl border-4 border-background shadow-[0_28px_60px_-24px_hsl(var(--primary)/0.4)] sm:end-0"
|
|
153
|
+
style={{ '--float-rotate': '3deg' } as React.CSSProperties}
|
|
154
|
+
>
|
|
155
|
+
<IconStar size={32} filled className="text-primary/20" />
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
)}
|
|
159
|
+
</div>
|
|
160
|
+
</section>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function HeroPrice({ product }: { product: Product }) {
|
|
165
|
+
const { price, originalPrice, isOnSale } = getProductPriceInfo(product);
|
|
166
|
+
return (
|
|
167
|
+
<PriceDisplay
|
|
168
|
+
price={originalPrice}
|
|
169
|
+
salePrice={isOnSale ? price : undefined}
|
|
170
|
+
size="sm"
|
|
171
|
+
className="mt-0.5"
|
|
172
|
+
/>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
@@ -1,54 +1,54 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useStoreInfo } from '@/core/providers/store-provider';
|
|
4
|
-
import { useHomeData } from '@/core/hooks/use-home-data';
|
|
5
|
-
import { LoadingSpinner } from '@/ui/shared/loading-spinner';
|
|
6
|
-
import { Reveal } from '@/ui/shared/reveal';
|
|
7
|
-
import { DiscountBannerStrip } from './discount-banner-strip';
|
|
8
|
-
import { HeroSection } from './hero-section';
|
|
9
|
-
import { BenefitsBand } from './benefits-band';
|
|
10
|
-
import { FeaturedProductsSection } from './featured-products-section';
|
|
11
|
-
import { CategoryTiles } from './category-tiles';
|
|
12
|
-
import { EditorialBand } from './editorial-band';
|
|
13
|
-
import { TestimonialsBand } from './testimonials-band';
|
|
14
|
-
import { NewsletterSection } from './newsletter-section';
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Homepage composition — data via useHomeData() and useStoreInfo():
|
|
18
|
-
* banners → hero (floating collage) → tilted values marquee → trust band →
|
|
19
|
-
* featured grid → category tiles → studio story → testimonials → newsletter.
|
|
20
|
-
* Sections fade up on scroll via <Reveal> (reduced-motion safe).
|
|
21
|
-
*/
|
|
22
|
-
export function HomeClient() {
|
|
23
|
-
const { storeInfo, loading: storeLoading } = useStoreInfo();
|
|
24
|
-
const { products, banners, loading } = useHomeData();
|
|
25
|
-
|
|
26
|
-
if (storeLoading || loading) {
|
|
27
|
-
return <LoadingSpinner size="lg" className="min-h-[70vh]" />;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return (
|
|
31
|
-
<div className="overflow-x-clip">
|
|
32
|
-
<DiscountBannerStrip banners={banners} />
|
|
33
|
-
<HeroSection storeName={storeInfo?.name} products={products} />
|
|
34
|
-
<Reveal>
|
|
35
|
-
<BenefitsBand />
|
|
36
|
-
</Reveal>
|
|
37
|
-
<Reveal>
|
|
38
|
-
<FeaturedProductsSection products={products} />
|
|
39
|
-
</Reveal>
|
|
40
|
-
<Reveal>
|
|
41
|
-
<CategoryTiles products={products} />
|
|
42
|
-
</Reveal>
|
|
43
|
-
<Reveal>
|
|
44
|
-
<EditorialBand />
|
|
45
|
-
</Reveal>
|
|
46
|
-
<Reveal>
|
|
47
|
-
<TestimonialsBand />
|
|
48
|
-
</Reveal>
|
|
49
|
-
<Reveal>
|
|
50
|
-
<NewsletterSection />
|
|
51
|
-
</Reveal>
|
|
52
|
-
</div>
|
|
53
|
-
);
|
|
54
|
-
}
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useStoreInfo } from '@/core/providers/store-provider';
|
|
4
|
+
import { useHomeData } from '@/core/hooks/use-home-data';
|
|
5
|
+
import { LoadingSpinner } from '@/ui/shared/loading-spinner';
|
|
6
|
+
import { Reveal } from '@/ui/shared/reveal';
|
|
7
|
+
import { DiscountBannerStrip } from './discount-banner-strip';
|
|
8
|
+
import { HeroSection } from './hero-section';
|
|
9
|
+
import { BenefitsBand } from './benefits-band';
|
|
10
|
+
import { FeaturedProductsSection } from './featured-products-section';
|
|
11
|
+
import { CategoryTiles } from './category-tiles';
|
|
12
|
+
import { EditorialBand } from './editorial-band';
|
|
13
|
+
import { TestimonialsBand } from './testimonials-band';
|
|
14
|
+
import { NewsletterSection } from './newsletter-section';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Homepage composition — data via useHomeData() and useStoreInfo():
|
|
18
|
+
* banners → hero (floating collage) → tilted values marquee → trust band →
|
|
19
|
+
* featured grid → category tiles → studio story → testimonials → newsletter.
|
|
20
|
+
* Sections fade up on scroll via <Reveal> (reduced-motion safe).
|
|
21
|
+
*/
|
|
22
|
+
export function HomeClient() {
|
|
23
|
+
const { storeInfo, loading: storeLoading } = useStoreInfo();
|
|
24
|
+
const { products, banners, loading } = useHomeData();
|
|
25
|
+
|
|
26
|
+
if (storeLoading || loading) {
|
|
27
|
+
return <LoadingSpinner size="lg" className="min-h-[70vh]" />;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div className="overflow-x-clip">
|
|
32
|
+
<DiscountBannerStrip banners={banners} />
|
|
33
|
+
<HeroSection storeName={storeInfo?.name} products={products} />
|
|
34
|
+
<Reveal>
|
|
35
|
+
<BenefitsBand />
|
|
36
|
+
</Reveal>
|
|
37
|
+
<Reveal>
|
|
38
|
+
<FeaturedProductsSection products={products} />
|
|
39
|
+
</Reveal>
|
|
40
|
+
<Reveal>
|
|
41
|
+
<CategoryTiles products={products} />
|
|
42
|
+
</Reveal>
|
|
43
|
+
<Reveal>
|
|
44
|
+
<EditorialBand />
|
|
45
|
+
</Reveal>
|
|
46
|
+
<Reveal>
|
|
47
|
+
<TestimonialsBand />
|
|
48
|
+
</Reveal>
|
|
49
|
+
<Reveal>
|
|
50
|
+
<NewsletterSection />
|
|
51
|
+
</Reveal>
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
@@ -1,78 +1,78 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Newsletter signup band — forest brand block with an inline email form.
|
|
5
|
-
* Client-side success state (template default; wire a provider later).
|
|
6
|
-
*/
|
|
7
|
-
import * as React from 'react';
|
|
8
|
-
import { useTranslations } from '@/core/lib/translations';
|
|
9
|
-
import { IconMail, IconCheck } from '@/ui/shared/icons';
|
|
10
|
-
|
|
11
|
-
export function NewsletterSection() {
|
|
12
|
-
const t = useTranslations('home');
|
|
13
|
-
const [email, setEmail] = React.useState('');
|
|
14
|
-
const [done, setDone] = React.useState(false);
|
|
15
|
-
|
|
16
|
-
function handleSubmit(e: React.FormEvent) {
|
|
17
|
-
e.preventDefault();
|
|
18
|
-
if (!email.trim()) return;
|
|
19
|
-
setDone(true);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
return (
|
|
23
|
-
<section className="section-pad">
|
|
24
|
-
<div className="container-page">
|
|
25
|
-
<div className="wash-forest relative overflow-hidden rounded-3xl px-6 py-12 text-primary-foreground shadow-[0_30px_70px_-28px_hsl(var(--primary)/0.55)] sm:px-12 lg:px-20 lg:py-16">
|
|
26
|
-
<div className="mx-auto flex max-w-2xl flex-col items-center text-center">
|
|
27
|
-
<span
|
|
28
|
-
aria-hidden="true"
|
|
29
|
-
className="mb-5 flex h-12 w-12 items-center justify-center rounded-full bg-primary-foreground/10"
|
|
30
|
-
>
|
|
31
|
-
<IconMail size={24} />
|
|
32
|
-
</span>
|
|
33
|
-
<h2 className="text-4xl text-primary-foreground sm:text-5xl">{t('newsletterTitle')}</h2>
|
|
34
|
-
<p className="mt-3 max-w-lg text-primary-foreground/75">{t('newsletterSubtitle')}</p>
|
|
35
|
-
|
|
36
|
-
{done ? (
|
|
37
|
-
<p
|
|
38
|
-
role="status"
|
|
39
|
-
className="mt-8 inline-flex items-center gap-2 rounded-full bg-primary-foreground/10 px-5 py-3 text-sm font-semibold"
|
|
40
|
-
>
|
|
41
|
-
<IconCheck size={18} />
|
|
42
|
-
{t('newsletterSuccess')}
|
|
43
|
-
</p>
|
|
44
|
-
) : (
|
|
45
|
-
<form
|
|
46
|
-
onSubmit={handleSubmit}
|
|
47
|
-
className="mt-8 flex w-full max-w-md flex-col gap-3 sm:flex-row"
|
|
48
|
-
>
|
|
49
|
-
<label className="sr-only" htmlFor="newsletter-email">
|
|
50
|
-
{t('newsletterPlaceholder')}
|
|
51
|
-
</label>
|
|
52
|
-
<input
|
|
53
|
-
id="newsletter-email"
|
|
54
|
-
type="email"
|
|
55
|
-
required
|
|
56
|
-
value={email}
|
|
57
|
-
onChange={(e) => setEmail(e.target.value)}
|
|
58
|
-
placeholder={t('newsletterPlaceholder')}
|
|
59
|
-
className="input h-12 flex-1 border-transparent bg-background text-foreground"
|
|
60
|
-
/>
|
|
61
|
-
<button
|
|
62
|
-
type="submit"
|
|
63
|
-
className="btn h-12 shrink-0 bg-background px-7 text-foreground hover:bg-background/90"
|
|
64
|
-
>
|
|
65
|
-
{t('newsletterCta')}
|
|
66
|
-
</button>
|
|
67
|
-
</form>
|
|
68
|
-
)}
|
|
69
|
-
|
|
70
|
-
{!done && (
|
|
71
|
-
<p className="mt-3 text-xs text-primary-foreground/60">{t('newsletterPrivacy')}</p>
|
|
72
|
-
)}
|
|
73
|
-
</div>
|
|
74
|
-
</div>
|
|
75
|
-
</div>
|
|
76
|
-
</section>
|
|
77
|
-
);
|
|
78
|
-
}
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Newsletter signup band — forest brand block with an inline email form.
|
|
5
|
+
* Client-side success state (template default; wire a provider later).
|
|
6
|
+
*/
|
|
7
|
+
import * as React from 'react';
|
|
8
|
+
import { useTranslations } from '@/core/lib/translations';
|
|
9
|
+
import { IconMail, IconCheck } from '@/ui/shared/icons';
|
|
10
|
+
|
|
11
|
+
export function NewsletterSection() {
|
|
12
|
+
const t = useTranslations('home');
|
|
13
|
+
const [email, setEmail] = React.useState('');
|
|
14
|
+
const [done, setDone] = React.useState(false);
|
|
15
|
+
|
|
16
|
+
function handleSubmit(e: React.FormEvent) {
|
|
17
|
+
e.preventDefault();
|
|
18
|
+
if (!email.trim()) return;
|
|
19
|
+
setDone(true);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<section className="section-pad">
|
|
24
|
+
<div className="container-page">
|
|
25
|
+
<div className="wash-forest relative overflow-hidden rounded-3xl px-6 py-12 text-primary-foreground shadow-[0_30px_70px_-28px_hsl(var(--primary)/0.55)] sm:px-12 lg:px-20 lg:py-16">
|
|
26
|
+
<div className="mx-auto flex max-w-2xl flex-col items-center text-center">
|
|
27
|
+
<span
|
|
28
|
+
aria-hidden="true"
|
|
29
|
+
className="mb-5 flex h-12 w-12 items-center justify-center rounded-full bg-primary-foreground/10"
|
|
30
|
+
>
|
|
31
|
+
<IconMail size={24} />
|
|
32
|
+
</span>
|
|
33
|
+
<h2 className="text-4xl text-primary-foreground sm:text-5xl">{t('newsletterTitle')}</h2>
|
|
34
|
+
<p className="mt-3 max-w-lg text-primary-foreground/75">{t('newsletterSubtitle')}</p>
|
|
35
|
+
|
|
36
|
+
{done ? (
|
|
37
|
+
<p
|
|
38
|
+
role="status"
|
|
39
|
+
className="mt-8 inline-flex items-center gap-2 rounded-full bg-primary-foreground/10 px-5 py-3 text-sm font-semibold"
|
|
40
|
+
>
|
|
41
|
+
<IconCheck size={18} />
|
|
42
|
+
{t('newsletterSuccess')}
|
|
43
|
+
</p>
|
|
44
|
+
) : (
|
|
45
|
+
<form
|
|
46
|
+
onSubmit={handleSubmit}
|
|
47
|
+
className="mt-8 flex w-full max-w-md flex-col gap-3 sm:flex-row"
|
|
48
|
+
>
|
|
49
|
+
<label className="sr-only" htmlFor="newsletter-email">
|
|
50
|
+
{t('newsletterPlaceholder')}
|
|
51
|
+
</label>
|
|
52
|
+
<input
|
|
53
|
+
id="newsletter-email"
|
|
54
|
+
type="email"
|
|
55
|
+
required
|
|
56
|
+
value={email}
|
|
57
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
58
|
+
placeholder={t('newsletterPlaceholder')}
|
|
59
|
+
className="input h-12 flex-1 border-transparent bg-background text-foreground"
|
|
60
|
+
/>
|
|
61
|
+
<button
|
|
62
|
+
type="submit"
|
|
63
|
+
className="btn h-12 shrink-0 bg-background px-7 text-foreground hover:bg-background/90"
|
|
64
|
+
>
|
|
65
|
+
{t('newsletterCta')}
|
|
66
|
+
</button>
|
|
67
|
+
</form>
|
|
68
|
+
)}
|
|
69
|
+
|
|
70
|
+
{!done && (
|
|
71
|
+
<p className="mt-3 text-xs text-primary-foreground/60">{t('newsletterPrivacy')}</p>
|
|
72
|
+
)}
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
</section>
|
|
77
|
+
);
|
|
78
|
+
}
|