create-brainerce-store 1.50.5 → 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.
- package/README.md +72 -0
- package/dist/index.js +146 -100
- package/messages/en.json +36 -1
- package/messages/he.json +36 -1
- package/package.json +1 -1
- package/templates/nextjs/base/.claude/commands/design.md +60 -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 +143 -0
- package/templates/nextjs/base/CLAUDE.md +52 -0
- package/templates/nextjs/base/TRANSLATIONS.md +1 -1
- package/templates/nextjs/base/next.config.ts +2 -2
- package/templates/nextjs/base/package.json.ejs +52 -39
- 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/globals.css +36 -30
- 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 +20 -16
- 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/components/ui/accordion.tsx +53 -0
- package/templates/nextjs/base/src/components/ui/badge.tsx +32 -0
- package/templates/nextjs/base/src/components/ui/button.tsx +49 -0
- package/templates/nextjs/base/src/components/ui/card.tsx +55 -0
- package/templates/nextjs/base/src/components/ui/checkbox.tsx +28 -0
- package/templates/nextjs/base/src/components/ui/dialog.tsx +107 -0
- package/templates/nextjs/base/src/components/ui/input.tsx +22 -0
- package/templates/nextjs/base/src/components/ui/label.tsx +21 -0
- package/templates/nextjs/base/src/components/ui/radio-group.tsx +38 -0
- package/templates/nextjs/base/src/components/ui/select.tsx +153 -0
- package/templates/nextjs/base/src/components/ui/separator.tsx +26 -0
- package/templates/nextjs/base/src/components/ui/sheet.tsx +125 -0
- package/templates/nextjs/base/src/components/ui/skeleton.tsx +7 -0
- package/templates/nextjs/base/src/components/ui/tabs.tsx +55 -0
- package/templates/nextjs/base/src/components/ui/textarea.tsx +21 -0
- package/templates/nextjs/base/src/components/ui/tooltip.tsx +32 -0
- 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 +22 -16
- package/templates/nextjs/base/src/{components → ui}/cart/cart-item.tsx +8 -14
- package/templates/nextjs/base/src/{components → ui}/cart/cart-nudges.tsx +28 -39
- package/templates/nextjs/base/src/{components → ui}/cart/cart-summary.tsx +7 -5
- package/templates/nextjs/base/src/{components → ui}/cart/cart-upgrade-banner.tsx +13 -28
- package/templates/nextjs/base/src/ui/cart/cart-view.tsx +140 -0
- package/templates/nextjs/base/src/{components → ui}/cart/coupon-input.tsx +14 -25
- package/templates/nextjs/base/src/{components → ui}/cart/free-shipping-bar.tsx +6 -7
- package/templates/nextjs/base/src/{components → ui}/cart/reservation-countdown.tsx +95 -103
- 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 +27 -0
- package/templates/nextjs/base/src/ui/home/home-client.tsx +29 -0
- package/templates/nextjs/base/src/{components/content → ui/layout}/announcement-bar.tsx.ejs +116 -125
- package/templates/nextjs/base/src/ui/layout/faq-section.tsx.ejs +90 -0
- package/templates/nextjs/base/src/{components/content → ui/layout}/header-account.tsx +5 -20
- package/templates/nextjs/base/src/ui/layout/language-switcher.tsx.ejs +80 -0
- 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-footer.tsx.ejs +144 -164
- package/templates/nextjs/base/src/{components/content → ui/layout}/site-header.tsx.ejs +152 -182
- package/templates/nextjs/base/src/{components/products → ui/product}/customization-fields.tsx +457 -478
- package/templates/nextjs/base/src/{components/products → ui/product}/discount-badge.tsx +18 -22
- package/templates/nextjs/base/src/{components/products → ui/product}/frequently-bought-together.tsx +21 -33
- package/templates/nextjs/base/src/ui/product/modifier-group-selector.tsx +206 -0
- package/templates/nextjs/base/src/{components/shared → ui/product}/price-display.tsx +5 -4
- package/templates/nextjs/base/src/{components/products → ui/product}/product-card.tsx +238 -271
- package/templates/nextjs/base/src/{app/products/[slug] → ui/product}/product-client-section.tsx +455 -631
- package/templates/nextjs/base/src/{components/products → ui/product}/product-grid.tsx +3 -3
- package/templates/nextjs/base/src/ui/product/product-listing.tsx +446 -0
- package/templates/nextjs/base/src/{components/products → ui/product}/recommendation-section.tsx +102 -107
- package/templates/nextjs/base/src/{components/reviews → ui/product}/review-form.tsx +46 -45
- package/templates/nextjs/base/src/{components/reviews/reviews-section.tsx → ui/product/reviews-section.tsx.ejs} +38 -15
- package/templates/nextjs/base/src/{components/products → ui/product}/stock-badge.tsx +61 -63
- 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 +69 -30
- package/templates/nextjs/designs/atelier/app-overlay/layout.tsx.ejs +251 -0
- package/templates/nextjs/designs/atelier/globals.css +396 -0
- package/templates/nextjs/designs/atelier/messages-patch/en.json +99 -0
- package/templates/nextjs/designs/atelier/messages-patch/he.json +99 -0
- package/templates/nextjs/designs/atelier/ui/cart/cart-bundle-offer.tsx +110 -0
- package/templates/nextjs/designs/atelier/ui/cart/cart-drawer.tsx +163 -0
- package/templates/nextjs/designs/atelier/ui/cart/cart-item.tsx +140 -0
- package/templates/nextjs/designs/atelier/ui/cart/cart-nudges.tsx +34 -0
- package/templates/nextjs/designs/atelier/ui/cart/cart-summary.tsx +107 -0
- package/templates/nextjs/designs/atelier/ui/cart/cart-upgrade-banner.tsx +123 -0
- package/templates/nextjs/designs/atelier/ui/cart/cart-view.tsx +147 -0
- package/templates/nextjs/designs/atelier/ui/cart/coupon-input.tsx +142 -0
- package/templates/nextjs/designs/atelier/ui/cart/free-shipping-bar.tsx +77 -0
- package/templates/nextjs/designs/atelier/ui/cart/reservation-countdown.tsx +89 -0
- package/templates/nextjs/designs/atelier/ui/home/benefits-band.tsx +34 -0
- package/templates/nextjs/designs/atelier/ui/home/category-tiles.tsx +148 -0
- package/templates/nextjs/designs/atelier/ui/home/discount-banner-strip.tsx +27 -0
- package/templates/nextjs/designs/atelier/ui/home/editorial-band.tsx +73 -0
- package/templates/nextjs/designs/atelier/ui/home/featured-products-section.tsx +36 -0
- package/templates/nextjs/designs/atelier/ui/home/hero-section.tsx +148 -0
- package/templates/nextjs/designs/atelier/ui/home/home-client.tsx +54 -0
- package/templates/nextjs/designs/atelier/ui/home/newsletter-section.tsx +78 -0
- package/templates/nextjs/designs/atelier/ui/home/testimonials-band.tsx +46 -0
- package/templates/nextjs/designs/atelier/ui/layout/announcement-bar.tsx +118 -0
- package/templates/nextjs/designs/atelier/ui/layout/faq-section.tsx +96 -0
- package/templates/nextjs/designs/atelier/ui/layout/header-account.tsx +36 -0
- package/templates/nextjs/designs/atelier/ui/layout/header-cart.tsx +36 -0
- package/templates/nextjs/designs/atelier/ui/layout/language-switcher.tsx.ejs +137 -0
- package/templates/nextjs/designs/atelier/ui/layout/nav-url.ts +14 -0
- package/templates/nextjs/designs/atelier/ui/layout/rich-text-block.tsx +34 -0
- package/templates/nextjs/designs/atelier/ui/layout/site-footer.tsx.ejs +141 -0
- package/templates/nextjs/designs/atelier/ui/layout/site-header.tsx.ejs +138 -0
- package/templates/nextjs/designs/atelier/ui/product/customization-fields.tsx +466 -0
- package/templates/nextjs/designs/atelier/ui/product/discount-badge.tsx +17 -0
- package/templates/nextjs/designs/atelier/ui/product/frequently-bought-together.tsx +196 -0
- package/templates/nextjs/designs/atelier/ui/product/modifier-group-selector.tsx +193 -0
- package/templates/nextjs/designs/atelier/ui/product/price-display.tsx +69 -0
- package/templates/nextjs/designs/atelier/ui/product/product-card.tsx +260 -0
- package/templates/nextjs/designs/atelier/ui/product/product-client-section.tsx +471 -0
- package/templates/nextjs/designs/atelier/ui/product/product-grid.tsx +47 -0
- package/templates/nextjs/designs/atelier/ui/product/product-listing.tsx +260 -0
- package/templates/nextjs/designs/atelier/ui/product/recommendation-section.tsx +95 -0
- package/templates/nextjs/designs/atelier/ui/product/review-form.tsx +267 -0
- package/templates/nextjs/designs/atelier/ui/product/reviews-section.tsx.ejs +148 -0
- package/templates/nextjs/designs/atelier/ui/product/stock-badge.tsx +66 -0
- package/templates/nextjs/designs/atelier/ui/product/variant-selector.tsx +206 -0
- package/templates/nextjs/designs/atelier/ui/shared/cdn-image.tsx +29 -0
- package/templates/nextjs/designs/atelier/ui/shared/fly-to-cart.ts +86 -0
- package/templates/nextjs/designs/atelier/ui/shared/icons.tsx +269 -0
- package/templates/nextjs/designs/atelier/ui/shared/loading-spinner.tsx +64 -0
- package/templates/nextjs/designs/atelier/ui/shared/reveal.tsx +71 -0
- package/templates/nextjs/designs/atelier/ui/shared/select-menu.tsx +125 -0
- package/templates/nextjs/themes/luxury/globals.css +405 -399
- package/templates/nextjs/themes/minimal/globals.css +36 -30
- package/templates/nextjs/themes/playful/globals.css +406 -400
- package/templates/nextjs/ui-canvas/cart/cart-bundle-offer.tsx +89 -0
- package/templates/nextjs/ui-canvas/cart/cart-item.tsx +123 -0
- package/templates/nextjs/ui-canvas/cart/cart-nudges.tsx +27 -0
- package/templates/nextjs/ui-canvas/cart/cart-summary.tsx +99 -0
- package/templates/nextjs/ui-canvas/cart/cart-upgrade-banner.tsx +105 -0
- package/templates/nextjs/ui-canvas/cart/cart-view.tsx +106 -0
- package/templates/nextjs/ui-canvas/cart/coupon-input.tsx +119 -0
- package/templates/nextjs/ui-canvas/cart/free-shipping-bar.tsx +54 -0
- package/templates/nextjs/ui-canvas/cart/reservation-countdown.tsx +81 -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/{base/src/components → ui-canvas}/layout/language-switcher.tsx.ejs +76 -70
- package/templates/nextjs/ui-canvas/layout/rich-text-block.tsx.ejs +36 -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 +445 -0
- package/templates/nextjs/ui-canvas/product/discount-badge.tsx +19 -0
- package/templates/nextjs/ui-canvas/product/frequently-bought-together.tsx +168 -0
- package/templates/nextjs/ui-canvas/product/modifier-group-selector.tsx +165 -0
- package/templates/nextjs/ui-canvas/product/price-display.tsx +52 -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 +99 -0
- package/templates/nextjs/ui-canvas/product/review-form.tsx +266 -0
- package/templates/nextjs/ui-canvas/product/reviews-section.tsx.ejs +96 -0
- package/templates/nextjs/ui-canvas/product/stock-badge.tsx +45 -0
- package/templates/nextjs/ui-canvas/product/variant-selector.tsx +212 -0
- package/templates/nextjs/ui-canvas/shared/cdn-image.tsx +31 -0
- package/templates/nextjs/ui-canvas/shared/loading-spinner.tsx +27 -0
- package/templates/nextjs/base/src/app/home-client.tsx +0 -98
- package/templates/nextjs/base/src/components/content/faq-section.tsx.ejs +0 -103
- package/templates/nextjs/base/src/components/products/modifier-group-selector.tsx +0 -238
- /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
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
{
|
|
2
|
+
"common": {
|
|
3
|
+
"close": "Close"
|
|
4
|
+
},
|
|
5
|
+
"nav": {
|
|
6
|
+
"cart": "Cart",
|
|
7
|
+
"home": "Home"
|
|
8
|
+
},
|
|
9
|
+
"home": {
|
|
10
|
+
"heroEyebrow": "New collection · Handcrafted",
|
|
11
|
+
"heroTitle": "Jewelry that tells your story",
|
|
12
|
+
"heroLead": "Delicate gold and silver designs, handcrafted with love — made to be with you through the small moments and the big ones.",
|
|
13
|
+
"heroCtaSecondary": "Browse categories",
|
|
14
|
+
"heroTrust": "Happy customers nationwide",
|
|
15
|
+
"benefit1Title": "Fast home delivery",
|
|
16
|
+
"benefit1Desc": "Express courier nationwide, gift-wrapped",
|
|
17
|
+
"benefit2Title": "Secure payment",
|
|
18
|
+
"benefit2Desc": "Encrypted checkout, highest standard",
|
|
19
|
+
"benefit3Title": "Quality materials",
|
|
20
|
+
"benefit3Desc": "Gold, 925 silver and genuine stones",
|
|
21
|
+
"benefit4Title": "Easy returns",
|
|
22
|
+
"benefit4Desc": "30 days to exchange or return, no questions",
|
|
23
|
+
"featuredEyebrow": "Our picks",
|
|
24
|
+
"featuredSubtitle": "The pieces our customers love most — hand-picked from the new collection.",
|
|
25
|
+
"categoriesEyebrow": "Categories",
|
|
26
|
+
"categoriesTitle": "Find the perfect piece",
|
|
27
|
+
"categoriesSubtitle": "From delicate rings to statement necklaces — each one speaks its own language.",
|
|
28
|
+
"catRings": "Rings",
|
|
29
|
+
"catNecklaces": "Necklaces",
|
|
30
|
+
"catEarrings": "Earrings",
|
|
31
|
+
"catBracelets": "Bracelets",
|
|
32
|
+
"catCta": "Shop the category",
|
|
33
|
+
"storyEyebrow": "Our studio",
|
|
34
|
+
"storyTitle": "Made by hand, worn by heart",
|
|
35
|
+
"storyP1": "Every piece begins as a small sketch on paper — and ends as delicate handwork in our studio. We choose every stone and every link with care, so the piece that reaches you is exactly as we imagined it.",
|
|
36
|
+
"storyP2": "We believe in jewelry that lasts: real materials, meticulous finish and full warranty on every piece. That is what love for the small details looks like.",
|
|
37
|
+
"storyCta": "Discover the collection",
|
|
38
|
+
"storyStat1Value": "Handmade",
|
|
39
|
+
"storyStat1Label": "Every piece is made in our studio",
|
|
40
|
+
"storyStat2Value": "925 Silver",
|
|
41
|
+
"storyStat2Label": "Genuine materials only",
|
|
42
|
+
"testimonialsEyebrow": "Customer stories",
|
|
43
|
+
"testimonialsTitle": "They already fell in love",
|
|
44
|
+
"testimonial1Quote": "The necklace arrived in gorgeous packaging and looks even better than the photos. I got compliments on day one.",
|
|
45
|
+
"testimonial1Name": "Noa L.",
|
|
46
|
+
"testimonial1Meta": "Tel Aviv",
|
|
47
|
+
"testimonial2Quote": "I bought earrings as a gift for my mom and she has not taken them off since. Amazing quality and truly personal service.",
|
|
48
|
+
"testimonial2Name": "Shira B.",
|
|
49
|
+
"testimonial2Meta": "Haifa",
|
|
50
|
+
"testimonial3Quote": "The ring fits perfectly, the work is delicate and precise. You can feel every detail was made with love. Highly recommend.",
|
|
51
|
+
"testimonial3Name": "Dana K.",
|
|
52
|
+
"testimonial3Meta": "Jerusalem",
|
|
53
|
+
"newsletterTitle": "Join our club",
|
|
54
|
+
"newsletterSubtitle": "Sign up to hear first about new collections, sales and member perks.",
|
|
55
|
+
"newsletterPlaceholder": "Your email address",
|
|
56
|
+
"newsletterCta": "Subscribe",
|
|
57
|
+
"newsletterSuccess": "You are in! We promise to send only beautiful things.",
|
|
58
|
+
"newsletterPrivacy": "No spam. Unsubscribe anytime."
|
|
59
|
+
},
|
|
60
|
+
"products": {
|
|
61
|
+
"listingSubtitle": "Our full collection — handmade, genuine materials and love for detail.",
|
|
62
|
+
"filtersLabel": "Filter",
|
|
63
|
+
"emptyTitle": "No results found",
|
|
64
|
+
"emptyBody": "Try clearing the filters or searching for something else.",
|
|
65
|
+
"clearFilters": "Clear filters"
|
|
66
|
+
},
|
|
67
|
+
"productDetail": {
|
|
68
|
+
"quantityLabel": "Quantity",
|
|
69
|
+
"shippingNote": "Fast home delivery · Easy 30-day returns",
|
|
70
|
+
"guaranteeNote": "Full warranty on every piece",
|
|
71
|
+
"pickOne": "Pick one",
|
|
72
|
+
"upTo": "Up to {max}",
|
|
73
|
+
"pickExactly": "Pick exactly {min}",
|
|
74
|
+
"atLeast": "At least {min}",
|
|
75
|
+
"pickRange": "{min}–{max}",
|
|
76
|
+
"soldOut": "Sold out",
|
|
77
|
+
"freeCounter": "{used} of {free} free"
|
|
78
|
+
},
|
|
79
|
+
"reviews": {
|
|
80
|
+
"beFirst": "No reviews for this product yet"
|
|
81
|
+
},
|
|
82
|
+
"cart": {
|
|
83
|
+
"itemsInCart": "Your items",
|
|
84
|
+
"secureCheckoutNote": "Secure, encrypted checkout",
|
|
85
|
+
"couponTitle": "Have a coupon code?",
|
|
86
|
+
"viewCart": "View cart",
|
|
87
|
+
"shippingAtCheckout": "Shipping calculated at checkout"
|
|
88
|
+
},
|
|
89
|
+
"footer": {
|
|
90
|
+
"tagline": "Handcrafted jewelry, designed with love in Israel.",
|
|
91
|
+
"followUs": "Follow us",
|
|
92
|
+
"quickLinksTitle": "Quick links",
|
|
93
|
+
"linkProducts": "All products",
|
|
94
|
+
"linkContact": "Contact",
|
|
95
|
+
"linkFaq": "FAQ",
|
|
96
|
+
"linkCart": "Shopping cart",
|
|
97
|
+
"securePayments": "Secure, encrypted payment"
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
{
|
|
2
|
+
"common": {
|
|
3
|
+
"close": "סגור"
|
|
4
|
+
},
|
|
5
|
+
"nav": {
|
|
6
|
+
"cart": "עגלה",
|
|
7
|
+
"home": "דף הבית"
|
|
8
|
+
},
|
|
9
|
+
"home": {
|
|
10
|
+
"heroEyebrow": "קולקציה חדשה · עבודת יד",
|
|
11
|
+
"heroTitle": "תכשיטים שמספרים את הסיפור שלך",
|
|
12
|
+
"heroLead": "עיצובים עדינים מזהב ומכסף, בעבודת יד ובאהבה — נוצרו ללוות אתכן ברגעים הקטנים והגדולים.",
|
|
13
|
+
"heroCtaSecondary": "לצפייה בקטגוריות",
|
|
14
|
+
"heroTrust": "לקוחות מרוצות ברחבי הארץ",
|
|
15
|
+
"benefit1Title": "משלוח מהיר עד הבית",
|
|
16
|
+
"benefit1Desc": "שליחות מהירה לכל הארץ באריזת מתנה",
|
|
17
|
+
"benefit2Title": "תשלום מאובטח",
|
|
18
|
+
"benefit2Desc": "סליקה מוצפנת בתקן המחמיר ביותר",
|
|
19
|
+
"benefit3Title": "חומרים איכותיים",
|
|
20
|
+
"benefit3Desc": "זהב, כסף 925 ואבני חן אמיתיות",
|
|
21
|
+
"benefit4Title": "החזרה קלה",
|
|
22
|
+
"benefit4Desc": "30 יום להחלפה או החזרה, בלי שאלות",
|
|
23
|
+
"featuredEyebrow": "הנבחרים שלנו",
|
|
24
|
+
"featuredSubtitle": "הפריטים שהלקוחות שלנו הכי אוהבות — נבחרו בקפידה מהקולקציה החדשה.",
|
|
25
|
+
"categoriesEyebrow": "קטגוריות",
|
|
26
|
+
"categoriesTitle": "מצאו את התכשיט המושלם",
|
|
27
|
+
"categoriesSubtitle": "מטבעות עדינות ועד שרשראות בולטות — לכל אחת יש את השפה שלה.",
|
|
28
|
+
"catRings": "טבעות",
|
|
29
|
+
"catNecklaces": "שרשראות",
|
|
30
|
+
"catEarrings": "עגילים",
|
|
31
|
+
"catBracelets": "צמידים",
|
|
32
|
+
"catCta": "לצפייה בקטגוריה",
|
|
33
|
+
"storyEyebrow": "הסטודיו שלנו",
|
|
34
|
+
"storyTitle": "נוצר ביד, נלבש בלב",
|
|
35
|
+
"storyP1": "כל תכשיט אצלנו מתחיל בסקיצה קטנה על נייר — ונגמר בעבודת יד עדינה בסטודיו. אנחנו בוחרים כל אבן וכל חוליה בקפידה, כדי שהתכשיט שיגיע אליכן יהיה בדיוק כמו שדמיינו אותו.",
|
|
36
|
+
"storyP2": "אנחנו מאמינים בתכשיטים שנשארים: חומרים אמיתיים, גימור מוקפד ואחריות מלאה על כל פריט. ככה נראית אהבה לפרטים הקטנים.",
|
|
37
|
+
"storyCta": "לגלות את הקולקציה",
|
|
38
|
+
"storyStat1Value": "עבודת יד",
|
|
39
|
+
"storyStat1Label": "כל פריט נוצר בסטודיו שלנו",
|
|
40
|
+
"storyStat2Value": "כסף 925",
|
|
41
|
+
"storyStat2Label": "חומרים אמיתיים בלבד",
|
|
42
|
+
"testimonialsEyebrow": "לקוחות מספרות",
|
|
43
|
+
"testimonialsTitle": "הן כבר התאהבו",
|
|
44
|
+
"testimonial1Quote": "השרשרת הגיעה באריזה מהממת ונראית אפילו יותר טוב מבתמונות. קיבלתי מחמאות כבר ביום הראשון.",
|
|
45
|
+
"testimonial1Name": "נועה ל.",
|
|
46
|
+
"testimonial1Meta": "תל אביב",
|
|
47
|
+
"testimonial2Quote": "קניתי עגילים במתנה לאמא שלי והיא לא מורידה אותם מאז. איכות מדהימה ושירות סופר אישי.",
|
|
48
|
+
"testimonial2Name": "שירה ב.",
|
|
49
|
+
"testimonial2Meta": "חיפה",
|
|
50
|
+
"testimonial3Quote": "הטבעת בדיוק במידה, העבודה עדינה ומוקפדת. מרגישים שכל פרט נעשה באהבה. ממליצה בחום.",
|
|
51
|
+
"testimonial3Name": "דנה כ.",
|
|
52
|
+
"testimonial3Meta": "ירושלים",
|
|
53
|
+
"newsletterTitle": "הצטרפו למועדון שלנו",
|
|
54
|
+
"newsletterSubtitle": "הרשמו וקבלו עדכון ראשונות על קולקציות חדשות, מבצעים והטבות לחברות מועדון.",
|
|
55
|
+
"newsletterPlaceholder": "כתובת האימייל שלך",
|
|
56
|
+
"newsletterCta": "הרשמה",
|
|
57
|
+
"newsletterSuccess": "נרשמת בהצלחה! מבטיחים לשלוח רק דברים יפים.",
|
|
58
|
+
"newsletterPrivacy": "בלי ספאם. אפשר להסיר את ההרשמה בכל רגע."
|
|
59
|
+
},
|
|
60
|
+
"products": {
|
|
61
|
+
"listingSubtitle": "הקולקציה המלאה שלנו — עבודת יד, חומרים אמיתיים ואהבה לפרטים.",
|
|
62
|
+
"filtersLabel": "סינון",
|
|
63
|
+
"emptyTitle": "לא מצאנו תוצאות",
|
|
64
|
+
"emptyBody": "נסו לנקות את הסינון או לחפש משהו אחר.",
|
|
65
|
+
"clearFilters": "ניקוי סינון"
|
|
66
|
+
},
|
|
67
|
+
"productDetail": {
|
|
68
|
+
"quantityLabel": "כמות",
|
|
69
|
+
"shippingNote": "משלוח מהיר עד הבית · החזרה קלה עד 30 יום",
|
|
70
|
+
"guaranteeNote": "אחריות מלאה על כל תכשיט",
|
|
71
|
+
"pickOne": "בחרו אחד",
|
|
72
|
+
"upTo": "עד {max}",
|
|
73
|
+
"pickExactly": "בחרו בדיוק {min}",
|
|
74
|
+
"atLeast": "לפחות {min}",
|
|
75
|
+
"pickRange": "{min}–{max}",
|
|
76
|
+
"soldOut": "אזל",
|
|
77
|
+
"freeCounter": "{used} מתוך {free} חינם"
|
|
78
|
+
},
|
|
79
|
+
"reviews": {
|
|
80
|
+
"beFirst": "עדיין אין ביקורות למוצר הזה"
|
|
81
|
+
},
|
|
82
|
+
"cart": {
|
|
83
|
+
"itemsInCart": "הפריטים שלך",
|
|
84
|
+
"secureCheckoutNote": "תשלום מאובטח ומוצפן",
|
|
85
|
+
"couponTitle": "יש לכם קוד קופון?",
|
|
86
|
+
"viewCart": "לצפייה בעגלה",
|
|
87
|
+
"shippingAtCheckout": "משלוח יחושב בקופה"
|
|
88
|
+
},
|
|
89
|
+
"footer": {
|
|
90
|
+
"tagline": "תכשיטים בעבודת יד, מעוצבים באהבה בישראל.",
|
|
91
|
+
"followUs": "עקבו אחרינו",
|
|
92
|
+
"quickLinksTitle": "ניווט מהיר",
|
|
93
|
+
"linkProducts": "כל המוצרים",
|
|
94
|
+
"linkContact": "צור קשר",
|
|
95
|
+
"linkFaq": "שאלות נפוצות",
|
|
96
|
+
"linkCart": "עגלת קניות",
|
|
97
|
+
"securePayments": "תשלום מאובטח ומוצפן"
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { CdnImage as Image } from '@/ui/shared/cdn-image';
|
|
5
|
+
import type { CartBundleOffer as CartBundleOfferType } from 'brainerce';
|
|
6
|
+
import { formatPrice } from 'brainerce';
|
|
7
|
+
import { useCurrency } from '@/core/lib/use-currency';
|
|
8
|
+
import { useTranslations } from '@/core/lib/translations';
|
|
9
|
+
import { cn } from '@/core/lib/utils';
|
|
10
|
+
import { IconBag } from '@/ui/shared/icons';
|
|
11
|
+
|
|
12
|
+
interface CartBundleOfferCardProps {
|
|
13
|
+
offer: CartBundleOfferType;
|
|
14
|
+
cartId: string;
|
|
15
|
+
onAdd: () => void;
|
|
16
|
+
className?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Bundle offer card on the cart page: offered products with original vs.
|
|
21
|
+
* discounted prices + add-bundle button; offers come from
|
|
22
|
+
* useCartPage().bundles.
|
|
23
|
+
*/
|
|
24
|
+
export function CartBundleOfferCard({ offer, cartId, onAdd, className }: CartBundleOfferCardProps) {
|
|
25
|
+
const t = useTranslations('cart');
|
|
26
|
+
const currency = useCurrency();
|
|
27
|
+
const [adding, setAdding] = useState(false);
|
|
28
|
+
|
|
29
|
+
const offered = offer.offeredProducts;
|
|
30
|
+
|
|
31
|
+
const totalOriginal = parseFloat(offer.totalOriginalPrice);
|
|
32
|
+
const totalDiscounted = parseFloat(offer.totalDiscountedPrice);
|
|
33
|
+
const discountLabel =
|
|
34
|
+
offer.discountType === 'PERCENTAGE'
|
|
35
|
+
? `${offer.discountValue}%`
|
|
36
|
+
: (formatPrice(parseFloat(offer.discountValue), { currency }) as string);
|
|
37
|
+
|
|
38
|
+
async function handleAdd() {
|
|
39
|
+
if (adding) return;
|
|
40
|
+
try {
|
|
41
|
+
setAdding(true);
|
|
42
|
+
const { getClient } = await import('@/core/lib/brainerce');
|
|
43
|
+
const client = getClient();
|
|
44
|
+
// No variant selections in the default template — products with
|
|
45
|
+
// variants are best handled with a dedicated picker per offered item.
|
|
46
|
+
await client.addBundleToCart(cartId, offer.id);
|
|
47
|
+
onAdd();
|
|
48
|
+
} catch (err) {
|
|
49
|
+
console.error('Failed to add bundle:', err);
|
|
50
|
+
} finally {
|
|
51
|
+
setAdding(false);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<article className={cn('card p-5', className)}>
|
|
57
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
58
|
+
<h4 className="font-sans text-base font-semibold">{offer.name}</h4>
|
|
59
|
+
<span className="badge-primary">-{discountLabel}</span>
|
|
60
|
+
</div>
|
|
61
|
+
{offer.description && (
|
|
62
|
+
<p className="mt-1 text-sm text-muted-foreground">{offer.description}</p>
|
|
63
|
+
)}
|
|
64
|
+
|
|
65
|
+
<ul className="mt-4 space-y-2.5">
|
|
66
|
+
{offered.map((p) => {
|
|
67
|
+
const firstImage = p.images?.[0];
|
|
68
|
+
const imageUrl = firstImage?.url ?? null;
|
|
69
|
+
return (
|
|
70
|
+
<li key={p.id} className="flex items-center gap-3">
|
|
71
|
+
{/* `relative` + fixed box are layout-critical for next/image fill */}
|
|
72
|
+
<span className="relative block h-12 w-12 shrink-0 overflow-hidden rounded-lg border bg-secondary">
|
|
73
|
+
{imageUrl ? (
|
|
74
|
+
<Image src={imageUrl} alt={p.name} fill sizes="48px" className="object-cover" />
|
|
75
|
+
) : null}
|
|
76
|
+
</span>
|
|
77
|
+
<span className="min-w-0 flex-1 text-sm font-medium text-foreground">{p.name}</span>
|
|
78
|
+
<s className="text-xs text-muted-foreground">
|
|
79
|
+
{formatPrice(parseFloat(p.originalPrice), { currency }) as string}
|
|
80
|
+
</s>
|
|
81
|
+
<span className="text-sm font-semibold text-foreground">
|
|
82
|
+
{formatPrice(parseFloat(p.discountedPrice), { currency }) as string}
|
|
83
|
+
</span>
|
|
84
|
+
</li>
|
|
85
|
+
);
|
|
86
|
+
})}
|
|
87
|
+
</ul>
|
|
88
|
+
|
|
89
|
+
<div className="mt-4 flex flex-wrap items-center gap-3 border-t pt-4">
|
|
90
|
+
<span className="inline-flex items-baseline gap-2">
|
|
91
|
+
<s className="text-sm text-muted-foreground">
|
|
92
|
+
{formatPrice(totalOriginal, { currency }) as string}
|
|
93
|
+
</s>
|
|
94
|
+
<span className="text-lg font-bold text-foreground">
|
|
95
|
+
{formatPrice(totalDiscounted, { currency }) as string}
|
|
96
|
+
</span>
|
|
97
|
+
</span>
|
|
98
|
+
<button
|
|
99
|
+
type="button"
|
|
100
|
+
onClick={handleAdd}
|
|
101
|
+
disabled={adding}
|
|
102
|
+
className="btn-primary btn-sm ms-auto"
|
|
103
|
+
>
|
|
104
|
+
<IconBag size={16} />
|
|
105
|
+
{adding ? t('addingBundle') : t('addBundleItem')}
|
|
106
|
+
</button>
|
|
107
|
+
</div>
|
|
108
|
+
</article>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Mini-cart drawer — a slide-over sheet anchored to the header cart side
|
|
5
|
+
* (logical `end`, so it hugs the cart icon in both RTL and LTR).
|
|
6
|
+
*
|
|
7
|
+
* Opens via `openCartDrawer()` (fired after a successful add-to-cart);
|
|
8
|
+
* the header cart icon itself still navigates to /cart — the drawer never
|
|
9
|
+
* hijacks that. Closes on overlay click, Escape, the X, or any CTA.
|
|
10
|
+
*
|
|
11
|
+
* Stays mounted so the slide transition runs both ways; when closed it is
|
|
12
|
+
* `inert` + aria-hidden so nothing inside is focusable.
|
|
13
|
+
*/
|
|
14
|
+
import { useEffect, useRef, useState } from 'react';
|
|
15
|
+
import { Link } from '@/core/lib/navigation';
|
|
16
|
+
import { formatPrice } from 'brainerce';
|
|
17
|
+
import { useCart } from '@/core/providers/store-provider';
|
|
18
|
+
import { useTranslations } from '@/core/lib/translations';
|
|
19
|
+
import { useCurrency } from '@/core/lib/use-currency';
|
|
20
|
+
import { CartItem } from '@/ui/cart/cart-item';
|
|
21
|
+
import { FreeShippingBar } from '@/ui/cart/free-shipping-bar';
|
|
22
|
+
import { cn } from '@/core/lib/utils';
|
|
23
|
+
import { IconBag, IconX } from '@/ui/shared/icons';
|
|
24
|
+
|
|
25
|
+
const OPEN_EVENT = 'cart-drawer:open';
|
|
26
|
+
|
|
27
|
+
/** Ask the mounted drawer to slide open (no-op during SSR). */
|
|
28
|
+
export function openCartDrawer() {
|
|
29
|
+
if (typeof window !== 'undefined') {
|
|
30
|
+
window.dispatchEvent(new Event(OPEN_EVENT));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function CartDrawer() {
|
|
35
|
+
const [open, setOpen] = useState(false);
|
|
36
|
+
const { cart, itemCount, totals, refreshCart } = useCart();
|
|
37
|
+
const t = useTranslations('cart');
|
|
38
|
+
const tc = useTranslations('common');
|
|
39
|
+
const currency = useCurrency();
|
|
40
|
+
const closeRef = useRef<HTMLButtonElement>(null);
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
const onOpen = () => setOpen(true);
|
|
44
|
+
window.addEventListener(OPEN_EVENT, onOpen);
|
|
45
|
+
return () => window.removeEventListener(OPEN_EVENT, onOpen);
|
|
46
|
+
}, []);
|
|
47
|
+
|
|
48
|
+
// Open state side-effects: Escape to close, scroll lock, focus the X.
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (!open) return;
|
|
51
|
+
const onKey = (e: KeyboardEvent) => {
|
|
52
|
+
if (e.key === 'Escape') setOpen(false);
|
|
53
|
+
};
|
|
54
|
+
document.addEventListener('keydown', onKey);
|
|
55
|
+
const prevOverflow = document.body.style.overflow;
|
|
56
|
+
document.body.style.overflow = 'hidden';
|
|
57
|
+
closeRef.current?.focus();
|
|
58
|
+
return () => {
|
|
59
|
+
document.removeEventListener('keydown', onKey);
|
|
60
|
+
document.body.style.overflow = prevOverflow;
|
|
61
|
+
};
|
|
62
|
+
}, [open]);
|
|
63
|
+
|
|
64
|
+
const close = () => setOpen(false);
|
|
65
|
+
const items = cart?.items ?? [];
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div
|
|
69
|
+
aria-hidden={!open}
|
|
70
|
+
inert={!open}
|
|
71
|
+
className={cn('fixed inset-0 z-50', !open && 'pointer-events-none')}
|
|
72
|
+
>
|
|
73
|
+
{/* Overlay */}
|
|
74
|
+
<div
|
|
75
|
+
onClick={close}
|
|
76
|
+
aria-hidden="true"
|
|
77
|
+
className={cn(
|
|
78
|
+
'absolute inset-0 bg-foreground/35 backdrop-blur-[2px] transition-opacity duration-300 motion-reduce:transition-none',
|
|
79
|
+
open ? 'opacity-100' : 'opacity-0'
|
|
80
|
+
)}
|
|
81
|
+
/>
|
|
82
|
+
|
|
83
|
+
{/* Panel — anchored at the logical end (same side as the cart icon) */}
|
|
84
|
+
<aside
|
|
85
|
+
role="dialog"
|
|
86
|
+
aria-modal="true"
|
|
87
|
+
aria-label={t('title')}
|
|
88
|
+
className={cn(
|
|
89
|
+
'absolute inset-y-0 end-0 flex w-full max-w-md flex-col bg-background shadow-2xl transition-transform duration-300 ease-out motion-reduce:transition-none sm:rounded-s-3xl sm:border-s',
|
|
90
|
+
open ? 'translate-x-0' : 'ltr:translate-x-full rtl:-translate-x-full'
|
|
91
|
+
)}
|
|
92
|
+
>
|
|
93
|
+
<header className="flex items-center gap-3 border-b px-5 py-4">
|
|
94
|
+
<h2 className="font-display text-xl font-semibold text-foreground">{t('title')}</h2>
|
|
95
|
+
{itemCount > 0 && (
|
|
96
|
+
<span className="flex h-6 min-w-6 items-center justify-center rounded-full bg-primary px-1.5 text-xs font-bold text-primary-foreground">
|
|
97
|
+
{itemCount}
|
|
98
|
+
</span>
|
|
99
|
+
)}
|
|
100
|
+
<button
|
|
101
|
+
ref={closeRef}
|
|
102
|
+
type="button"
|
|
103
|
+
onClick={close}
|
|
104
|
+
aria-label={tc('close')}
|
|
105
|
+
className="ms-auto flex h-10 w-10 items-center justify-center rounded-full text-foreground transition-colors hover:bg-secondary"
|
|
106
|
+
>
|
|
107
|
+
<IconX size={20} />
|
|
108
|
+
</button>
|
|
109
|
+
</header>
|
|
110
|
+
|
|
111
|
+
{items.length === 0 ? (
|
|
112
|
+
<div className="flex flex-1 flex-col items-center justify-center gap-3 px-6 text-center">
|
|
113
|
+
<span className="flex h-16 w-16 items-center justify-center rounded-full bg-secondary text-muted-foreground">
|
|
114
|
+
<IconBag size={24} />
|
|
115
|
+
</span>
|
|
116
|
+
<p className="font-display text-lg font-semibold text-foreground">{t('emptyTitle')}</p>
|
|
117
|
+
<p className="max-w-xs text-sm text-muted-foreground">{t('emptySubtitle')}</p>
|
|
118
|
+
<Link href="/products" onClick={close} className="btn-primary btn-sm mt-2">
|
|
119
|
+
{tc('continueShopping')}
|
|
120
|
+
</Link>
|
|
121
|
+
</div>
|
|
122
|
+
) : (
|
|
123
|
+
<>
|
|
124
|
+
<div className="flex-1 overflow-y-auto px-5 py-4">
|
|
125
|
+
<FreeShippingBar className="mb-4" />
|
|
126
|
+
<ul className="divide-y">
|
|
127
|
+
{items.map((item) => (
|
|
128
|
+
<li key={item.id} className="py-4 first:pt-0">
|
|
129
|
+
<CartItem item={item} onUpdate={refreshCart} />
|
|
130
|
+
</li>
|
|
131
|
+
))}
|
|
132
|
+
</ul>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
<footer className="border-t bg-secondary/40 px-5 py-4">
|
|
136
|
+
<div className="flex items-center justify-between text-sm">
|
|
137
|
+
<span className="text-muted-foreground">{tc('subtotal')}</span>
|
|
138
|
+
<strong className="text-base text-foreground">
|
|
139
|
+
{formatPrice(totals.subtotal, { currency }) as string}
|
|
140
|
+
</strong>
|
|
141
|
+
</div>
|
|
142
|
+
{totals.discount > 0 && (
|
|
143
|
+
<div className="mt-1 flex items-center justify-between text-sm text-primary">
|
|
144
|
+
<span>{tc('discount')}</span>
|
|
145
|
+
<span>-{formatPrice(totals.discount, { currency }) as string}</span>
|
|
146
|
+
</div>
|
|
147
|
+
)}
|
|
148
|
+
<p className="mt-1 text-xs text-muted-foreground">{t('shippingAtCheckout')}</p>
|
|
149
|
+
<div className="mt-4 grid gap-2">
|
|
150
|
+
<Link href="/checkout" onClick={close} className="btn-primary w-full">
|
|
151
|
+
{t('proceedToCheckout')}
|
|
152
|
+
</Link>
|
|
153
|
+
<Link href="/cart" onClick={close} className="btn-outline w-full">
|
|
154
|
+
{t('viewCart')}
|
|
155
|
+
</Link>
|
|
156
|
+
</div>
|
|
157
|
+
</footer>
|
|
158
|
+
</>
|
|
159
|
+
)}
|
|
160
|
+
</aside>
|
|
161
|
+
</div>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { CdnImage as Image } from '@/ui/shared/cdn-image';
|
|
5
|
+
import type { CartItem as CartItemType } from 'brainerce';
|
|
6
|
+
import { getCartItemImage, formatPrice } from 'brainerce';
|
|
7
|
+
import { getClient } from '@/core/lib/brainerce';
|
|
8
|
+
import { useTranslations } from '@/core/lib/translations';
|
|
9
|
+
import { useCurrency } from '@/core/lib/use-currency';
|
|
10
|
+
import { LoadingSpinner } from '@/ui/shared/loading-spinner';
|
|
11
|
+
import { cn } from '@/core/lib/utils';
|
|
12
|
+
import { IconMinus, IconPlus, IconTrash } from '@/ui/shared/icons';
|
|
13
|
+
|
|
14
|
+
interface CartItemProps {
|
|
15
|
+
item: CartItemType;
|
|
16
|
+
onUpdate: () => void;
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Single cart line: image, name, variant, unit price, quantity controls,
|
|
22
|
+
* remove, line total; the `item` prop comes from useCartPage().cart.
|
|
23
|
+
*/
|
|
24
|
+
export function CartItem({ item, onUpdate, className }: CartItemProps) {
|
|
25
|
+
const t = useTranslations('common');
|
|
26
|
+
const td = useTranslations('productDetail');
|
|
27
|
+
const currency = useCurrency();
|
|
28
|
+
const [updating, setUpdating] = useState(false);
|
|
29
|
+
const [removing, setRemoving] = useState(false);
|
|
30
|
+
|
|
31
|
+
const productName = item.product.name;
|
|
32
|
+
const imageUrl = getCartItemImage(item);
|
|
33
|
+
const variantName = item.variant?.name;
|
|
34
|
+
const unitPrice = parseFloat(item.unitPrice);
|
|
35
|
+
const lineTotal = unitPrice * item.quantity;
|
|
36
|
+
|
|
37
|
+
async function handleQuantityChange(newQuantity: number) {
|
|
38
|
+
if (newQuantity < 1 || updating) return;
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
setUpdating(true);
|
|
42
|
+
const client = getClient();
|
|
43
|
+
await client.smartUpdateCartItem(item.productId, newQuantity, item.variantId || undefined);
|
|
44
|
+
onUpdate();
|
|
45
|
+
} catch (err) {
|
|
46
|
+
console.error('Failed to update quantity:', err);
|
|
47
|
+
} finally {
|
|
48
|
+
setUpdating(false);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function handleRemove() {
|
|
53
|
+
if (removing) return;
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
setRemoving(true);
|
|
57
|
+
const client = getClient();
|
|
58
|
+
await client.smartRemoveFromCart(item.productId, item.variantId || undefined);
|
|
59
|
+
onUpdate();
|
|
60
|
+
} catch (err) {
|
|
61
|
+
console.error('Failed to remove item:', err);
|
|
62
|
+
} finally {
|
|
63
|
+
setRemoving(false);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div className={cn('flex items-start gap-4', className)}>
|
|
69
|
+
{/* Image — `relative` + fixed box are layout-critical for next/image fill */}
|
|
70
|
+
<div className="relative h-20 w-20 shrink-0 overflow-hidden rounded-lg border bg-secondary sm:h-24 sm:w-24">
|
|
71
|
+
{imageUrl ? (
|
|
72
|
+
<Image src={imageUrl} alt={productName} fill sizes="96px" className="object-cover" />
|
|
73
|
+
) : (
|
|
74
|
+
<span className="sr-only">{productName}</span>
|
|
75
|
+
)}
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
{/* Details */}
|
|
79
|
+
<div className="min-w-0 flex-1">
|
|
80
|
+
<h3 className="font-sans text-[15px] font-semibold leading-snug text-foreground">
|
|
81
|
+
{productName}
|
|
82
|
+
</h3>
|
|
83
|
+
|
|
84
|
+
{/* Variant name */}
|
|
85
|
+
{variantName && <p className="mt-0.5 text-xs text-muted-foreground">{variantName}</p>}
|
|
86
|
+
|
|
87
|
+
{/* Unit price */}
|
|
88
|
+
<p className="mt-1 text-sm text-muted-foreground">
|
|
89
|
+
{formatPrice(unitPrice, { currency }) as string}
|
|
90
|
+
</p>
|
|
91
|
+
|
|
92
|
+
{/* Quantity controls + remove */}
|
|
93
|
+
<div className="mt-3 flex flex-wrap items-center gap-3">
|
|
94
|
+
<div
|
|
95
|
+
className="flex h-9 items-center rounded-full border"
|
|
96
|
+
role="group"
|
|
97
|
+
aria-label={td('quantityLabel')}
|
|
98
|
+
>
|
|
99
|
+
<button
|
|
100
|
+
type="button"
|
|
101
|
+
onClick={() => handleQuantityChange(item.quantity - 1)}
|
|
102
|
+
disabled={updating || item.quantity <= 1}
|
|
103
|
+
aria-label={td('decreaseQuantity')}
|
|
104
|
+
className="flex h-full w-9 items-center justify-center rounded-s-full transition-colors hover:bg-secondary disabled:opacity-40"
|
|
105
|
+
>
|
|
106
|
+
<IconMinus size={16} />
|
|
107
|
+
</button>
|
|
108
|
+
<span aria-live="polite" className="min-w-7 text-center text-sm font-semibold">
|
|
109
|
+
{updating ? <LoadingSpinner size="sm" /> : item.quantity}
|
|
110
|
+
</span>
|
|
111
|
+
<button
|
|
112
|
+
type="button"
|
|
113
|
+
onClick={() => handleQuantityChange(item.quantity + 1)}
|
|
114
|
+
disabled={updating}
|
|
115
|
+
aria-label={td('increaseQuantity')}
|
|
116
|
+
className="flex h-full w-9 items-center justify-center rounded-e-full transition-colors hover:bg-secondary disabled:opacity-40"
|
|
117
|
+
>
|
|
118
|
+
<IconPlus size={16} />
|
|
119
|
+
</button>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<button
|
|
123
|
+
type="button"
|
|
124
|
+
onClick={handleRemove}
|
|
125
|
+
disabled={removing}
|
|
126
|
+
className="inline-flex items-center gap-1.5 text-xs font-medium text-muted-foreground transition-colors hover:text-destructive disabled:opacity-50"
|
|
127
|
+
>
|
|
128
|
+
<IconTrash size={16} />
|
|
129
|
+
{removing ? t('removing') : t('remove')}
|
|
130
|
+
</button>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
{/* Line total */}
|
|
135
|
+
<span className="text-base font-semibold text-foreground">
|
|
136
|
+
{formatPrice(lineTotal, { currency }) as string}
|
|
137
|
+
</span>
|
|
138
|
+
</div>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { CartNudge } from 'brainerce';
|
|
4
|
+
import { cn } from '@/core/lib/utils';
|
|
5
|
+
import { IconSparkle } from '@/ui/shared/icons';
|
|
6
|
+
|
|
7
|
+
interface CartNudgesProps {
|
|
8
|
+
nudges: CartNudge[];
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Discount-rule nudges on the cart page ("add X more for Y");
|
|
14
|
+
* texts come from useCartPage().cart.nudges.
|
|
15
|
+
*/
|
|
16
|
+
export function CartNudges({ nudges, className }: CartNudgesProps) {
|
|
17
|
+
if (nudges.length === 0) return null;
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<ul className={cn('space-y-2', className)} aria-label="Cart suggestions">
|
|
21
|
+
{nudges.map((nudge) => (
|
|
22
|
+
<li
|
|
23
|
+
key={nudge.ruleId}
|
|
24
|
+
className="flex items-center gap-2.5 rounded-lg bg-secondary px-3.5 py-2.5 text-sm font-medium text-foreground"
|
|
25
|
+
>
|
|
26
|
+
<span aria-hidden="true" className="shrink-0 text-primary">
|
|
27
|
+
<IconSparkle size={18} />
|
|
28
|
+
</span>
|
|
29
|
+
{nudge.text}
|
|
30
|
+
</li>
|
|
31
|
+
))}
|
|
32
|
+
</ul>
|
|
33
|
+
);
|
|
34
|
+
}
|