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
@@ -1,39 +0,0 @@
1
- 'use client';
2
-
3
- import type { CartNudge } from 'brainerce';
4
- import { cn } from '@/core/lib/utils';
5
-
6
- interface CartNudgesProps {
7
- nudges: CartNudge[];
8
- className?: string;
9
- }
10
-
11
- export function CartNudges({ nudges, className }: CartNudgesProps) {
12
- if (nudges.length === 0) return null;
13
-
14
- return (
15
- <div className={cn('space-y-2', className)}>
16
- {nudges.map((nudge) => (
17
- <div
18
- key={nudge.ruleId}
19
- className="bg-primary/5 border-primary/20 flex items-start gap-3 rounded-none border px-4 py-3"
20
- >
21
- <svg
22
- className="text-primary mt-0.5 h-5 w-5 flex-shrink-0"
23
- fill="none"
24
- viewBox="0 0 24 24"
25
- stroke="currentColor"
26
- >
27
- <path
28
- strokeLinecap="round"
29
- strokeLinejoin="round"
30
- strokeWidth={2}
31
- d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
32
- />
33
- </svg>
34
- <p className="text-foreground flex-1 text-sm">{nudge.text}</p>
35
- </div>
36
- ))}
37
- </div>
38
- );
39
- }
@@ -1,149 +0,0 @@
1
- 'use client';
2
-
3
- import { Link } from '@/core/lib/navigation';
4
- import { useCartPage } from '@/core/hooks/use-cart-page';
5
- import { CartItem } from '@/ui/cart/cart-item';
6
- import { CartUpgradeBanner } from '@/ui/cart/cart-upgrade-banner';
7
- import { CartBundleOfferCard } from '@/ui/cart/cart-bundle-offer';
8
- import { CartSummary } from '@/ui/cart/cart-summary';
9
- import { CouponInput } from '@/ui/cart/coupon-input';
10
- import { CartNudges } from '@/ui/cart/cart-nudges';
11
- import { FreeShippingBar } from '@/ui/cart/free-shipping-bar';
12
- import { ReservationCountdown } from '@/ui/cart/reservation-countdown';
13
- import { CartRecommendationSection } from '@/ui/product/recommendation-section';
14
- import { LoadingSpinner } from '@/ui/shared/loading-spinner';
15
- import { useTranslations } from '@/core/lib/translations';
16
-
17
- export function CartView() {
18
- const t = useTranslations('cart');
19
- const tc = useTranslations('common');
20
- const { cart, cartLoading, refreshCart, itemCount, cartRecs, upgrades, bundles } = useCartPage();
21
-
22
- if (cartLoading) {
23
- return (
24
- <div className="flex min-h-[60vh] items-center justify-center">
25
- <LoadingSpinner size="lg" />
26
- </div>
27
- );
28
- }
29
-
30
- // Empty cart state
31
- if (!cart || cart.items.length === 0) {
32
- return (
33
- <div className="mx-auto max-w-7xl px-4 py-16 text-center sm:px-6 lg:px-8">
34
- <svg
35
- className="text-muted-foreground mx-auto mb-4 h-16 w-16"
36
- fill="none"
37
- viewBox="0 0 24 24"
38
- stroke="currentColor"
39
- >
40
- <path
41
- strokeLinecap="round"
42
- strokeLinejoin="round"
43
- strokeWidth={1.5}
44
- d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z"
45
- />
46
- </svg>
47
- <h1 className="text-foreground text-2xl font-bold">{t('emptyTitle')}</h1>
48
- <p className="text-muted-foreground mt-2">{t('emptySubtitle')}</p>
49
- <Link
50
- href="/products"
51
- className="bg-primary text-primary-foreground mt-6 inline-flex items-center rounded px-6 py-3 font-medium transition-opacity hover:opacity-90"
52
- >
53
- {tc('continueShopping')}
54
- </Link>
55
- </div>
56
- );
57
- }
58
-
59
- return (
60
- <div className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
61
- <h1 className="text-foreground mb-6 text-2xl font-bold">
62
- {t('title')} ({itemCount} {itemCount === 1 ? tc('item') : tc('items')})
63
- </h1>
64
-
65
- {/* Reservation countdown */}
66
- {cart.reservation?.hasReservation && (
67
- <ReservationCountdown reservation={cart.reservation} className="mb-6" />
68
- )}
69
-
70
- <div className="grid grid-cols-1 gap-8 lg:grid-cols-3">
71
- {/* Cart Items */}
72
- <div className="lg:col-span-2">
73
- {/* Nudges */}
74
- {cart.nudges && cart.nudges.length > 0 && (
75
- <CartNudges nudges={cart.nudges} className="mb-4" />
76
- )}
77
-
78
- {/* Cart items */}
79
- <div>
80
- {cart.items.map((item) => (
81
- <div key={item.id}>
82
- <CartItem item={item} onUpdate={refreshCart} />
83
- {upgrades?.upgrades?.[item.productId] && (
84
- <CartUpgradeBanner
85
- suggestion={upgrades.upgrades[item.productId]}
86
- cartItem={item}
87
- onUpgrade={refreshCart}
88
- className="mb-2 ms-24"
89
- />
90
- )}
91
- </div>
92
- ))}
93
- </div>
94
-
95
- {/* Bundle offers */}
96
- {bundles?.bundles && bundles.bundles.length > 0 && (
97
- <div className="mt-6 space-y-3">
98
- <h3 className="text-foreground text-sm font-semibold">{t('bundleOffers')}</h3>
99
- {bundles.bundles.map((offer) => (
100
- <CartBundleOfferCard
101
- key={offer.id}
102
- offer={offer}
103
- cartId={cart.id}
104
- onAdd={refreshCart}
105
- />
106
- ))}
107
- </div>
108
- )}
109
-
110
- {/* Coupon input */}
111
- <div className="border-border mt-6 border-t pt-4">
112
- <CouponInput cart={cart} onUpdate={refreshCart} />
113
- </div>
114
- </div>
115
-
116
- {/* Summary sidebar */}
117
- <div className="lg:col-span-1">
118
- <div className="bg-muted/50 border-border sticky top-24 rounded-none border p-6">
119
- <FreeShippingBar className="mb-4" />
120
- <CartSummary />
121
-
122
- <Link
123
- href="/checkout"
124
- className="bg-primary text-primary-foreground mt-6 inline-flex w-full items-center justify-center rounded px-6 py-3 text-sm font-medium transition-opacity hover:opacity-90"
125
- >
126
- {t('proceedToCheckout')}
127
- </Link>
128
-
129
- <Link
130
- href="/products"
131
- className="text-muted-foreground hover:text-foreground mt-3 inline-flex w-full items-center justify-center px-6 py-2 text-sm transition-colors"
132
- >
133
- {tc('continueShopping')}
134
- </Link>
135
- </div>
136
- </div>
137
- </div>
138
-
139
- {/* Cross-sell recommendations */}
140
- {cartRecs?.recommendations && cartRecs.recommendations.length > 0 && (
141
- <CartRecommendationSection
142
- title={t('youMightAlsoNeed')}
143
- items={cartRecs.recommendations}
144
- className="mt-10"
145
- />
146
- )}
147
- </div>
148
- );
149
- }
@@ -1,401 +0,0 @@
1
- 'use client';
2
-
3
- import { useEffect, useMemo, useState } from 'react';
4
- import { Link } from '@/core/lib/navigation';
5
- import { CdnImage as Image } from '@/ui/shared/cdn-image';
6
- import { useStoreInfo } from '@/core/providers/store-provider';
7
- import { useHomeData } from '@/core/hooks/use-home-data';
8
- import { ProductGrid } from '@/ui/product/product-grid';
9
- import { ProductCard } from '@/ui/product/product-card';
10
- import { LoadingSpinner } from '@/ui/shared/loading-spinner';
11
- import { useTranslations } from '@/core/lib/translations';
12
- import { TrustIcons } from '@/ui/layout/trust-icons';
13
-
14
- /** Adds `revealed` to every [data-reveal] element as it enters the viewport. */
15
- function useScrollReveal(ready: boolean) {
16
- useEffect(() => {
17
- if (!ready || typeof IntersectionObserver === 'undefined') return;
18
- const els = document.querySelectorAll('[data-reveal]');
19
- const io = new IntersectionObserver(
20
- (entries) => {
21
- for (const entry of entries) {
22
- if (entry.isIntersecting) {
23
- entry.target.classList.add('revealed');
24
- io.unobserve(entry.target);
25
- }
26
- }
27
- },
28
- { threshold: 0.12 }
29
- );
30
- els.forEach((el) => io.observe(el));
31
- return () => io.disconnect();
32
- }, [ready]);
33
- }
34
-
35
- /** Catalog-numbered section header — the exhibition idiom. */
36
- function SectionHeader({
37
- kicker,
38
- title,
39
- end,
40
- }: {
41
- kicker: string;
42
- title: string;
43
- end?: React.ReactNode;
44
- }) {
45
- return (
46
- <>
47
- <div className="flex items-baseline justify-between">
48
- <div>
49
- <p className="placard tabular">{kicker}</p>
50
- <h2 className="text-foreground mt-1 font-serif text-2xl font-normal">{title}</h2>
51
- </div>
52
- {end}
53
- </div>
54
- <div className="border-border mt-6 border-b" />
55
- </>
56
- );
57
- }
58
-
59
- export function HomeClient() {
60
- const { storeInfo, loading: storeLoading } = useStoreInfo();
61
- const { products, banners, loading } = useHomeData();
62
- const [joined, setJoined] = useState(false);
63
- const t = useTranslations('home');
64
- const tc = useTranslations('common');
65
-
66
- const storeName = storeInfo?.name || process.env.NEXT_PUBLIC_STORE_NAME || '';
67
-
68
- const ready = !storeLoading && !loading;
69
- useScrollReveal(ready);
70
-
71
- // One tile per category, dressed with the first product photo we have for
72
- // it. When the channel exposes no category data, the wall simply skips
73
- // this section — never invent catalog content.
74
- const categoryTiles = useMemo(() => {
75
- const map = new Map<string, { name: string; image: string | null; href: string }>();
76
- for (const p of products) {
77
- for (const c of p.categories ?? []) {
78
- if (!map.has(c.id)) {
79
- map.set(c.id, {
80
- name: c.name,
81
- image: p.images?.[0]?.url ?? null,
82
- href: `/products?category=${c.id}`,
83
- });
84
- }
85
- }
86
- }
87
- return [...map.values()].filter((c) => c.image).slice(0, 4);
88
- }, [products]);
89
-
90
- // "On the wall now" — sale pieces first, newest fill the rest.
91
- const wallStrip = useMemo(() => {
92
- const sale = products.filter((p) => p.salePrice);
93
- const rest = products.filter((p) => !p.salePrice);
94
- return [...sale, ...rest].slice(0, 6);
95
- }, [products]);
96
-
97
- const heroA = products[0]?.images?.[0]?.url ?? null;
98
- const heroB = products[2]?.images?.[0]?.url ?? null;
99
- const storyImage =
100
- products.find((p) => (p.images?.length ?? 0) > 2)?.images?.[1]?.url ??
101
- products[1]?.images?.[0]?.url ??
102
- null;
103
-
104
- if (!ready) {
105
- return (
106
- <div className="flex min-h-[60vh] items-center justify-center">
107
- <LoadingSpinner size="lg" />
108
- </div>
109
- );
110
- }
111
-
112
- const marqueeItems = [
113
- t('marqueeHandmade'),
114
- t('trustShipping'),
115
- t('trustWarranty'),
116
- t('trustGift'),
117
- t('marqueeOneOff'),
118
- ];
119
-
120
- return (
121
- <div>
122
- {/* Discount banners — a quiet vernissage line, not a promo slab */}
123
- {banners.length > 0 && (
124
- <div className="bg-background border-border border-b">
125
- <div className="mx-auto flex max-w-7xl flex-wrap items-center justify-center gap-x-3 gap-y-1 px-4 py-2.5 sm:px-6 lg:px-8">
126
- {banners.map((banner, idx) => (
127
- <span
128
- key={banner.ruleId}
129
- className="text-muted-foreground text-[0.6875rem] tracking-[0.08em]"
130
- >
131
- {idx > 0 ? (
132
- <span aria-hidden="true" className="text-accent me-3">
133
- ·
134
- </span>
135
- ) : null}
136
- {banner.text}
137
- </span>
138
- ))}
139
- </div>
140
- </div>
141
- )}
142
-
143
- {/* Hero — editorial split: statement wall + hung collage */}
144
- <section className="relative overflow-hidden">
145
- <div aria-hidden="true">
146
- <div className="wash wash-sand -top-40 start-[-12%] h-[36rem] w-[36rem]" />
147
- <div className="wash wash-blush bottom-[-8rem] end-[-10%] h-[30rem] w-[30rem]" />
148
- {/* The ghost word — the store's own name, printed into the wall */}
149
- {storeName && (
150
- <span className="text-foreground/[0.045] pointer-events-none absolute -bottom-16 start-2 select-none font-serif text-[clamp(7rem,20vw,17rem)] font-light leading-none">
151
- {storeName}
152
- </span>
153
- )}
154
- </div>
155
- <div className="relative mx-auto grid max-w-7xl grid-cols-12 items-center gap-y-12 px-4 py-16 sm:px-6 md:min-h-[78vh] md:py-20 lg:px-8">
156
- <div className="col-span-12 text-start md:col-span-5">
157
- <p className="placard">{t('heroOverline')}</p>
158
- <h1 className="text-foreground mt-5 font-serif text-[clamp(2.5rem,6.5vw,4.25rem)] font-light leading-[1.12]">
159
- {storeName}
160
- </h1>
161
- <p className="text-foreground/70 mt-5 max-w-md font-serif text-lg font-light leading-relaxed">
162
- {t('heroSubtitle')}
163
- </p>
164
- <div className="brass-rule mt-8 w-24" />
165
- <div className="mt-10 flex flex-wrap items-center gap-4">
166
- <Link
167
- href="/products"
168
- className="bg-primary text-primary-foreground inline-flex items-center px-8 py-3.5 text-[0.8125rem] font-semibold"
169
- >
170
- {tc('shopNow')}
171
- </Link>
172
- <Link href="/contact" className="text-foreground text-xs">
173
- {t('ctaStory')} ←
174
- </Link>
175
- </div>
176
- </div>
177
-
178
- {/* The hung collage — two framed works, offset like a gallery wall */}
179
- <div className="relative col-span-12 h-[46vh] md:col-span-7 md:h-[62vh]">
180
- {heroA && (
181
- <div className="bg-surface border-border hung absolute top-0 h-[78%] w-[68%] overflow-hidden border md:end-10">
182
- <Image
183
- src={heroA}
184
- alt={products[0]?.name ?? ''}
185
- fill
186
- sizes="(max-width: 768px) 70vw, 40vw"
187
- className="object-cover"
188
- priority
189
- />
190
- </div>
191
- )}
192
- {heroB && (
193
- <div className="bg-surface border-border hung absolute bottom-0 start-0 h-[52%] w-[44%] overflow-hidden border md:start-6">
194
- <Image
195
- src={heroB}
196
- alt={products[2]?.name ?? ''}
197
- fill
198
- sizes="(max-width: 768px) 45vw, 25vw"
199
- className="object-cover"
200
- />
201
- </div>
202
- )}
203
- <div className="brass-rule absolute -bottom-6 end-16 hidden w-24 md:block" />
204
- </div>
205
- </div>
206
- </section>
207
-
208
- {/* Marquee — the engraved drifting band */}
209
- <div className="marquee border-border border-y py-3" aria-hidden="true">
210
- <div className="marquee-track">
211
- {[0, 1].map((half) => (
212
- <span key={half} className="inline-flex">
213
- {marqueeItems.map((item, i) => (
214
- <span
215
- key={`${half}-${i}`}
216
- className="text-muted-foreground mx-6 inline-flex items-center gap-6 text-[0.6875rem] tracking-[0.14em]"
217
- >
218
- {item}
219
- <span className="text-accent">◆</span>
220
- </span>
221
- ))}
222
- </span>
223
- ))}
224
- </div>
225
- </div>
226
-
227
- {/* No. 01 — Collections */}
228
- {categoryTiles.length > 0 && (
229
- <section className="mx-auto max-w-7xl px-4 py-20 sm:px-6 lg:px-8" data-reveal>
230
- <SectionHeader kicker={t('collectionsKicker')} title={t('collections')} />
231
- <div className="mt-10 grid grid-cols-2 gap-x-4 gap-y-10 lg:grid-cols-4 lg:gap-x-6">
232
- {categoryTiles.map((cat) => (
233
- <Link key={cat.name} href={cat.href} className="group block">
234
- <div className="bg-surface border-border hung relative aspect-[4/5] overflow-hidden border">
235
- <Image
236
- src={cat.image as string}
237
- alt={cat.name}
238
- fill
239
- sizes="(max-width: 1024px) 50vw, 25vw"
240
- className="object-cover transition-transform duration-700 group-hover:scale-[1.04]"
241
- />
242
- <div className="bg-surface/95 border-border absolute inset-x-0 bottom-0 border-t py-2.5 text-center">
243
- <span className="text-foreground font-serif text-sm">{cat.name}</span>
244
- </div>
245
- </div>
246
- </Link>
247
- ))}
248
- </div>
249
- </section>
250
- )}
251
-
252
- <div className="hairline-fade" />
253
-
254
- {/* No. 02 — Selected pieces */}
255
- <section className="mx-auto max-w-7xl px-4 py-20 sm:px-6 lg:px-8" data-reveal>
256
- <SectionHeader
257
- kicker={t('featuredKicker')}
258
- title={t('featuredProducts')}
259
- end={
260
- <Link href="/products" className="text-foreground text-xs">
261
- {t('viewExhibition')} ←
262
- </Link>
263
- }
264
- />
265
- <div className="mt-10">
266
- <ProductGrid products={products.slice(0, 8)} />
267
- </div>
268
- </section>
269
-
270
- {/* The story — editorial band on the mat */}
271
- <section className="bg-surface border-border border-y" data-reveal>
272
- <div className="mx-auto grid max-w-7xl grid-cols-12 items-center gap-y-10 px-4 py-16 sm:px-6 md:py-24 lg:px-8">
273
- <div className="col-span-12 md:col-span-5">
274
- <p className="placard">{t('storyOverline')}</p>
275
- <h2 className="text-foreground mt-4 font-serif text-[clamp(1.75rem,4vw,2.75rem)] font-light leading-[1.2]">
276
- {t('storyTitle')}
277
- </h2>
278
- <p className="text-foreground/75 mt-6 max-w-md font-serif text-[1.0625rem] font-light leading-[1.9]">
279
- {t('storyBody')}
280
- </p>
281
- <div className="brass-rule mt-8 w-16" />
282
- <Link href="/products" className="text-foreground mt-8 inline-block text-xs">
283
- {t('storyCta')} ←
284
- </Link>
285
- </div>
286
- <div className="col-span-12 md:col-span-6 md:col-start-7">
287
- {storyImage && (
288
- <div className="bg-background border-border hung relative aspect-[4/3] overflow-hidden border">
289
- <Image
290
- src={storyImage}
291
- alt={t('storyOverline')}
292
- fill
293
- sizes="(max-width: 768px) 100vw, 50vw"
294
- className="object-cover"
295
- />
296
- </div>
297
- )}
298
- </div>
299
- </div>
300
- </section>
301
-
302
- {/* No. 03 — On the wall now: horizontal snap strip */}
303
- {wallStrip.length > 0 && (
304
- <section className="mx-auto max-w-7xl px-4 py-20 sm:px-6 lg:px-8" data-reveal>
305
- <SectionHeader kicker={t('onWallKicker')} title={t('onWall')} />
306
- <div className="-mx-4 mt-10 flex snap-x snap-mandatory gap-5 overflow-x-auto px-4 pb-4 sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8">
307
- {wallStrip.map((p) => (
308
- <div key={p.id} className="w-[70vw] flex-shrink-0 snap-start sm:w-[40vw] lg:w-[23%]">
309
- <ProductCard product={p} />
310
- </div>
311
- ))}
312
- </div>
313
- </section>
314
- )}
315
-
316
- <div className="hairline-fade" />
317
-
318
- {/* No. 04 — Kind words: quotes as placards */}
319
- <section className="mx-auto max-w-7xl px-4 py-20 sm:px-6 lg:px-8" data-reveal>
320
- <SectionHeader kicker={t('voicesKicker')} title={t('voices')} />
321
- <div className="mt-10 grid grid-cols-1 gap-6 md:grid-cols-3">
322
- {[1, 2, 3].map((n) => (
323
- <figure key={n} className="bg-surface border-border hung border p-8">
324
- <div className="text-accent text-lg" aria-hidden="true">
325
- ★★★★★
326
- </div>
327
- <blockquote className="text-foreground/85 mt-4 font-serif text-[1.0625rem] font-light leading-[1.8]">
328
- {t(`quote${n}` as 'quote1')}
329
- </blockquote>
330
- <figcaption className="placard mt-6">{t(`name${n}` as 'name1')}</figcaption>
331
- </figure>
332
- ))}
333
- </div>
334
- </section>
335
-
336
- {/* Visit band — the closing wall */}
337
- <section className="border-border relative overflow-hidden border-t" data-reveal>
338
- <div aria-hidden="true">
339
- <div className="wash wash-sand -top-24 end-[15%] h-[26rem] w-[26rem]" />
340
- </div>
341
- <div className="relative mx-auto max-w-7xl px-4 py-20 text-center sm:px-6 md:py-28 lg:px-8">
342
- <p className="placard">{t('heroOverline')}</p>
343
- <h2 className="text-foreground mt-4 font-serif text-[clamp(1.75rem,4.5vw,3rem)] font-light">
344
- {t('visitTitle')}
345
- </h2>
346
- <p className="text-muted-foreground tabular mt-4 text-[0.8125rem] tracking-[0.08em]">
347
- {t('visitSub')}
348
- </p>
349
- <div className="brass-rule mx-auto mt-8 w-24" />
350
- <div className="mt-10 flex flex-wrap items-center justify-center gap-4">
351
- <Link
352
- href="/contact"
353
- className="bg-primary text-primary-foreground inline-flex items-center px-8 py-3.5 text-[0.8125rem] font-semibold"
354
- >
355
- {t('visitCta')}
356
- </Link>
357
- <Link href="/products" className="text-foreground text-xs">
358
- {t('viewExhibition')} ←
359
- </Link>
360
- </div>
361
-
362
- {/* Newsletter — the guest book */}
363
- <div className="mx-auto mt-14 max-w-md">
364
- {joined ? (
365
- <p className="border-border bg-surface text-foreground border py-4 text-center text-sm tracking-[0.04em]">
366
- {t('newsletterSuccess')}
367
- </p>
368
- ) : (
369
- <form
370
- onSubmit={(e) => {
371
- e.preventDefault();
372
- setJoined(true);
373
- }}
374
- className="flex items-stretch"
375
- >
376
- <input
377
- type="email"
378
- required
379
- placeholder={t('newsletterPlaceholder')}
380
- className="border-border bg-surface min-w-0 flex-1 border px-4 py-3 text-sm focus:outline-none"
381
- />
382
- <button
383
- type="submit"
384
- className="bg-primary text-primary-foreground border-primary border px-6 text-[0.8125rem] font-semibold"
385
- >
386
- {t('newsletterCta')}
387
- </button>
388
- </form>
389
- )}
390
- {!joined && <p className="placard mt-3 text-center">{t('newsletterHook')}</p>}
391
- </div>
392
-
393
- {/* Trust icons — the promise row */}
394
- <div className="mt-16">
395
- <TrustIcons />
396
- </div>
397
- </div>
398
- </section>
399
- </div>
400
- );
401
- }