@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
@@ -0,0 +1,131 @@
1
+ "use client"
2
+
3
+ import { useEffect, useRef, useState } from "react"
4
+ import { ChevronLeft, ChevronRight } from "lucide-react"
5
+ import { HttpTypes } from "@medusajs/types"
6
+ import type { ProductCardRating } from "@core/types/product-card"
7
+ import LocalizedClientLink from "@modules/common/components/localized-client-link"
8
+ import ProductCard from "@theme/slots/product/ProductCard"
9
+ import type { CollectionTab } from "./collections-showcase-types"
10
+
11
+ type CollectionsShowcaseClientProps = {
12
+ tabs: CollectionTab[]
13
+ productsByCollection: Record<string, HttpTypes.StoreProduct[]>
14
+ region: HttpTypes.StoreRegion
15
+ ratings?: ProductCardRating[]
16
+ }
17
+
18
+ function getDefaultActiveTabId(tabs: CollectionTab[]) {
19
+ if (!tabs.length) {
20
+ return ""
21
+ }
22
+
23
+ const middleIndex = Math.floor(tabs.length / 2)
24
+ return tabs[middleIndex]?.id ?? tabs[0].id
25
+ }
26
+
27
+ const CollectionsShowcaseClient = ({
28
+ tabs,
29
+ productsByCollection,
30
+ region,
31
+ ratings,
32
+ }: CollectionsShowcaseClientProps) => {
33
+ const viewportRef = useRef<HTMLDivElement>(null)
34
+ const [activeId, setActiveId] = useState(() => getDefaultActiveTabId(tabs))
35
+
36
+ const activeProducts = productsByCollection[activeId] ?? []
37
+
38
+ useEffect(() => {
39
+ const viewport = viewportRef.current
40
+ if (viewport) {
41
+ viewport.scrollLeft = 0
42
+ }
43
+ }, [activeId])
44
+
45
+ const scrollByPage = (direction: "prev" | "next") => {
46
+ const viewport = viewportRef.current
47
+ if (!viewport) return
48
+
49
+ const amount = Math.max(viewport.clientWidth * 0.85, 280)
50
+ viewport.scrollBy({
51
+ left: direction === "next" ? amount : -amount,
52
+ behavior: "smooth",
53
+ })
54
+ }
55
+
56
+ if (!tabs.length) {
57
+ return null
58
+ }
59
+
60
+ return (
61
+ <>
62
+ <nav className="collections-showcase__tabs" aria-label="Collections">
63
+ {tabs.map((tab) => {
64
+ const isActive = tab.id === activeId
65
+
66
+ return (
67
+ <button
68
+ key={tab.id}
69
+ type="button"
70
+ className={`collections-showcase__tab${isActive ? " is-active" : ""}`}
71
+ aria-current={isActive ? "true" : undefined}
72
+ onClick={() => setActiveId(tab.id)}
73
+ >
74
+ {tab.title}
75
+ </button>
76
+ )
77
+ })}
78
+ </nav>
79
+
80
+ <div className="collections-showcase__slider">
81
+ <button
82
+ type="button"
83
+ className="collections-showcase__nav collections-showcase__nav--prev"
84
+ onClick={() => scrollByPage("prev")}
85
+ aria-label="Previous products"
86
+ >
87
+ <ChevronLeft size={18} strokeWidth={1.5} />
88
+ </button>
89
+
90
+ <div ref={viewportRef} className="collections-showcase__viewport">
91
+ <div className="collections-showcase__track">
92
+ {activeProducts.length > 0 ? (
93
+ activeProducts.map((product) => (
94
+ <div key={product.id} className="collections-showcase__slide">
95
+ <ProductCard
96
+ product={product}
97
+ region={region}
98
+ rating={ratings?.find((r) => r.product_id === product.id)}
99
+ className="h-full"
100
+ imageClassName="shop-grid__card-image"
101
+ quickAddClassName="product-card__quick-add--hero"
102
+ showWishlistIcon
103
+ />
104
+ </div>
105
+ ))
106
+ ) : (
107
+ <p className="collections-showcase__empty">No products in this collection yet.</p>
108
+ )}
109
+ </div>
110
+ </div>
111
+
112
+ <button
113
+ type="button"
114
+ className="collections-showcase__nav collections-showcase__nav--next"
115
+ onClick={() => scrollByPage("next")}
116
+ aria-label="Next products"
117
+ >
118
+ <ChevronRight size={18} strokeWidth={1.5} />
119
+ </button>
120
+ </div>
121
+
122
+ <div className="collections-showcase__footer">
123
+ <LocalizedClientLink href="/collections" className="collections-showcase__view-all">
124
+ View all
125
+ </LocalizedClientLink>
126
+ </div>
127
+ </>
128
+ )
129
+ }
130
+
131
+ export default CollectionsShowcaseClient
@@ -0,0 +1,4 @@
1
+ export type CollectionTab = {
2
+ id: string
3
+ title: string
4
+ }
@@ -0,0 +1,168 @@
1
+ import { listCollections } from "@core/data/collections"
2
+ import { listProducts } from "@core/data/products"
3
+ import { fetchRatings } from "@core/data/reviews"
4
+ import { getRegion } from "@core/data/regions"
5
+ import type { ProductCardRating } from "@core/types/product-card"
6
+ import { HttpTypes } from "@medusajs/types"
7
+ import { storefrontConfig } from "@storefront-config"
8
+ import CollectionsShowcaseClient from "./collections-showcase-client"
9
+ import type { CollectionTab } from "./collections-showcase-types"
10
+
11
+ type ShopByAgeProps = {
12
+ collections: HttpTypes.StoreCollection[]
13
+ }
14
+
15
+ const FEATURED_TAB_COUNT = 3
16
+ const PRODUCTS_PER_COLLECTION = 12
17
+
18
+ function normalizeCollectionKey(value: string) {
19
+ return value.toLowerCase().replace(/[\s_-]+/g, "")
20
+ }
21
+
22
+ function getCollectionMetadata(collection: HttpTypes.StoreCollection) {
23
+ return (collection.metadata || {}) as Record<string, unknown>
24
+ }
25
+
26
+ function isHomeFeatured(collection: HttpTypes.StoreCollection) {
27
+ const value = getCollectionMetadata(collection).home_featured
28
+ return value === true || value === "true" || value === "1"
29
+ }
30
+
31
+ function getHomeSort(collection: HttpTypes.StoreCollection) {
32
+ const raw = getCollectionMetadata(collection).home_sort
33
+ const num = Number.parseInt(String(raw ?? ""), 10)
34
+ return Number.isFinite(num) ? num : 999
35
+ }
36
+
37
+ function pickFeaturedFromMetadata(collections: HttpTypes.StoreCollection[]) {
38
+ return collections
39
+ .filter(isHomeFeatured)
40
+ .sort((a, b) => getHomeSort(a) - getHomeSort(b))
41
+ .slice(0, FEATURED_TAB_COUNT)
42
+ }
43
+
44
+ function pickFeaturedFromConfig(collections: HttpTypes.StoreCollection[]) {
45
+ const configuredHandles = storefrontConfig.homeFeaturedCollectionHandles ?? []
46
+ if (!configuredHandles.length) {
47
+ return []
48
+ }
49
+
50
+ const picked: HttpTypes.StoreCollection[] = []
51
+
52
+ for (const handle of configuredHandles) {
53
+ const key = normalizeCollectionKey(handle)
54
+ const found = collections.find((collection) => {
55
+ const byHandle = normalizeCollectionKey(collection.handle || "")
56
+ const byTitle = normalizeCollectionKey(collection.title || "")
57
+ return byHandle === key || byTitle === key
58
+ })
59
+
60
+ if (found && !picked.some((item) => item.id === found.id)) {
61
+ picked.push(found)
62
+ }
63
+ }
64
+
65
+ return picked
66
+ }
67
+
68
+ function pickFeaturedCollections(collections: HttpTypes.StoreCollection[]) {
69
+ const fromMetadata = pickFeaturedFromMetadata(collections)
70
+ if (fromMetadata.length > 0) {
71
+ return fromMetadata
72
+ }
73
+
74
+ const fromConfig = pickFeaturedFromConfig(collections)
75
+ if (fromConfig.length > 0) {
76
+ return fromConfig
77
+ }
78
+
79
+ return collections.slice(0, FEATURED_TAB_COUNT)
80
+ }
81
+
82
+ const ShopByAge = async ({ collections: fallbackCollections }: ShopByAgeProps) => {
83
+ let collections = fallbackCollections
84
+
85
+ try {
86
+ const result = await listCollections({
87
+ limit: "100",
88
+ fields: "id,handle,title,metadata",
89
+ })
90
+ if (result.collections.length > 0) {
91
+ collections = result.collections
92
+ }
93
+ } catch {
94
+ collections = fallbackCollections
95
+ }
96
+
97
+ if (!collections || collections.length === 0) {
98
+ return null
99
+ }
100
+
101
+ const countryCode = process.env.NEXT_PUBLIC_DEFAULT_REGION ?? "in"
102
+ const region = await getRegion(countryCode)
103
+
104
+ if (!region) {
105
+ return null
106
+ }
107
+
108
+ const ratings = (await fetchRatings().catch(() => [])) as ProductCardRating[]
109
+ const featuredCollections = pickFeaturedCollections(collections)
110
+ const tabs: CollectionTab[] = featuredCollections.map((collection) => ({
111
+ id: collection.id,
112
+ title: collection.title || "Collection",
113
+ }))
114
+
115
+ const productGroups = await Promise.all(
116
+ featuredCollections.map(async (collection) => {
117
+ try {
118
+ const { response } = await listProducts({
119
+ regionId: region.id,
120
+ queryParams: {
121
+ collection_id: collection.id,
122
+ limit: PRODUCTS_PER_COLLECTION,
123
+ fields: "*variants.calculated_price",
124
+ },
125
+ })
126
+
127
+ return {
128
+ collectionId: collection.id,
129
+ products: response.products ?? [],
130
+ }
131
+ } catch {
132
+ return {
133
+ collectionId: collection.id,
134
+ products: [] as HttpTypes.StoreProduct[],
135
+ }
136
+ }
137
+ })
138
+ )
139
+
140
+ const productsByCollection = productGroups.reduce<
141
+ Record<string, HttpTypes.StoreProduct[]>
142
+ >((acc, group) => {
143
+ acc[group.collectionId] = group.products
144
+ return acc
145
+ }, {})
146
+
147
+ return (
148
+ <section className="collections-showcase w-full bg-page-bg">
149
+ <div
150
+ className="collections-showcase__inner mx-auto"
151
+ style={{ maxWidth: "var(--container-max)" }}
152
+ >
153
+ <header className="collections-showcase__header">
154
+ <p className="collections-showcase__eyebrow">What&apos;s new</p>
155
+ </header>
156
+
157
+ <CollectionsShowcaseClient
158
+ tabs={tabs}
159
+ productsByCollection={productsByCollection}
160
+ region={region}
161
+ ratings={ratings}
162
+ />
163
+ </div>
164
+ </section>
165
+ )
166
+ }
167
+
168
+ export default ShopByAge
@@ -0,0 +1,111 @@
1
+ import Image from "next/image"
2
+ import LocalizedClientLink from "@modules/common/components/localized-client-link"
3
+ import { HttpTypes } from "@medusajs/types"
4
+ import PlaceholderImage from "@modules/common/icons/placeholder-image"
5
+ import {
6
+ getCategoryImage,
7
+ getCategoryProductCount,
8
+ } from "@lib/category-collection-images"
9
+
10
+ type ShopByCategoryProps = {
11
+ categories: HttpTypes.StoreProductCategory[]
12
+ }
13
+
14
+ const ShopByCategory = ({ categories }: ShopByCategoryProps) => {
15
+ if (!categories || categories.length === 0) {
16
+ return null
17
+ }
18
+
19
+ const parentCategories = categories.filter(
20
+ (category) => !category.parent_category_id
21
+ )
22
+
23
+ if (parentCategories.length === 0) {
24
+ return null
25
+ }
26
+
27
+ const categoryGroups: HttpTypes.StoreProductCategory[][] = []
28
+ for (let i = 0; i < parentCategories.length; i += 5) {
29
+ categoryGroups.push(parentCategories.slice(i, i + 5))
30
+ }
31
+
32
+ return (
33
+ <section className="shop-by-category w-full bg-white py-8 sm:py-9 md:py-10 font-[family-name:var(--sf-font-family)]">
34
+ <div
35
+ className="shop-by-category__inner"
36
+ style={{ maxWidth: "var(--container-max)" }}
37
+ >
38
+ <div className="shop-by-category__header">
39
+ <h2 className="font-display text-2xl md:text-3xl text-heading">
40
+ Shop by category
41
+ </h2>
42
+ </div>
43
+
44
+ <div className="shop-by-category__groups">
45
+ {categoryGroups.map((group, groupIndex) => (
46
+ <div key={groupIndex} className="shop-by-category__grid">
47
+ {group.map((category, index) => {
48
+ const categoryImage = getCategoryImage(category)
49
+ const productCount = getCategoryProductCount(category)
50
+ const categoryUrl = category.id
51
+ ? `/store?category=${category.id}`
52
+ : "#"
53
+ const rowClass =
54
+ index < 3
55
+ ? "shop-by-category__card--top"
56
+ : "shop-by-category__card--bottom"
57
+
58
+ return (
59
+ <LocalizedClientLink
60
+ key={category.id}
61
+ href={categoryUrl}
62
+ data-ga-event="shop_by_category_click"
63
+ data-ga-label={category.name || "Category"}
64
+ className={`shop-by-category__card group ${rowClass}`}
65
+ >
66
+ <div className="shop-by-category__frame">
67
+ {categoryImage ? (
68
+ <Image
69
+ src={categoryImage}
70
+ alt={category.name || "Category"}
71
+ fill
72
+ sizes="(max-width: 1024px) 50vw, 33vw"
73
+ className="shop-by-category__image"
74
+ />
75
+ ) : (
76
+ <div className="shop-by-category__placeholder">
77
+ <PlaceholderImage size={48} />
78
+ </div>
79
+ )}
80
+
81
+ <div className="shop-by-category__overlay" aria-hidden />
82
+
83
+ {productCount > 0 && (
84
+ <p className="shop-by-category__count">
85
+ {productCount}{" "}
86
+ {productCount === 1 ? "Product" : "Products"}
87
+ </p>
88
+ )}
89
+
90
+ <div className="shop-by-category__content">
91
+ <h3 className="shop-by-category__name">{category.name}</h3>
92
+ <p className="shop-by-category__cta">
93
+ View the collection
94
+ <span className="shop-by-category__cta-arrow" aria-hidden>
95
+
96
+ </span>
97
+ </p>
98
+ </div>
99
+ </div>
100
+ </LocalizedClientLink>
101
+ )
102
+ })}
103
+ </div>
104
+ ))}
105
+ </div>
106
+ </div>
107
+ </section>
108
+ )
109
+ }
110
+
111
+ export default ShopByCategory
@@ -0,0 +1,25 @@
1
+ import { enrichTestimonialsData } from "@lib/data/testimonials"
2
+ import TestimonialsClient from "./testimonials-client"
3
+
4
+ interface TestimonialsProps {
5
+ initialData?: {
6
+ title: string
7
+ eyebrow?: string
8
+ description?: string
9
+ testimonials: Array<{
10
+ id: string
11
+ text: string
12
+ name: string
13
+ rating: number
14
+ avatar?: string
15
+ }>
16
+ } | null
17
+ }
18
+
19
+ const Testimonials = async ({ initialData }: TestimonialsProps) => {
20
+ const enrichedData = await enrichTestimonialsData(initialData ?? null)
21
+
22
+ return <TestimonialsClient initialData={enrichedData} />
23
+ }
24
+
25
+ export default Testimonials
@@ -0,0 +1,122 @@
1
+ "use client"
2
+
3
+ import { useEffect, useState, type CSSProperties } from "react"
4
+ import Image from "next/image"
5
+ import PlaceholderImage from "@modules/common/icons/placeholder-image"
6
+
7
+ export type ReviewItem = {
8
+ id: string
9
+ text: string
10
+ name: string
11
+ rating: number
12
+ avatar?: string
13
+ }
14
+
15
+ type ReviewsScrollProps = {
16
+ items: ReviewItem[]
17
+ }
18
+
19
+ function CardStars({ rating }: { rating: number }) {
20
+ const clamped = Math.max(0, Math.min(5, rating))
21
+ const fullStars = Math.floor(clamped)
22
+ const hasHalf = clamped % 1 >= 0.5
23
+
24
+ return (
25
+ <div
26
+ className="customer-reviews__card-stars"
27
+ aria-label={`${clamped} out of 5 stars`}
28
+ >
29
+ {Array.from({ length: 5 }, (_, index) => {
30
+ const filled = index < fullStars || (index === fullStars && hasHalf)
31
+
32
+ return (
33
+ <span
34
+ key={index}
35
+ className={
36
+ filled
37
+ ? "customer-reviews__star customer-reviews__star--filled"
38
+ : "customer-reviews__star"
39
+ }
40
+ >
41
+
42
+ </span>
43
+ )
44
+ })}
45
+ </div>
46
+ )
47
+ }
48
+
49
+ function ReviewCard({ item }: { item: ReviewItem }) {
50
+ return (
51
+ <article className="customer-reviews__card">
52
+ <div className="customer-reviews__card-media">
53
+ {item.avatar ? (
54
+ <Image
55
+ src={item.avatar}
56
+ alt={item.name}
57
+ fill
58
+ sizes="(max-width: 768px) 80vw, 320px"
59
+ className="customer-reviews__card-image"
60
+ unoptimized
61
+ />
62
+ ) : (
63
+ <div className="customer-reviews__card-placeholder">
64
+ <PlaceholderImage size={36} />
65
+ </div>
66
+ )}
67
+ </div>
68
+
69
+ <div className="customer-reviews__card-body">
70
+ <p className="customer-reviews__card-quote">{item.text}</p>
71
+ <CardStars rating={item.rating} />
72
+
73
+ <p className="customer-reviews__card-meta">
74
+ <span className="customer-reviews__card-name">{item.name}</span>
75
+ <span className="customer-reviews__card-divider" aria-hidden>
76
+ |
77
+ </span>
78
+ <span className="customer-reviews__card-verified">Verified</span>
79
+ </p>
80
+ </div>
81
+ </article>
82
+ )
83
+ }
84
+
85
+ const ReviewsScroll = ({ items }: ReviewsScrollProps) => {
86
+ const [reduceMotion, setReduceMotion] = useState(false)
87
+ const loopedItems = items.length > 1 ? [...items, ...items] : items
88
+ const canAnimate = !reduceMotion && items.length > 1
89
+ const duration = Math.max(items.length * 7, 18)
90
+
91
+ useEffect(() => {
92
+ const media = window.matchMedia("(prefers-reduced-motion: reduce)")
93
+ const update = () => setReduceMotion(media.matches)
94
+ update()
95
+ media.addEventListener("change", update)
96
+ return () => media.removeEventListener("change", update)
97
+ }, [])
98
+
99
+ return (
100
+ <div className="customer-reviews__scroll">
101
+ <div className="customer-reviews__fade customer-reviews__fade--top" aria-hidden />
102
+ <div className="customer-reviews__fade customer-reviews__fade--bottom" aria-hidden />
103
+
104
+ <div className="customer-reviews__viewport">
105
+ <div
106
+ className={`customer-reviews__track${canAnimate ? " customer-reviews__track--animate" : ""}`}
107
+ style={
108
+ canAnimate
109
+ ? ({ "--reviews-duration": `${duration}s` } as CSSProperties)
110
+ : undefined
111
+ }
112
+ >
113
+ {loopedItems.map((item, index) => (
114
+ <ReviewCard key={`${item.id}-${index}`} item={item} />
115
+ ))}
116
+ </div>
117
+ </div>
118
+ </div>
119
+ )
120
+ }
121
+
122
+ export default ReviewsScroll
@@ -0,0 +1,127 @@
1
+ "use client"
2
+
3
+ import ReviewsScroll, { type ReviewItem } from "./reviews-scroll"
4
+
5
+ interface TestimonialsClientProps {
6
+ initialData?: {
7
+ title: string
8
+ eyebrow?: string
9
+ description?: string
10
+ testimonials: ReviewItem[]
11
+ } | null
12
+ }
13
+
14
+ const DEFAULT_EYEBROW = "Trusted by Those Who Lounge in Luxury"
15
+ const DEFAULT_TITLE_LINE_1 = "What Our Customers"
16
+ const DEFAULT_TITLE_LINE_2 = "Are Saying"
17
+ const DEFAULT_DESCRIPTION =
18
+ "Crafted with the finest materials and designed for the modern woman, our pieces promise unparalleled comfort without compromising on style. Join the community of women who have transformed their at-home attire into statements of sophistication"
19
+
20
+ const defaultTestimonials: ReviewItem[] = [
21
+ {
22
+ id: "1",
23
+ text: "Absolutely love the quality! The rompers are so soft, and the colors stay vibrant even after multiple washes.",
24
+ name: "Priya Sharma",
25
+ rating: 5,
26
+ },
27
+ {
28
+ id: "2",
29
+ text: "The sizing guide was perfect. Shipping was super fast — received it in 2 days. Packaging was cute and sustainable.",
30
+ name: "Rahul Mehta",
31
+ rating: 5,
32
+ },
33
+ {
34
+ id: "3",
35
+ text: "Finally found trendy clothes for my toddler that are comfortable. ChocoMelon is my go-to for birthday outfits now!",
36
+ name: "Anita Kulkarni",
37
+ rating: 5,
38
+ },
39
+ {
40
+ id: "4",
41
+ text: "Best quality kids clothing I have ever bought. The fabric is soft and the designs are adorable. Highly recommend!",
42
+ name: "Sneha Patel",
43
+ rating: 5,
44
+ },
45
+ ]
46
+
47
+ function HeaderStars() {
48
+ return (
49
+ <div className="customer-reviews__header-stars" aria-label="5 out of 5 stars">
50
+ {Array.from({ length: 5 }, (_, index) => (
51
+ <span
52
+ key={index}
53
+ className="customer-reviews__star customer-reviews__star--filled"
54
+ >
55
+
56
+ </span>
57
+ ))}
58
+ </div>
59
+ )
60
+ }
61
+
62
+ function ReviewTitle({ title }: { title: string }) {
63
+ const normalized = title.trim()
64
+ const upper = normalized.toUpperCase()
65
+
66
+ if (upper === "WHAT OUR CUSTOMERS ARE SAYING") {
67
+ return (
68
+ <h2 className="customer-reviews__title">
69
+ <span className="customer-reviews__title-line">{DEFAULT_TITLE_LINE_1}</span>
70
+ <span className="customer-reviews__title-line">{DEFAULT_TITLE_LINE_2}</span>
71
+ </h2>
72
+ )
73
+ }
74
+
75
+ const areIndex = upper.indexOf(" ARE ")
76
+ if (areIndex > 0) {
77
+ return (
78
+ <h2 className="customer-reviews__title">
79
+ <span>{normalized.slice(0, areIndex + 1)}</span>
80
+ <span>{normalized.slice(areIndex + 1).trim()}</span>
81
+ </h2>
82
+ )
83
+ }
84
+
85
+ return <h2 className="customer-reviews__title">{normalized}</h2>
86
+ }
87
+
88
+ const TestimonialsClient = ({ initialData }: TestimonialsClientProps) => {
89
+ const title = initialData?.title || `${DEFAULT_TITLE_LINE_1} ${DEFAULT_TITLE_LINE_2}`
90
+ const eyebrow = initialData?.eyebrow || DEFAULT_EYEBROW
91
+ const description = initialData?.description || DEFAULT_DESCRIPTION
92
+ const testimonials = (
93
+ initialData?.testimonials && initialData.testimonials.length > 0
94
+ ? initialData.testimonials
95
+ : defaultTestimonials
96
+ ).map((testimonial, index) => ({
97
+ ...testimonial,
98
+ id: testimonial.id || `testimonial-${index}`,
99
+ rating: Number.isFinite(testimonial.rating) ? testimonial.rating : 5,
100
+ }))
101
+
102
+ if (testimonials.length === 0) {
103
+ return null
104
+ }
105
+
106
+ return (
107
+ <section className="customer-reviews w-full bg-white">
108
+ <div className="customer-reviews__inner">
109
+ <div className="customer-reviews__layout">
110
+ <div className="customer-reviews__intro">
111
+ <div className="customer-reviews__intro-top">
112
+ <HeaderStars />
113
+ <p className="customer-reviews__eyebrow">{eyebrow}</p>
114
+ </div>
115
+
116
+ <ReviewTitle title={title} />
117
+ <p className="customer-reviews__description">{description}</p>
118
+ </div>
119
+
120
+ <ReviewsScroll items={testimonials} />
121
+ </div>
122
+ </div>
123
+ </section>
124
+ )
125
+ }
126
+
127
+ export default TestimonialsClient