medusa-ui-home 3.0.3 → 3.0.4
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/package.json +14 -19
- package/src/home/sections/customerStories/component/index.tsx +286 -0
- package/src/home/sections/customerStories/component/section-header.tsx +43 -0
- package/src/home/sections/customerStories/component/types.ts +14 -0
- package/src/home/sections/customerStories/copy-defaults.ts +15 -0
- package/src/home/sections/customerStories/index.ts +17 -0
- package/src/home/sections/customerStories/section.tsx +29 -0
- package/src/home/sections/customerStories/theme.ts +67 -0
- package/src/theme/default-home-theme.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "medusa-ui-home",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.4",
|
|
4
4
|
"description": "Homepage sections.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -21,11 +21,11 @@
|
|
|
21
21
|
"lodash": ">=4",
|
|
22
22
|
"lucide-react": "*",
|
|
23
23
|
"medusa-plugin-dynamic-config": ">=0.0.31",
|
|
24
|
-
"medusa-ui-home-config": "^1.
|
|
24
|
+
"medusa-ui-home-config": "^1.0.0",
|
|
25
25
|
"medusa-contact-logic-plugin": "^2.0.0",
|
|
26
26
|
"medusa-reviews-logic": "^2.0.0",
|
|
27
27
|
"medusa-storefront-analytics": "^1.0.0",
|
|
28
|
-
"medusa-storefront-data": "^2.
|
|
28
|
+
"medusa-storefront-data": "^2.0.0",
|
|
29
29
|
"medusa-storefront-hooks": "^1.0.0",
|
|
30
30
|
"medusa-wishlist-logic": "^2.0.0",
|
|
31
31
|
"medusa-storefront-theme-base": "^3.0.0",
|
|
@@ -122,6 +122,11 @@
|
|
|
122
122
|
"import": "./src/home/sections/testimonials/index.ts",
|
|
123
123
|
"default": "./src/home/sections/testimonials/index.ts"
|
|
124
124
|
},
|
|
125
|
+
"./sections/customerStories": {
|
|
126
|
+
"types": "./src/home/sections/customerStories/index.ts",
|
|
127
|
+
"import": "./src/home/sections/customerStories/index.ts",
|
|
128
|
+
"default": "./src/home/sections/customerStories/index.ts"
|
|
129
|
+
},
|
|
125
130
|
"./sections/brandPillars": {
|
|
126
131
|
"types": "./src/home/sections/brandPillars/index.ts",
|
|
127
132
|
"import": "./src/home/sections/brandPillars/index.ts",
|
|
@@ -161,16 +166,6 @@
|
|
|
161
166
|
"types": "./src/home/sections/blogPosts/index.ts",
|
|
162
167
|
"import": "./src/home/sections/blogPosts/index.ts",
|
|
163
168
|
"default": "./src/home/sections/blogPosts/index.ts"
|
|
164
|
-
},
|
|
165
|
-
"./catalog/page": {
|
|
166
|
-
"types": "./src/home/catalog/page.tsx",
|
|
167
|
-
"import": "./src/home/catalog/page.tsx",
|
|
168
|
-
"default": "./src/home/catalog/page.tsx"
|
|
169
|
-
},
|
|
170
|
-
"./components/home-page-json-ld": {
|
|
171
|
-
"types": "./src/home/components/home-page-json-ld.tsx",
|
|
172
|
-
"import": "./src/home/components/home-page-json-ld.tsx",
|
|
173
|
-
"default": "./src/home/components/home-page-json-ld.tsx"
|
|
174
169
|
}
|
|
175
170
|
},
|
|
176
171
|
"typesVersions": {
|
|
@@ -192,11 +187,11 @@
|
|
|
192
187
|
"@types/react-dom": "^18.3.5",
|
|
193
188
|
"next": "^15.0.0",
|
|
194
189
|
"typescript": "^5.6.0",
|
|
195
|
-
"medusa-storefront-analytics": "
|
|
196
|
-
"medusa-storefront-data": "
|
|
197
|
-
"medusa-storefront-theme-base": "
|
|
198
|
-
"medusa-ui-common": "
|
|
199
|
-
"medusa-ui-product": "
|
|
200
|
-
"medusa-ui-home-config": "
|
|
190
|
+
"medusa-storefront-analytics": "1.5.1",
|
|
191
|
+
"medusa-storefront-data": "2.5.9",
|
|
192
|
+
"medusa-storefront-theme-base": "3.0.1",
|
|
193
|
+
"medusa-ui-common": "3.0.1",
|
|
194
|
+
"medusa-ui-product": "3.0.2",
|
|
195
|
+
"medusa-ui-home-config": "1.1.3"
|
|
201
196
|
}
|
|
202
197
|
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from "react"
|
|
4
|
+
import Image from "next/image"
|
|
5
|
+
import type { HomeThemeClassNames } from "medusa-storefront-theme-base"
|
|
6
|
+
import { useThemeSection } from "medusa-ui-common/providers"
|
|
7
|
+
import { DEFAULT_CUSTOMER_STORIES_COPY } from "../copy-defaults"
|
|
8
|
+
import CustomerStoriesSectionHeader from "./section-header"
|
|
9
|
+
import type { CustomerStoryItem } from "./types"
|
|
10
|
+
|
|
11
|
+
const GAP_PX = 20
|
|
12
|
+
|
|
13
|
+
export type CustomerStoriesProps = {
|
|
14
|
+
title?: string
|
|
15
|
+
eyebrow?: string
|
|
16
|
+
items?: CustomerStoryItem[]
|
|
17
|
+
classNames?: Partial<HomeThemeClassNames>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function Stars({
|
|
21
|
+
rating,
|
|
22
|
+
classNames,
|
|
23
|
+
}: {
|
|
24
|
+
rating: number
|
|
25
|
+
classNames: { stars: string; starFilled: string; starMuted: string }
|
|
26
|
+
}) {
|
|
27
|
+
const clamped = Math.max(0, Math.min(5, Math.round(rating)))
|
|
28
|
+
return (
|
|
29
|
+
<div className={classNames.stars} aria-label={`${clamped} out of 5 stars`}>
|
|
30
|
+
{Array.from({ length: 5 }, (_, i) => (
|
|
31
|
+
<span
|
|
32
|
+
key={i}
|
|
33
|
+
className={i < clamped ? classNames.starFilled : classNames.starMuted}
|
|
34
|
+
>
|
|
35
|
+
★
|
|
36
|
+
</span>
|
|
37
|
+
))}
|
|
38
|
+
</div>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function Chevron({ dir }: { dir: "left" | "right" }) {
|
|
43
|
+
return (
|
|
44
|
+
<svg
|
|
45
|
+
width="18"
|
|
46
|
+
height="18"
|
|
47
|
+
viewBox="0 0 24 24"
|
|
48
|
+
fill="none"
|
|
49
|
+
stroke="currentColor"
|
|
50
|
+
strokeWidth="1.75"
|
|
51
|
+
strokeLinecap="round"
|
|
52
|
+
strokeLinejoin="round"
|
|
53
|
+
aria-hidden
|
|
54
|
+
>
|
|
55
|
+
{dir === "left" ? (
|
|
56
|
+
<path d="M15 18l-6-6 6-6" />
|
|
57
|
+
) : (
|
|
58
|
+
<path d="M9 18l6-6-6-6" />
|
|
59
|
+
)}
|
|
60
|
+
</svg>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function useCardsPerView() {
|
|
65
|
+
const [perView, setPerView] = useState(1)
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
const mq = window.matchMedia("(min-width: 768px)")
|
|
69
|
+
const update = () => setPerView(mq.matches ? 2 : 1)
|
|
70
|
+
update()
|
|
71
|
+
mq.addEventListener("change", update)
|
|
72
|
+
return () => mq.removeEventListener("change", update)
|
|
73
|
+
}, [])
|
|
74
|
+
|
|
75
|
+
return perView
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function StoryCard({
|
|
79
|
+
story,
|
|
80
|
+
classNames,
|
|
81
|
+
}: {
|
|
82
|
+
story: CustomerStoryItem
|
|
83
|
+
classNames: {
|
|
84
|
+
card: string
|
|
85
|
+
imageWrap: string
|
|
86
|
+
image: string
|
|
87
|
+
body: string
|
|
88
|
+
quote: string
|
|
89
|
+
stars: string
|
|
90
|
+
starFilled: string
|
|
91
|
+
starMuted: string
|
|
92
|
+
name: string
|
|
93
|
+
}
|
|
94
|
+
}) {
|
|
95
|
+
const rating = Number(story.rating ?? 5)
|
|
96
|
+
return (
|
|
97
|
+
<article className={classNames.card}>
|
|
98
|
+
<div className={classNames.imageWrap}>
|
|
99
|
+
<Image
|
|
100
|
+
src={story.image}
|
|
101
|
+
alt={`${story.name} — customer story`}
|
|
102
|
+
width={320}
|
|
103
|
+
height={400}
|
|
104
|
+
className={classNames.image}
|
|
105
|
+
loading="lazy"
|
|
106
|
+
unoptimized
|
|
107
|
+
/>
|
|
108
|
+
</div>
|
|
109
|
+
<div className={classNames.body}>
|
|
110
|
+
<p className={classNames.quote}>“{story.text}”</p>
|
|
111
|
+
<Stars
|
|
112
|
+
rating={Number.isFinite(rating) ? rating : 5}
|
|
113
|
+
classNames={{
|
|
114
|
+
stars: classNames.stars,
|
|
115
|
+
starFilled: classNames.starFilled,
|
|
116
|
+
starMuted: classNames.starMuted,
|
|
117
|
+
}}
|
|
118
|
+
/>
|
|
119
|
+
<p className={classNames.name}>{story.name}</p>
|
|
120
|
+
</div>
|
|
121
|
+
</article>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export default function CustomerStories({
|
|
126
|
+
title: titleProp,
|
|
127
|
+
eyebrow: eyebrowProp,
|
|
128
|
+
items: itemsProp,
|
|
129
|
+
classNames: classNamesProp,
|
|
130
|
+
}: CustomerStoriesProps) {
|
|
131
|
+
const cn = useThemeSection("home", classNamesProp)
|
|
132
|
+
const title = titleProp?.trim() || DEFAULT_CUSTOMER_STORIES_COPY.title
|
|
133
|
+
const eyebrow = eyebrowProp?.trim() || DEFAULT_CUSTOMER_STORIES_COPY.eyebrow
|
|
134
|
+
/** Use CMS/server items only; do not fall back to package copy when server sends an empty list. */
|
|
135
|
+
const items = Array.isArray(itemsProp)
|
|
136
|
+
? itemsProp
|
|
137
|
+
: DEFAULT_CUSTOMER_STORIES_COPY.items
|
|
138
|
+
|
|
139
|
+
const perView = useCardsPerView()
|
|
140
|
+
const [activeIndex, setActiveIndex] = useState(0)
|
|
141
|
+
const [translateX, setTranslateX] = useState(0)
|
|
142
|
+
const viewportRef = useRef<HTMLDivElement>(null)
|
|
143
|
+
const trackRef = useRef<HTMLDivElement>(null)
|
|
144
|
+
|
|
145
|
+
const maxIndex = Math.max(0, items.length - perView)
|
|
146
|
+
const dotCount = maxIndex + 1
|
|
147
|
+
|
|
148
|
+
const measureOffset = useCallback(() => {
|
|
149
|
+
const slide = trackRef.current?.querySelector<HTMLElement>("[data-story-slide]")
|
|
150
|
+
if (!slide) return 0
|
|
151
|
+
return activeIndex * (slide.offsetWidth + GAP_PX)
|
|
152
|
+
}, [activeIndex])
|
|
153
|
+
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
setTranslateX(measureOffset())
|
|
156
|
+
}, [measureOffset, perView])
|
|
157
|
+
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
const viewport = viewportRef.current
|
|
160
|
+
if (!viewport) return
|
|
161
|
+
const ro = new ResizeObserver(() => setTranslateX(measureOffset()))
|
|
162
|
+
ro.observe(viewport)
|
|
163
|
+
return () => ro.disconnect()
|
|
164
|
+
}, [measureOffset])
|
|
165
|
+
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
setActiveIndex((i) => Math.min(i, maxIndex))
|
|
168
|
+
}, [maxIndex, perView])
|
|
169
|
+
|
|
170
|
+
const goTo = useCallback(
|
|
171
|
+
(index: number) => {
|
|
172
|
+
setActiveIndex(Math.min(Math.max(index, 0), maxIndex))
|
|
173
|
+
},
|
|
174
|
+
[maxIndex]
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
if (items.length === 0) return null
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<section
|
|
181
|
+
className={`${cn.customerStoriesSection ?? ""} ${cn.section ?? ""}`}
|
|
182
|
+
aria-labelledby="customer-stories-heading"
|
|
183
|
+
>
|
|
184
|
+
<CustomerStoriesSectionHeader
|
|
185
|
+
eyebrow={eyebrow}
|
|
186
|
+
title={title}
|
|
187
|
+
id="customer-stories-heading"
|
|
188
|
+
classNames={{
|
|
189
|
+
wrap: cn.customerStoriesHeaderWrap ?? "",
|
|
190
|
+
inner: cn.customerStoriesHeaderInner ?? "",
|
|
191
|
+
header: cn.customerStoriesHeader ?? "",
|
|
192
|
+
eyebrow: cn.customerStoriesEyebrow ?? "",
|
|
193
|
+
heading: cn.customerStoriesTitle ?? "",
|
|
194
|
+
accent: cn.customerStoriesAccent ?? "",
|
|
195
|
+
accentLine: cn.customerStoriesAccentLine ?? "",
|
|
196
|
+
accentGem: cn.customerStoriesAccentGem ?? "",
|
|
197
|
+
}}
|
|
198
|
+
/>
|
|
199
|
+
|
|
200
|
+
<div className={cn.customerStoriesInner ?? ""}>
|
|
201
|
+
<div className={cn.customerStoriesCarouselWrap ?? ""}>
|
|
202
|
+
<div className={cn.customerStoriesSlidesRow ?? ""}>
|
|
203
|
+
{dotCount > 1 ? (
|
|
204
|
+
<>
|
|
205
|
+
<button
|
|
206
|
+
type="button"
|
|
207
|
+
className={`${cn.customerStoriesNavButton ?? ""} ${cn.customerStoriesNavPrev ?? ""}`}
|
|
208
|
+
onClick={() => goTo(activeIndex - 1)}
|
|
209
|
+
disabled={activeIndex === 0}
|
|
210
|
+
aria-label="Previous customer story"
|
|
211
|
+
>
|
|
212
|
+
<Chevron dir="left" />
|
|
213
|
+
</button>
|
|
214
|
+
<button
|
|
215
|
+
type="button"
|
|
216
|
+
className={`${cn.customerStoriesNavButton ?? ""} ${cn.customerStoriesNavNext ?? ""}`}
|
|
217
|
+
onClick={() => goTo(activeIndex + 1)}
|
|
218
|
+
disabled={activeIndex >= maxIndex}
|
|
219
|
+
aria-label="Next customer story"
|
|
220
|
+
>
|
|
221
|
+
<Chevron dir="right" />
|
|
222
|
+
</button>
|
|
223
|
+
</>
|
|
224
|
+
) : null}
|
|
225
|
+
|
|
226
|
+
<div ref={viewportRef} className={cn.customerStoriesViewport ?? ""}>
|
|
227
|
+
<div
|
|
228
|
+
ref={trackRef}
|
|
229
|
+
className={cn.customerStoriesTrack ?? ""}
|
|
230
|
+
style={{ transform: `translate3d(-${translateX}px, 0, 0)` }}
|
|
231
|
+
>
|
|
232
|
+
{items.map((story, index) => (
|
|
233
|
+
<div
|
|
234
|
+
key={story.id}
|
|
235
|
+
data-story-slide
|
|
236
|
+
className={cn.customerStoriesSlide ?? ""}
|
|
237
|
+
aria-hidden={
|
|
238
|
+
index < activeIndex || index >= activeIndex + perView
|
|
239
|
+
}
|
|
240
|
+
>
|
|
241
|
+
<StoryCard
|
|
242
|
+
story={story}
|
|
243
|
+
classNames={{
|
|
244
|
+
card: cn.customerStoriesCard ?? "",
|
|
245
|
+
imageWrap: cn.customerStoriesImageWrap ?? "",
|
|
246
|
+
image: cn.customerStoriesImage ?? "",
|
|
247
|
+
body: cn.customerStoriesBody ?? "",
|
|
248
|
+
quote: cn.customerStoriesQuote ?? "",
|
|
249
|
+
stars: cn.customerStoriesStars ?? "",
|
|
250
|
+
starFilled: cn.customerStoriesStarFilled ?? "",
|
|
251
|
+
starMuted: cn.customerStoriesStarMuted ?? "",
|
|
252
|
+
name: cn.customerStoriesName ?? "",
|
|
253
|
+
}}
|
|
254
|
+
/>
|
|
255
|
+
</div>
|
|
256
|
+
))}
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
|
|
261
|
+
{dotCount > 1 ? (
|
|
262
|
+
<div
|
|
263
|
+
className={cn.customerStoriesDots ?? ""}
|
|
264
|
+
role="tablist"
|
|
265
|
+
aria-label="Story slides"
|
|
266
|
+
>
|
|
267
|
+
{Array.from({ length: dotCount }).map((_, i) => (
|
|
268
|
+
<button
|
|
269
|
+
key={i}
|
|
270
|
+
type="button"
|
|
271
|
+
role="tab"
|
|
272
|
+
aria-selected={i === activeIndex}
|
|
273
|
+
aria-label={`Go to story ${i + 1}`}
|
|
274
|
+
className={`${cn.customerStoriesDot ?? ""} ${
|
|
275
|
+
i === activeIndex ? cn.customerStoriesDotActive ?? "" : ""
|
|
276
|
+
}`}
|
|
277
|
+
onClick={() => goTo(i)}
|
|
278
|
+
/>
|
|
279
|
+
))}
|
|
280
|
+
</div>
|
|
281
|
+
) : null}
|
|
282
|
+
</div>
|
|
283
|
+
</div>
|
|
284
|
+
</section>
|
|
285
|
+
)
|
|
286
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
type CustomerStoriesSectionHeaderProps = {
|
|
2
|
+
eyebrow?: string | null
|
|
3
|
+
title: string
|
|
4
|
+
id?: string
|
|
5
|
+
classNames: {
|
|
6
|
+
wrap: string
|
|
7
|
+
inner: string
|
|
8
|
+
header: string
|
|
9
|
+
eyebrow: string
|
|
10
|
+
heading: string
|
|
11
|
+
accent: string
|
|
12
|
+
accentLine: string
|
|
13
|
+
accentGem: string
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default function CustomerStoriesSectionHeader({
|
|
18
|
+
eyebrow,
|
|
19
|
+
title,
|
|
20
|
+
id,
|
|
21
|
+
classNames: cn,
|
|
22
|
+
}: CustomerStoriesSectionHeaderProps) {
|
|
23
|
+
const trimmed = title.trim()
|
|
24
|
+
if (!trimmed) return null
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className={cn.wrap}>
|
|
28
|
+
<div className={cn.inner}>
|
|
29
|
+
<header className={cn.header}>
|
|
30
|
+
{eyebrow?.trim() ? <p className={cn.eyebrow}>{eyebrow.trim()}</p> : null}
|
|
31
|
+
<h2 id={id} className={cn.heading}>
|
|
32
|
+
{trimmed}
|
|
33
|
+
</h2>
|
|
34
|
+
<div className={cn.accent} aria-hidden>
|
|
35
|
+
<span className={cn.accentLine} />
|
|
36
|
+
<span className={cn.accentGem} />
|
|
37
|
+
<span className={`${cn.accentLine} rotate-180`} />
|
|
38
|
+
</div>
|
|
39
|
+
</header>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { customerStoriesSectionDefaults } from "medusa-ui-home-config/sections/customerStories/defaults"
|
|
2
|
+
import type { CustomerStoryItem } from "./component/types"
|
|
3
|
+
|
|
4
|
+
export type CustomerStoriesCopy = {
|
|
5
|
+
eyebrow: string
|
|
6
|
+
title: string
|
|
7
|
+
items: CustomerStoryItem[]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** UI fallback copy — sourced from medusa-ui-home-config (single canonical data.json). */
|
|
11
|
+
export const DEFAULT_CUSTOMER_STORIES_COPY: CustomerStoriesCopy = {
|
|
12
|
+
eyebrow: customerStoriesSectionDefaults.eyebrow,
|
|
13
|
+
title: customerStoriesSectionDefaults.title,
|
|
14
|
+
items: customerStoriesSectionDefaults.items as CustomerStoryItem[],
|
|
15
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { default } from "./section"
|
|
2
|
+
export * from "./section"
|
|
3
|
+
export {
|
|
4
|
+
defaultTheme,
|
|
5
|
+
mergeSectionTheme,
|
|
6
|
+
type ThemeOverrides,
|
|
7
|
+
} from "./theme"
|
|
8
|
+
export { sectionTheme } from "./theme"
|
|
9
|
+
export {
|
|
10
|
+
DEFAULT_CUSTOMER_STORIES_COPY,
|
|
11
|
+
type CustomerStoriesCopy,
|
|
12
|
+
} from "./copy-defaults"
|
|
13
|
+
export type {
|
|
14
|
+
CustomerStoryItem,
|
|
15
|
+
CustomerStoriesContent,
|
|
16
|
+
} from "./component/types"
|
|
17
|
+
export * from "medusa-ui-home-config/sections/customerStories"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ThemeOverrides } from "./theme"
|
|
2
|
+
import type { CustomerStoriesSectionData } from "medusa-storefront-data/home/sections/customerStories"
|
|
3
|
+
import CustomerStories from "./component"
|
|
4
|
+
import {
|
|
5
|
+
copyEyebrow,
|
|
6
|
+
copyTitle,
|
|
7
|
+
} from "medusa-ui-home/home/lib/section-copy"
|
|
8
|
+
|
|
9
|
+
export type CustomerStoriesSectionProps = CustomerStoriesSectionData & {
|
|
10
|
+
/** Override class names for slots this section uses. */
|
|
11
|
+
theme?: ThemeOverrides
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function CustomerStoriesSection({
|
|
15
|
+
customerStories,
|
|
16
|
+
copy,
|
|
17
|
+
theme,
|
|
18
|
+
}: CustomerStoriesSectionProps) {
|
|
19
|
+
return (
|
|
20
|
+
<CustomerStories
|
|
21
|
+
classNames={theme}
|
|
22
|
+
title={copyTitle(copy) || customerStories.title}
|
|
23
|
+
eyebrow={copyEyebrow(copy) || customerStories.eyebrow}
|
|
24
|
+
items={customerStories.items}
|
|
25
|
+
/>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default CustomerStoriesSection
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { mergeThemeSlots } from "medusa-storefront-theme-base"
|
|
2
|
+
import { pickThemeKeys } from "../../theme/pick-theme"
|
|
3
|
+
import { sharedHomeTheme } from "../../theme/shared"
|
|
4
|
+
|
|
5
|
+
/** Slots unique to this section (merged into the global home theme). */
|
|
6
|
+
export const sectionTheme = {
|
|
7
|
+
customerStoriesSection:
|
|
8
|
+
"w-full bg-[color-mix(in_srgb,var(--sf-color-secondary)_8%,var(--sf-color-background))] py-10 md:py-12 lg:py-14",
|
|
9
|
+
customerStoriesInner: "mx-auto w-full max-w-[1440px] px-4 sm:px-6 lg:px-8",
|
|
10
|
+
customerStoriesHeaderWrap: "w-full px-4 sm:px-6 md:px-8 lg:px-12",
|
|
11
|
+
customerStoriesHeaderInner: "mx-auto w-full max-w-[1440px]",
|
|
12
|
+
customerStoriesHeader:
|
|
13
|
+
"mb-5 flex flex-col items-center text-center sm:mb-6 md:mb-7",
|
|
14
|
+
customerStoriesEyebrow:
|
|
15
|
+
"mb-1.5 text-[0.6rem] font-medium uppercase tracking-[0.24em] text-[var(--sf-color-text-muted)] sm:text-[0.65rem] [font-family:var(--sf-font-family)]",
|
|
16
|
+
customerStoriesTitle:
|
|
17
|
+
"text-2xl font-semibold leading-tight tracking-tight text-[var(--sf-color-primary)] sm:text-[1.75rem] md:text-3xl [font-family:var(--sf-font-family-heading)]",
|
|
18
|
+
customerStoriesAccent: "mx-auto mt-3 flex items-center justify-center gap-2",
|
|
19
|
+
customerStoriesAccentLine:
|
|
20
|
+
"h-px w-8 bg-gradient-to-r from-transparent to-[var(--sf-color-secondary)] sm:w-10",
|
|
21
|
+
customerStoriesAccentGem: "h-1 w-1 rotate-45 bg-[var(--sf-color-secondary)]",
|
|
22
|
+
customerStoriesCarouselWrap: "relative",
|
|
23
|
+
customerStoriesSlidesRow: "relative px-8 sm:px-10 md:px-12",
|
|
24
|
+
customerStoriesViewport: "w-full overflow-hidden",
|
|
25
|
+
customerStoriesTrack:
|
|
26
|
+
"flex gap-5 motion-safe:transition-transform motion-safe:duration-[550ms] motion-safe:ease-[cubic-bezier(0.4,0,0.2,1)]",
|
|
27
|
+
customerStoriesSlide: "w-full shrink-0 md:w-[calc((100%-20px)/2)]",
|
|
28
|
+
customerStoriesCard:
|
|
29
|
+
"flex h-[250px] w-full flex-row overflow-hidden rounded-[8px] border border-[var(--sf-color-border)] bg-[var(--sf-color-surface)] shadow-sm md:h-[272px]",
|
|
30
|
+
customerStoriesImageWrap:
|
|
31
|
+
"relative h-full w-[40%] shrink-0 overflow-hidden bg-[color-mix(in_srgb,var(--sf-color-text-muted)_12%,var(--sf-color-background-alt))]",
|
|
32
|
+
customerStoriesImage: "block h-full w-full object-cover object-top",
|
|
33
|
+
customerStoriesBody:
|
|
34
|
+
"flex min-w-0 flex-1 flex-col items-center justify-center px-6 py-5 text-center md:px-8 md:py-6",
|
|
35
|
+
customerStoriesQuote:
|
|
36
|
+
"max-w-[15.5rem] text-[13px] font-normal italic leading-[1.7] text-[var(--sf-color-text-muted)] sm:max-w-[16.5rem] sm:text-[14px] md:max-w-[17.5rem] [font-family:var(--sf-font-family-heading)]",
|
|
37
|
+
customerStoriesStars: "mt-3 flex items-center gap-1",
|
|
38
|
+
customerStoriesStarFilled: "text-sm leading-none text-[var(--sf-color-secondary)]",
|
|
39
|
+
customerStoriesStarMuted:
|
|
40
|
+
"text-sm leading-none text-[color-mix(in_srgb,var(--sf-color-text-muted)_35%,transparent)]",
|
|
41
|
+
customerStoriesName:
|
|
42
|
+
"mt-[1.125rem] text-[14px] font-bold leading-tight text-[var(--sf-color-text)] md:mt-5 md:text-[15px] [font-family:var(--sf-font-family)]",
|
|
43
|
+
customerStoriesNavButton:
|
|
44
|
+
"absolute top-1/2 z-20 flex h-8 w-8 -translate-y-1/2 items-center justify-center border-0 bg-transparent p-0 text-[var(--sf-color-text-muted)] transition-colors hover:text-[var(--sf-color-text)] disabled:pointer-events-none disabled:opacity-25",
|
|
45
|
+
customerStoriesNavPrev: "left-0",
|
|
46
|
+
customerStoriesNavNext: "right-0",
|
|
47
|
+
customerStoriesDots: "mt-7 flex items-center justify-center gap-2 md:mt-8",
|
|
48
|
+
customerStoriesDot:
|
|
49
|
+
"h-[7px] w-[7px] rounded-full bg-[color-mix(in_srgb,var(--sf-color-text-muted)_35%,transparent)] p-0 transition-colors duration-300",
|
|
50
|
+
customerStoriesDotActive: "!bg-[var(--sf-color-text)]",
|
|
51
|
+
} as const
|
|
52
|
+
|
|
53
|
+
export const themeKeys = ["section"] as const
|
|
54
|
+
|
|
55
|
+
const sharedDefaults = pickThemeKeys(sharedHomeTheme, themeKeys)
|
|
56
|
+
|
|
57
|
+
export const defaultTheme = {
|
|
58
|
+
...sharedDefaults,
|
|
59
|
+
...sectionTheme,
|
|
60
|
+
} as const
|
|
61
|
+
|
|
62
|
+
export type ThemeOverrides = Partial<typeof defaultTheme>
|
|
63
|
+
|
|
64
|
+
/** Merge site or page-level overrides onto this section's default theme slots. */
|
|
65
|
+
export function mergeSectionTheme(overrides?: ThemeOverrides) {
|
|
66
|
+
return mergeThemeSlots(defaultTheme, overrides)
|
|
67
|
+
}
|
|
@@ -8,6 +8,7 @@ import { sectionTheme as baptismPicksSectionTheme } from "../home/sections/bapti
|
|
|
8
8
|
import { sectionTheme as testimonialsSectionTheme } from "../home/sections/testimonials/theme"
|
|
9
9
|
import { sectionTheme as instagramPostsSectionTheme } from "../home/sections/instagramPosts/theme"
|
|
10
10
|
import { sectionTheme as videoStoriesSectionTheme } from "../home/sections/videoStories/theme"
|
|
11
|
+
import { sectionTheme as customerStoriesSectionTheme } from "../home/sections/customerStories/theme"
|
|
11
12
|
|
|
12
13
|
export const defaultHomeThemeClassNames = {
|
|
13
14
|
...sharedHomeTheme,
|
|
@@ -19,6 +20,7 @@ export const defaultHomeThemeClassNames = {
|
|
|
19
20
|
...testimonialsSectionTheme,
|
|
20
21
|
...instagramPostsSectionTheme,
|
|
21
22
|
...videoStoriesSectionTheme,
|
|
23
|
+
...customerStoriesSectionTheme,
|
|
22
24
|
} as const
|
|
23
25
|
|
|
24
26
|
export type HomeThemeSlots = {
|