@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.
Files changed (65) hide show
  1. package/README.md +29 -0
  2. package/assets/hero-desktop.svg +10 -0
  3. package/assets/hero-mobile.svg +9 -0
  4. package/assets/logo.svg +3 -0
  5. package/package.json +60 -0
  6. package/src/blocks/home/Features/index.tsx +87 -0
  7. package/src/blocks/home/Hero/index.tsx +98 -0
  8. package/src/blocks/home/LovedByMoms/bestsellers-carousel.tsx +1 -0
  9. package/src/blocks/home/LovedByMoms/index.tsx +43 -0
  10. package/src/blocks/home/LovedByMoms/loved-by-moms-section.tsx +46 -0
  11. package/src/blocks/home/NewArrivals/index.tsx +91 -0
  12. package/src/blocks/home/PromotionalBanners/index.tsx +81 -0
  13. package/src/blocks/home/ShopByAge/collections-showcase-client.tsx +131 -0
  14. package/src/blocks/home/ShopByAge/collections-showcase-types.ts +4 -0
  15. package/src/blocks/home/ShopByAge/index.tsx +168 -0
  16. package/src/blocks/home/ShopByCategory/index.tsx +111 -0
  17. package/src/blocks/home/Testimonials/index.tsx +25 -0
  18. package/src/blocks/home/Testimonials/reviews-scroll.tsx +122 -0
  19. package/src/blocks/home/Testimonials/testimonials-client.tsx +127 -0
  20. package/src/blocks/home/WhyChooseUs/index.tsx +39 -0
  21. package/src/components/product-carousel.tsx +79 -0
  22. package/src/layouts/MainLayoutShell.tsx +14 -0
  23. package/src/primitives/Button.tsx +31 -0
  24. package/src/primitives/Card.tsx +32 -0
  25. package/src/primitives/index.ts +2 -0
  26. package/src/slots/account/ForgotPassword/index.tsx +1 -0
  27. package/src/slots/account/GoogleLogin/index.tsx +28 -0
  28. package/src/slots/account/Login/index.tsx +1 -0
  29. package/src/slots/account/LoginTemplate/index.tsx +12 -0
  30. package/src/slots/account/LoginTemplate/login-template-client.tsx +83 -0
  31. package/src/slots/account/Register/index.tsx +1 -0
  32. package/src/slots/cart/CartItem/index.tsx +11 -0
  33. package/src/slots/cart/CartSummary/index.tsx +8 -0
  34. package/src/slots/checkout/CheckoutForm/index.tsx +1 -0
  35. package/src/slots/checkout/CheckoutSummary/index.tsx +1 -0
  36. package/src/slots/layout/Footer/index.tsx +95 -0
  37. package/src/slots/layout/Nav/index.tsx +50 -0
  38. package/src/slots/layout/Nav/nav-categories-dropdown.tsx +74 -0
  39. package/src/slots/layout/Nav/nav-collections-dropdown.tsx +106 -0
  40. package/src/slots/layout/Nav/nav-header-content.tsx +165 -0
  41. package/src/slots/layout/Nav/nav-header-shell.tsx +47 -0
  42. package/src/slots/layout/Nav/nav-link-luxury.tsx +15 -0
  43. package/src/slots/layout/PromoBar/index.tsx +9 -0
  44. package/src/slots/layout/PromoBar/promo-bar-content.tsx +118 -0
  45. package/src/slots/order/OrderDetails/index.tsx +12 -0
  46. package/src/slots/product/ProductActions/ProductCTASection.tsx +232 -0
  47. package/src/slots/product/ProductActions/ProductDetailsSection.tsx +200 -0
  48. package/src/slots/product/ProductActions/ProductFeaturePanel.tsx +150 -0
  49. package/src/slots/product/ProductActions/ProductHighlightsSection.tsx +112 -0
  50. package/src/slots/product/ProductActions/ProductOptionsSection.tsx +215 -0
  51. package/src/slots/product/ProductActions/ProductPriceSection.tsx +53 -0
  52. package/src/slots/product/ProductActions/ProductTrustSection.tsx +84 -0
  53. package/src/slots/product/ProductActions/SizeChartPanel.tsx +93 -0
  54. package/src/slots/product/ProductActions/index.tsx +156 -0
  55. package/src/slots/product/ProductActions/product-metadata-fields.ts +503 -0
  56. package/src/slots/product/ProductActions/size-chart-data.ts +108 -0
  57. package/src/slots/product/ProductCard/index.tsx +258 -0
  58. package/src/slots/product/ProductInfo/index.tsx +35 -0
  59. package/src/templates/CollectionsPage/index.tsx +72 -0
  60. package/src/templates/StorePage/index.tsx +134 -0
  61. package/src/tokens/colors.ts +21 -0
  62. package/src/tokens/fonts.ts +16 -0
  63. package/src/tokens/index.ts +3 -0
  64. package/src/tokens/spacing.ts +9 -0
  65. 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>
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 48" fill="none">
2
+ <text x="0" y="34" font-family="Georgia, serif" font-size="28" fill="#1a1a1a">Your Store</text>
3
+ </svg>
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