medusa-ui-home 2.3.0 → 2.4.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/package.json +131 -7
- package/src/home/components/baptism-picks/index.tsx +2 -70
- package/src/home/components/category-pills/index.tsx +126 -44
- package/src/home/components/category-pills/utils.ts +132 -0
- package/src/home/components/dynamic-banner/index.tsx +48 -43
- package/src/home/components/hero/index.tsx +11 -2
- package/src/home/components/instagram-posts/index.tsx +199 -0
- package/src/home/components/new-arrivals/classic.tsx +65 -0
- package/src/home/components/new-arrivals/grid.tsx +58 -0
- package/src/home/components/new-arrivals/index.tsx +21 -55
- package/src/home/components/new-arrivals/types.ts +13 -0
- package/src/home/components/promo-announcements/index.tsx +3 -5
- package/src/home/components/promo-countdown/index.tsx +56 -36
- package/src/home/components/shared/product-grid-section.tsx +10 -9
- package/src/home/components/shoppable-gallery/index.tsx +232 -0
- package/src/home/components/testimonials/index.tsx +139 -686
- package/src/home/components/why-choose-us/dynamic-features.tsx +13 -7
- package/src/home/components/why-choose-us/index.tsx +16 -2
- package/src/home/home-content.tsx +4 -1
- package/src/home/lib/section-copy.ts +8 -0
- package/src/home/register-sections.ts +52 -43
- package/src/home/sections/about-brand-section.tsx +20 -14
- package/src/home/sections/adapt-home-section.tsx +13 -0
- package/src/home/sections/baptism-picks-section.tsx +18 -6
- package/src/home/sections/baptism-section.tsx +29 -9
- package/src/home/sections/blog-posts-section.tsx +17 -5
- package/src/home/sections/brand-marquee-section.tsx +14 -9
- package/src/home/sections/brand-pillars-section.tsx +19 -11
- package/src/home/sections/category-pills-section.tsx +13 -3
- package/src/home/sections/celebrity-trust-section.tsx +23 -11
- package/src/home/sections/features-section.tsx +20 -13
- package/src/home/sections/hero-section.tsx +16 -3
- package/src/home/sections/instagram-posts-section.tsx +32 -0
- package/src/home/sections/loved-by-moms-section.tsx +26 -12
- package/src/home/sections/luxe-favourites-section.tsx +23 -7
- package/src/home/sections/new-arrivals-classic-section.tsx +36 -0
- package/src/home/sections/new-arrivals-section.tsx +37 -8
- package/src/home/sections/promo-announcements-section.tsx +13 -3
- package/src/home/sections/promo-countdown-section.tsx +14 -3
- package/src/home/sections/shop-by-age-section.tsx +18 -10
- package/src/home/sections/shop-by-category-section.tsx +17 -9
- package/src/home/sections/testimonials-section.tsx +23 -20
- package/src/home/sections/theme-dresses-section.tsx +26 -12
- package/src/home/sections/video-stories-section.tsx +17 -9
- package/src/home/sections/why-choose-us-section.tsx +26 -5
- package/src/home/segment-data/_comment.json +1 -0
- package/src/home/segment-data/aboutBrand.json +20 -0
- package/src/home/segment-data/baptism.json +3 -0
- package/src/home/segment-data/baptismPicks.json +4 -0
- package/src/home/segment-data/blogPosts.json +11 -0
- package/src/home/segment-data/brandMarquee.json +3 -0
- package/src/home/segment-data/brandPillars.json +10 -0
- package/src/home/segment-data/categoryPills.json +1 -0
- package/src/home/segment-data/celebrityTrust.json +4 -0
- package/src/home/segment-data/features.json +14 -0
- package/src/home/segment-data/hero.json +1 -0
- package/src/home/segment-data/homepage-config.json +57 -0
- package/src/home/segment-data/index.ts +62 -0
- package/src/home/segment-data/instagramPosts.json +7 -0
- package/src/home/segment-data/lovedByMoms.json +4 -0
- package/src/home/segment-data/luxeFavourites.json +3 -0
- package/src/home/segment-data/newArrivals.json +5 -0
- package/src/home/segment-data/newArrivalsClassic.json +5 -0
- package/src/home/segment-data/promoAnnouncements.json +8 -0
- package/src/home/segment-data/promoCountdown.json +8 -0
- package/src/home/segment-data/shopByAge.json +3 -0
- package/src/home/segment-data/shopByCategory.json +4 -0
- package/src/home/segment-data/testimonials.json +14 -0
- package/src/home/segment-data/themeDresses.json +4 -0
- package/src/home/segment-data/videoStories.json +4 -0
- package/src/home/segment-data/whyChooseUs.json +9 -0
- package/src/theme/default-home-theme.ts +71 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "medusa-ui-home",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "Homepage sections.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -46,10 +46,125 @@
|
|
|
46
46
|
"import": "./src/theme/index.ts",
|
|
47
47
|
"default": "./src/theme/index.ts"
|
|
48
48
|
},
|
|
49
|
-
"./
|
|
50
|
-
"types": "./src/home
|
|
51
|
-
"import": "./src/home
|
|
52
|
-
"default": "./src/home
|
|
49
|
+
"./sections/promoAnnouncements": {
|
|
50
|
+
"types": "./src/home/sections/promo-announcements-section.tsx",
|
|
51
|
+
"import": "./src/home/sections/promo-announcements-section.tsx",
|
|
52
|
+
"default": "./src/home/sections/promo-announcements-section.tsx"
|
|
53
|
+
},
|
|
54
|
+
"./sections/hero": {
|
|
55
|
+
"types": "./src/home/sections/hero-section.tsx",
|
|
56
|
+
"import": "./src/home/sections/hero-section.tsx",
|
|
57
|
+
"default": "./src/home/sections/hero-section.tsx"
|
|
58
|
+
},
|
|
59
|
+
"./sections/categoryPills": {
|
|
60
|
+
"types": "./src/home/sections/category-pills-section.tsx",
|
|
61
|
+
"import": "./src/home/sections/category-pills-section.tsx",
|
|
62
|
+
"default": "./src/home/sections/category-pills-section.tsx"
|
|
63
|
+
},
|
|
64
|
+
"./sections/newArrivals": {
|
|
65
|
+
"types": "./src/home/sections/new-arrivals-section.tsx",
|
|
66
|
+
"import": "./src/home/sections/new-arrivals-section.tsx",
|
|
67
|
+
"default": "./src/home/sections/new-arrivals-section.tsx"
|
|
68
|
+
},
|
|
69
|
+
"./sections/newArrivalsClassic": {
|
|
70
|
+
"types": "./src/home/sections/new-arrivals-classic-section.tsx",
|
|
71
|
+
"import": "./src/home/sections/new-arrivals-classic-section.tsx",
|
|
72
|
+
"default": "./src/home/sections/new-arrivals-classic-section.tsx"
|
|
73
|
+
},
|
|
74
|
+
"./sections/promoCountdown": {
|
|
75
|
+
"types": "./src/home/sections/promo-countdown-section.tsx",
|
|
76
|
+
"import": "./src/home/sections/promo-countdown-section.tsx",
|
|
77
|
+
"default": "./src/home/sections/promo-countdown-section.tsx"
|
|
78
|
+
},
|
|
79
|
+
"./sections/celebrityTrust": {
|
|
80
|
+
"types": "./src/home/sections/celebrity-trust-section.tsx",
|
|
81
|
+
"import": "./src/home/sections/celebrity-trust-section.tsx",
|
|
82
|
+
"default": "./src/home/sections/celebrity-trust-section.tsx"
|
|
83
|
+
},
|
|
84
|
+
"./sections/luxeFavourites": {
|
|
85
|
+
"types": "./src/home/sections/luxe-favourites-section.tsx",
|
|
86
|
+
"import": "./src/home/sections/luxe-favourites-section.tsx",
|
|
87
|
+
"default": "./src/home/sections/luxe-favourites-section.tsx"
|
|
88
|
+
},
|
|
89
|
+
"./sections/shopByCategory": {
|
|
90
|
+
"types": "./src/home/sections/shop-by-category-section.tsx",
|
|
91
|
+
"import": "./src/home/sections/shop-by-category-section.tsx",
|
|
92
|
+
"default": "./src/home/sections/shop-by-category-section.tsx"
|
|
93
|
+
},
|
|
94
|
+
"./sections/baptism": {
|
|
95
|
+
"types": "./src/home/sections/baptism-section.tsx",
|
|
96
|
+
"import": "./src/home/sections/baptism-section.tsx",
|
|
97
|
+
"default": "./src/home/sections/baptism-section.tsx"
|
|
98
|
+
},
|
|
99
|
+
"./sections/baptismPicks": {
|
|
100
|
+
"types": "./src/home/sections/baptism-picks-section.tsx",
|
|
101
|
+
"import": "./src/home/sections/baptism-picks-section.tsx",
|
|
102
|
+
"default": "./src/home/sections/baptism-picks-section.tsx"
|
|
103
|
+
},
|
|
104
|
+
"./sections/themeDresses": {
|
|
105
|
+
"types": "./src/home/sections/theme-dresses-section.tsx",
|
|
106
|
+
"import": "./src/home/sections/theme-dresses-section.tsx",
|
|
107
|
+
"default": "./src/home/sections/theme-dresses-section.tsx"
|
|
108
|
+
},
|
|
109
|
+
"./sections/brandMarquee": {
|
|
110
|
+
"types": "./src/home/sections/brand-marquee-section.tsx",
|
|
111
|
+
"import": "./src/home/sections/brand-marquee-section.tsx",
|
|
112
|
+
"default": "./src/home/sections/brand-marquee-section.tsx"
|
|
113
|
+
},
|
|
114
|
+
"./sections/aboutBrand": {
|
|
115
|
+
"types": "./src/home/sections/about-brand-section.tsx",
|
|
116
|
+
"import": "./src/home/sections/about-brand-section.tsx",
|
|
117
|
+
"default": "./src/home/sections/about-brand-section.tsx"
|
|
118
|
+
},
|
|
119
|
+
"./sections/testimonials": {
|
|
120
|
+
"types": "./src/home/sections/testimonials-section.tsx",
|
|
121
|
+
"import": "./src/home/sections/testimonials-section.tsx",
|
|
122
|
+
"default": "./src/home/sections/testimonials-section.tsx"
|
|
123
|
+
},
|
|
124
|
+
"./sections/brandPillars": {
|
|
125
|
+
"types": "./src/home/sections/brand-pillars-section.tsx",
|
|
126
|
+
"import": "./src/home/sections/brand-pillars-section.tsx",
|
|
127
|
+
"default": "./src/home/sections/brand-pillars-section.tsx"
|
|
128
|
+
},
|
|
129
|
+
"./sections/instagramPosts": {
|
|
130
|
+
"types": "./src/home/sections/instagram-posts-section.tsx",
|
|
131
|
+
"import": "./src/home/sections/instagram-posts-section.tsx",
|
|
132
|
+
"default": "./src/home/sections/instagram-posts-section.tsx"
|
|
133
|
+
},
|
|
134
|
+
"./sections/features": {
|
|
135
|
+
"types": "./src/home/sections/features-section.tsx",
|
|
136
|
+
"import": "./src/home/sections/features-section.tsx",
|
|
137
|
+
"default": "./src/home/sections/features-section.tsx"
|
|
138
|
+
},
|
|
139
|
+
"./sections/shopByAge": {
|
|
140
|
+
"types": "./src/home/sections/shop-by-age-section.tsx",
|
|
141
|
+
"import": "./src/home/sections/shop-by-age-section.tsx",
|
|
142
|
+
"default": "./src/home/sections/shop-by-age-section.tsx"
|
|
143
|
+
},
|
|
144
|
+
"./sections/whyChooseUs": {
|
|
145
|
+
"types": "./src/home/sections/why-choose-us-section.tsx",
|
|
146
|
+
"import": "./src/home/sections/why-choose-us-section.tsx",
|
|
147
|
+
"default": "./src/home/sections/why-choose-us-section.tsx"
|
|
148
|
+
},
|
|
149
|
+
"./sections/lovedByMoms": {
|
|
150
|
+
"types": "./src/home/sections/loved-by-moms-section.tsx",
|
|
151
|
+
"import": "./src/home/sections/loved-by-moms-section.tsx",
|
|
152
|
+
"default": "./src/home/sections/loved-by-moms-section.tsx"
|
|
153
|
+
},
|
|
154
|
+
"./sections/videoStories": {
|
|
155
|
+
"types": "./src/home/sections/video-stories-section.tsx",
|
|
156
|
+
"import": "./src/home/sections/video-stories-section.tsx",
|
|
157
|
+
"default": "./src/home/sections/video-stories-section.tsx"
|
|
158
|
+
},
|
|
159
|
+
"./sections/blogPosts": {
|
|
160
|
+
"types": "./src/home/sections/blog-posts-section.tsx",
|
|
161
|
+
"import": "./src/home/sections/blog-posts-section.tsx",
|
|
162
|
+
"default": "./src/home/sections/blog-posts-section.tsx"
|
|
163
|
+
},
|
|
164
|
+
"./sections/adapt-home-section": {
|
|
165
|
+
"types": "./src/home/sections/adapt-home-section.tsx",
|
|
166
|
+
"import": "./src/home/sections/adapt-home-section.tsx",
|
|
167
|
+
"default": "./src/home/sections/adapt-home-section.tsx"
|
|
53
168
|
},
|
|
54
169
|
"./home/register-sections": {
|
|
55
170
|
"types": "./src/home/register-sections.ts",
|
|
@@ -64,8 +179,17 @@
|
|
|
64
179
|
},
|
|
65
180
|
"typesVersions": {
|
|
66
181
|
"*": {
|
|
67
|
-
"
|
|
68
|
-
"src
|
|
182
|
+
"sections/*": [
|
|
183
|
+
"src/home/sections/*-section.tsx"
|
|
184
|
+
],
|
|
185
|
+
"theme": [
|
|
186
|
+
"src/theme/index.ts"
|
|
187
|
+
],
|
|
188
|
+
"home/register-sections": [
|
|
189
|
+
"src/home/register-sections.ts"
|
|
190
|
+
],
|
|
191
|
+
"home/home-content": [
|
|
192
|
+
"src/home/home-content.tsx"
|
|
69
193
|
]
|
|
70
194
|
}
|
|
71
195
|
},
|
|
@@ -1,70 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import Image from "next/image"
|
|
4
|
-
import { HttpTypes } from "@medusajs/types"
|
|
5
|
-
import LocalizedClientLink from "medusa-ui-common/common/components/localized-client-link"
|
|
6
|
-
import type { HomeThemeClassNames } from "medusa-storefront-theme-base"
|
|
7
|
-
import { useThemeSection } from "medusa-ui-common/providers"
|
|
8
|
-
|
|
9
|
-
type BaptismPicksProps = {
|
|
10
|
-
title?: string
|
|
11
|
-
products: HttpTypes.StoreProduct[]
|
|
12
|
-
classNames?: Partial<HomeThemeClassNames>
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export default function BaptismPicks({
|
|
16
|
-
title,
|
|
17
|
-
products,
|
|
18
|
-
classNames: classNamesProp,
|
|
19
|
-
}: BaptismPicksProps) {
|
|
20
|
-
const cn = useThemeSection("home", classNamesProp)
|
|
21
|
-
const items = (products ?? []).slice(0, 5)
|
|
22
|
-
if (items.length === 0) return null
|
|
23
|
-
|
|
24
|
-
return (
|
|
25
|
-
<section className={`${cn.sectionPad} ${cn.section}`}>
|
|
26
|
-
<div className="px-4 sm:px-6 md:px-8">
|
|
27
|
-
<div className="mx-auto max-w-[1360px]">
|
|
28
|
-
{title ? <h2 className={`${cn.sectionTitle} mb-8`}>{title}</h2> : null}
|
|
29
|
-
<div className="flex gap-4 sm:gap-6 overflow-x-auto pb-2 justify-center">
|
|
30
|
-
{items.map((product, index) => {
|
|
31
|
-
const thumb = product.thumbnail || product.images?.[0]?.url
|
|
32
|
-
return (
|
|
33
|
-
<LocalizedClientLink
|
|
34
|
-
key={product.id}
|
|
35
|
-
href={`/products/${product.handle}`}
|
|
36
|
-
className="shrink-0 flex flex-col items-center gap-3 group"
|
|
37
|
-
>
|
|
38
|
-
<div className="relative">
|
|
39
|
-
<span className="absolute -top-2 -right-2 z-10 flex h-7 w-7 items-center justify-center rounded-full bg-[var(--sf-btn-primary)] text-xs font-bold text-[var(--sf-color-text-on-primary)]">
|
|
40
|
-
{index + 1}
|
|
41
|
-
</span>
|
|
42
|
-
<div
|
|
43
|
-
className={`w-24 h-24 sm:w-28 sm:h-28 rounded-2xl overflow-hidden ring-2 ring-[var(--sf-color-border)] group-hover:ring-[var(--sf-color-primary)] ${cn.cardSurface}`}
|
|
44
|
-
>
|
|
45
|
-
{thumb ? (
|
|
46
|
-
<Image
|
|
47
|
-
src={thumb}
|
|
48
|
-
alt={product.title ?? ""}
|
|
49
|
-
width={112}
|
|
50
|
-
height={112}
|
|
51
|
-
className="w-full h-full object-cover"
|
|
52
|
-
unoptimized
|
|
53
|
-
/>
|
|
54
|
-
) : (
|
|
55
|
-
<div className={`w-full h-full ${cn.imagePlaceholder}`} />
|
|
56
|
-
)}
|
|
57
|
-
</div>
|
|
58
|
-
</div>
|
|
59
|
-
<span className={`text-[11px] sm:text-xs text-center max-w-[100px] line-clamp-2 ${cn.itemTitle}`}>
|
|
60
|
-
{product.title}
|
|
61
|
-
</span>
|
|
62
|
-
</LocalizedClientLink>
|
|
63
|
-
)
|
|
64
|
-
})}
|
|
65
|
-
</div>
|
|
66
|
-
</div>
|
|
67
|
-
</div>
|
|
68
|
-
</section>
|
|
69
|
-
)
|
|
70
|
-
}
|
|
1
|
+
export { default } from "../shoppable-gallery"
|
|
2
|
+
export type { ShoppableLook } from "../shoppable-gallery"
|
|
@@ -1,72 +1,154 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
+
import { useCallback, useRef, useState, useEffect } from "react"
|
|
3
4
|
import Image from "next/image"
|
|
4
5
|
import { HttpTypes } from "@medusajs/types"
|
|
5
6
|
import LocalizedClientLink from "medusa-ui-common/common/components/localized-client-link"
|
|
6
7
|
import type { HomeThemeClassNames } from "medusa-storefront-theme-base"
|
|
7
8
|
import { useThemeSection } from "medusa-ui-common/providers"
|
|
9
|
+
import {
|
|
10
|
+
formatCategoryLabel,
|
|
11
|
+
pickCategoryPillItems,
|
|
12
|
+
resolveCategoryImage,
|
|
13
|
+
} from "./utils"
|
|
8
14
|
|
|
9
15
|
type CategoryPillsProps = {
|
|
10
16
|
categories: HttpTypes.StoreProductCategory[]
|
|
17
|
+
collections?: Array<Record<string, unknown>>
|
|
11
18
|
classNames?: Partial<HomeThemeClassNames>
|
|
12
19
|
}
|
|
13
20
|
|
|
21
|
+
function ChevronIcon({ dir }: { dir: "left" | "right" }) {
|
|
22
|
+
return (
|
|
23
|
+
<svg
|
|
24
|
+
width="20"
|
|
25
|
+
height="20"
|
|
26
|
+
viewBox="0 0 24 24"
|
|
27
|
+
fill="none"
|
|
28
|
+
stroke="currentColor"
|
|
29
|
+
strokeWidth="2"
|
|
30
|
+
strokeLinecap="round"
|
|
31
|
+
strokeLinejoin="round"
|
|
32
|
+
aria-hidden
|
|
33
|
+
>
|
|
34
|
+
{dir === "left" ? (
|
|
35
|
+
<path d="M15 18l-6-6 6-6" />
|
|
36
|
+
) : (
|
|
37
|
+
<path d="M9 18l6-6-6-6" />
|
|
38
|
+
)}
|
|
39
|
+
</svg>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
14
43
|
export default function CategoryPills({
|
|
15
44
|
categories,
|
|
45
|
+
collections,
|
|
16
46
|
classNames: classNamesProp,
|
|
17
47
|
}: CategoryPillsProps) {
|
|
18
48
|
const cn = useThemeSection("home", classNamesProp)
|
|
49
|
+
const trackRef = useRef<HTMLDivElement>(null)
|
|
50
|
+
const [canScrollLeft, setCanScrollLeft] = useState(false)
|
|
51
|
+
const [canScrollRight, setCanScrollRight] = useState(false)
|
|
52
|
+
const [hasOverflow, setHasOverflow] = useState(false)
|
|
53
|
+
|
|
54
|
+
const pills = pickCategoryPillItems(categories, collections)
|
|
19
55
|
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
56
|
+
const updateScrollState = useCallback(() => {
|
|
57
|
+
const el = trackRef.current
|
|
58
|
+
if (!el) return
|
|
59
|
+
const { scrollLeft, scrollWidth, clientWidth } = el
|
|
60
|
+
setCanScrollLeft(scrollLeft > 4)
|
|
61
|
+
setCanScrollRight(scrollLeft + clientWidth < scrollWidth - 4)
|
|
62
|
+
setHasOverflow(scrollWidth > clientWidth + 4)
|
|
63
|
+
}, [])
|
|
64
|
+
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
updateScrollState()
|
|
67
|
+
const el = trackRef.current
|
|
68
|
+
if (!el) return
|
|
69
|
+
const ro = new ResizeObserver(updateScrollState)
|
|
70
|
+
ro.observe(el)
|
|
71
|
+
return () => ro.disconnect()
|
|
72
|
+
}, [pills.length, updateScrollState])
|
|
73
|
+
|
|
74
|
+
const scrollBy = (direction: "left" | "right") => {
|
|
75
|
+
const el = trackRef.current
|
|
76
|
+
if (!el) return
|
|
77
|
+
const amount = Math.max(el.clientWidth * 0.55, 320)
|
|
78
|
+
el.scrollBy({
|
|
79
|
+
left: direction === "left" ? -amount : amount,
|
|
80
|
+
behavior: "smooth",
|
|
81
|
+
})
|
|
82
|
+
}
|
|
23
83
|
|
|
24
84
|
if (pills.length === 0) return null
|
|
25
85
|
|
|
26
86
|
return (
|
|
27
|
-
<section className={`w-full py-
|
|
28
|
-
<div className="px-4 sm:px-6 md:px-8">
|
|
29
|
-
<div className="mx-auto max-w-[1360px]">
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
(
|
|
87
|
+
<section className={`w-full py-10 sm:py-12 md:py-14 ${cn.section}`}>
|
|
88
|
+
<div className="px-4 sm:px-6 md:px-8 lg:px-12">
|
|
89
|
+
<div className="relative mx-auto max-w-[1360px]">
|
|
90
|
+
{hasOverflow ? (
|
|
91
|
+
<>
|
|
92
|
+
<button
|
|
93
|
+
type="button"
|
|
94
|
+
onClick={() => scrollBy("left")}
|
|
95
|
+
disabled={!canScrollLeft}
|
|
96
|
+
className={`absolute left-0 top-[72px] z-10 -translate-x-1/2 sm:top-[84px] md:top-[92px] disabled:opacity-35 ${cn.categoryPillNavButton}`}
|
|
97
|
+
aria-label="Scroll categories left"
|
|
98
|
+
>
|
|
99
|
+
<ChevronIcon dir="left" />
|
|
100
|
+
</button>
|
|
101
|
+
<button
|
|
102
|
+
type="button"
|
|
103
|
+
onClick={() => scrollBy("right")}
|
|
104
|
+
disabled={!canScrollRight}
|
|
105
|
+
className={`absolute right-0 top-[72px] z-10 translate-x-1/2 sm:top-[84px] md:top-[92px] disabled:opacity-35 ${cn.categoryPillNavButton}`}
|
|
106
|
+
aria-label="Scroll categories right"
|
|
107
|
+
>
|
|
108
|
+
<ChevronIcon dir="right" />
|
|
109
|
+
</button>
|
|
110
|
+
</>
|
|
111
|
+
) : null}
|
|
35
112
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
className="w-full h-full object-cover"
|
|
52
|
-
unoptimized
|
|
53
|
-
/>
|
|
54
|
-
) : (
|
|
55
|
-
<div
|
|
56
|
-
className={`w-full h-full flex items-center justify-center text-lg font-bold ${cn.imagePlaceholder} text-[var(--sf-color-primary)]`}
|
|
57
|
-
>
|
|
58
|
-
{category.name?.charAt(0)}
|
|
59
|
-
</div>
|
|
60
|
-
)}
|
|
61
|
-
</div>
|
|
62
|
-
<span
|
|
63
|
-
className={`text-[11px] sm:text-xs text-center font-medium leading-tight max-w-[92px] ${cn.categoryTitle}`}
|
|
113
|
+
<div
|
|
114
|
+
ref={trackRef}
|
|
115
|
+
onScroll={updateScrollState}
|
|
116
|
+
className={cn.categoryPillScroll}
|
|
117
|
+
>
|
|
118
|
+
<div className={`flex w-max min-w-full ${cn.categoryPillTrack}`}>
|
|
119
|
+
{pills.map((category) => {
|
|
120
|
+
const thumb = resolveCategoryImage(category, collections)
|
|
121
|
+
const label = formatCategoryLabel(category.name ?? "")
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<LocalizedClientLink
|
|
125
|
+
key={category.id}
|
|
126
|
+
href={`/categories/${category.handle}`}
|
|
127
|
+
className={`group ${cn.categoryPillItem}`}
|
|
64
128
|
>
|
|
65
|
-
{
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
129
|
+
<div className={cn.categoryPillImageWrap}>
|
|
130
|
+
{thumb ? (
|
|
131
|
+
<Image
|
|
132
|
+
src={thumb}
|
|
133
|
+
alt={label}
|
|
134
|
+
width={160}
|
|
135
|
+
height={160}
|
|
136
|
+
className={cn.categoryPillImage}
|
|
137
|
+
unoptimized
|
|
138
|
+
/>
|
|
139
|
+
) : (
|
|
140
|
+
<div
|
|
141
|
+
className={`flex h-full w-full items-center justify-center text-2xl font-semibold sm:text-3xl ${cn.imagePlaceholder} text-[var(--sf-color-primary)]`}
|
|
142
|
+
>
|
|
143
|
+
{label.charAt(0)}
|
|
144
|
+
</div>
|
|
145
|
+
)}
|
|
146
|
+
</div>
|
|
147
|
+
<span className={cn.categoryPillLabel}>{label}</span>
|
|
148
|
+
</LocalizedClientLink>
|
|
149
|
+
)
|
|
150
|
+
})}
|
|
151
|
+
</div>
|
|
70
152
|
</div>
|
|
71
153
|
</div>
|
|
72
154
|
</div>
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { HttpTypes } from "@medusajs/types"
|
|
2
|
+
|
|
3
|
+
type CategoryMetadata = {
|
|
4
|
+
category_image?: string
|
|
5
|
+
image?: string
|
|
6
|
+
thumbnail?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
type CollectionMetadata = {
|
|
10
|
+
image?: string
|
|
11
|
+
thumbnail?: string
|
|
12
|
+
category_image?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function normalizeKey(value: string): string {
|
|
16
|
+
return value
|
|
17
|
+
.toLowerCase()
|
|
18
|
+
.trim()
|
|
19
|
+
.replace(/&/g, "and")
|
|
20
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
21
|
+
.replace(/^-+|-+$/g, "")
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function formatCategoryLabel(name: string | undefined): string {
|
|
25
|
+
if (!name) return ""
|
|
26
|
+
const trimmed = name.trim()
|
|
27
|
+
if (!trimmed) return ""
|
|
28
|
+
if (trimmed === trimmed.toUpperCase() && /[A-Z]/.test(trimmed)) {
|
|
29
|
+
return trimmed
|
|
30
|
+
.toLowerCase()
|
|
31
|
+
.split(/\s+/)
|
|
32
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
33
|
+
.join(" ")
|
|
34
|
+
}
|
|
35
|
+
return trimmed
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function collectionImageForCategory(
|
|
39
|
+
collections: Array<Record<string, unknown>> | undefined,
|
|
40
|
+
category: HttpTypes.StoreProductCategory
|
|
41
|
+
): string | null {
|
|
42
|
+
if (!collections?.length) return null
|
|
43
|
+
|
|
44
|
+
const handleKey = category.handle ? normalizeKey(category.handle) : ""
|
|
45
|
+
const nameKey = category.name ? normalizeKey(category.name) : ""
|
|
46
|
+
|
|
47
|
+
for (const entry of collections) {
|
|
48
|
+
const meta = (entry.metadata as CollectionMetadata | undefined) ?? {}
|
|
49
|
+
const image = meta.category_image || meta.image || meta.thumbnail
|
|
50
|
+
if (!image) continue
|
|
51
|
+
|
|
52
|
+
const cHandle = (entry.handle as string | undefined) ?? ""
|
|
53
|
+
const cTitle = (entry.title as string | undefined) ?? ""
|
|
54
|
+
const handleMatch = handleKey && normalizeKey(cHandle) === handleKey
|
|
55
|
+
const titleMatch = nameKey && normalizeKey(cTitle) === nameKey
|
|
56
|
+
|
|
57
|
+
if (handleMatch || titleMatch) return image
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function imageFromCategoryRecord(
|
|
64
|
+
category: HttpTypes.StoreProductCategory
|
|
65
|
+
): string | null {
|
|
66
|
+
const meta = (category.metadata as CategoryMetadata | undefined) ?? {}
|
|
67
|
+
return (
|
|
68
|
+
meta.category_image ||
|
|
69
|
+
meta.image ||
|
|
70
|
+
meta.thumbnail ||
|
|
71
|
+
(category as { thumbnail?: string }).thumbnail ||
|
|
72
|
+
null
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function resolveCategoryImage(
|
|
77
|
+
category: HttpTypes.StoreProductCategory,
|
|
78
|
+
collections?: Array<Record<string, unknown>>
|
|
79
|
+
): string | null {
|
|
80
|
+
const fromCategory = imageFromCategoryRecord(category)
|
|
81
|
+
if (fromCategory) return fromCategory
|
|
82
|
+
|
|
83
|
+
const parent = (
|
|
84
|
+
category as HttpTypes.StoreProductCategory & {
|
|
85
|
+
parent_category?: HttpTypes.StoreProductCategory;
|
|
86
|
+
}
|
|
87
|
+
).parent_category
|
|
88
|
+
if (parent) {
|
|
89
|
+
const fromParent = imageFromCategoryRecord(parent)
|
|
90
|
+
if (fromParent) return fromParent
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return collectionImageForCategory(collections, category)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function hasCategoryImage(
|
|
97
|
+
category: HttpTypes.StoreProductCategory,
|
|
98
|
+
collections?: Array<Record<string, unknown>>
|
|
99
|
+
): boolean {
|
|
100
|
+
return Boolean(resolveCategoryImage(category, collections))
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function pickCategoryPillItems(
|
|
104
|
+
categories: HttpTypes.StoreProductCategory[],
|
|
105
|
+
collections?: Array<Record<string, unknown>>,
|
|
106
|
+
limit = 12
|
|
107
|
+
): HttpTypes.StoreProductCategory[] {
|
|
108
|
+
const valid = (categories ?? []).filter((c) => c.name && c.handle)
|
|
109
|
+
const withImage = valid.filter((c) => hasCategoryImage(c, collections))
|
|
110
|
+
const topLevel = valid.filter((c) => !c.parent_category_id)
|
|
111
|
+
const topLevelWithImage = withImage.filter((c) => !c.parent_category_id)
|
|
112
|
+
|
|
113
|
+
let pool = valid
|
|
114
|
+
if (withImage.length >= 4) {
|
|
115
|
+
pool =
|
|
116
|
+
topLevelWithImage.length >= 4
|
|
117
|
+
? topLevelWithImage
|
|
118
|
+
: withImage
|
|
119
|
+
} else if (topLevel.length >= 4) {
|
|
120
|
+
pool = topLevel
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return [...pool]
|
|
124
|
+
.sort(
|
|
125
|
+
(a, b) =>
|
|
126
|
+
Number(hasCategoryImage(b, collections)) -
|
|
127
|
+
Number(hasCategoryImage(a, collections)) ||
|
|
128
|
+
Number(!b.parent_category_id) - Number(!a.parent_category_id) ||
|
|
129
|
+
(a.name ?? "").localeCompare(b.name ?? "")
|
|
130
|
+
)
|
|
131
|
+
.slice(0, limit)
|
|
132
|
+
}
|