create-brainerce-store 1.51.0 → 1.52.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 (170) hide show
  1. package/README.md +72 -0
  2. package/dist/index.js +9 -68
  3. package/messages/en.json +32 -0
  4. package/messages/he.json +32 -0
  5. package/package.json +1 -1
  6. package/templates/nextjs/base/.claude/commands/design.md +9 -0
  7. package/templates/nextjs/base/AI-GUIDE.md +52 -3
  8. package/templates/nextjs/base/CLAUDE.md +10 -0
  9. package/templates/nextjs/base/package.json.ejs +52 -39
  10. package/templates/nextjs/base/src/app/globals.css +36 -30
  11. package/templates/nextjs/base/src/components/checkout/payment-step.tsx +14 -10
  12. package/templates/nextjs/base/src/components/ui/accordion.tsx +53 -0
  13. package/templates/nextjs/base/src/components/ui/badge.tsx +32 -0
  14. package/templates/nextjs/base/src/components/ui/button.tsx +49 -0
  15. package/templates/nextjs/base/src/components/ui/card.tsx +55 -0
  16. package/templates/nextjs/base/src/components/ui/checkbox.tsx +28 -0
  17. package/templates/nextjs/base/src/components/ui/dialog.tsx +107 -0
  18. package/templates/nextjs/base/src/components/ui/input.tsx +22 -0
  19. package/templates/nextjs/base/src/components/ui/label.tsx +21 -0
  20. package/templates/nextjs/base/src/components/ui/radio-group.tsx +38 -0
  21. package/templates/nextjs/base/src/components/ui/select.tsx +153 -0
  22. package/templates/nextjs/base/src/components/ui/separator.tsx +26 -0
  23. package/templates/nextjs/base/src/components/ui/sheet.tsx +125 -0
  24. package/templates/nextjs/base/src/components/ui/skeleton.tsx +7 -0
  25. package/templates/nextjs/base/src/components/ui/tabs.tsx +55 -0
  26. package/templates/nextjs/base/src/components/ui/textarea.tsx +21 -0
  27. package/templates/nextjs/base/src/components/ui/tooltip.tsx +32 -0
  28. package/templates/nextjs/base/src/ui/cart/cart-bundle-offer.tsx +17 -11
  29. package/templates/nextjs/base/src/ui/cart/cart-item.tsx +2 -8
  30. package/templates/nextjs/base/src/ui/cart/cart-nudges.tsx +28 -39
  31. package/templates/nextjs/base/src/ui/cart/cart-summary.tsx +3 -1
  32. package/templates/nextjs/base/src/ui/cart/cart-upgrade-banner.tsx +8 -23
  33. package/templates/nextjs/base/src/ui/cart/cart-view.tsx +18 -27
  34. package/templates/nextjs/base/src/ui/cart/coupon-input.tsx +10 -21
  35. package/templates/nextjs/base/src/ui/cart/free-shipping-bar.tsx +2 -3
  36. package/templates/nextjs/base/src/ui/cart/reservation-countdown.tsx +95 -103
  37. package/templates/nextjs/base/src/ui/home/hero-section.tsx +4 -6
  38. package/templates/nextjs/base/src/ui/layout/announcement-bar.tsx.ejs +116 -125
  39. package/templates/nextjs/base/src/ui/layout/faq-section.tsx.ejs +90 -103
  40. package/templates/nextjs/base/src/ui/layout/header-account.tsx +2 -17
  41. package/templates/nextjs/base/src/ui/layout/language-switcher.tsx.ejs +80 -70
  42. package/templates/nextjs/base/src/ui/layout/site-footer.tsx.ejs +144 -164
  43. package/templates/nextjs/base/src/ui/layout/site-header.tsx.ejs +152 -182
  44. package/templates/nextjs/base/src/ui/product/customization-fields.tsx +456 -444
  45. package/templates/nextjs/base/src/ui/product/discount-badge.tsx +18 -22
  46. package/templates/nextjs/base/src/ui/product/frequently-bought-together.tsx +15 -27
  47. package/templates/nextjs/base/src/ui/product/modifier-group-selector.tsx +206 -175
  48. package/templates/nextjs/base/src/ui/product/price-display.tsx +3 -2
  49. package/templates/nextjs/base/src/ui/product/product-card.tsx +238 -271
  50. package/templates/nextjs/base/src/ui/product/product-client-section.tsx +455 -470
  51. package/templates/nextjs/base/src/ui/product/product-listing.tsx +96 -79
  52. package/templates/nextjs/base/src/ui/product/recommendation-section.tsx +102 -107
  53. package/templates/nextjs/base/src/ui/product/review-form.tsx +44 -43
  54. package/templates/nextjs/{designs/passepartout → base/src}/ui/product/reviews-section.tsx.ejs +108 -96
  55. package/templates/nextjs/base/src/ui/product/stock-badge.tsx +61 -63
  56. package/templates/nextjs/base/tailwind.config.ts +69 -42
  57. package/templates/nextjs/designs/{passepartout → atelier}/app-overlay/layout.tsx.ejs +23 -25
  58. package/templates/nextjs/designs/atelier/globals.css +396 -0
  59. package/templates/nextjs/designs/atelier/messages-patch/en.json +99 -0
  60. package/templates/nextjs/designs/atelier/messages-patch/he.json +99 -0
  61. package/templates/nextjs/designs/{passepartout → atelier}/ui/cart/cart-bundle-offer.tsx +110 -111
  62. package/templates/nextjs/designs/atelier/ui/cart/cart-drawer.tsx +163 -0
  63. package/templates/nextjs/designs/atelier/ui/cart/cart-item.tsx +140 -0
  64. package/templates/nextjs/designs/atelier/ui/cart/cart-nudges.tsx +34 -0
  65. package/templates/nextjs/designs/atelier/ui/cart/cart-summary.tsx +107 -0
  66. package/templates/nextjs/designs/{passepartout → atelier}/ui/cart/cart-upgrade-banner.tsx +123 -141
  67. package/templates/nextjs/designs/atelier/ui/cart/cart-view.tsx +147 -0
  68. package/templates/nextjs/designs/atelier/ui/cart/coupon-input.tsx +142 -0
  69. package/templates/nextjs/designs/{passepartout → atelier}/ui/cart/free-shipping-bar.tsx +77 -62
  70. package/templates/nextjs/designs/{passepartout → atelier}/ui/cart/reservation-countdown.tsx +89 -103
  71. package/templates/nextjs/designs/atelier/ui/home/benefits-band.tsx +34 -0
  72. package/templates/nextjs/designs/atelier/ui/home/category-tiles.tsx +148 -0
  73. package/templates/nextjs/designs/atelier/ui/home/discount-banner-strip.tsx +27 -0
  74. package/templates/nextjs/designs/atelier/ui/home/editorial-band.tsx +73 -0
  75. package/templates/nextjs/designs/atelier/ui/home/featured-products-section.tsx +36 -0
  76. package/templates/nextjs/designs/atelier/ui/home/hero-section.tsx +148 -0
  77. package/templates/nextjs/designs/atelier/ui/home/home-client.tsx +54 -0
  78. package/templates/nextjs/designs/atelier/ui/home/newsletter-section.tsx +78 -0
  79. package/templates/nextjs/designs/atelier/ui/home/testimonials-band.tsx +46 -0
  80. package/templates/nextjs/designs/atelier/ui/layout/announcement-bar.tsx +118 -0
  81. package/templates/nextjs/designs/atelier/ui/layout/faq-section.tsx +96 -0
  82. package/templates/nextjs/designs/atelier/ui/layout/header-account.tsx +36 -0
  83. package/templates/nextjs/designs/atelier/ui/layout/header-cart.tsx +36 -0
  84. package/templates/nextjs/designs/atelier/ui/layout/language-switcher.tsx.ejs +137 -0
  85. package/templates/nextjs/designs/atelier/ui/layout/nav-url.ts +14 -0
  86. package/templates/nextjs/designs/atelier/ui/layout/rich-text-block.tsx +34 -0
  87. package/templates/nextjs/designs/atelier/ui/layout/site-footer.tsx.ejs +141 -0
  88. package/templates/nextjs/designs/atelier/ui/layout/site-header.tsx.ejs +138 -0
  89. package/templates/nextjs/designs/atelier/ui/product/customization-fields.tsx +466 -0
  90. package/templates/nextjs/designs/{passepartout → atelier}/ui/product/discount-badge.tsx +17 -22
  91. package/templates/nextjs/designs/{passepartout → atelier}/ui/product/frequently-bought-together.tsx +196 -205
  92. package/templates/nextjs/designs/atelier/ui/product/modifier-group-selector.tsx +193 -0
  93. package/templates/nextjs/designs/atelier/ui/product/price-display.tsx +69 -0
  94. package/templates/nextjs/designs/atelier/ui/product/product-card.tsx +260 -0
  95. package/templates/nextjs/designs/atelier/ui/product/product-client-section.tsx +471 -0
  96. package/templates/nextjs/designs/atelier/ui/product/product-grid.tsx +47 -0
  97. package/templates/nextjs/designs/atelier/ui/product/product-listing.tsx +260 -0
  98. package/templates/nextjs/designs/atelier/ui/product/recommendation-section.tsx +95 -0
  99. package/templates/nextjs/designs/{passepartout → atelier}/ui/product/review-form.tsx +267 -294
  100. package/templates/nextjs/designs/atelier/ui/product/reviews-section.tsx.ejs +148 -0
  101. package/templates/nextjs/designs/atelier/ui/product/stock-badge.tsx +66 -0
  102. package/templates/nextjs/designs/atelier/ui/product/variant-selector.tsx +206 -0
  103. package/templates/nextjs/designs/atelier/ui/shared/cdn-image.tsx +29 -0
  104. package/templates/nextjs/designs/{passepartout → atelier}/ui/shared/fly-to-cart.ts +86 -85
  105. package/templates/nextjs/designs/atelier/ui/shared/icons.tsx +269 -0
  106. package/templates/nextjs/designs/atelier/ui/shared/loading-spinner.tsx +64 -0
  107. package/templates/nextjs/designs/atelier/ui/shared/reveal.tsx +71 -0
  108. package/templates/nextjs/designs/atelier/ui/shared/select-menu.tsx +125 -0
  109. package/templates/nextjs/themes/luxury/globals.css +405 -399
  110. package/templates/nextjs/themes/minimal/globals.css +36 -30
  111. package/templates/nextjs/themes/playful/globals.css +406 -400
  112. package/templates/nextjs/ui-canvas/cart/cart-bundle-offer.tsx +2 -0
  113. package/templates/nextjs/ui-canvas/cart/cart-item.tsx +2 -0
  114. package/templates/nextjs/ui-canvas/cart/cart-nudges.tsx +2 -0
  115. package/templates/nextjs/ui-canvas/cart/cart-summary.tsx +2 -0
  116. package/templates/nextjs/ui-canvas/cart/cart-upgrade-banner.tsx +2 -0
  117. package/templates/nextjs/ui-canvas/cart/cart-view.tsx +2 -2
  118. package/templates/nextjs/ui-canvas/cart/coupon-input.tsx +2 -0
  119. package/templates/nextjs/ui-canvas/cart/free-shipping-bar.tsx +2 -0
  120. package/templates/nextjs/ui-canvas/cart/reservation-countdown.tsx +2 -0
  121. package/templates/nextjs/ui-canvas/home/discount-banner-strip.tsx +1 -1
  122. package/templates/nextjs/ui-canvas/home/featured-products-section.tsx +1 -1
  123. package/templates/nextjs/ui-canvas/home/hero-section.tsx +1 -1
  124. package/templates/nextjs/ui-canvas/home/home-client.tsx +1 -1
  125. package/templates/nextjs/ui-canvas/layout/announcement-bar.tsx.ejs +1 -1
  126. package/templates/nextjs/ui-canvas/layout/faq-section.tsx.ejs +2 -2
  127. package/templates/nextjs/ui-canvas/layout/header-account.tsx +1 -1
  128. package/templates/nextjs/ui-canvas/layout/language-switcher.tsx.ejs +2 -0
  129. package/templates/nextjs/ui-canvas/layout/rich-text-block.tsx.ejs +2 -0
  130. package/templates/nextjs/ui-canvas/layout/site-footer.tsx.ejs +2 -2
  131. package/templates/nextjs/ui-canvas/layout/site-header.tsx.ejs +2 -2
  132. package/templates/nextjs/ui-canvas/product/customization-fields.tsx +2 -0
  133. package/templates/nextjs/ui-canvas/product/discount-badge.tsx +2 -0
  134. package/templates/nextjs/ui-canvas/product/frequently-bought-together.tsx +2 -0
  135. package/templates/nextjs/ui-canvas/product/modifier-group-selector.tsx +2 -0
  136. package/templates/nextjs/ui-canvas/product/price-display.tsx +2 -0
  137. package/templates/nextjs/ui-canvas/product/product-card.tsx +1 -1
  138. package/templates/nextjs/ui-canvas/product/product-client-section.tsx +1 -1
  139. package/templates/nextjs/ui-canvas/product/product-grid.tsx +1 -1
  140. package/templates/nextjs/ui-canvas/product/product-listing.tsx +1 -1
  141. package/templates/nextjs/ui-canvas/product/recommendation-section.tsx +4 -0
  142. package/templates/nextjs/ui-canvas/product/review-form.tsx +29 -32
  143. package/templates/nextjs/ui-canvas/product/{reviews-section.tsx → reviews-section.tsx.ejs} +26 -8
  144. package/templates/nextjs/ui-canvas/product/stock-badge.tsx +2 -0
  145. package/templates/nextjs/ui-canvas/product/variant-selector.tsx +2 -0
  146. package/templates/nextjs/ui-canvas/shared/cdn-image.tsx +2 -0
  147. package/templates/nextjs/ui-canvas/shared/loading-spinner.tsx +2 -0
  148. package/templates/nextjs/base/src/ui/product/reviews-section.tsx +0 -85
  149. package/templates/nextjs/designs/passepartout/globals.css +0 -412
  150. package/templates/nextjs/designs/passepartout/messages-patch/en.json +0 -87
  151. package/templates/nextjs/designs/passepartout/messages-patch/he.json +0 -87
  152. package/templates/nextjs/designs/passepartout/ui/cart/cart-nudges.tsx +0 -39
  153. package/templates/nextjs/designs/passepartout/ui/cart/cart-view.tsx +0 -149
  154. package/templates/nextjs/designs/passepartout/ui/home/home-client.tsx +0 -401
  155. package/templates/nextjs/designs/passepartout/ui/layout/announcement-bar.tsx +0 -139
  156. package/templates/nextjs/designs/passepartout/ui/layout/cart-link.tsx +0 -45
  157. package/templates/nextjs/designs/passepartout/ui/layout/faq-section.tsx +0 -103
  158. package/templates/nextjs/designs/passepartout/ui/layout/site-footer.tsx +0 -159
  159. package/templates/nextjs/designs/passepartout/ui/layout/site-header.tsx.ejs +0 -155
  160. package/templates/nextjs/designs/passepartout/ui/layout/trust-icons.tsx +0 -60
  161. package/templates/nextjs/designs/passepartout/ui/layout/whatsapp-button.tsx +0 -34
  162. package/templates/nextjs/designs/passepartout/ui/product/price-display.tsx +0 -111
  163. package/templates/nextjs/designs/passepartout/ui/product/product-card.tsx +0 -265
  164. package/templates/nextjs/designs/passepartout/ui/product/product-client-section.tsx +0 -667
  165. package/templates/nextjs/designs/passepartout/ui/product/product-grid.tsx +0 -35
  166. package/templates/nextjs/designs/passepartout/ui/product/product-listing.tsx +0 -427
  167. package/templates/nextjs/designs/passepartout/ui/product/recommendation-section.tsx +0 -113
  168. package/templates/nextjs/designs/passepartout/ui/product/stock-badge.tsx +0 -52
  169. package/templates/nextjs/designs/passepartout/ui/product/variant-selector.tsx +0 -307
  170. package/templates/nextjs/designs/passepartout/ui/shared/loading-spinner.tsx +0 -33
@@ -0,0 +1,148 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * Category tiles derived from the live catalog. Uses product categories when
5
+ * present; otherwise derives collections from Hebrew product-name prefixes
6
+ * (טבעת / שרשרת / עגילי / צמיד) so the section is never empty on stores that
7
+ * haven't seeded category data. Each tile shows a real catalog photo.
8
+ */
9
+ import type { Product } from 'brainerce';
10
+ import { Link } from '@/core/lib/navigation';
11
+ import { CdnImage as Image } from '@/ui/shared/cdn-image';
12
+ import { useTranslations } from '@/core/lib/translations';
13
+ import { IconArrowEnd } from '@/ui/shared/icons';
14
+
15
+ const PREFIX_GROUPS = [
16
+ { prefixes: ['טבעת', 'טבעות', 'ring'], labelKey: 'catRings' },
17
+ { prefixes: ['שרשרת', 'שרשראות', 'necklace'], labelKey: 'catNecklaces' },
18
+ { prefixes: ['עגיל', 'earring'], labelKey: 'catEarrings' },
19
+ { prefixes: ['צמיד', 'bracelet'], labelKey: 'catBracelets' },
20
+ ] as const;
21
+
22
+ interface Tile {
23
+ key: string;
24
+ label: string;
25
+ imageUrl: string;
26
+ imageAlt: string;
27
+ href: string;
28
+ count: number;
29
+ }
30
+
31
+ export function CategoryTiles({ products }: { products: Product[] }) {
32
+ const t = useTranslations('home');
33
+ const tc = useTranslations('common');
34
+
35
+ const tiles: Tile[] = [];
36
+
37
+ // Prefer real category data when the catalog has it.
38
+ const byCategory = new Map<string, { name: string; items: Product[] }>();
39
+ for (const p of products) {
40
+ for (const cat of p.categories ?? []) {
41
+ const entry = byCategory.get(cat.id) ?? { name: cat.name, items: [] };
42
+ entry.items.push(p);
43
+ byCategory.set(cat.id, entry);
44
+ }
45
+ }
46
+
47
+ // Tile photo: prefer the LAST matching product's image — the featured grid
48
+ // above leads with the first ones, so tiles avoid repeating the same shots.
49
+ const pickImage = (items: Product[]) =>
50
+ [...items].reverse().find((p) => p.images?.[0]?.url)?.images?.[0];
51
+
52
+ if (byCategory.size >= 2) {
53
+ for (const [id, { name, items }] of Array.from(byCategory.entries()).slice(0, 4)) {
54
+ const img = pickImage(items);
55
+ if (!img?.url) continue;
56
+ tiles.push({
57
+ key: id,
58
+ label: name,
59
+ imageUrl: img.url,
60
+ imageAlt: img.alt || name,
61
+ href: `/products?category=${encodeURIComponent(id)}`,
62
+ count: items.length,
63
+ });
64
+ }
65
+ } else {
66
+ // Derive from name prefixes — never hardcodes products, only groups them.
67
+ for (const group of PREFIX_GROUPS) {
68
+ const items = products.filter((p) =>
69
+ group.prefixes.some((prefix) => p.name?.toLowerCase().startsWith(prefix))
70
+ );
71
+ const img = pickImage(items);
72
+ if (!img?.url) continue;
73
+ tiles.push({
74
+ key: group.labelKey,
75
+ label: t(group.labelKey),
76
+ imageUrl: img.url,
77
+ imageAlt: img.alt || t(group.labelKey),
78
+ href: `/products?search=${encodeURIComponent(group.prefixes[0])}`,
79
+ count: items.length,
80
+ });
81
+ }
82
+ }
83
+
84
+ if (tiles.length === 0) return null;
85
+
86
+ return (
87
+ <section id="categories" className="section-pad wash-sand relative overflow-hidden">
88
+ <div aria-hidden="true" className="glow-blob -start-24 top-1/3 h-72 w-72 opacity-70" />
89
+ <div className="container-page relative">
90
+ <div className="mb-8 max-w-xl lg:mb-10">
91
+ <span className="eyebrow">{t('categoriesEyebrow')}</span>
92
+ <h2 className="text-4xl sm:text-5xl">{t('categoriesTitle')}</h2>
93
+ <p className="mt-3 text-muted-foreground">{t('categoriesSubtitle')}</p>
94
+ </div>
95
+
96
+ <ul
97
+ className={`grid grid-cols-2 gap-3 sm:gap-5 lg:gap-6 ${
98
+ tiles.length === 3 ? 'lg:grid-cols-3' : 'lg:grid-cols-4'
99
+ }`}
100
+ >
101
+ {tiles.map((tile, idx) => (
102
+ <li
103
+ key={tile.key}
104
+ className={
105
+ // Odd tile count: let the last tile span the full row on
106
+ // mobile so the 2-col grid never shows an orphan hole.
107
+ tiles.length % 2 === 1 && idx === tiles.length - 1
108
+ ? 'col-span-2 lg:col-span-1'
109
+ : undefined
110
+ }
111
+ >
112
+ <Link
113
+ href={tile.href}
114
+ className="group card card-hover block overflow-hidden"
115
+ >
116
+ <span className="relative block aspect-square overflow-hidden bg-secondary">
117
+ <Image
118
+ src={tile.imageUrl}
119
+ alt={tile.imageAlt}
120
+ fill
121
+ sizes="(max-width: 640px) 50vw, 25vw"
122
+ className="img-zoom object-cover"
123
+ />
124
+ </span>
125
+ <span className="flex items-center justify-between gap-2 px-4 py-3.5">
126
+ <span>
127
+ <span className="block text-[15px] font-semibold text-foreground">
128
+ {tile.label}
129
+ </span>
130
+ <span className="block text-xs text-muted-foreground">
131
+ {tile.count} {tile.count === 1 ? tc('product') : tc('products')}
132
+ </span>
133
+ </span>
134
+ <span
135
+ aria-hidden="true"
136
+ className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-secondary text-foreground transition-colors group-hover:bg-primary group-hover:text-primary-foreground"
137
+ >
138
+ <IconArrowEnd size={16} className="rtl-flip" />
139
+ </span>
140
+ </span>
141
+ </Link>
142
+ </li>
143
+ ))}
144
+ </ul>
145
+ </div>
146
+ </section>
147
+ );
148
+ }
@@ -0,0 +1,27 @@
1
+ 'use client';
2
+
3
+ import type { DiscountBanner } from 'brainerce';
4
+ import { IconTag } from '@/ui/shared/icons';
5
+
6
+ /** Store-wide discount banner strip shown at the very top of the homepage. */
7
+ export function DiscountBannerStrip({ banners }: { banners: DiscountBanner[] }) {
8
+ if (banners.length === 0) return null;
9
+
10
+ return (
11
+ <div role="region" aria-label="Promotions" className="border-b bg-secondary">
12
+ <ul className="container-page flex flex-wrap items-center justify-center gap-x-8 gap-y-1 py-2.5">
13
+ {banners.map((banner) => (
14
+ <li
15
+ key={banner.ruleId}
16
+ className="inline-flex items-center gap-2 text-[13px] font-semibold text-foreground"
17
+ >
18
+ <span aria-hidden="true" className="text-primary">
19
+ <IconTag size={16} />
20
+ </span>
21
+ {banner.text}
22
+ </li>
23
+ ))}
24
+ </ul>
25
+ </div>
26
+ );
27
+ }
@@ -0,0 +1,73 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * Editorial "our studio" band — lifestyle photography (Pexels hotlinks,
5
+ * HEAD-verified) + brand-story copy from translation keys. Purely template
6
+ * marketing content; no catalog data is hardcoded here.
7
+ */
8
+ import { Link } from '@/core/lib/navigation';
9
+ import { useTranslations } from '@/core/lib/translations';
10
+ import { IconArrowEnd } from '@/ui/shared/icons';
11
+
12
+ // Verified free-stock imagery (images.pexels.com allows hotlinking).
13
+ const IMG_MODEL =
14
+ 'https://images.pexels.com/photos/1454171/pexels-photo-1454171.jpeg?auto=compress&cs=tinysrgb&w=1200';
15
+ const IMG_DETAIL =
16
+ 'https://images.pexels.com/photos/1191531/pexels-photo-1191531.jpeg?auto=compress&cs=tinysrgb&w=800';
17
+
18
+ export function EditorialBand() {
19
+ const t = useTranslations('home');
20
+
21
+ return (
22
+ <section className="section-pad">
23
+ <div className="container-page grid items-center gap-10 lg:grid-cols-2 lg:gap-16">
24
+ {/* Image pair */}
25
+ <div className="grid grid-cols-12 gap-4">
26
+ <figure className="col-span-8 -rotate-2 overflow-hidden rounded-3xl border-4 border-background shadow-[0_28px_60px_-24px_hsl(var(--primary)/0.35)]">
27
+ {/* eslint-disable-next-line @next/next/no-img-element -- editorial stock hotlink, host not in next remotePatterns */}
28
+ <img
29
+ src={IMG_MODEL}
30
+ alt={t('storyTitle')}
31
+ loading="lazy"
32
+ className="aspect-[4/5] h-full w-full object-cover"
33
+ />
34
+ </figure>
35
+ <figure className="col-span-4 -mb-8 self-end rotate-3 overflow-hidden rounded-3xl border-4 border-background shadow-[0_24px_50px_-20px_hsl(var(--primary)/0.35)]">
36
+ {/* eslint-disable-next-line @next/next/no-img-element -- editorial stock hotlink, host not in next remotePatterns */}
37
+ <img
38
+ src={IMG_DETAIL}
39
+ alt={t('storyStat1Value')}
40
+ loading="lazy"
41
+ className="aspect-[3/4] h-full w-full object-cover"
42
+ />
43
+ </figure>
44
+ </div>
45
+
46
+ {/* Copy */}
47
+ <div className="max-w-xl">
48
+ <span className="eyebrow">{t('storyEyebrow')}</span>
49
+ <h2 className="text-4xl sm:text-5xl">{t('storyTitle')}</h2>
50
+ <p className="mt-5 leading-8 text-muted-foreground">{t('storyP1')}</p>
51
+ <p className="mt-4 leading-8 text-muted-foreground">{t('storyP2')}</p>
52
+
53
+ {/* Craft highlights */}
54
+ <dl className="mt-8 grid grid-cols-2 gap-6 border-t pt-8">
55
+ <div>
56
+ <dt className="font-display text-2xl text-foreground">{t('storyStat1Value')}</dt>
57
+ <dd className="mt-1 text-sm text-muted-foreground">{t('storyStat1Label')}</dd>
58
+ </div>
59
+ <div>
60
+ <dt className="font-display text-2xl text-foreground">{t('storyStat2Value')}</dt>
61
+ <dd className="mt-1 text-sm text-muted-foreground">{t('storyStat2Label')}</dd>
62
+ </div>
63
+ </dl>
64
+
65
+ <Link href="/products" className="btn-outline btn-md mt-8">
66
+ {t('storyCta')}
67
+ <IconArrowEnd size={18} className="rtl-flip" />
68
+ </Link>
69
+ </div>
70
+ </div>
71
+ </section>
72
+ );
73
+ }
@@ -0,0 +1,36 @@
1
+ 'use client';
2
+
3
+ import type { Product } from 'brainerce';
4
+ import { Link } from '@/core/lib/navigation';
5
+ import { useTranslations } from '@/core/lib/translations';
6
+ import { ProductGrid } from '@/ui/product/product-grid';
7
+ import { IconArrowEnd } from '@/ui/shared/icons';
8
+
9
+ /** Homepage featured-products grid with a section header + view-all link. */
10
+ export function FeaturedProductsSection({ products }: { products: Product[] }) {
11
+ const t = useTranslations('home');
12
+ const tc = useTranslations('common');
13
+
14
+ return (
15
+ <section className="section-pad">
16
+ <div className="container-page">
17
+ <div className="mb-8 flex flex-wrap items-end justify-between gap-4 lg:mb-10">
18
+ <div className="max-w-xl">
19
+ <span className="eyebrow">{t('featuredEyebrow')}</span>
20
+ <h2 className="text-4xl sm:text-5xl">{t('featuredProducts')}</h2>
21
+ <p className="mt-3 text-muted-foreground">{t('featuredSubtitle')}</p>
22
+ </div>
23
+ <Link
24
+ href="/products"
25
+ className="group inline-flex items-center gap-1.5 text-sm font-semibold text-primary hover:underline"
26
+ >
27
+ {tc('viewAll')}
28
+ <IconArrowEnd size={16} className="rtl-flip" />
29
+ </Link>
30
+ </div>
31
+
32
+ <ProductGrid products={products.slice(0, 8)} />
33
+ </div>
34
+ </section>
35
+ );
36
+ }
@@ -0,0 +1,148 @@
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
+ </div>
134
+ </section>
135
+ );
136
+ }
137
+
138
+ function HeroPrice({ product }: { product: Product }) {
139
+ const { price, originalPrice, isOnSale } = getProductPriceInfo(product);
140
+ return (
141
+ <PriceDisplay
142
+ price={originalPrice}
143
+ salePrice={isOnSale ? price : undefined}
144
+ size="sm"
145
+ className="mt-0.5"
146
+ />
147
+ );
148
+ }
@@ -0,0 +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
+ }
@@ -0,0 +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
+ }
@@ -0,0 +1,46 @@
1
+ 'use client';
2
+
3
+ /** Testimonials band — three quote cards, copy from translation keys. */
4
+ import { useTranslations } from '@/core/lib/translations';
5
+ import { IconStar, IconQuote } from '@/ui/shared/icons';
6
+
7
+ const TESTIMONIALS = [1, 2, 3] as const;
8
+
9
+ export function TestimonialsBand() {
10
+ const t = useTranslations('home');
11
+
12
+ return (
13
+ <section className="section-pad wash-sand">
14
+ <div className="container-page">
15
+ <div className="mb-8 max-w-xl lg:mb-10">
16
+ <span className="eyebrow">{t('testimonialsEyebrow')}</span>
17
+ <h2 className="text-4xl sm:text-5xl">{t('testimonialsTitle')}</h2>
18
+ </div>
19
+
20
+ <ul className="grid gap-4 sm:gap-6 lg:grid-cols-3">
21
+ {TESTIMONIALS.map((n) => (
22
+ <li key={n}>
23
+ <figure className={`card card-hover flex h-full flex-col gap-4 p-6 ${n === 2 ? 'lg:rotate-1' : 'lg:-rotate-1'} lg:hover:rotate-0`}>
24
+ <span className="text-primary/30" aria-hidden="true">
25
+ <IconQuote size={28} />
26
+ </span>
27
+ <span className="inline-flex text-primary" aria-label="5/5">
28
+ {[1, 2, 3, 4, 5].map((s) => (
29
+ <IconStar key={s} size={16} filled />
30
+ ))}
31
+ </span>
32
+ <blockquote className="flex-1 leading-7 text-foreground">
33
+ “{t(`testimonial${n}Quote`)}”
34
+ </blockquote>
35
+ <figcaption className="border-t pt-4 text-sm">
36
+ <span className="font-semibold text-foreground">{t(`testimonial${n}Name`)}</span>
37
+ <span className="text-muted-foreground"> · {t(`testimonial${n}Meta`)}</span>
38
+ </figcaption>
39
+ </figure>
40
+ </li>
41
+ ))}
42
+ </ul>
43
+ </div>
44
+ </section>
45
+ );
46
+ }