create-brainerce-store 1.72.0 → 1.74.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 (88) hide show
  1. package/dist/index.js +37 -4
  2. package/messages/en.json +30 -1
  3. package/messages/he.json +30 -1
  4. package/package.json +1 -1
  5. package/templates/nextjs/base/AGENTS.md.ejs +63 -16
  6. package/templates/nextjs/base/AI-GUIDE.md +17 -4
  7. package/templates/nextjs/base/CLAUDE.md.ejs +67 -20
  8. package/templates/nextjs/base/TRANSLATIONS.md +207 -207
  9. package/templates/nextjs/base/src/app/account/page.tsx +168 -122
  10. package/templates/nextjs/base/src/app/category/[slug]/page.tsx +10 -1
  11. package/templates/nextjs/base/src/app/checkout/page.tsx +1179 -1074
  12. package/templates/nextjs/base/src/app/layout.tsx.ejs +48 -4
  13. package/templates/nextjs/base/src/app/order-confirmation/page.tsx +98 -3
  14. package/templates/nextjs/base/src/app/products/[slug]/page.tsx +14 -2
  15. package/templates/nextjs/base/src/components/account/loyalty-panel.tsx +226 -0
  16. package/templates/nextjs/base/src/components/account/order-history.tsx +422 -371
  17. package/templates/nextjs/base/src/components/account/order-status-timeline.tsx +85 -85
  18. package/templates/nextjs/base/src/components/account/saved-payment-methods.tsx +104 -0
  19. package/templates/nextjs/base/src/components/checkout/checkout-form.tsx +618 -612
  20. package/templates/nextjs/base/src/components/checkout/custom-fields-step.tsx +354 -306
  21. package/templates/nextjs/base/src/components/checkout/tax-display.tsx +141 -74
  22. package/templates/nextjs/base/src/core/hooks/use-cart-page.ts +218 -127
  23. package/templates/nextjs/base/src/core/hooks/use-home-data.ts +63 -51
  24. package/templates/nextjs/base/src/core/hooks/use-product-listing.ts +379 -295
  25. package/templates/nextjs/base/src/core/hooks/use-product-page.ts +33 -1
  26. package/templates/nextjs/base/src/core/lib/capabilities.ts +65 -0
  27. package/templates/nextjs/base/src/core/lib/display-price.ts +139 -0
  28. package/templates/nextjs/base/src/core/lib/region.server.ts +112 -0
  29. package/templates/nextjs/base/src/core/lib/region.ts +112 -0
  30. package/templates/nextjs/base/src/core/lib/store-info.ts +19 -0
  31. package/templates/nextjs/base/src/core/providers/store-provider.tsx.ejs +423 -243
  32. package/templates/nextjs/base/src/ui/cart/cart-item.tsx +174 -164
  33. package/templates/nextjs/base/src/ui/cart/cart-summary.tsx +109 -110
  34. package/templates/nextjs/base/src/ui/cart/cart-view.tsx +219 -176
  35. package/templates/nextjs/base/src/ui/cart/gift-card-input.tsx +191 -0
  36. package/templates/nextjs/base/src/ui/cart/reservation-countdown.tsx +137 -137
  37. package/templates/nextjs/base/src/ui/cart/tax-estimate-line.tsx +108 -0
  38. package/templates/nextjs/base/src/ui/home/home-client.tsx +27 -29
  39. package/templates/nextjs/base/src/ui/layout/header-search.tsx +249 -0
  40. package/templates/nextjs/base/src/ui/layout/region-switcher.tsx +59 -0
  41. package/templates/nextjs/base/src/ui/layout/site-header.tsx.ejs +171 -152
  42. package/templates/nextjs/base/src/ui/product/frequently-bought-together.tsx +197 -191
  43. package/templates/nextjs/base/src/ui/product/product-card.tsx +25 -52
  44. package/templates/nextjs/base/src/ui/product/product-client-section.tsx +12 -3
  45. package/templates/nextjs/base/src/ui/product/product-listing.tsx +460 -446
  46. package/templates/nextjs/base/src/ui/product/recommendation-section.tsx +108 -102
  47. package/templates/nextjs/base/src/ui/product/review-form.tsx +33 -11
  48. package/templates/nextjs/base/src/ui/product/stock-badge.tsx +15 -3
  49. package/templates/nextjs/designs/atelier/app-overlay/layout.tsx.ejs +48 -4
  50. package/templates/nextjs/designs/atelier/messages-patch/en.json +21 -25
  51. package/templates/nextjs/designs/atelier/messages-patch/he.json +21 -25
  52. package/templates/nextjs/designs/atelier/ui/cart/cart-drawer.tsx +177 -181
  53. package/templates/nextjs/designs/atelier/ui/cart/cart-item.tsx +168 -158
  54. package/templates/nextjs/designs/atelier/ui/cart/cart-summary.tsx +106 -107
  55. package/templates/nextjs/designs/atelier/ui/cart/cart-view.tsx +229 -184
  56. package/templates/nextjs/designs/atelier/ui/cart/reservation-countdown.tsx +131 -131
  57. package/templates/nextjs/designs/atelier/ui/cart/tax-estimate-line.tsx +108 -0
  58. package/templates/nextjs/designs/atelier/ui/home/benefits-band.tsx +10 -1
  59. package/templates/nextjs/designs/atelier/ui/home/hero-section.tsx +178 -174
  60. package/templates/nextjs/designs/atelier/ui/home/home-client.tsx +4 -6
  61. package/templates/nextjs/designs/atelier/ui/home/newsletter-section.tsx +150 -78
  62. package/templates/nextjs/designs/atelier/ui/home/testimonials-band.tsx +79 -46
  63. package/templates/nextjs/designs/atelier/ui/layout/header-search.tsx +264 -0
  64. package/templates/nextjs/designs/atelier/ui/layout/region-switcher.tsx +44 -0
  65. package/templates/nextjs/designs/atelier/ui/layout/site-header.tsx.ejs +5 -0
  66. package/templates/nextjs/designs/atelier/ui/product/frequently-bought-together.tsx +202 -196
  67. package/templates/nextjs/designs/atelier/ui/product/product-card.tsx +27 -52
  68. package/templates/nextjs/designs/atelier/ui/product/product-client-section.tsx +12 -3
  69. package/templates/nextjs/designs/atelier/ui/product/product-listing.tsx +332 -260
  70. package/templates/nextjs/designs/atelier/ui/product/recommendation-section.tsx +101 -95
  71. package/templates/nextjs/designs/atelier/ui/product/review-form.tsx +30 -10
  72. package/templates/nextjs/designs/atelier/ui/product/stock-badge.tsx +17 -2
  73. package/templates/nextjs/designs/atelier/ui/shared/icons.tsx +9 -0
  74. package/templates/nextjs/ui-canvas/cart/cart-item.tsx +137 -137
  75. package/templates/nextjs/ui-canvas/cart/cart-summary.tsx +98 -99
  76. package/templates/nextjs/ui-canvas/cart/cart-view.tsx +140 -140
  77. package/templates/nextjs/ui-canvas/cart/gift-card-input.tsx +142 -0
  78. package/templates/nextjs/ui-canvas/cart/reservation-countdown.tsx +124 -124
  79. package/templates/nextjs/ui-canvas/cart/tax-estimate-line.tsx +104 -0
  80. package/templates/nextjs/ui-canvas/home/home-client.tsx +24 -26
  81. package/templates/nextjs/ui-canvas/layout/region-switcher.tsx +51 -0
  82. package/templates/nextjs/ui-canvas/layout/site-header.tsx.ejs +3 -0
  83. package/templates/nextjs/ui-canvas/product/frequently-bought-together.tsx +174 -168
  84. package/templates/nextjs/ui-canvas/product/product-card.tsx +9 -51
  85. package/templates/nextjs/ui-canvas/product/product-client-section.tsx +5 -3
  86. package/templates/nextjs/ui-canvas/product/recommendation-section.tsx +105 -99
  87. package/templates/nextjs/ui-canvas/product/review-form.tsx +9 -1
  88. package/templates/nextjs/ui-canvas/product/stock-badge.tsx +17 -2
@@ -1,137 +1,137 @@
1
- 'use client';
2
-
3
- import { useState, useEffect, useCallback, useRef } from 'react';
4
- import { Clock } from 'lucide-react';
5
- import type { ReservationInfo } from 'brainerce';
6
- import { useTranslations } from '@/core/lib/translations';
7
- import { cn } from '@/core/lib/utils';
8
-
9
- interface ReservationCountdownProps {
10
- reservation: ReservationInfo;
11
- /**
12
- * Fired once per reservation window when the timer reaches zero, including
13
- * when the page opens on an already-expired reservation. The handler is
14
- * expected to refresh the cart and gate checkout: this component only
15
- * displays, it never decides what is still purchasable. Wire it to
16
- * `useCartPage().onReservationExpired`.
17
- */
18
- onExpire?: () => void;
19
- className?: string;
20
- }
21
-
22
- export function ReservationCountdown({
23
- reservation,
24
- onExpire,
25
- className,
26
- }: ReservationCountdownProps) {
27
- const t = useTranslations('reservation');
28
- // `null` means "not measured yet". Server-rendered HTML must not compute a
29
- // time, or it disagrees with the client on hydration; starting at 0 instead
30
- // would render the expired banner for one frame on a perfectly live
31
- // reservation, and would fire onExpire on every mount.
32
- const [remainingSeconds, setRemainingSeconds] = useState<number | null>(null);
33
-
34
- const calculateRemaining = useCallback(() => {
35
- if (!reservation.expiresAt) return 0;
36
- const expiresAtMs = new Date(reservation.expiresAt).getTime();
37
- const nowMs = Date.now();
38
- return Math.max(0, Math.floor((expiresAtMs - nowMs) / 1000));
39
- }, [reservation.expiresAt]);
40
-
41
- // Report each expiry window at most once. Keyed on `expiresAt` rather than a
42
- // boolean, so the cart refresh that expiry triggers (which remounts this
43
- // component) cannot bounce straight back into a second report.
44
- const expiresAt = reservation.expiresAt;
45
- const reportedExpiryRef = useRef<string | null>(null);
46
- const onExpireRef = useRef(onExpire);
47
- // Kept in a ref, and synced in an effect rather than during render, so a
48
- // caller passing an inline arrow does not restart the timer every render.
49
- useEffect(() => {
50
- onExpireRef.current = onExpire;
51
- }, [onExpire]);
52
-
53
- const reportExpired = useCallback(() => {
54
- if (!expiresAt) return;
55
- if (reportedExpiryRef.current === expiresAt) return;
56
- reportedExpiryRef.current = expiresAt;
57
- onExpireRef.current?.();
58
- }, [expiresAt]);
59
-
60
- useEffect(() => {
61
- const initial = calculateRemaining();
62
- setRemainingSeconds(initial);
63
- if (initial <= 0) reportExpired();
64
-
65
- const interval = setInterval(() => {
66
- const remaining = calculateRemaining();
67
- setRemainingSeconds(remaining);
68
-
69
- if (remaining <= 0) {
70
- clearInterval(interval);
71
- reportExpired();
72
- }
73
- }, 1000);
74
-
75
- return () => clearInterval(interval);
76
- }, [calculateRemaining, reportExpired]);
77
-
78
- if (!reservation.hasReservation) return null;
79
-
80
- const displaySeconds = remainingSeconds ?? 0;
81
- const minutes = Math.floor(displaySeconds / 60);
82
- const seconds = displaySeconds % 60;
83
- const isExpired = remainingSeconds !== null && remainingSeconds <= 0;
84
- const isUrgent = displaySeconds > 0 && displaySeconds < 120;
85
-
86
- const displayMessage = reservation.countdownMessage
87
- ? reservation.countdownMessage.replace(
88
- '{time}',
89
- `${minutes}:${seconds.toString().padStart(2, '0')}`
90
- )
91
- : null;
92
-
93
- return (
94
- <div
95
- className={cn(
96
- 'flex items-center gap-3 rounded-lg px-4 py-3 text-sm',
97
- isExpired
98
- ? 'bg-destructive/10 border-destructive/20 text-destructive border'
99
- : isUrgent
100
- ? 'border border-orange-200 bg-orange-50 text-orange-800 dark:border-orange-800 dark:bg-orange-950/30 dark:text-orange-300'
101
- : 'bg-primary/5 border-primary/20 text-foreground border',
102
- className
103
- )}
104
- >
105
- <Clock
106
- className={cn(
107
- 'h-5 w-5 flex-shrink-0',
108
- isExpired
109
- ? 'text-destructive'
110
- : isUrgent
111
- ? 'text-orange-600 dark:text-orange-400'
112
- : 'text-primary'
113
- )}
114
- aria-hidden="true"
115
- />
116
-
117
- <div className="flex-1">
118
- {isExpired ? (
119
- <>
120
- <p className="font-medium">{t('expired')}</p>
121
- <p className="text-xs opacity-90">{t('expiredHint')}</p>
122
- </>
123
- ) : displayMessage ? (
124
- <p>{displayMessage}</p>
125
- ) : (
126
- <p>
127
- {isUrgent ? `${t('hurry')} ` : ''}
128
- {t('reservedFor')}{' '}
129
- <span className="font-semibold tabular-nums">
130
- {minutes}:{seconds.toString().padStart(2, '0')}
131
- </span>
132
- </p>
133
- )}
134
- </div>
135
- </div>
136
- );
137
- }
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useCallback, useRef } from 'react';
4
+ import { Clock } from 'lucide-react';
5
+ import type { ReservationInfo } from 'brainerce';
6
+ import { useTranslations } from '@/core/lib/translations';
7
+ import { cn } from '@/core/lib/utils';
8
+
9
+ interface ReservationCountdownProps {
10
+ reservation: ReservationInfo;
11
+ /**
12
+ * Fired once per reservation window when the timer reaches zero, including
13
+ * when the page opens on an already-expired reservation. The handler is
14
+ * expected to refresh the cart and gate checkout: this component only
15
+ * displays, it never decides what is still purchasable. Wire it to
16
+ * `useCartPage().onReservationExpired`.
17
+ */
18
+ onExpire?: () => void;
19
+ className?: string;
20
+ }
21
+
22
+ export function ReservationCountdown({
23
+ reservation,
24
+ onExpire,
25
+ className,
26
+ }: ReservationCountdownProps) {
27
+ const t = useTranslations('reservation');
28
+ // `null` means "not measured yet". Server-rendered HTML must not compute a
29
+ // time, or it disagrees with the client on hydration; starting at 0 instead
30
+ // would render the expired banner for one frame on a perfectly live
31
+ // reservation, and would fire onExpire on every mount.
32
+ const [remainingSeconds, setRemainingSeconds] = useState<number | null>(null);
33
+
34
+ const calculateRemaining = useCallback(() => {
35
+ if (!reservation.expiresAt) return 0;
36
+ const expiresAtMs = new Date(reservation.expiresAt).getTime();
37
+ const nowMs = Date.now();
38
+ return Math.max(0, Math.floor((expiresAtMs - nowMs) / 1000));
39
+ }, [reservation.expiresAt]);
40
+
41
+ // Report each expiry window at most once. Keyed on `expiresAt` rather than a
42
+ // boolean, so the cart refresh that expiry triggers (which remounts this
43
+ // component) cannot bounce straight back into a second report.
44
+ const expiresAt = reservation.expiresAt;
45
+ const reportedExpiryRef = useRef<string | null>(null);
46
+ const onExpireRef = useRef(onExpire);
47
+ // Kept in a ref, and synced in an effect rather than during render, so a
48
+ // caller passing an inline arrow does not restart the timer every render.
49
+ useEffect(() => {
50
+ onExpireRef.current = onExpire;
51
+ }, [onExpire]);
52
+
53
+ const reportExpired = useCallback(() => {
54
+ if (!expiresAt) return;
55
+ if (reportedExpiryRef.current === expiresAt) return;
56
+ reportedExpiryRef.current = expiresAt;
57
+ onExpireRef.current?.();
58
+ }, [expiresAt]);
59
+
60
+ useEffect(() => {
61
+ const initial = calculateRemaining();
62
+ setRemainingSeconds(initial);
63
+ if (initial <= 0) reportExpired();
64
+
65
+ const interval = setInterval(() => {
66
+ const remaining = calculateRemaining();
67
+ setRemainingSeconds(remaining);
68
+
69
+ if (remaining <= 0) {
70
+ clearInterval(interval);
71
+ reportExpired();
72
+ }
73
+ }, 1000);
74
+
75
+ return () => clearInterval(interval);
76
+ }, [calculateRemaining, reportExpired]);
77
+
78
+ if (!reservation.hasReservation) return null;
79
+
80
+ const displaySeconds = remainingSeconds ?? 0;
81
+ const minutes = Math.floor(displaySeconds / 60);
82
+ const seconds = displaySeconds % 60;
83
+ const isExpired = remainingSeconds !== null && remainingSeconds <= 0;
84
+ const isUrgent = displaySeconds > 0 && displaySeconds < 120;
85
+
86
+ const displayMessage = reservation.countdownMessage
87
+ ? reservation.countdownMessage.replace(
88
+ '{time}',
89
+ `${minutes}:${seconds.toString().padStart(2, '0')}`
90
+ )
91
+ : null;
92
+
93
+ return (
94
+ <div
95
+ className={cn(
96
+ 'flex items-center gap-3 rounded-lg px-4 py-3 text-sm',
97
+ isExpired
98
+ ? 'bg-destructive/10 border-destructive/20 text-destructive border'
99
+ : isUrgent
100
+ ? 'border border-orange-200 bg-orange-50 text-orange-800 dark:border-orange-800 dark:bg-orange-950/30 dark:text-orange-300'
101
+ : 'bg-primary/5 border-primary/20 text-foreground border',
102
+ className
103
+ )}
104
+ >
105
+ <Clock
106
+ className={cn(
107
+ 'h-5 w-5 flex-shrink-0',
108
+ isExpired
109
+ ? 'text-destructive'
110
+ : isUrgent
111
+ ? 'text-orange-600 dark:text-orange-400'
112
+ : 'text-primary'
113
+ )}
114
+ aria-hidden="true"
115
+ />
116
+
117
+ <div className="flex-1">
118
+ {isExpired ? (
119
+ <>
120
+ <p className="font-medium">{t('expired')}</p>
121
+ <p className="text-xs opacity-90">{t('expiredHint')}</p>
122
+ </>
123
+ ) : displayMessage ? (
124
+ <p>{displayMessage}</p>
125
+ ) : (
126
+ <p>
127
+ {isUrgent ? `${t('hurry')} ` : ''}
128
+ {t('reservedFor')}{' '}
129
+ <span className="font-semibold tabular-nums">
130
+ {minutes}:{seconds.toString().padStart(2, '0')}
131
+ </span>
132
+ </p>
133
+ )}
134
+ </div>
135
+ </div>
136
+ );
137
+ }
@@ -0,0 +1,108 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * Non-binding tax preview for the cart.
5
+ *
6
+ * Shoppers in VAT markets expect a tax figure before they reach the address
7
+ * form, and "calculated at checkout" reads as an unpleasant surprise waiting
8
+ * to happen. `estimateTax` gives them a number early without pretending it is
9
+ * final.
10
+ *
11
+ * ⛔ NEVER PRESENT THIS AS THE REAL TAX. The authoritative calculation runs at
12
+ * checkout against the full shipping address. A preview has no province, so a
13
+ * Canadian estimate shows federal GST alone and the provincial PST/QST joins
14
+ * it at checkout. That is what `note` is for, and why the "calculated at
15
+ * checkout" caveat stays on the row even when a figure is showing.
16
+ *
17
+ * ⛔ NO ESTIMATE IS RENDERED when `appliesTax` is false, which is what an
18
+ * empty or freshly created store returns: tax is off, no country reached us,
19
+ * or no active rate covers it. In that case (and when the call fails, and
20
+ * while it is in flight, and on an empty cart) this falls back to EXACTLY the
21
+ * row the cart shipped with before the estimate existed: "Tax / calculated at
22
+ * checkout". There is no spinner, no error box, and no empty amount.
23
+ *
24
+ * `getStoreTaxClasses()` would let you add a separate "9% VAT" transparency
25
+ * badge, but `rateName` + `rate` already come back on this response and say
26
+ * the same thing, so a second request would buy nothing.
27
+ */
28
+
29
+ import { useEffect, useState } from 'react';
30
+ import type { TaxEstimateResponse } from 'brainerce';
31
+ import { formatPrice } from 'brainerce';
32
+ import { getClient } from '@/core/lib/brainerce';
33
+ import { useTranslations } from '@/core/lib/translations';
34
+ import { useCart, useRegion } from '@/core/providers/store-provider';
35
+
36
+ export function useTaxEstimate(): TaxEstimateResponse | null {
37
+ const { totals } = useCart();
38
+ // The same country the region lookup already extracted from the edge. Asking
39
+ // for it twice would be two sources that can disagree.
40
+ const { buyerCountry } = useRegion();
41
+ const [estimate, setEstimate] = useState<TaxEstimateResponse | null>(null);
42
+ const subtotal = totals.subtotal;
43
+
44
+ useEffect(() => {
45
+ // An empty cart has nothing to tax. Skipping the call also keeps a
46
+ // brand-new store from firing a request on every page load forever.
47
+ if (!(subtotal > 0)) {
48
+ setEstimate(null);
49
+ return;
50
+ }
51
+ let cancelled = false;
52
+ getClient()
53
+ .estimateTax({ ...(buyerCountry ? { country: buyerCountry } : {}), subtotal })
54
+ .then((result) => {
55
+ if (!cancelled) setEstimate(result);
56
+ })
57
+ .catch(() => {
58
+ // Swallowed on purpose. A tax preview is a courtesy; it must never
59
+ // take the cart down with it. The caller falls back to the plain
60
+ // "calculated at checkout" row.
61
+ if (!cancelled) setEstimate(null);
62
+ });
63
+ return () => {
64
+ cancelled = true;
65
+ };
66
+ }, [subtotal, buyerCountry]);
67
+
68
+ return estimate;
69
+ }
70
+
71
+ export function TaxEstimateLine() {
72
+ const t = useTranslations('cart');
73
+ const tc = useTranslations('common');
74
+ const estimate = useTaxEstimate();
75
+
76
+ // No estimate: the row the cart has always shown.
77
+ if (!estimate || !estimate.appliesTax) {
78
+ return (
79
+ <div className="flex items-center justify-between">
80
+ <span className="text-muted-foreground">{tc('tax')}</span>
81
+ <span className="text-muted-foreground text-xs">{t('taxAtCheckout')}</span>
82
+ </div>
83
+ );
84
+ }
85
+
86
+ // `rateName` is one rate or several joined ("GST + QST"); `rate` is the
87
+ // summed effective percent and is null when no rule matched.
88
+ const label =
89
+ estimate.rateName && estimate.rate != null
90
+ ? `${estimate.rateName} ${estimate.rate}%`
91
+ : (estimate.rateName ?? tc('tax'));
92
+
93
+ return (
94
+ <div className="space-y-1">
95
+ <div className="flex items-center justify-between">
96
+ <span className="text-muted-foreground">{label}</span>
97
+ <span className="text-foreground font-medium">
98
+ {formatPrice(estimate.estimatedTax, { currency: estimate.currency }) as string}
99
+ </span>
100
+ </div>
101
+ {/* The caveat travels WITH the number, not in a footnote nobody reads. */}
102
+ <p className="text-muted-foreground text-xs">{t('taxAtCheckout')}</p>
103
+ {/* Server-written and already localized. Never replace it with copy of
104
+ your own, it is what explains a partial estimate. */}
105
+ {estimate.note ? <p className="text-muted-foreground text-xs">{estimate.note}</p> : null}
106
+ </div>
107
+ );
108
+ }
@@ -1,29 +1,27 @@
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 { DiscountBannerStrip } from './discount-banner-strip';
7
- import { HeroSection } from './hero-section';
8
- import { FeaturedProductsSection } from './featured-products-section';
9
-
10
- export function HomeClient() {
11
- const { storeInfo, loading: storeLoading } = useStoreInfo();
12
- const { products, banners, loading } = useHomeData();
13
-
14
- if (storeLoading || loading) {
15
- return (
16
- <div className="flex min-h-[60vh] items-center justify-center">
17
- <LoadingSpinner size="lg" />
18
- </div>
19
- );
20
- }
21
-
22
- return (
23
- <div>
24
- <DiscountBannerStrip banners={banners} />
25
- <HeroSection storeName={storeInfo?.name} />
26
- <FeaturedProductsSection products={products} />
27
- </div>
28
- );
29
- }
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 { HeroSection } from './hero-section';
7
+ import { FeaturedProductsSection } from './featured-products-section';
8
+
9
+ export function HomeClient() {
10
+ const { storeInfo, loading: storeLoading } = useStoreInfo();
11
+ const { products, loading } = useHomeData();
12
+
13
+ if (storeLoading || loading) {
14
+ return (
15
+ <div className="flex min-h-[60vh] items-center justify-center">
16
+ <LoadingSpinner size="lg" />
17
+ </div>
18
+ );
19
+ }
20
+
21
+ return (
22
+ <div>
23
+ <HeroSection storeName={storeInfo?.name} />
24
+ <FeaturedProductsSection products={products} />
25
+ </div>
26
+ );
27
+ }