create-brainerce-store 1.50.5 → 1.51.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +171 -66
- package/messages/en.json +4 -1
- package/messages/he.json +4 -1
- package/package.json +1 -1
- package/templates/nextjs/base/.claude/commands/design.md +51 -0
- package/templates/nextjs/base/.env.local.ejs +5 -0
- package/templates/nextjs/base/.eslintrc.json +57 -3
- package/templates/nextjs/base/AGENTS.md +41 -0
- package/templates/nextjs/base/AI-GUIDE.md +94 -0
- package/templates/nextjs/base/CLAUDE.md +42 -0
- package/templates/nextjs/base/TRANSLATIONS.md +1 -1
- package/templates/nextjs/base/next.config.ts +2 -2
- package/templates/nextjs/base/src/app/account/page.tsx +5 -5
- package/templates/nextjs/base/src/app/api/auth/logout/route.ts +1 -1
- package/templates/nextjs/base/src/app/api/auth/reset-password/route.ts +2 -2
- package/templates/nextjs/base/src/app/api/store/[...path]/route.ts +1 -1
- package/templates/nextjs/base/src/app/auth/callback/page.tsx +4 -4
- package/templates/nextjs/base/src/app/blog/[slug]/page.tsx.ejs +4 -4
- package/templates/nextjs/base/src/app/blog/page.tsx.ejs +2 -2
- package/templates/nextjs/base/src/app/cart/page.tsx +2 -173
- package/templates/nextjs/base/src/app/checkout/page.tsx +12 -11
- package/templates/nextjs/base/src/app/contact/page.tsx +3 -3
- package/templates/nextjs/base/src/app/faq/page.tsx.ejs +2 -2
- package/templates/nextjs/base/src/app/forgot-password/page.tsx +4 -4
- package/templates/nextjs/base/src/app/layout.tsx.ejs +12 -12
- package/templates/nextjs/base/src/app/login/page.tsx +4 -4
- package/templates/nextjs/base/src/app/order-confirmation/page.tsx +19 -12
- package/templates/nextjs/base/src/app/page.tsx +4 -4
- package/templates/nextjs/base/src/app/pages/[slug]/page.tsx.ejs +3 -3
- package/templates/nextjs/base/src/app/payment-complete/page.tsx +1 -1
- package/templates/nextjs/base/src/app/products/[slug]/page.tsx +6 -6
- package/templates/nextjs/base/src/app/products/page.tsx +4 -623
- package/templates/nextjs/base/src/app/register/page.tsx +4 -4
- package/templates/nextjs/base/src/app/reset-password/page.tsx +5 -5
- package/templates/nextjs/base/src/app/sitemap.ts +1 -1
- package/templates/nextjs/base/src/app/verify-email/page.tsx +5 -5
- package/templates/nextjs/base/src/components/account/address-book.tsx +3 -3
- package/templates/nextjs/base/src/components/account/order-customizations.tsx +3 -3
- package/templates/nextjs/base/src/components/account/order-history.tsx +13 -5
- package/templates/nextjs/base/src/components/account/order-payment-block.tsx +2 -2
- package/templates/nextjs/base/src/components/account/order-shipping-block.tsx +2 -2
- package/templates/nextjs/base/src/components/account/order-status-timeline.tsx +2 -2
- package/templates/nextjs/base/src/components/account/profile-section.tsx +3 -3
- package/templates/nextjs/base/src/components/auth/login-form.tsx +4 -4
- package/templates/nextjs/base/src/components/auth/oauth-buttons.tsx +4 -4
- package/templates/nextjs/base/src/components/auth/register-form.tsx +4 -4
- package/templates/nextjs/base/src/components/brainerce-bot.tsx +2 -2
- package/templates/nextjs/base/src/components/checkout/checkout-form.tsx +24 -3
- package/templates/nextjs/base/src/components/checkout/custom-fields-step.tsx +2 -2
- package/templates/nextjs/base/src/components/checkout/delivery-method-step.tsx +2 -2
- package/templates/nextjs/base/src/components/checkout/order-bump-card.tsx +4 -4
- package/templates/nextjs/base/src/components/checkout/payment-step.tsx +6 -6
- package/templates/nextjs/base/src/components/checkout/pickup-step.tsx +3 -3
- package/templates/nextjs/base/src/components/checkout/shipping-step.tsx +3 -3
- package/templates/nextjs/base/src/components/checkout/tax-display.tsx +3 -3
- package/templates/nextjs/base/src/components/seo/organization-json-ld.tsx +2 -2
- package/templates/nextjs/base/src/components/seo/product-json-ld.tsx +3 -3
- package/templates/nextjs/base/src/core/hooks/use-cart-page.ts +58 -0
- package/templates/nextjs/base/src/core/hooks/use-home-data.ts +51 -0
- package/templates/nextjs/base/src/core/hooks/use-product-listing.ts +295 -0
- package/templates/nextjs/base/src/core/hooks/use-product-page.ts +278 -0
- package/templates/nextjs/base/src/{hooks → core/hooks}/use-search.ts +1 -1
- package/templates/nextjs/base/src/{lib → core/lib}/brainerce.ts.ejs +1 -1
- package/templates/nextjs/base/src/core/lib/product-options.ts +121 -0
- package/templates/nextjs/base/src/{lib → core/lib}/resolve-currency.ts +1 -1
- package/templates/nextjs/base/src/{lib → core/lib}/safe-redirect.ts +1 -1
- package/templates/nextjs/base/src/{lib → core/lib}/sanitize.ts.ejs +1 -1
- package/templates/nextjs/base/src/{lib → core/lib}/use-currency.ts +1 -1
- package/templates/nextjs/base/src/{providers → core/providers}/store-provider.tsx.ejs +4 -4
- package/templates/nextjs/base/src/{components → ui}/cart/cart-bundle-offer.tsx +5 -5
- package/templates/nextjs/base/src/{components → ui}/cart/cart-item.tsx +6 -6
- package/templates/nextjs/base/src/{components → ui}/cart/cart-nudges.tsx +1 -1
- package/templates/nextjs/base/src/{components → ui}/cart/cart-summary.tsx +4 -4
- package/templates/nextjs/base/src/{components → ui}/cart/cart-upgrade-banner.tsx +5 -5
- package/templates/nextjs/base/src/ui/cart/cart-view.tsx +149 -0
- package/templates/nextjs/base/src/{components → ui}/cart/coupon-input.tsx +4 -4
- package/templates/nextjs/base/src/{components → ui}/cart/free-shipping-bar.tsx +4 -4
- package/templates/nextjs/base/src/{components → ui}/cart/reservation-countdown.tsx +2 -2
- package/templates/nextjs/base/src/ui/home/discount-banner-strip.tsx +20 -0
- package/templates/nextjs/base/src/ui/home/featured-products-section.tsx +24 -0
- package/templates/nextjs/base/src/ui/home/hero-section.tsx +29 -0
- package/templates/nextjs/base/src/ui/home/home-client.tsx +29 -0
- package/templates/nextjs/base/src/{components/content → ui/layout}/faq-section.tsx.ejs +2 -2
- package/templates/nextjs/base/src/{components/content → ui/layout}/header-account.tsx +3 -3
- package/templates/nextjs/base/src/{components/content → ui/layout}/rich-text-block.tsx.ejs +1 -1
- package/templates/nextjs/base/src/{components/content → ui/layout}/site-header.tsx.ejs +1 -1
- package/templates/nextjs/base/src/{components/products → ui/product}/customization-fields.tsx +18 -51
- package/templates/nextjs/base/src/{components/products → ui/product}/discount-badge.tsx +1 -1
- package/templates/nextjs/base/src/{components/products → ui/product}/frequently-bought-together.tsx +6 -6
- package/templates/nextjs/base/src/{components/products → ui/product}/modifier-group-selector.tsx +10 -73
- package/templates/nextjs/base/src/{components/shared → ui/product}/price-display.tsx +2 -2
- package/templates/nextjs/base/src/{components/products → ui/product}/product-card.tsx +10 -10
- package/templates/nextjs/base/src/{app/products/[slug] → ui/product}/product-client-section.tsx +45 -206
- package/templates/nextjs/base/src/{components/products → ui/product}/product-grid.tsx +3 -3
- package/templates/nextjs/base/src/ui/product/product-listing.tsx +429 -0
- package/templates/nextjs/base/src/{components/products → ui/product}/recommendation-section.tsx +4 -4
- package/templates/nextjs/base/src/{components/reviews → ui/product}/review-form.tsx +2 -2
- package/templates/nextjs/base/src/{components/reviews → ui/product}/reviews-section.tsx +1 -1
- package/templates/nextjs/base/src/{components/products → ui/product}/stock-badge.tsx +2 -2
- package/templates/nextjs/base/src/{components/products → ui/product}/variant-selector.tsx +3 -3
- package/templates/nextjs/base/src/{components → ui}/shared/cdn-image.tsx +1 -1
- package/templates/nextjs/base/src/{components → ui}/shared/loading-spinner.tsx +2 -2
- package/templates/nextjs/base/tailwind.config.ts +12 -0
- package/templates/nextjs/designs/passepartout/app-overlay/layout.tsx.ejs +253 -0
- package/templates/nextjs/designs/passepartout/globals.css +412 -0
- package/templates/nextjs/designs/passepartout/messages-patch/en.json +87 -0
- package/templates/nextjs/designs/passepartout/messages-patch/he.json +87 -0
- package/templates/nextjs/designs/passepartout/ui/cart/cart-bundle-offer.tsx +111 -0
- package/templates/nextjs/designs/passepartout/ui/cart/cart-nudges.tsx +39 -0
- package/templates/nextjs/designs/passepartout/ui/cart/cart-upgrade-banner.tsx +141 -0
- package/templates/nextjs/designs/passepartout/ui/cart/cart-view.tsx +149 -0
- package/templates/nextjs/designs/passepartout/ui/cart/free-shipping-bar.tsx +62 -0
- package/templates/nextjs/designs/passepartout/ui/cart/reservation-countdown.tsx +103 -0
- package/templates/nextjs/designs/passepartout/ui/home/home-client.tsx +401 -0
- package/templates/nextjs/designs/passepartout/ui/layout/announcement-bar.tsx +139 -0
- package/templates/nextjs/designs/passepartout/ui/layout/cart-link.tsx +45 -0
- package/templates/nextjs/designs/passepartout/ui/layout/faq-section.tsx +103 -0
- package/templates/nextjs/designs/passepartout/ui/layout/site-footer.tsx +159 -0
- package/templates/nextjs/designs/passepartout/ui/layout/site-header.tsx.ejs +155 -0
- package/templates/nextjs/designs/passepartout/ui/layout/trust-icons.tsx +60 -0
- package/templates/nextjs/designs/passepartout/ui/layout/whatsapp-button.tsx +34 -0
- package/templates/nextjs/designs/passepartout/ui/product/discount-badge.tsx +22 -0
- package/templates/nextjs/designs/passepartout/ui/product/frequently-bought-together.tsx +205 -0
- package/templates/nextjs/designs/passepartout/ui/product/price-display.tsx +111 -0
- package/templates/nextjs/designs/passepartout/ui/product/product-card.tsx +265 -0
- package/templates/nextjs/designs/passepartout/ui/product/product-client-section.tsx +667 -0
- package/templates/nextjs/designs/passepartout/ui/product/product-grid.tsx +35 -0
- package/templates/nextjs/designs/passepartout/ui/product/product-listing.tsx +427 -0
- package/templates/nextjs/designs/passepartout/ui/product/recommendation-section.tsx +113 -0
- package/templates/nextjs/designs/passepartout/ui/product/review-form.tsx +294 -0
- package/templates/nextjs/designs/passepartout/ui/product/reviews-section.tsx.ejs +96 -0
- package/templates/nextjs/designs/passepartout/ui/product/stock-badge.tsx +52 -0
- package/templates/nextjs/designs/passepartout/ui/product/variant-selector.tsx +307 -0
- package/templates/nextjs/designs/passepartout/ui/shared/fly-to-cart.ts +85 -0
- package/templates/nextjs/designs/passepartout/ui/shared/loading-spinner.tsx +33 -0
- package/templates/nextjs/ui-canvas/cart/cart-bundle-offer.tsx +87 -0
- package/templates/nextjs/ui-canvas/cart/cart-item.tsx +121 -0
- package/templates/nextjs/ui-canvas/cart/cart-nudges.tsx +25 -0
- package/templates/nextjs/ui-canvas/cart/cart-summary.tsx +97 -0
- package/templates/nextjs/ui-canvas/cart/cart-upgrade-banner.tsx +103 -0
- package/templates/nextjs/ui-canvas/cart/cart-view.tsx +106 -0
- package/templates/nextjs/ui-canvas/cart/coupon-input.tsx +117 -0
- package/templates/nextjs/ui-canvas/cart/free-shipping-bar.tsx +52 -0
- package/templates/nextjs/ui-canvas/cart/reservation-countdown.tsx +79 -0
- package/templates/nextjs/ui-canvas/home/discount-banner-strip.tsx +19 -0
- package/templates/nextjs/ui-canvas/home/featured-products-section.tsx +23 -0
- package/templates/nextjs/ui-canvas/home/hero-section.tsx +21 -0
- package/templates/nextjs/ui-canvas/home/home-client.tsx +26 -0
- package/templates/nextjs/ui-canvas/layout/announcement-bar.tsx.ejs +101 -0
- package/templates/nextjs/ui-canvas/layout/faq-section.tsx.ejs +71 -0
- package/templates/nextjs/ui-canvas/layout/header-account.tsx +31 -0
- package/templates/nextjs/ui-canvas/layout/language-switcher.tsx.ejs +74 -0
- package/templates/nextjs/ui-canvas/layout/rich-text-block.tsx.ejs +34 -0
- package/templates/nextjs/ui-canvas/layout/site-footer.tsx.ejs +82 -0
- package/templates/nextjs/ui-canvas/layout/site-header.tsx.ejs +119 -0
- package/templates/nextjs/ui-canvas/product/customization-fields.tsx +443 -0
- package/templates/nextjs/ui-canvas/product/discount-badge.tsx +17 -0
- package/templates/nextjs/ui-canvas/product/frequently-bought-together.tsx +166 -0
- package/templates/nextjs/ui-canvas/product/modifier-group-selector.tsx +163 -0
- package/templates/nextjs/ui-canvas/product/price-display.tsx +50 -0
- package/templates/nextjs/ui-canvas/product/product-card.tsx +197 -0
- package/templates/nextjs/ui-canvas/product/product-client-section.tsx +352 -0
- package/templates/nextjs/ui-canvas/product/product-grid.tsx +27 -0
- package/templates/nextjs/ui-canvas/product/product-listing.tsx +224 -0
- package/templates/nextjs/ui-canvas/product/recommendation-section.tsx +95 -0
- package/templates/nextjs/ui-canvas/product/review-form.tsx +269 -0
- package/templates/nextjs/ui-canvas/product/reviews-section.tsx +78 -0
- package/templates/nextjs/ui-canvas/product/stock-badge.tsx +43 -0
- package/templates/nextjs/ui-canvas/product/variant-selector.tsx +210 -0
- package/templates/nextjs/ui-canvas/shared/cdn-image.tsx +29 -0
- package/templates/nextjs/ui-canvas/shared/loading-spinner.tsx +25 -0
- package/templates/nextjs/base/src/app/home-client.tsx +0 -98
- /package/templates/nextjs/base/src/{lib → core/lib}/auth.ts +0 -0
- /package/templates/nextjs/base/src/{lib → core/lib}/csrf.ts +0 -0
- /package/templates/nextjs/base/src/{lib → core/lib}/image-hosts.ts +0 -0
- /package/templates/nextjs/base/src/{lib → core/lib}/navigation.tsx.ejs +0 -0
- /package/templates/nextjs/base/src/{lib → core/lib}/nonce.ts +0 -0
- /package/templates/nextjs/base/src/{lib → core/lib}/sanitize-html.ts +0 -0
- /package/templates/nextjs/base/src/{lib → core/lib}/seo.ts +0 -0
- /package/templates/nextjs/base/src/{lib → core/lib}/store-info.ts +0 -0
- /package/templates/nextjs/base/src/{lib → core/lib}/translations.ts.ejs +0 -0
- /package/templates/nextjs/base/src/{lib → core/lib}/utils.ts +0 -0
- /package/templates/nextjs/base/src/{lib → core/lib}/validation.ts +0 -0
- /package/templates/nextjs/base/src/{components/content → ui/layout}/announcement-bar.tsx.ejs +0 -0
- /package/templates/nextjs/base/src/{components → ui}/layout/language-switcher.tsx.ejs +0 -0
- /package/templates/nextjs/base/src/{components/content → ui/layout}/site-footer.tsx.ejs +0 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { formatPrice } from 'brainerce';
|
|
4
|
+
import { useTranslations } from '@/core/lib/translations';
|
|
5
|
+
import { useCart } from '@/core/providers/store-provider';
|
|
6
|
+
import { useCurrency } from '@/core/lib/use-currency';
|
|
7
|
+
import { cn } from '@/core/lib/utils';
|
|
8
|
+
|
|
9
|
+
interface CartSummaryProps {
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* DESIGN ME — order summary (subtotal, rule/coupon discounts, shipping, tax
|
|
15
|
+
* note, total); all figures come from useCart().totals and useCart().cart.
|
|
16
|
+
* Keep every discount branch — merchants rely on the per-rule breakdown.
|
|
17
|
+
*/
|
|
18
|
+
export function CartSummary({ className }: CartSummaryProps) {
|
|
19
|
+
const t = useTranslations('cart');
|
|
20
|
+
const tc = useTranslations('common');
|
|
21
|
+
const { totals, cart } = useCart();
|
|
22
|
+
const currency = useCurrency();
|
|
23
|
+
|
|
24
|
+
const rules = cart?.appliedDiscounts;
|
|
25
|
+
const ruleAmt = cart?.ruleDiscountAmount ? parseFloat(cart.ruleDiscountAmount) : 0;
|
|
26
|
+
const couponAmt = totals.discount - ruleAmt;
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<section className={cn(className)}>
|
|
30
|
+
<h3>{t('orderSummary')}</h3>
|
|
31
|
+
|
|
32
|
+
<dl>
|
|
33
|
+
{/* Subtotal */}
|
|
34
|
+
<div className="flex items-center justify-between gap-4">
|
|
35
|
+
<dt>{tc('subtotal')}</dt>
|
|
36
|
+
<dd>{formatPrice(totals.subtotal, { currency }) as string}</dd>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
{/* Rule discounts - show each rule by name */}
|
|
40
|
+
{rules && rules.length > 0
|
|
41
|
+
? rules.map((rule) => (
|
|
42
|
+
<div key={rule.ruleId} className="flex items-center justify-between gap-4">
|
|
43
|
+
<dt>{rule.ruleName}</dt>
|
|
44
|
+
<dd>-{formatPrice(parseFloat(rule.discountAmount), { currency }) as string}</dd>
|
|
45
|
+
</div>
|
|
46
|
+
))
|
|
47
|
+
: ruleAmt > 0 && (
|
|
48
|
+
<div className="flex items-center justify-between gap-4">
|
|
49
|
+
<dt>{tc('generalDiscount')}</dt>
|
|
50
|
+
<dd>-{formatPrice(ruleAmt, { currency }) as string}</dd>
|
|
51
|
+
</div>
|
|
52
|
+
)}
|
|
53
|
+
|
|
54
|
+
{/* Coupon discount */}
|
|
55
|
+
{cart?.couponCode && couponAmt > 0 && (
|
|
56
|
+
<div className="flex items-center justify-between gap-4">
|
|
57
|
+
<dt>
|
|
58
|
+
{tc('couponDiscount')} ({cart.couponCode})
|
|
59
|
+
</dt>
|
|
60
|
+
<dd>-{formatPrice(couponAmt, { currency }) as string}</dd>
|
|
61
|
+
</div>
|
|
62
|
+
)}
|
|
63
|
+
|
|
64
|
+
{/* Fallback: generic discount when no breakdown available */}
|
|
65
|
+
{totals.discount > 0 &&
|
|
66
|
+
ruleAmt <= 0 &&
|
|
67
|
+
!cart?.couponCode &&
|
|
68
|
+
(!rules || rules.length === 0) && (
|
|
69
|
+
<div className="flex items-center justify-between gap-4">
|
|
70
|
+
<dt>{tc('discount')}</dt>
|
|
71
|
+
<dd>-{formatPrice(totals.discount, { currency }) as string}</dd>
|
|
72
|
+
</div>
|
|
73
|
+
)}
|
|
74
|
+
|
|
75
|
+
{/* Shipping */}
|
|
76
|
+
{totals.shipping > 0 && (
|
|
77
|
+
<div className="flex items-center justify-between gap-4">
|
|
78
|
+
<dt>{tc('shipping')}</dt>
|
|
79
|
+
<dd>{formatPrice(totals.shipping, { currency }) as string}</dd>
|
|
80
|
+
</div>
|
|
81
|
+
)}
|
|
82
|
+
|
|
83
|
+
{/* Tax */}
|
|
84
|
+
<div className="flex items-center justify-between gap-4">
|
|
85
|
+
<dt>{tc('tax')}</dt>
|
|
86
|
+
<dd>{t('taxAtCheckout')}</dd>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
{/* Total */}
|
|
90
|
+
<div className="flex items-center justify-between gap-4">
|
|
91
|
+
<dt>{tc('total')}</dt>
|
|
92
|
+
<dd>{formatPrice(totals.total, { currency }) as string}</dd>
|
|
93
|
+
</div>
|
|
94
|
+
</dl>
|
|
95
|
+
</section>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import { CdnImage as Image } from '@/ui/shared/cdn-image';
|
|
5
|
+
import type { CartUpgradeSuggestion, CartItem as CartItemType } from 'brainerce';
|
|
6
|
+
import { formatPrice } from 'brainerce';
|
|
7
|
+
import { getClient } from '@/core/lib/brainerce';
|
|
8
|
+
import { useCurrency } from '@/core/lib/use-currency';
|
|
9
|
+
import { useTranslations } from '@/core/lib/translations';
|
|
10
|
+
import { cn } from '@/core/lib/utils';
|
|
11
|
+
|
|
12
|
+
interface CartUpgradeBannerProps {
|
|
13
|
+
suggestion: CartUpgradeSuggestion;
|
|
14
|
+
cartItem: CartItemType;
|
|
15
|
+
onUpgrade: () => void;
|
|
16
|
+
className?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* DESIGN ME — per-line upgrade suggestion under a cart item ("upgrade to X
|
|
21
|
+
* for +Y"); the suggestion comes from useCartPage().upgrades keyed by
|
|
22
|
+
* productId. Keep the session-scoped dismiss and the swap logic.
|
|
23
|
+
*/
|
|
24
|
+
export function CartUpgradeBanner({
|
|
25
|
+
suggestion,
|
|
26
|
+
cartItem,
|
|
27
|
+
onUpgrade,
|
|
28
|
+
className,
|
|
29
|
+
}: CartUpgradeBannerProps) {
|
|
30
|
+
const t = useTranslations('cart');
|
|
31
|
+
const currency = useCurrency();
|
|
32
|
+
const [upgrading, setUpgrading] = useState(false);
|
|
33
|
+
const [dismissed, setDismissed] = useState(false);
|
|
34
|
+
|
|
35
|
+
const storageKey = `dismissed_upgrade_${suggestion.sourceProductId}`;
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
try {
|
|
39
|
+
if (sessionStorage.getItem(storageKey)) {
|
|
40
|
+
setDismissed(true);
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
/* ignore */
|
|
44
|
+
}
|
|
45
|
+
}, [storageKey]);
|
|
46
|
+
|
|
47
|
+
if (dismissed) return null;
|
|
48
|
+
|
|
49
|
+
const target = suggestion.targetProduct;
|
|
50
|
+
const firstImage = target.images?.[0];
|
|
51
|
+
const imageUrl = firstImage
|
|
52
|
+
? typeof firstImage === 'string'
|
|
53
|
+
? firstImage
|
|
54
|
+
: firstImage.url
|
|
55
|
+
: null;
|
|
56
|
+
const formattedDelta = formatPrice(parseFloat(suggestion.priceDelta), { currency }) as string;
|
|
57
|
+
|
|
58
|
+
function handleDismiss() {
|
|
59
|
+
try {
|
|
60
|
+
sessionStorage.setItem(storageKey, '1');
|
|
61
|
+
} catch {
|
|
62
|
+
/* ignore */
|
|
63
|
+
}
|
|
64
|
+
setDismissed(true);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function handleUpgrade() {
|
|
68
|
+
if (upgrading) return;
|
|
69
|
+
try {
|
|
70
|
+
setUpgrading(true);
|
|
71
|
+
const client = getClient();
|
|
72
|
+
await client.smartRemoveFromCart(cartItem.productId, cartItem.variantId || undefined);
|
|
73
|
+
await client.smartAddToCart({ productId: target.id, quantity: cartItem.quantity });
|
|
74
|
+
onUpgrade();
|
|
75
|
+
} catch (err) {
|
|
76
|
+
console.error('Failed to upgrade cart item:', err);
|
|
77
|
+
} finally {
|
|
78
|
+
setUpgrading(false);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<div className={cn('flex items-center gap-3', className)}>
|
|
84
|
+
{/* Product image — `relative` + fixed box are layout-critical for next/image fill */}
|
|
85
|
+
<span className="relative block h-12 w-12">
|
|
86
|
+
{imageUrl ? <Image src={imageUrl} alt={target.name} fill sizes="48px" /> : null}
|
|
87
|
+
</span>
|
|
88
|
+
|
|
89
|
+
{/* Text */}
|
|
90
|
+
<p>{t('upgradeFor', { name: target.name, amount: formattedDelta })}</p>
|
|
91
|
+
|
|
92
|
+
{/* Upgrade button */}
|
|
93
|
+
<button type="button" onClick={handleUpgrade} disabled={upgrading}>
|
|
94
|
+
{upgrading ? t('upgrading') : t('upgrade')}
|
|
95
|
+
</button>
|
|
96
|
+
|
|
97
|
+
{/* Dismiss button */}
|
|
98
|
+
<button type="button" onClick={handleDismiss} aria-label={t('dismissUpgrade')}>
|
|
99
|
+
×
|
|
100
|
+
</button>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
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 <LoadingSpinner size="lg" />;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Empty cart state
|
|
27
|
+
if (!cart || cart.items.length === 0) {
|
|
28
|
+
return (
|
|
29
|
+
<section>
|
|
30
|
+
{/* DESIGN ME — empty-cart state: message + continue-shopping CTA; state comes from useCartPage() */}
|
|
31
|
+
<h1>{t('emptyTitle')}</h1>
|
|
32
|
+
<p>{t('emptySubtitle')}</p>
|
|
33
|
+
<Link href="/products">{tc('continueShopping')}</Link>
|
|
34
|
+
</section>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<section>
|
|
40
|
+
{/* DESIGN ME — cart page: items list, upgrade/bundle offers, coupon, summary sidebar, checkout CTA, cross-sells; everything comes from useCartPage() */}
|
|
41
|
+
<h1>
|
|
42
|
+
{t('title')} ({itemCount} {itemCount === 1 ? tc('item') : tc('items')})
|
|
43
|
+
</h1>
|
|
44
|
+
|
|
45
|
+
{/* Reservation countdown */}
|
|
46
|
+
{cart.reservation?.hasReservation && <ReservationCountdown reservation={cart.reservation} />}
|
|
47
|
+
|
|
48
|
+
<div className="grid grid-cols-1 gap-8 lg:grid-cols-3">
|
|
49
|
+
{/* Cart Items */}
|
|
50
|
+
<div className="lg:col-span-2">
|
|
51
|
+
{/* Nudges */}
|
|
52
|
+
{cart.nudges && cart.nudges.length > 0 && <CartNudges nudges={cart.nudges} />}
|
|
53
|
+
|
|
54
|
+
{/* Cart items */}
|
|
55
|
+
<ul>
|
|
56
|
+
{cart.items.map((item) => (
|
|
57
|
+
<li key={item.id}>
|
|
58
|
+
<CartItem item={item} onUpdate={refreshCart} />
|
|
59
|
+
{upgrades?.upgrades?.[item.productId] && (
|
|
60
|
+
<CartUpgradeBanner
|
|
61
|
+
suggestion={upgrades.upgrades[item.productId]}
|
|
62
|
+
cartItem={item}
|
|
63
|
+
onUpgrade={refreshCart}
|
|
64
|
+
/>
|
|
65
|
+
)}
|
|
66
|
+
</li>
|
|
67
|
+
))}
|
|
68
|
+
</ul>
|
|
69
|
+
|
|
70
|
+
{/* Bundle offers */}
|
|
71
|
+
{bundles?.bundles && bundles.bundles.length > 0 && (
|
|
72
|
+
<section>
|
|
73
|
+
<h3>{t('bundleOffers')}</h3>
|
|
74
|
+
{bundles.bundles.map((offer) => (
|
|
75
|
+
<CartBundleOfferCard
|
|
76
|
+
key={offer.id}
|
|
77
|
+
offer={offer}
|
|
78
|
+
cartId={cart.id}
|
|
79
|
+
onAdd={refreshCart}
|
|
80
|
+
/>
|
|
81
|
+
))}
|
|
82
|
+
</section>
|
|
83
|
+
)}
|
|
84
|
+
|
|
85
|
+
{/* Coupon input */}
|
|
86
|
+
<CouponInput cart={cart} onUpdate={refreshCart} />
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
{/* Summary sidebar */}
|
|
90
|
+
<aside className="lg:col-span-1">
|
|
91
|
+
<FreeShippingBar />
|
|
92
|
+
<CartSummary />
|
|
93
|
+
|
|
94
|
+
<Link href="/checkout">{t('proceedToCheckout')}</Link>
|
|
95
|
+
|
|
96
|
+
<Link href="/products">{tc('continueShopping')}</Link>
|
|
97
|
+
</aside>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
{/* Cross-sell recommendations */}
|
|
101
|
+
{cartRecs?.recommendations && cartRecs.recommendations.length > 0 && (
|
|
102
|
+
<CartRecommendationSection title={t('youMightAlsoNeed')} items={cartRecs.recommendations} />
|
|
103
|
+
)}
|
|
104
|
+
</section>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import type { Cart } from 'brainerce';
|
|
5
|
+
import { getClient } from '@/core/lib/brainerce';
|
|
6
|
+
import { useTranslations } from '@/core/lib/translations';
|
|
7
|
+
import { LoadingSpinner } from '@/ui/shared/loading-spinner';
|
|
8
|
+
import { cn } from '@/core/lib/utils';
|
|
9
|
+
|
|
10
|
+
interface CouponInputProps {
|
|
11
|
+
cart: Cart;
|
|
12
|
+
/** When provided, uses applyCheckoutCoupon/removeCheckoutCoupon instead of
|
|
13
|
+
* applyCoupon/removeCoupon. Always pass this when a checkout session exists. */
|
|
14
|
+
checkoutId?: string;
|
|
15
|
+
onUpdate: () => void;
|
|
16
|
+
className?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* DESIGN ME — coupon apply/remove control used on the cart page AND the
|
|
21
|
+
* checkout page (via `checkoutId`); the `cart` prop comes from useCartPage().
|
|
22
|
+
* Keep the applied-coupon state and the error message.
|
|
23
|
+
*/
|
|
24
|
+
export function CouponInput({ cart, checkoutId, onUpdate, className }: CouponInputProps) {
|
|
25
|
+
const t = useTranslations('coupon');
|
|
26
|
+
const tc = useTranslations('common');
|
|
27
|
+
const [code, setCode] = useState('');
|
|
28
|
+
const [applying, setApplying] = useState(false);
|
|
29
|
+
const [removing, setRemoving] = useState(false);
|
|
30
|
+
const [error, setError] = useState<string | null>(null);
|
|
31
|
+
|
|
32
|
+
const appliedCoupon = cart.couponCode || null;
|
|
33
|
+
|
|
34
|
+
async function handleApply() {
|
|
35
|
+
const trimmed = code.trim();
|
|
36
|
+
if (!trimmed || applying) return;
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
setApplying(true);
|
|
40
|
+
setError(null);
|
|
41
|
+
const client = getClient();
|
|
42
|
+
if (checkoutId) {
|
|
43
|
+
await client.applyCheckoutCoupon(checkoutId, trimmed);
|
|
44
|
+
} else {
|
|
45
|
+
await client.applyCoupon(cart.id, trimmed);
|
|
46
|
+
}
|
|
47
|
+
setCode('');
|
|
48
|
+
onUpdate();
|
|
49
|
+
} catch (err) {
|
|
50
|
+
const message = err instanceof Error ? err.message : t('invalidCode');
|
|
51
|
+
setError(message);
|
|
52
|
+
} finally {
|
|
53
|
+
setApplying(false);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function handleRemove() {
|
|
58
|
+
if (removing) return;
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
setRemoving(true);
|
|
62
|
+
setError(null);
|
|
63
|
+
const client = getClient();
|
|
64
|
+
if (checkoutId) {
|
|
65
|
+
await client.removeCheckoutCoupon(checkoutId);
|
|
66
|
+
} else {
|
|
67
|
+
await client.removeCoupon(cart.id);
|
|
68
|
+
}
|
|
69
|
+
onUpdate();
|
|
70
|
+
} catch (err) {
|
|
71
|
+
console.error('Failed to remove coupon:', err);
|
|
72
|
+
} finally {
|
|
73
|
+
setRemoving(false);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Show applied coupon
|
|
78
|
+
if (appliedCoupon) {
|
|
79
|
+
return (
|
|
80
|
+
<div className={cn('flex items-center gap-2', className)}>
|
|
81
|
+
<span>{appliedCoupon}</span>
|
|
82
|
+
<button type="button" onClick={handleRemove} disabled={removing}>
|
|
83
|
+
{removing ? tc('removing') : tc('remove')}
|
|
84
|
+
</button>
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<div className={cn(className)}>
|
|
91
|
+
<div className="flex gap-2">
|
|
92
|
+
<input
|
|
93
|
+
type="text"
|
|
94
|
+
value={code}
|
|
95
|
+
onChange={(e) => {
|
|
96
|
+
setCode(e.target.value);
|
|
97
|
+
if (error) setError(null);
|
|
98
|
+
}}
|
|
99
|
+
onKeyDown={(e) => {
|
|
100
|
+
if (e.key === 'Enter') {
|
|
101
|
+
e.preventDefault();
|
|
102
|
+
handleApply();
|
|
103
|
+
}
|
|
104
|
+
}}
|
|
105
|
+
placeholder={t('placeholder')}
|
|
106
|
+
aria-label={t('placeholder')}
|
|
107
|
+
aria-invalid={error ? true : undefined}
|
|
108
|
+
/>
|
|
109
|
+
<button type="button" onClick={handleApply} disabled={applying || !code.trim()}>
|
|
110
|
+
{applying ? <LoadingSpinner size="sm" /> : tc('apply')}
|
|
111
|
+
</button>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
{error && <p role="alert">{error}</p>}
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { formatPrice } from 'brainerce';
|
|
4
|
+
import { useStoreInfo, useCart } from '@/core/providers/store-provider';
|
|
5
|
+
import { useCurrency } from '@/core/lib/use-currency';
|
|
6
|
+
import { useTranslations } from '@/core/lib/translations';
|
|
7
|
+
import { cn } from '@/core/lib/utils';
|
|
8
|
+
|
|
9
|
+
interface FreeShippingBarProps {
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* DESIGN ME — free-shipping progress on the cart page ("add X more for free
|
|
15
|
+
* shipping"); threshold from useStoreInfo().upsell, subtotal from
|
|
16
|
+
* useCart().totals. The semantic <progress> is a placeholder — build your own
|
|
17
|
+
* meter.
|
|
18
|
+
*/
|
|
19
|
+
export function FreeShippingBar({ className }: FreeShippingBarProps) {
|
|
20
|
+
// Hooks must be called unconditionally and in the same order on every
|
|
21
|
+
// render — keep all of them above any early `return null` branch.
|
|
22
|
+
const t = useTranslations('cart');
|
|
23
|
+
const { storeInfo } = useStoreInfo();
|
|
24
|
+
const { totals } = useCart();
|
|
25
|
+
const currency = useCurrency();
|
|
26
|
+
|
|
27
|
+
const upsell = storeInfo?.upsell;
|
|
28
|
+
const threshold = upsell?.freeShippingThreshold;
|
|
29
|
+
const enabled = upsell?.freeShippingBarEnabled !== false;
|
|
30
|
+
|
|
31
|
+
// Don't render if disabled or no threshold configured
|
|
32
|
+
if (!enabled || !threshold || threshold <= 0) return null;
|
|
33
|
+
|
|
34
|
+
const subtotal = totals.subtotal;
|
|
35
|
+
const remaining = Math.max(0, threshold - subtotal);
|
|
36
|
+
const qualified = remaining <= 0;
|
|
37
|
+
|
|
38
|
+
if (qualified) {
|
|
39
|
+
return <p className={cn(className)}>{t('freeShippingQualified')}</p>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div className={cn(className)}>
|
|
44
|
+
<p>
|
|
45
|
+
{t('freeShippingRemaining', {
|
|
46
|
+
amount: formatPrice(remaining, { currency }) as string,
|
|
47
|
+
})}
|
|
48
|
+
</p>
|
|
49
|
+
<progress value={Math.min(subtotal, threshold)} max={threshold} />
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
4
|
+
import type { ReservationInfo } from 'brainerce';
|
|
5
|
+
import { useTranslations } from '@/core/lib/translations';
|
|
6
|
+
import { cn } from '@/core/lib/utils';
|
|
7
|
+
|
|
8
|
+
interface ReservationCountdownProps {
|
|
9
|
+
reservation: ReservationInfo;
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* DESIGN ME — stock-reservation countdown on the cart/checkout pages; the
|
|
15
|
+
* `reservation` prop comes from useCartPage().cart.reservation. Keep the
|
|
16
|
+
* ticking logic and the expired/urgent states (exposed via data-state).
|
|
17
|
+
*/
|
|
18
|
+
export function ReservationCountdown({ reservation, className }: ReservationCountdownProps) {
|
|
19
|
+
const t = useTranslations('reservation');
|
|
20
|
+
const [remainingSeconds, setRemainingSeconds] = useState<number>(0);
|
|
21
|
+
|
|
22
|
+
const calculateRemaining = useCallback(() => {
|
|
23
|
+
if (!reservation.expiresAt) return 0;
|
|
24
|
+
const expiresAtMs = new Date(reservation.expiresAt).getTime();
|
|
25
|
+
const nowMs = Date.now();
|
|
26
|
+
return Math.max(0, Math.floor((expiresAtMs - nowMs) / 1000));
|
|
27
|
+
}, [reservation.expiresAt]);
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
setRemainingSeconds(calculateRemaining());
|
|
31
|
+
|
|
32
|
+
const interval = setInterval(() => {
|
|
33
|
+
const remaining = calculateRemaining();
|
|
34
|
+
setRemainingSeconds(remaining);
|
|
35
|
+
|
|
36
|
+
if (remaining <= 0) {
|
|
37
|
+
clearInterval(interval);
|
|
38
|
+
}
|
|
39
|
+
}, 1000);
|
|
40
|
+
|
|
41
|
+
return () => clearInterval(interval);
|
|
42
|
+
}, [calculateRemaining]);
|
|
43
|
+
|
|
44
|
+
if (!reservation.hasReservation) return null;
|
|
45
|
+
|
|
46
|
+
const minutes = Math.floor(remainingSeconds / 60);
|
|
47
|
+
const seconds = remainingSeconds % 60;
|
|
48
|
+
const isExpired = remainingSeconds <= 0;
|
|
49
|
+
const isUrgent = remainingSeconds > 0 && remainingSeconds < 120;
|
|
50
|
+
|
|
51
|
+
const displayMessage = reservation.countdownMessage
|
|
52
|
+
? reservation.countdownMessage.replace(
|
|
53
|
+
'{time}',
|
|
54
|
+
`${minutes}:${seconds.toString().padStart(2, '0')}`
|
|
55
|
+
)
|
|
56
|
+
: null;
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div
|
|
60
|
+
role="timer"
|
|
61
|
+
data-state={isExpired ? 'expired' : isUrgent ? 'urgent' : 'active'}
|
|
62
|
+
className={cn(className)}
|
|
63
|
+
>
|
|
64
|
+
{isExpired ? (
|
|
65
|
+
<p>{t('expired')}</p>
|
|
66
|
+
) : displayMessage ? (
|
|
67
|
+
<p>{displayMessage}</p>
|
|
68
|
+
) : (
|
|
69
|
+
<p>
|
|
70
|
+
{isUrgent ? `${t('hurry')} ` : ''}
|
|
71
|
+
{t('reservedFor')}{' '}
|
|
72
|
+
<span>
|
|
73
|
+
{minutes}:{seconds.toString().padStart(2, '0')}
|
|
74
|
+
</span>
|
|
75
|
+
</p>
|
|
76
|
+
)}
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { DiscountBanner } from 'brainerce';
|
|
4
|
+
|
|
5
|
+
/** Store-wide discount banner strip (bare skeleton) shown at the very top of the homepage. */
|
|
6
|
+
export function DiscountBannerStrip({ banners }: { banners: DiscountBanner[] }) {
|
|
7
|
+
if (banners.length === 0) return null;
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<div role="region" aria-label="Promotions">
|
|
11
|
+
{/* DESIGN ME — discount banner strip: active discount-rule texts; banners come from useHomeData() in the parent */}
|
|
12
|
+
<ul className="flex items-center gap-4">
|
|
13
|
+
{banners.map((banner) => (
|
|
14
|
+
<li key={banner.ruleId}>{banner.text}</li>
|
|
15
|
+
))}
|
|
16
|
+
</ul>
|
|
17
|
+
</div>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
|
|
8
|
+
/** Homepage featured-products grid (bare skeleton) with a "view all" link. */
|
|
9
|
+
export function FeaturedProductsSection({ products }: { products: Product[] }) {
|
|
10
|
+
const t = useTranslations('home');
|
|
11
|
+
const tc = useTranslations('common');
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<section>
|
|
15
|
+
{/* DESIGN ME — homepage featured products: heading + view-all link + product grid; products come from useHomeData() in the parent */}
|
|
16
|
+
<div className="flex items-center justify-between">
|
|
17
|
+
<h2>{t('featuredProducts')}</h2>
|
|
18
|
+
<Link href="/products">{tc('viewAll')}</Link>
|
|
19
|
+
</div>
|
|
20
|
+
<ProductGrid products={products} />
|
|
21
|
+
</section>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Link } from '@/core/lib/navigation';
|
|
4
|
+
import { useTranslations } from '@/core/lib/translations';
|
|
5
|
+
|
|
6
|
+
/** Homepage hero (bare skeleton): welcome headline, subtitle, and shop-now CTA. */
|
|
7
|
+
export function HeroSection({ storeName }: { storeName?: string | null }) {
|
|
8
|
+
const t = useTranslations('home');
|
|
9
|
+
const tc = useTranslations('common');
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<section>
|
|
13
|
+
{/* DESIGN ME — homepage hero: headline + subtitle + shop-now CTA; store name via useStoreInfo() in the parent, copy via the `home`/`common` translation namespaces */}
|
|
14
|
+
<h1>
|
|
15
|
+
{t('welcomeTo')} {storeName || tc('store')}
|
|
16
|
+
</h1>
|
|
17
|
+
<p>{t('heroSubtitle')}</p>
|
|
18
|
+
<Link href="/products">{tc('shopNow')}</Link>
|
|
19
|
+
</section>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
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 <LoadingSpinner size="lg" />;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div>
|
|
20
|
+
{/* DESIGN ME — homepage composition: discount banners + hero + featured products; data via useHomeData() and useStoreInfo(). Add/remove/reorder sections freely. */}
|
|
21
|
+
<DiscountBannerStrip banners={banners} />
|
|
22
|
+
<HeroSection storeName={storeInfo?.name} />
|
|
23
|
+
<FeaturedProductsSection products={products} />
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
}
|