@pradip1995/theme-sahsha 3.1.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 +29 -0
- package/assets/hero-desktop.svg +10 -0
- package/assets/hero-mobile.svg +9 -0
- package/assets/logo.svg +3 -0
- package/package.json +60 -0
- package/src/blocks/home/Features/index.tsx +87 -0
- package/src/blocks/home/Hero/index.tsx +98 -0
- package/src/blocks/home/LovedByMoms/bestsellers-carousel.tsx +1 -0
- package/src/blocks/home/LovedByMoms/index.tsx +43 -0
- package/src/blocks/home/LovedByMoms/loved-by-moms-section.tsx +46 -0
- package/src/blocks/home/NewArrivals/index.tsx +91 -0
- package/src/blocks/home/PromotionalBanners/index.tsx +81 -0
- package/src/blocks/home/ShopByAge/collections-showcase-client.tsx +131 -0
- package/src/blocks/home/ShopByAge/collections-showcase-types.ts +4 -0
- package/src/blocks/home/ShopByAge/index.tsx +168 -0
- package/src/blocks/home/ShopByCategory/index.tsx +111 -0
- package/src/blocks/home/Testimonials/index.tsx +25 -0
- package/src/blocks/home/Testimonials/reviews-scroll.tsx +122 -0
- package/src/blocks/home/Testimonials/testimonials-client.tsx +127 -0
- package/src/blocks/home/WhyChooseUs/index.tsx +39 -0
- package/src/components/product-carousel.tsx +79 -0
- package/src/layouts/MainLayoutShell.tsx +14 -0
- package/src/primitives/Button.tsx +31 -0
- package/src/primitives/Card.tsx +32 -0
- package/src/primitives/index.ts +2 -0
- package/src/slots/account/ForgotPassword/index.tsx +1 -0
- package/src/slots/account/GoogleLogin/index.tsx +28 -0
- package/src/slots/account/Login/index.tsx +1 -0
- package/src/slots/account/LoginTemplate/index.tsx +12 -0
- package/src/slots/account/LoginTemplate/login-template-client.tsx +83 -0
- package/src/slots/account/Register/index.tsx +1 -0
- package/src/slots/cart/CartItem/index.tsx +11 -0
- package/src/slots/cart/CartSummary/index.tsx +8 -0
- package/src/slots/checkout/CheckoutForm/index.tsx +1 -0
- package/src/slots/checkout/CheckoutSummary/index.tsx +1 -0
- package/src/slots/layout/Footer/index.tsx +95 -0
- package/src/slots/layout/Nav/index.tsx +50 -0
- package/src/slots/layout/Nav/nav-categories-dropdown.tsx +74 -0
- package/src/slots/layout/Nav/nav-collections-dropdown.tsx +106 -0
- package/src/slots/layout/Nav/nav-header-content.tsx +165 -0
- package/src/slots/layout/Nav/nav-header-shell.tsx +47 -0
- package/src/slots/layout/Nav/nav-link-luxury.tsx +15 -0
- package/src/slots/layout/PromoBar/index.tsx +9 -0
- package/src/slots/layout/PromoBar/promo-bar-content.tsx +118 -0
- package/src/slots/order/OrderDetails/index.tsx +12 -0
- package/src/slots/product/ProductActions/ProductCTASection.tsx +232 -0
- package/src/slots/product/ProductActions/ProductDetailsSection.tsx +200 -0
- package/src/slots/product/ProductActions/ProductFeaturePanel.tsx +150 -0
- package/src/slots/product/ProductActions/ProductHighlightsSection.tsx +112 -0
- package/src/slots/product/ProductActions/ProductOptionsSection.tsx +215 -0
- package/src/slots/product/ProductActions/ProductPriceSection.tsx +53 -0
- package/src/slots/product/ProductActions/ProductTrustSection.tsx +84 -0
- package/src/slots/product/ProductActions/SizeChartPanel.tsx +93 -0
- package/src/slots/product/ProductActions/index.tsx +156 -0
- package/src/slots/product/ProductActions/product-metadata-fields.ts +503 -0
- package/src/slots/product/ProductActions/size-chart-data.ts +108 -0
- package/src/slots/product/ProductCard/index.tsx +258 -0
- package/src/slots/product/ProductInfo/index.tsx +35 -0
- package/src/templates/CollectionsPage/index.tsx +72 -0
- package/src/templates/StorePage/index.tsx +134 -0
- package/src/tokens/colors.ts +21 -0
- package/src/tokens/fonts.ts +16 -0
- package/src/tokens/index.ts +3 -0
- package/src/tokens/spacing.ts +9 -0
- package/src/tokens/theme.css +12754 -0
package/README.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# @pradip1995/theme-sahsha
|
|
2
|
+
|
|
3
|
+
Production Sahsha brand theme for Medusa storefronts — forked from Impulse with luxury nav, editorial home blocks, account login shell, and Sahsha design tokens.
|
|
4
|
+
|
|
5
|
+
## Design highlights
|
|
6
|
+
|
|
7
|
+
- Montserrat + Playfair Display typography (`#5a2a43` accent)
|
|
8
|
+
- Split nav header with categories/collections dropdowns and smart search
|
|
9
|
+
- Hero, shop-by-category, new arrivals, shop-by-age, testimonials, loved-by-moms blocks
|
|
10
|
+
- Account login split-panel layout with OTP guest-order flow
|
|
11
|
+
- Cashfree-oriented checkout slots (payment UI lives in app modules)
|
|
12
|
+
|
|
13
|
+
## Use in a storefront
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
./scripts/create-storefront.sh my-shop --theme sahsha --use-package
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Monorepo reference app: `apps/storefront-template` with `pnpm dev:sahsha` (uses this theme package).
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import "@pradip1995/theme-sahsha/tokens/theme.css"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Fork for full customization
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
./scripts/create-theme.sh my-brand --from sahsha
|
|
29
|
+
```
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 600" preserveAspectRatio="xMidYMid slice">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="hero" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
4
|
+
<stop offset="0%" stop-color="#e8e4df"/>
|
|
5
|
+
<stop offset="50%" stop-color="#d4cfc8"/>
|
|
6
|
+
<stop offset="100%" stop-color="#b8b0a6"/>
|
|
7
|
+
</linearGradient>
|
|
8
|
+
</defs>
|
|
9
|
+
<rect width="1440" height="600" fill="url(#hero)"/>
|
|
10
|
+
</svg>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 750 960" preserveAspectRatio="xMidYMid slice">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="hero-m" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
4
|
+
<stop offset="0%" stop-color="#e8e4df"/>
|
|
5
|
+
<stop offset="100%" stop-color="#b8b0a6"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
</defs>
|
|
8
|
+
<rect width="750" height="960" fill="url(#hero-m)"/>
|
|
9
|
+
</svg>
|
package/assets/logo.svg
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pradip1995/theme-sahsha",
|
|
3
|
+
"version": "3.1.0",
|
|
4
|
+
"description": "Sahsha storefront theme — Impulse-based editorial layout, luxury nav, and brand tokens",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public",
|
|
8
|
+
"registry": "https://registry.npmjs.org/"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "https://github.com/SmartByteLabs/medusa-storefront-kit.git",
|
|
13
|
+
"directory": "packages/theme-sahsha"
|
|
14
|
+
},
|
|
15
|
+
"sideEffects": [
|
|
16
|
+
"**/*.css"
|
|
17
|
+
],
|
|
18
|
+
"files": [
|
|
19
|
+
"src",
|
|
20
|
+
"assets"
|
|
21
|
+
],
|
|
22
|
+
"exports": {
|
|
23
|
+
"./tokens/theme.css": "./src/tokens/theme.css",
|
|
24
|
+
"./tokens": "./src/tokens/index.ts",
|
|
25
|
+
"./tokens/*": "./src/tokens/*",
|
|
26
|
+
"./layouts/*": "./src/layouts/*",
|
|
27
|
+
"./primitives": "./src/primitives/index.ts",
|
|
28
|
+
"./primitives/*": "./src/primitives/*",
|
|
29
|
+
"./blocks/*": "./src/blocks/*/index.tsx",
|
|
30
|
+
"./slots/*": "./src/slots/*/index.tsx",
|
|
31
|
+
"./*": "./src/*"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"@pradip1995/commerce-auth": "^3.0.0",
|
|
35
|
+
"@pradip1995/commerce-core": "^3.0.0",
|
|
36
|
+
"@medusajs/types": ">=2",
|
|
37
|
+
"@medusajs/ui": ">=4",
|
|
38
|
+
"color": ">=5",
|
|
39
|
+
"next": ">=15",
|
|
40
|
+
"react": ">=19",
|
|
41
|
+
"react-dom": ">=19"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@pradip1995/commerce-auth": "^3.0.0",
|
|
45
|
+
"@pradip1995/commerce-core": "^3.0.0",
|
|
46
|
+
"@medusajs/types": "latest",
|
|
47
|
+
"@medusajs/ui": "latest",
|
|
48
|
+
"@types/color": "^4.2.0",
|
|
49
|
+
"@types/react": "^19",
|
|
50
|
+
"color": "^5.0.3",
|
|
51
|
+
"next": "15.3.8",
|
|
52
|
+
"react": "19.0.3",
|
|
53
|
+
"react-dom": "19.0.3",
|
|
54
|
+
"typescript": "^5.7.2"
|
|
55
|
+
},
|
|
56
|
+
"scripts": {
|
|
57
|
+
"typecheck": "tsc --noEmit -p tsconfig.typecheck.json",
|
|
58
|
+
"lint": "tsc --noEmit -p tsconfig.typecheck.json"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import Image from "next/image"
|
|
2
|
+
|
|
3
|
+
const features = [
|
|
4
|
+
{
|
|
5
|
+
name: "Free Shipping",
|
|
6
|
+
description: "On all orders over ₹999",
|
|
7
|
+
icon: "/delivery.svg"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
name: "Easy Returns",
|
|
11
|
+
description: "Hassle-free 15-day returns",
|
|
12
|
+
icon: "/Return.svg"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
name: "Secure Checkout",
|
|
16
|
+
description: "100% protected payments",
|
|
17
|
+
icon: "/secure-shield.png"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: "Cash on Delivery",
|
|
21
|
+
description: "Pay at your doorstep",
|
|
22
|
+
icon: "/Cod.svg"
|
|
23
|
+
},
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
/** Premium Impulse-style trust badge strip. */
|
|
27
|
+
const Features = () => {
|
|
28
|
+
return (
|
|
29
|
+
<section className="w-full bg-page-bg py-4 md:py-8 border-y border-[var(--color-border)] overflow-hidden">
|
|
30
|
+
<style>{`
|
|
31
|
+
@keyframes infinite-scroll {
|
|
32
|
+
0% { transform: translateX(0); }
|
|
33
|
+
100% { transform: translateX(-50%); }
|
|
34
|
+
}
|
|
35
|
+
.animate-infinite-scroll {
|
|
36
|
+
animation: infinite-scroll 12s linear infinite;
|
|
37
|
+
}
|
|
38
|
+
`}</style>
|
|
39
|
+
<div className="mx-auto px-4 sm:px-6" style={{ maxWidth: "var(--container-max)" }}>
|
|
40
|
+
|
|
41
|
+
{/* Mobile Continuous Marquee */}
|
|
42
|
+
<div className="flex sm:hidden relative w-full overflow-hidden -mx-4 px-4" style={{ width: 'calc(100% + 2rem)' }}>
|
|
43
|
+
<div className="flex animate-infinite-scroll w-max hover:[animation-play-state:paused]">
|
|
44
|
+
{[...features, ...features].map((feature, idx) => (
|
|
45
|
+
<div
|
|
46
|
+
key={`${feature.name}-${idx}`}
|
|
47
|
+
className="flex flex-col items-center text-center px-4 w-[140px]"
|
|
48
|
+
>
|
|
49
|
+
<div className="w-10 h-10 rounded-full bg-surface flex items-center justify-center border border-[var(--color-border)] mb-2">
|
|
50
|
+
<Image src={feature.icon} alt={feature.name} width={36} height={36} className="w-5 h-5 object-contain" />
|
|
51
|
+
</div>
|
|
52
|
+
<h3 className="whitespace-nowrap text-[10px] font-black uppercase tracking-tighter text-heading mb-0.5">
|
|
53
|
+
{feature.name}
|
|
54
|
+
</h3>
|
|
55
|
+
<p className="whitespace-nowrap text-[8px] text-muted font-medium">
|
|
56
|
+
{feature.description}
|
|
57
|
+
</p>
|
|
58
|
+
</div>
|
|
59
|
+
))}
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
{/* Desktop Grid */}
|
|
64
|
+
<div className="hidden sm:grid sm:grid-cols-4 gap-4 md:gap-6 lg:gap-8">
|
|
65
|
+
{features.map((feature) => (
|
|
66
|
+
<div key={feature.name} className="group flex flex-col items-center text-center space-y-3">
|
|
67
|
+
<div className="w-14 h-14 lg:w-[72px] lg:h-[72px] rounded-full bg-surface flex items-center justify-center transition-all duration-300 group-hover:-translate-y-1 group-hover:shadow-brand-sm border border-[var(--color-border)]">
|
|
68
|
+
<Image src={feature.icon} alt={feature.name} width={36} height={36} className="w-7 h-7 lg:w-9 lg:h-9 object-contain opacity-80 group-hover:opacity-100 transition-opacity" />
|
|
69
|
+
</div>
|
|
70
|
+
<div>
|
|
71
|
+
<h3 className="whitespace-nowrap sm:text-[11px] lg:text-[13px] font-black uppercase sm:tracking-widest text-heading mb-1.5">
|
|
72
|
+
{feature.name}
|
|
73
|
+
</h3>
|
|
74
|
+
<p className="whitespace-nowrap sm:text-[10px] lg:text-xs text-muted max-w-[200px] mx-auto font-medium leading-normal">
|
|
75
|
+
{feature.description}
|
|
76
|
+
</p>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
))}
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
</div>
|
|
83
|
+
</section>
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export default Features
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import Image from "next/image"
|
|
2
|
+
import LocalizedClientLink from "@modules/common/components/localized-client-link"
|
|
3
|
+
import type { HeroBlockData } from "@core/types/home"
|
|
4
|
+
|
|
5
|
+
const Hero = ({ homeBanner, appBanner }: HeroBlockData) => {
|
|
6
|
+
const desktopImage = homeBanner.image!
|
|
7
|
+
const mobileImage = appBanner.image || desktopImage
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<section className="w-full impulse-hero-nav-overlap">
|
|
11
|
+
{/* Desktop */}
|
|
12
|
+
<div className="hidden min-[551px]:block impulse-hero impulse-hero--desktop">
|
|
13
|
+
<Image
|
|
14
|
+
src={desktopImage}
|
|
15
|
+
alt={homeBanner.subtitle || homeBanner.title || "Hero banner"}
|
|
16
|
+
fill
|
|
17
|
+
priority
|
|
18
|
+
sizes="100vw"
|
|
19
|
+
className="impulse-hero__image"
|
|
20
|
+
/>
|
|
21
|
+
|
|
22
|
+
<div className="impulse-hero__panel">
|
|
23
|
+
<div className="impulse-hero__panel-inner">
|
|
24
|
+
<div className="impulse-hero__overlay" aria-hidden />
|
|
25
|
+
<div className="impulse-hero__content">
|
|
26
|
+
{homeBanner.title && (
|
|
27
|
+
<p className="impulse-hero__eyebrow">{homeBanner.title}</p>
|
|
28
|
+
)}
|
|
29
|
+
{homeBanner.subtitle && (
|
|
30
|
+
<h1 className="impulse-hero__title">{homeBanner.subtitle}</h1>
|
|
31
|
+
)}
|
|
32
|
+
{homeBanner.description && (
|
|
33
|
+
<p className="impulse-hero__description">
|
|
34
|
+
{homeBanner.description}
|
|
35
|
+
</p>
|
|
36
|
+
)}
|
|
37
|
+
{homeBanner.buttonName && (
|
|
38
|
+
<LocalizedClientLink href={homeBanner.buttonLink || "/store"}>
|
|
39
|
+
<span
|
|
40
|
+
className="impulse-hero__cta"
|
|
41
|
+
data-ga-event="hero_banner_click"
|
|
42
|
+
data-ga-label={homeBanner.buttonName}
|
|
43
|
+
>
|
|
44
|
+
<span className="impulse-hero__cta-label">
|
|
45
|
+
{homeBanner.buttonName}
|
|
46
|
+
</span>
|
|
47
|
+
</span>
|
|
48
|
+
</LocalizedClientLink>
|
|
49
|
+
)}
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
{/* Mobile */}
|
|
56
|
+
<div className="block min-[551px]:hidden impulse-hero impulse-hero--mobile">
|
|
57
|
+
<Image
|
|
58
|
+
src={mobileImage}
|
|
59
|
+
alt={appBanner.subtitle || appBanner.title || "Hero banner"}
|
|
60
|
+
fill
|
|
61
|
+
priority
|
|
62
|
+
sizes="100vw"
|
|
63
|
+
className="impulse-hero__image"
|
|
64
|
+
/>
|
|
65
|
+
|
|
66
|
+
<div className="impulse-hero__panel">
|
|
67
|
+
<div className="impulse-hero__panel-inner">
|
|
68
|
+
<div className="impulse-hero__overlay" aria-hidden />
|
|
69
|
+
<div className="impulse-hero__content">
|
|
70
|
+
{appBanner.title && (
|
|
71
|
+
<p className="impulse-hero__eyebrow">{appBanner.title}</p>
|
|
72
|
+
)}
|
|
73
|
+
{appBanner.subtitle && (
|
|
74
|
+
<h1 className="impulse-hero__title">{appBanner.subtitle}</h1>
|
|
75
|
+
)}
|
|
76
|
+
{appBanner.description && (
|
|
77
|
+
<p className="impulse-hero__description">
|
|
78
|
+
{appBanner.description}
|
|
79
|
+
</p>
|
|
80
|
+
)}
|
|
81
|
+
{appBanner.buttonName && (
|
|
82
|
+
<LocalizedClientLink href={appBanner.buttonLink || "/store"}>
|
|
83
|
+
<span className="impulse-hero__cta">
|
|
84
|
+
<span className="impulse-hero__cta-label">
|
|
85
|
+
{appBanner.buttonName}
|
|
86
|
+
</span>
|
|
87
|
+
</span>
|
|
88
|
+
</LocalizedClientLink>
|
|
89
|
+
)}
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
</section>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export default Hero
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from "@theme/components/product-carousel"
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { HttpTypes } from "@medusajs/types"
|
|
2
|
+
import type { ProductCardRating } from "@core/types/product-card"
|
|
3
|
+
import { getBestsellerProducts } from "@lib/data/bestsellers"
|
|
4
|
+
import LovedByMomsSection from "./loved-by-moms-section"
|
|
5
|
+
|
|
6
|
+
type LovedByMomsProps = {
|
|
7
|
+
products: HttpTypes.StoreProduct[]
|
|
8
|
+
region: HttpTypes.StoreRegion
|
|
9
|
+
ratings?: ProductCardRating[]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Best seller products — fetched by `best seller` tag, shown in swipe carousel. */
|
|
13
|
+
const LovedByMoms = async ({
|
|
14
|
+
products: fallbackProducts,
|
|
15
|
+
region,
|
|
16
|
+
ratings,
|
|
17
|
+
}: LovedByMomsProps) => {
|
|
18
|
+
const countryCode =
|
|
19
|
+
region.countries?.[0]?.iso_2?.toLowerCase() ??
|
|
20
|
+
process.env.NEXT_PUBLIC_DEFAULT_REGION ??
|
|
21
|
+
"in"
|
|
22
|
+
|
|
23
|
+
let products = fallbackProducts ?? []
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const taggedProducts = await getBestsellerProducts(countryCode, 12)
|
|
27
|
+
if (taggedProducts.length > 0) {
|
|
28
|
+
products = taggedProducts
|
|
29
|
+
}
|
|
30
|
+
} catch {
|
|
31
|
+
products = fallbackProducts ?? []
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<LovedByMomsSection
|
|
36
|
+
products={products}
|
|
37
|
+
region={region}
|
|
38
|
+
ratings={ratings}
|
|
39
|
+
/>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default LovedByMoms
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import LocalizedClientLink from "@modules/common/components/localized-client-link"
|
|
4
|
+
import { HttpTypes } from "@medusajs/types"
|
|
5
|
+
import type { ProductCardRating } from "@core/types/product-card"
|
|
6
|
+
import BestsellersCarousel from "./bestsellers-carousel"
|
|
7
|
+
|
|
8
|
+
type LovedByMomsSectionProps = {
|
|
9
|
+
products: HttpTypes.StoreProduct[]
|
|
10
|
+
region: HttpTypes.StoreRegion
|
|
11
|
+
ratings?: ProductCardRating[]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const LovedByMomsSection = ({
|
|
15
|
+
products,
|
|
16
|
+
region,
|
|
17
|
+
ratings,
|
|
18
|
+
}: LovedByMomsSectionProps) => {
|
|
19
|
+
if (products.length === 0) {
|
|
20
|
+
return null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<section className="bestsellers w-full bg-page-bg">
|
|
25
|
+
<div className="bestsellers__inner">
|
|
26
|
+
<div className="flex flex-row items-center justify-between mb-6 sm:mb-8">
|
|
27
|
+
<h2 className="bestsellers__title !mb-0">Best sellers</h2>
|
|
28
|
+
<LocalizedClientLink
|
|
29
|
+
href="/store?sortBy=bestsellers"
|
|
30
|
+
className="bestsellers__link !mt-0"
|
|
31
|
+
>
|
|
32
|
+
View all
|
|
33
|
+
</LocalizedClientLink>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<BestsellersCarousel
|
|
37
|
+
products={products}
|
|
38
|
+
region={region}
|
|
39
|
+
ratings={ratings}
|
|
40
|
+
/>
|
|
41
|
+
</div>
|
|
42
|
+
</section>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default LovedByMomsSection
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { listStoreProductsWithSort } from "@lib/data/store-products"
|
|
2
|
+
import { fetchRatings } from "@core/data/reviews"
|
|
3
|
+
import type { ProductCardRating } from "@core/types/product-card"
|
|
4
|
+
import { HttpTypes } from "@medusajs/types"
|
|
5
|
+
import LocalizedClientLink from "@modules/common/components/localized-client-link"
|
|
6
|
+
import ProductCarousel from "@theme/components/product-carousel"
|
|
7
|
+
import PromotionalBanners from "../PromotionalBanners"
|
|
8
|
+
|
|
9
|
+
type NewArrivalsProps = {
|
|
10
|
+
products: HttpTypes.StoreProduct[]
|
|
11
|
+
region: HttpTypes.StoreRegion
|
|
12
|
+
ratings?: ProductCardRating[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const NewArrivals = async ({
|
|
16
|
+
products: fallbackProducts,
|
|
17
|
+
region,
|
|
18
|
+
ratings,
|
|
19
|
+
}: NewArrivalsProps) => {
|
|
20
|
+
const countryCode =
|
|
21
|
+
region.countries?.[0]?.iso_2?.toLowerCase() ??
|
|
22
|
+
process.env.NEXT_PUBLIC_DEFAULT_REGION ??
|
|
23
|
+
"in"
|
|
24
|
+
|
|
25
|
+
let products = fallbackProducts ?? []
|
|
26
|
+
let allRatings: ProductCardRating[] = ratings ?? []
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const [productsResult, ratingsResult] = await Promise.all([
|
|
30
|
+
listStoreProductsWithSort({
|
|
31
|
+
page: 1,
|
|
32
|
+
queryParams: { limit: 12 },
|
|
33
|
+
sortBy: "created_at_desc",
|
|
34
|
+
countryCode,
|
|
35
|
+
}),
|
|
36
|
+
fetchRatings(),
|
|
37
|
+
])
|
|
38
|
+
|
|
39
|
+
if (productsResult.response.products?.length > 0) {
|
|
40
|
+
products = productsResult.response.products
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (Array.isArray(ratingsResult) && ratingsResult.length > 0) {
|
|
44
|
+
allRatings = ratingsResult as ProductCardRating[]
|
|
45
|
+
}
|
|
46
|
+
} catch {
|
|
47
|
+
products = fallbackProducts ?? []
|
|
48
|
+
allRatings = ratings ?? []
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const visibleProducts = products.slice(0, 12)
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<>
|
|
55
|
+
<section className="new-arrivals w-full py-8 md:py-12 bg-page-bg">
|
|
56
|
+
<div
|
|
57
|
+
className="mx-auto px-2 sm:px-3 lg:px-4"
|
|
58
|
+
style={{ maxWidth: "var(--container-max)" }}
|
|
59
|
+
>
|
|
60
|
+
<div className="flex items-end justify-between gap-4 mb-6 md:mb-8">
|
|
61
|
+
<h2 className="font-display text-2xl md:text-3xl text-heading">
|
|
62
|
+
New arrivals
|
|
63
|
+
</h2>
|
|
64
|
+
<LocalizedClientLink
|
|
65
|
+
href="/store"
|
|
66
|
+
className="text-[11px] font-semibold uppercase tracking-[var(--letter-spacing-nav)] text-heading border-b border-heading pb-0.5 hover:opacity-70 transition-opacity shrink-0"
|
|
67
|
+
>
|
|
68
|
+
View all
|
|
69
|
+
</LocalizedClientLink>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
{visibleProducts.length > 0 ? (
|
|
73
|
+
<ProductCarousel
|
|
74
|
+
products={visibleProducts}
|
|
75
|
+
region={region}
|
|
76
|
+
ratings={allRatings}
|
|
77
|
+
/>
|
|
78
|
+
) : (
|
|
79
|
+
<p className="text-center py-12 text-[var(--color-text-muted)]">
|
|
80
|
+
No new arrivals at the moment. Check back soon!
|
|
81
|
+
</p>
|
|
82
|
+
)}
|
|
83
|
+
</div>
|
|
84
|
+
</section>
|
|
85
|
+
|
|
86
|
+
<PromotionalBanners />
|
|
87
|
+
</>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export default NewArrivals
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import Image from "next/image"
|
|
2
|
+
import LocalizedClientLink from "@modules/common/components/localized-client-link"
|
|
3
|
+
import { getPromotionalBannersFromConfig } from "@lib/data/promotional-banners"
|
|
4
|
+
|
|
5
|
+
const PromotionalBanners = async () => {
|
|
6
|
+
const banners = await getPromotionalBannersFromConfig()
|
|
7
|
+
|
|
8
|
+
if (banners.length === 0) {
|
|
9
|
+
return null
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const isSingle = banners.length === 1
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<section className="promo-banners" aria-label="Promotions">
|
|
16
|
+
<div
|
|
17
|
+
className={
|
|
18
|
+
isSingle ? "promo-banners__single" : "promo-banners__grid"
|
|
19
|
+
}
|
|
20
|
+
>
|
|
21
|
+
{banners.slice(0, 2).map((banner, index) => {
|
|
22
|
+
const content = (
|
|
23
|
+
<>
|
|
24
|
+
<Image
|
|
25
|
+
src={banner.image}
|
|
26
|
+
alt={banner.title || `Promotional banner ${index + 1}`}
|
|
27
|
+
fill
|
|
28
|
+
className="promo-banners__image"
|
|
29
|
+
sizes={
|
|
30
|
+
isSingle
|
|
31
|
+
? "100vw"
|
|
32
|
+
: "(max-width: 768px) 100vw, 50vw"
|
|
33
|
+
}
|
|
34
|
+
unoptimized
|
|
35
|
+
/>
|
|
36
|
+
<div className="promo-banners__overlay" aria-hidden />
|
|
37
|
+
<div className="promo-banners__content">
|
|
38
|
+
{banner.title && (
|
|
39
|
+
<h3 className="promo-banners__title">{banner.title}</h3>
|
|
40
|
+
)}
|
|
41
|
+
{banner.description && (
|
|
42
|
+
<p className="promo-banners__description">
|
|
43
|
+
{banner.description}
|
|
44
|
+
</p>
|
|
45
|
+
)}
|
|
46
|
+
{banner.buttonName && (
|
|
47
|
+
<span className="promo-banners__cta">
|
|
48
|
+
{banner.buttonName}
|
|
49
|
+
</span>
|
|
50
|
+
)}
|
|
51
|
+
</div>
|
|
52
|
+
</>
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
if (banner.link) {
|
|
56
|
+
return (
|
|
57
|
+
<LocalizedClientLink
|
|
58
|
+
key={`${banner.image}-${index}`}
|
|
59
|
+
href={banner.link}
|
|
60
|
+
className="promo-banners__tile"
|
|
61
|
+
>
|
|
62
|
+
{content}
|
|
63
|
+
</LocalizedClientLink>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div
|
|
69
|
+
key={`${banner.image}-${index}`}
|
|
70
|
+
className="promo-banners__tile"
|
|
71
|
+
>
|
|
72
|
+
{content}
|
|
73
|
+
</div>
|
|
74
|
+
)
|
|
75
|
+
})}
|
|
76
|
+
</div>
|
|
77
|
+
</section>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export default PromotionalBanners
|