create-brainerce-store 1.62.0 → 1.64.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.
Files changed (203) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +71 -71
  3. package/dist/index.js +26 -3
  4. package/messages/en.json +502 -502
  5. package/messages/he.json +502 -502
  6. package/package.json +1 -1
  7. package/templates/nextjs/base/.claude/commands/design.md +60 -60
  8. package/templates/nextjs/base/.env.local.ejs +24 -24
  9. package/templates/nextjs/base/.eslintrc.json +57 -57
  10. package/templates/nextjs/base/.mcp.json +8 -8
  11. package/templates/nextjs/base/AI-GUIDE.md +174 -174
  12. package/templates/nextjs/base/TRANSLATIONS.md +200 -200
  13. package/templates/nextjs/base/package.json.ejs +52 -52
  14. package/templates/nextjs/base/src/app/agents.md/route.ts +87 -0
  15. package/templates/nextjs/base/src/app/blog/[slug]/page.tsx.ejs +308 -262
  16. package/templates/nextjs/base/src/app/blog/page.tsx.ejs +276 -276
  17. package/templates/nextjs/base/src/app/blog/rss.xml/route.ts +70 -0
  18. package/templates/nextjs/base/src/app/cart/page.tsx +7 -7
  19. package/templates/nextjs/base/src/app/category/[slug]/page.tsx +29 -3
  20. package/templates/nextjs/base/src/app/checkout/page.tsx +12 -0
  21. package/templates/nextjs/base/src/app/contact/layout.tsx.ejs +14 -0
  22. package/templates/nextjs/base/src/app/globals.css +36 -36
  23. package/templates/nextjs/base/src/app/indexnow-key.txt/route.ts +26 -26
  24. package/templates/nextjs/base/src/app/layout.tsx.ejs +30 -0
  25. package/templates/nextjs/base/src/app/llms.txt/route.ts +28 -5
  26. package/templates/nextjs/base/src/app/not-found.tsx.ejs +35 -0
  27. package/templates/nextjs/base/src/app/opengraph-image.tsx +109 -0
  28. package/templates/nextjs/base/src/app/order-confirmation/page.tsx +295 -278
  29. package/templates/nextjs/base/src/app/order-status/page.tsx +121 -121
  30. package/templates/nextjs/base/src/app/page.tsx +64 -64
  31. package/templates/nextjs/base/src/app/products/[slug]/page.tsx +14 -2
  32. package/templates/nextjs/base/src/app/products/page.tsx +19 -19
  33. package/templates/nextjs/base/src/app/robots.ts +73 -14
  34. package/templates/nextjs/base/src/app/sitemap.ts +89 -66
  35. package/templates/nextjs/base/src/components/account/order-history.tsx +385 -385
  36. package/templates/nextjs/base/src/components/brainerce-bot.tsx +70 -70
  37. package/templates/nextjs/base/src/components/checkout/checkout-form.tsx +612 -612
  38. package/templates/nextjs/base/src/components/checkout/custom-fields-step.tsx +292 -292
  39. package/templates/nextjs/base/src/components/checkout/date-picker.tsx +309 -309
  40. package/templates/nextjs/base/src/components/checkout/datetime-picker.tsx +144 -144
  41. package/templates/nextjs/base/src/components/checkout/order-bump-card.tsx +242 -242
  42. package/templates/nextjs/base/src/components/checkout/pickup-step.tsx +198 -198
  43. package/templates/nextjs/base/src/components/checkout/shipping-step.tsx +125 -125
  44. package/templates/nextjs/base/src/components/checkout/tax-display.tsx +74 -74
  45. package/templates/nextjs/base/src/components/seo/article-json-ld.tsx +59 -59
  46. package/templates/nextjs/base/src/components/seo/breadcrumbs.tsx +37 -37
  47. package/templates/nextjs/base/src/components/seo/category-json-ld.tsx +61 -61
  48. package/templates/nextjs/base/src/components/seo/organization-json-ld.tsx +97 -97
  49. package/templates/nextjs/base/src/components/seo/product-json-ld.tsx +23 -131
  50. package/templates/nextjs/base/src/components/tracking-bootstrap.tsx +62 -0
  51. package/templates/nextjs/base/src/components/ui/accordion.tsx +53 -53
  52. package/templates/nextjs/base/src/components/ui/badge.tsx +32 -32
  53. package/templates/nextjs/base/src/components/ui/button.tsx +49 -49
  54. package/templates/nextjs/base/src/components/ui/card.tsx +55 -55
  55. package/templates/nextjs/base/src/components/ui/checkbox.tsx +28 -28
  56. package/templates/nextjs/base/src/components/ui/dialog.tsx +107 -107
  57. package/templates/nextjs/base/src/components/ui/input.tsx +22 -22
  58. package/templates/nextjs/base/src/components/ui/label.tsx +21 -21
  59. package/templates/nextjs/base/src/components/ui/radio-group.tsx +38 -38
  60. package/templates/nextjs/base/src/components/ui/select.tsx +153 -153
  61. package/templates/nextjs/base/src/components/ui/separator.tsx +26 -26
  62. package/templates/nextjs/base/src/components/ui/sheet.tsx +125 -125
  63. package/templates/nextjs/base/src/components/ui/skeleton.tsx +7 -7
  64. package/templates/nextjs/base/src/components/ui/tabs.tsx +55 -55
  65. package/templates/nextjs/base/src/components/ui/textarea.tsx +21 -21
  66. package/templates/nextjs/base/src/components/ui/tooltip.tsx +32 -32
  67. package/templates/nextjs/base/src/core/hooks/use-cart-page.ts +58 -58
  68. package/templates/nextjs/base/src/core/hooks/use-home-data.ts +51 -51
  69. package/templates/nextjs/base/src/core/hooks/use-product-listing.ts +295 -295
  70. package/templates/nextjs/base/src/core/hooks/use-product-page.ts +20 -2
  71. package/templates/nextjs/base/src/core/lib/image-hosts.ts +7 -7
  72. package/templates/nextjs/base/src/core/lib/product-options.ts +121 -121
  73. package/templates/nextjs/base/src/core/lib/resolve-currency.ts +20 -20
  74. package/templates/nextjs/base/src/core/lib/safe-redirect.ts +38 -38
  75. package/templates/nextjs/base/src/core/lib/seo.ts +46 -46
  76. package/templates/nextjs/base/src/core/lib/store-info.ts +17 -0
  77. package/templates/nextjs/base/src/core/lib/tracking.ts +171 -0
  78. package/templates/nextjs/base/src/core/lib/use-currency.ts +19 -19
  79. package/templates/nextjs/base/src/core/lib/utils.ts +21 -21
  80. package/templates/nextjs/base/src/middleware.ts.ejs +54 -2
  81. package/templates/nextjs/base/src/ui/cart/cart-bundle-offer.tsx +117 -117
  82. package/templates/nextjs/base/src/ui/cart/cart-item.tsx +146 -146
  83. package/templates/nextjs/base/src/ui/cart/cart-nudges.tsx +28 -28
  84. package/templates/nextjs/base/src/ui/cart/cart-summary.tsx +110 -110
  85. package/templates/nextjs/base/src/ui/cart/cart-upgrade-banner.tsx +126 -126
  86. package/templates/nextjs/base/src/ui/cart/cart-view.tsx +140 -140
  87. package/templates/nextjs/base/src/ui/cart/coupon-input.tsx +134 -134
  88. package/templates/nextjs/base/src/ui/cart/free-shipping-bar.tsx +61 -61
  89. package/templates/nextjs/base/src/ui/cart/reservation-countdown.tsx +95 -95
  90. package/templates/nextjs/base/src/ui/home/discount-banner-strip.tsx +22 -20
  91. package/templates/nextjs/base/src/ui/home/featured-products-section.tsx +24 -24
  92. package/templates/nextjs/base/src/ui/home/hero-section.tsx +27 -27
  93. package/templates/nextjs/base/src/ui/home/home-client.tsx +29 -29
  94. package/templates/nextjs/base/src/ui/layout/announcement-bar.tsx.ejs +116 -116
  95. package/templates/nextjs/base/src/ui/layout/faq-section.tsx.ejs +90 -90
  96. package/templates/nextjs/base/src/ui/layout/header-account.tsx +40 -40
  97. package/templates/nextjs/base/src/ui/layout/language-switcher.tsx.ejs +80 -80
  98. package/templates/nextjs/base/src/ui/layout/site-footer.tsx.ejs +144 -144
  99. package/templates/nextjs/base/src/ui/layout/site-header.tsx.ejs +152 -152
  100. package/templates/nextjs/base/src/ui/product/customization-fields.tsx +457 -457
  101. package/templates/nextjs/base/src/ui/product/discount-badge.tsx +18 -18
  102. package/templates/nextjs/base/src/ui/product/frequently-bought-together.tsx +191 -191
  103. package/templates/nextjs/base/src/ui/product/modifier-group-selector.tsx +206 -206
  104. package/templates/nextjs/base/src/ui/product/price-display.tsx +62 -62
  105. package/templates/nextjs/base/src/ui/product/product-card.tsx +7 -2
  106. package/templates/nextjs/base/src/ui/product/product-client-section.tsx +455 -455
  107. package/templates/nextjs/base/src/ui/product/product-listing.tsx +446 -446
  108. package/templates/nextjs/base/src/ui/product/recommendation-section.tsx +102 -102
  109. package/templates/nextjs/base/src/ui/product/review-form.tsx +294 -294
  110. package/templates/nextjs/base/src/ui/product/reviews-section.tsx.ejs +108 -108
  111. package/templates/nextjs/base/src/ui/product/stock-badge.tsx +61 -61
  112. package/templates/nextjs/base/src/ui/product/variant-selector.tsx +291 -291
  113. package/templates/nextjs/base/src/ui/shared/cdn-image.tsx +26 -26
  114. package/templates/nextjs/base/tailwind.config.ts +69 -69
  115. package/templates/nextjs/designs/atelier/app-overlay/layout.tsx.ejs +30 -0
  116. package/templates/nextjs/designs/atelier/globals.css +402 -402
  117. package/templates/nextjs/designs/atelier/messages-patch/en.json +100 -100
  118. package/templates/nextjs/designs/atelier/messages-patch/he.json +100 -100
  119. package/templates/nextjs/designs/atelier/ui/cart/cart-bundle-offer.tsx +110 -110
  120. package/templates/nextjs/designs/atelier/ui/cart/cart-drawer.tsx +163 -163
  121. package/templates/nextjs/designs/atelier/ui/cart/cart-item.tsx +140 -140
  122. package/templates/nextjs/designs/atelier/ui/cart/cart-nudges.tsx +34 -34
  123. package/templates/nextjs/designs/atelier/ui/cart/cart-summary.tsx +107 -107
  124. package/templates/nextjs/designs/atelier/ui/cart/cart-upgrade-banner.tsx +123 -123
  125. package/templates/nextjs/designs/atelier/ui/cart/cart-view.tsx +147 -147
  126. package/templates/nextjs/designs/atelier/ui/cart/coupon-input.tsx +142 -142
  127. package/templates/nextjs/designs/atelier/ui/cart/free-shipping-bar.tsx +77 -77
  128. package/templates/nextjs/designs/atelier/ui/cart/reservation-countdown.tsx +89 -89
  129. package/templates/nextjs/designs/atelier/ui/home/benefits-band.tsx +34 -34
  130. package/templates/nextjs/designs/atelier/ui/home/category-tiles.tsx +151 -151
  131. package/templates/nextjs/designs/atelier/ui/home/discount-banner-strip.tsx +27 -27
  132. package/templates/nextjs/designs/atelier/ui/home/editorial-band.tsx +75 -75
  133. package/templates/nextjs/designs/atelier/ui/home/featured-products-section.tsx +36 -36
  134. package/templates/nextjs/designs/atelier/ui/home/hero-section.tsx +174 -174
  135. package/templates/nextjs/designs/atelier/ui/home/home-client.tsx +54 -54
  136. package/templates/nextjs/designs/atelier/ui/home/newsletter-section.tsx +78 -78
  137. package/templates/nextjs/designs/atelier/ui/home/testimonials-band.tsx +46 -46
  138. package/templates/nextjs/designs/atelier/ui/layout/announcement-bar.tsx +118 -118
  139. package/templates/nextjs/designs/atelier/ui/layout/faq-section.tsx +96 -96
  140. package/templates/nextjs/designs/atelier/ui/layout/header-account.tsx +36 -36
  141. package/templates/nextjs/designs/atelier/ui/layout/header-cart.tsx +36 -36
  142. package/templates/nextjs/designs/atelier/ui/layout/language-switcher.tsx.ejs +137 -137
  143. package/templates/nextjs/designs/atelier/ui/layout/nav-url.ts +14 -14
  144. package/templates/nextjs/designs/atelier/ui/layout/rich-text-block.tsx +34 -34
  145. package/templates/nextjs/designs/atelier/ui/layout/site-footer.tsx.ejs +141 -141
  146. package/templates/nextjs/designs/atelier/ui/layout/site-header.tsx.ejs +138 -138
  147. package/templates/nextjs/designs/atelier/ui/product/customization-fields.tsx +466 -466
  148. package/templates/nextjs/designs/atelier/ui/product/discount-badge.tsx +17 -17
  149. package/templates/nextjs/designs/atelier/ui/product/frequently-bought-together.tsx +196 -196
  150. package/templates/nextjs/designs/atelier/ui/product/modifier-group-selector.tsx +193 -193
  151. package/templates/nextjs/designs/atelier/ui/product/price-display.tsx +69 -69
  152. package/templates/nextjs/designs/atelier/ui/product/product-card.tsx +13 -11
  153. package/templates/nextjs/designs/atelier/ui/product/product-client-section.tsx +477 -477
  154. package/templates/nextjs/designs/atelier/ui/product/product-grid.tsx +42 -42
  155. package/templates/nextjs/designs/atelier/ui/product/product-listing.tsx +260 -260
  156. package/templates/nextjs/designs/atelier/ui/product/recommendation-section.tsx +95 -95
  157. package/templates/nextjs/designs/atelier/ui/product/review-form.tsx +267 -267
  158. package/templates/nextjs/designs/atelier/ui/product/reviews-section.tsx.ejs +148 -148
  159. package/templates/nextjs/designs/atelier/ui/product/stock-badge.tsx +66 -66
  160. package/templates/nextjs/designs/atelier/ui/product/variant-selector.tsx +206 -206
  161. package/templates/nextjs/designs/atelier/ui/shared/cdn-image.tsx +29 -29
  162. package/templates/nextjs/designs/atelier/ui/shared/fly-to-cart.ts +86 -86
  163. package/templates/nextjs/designs/atelier/ui/shared/icons.tsx +276 -276
  164. package/templates/nextjs/designs/atelier/ui/shared/loading-spinner.tsx +63 -63
  165. package/templates/nextjs/designs/atelier/ui/shared/reveal.tsx +71 -71
  166. package/templates/nextjs/designs/atelier/ui/shared/select-menu.tsx +125 -125
  167. package/templates/nextjs/ui-canvas/cart/cart-bundle-offer.tsx +89 -89
  168. package/templates/nextjs/ui-canvas/cart/cart-item.tsx +123 -123
  169. package/templates/nextjs/ui-canvas/cart/cart-nudges.tsx +29 -27
  170. package/templates/nextjs/ui-canvas/cart/cart-summary.tsx +99 -99
  171. package/templates/nextjs/ui-canvas/cart/cart-upgrade-banner.tsx +105 -105
  172. package/templates/nextjs/ui-canvas/cart/cart-view.tsx +106 -106
  173. package/templates/nextjs/ui-canvas/cart/coupon-input.tsx +119 -119
  174. package/templates/nextjs/ui-canvas/cart/free-shipping-bar.tsx +54 -54
  175. package/templates/nextjs/ui-canvas/cart/reservation-countdown.tsx +81 -81
  176. package/templates/nextjs/ui-canvas/home/discount-banner-strip.tsx +21 -19
  177. package/templates/nextjs/ui-canvas/home/featured-products-section.tsx +23 -23
  178. package/templates/nextjs/ui-canvas/home/hero-section.tsx +21 -21
  179. package/templates/nextjs/ui-canvas/home/home-client.tsx +26 -26
  180. package/templates/nextjs/ui-canvas/layout/announcement-bar.tsx.ejs +101 -101
  181. package/templates/nextjs/ui-canvas/layout/faq-section.tsx.ejs +71 -71
  182. package/templates/nextjs/ui-canvas/layout/header-account.tsx +31 -31
  183. package/templates/nextjs/ui-canvas/layout/language-switcher.tsx.ejs +76 -76
  184. package/templates/nextjs/ui-canvas/layout/rich-text-block.tsx.ejs +36 -36
  185. package/templates/nextjs/ui-canvas/layout/site-footer.tsx.ejs +82 -82
  186. package/templates/nextjs/ui-canvas/layout/site-header.tsx.ejs +119 -119
  187. package/templates/nextjs/ui-canvas/product/customization-fields.tsx +445 -445
  188. package/templates/nextjs/ui-canvas/product/discount-badge.tsx +19 -19
  189. package/templates/nextjs/ui-canvas/product/frequently-bought-together.tsx +168 -168
  190. package/templates/nextjs/ui-canvas/product/modifier-group-selector.tsx +165 -165
  191. package/templates/nextjs/ui-canvas/product/price-display.tsx +52 -52
  192. package/templates/nextjs/ui-canvas/product/product-card.tsx +7 -2
  193. package/templates/nextjs/ui-canvas/product/product-client-section.tsx +352 -352
  194. package/templates/nextjs/ui-canvas/product/product-grid.tsx +27 -27
  195. package/templates/nextjs/ui-canvas/product/product-listing.tsx +224 -224
  196. package/templates/nextjs/ui-canvas/product/recommendation-section.tsx +99 -99
  197. package/templates/nextjs/ui-canvas/product/review-form.tsx +266 -266
  198. package/templates/nextjs/ui-canvas/product/reviews-section.tsx.ejs +96 -96
  199. package/templates/nextjs/ui-canvas/product/stock-badge.tsx +45 -45
  200. package/templates/nextjs/ui-canvas/product/variant-selector.tsx +212 -212
  201. package/templates/nextjs/ui-canvas/shared/cdn-image.tsx +31 -31
  202. package/templates/nextjs/ui-canvas/shared/loading-spinner.tsx +27 -27
  203. 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
+ }