@pradip1995/segment-vertical-collection-scroll 0.1.1
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 +40 -0
- package/public/images/collections/emerald-sapphire.jpg +0 -0
- package/public/images/collections/white-diamond.jpg +0 -0
- package/public/images/collections/yellow-diamond.jpg +0 -0
- package/src/commerce-core-shim.d.ts +10 -0
- package/src/index.ts +3 -0
- package/src/manifest.ts +11 -0
- package/src/resolve-collection.ts +261 -0
- package/src/segment.css +398 -0
- package/src/segment.tsx +51 -0
- package/src/types.ts +37 -0
- package/src/vertical-scroll-client.tsx +216 -0
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pradip1995/segment-vertical-collection-scroll",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"sideEffects": [
|
|
9
|
+
"src/segment.css"
|
|
10
|
+
],
|
|
11
|
+
"files": [
|
|
12
|
+
"src",
|
|
13
|
+
"public"
|
|
14
|
+
],
|
|
15
|
+
"exports": {
|
|
16
|
+
".": "./src/index.ts",
|
|
17
|
+
"./manifest": "./src/manifest.ts"
|
|
18
|
+
},
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"@pradip1995/plugin-sdk": "^0.2.0",
|
|
21
|
+
"react": ">=19",
|
|
22
|
+
"react-dom": ">=19",
|
|
23
|
+
"next": ">=15"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@pradip1995/segment-primitives": "^0.4.11",
|
|
27
|
+
"@pradip1995/segment-tokens": "^0.3.14"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@medusajs/types": "^2.0.0",
|
|
31
|
+
"@pradip1995/plugin-sdk": "^0.2.0",
|
|
32
|
+
"@types/react": "^19",
|
|
33
|
+
"react": "19.0.3",
|
|
34
|
+
"typescript": "^5.7.2"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"typecheck": "tsc --noEmit",
|
|
38
|
+
"lint": "tsc --noEmit"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
declare module "@pradip1995/commerce-core/data/collections" {
|
|
2
|
+
import type { HttpTypes } from "@medusajs/types"
|
|
3
|
+
|
|
4
|
+
export function listCollections(queryParams?: Record<string, any>): Promise<{
|
|
5
|
+
collections: HttpTypes.StoreCollection[]
|
|
6
|
+
count: number
|
|
7
|
+
offset: number
|
|
8
|
+
limit: number
|
|
9
|
+
}>
|
|
10
|
+
}
|
package/src/index.ts
ADDED
package/src/manifest.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { SegmentManifest } from "@pradip1995/plugin-sdk"
|
|
2
|
+
|
|
3
|
+
const manifest: SegmentManifest = {
|
|
4
|
+
id: "vertical-collection-scroll",
|
|
5
|
+
type: "segment",
|
|
6
|
+
version: "0.1.0",
|
|
7
|
+
compatibleFramework: ["^1.0.0"],
|
|
8
|
+
dataKey: "verticalCollectionScroll",
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default manifest
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { resolveMediaUrl } from "@pradip1995/segment-primitives/resolve-media-url"
|
|
2
|
+
import type { HttpTypes } from "@medusajs/types"
|
|
3
|
+
import type { VerticalCollectionSlide } from "./types"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Fallback luxury imagery when a collection in Admin does not yet have an image uploaded.
|
|
7
|
+
*/
|
|
8
|
+
function getMatchingFallbackImage(title: string, index: number): string {
|
|
9
|
+
const t = title.toLowerCase()
|
|
10
|
+
if (t.includes("bridal") || t.includes("wedding")) {
|
|
11
|
+
return "/images/bridal/bridal-hero.png"
|
|
12
|
+
}
|
|
13
|
+
if (t.includes("diamond")) {
|
|
14
|
+
return "/images/collections/white-diamond.jpg"
|
|
15
|
+
}
|
|
16
|
+
if (t.includes("gold") || t.includes("antique")) {
|
|
17
|
+
return "/images/Old_Cut_Jewelry_02_copy_1_1.webp"
|
|
18
|
+
}
|
|
19
|
+
if (t.includes("lab grown")) {
|
|
20
|
+
return "/images/collections/yellow-diamond.jpg"
|
|
21
|
+
}
|
|
22
|
+
const defaults = [
|
|
23
|
+
"/images/collections/emerald-sapphire.jpg",
|
|
24
|
+
"/images/collections/white-diamond.jpg",
|
|
25
|
+
"/images/bridal/bridal-hero.png",
|
|
26
|
+
"/images/Old_Cut_Jewelry_02_copy_1_1.webp",
|
|
27
|
+
"/images/collections/yellow-diamond.jpg",
|
|
28
|
+
]
|
|
29
|
+
return defaults[index % defaults.length]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function extractMediaUrl(val: unknown): string | null {
|
|
33
|
+
if (!val || typeof val !== "string") return null
|
|
34
|
+
const trimmed = val.trim()
|
|
35
|
+
if (!trimmed) return null
|
|
36
|
+
const resolved = resolveMediaUrl(trimmed)
|
|
37
|
+
return resolved || trimmed
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getCollectionMetadata(collection: HttpTypes.StoreCollection) {
|
|
41
|
+
return (collection.metadata || {}) as Record<string, unknown>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isHomeFeatured(collection: HttpTypes.StoreCollection) {
|
|
45
|
+
const value = getCollectionMetadata(collection).home_featured
|
|
46
|
+
return value === true || value === "true" || value === "1"
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getHomeSort(collection: HttpTypes.StoreCollection) {
|
|
50
|
+
const raw = getCollectionMetadata(collection).home_sort
|
|
51
|
+
const num = Number.parseInt(String(raw ?? ""), 10)
|
|
52
|
+
return Number.isFinite(num) ? num : 999
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function readParallaxText(meta: Record<string, unknown>, keys: string[]) {
|
|
56
|
+
for (const key of keys) {
|
|
57
|
+
const val = meta[key]
|
|
58
|
+
if (typeof val === "string" && val.trim()) return val.trim()
|
|
59
|
+
}
|
|
60
|
+
return ""
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function getAdminCollectionImage(col: any, index: number): string {
|
|
64
|
+
if (!col) return getMatchingFallbackImage("", index)
|
|
65
|
+
|
|
66
|
+
const meta = (col.metadata || {}) as Record<string, unknown>
|
|
67
|
+
|
|
68
|
+
// Priority 1: Collection Banner uploaded in Medusa Admin
|
|
69
|
+
const banner =
|
|
70
|
+
extractMediaUrl(meta.collection_Banner) ||
|
|
71
|
+
extractMediaUrl(meta.collection_banner) ||
|
|
72
|
+
extractMediaUrl(meta.Collection_Banner) ||
|
|
73
|
+
extractMediaUrl(meta.banner)
|
|
74
|
+
if (banner) return banner
|
|
75
|
+
|
|
76
|
+
// Priority 2: Collection Image uploaded in Medusa Admin
|
|
77
|
+
const image =
|
|
78
|
+
extractMediaUrl(meta.collection_image) ||
|
|
79
|
+
extractMediaUrl(meta.collection_Image) ||
|
|
80
|
+
extractMediaUrl(meta["Collection Image"]) ||
|
|
81
|
+
extractMediaUrl(meta["collection image"]) ||
|
|
82
|
+
extractMediaUrl(meta.Collection_Image) ||
|
|
83
|
+
extractMediaUrl(meta.image) ||
|
|
84
|
+
extractMediaUrl(meta.imageUrl) ||
|
|
85
|
+
extractMediaUrl(meta.photo) ||
|
|
86
|
+
extractMediaUrl(meta.thumbnail)
|
|
87
|
+
if (image) return image
|
|
88
|
+
|
|
89
|
+
// Priority 3: Collection Icon uploaded in Medusa Admin
|
|
90
|
+
const icon = extractMediaUrl(meta.collection_icon) || extractMediaUrl(meta.icon)
|
|
91
|
+
if (icon) return icon
|
|
92
|
+
|
|
93
|
+
// Priority 4: Collection thumbnail / image field
|
|
94
|
+
const direct = extractMediaUrl(col.thumbnail) || extractMediaUrl(col.image)
|
|
95
|
+
if (direct) return direct
|
|
96
|
+
|
|
97
|
+
// Priority 5: First product image in the collection
|
|
98
|
+
if (Array.isArray(col.products) && col.products.length > 0) {
|
|
99
|
+
const p = col.products[0]
|
|
100
|
+
const prodImg =
|
|
101
|
+
extractMediaUrl(p.thumbnail) ||
|
|
102
|
+
(Array.isArray(p.images) && p.images[0]?.url ? extractMediaUrl(p.images[0].url) : null)
|
|
103
|
+
if (prodImg) return prodImg
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Priority 6: Matching fallback image
|
|
107
|
+
return getMatchingFallbackImage(col.title || "", index)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function resolveAdminCollections(
|
|
111
|
+
collections?: HttpTypes.StoreCollection[],
|
|
112
|
+
customItems?: VerticalCollectionSlide[]
|
|
113
|
+
): VerticalCollectionSlide[] {
|
|
114
|
+
// If user passes explicit customItems, use them
|
|
115
|
+
if (customItems && customItems.length > 0) {
|
|
116
|
+
return customItems.map((item, index) => {
|
|
117
|
+
const title = (item.title || item.name || "Collection").trim()
|
|
118
|
+
const image =
|
|
119
|
+
extractMediaUrl(item.image) ||
|
|
120
|
+
extractMediaUrl(item.imageUrl) ||
|
|
121
|
+
extractMediaUrl(item.photo) ||
|
|
122
|
+
extractMediaUrl(item.thumbnail) ||
|
|
123
|
+
getMatchingFallbackImage(title, index)
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
id: item.id || `col-${index}`,
|
|
127
|
+
title,
|
|
128
|
+
subtitle: item.subtitle || item.description || "Discover the handcrafted collection.",
|
|
129
|
+
eyebrow: item.eyebrow || item.tag || "COLLECTION",
|
|
130
|
+
buttonText: item.buttonText || item.ctaText || "DISCOVER",
|
|
131
|
+
buttonLink:
|
|
132
|
+
item.buttonLink ||
|
|
133
|
+
item.link ||
|
|
134
|
+
item.href ||
|
|
135
|
+
(item.handle ? `/store?collection=${item.handle}` : "/store"),
|
|
136
|
+
image,
|
|
137
|
+
imageAlt: title,
|
|
138
|
+
theme: item.theme || (index % 2 === 1 ? "light" : "dark"),
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Pure dynamic mode: Read directly from Admin Collections
|
|
144
|
+
if (collections && collections.length > 0) {
|
|
145
|
+
const valid = collections.filter((c) => c.title && c.title.trim())
|
|
146
|
+
const featured = valid.filter(isHomeFeatured).sort(
|
|
147
|
+
(a, b) => getHomeSort(a) - getHomeSort(b)
|
|
148
|
+
)
|
|
149
|
+
const source = featured.length > 0 ? featured : valid
|
|
150
|
+
|
|
151
|
+
return source.map((col, index) => {
|
|
152
|
+
const title = col.title.trim()
|
|
153
|
+
const image = getAdminCollectionImage(col, index)
|
|
154
|
+
const meta = getCollectionMetadata(col)
|
|
155
|
+
|
|
156
|
+
const subtitle =
|
|
157
|
+
readParallaxText(meta, [
|
|
158
|
+
"collection_subtitle",
|
|
159
|
+
"subtitle",
|
|
160
|
+
"description",
|
|
161
|
+
"home_parallax_description",
|
|
162
|
+
]) || `Explore ${title} — handcrafted luxury by H.Ankit Jewels.`
|
|
163
|
+
|
|
164
|
+
const eyebrow =
|
|
165
|
+
readParallaxText(meta, ["collection_eyebrow", "eyebrow", "tag"]) ||
|
|
166
|
+
"COLLECTION"
|
|
167
|
+
|
|
168
|
+
const buttonText =
|
|
169
|
+
readParallaxText(meta, [
|
|
170
|
+
"collection_button_text",
|
|
171
|
+
"buttonText",
|
|
172
|
+
"ctaText",
|
|
173
|
+
]) || "DISCOVER"
|
|
174
|
+
|
|
175
|
+
const themeRaw = readParallaxText(meta, [
|
|
176
|
+
"home_parallax_theme",
|
|
177
|
+
"theme",
|
|
178
|
+
"parallax_theme",
|
|
179
|
+
])
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
id: col.id || `col-${index}`,
|
|
183
|
+
title,
|
|
184
|
+
subtitle,
|
|
185
|
+
eyebrow,
|
|
186
|
+
buttonText,
|
|
187
|
+
buttonLink: `/store?collection=${col.handle || col.id}`,
|
|
188
|
+
image,
|
|
189
|
+
imageAlt: title,
|
|
190
|
+
theme:
|
|
191
|
+
themeRaw === "light"
|
|
192
|
+
? "light"
|
|
193
|
+
: themeRaw === "dark"
|
|
194
|
+
? "dark"
|
|
195
|
+
: index % 2 === 1
|
|
196
|
+
? "light"
|
|
197
|
+
: "dark",
|
|
198
|
+
}
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Fallback high-luxury collections when Admin collections are empty
|
|
203
|
+
return [
|
|
204
|
+
{
|
|
205
|
+
id: "fallback-col-silver",
|
|
206
|
+
title: "Silver Collection",
|
|
207
|
+
subtitle: "Effortlessly elegant sterling silver jewellery designed for contemporary sophistication.",
|
|
208
|
+
eyebrow: "EXCLUSIVE COLLECTION",
|
|
209
|
+
buttonText: "DISCOVER",
|
|
210
|
+
buttonLink: "/store?collection=silver-collection",
|
|
211
|
+
image: "/images/collections/emerald-sapphire.jpg",
|
|
212
|
+
imageAlt: "Silver Collection",
|
|
213
|
+
theme: "dark" as const,
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
id: "fallback-col-lab-grown",
|
|
217
|
+
title: "Lab Grown Diamonds",
|
|
218
|
+
subtitle: "Brilliant, conflict-free diamond creations sculpted with flawless ethical precision.",
|
|
219
|
+
eyebrow: "SUSTAINABLE LUXURY",
|
|
220
|
+
buttonText: "DISCOVER",
|
|
221
|
+
buttonLink: "/store?collection=lab-grown-collection",
|
|
222
|
+
image: "/images/collections/yellow-diamond.jpg",
|
|
223
|
+
imageAlt: "Lab Grown Diamonds",
|
|
224
|
+
theme: "light" as const,
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
id: "fallback-col-bridal",
|
|
228
|
+
title: "Bridal Suite",
|
|
229
|
+
subtitle: "Celebrate your eternal union with handcrafted heirloom jewellery crafted to transcend time.",
|
|
230
|
+
eyebrow: "CELEBRATE LOVE",
|
|
231
|
+
buttonText: "DISCOVER",
|
|
232
|
+
buttonLink: "/store?collection=-bridal-collection",
|
|
233
|
+
image: "/images/bridal/bridal-hero.png",
|
|
234
|
+
imageAlt: "Bridal Suite",
|
|
235
|
+
theme: "dark" as const,
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
id: "fallback-col-gold",
|
|
239
|
+
title: "Gold Heritage",
|
|
240
|
+
subtitle: "Intricate antique gold craftsmanship echoing the grandeur of timeless royalty.",
|
|
241
|
+
eyebrow: "ROYAL HERITAGE",
|
|
242
|
+
buttonText: "DISCOVER",
|
|
243
|
+
buttonLink: "/store?collection=gold-collection",
|
|
244
|
+
image: "/images/Old_Cut_Jewelry_02_copy_1_1.webp",
|
|
245
|
+
imageAlt: "Gold Heritage",
|
|
246
|
+
theme: "light" as const,
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
id: "fallback-col-diamond",
|
|
250
|
+
title: "High Jewellery Diamonds",
|
|
251
|
+
subtitle: "Magnificent diamond statement pieces radiating unmatched fire, light, and brilliance.",
|
|
252
|
+
eyebrow: "THE APEX OF LUXURY",
|
|
253
|
+
buttonText: "DISCOVER",
|
|
254
|
+
buttonLink: "/store?collection=diamond-collection",
|
|
255
|
+
image: "/images/collections/white-diamond.jpg",
|
|
256
|
+
imageAlt: "High Jewellery Diamonds",
|
|
257
|
+
theme: "dark" as const,
|
|
258
|
+
},
|
|
259
|
+
]
|
|
260
|
+
}
|
|
261
|
+
|
package/src/segment.css
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
/* ==========================================================================
|
|
2
|
+
Segment: Graff Multiple-Layered Sticky Collection Scroll
|
|
3
|
+
Identical to https://www.graff.com/international-en/home/
|
|
4
|
+
========================================================================== */
|
|
5
|
+
|
|
6
|
+
.graff-parallax-section {
|
|
7
|
+
position: relative;
|
|
8
|
+
width: 100%;
|
|
9
|
+
margin: 0;
|
|
10
|
+
padding: 0;
|
|
11
|
+
overscroll-behavior: none;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/* --------------------------------------------------------------------------
|
|
15
|
+
Sticky Stacking Slides: Each slide is sticky at top:0 with stacking z-index
|
|
16
|
+
As user scrolls, Slide 2 rolls UP and OVER Slide 1 seamlessly!
|
|
17
|
+
-------------------------------------------------------------------------- */
|
|
18
|
+
.graff-parallax__slide {
|
|
19
|
+
position: sticky;
|
|
20
|
+
top: 0;
|
|
21
|
+
left: 0;
|
|
22
|
+
width: 100%;
|
|
23
|
+
height: 100vh;
|
|
24
|
+
height: 100dvh;
|
|
25
|
+
overflow: hidden;
|
|
26
|
+
background-color: #06090e;
|
|
27
|
+
color: #ffffff;
|
|
28
|
+
user-select: none;
|
|
29
|
+
font-family: var(--font-sans, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif);
|
|
30
|
+
box-shadow: none;
|
|
31
|
+
isolation: isolate;
|
|
32
|
+
transform: translateZ(0);
|
|
33
|
+
backface-visibility: hidden;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.graff-parallax__slide--light {
|
|
37
|
+
background-color: #f7f5f2;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.graff-parallax__slide-inner {
|
|
41
|
+
position: relative;
|
|
42
|
+
width: 100%;
|
|
43
|
+
height: 100%;
|
|
44
|
+
overflow: hidden;
|
|
45
|
+
display: flex;
|
|
46
|
+
align-items: center;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* --------------------------------------------------------------------------
|
|
50
|
+
Background Media
|
|
51
|
+
-------------------------------------------------------------------------- */
|
|
52
|
+
.graff-parallax__bg {
|
|
53
|
+
position: absolute;
|
|
54
|
+
inset: 0;
|
|
55
|
+
width: 100%;
|
|
56
|
+
height: 100%;
|
|
57
|
+
z-index: 1;
|
|
58
|
+
overflow: hidden;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* Graff: images stay fixed — no zoom on scroll or slide change */
|
|
62
|
+
.graff-parallax__image {
|
|
63
|
+
object-fit: cover;
|
|
64
|
+
object-position: center center;
|
|
65
|
+
transform: none;
|
|
66
|
+
animation: none;
|
|
67
|
+
transition: none;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.graff-parallax__overlay {
|
|
71
|
+
display: none;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
/* --------------------------------------------------------------------------
|
|
76
|
+
Editorial Content & Typography (Graff Style)
|
|
77
|
+
-------------------------------------------------------------------------- */
|
|
78
|
+
.graff-parallax__content-wrap {
|
|
79
|
+
position: relative;
|
|
80
|
+
z-index: 10;
|
|
81
|
+
width: 100%;
|
|
82
|
+
max-width: var(--container-max, 1440px);
|
|
83
|
+
margin-left: auto;
|
|
84
|
+
margin-right: auto;
|
|
85
|
+
padding-left: 2rem;
|
|
86
|
+
padding-right: 5rem;
|
|
87
|
+
pointer-events: none;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
@media (min-width: 768px) {
|
|
91
|
+
.graff-parallax__content-wrap {
|
|
92
|
+
padding-left: 4.5rem;
|
|
93
|
+
padding-right: 6rem;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@media (min-width: 1280px) {
|
|
98
|
+
.graff-parallax__content-wrap {
|
|
99
|
+
padding-left: 6.5rem;
|
|
100
|
+
padding-right: 7rem;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.graff-parallax__content {
|
|
105
|
+
max-width: 620px;
|
|
106
|
+
pointer-events: auto;
|
|
107
|
+
display: flex;
|
|
108
|
+
flex-direction: column;
|
|
109
|
+
align-items: flex-start;
|
|
110
|
+
opacity: 0;
|
|
111
|
+
visibility: hidden;
|
|
112
|
+
transform: none;
|
|
113
|
+
transition:
|
|
114
|
+
opacity 0.4s ease-out,
|
|
115
|
+
visibility 0s linear 0.4s;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.graff-parallax__slide.is-active .graff-parallax__content {
|
|
119
|
+
opacity: 1;
|
|
120
|
+
visibility: visible;
|
|
121
|
+
transition:
|
|
122
|
+
opacity 0.4s ease-out,
|
|
123
|
+
visibility 0s linear 0s;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* First slide copy visible as soon as section enters view (Graff hero read) */
|
|
127
|
+
.graff-parallax-section.is-ready
|
|
128
|
+
.graff-parallax__slide:first-child.is-active
|
|
129
|
+
.graff-parallax__content {
|
|
130
|
+
opacity: 1;
|
|
131
|
+
visibility: visible;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
@media (prefers-reduced-motion: reduce) {
|
|
135
|
+
.graff-parallax__content,
|
|
136
|
+
.graff-parallax__slide.is-active .graff-parallax__content {
|
|
137
|
+
transition: none;
|
|
138
|
+
opacity: 1;
|
|
139
|
+
visibility: visible;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* Eyebrow tag */
|
|
144
|
+
.graff-parallax__eyebrow {
|
|
145
|
+
display: inline-block;
|
|
146
|
+
font-size: 0.75rem;
|
|
147
|
+
font-weight: 600;
|
|
148
|
+
letter-spacing: 0.25em;
|
|
149
|
+
text-transform: uppercase;
|
|
150
|
+
margin-bottom: 0.875rem;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.graff-parallax__slide--dark .graff-parallax__eyebrow {
|
|
154
|
+
color: #c9a86c;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.graff-parallax__slide--light .graff-parallax__eyebrow {
|
|
158
|
+
color: #8c7349;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/* Luxury Serif Title (Identical to Graff font styling) */
|
|
162
|
+
.graff-parallax__title {
|
|
163
|
+
font-family: var(--font-heading, "Cormorant Garamond", Georgia, serif);
|
|
164
|
+
font-size: 2.75rem;
|
|
165
|
+
line-height: 1.12;
|
|
166
|
+
font-weight: 400;
|
|
167
|
+
letter-spacing: -0.015em;
|
|
168
|
+
margin: 0 0 1.25rem;
|
|
169
|
+
text-transform: none;
|
|
170
|
+
font-style: normal;
|
|
171
|
+
word-break: break-word;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
@media (min-width: 768px) {
|
|
175
|
+
.graff-parallax__title {
|
|
176
|
+
font-size: 3.75rem;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
@media (min-width: 1280px) {
|
|
181
|
+
.graff-parallax__title {
|
|
182
|
+
font-size: 4.5rem;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.graff-parallax__slide--dark .graff-parallax__title {
|
|
187
|
+
color: #ffffff;
|
|
188
|
+
text-shadow: 0 2px 14px rgba(0, 0, 0, 0.4);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.graff-parallax__slide--light .graff-parallax__title {
|
|
192
|
+
color: #121417;
|
|
193
|
+
text-shadow: 0 1px 6px rgba(255, 255, 255, 0.8);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/* Subtitle */
|
|
197
|
+
.graff-parallax__subtitle {
|
|
198
|
+
font-family: var(--font-sans, -apple-system, sans-serif);
|
|
199
|
+
font-size: 1rem;
|
|
200
|
+
line-height: 1.65;
|
|
201
|
+
font-weight: 300;
|
|
202
|
+
margin-bottom: 2rem;
|
|
203
|
+
max-width: 520px;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
@media (min-width: 768px) {
|
|
207
|
+
.graff-parallax__subtitle {
|
|
208
|
+
font-size: 1.125rem;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.graff-parallax__slide--dark .graff-parallax__subtitle {
|
|
213
|
+
color: rgba(255, 255, 255, 0.85);
|
|
214
|
+
text-shadow: 0 1px 8px rgba(0, 0, 0, 0.3);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.graff-parallax__slide--light .graff-parallax__subtitle {
|
|
218
|
+
color: #383e47;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/* Discover CTA Button */
|
|
222
|
+
.graff-parallax__btn {
|
|
223
|
+
display: inline-flex;
|
|
224
|
+
align-items: center;
|
|
225
|
+
justify-content: center;
|
|
226
|
+
padding: 0.95rem 2.25rem;
|
|
227
|
+
font-size: 0.8125rem;
|
|
228
|
+
font-weight: 500;
|
|
229
|
+
letter-spacing: 0.22em;
|
|
230
|
+
text-transform: uppercase;
|
|
231
|
+
text-decoration: none;
|
|
232
|
+
border-radius: 0;
|
|
233
|
+
position: relative;
|
|
234
|
+
overflow: hidden;
|
|
235
|
+
transition: all 0.35s cubic-bezier(0.16, 1, 0.3, 1);
|
|
236
|
+
box-shadow: 0 4px 18px rgba(0, 0, 0, 0.15);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.graff-parallax__slide--dark .graff-parallax__btn {
|
|
240
|
+
background: rgba(181, 148, 97, 0.92);
|
|
241
|
+
color: #ffffff;
|
|
242
|
+
border: 1px solid rgba(201, 168, 108, 0.7);
|
|
243
|
+
backdrop-filter: blur(8px);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.graff-parallax__slide--dark .graff-parallax__btn:hover {
|
|
247
|
+
background: #ffffff;
|
|
248
|
+
color: #0b0f17;
|
|
249
|
+
border-color: #ffffff;
|
|
250
|
+
transform: translateY(-2px);
|
|
251
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.graff-parallax__slide--light .graff-parallax__btn {
|
|
255
|
+
background: #13264d;
|
|
256
|
+
color: #ffffff;
|
|
257
|
+
border: 1px solid #13264d;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.graff-parallax__slide--light .graff-parallax__btn:hover {
|
|
261
|
+
background: #0b0f17;
|
|
262
|
+
border-color: #0b0f17;
|
|
263
|
+
color: #ffffff;
|
|
264
|
+
transform: translateY(-2px);
|
|
265
|
+
box-shadow: 0 8px 22px rgba(19, 38, 77, 0.3);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/* --------------------------------------------------------------------------
|
|
269
|
+
Floating Pagination Dots on Right (Identical to Graff .b-parallax__pagination)
|
|
270
|
+
-------------------------------------------------------------------------- */
|
|
271
|
+
.graff-parallax__pagination {
|
|
272
|
+
position: fixed;
|
|
273
|
+
right: clamp(18px, 2.4vw, 30px);
|
|
274
|
+
top: 50%;
|
|
275
|
+
transform: translateY(-50%);
|
|
276
|
+
z-index: 1000;
|
|
277
|
+
display: flex;
|
|
278
|
+
flex-direction: column;
|
|
279
|
+
align-items: center;
|
|
280
|
+
gap: 15px;
|
|
281
|
+
opacity: 0;
|
|
282
|
+
visibility: hidden;
|
|
283
|
+
pointer-events: none;
|
|
284
|
+
transition: opacity 0.4s ease, visibility 0.4s ease;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.graff-parallax__pagination.is-visible {
|
|
288
|
+
opacity: 1;
|
|
289
|
+
visibility: visible;
|
|
290
|
+
pointer-events: auto;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.graff-parallax__pagination-dot {
|
|
294
|
+
position: relative;
|
|
295
|
+
flex-shrink: 0;
|
|
296
|
+
width: 7.5px;
|
|
297
|
+
height: 7.5px;
|
|
298
|
+
border-radius: 50%;
|
|
299
|
+
border: 2px solid #ffffff;
|
|
300
|
+
background: #ffffff;
|
|
301
|
+
cursor: pointer;
|
|
302
|
+
padding: 0;
|
|
303
|
+
transition:
|
|
304
|
+
transform 0.3s cubic-bezier(0.16, 1, 0.3, 1),
|
|
305
|
+
background 0.3s ease,
|
|
306
|
+
border-color 0.3s ease;
|
|
307
|
+
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.12);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.graff-parallax__pagination-dot:hover {
|
|
311
|
+
transform: scale(1.15);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.graff-parallax__pagination-dot.is-active {
|
|
315
|
+
background: transparent;
|
|
316
|
+
border: 2px solid #ffffff;
|
|
317
|
+
transform: scale(1);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.graff-parallax__dot-tooltip {
|
|
321
|
+
display: none;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/* --------------------------------------------------------------------------
|
|
325
|
+
Bottom Scroll Down Indicator
|
|
326
|
+
-------------------------------------------------------------------------- */
|
|
327
|
+
.graff-parallax__scroll-hint {
|
|
328
|
+
position: absolute;
|
|
329
|
+
bottom: 2rem;
|
|
330
|
+
left: 50%;
|
|
331
|
+
transform: translateX(-50%);
|
|
332
|
+
z-index: 20;
|
|
333
|
+
display: flex;
|
|
334
|
+
flex-direction: column;
|
|
335
|
+
align-items: center;
|
|
336
|
+
gap: 0.5rem;
|
|
337
|
+
background: transparent;
|
|
338
|
+
border: none;
|
|
339
|
+
cursor: pointer;
|
|
340
|
+
padding: 0.5rem;
|
|
341
|
+
transition: opacity 0.4s ease;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
.graff-parallax__scroll-hint-text {
|
|
345
|
+
font-size: 0.6875rem;
|
|
346
|
+
letter-spacing: 0.25em;
|
|
347
|
+
text-transform: uppercase;
|
|
348
|
+
font-weight: 500;
|
|
349
|
+
color: rgba(255, 255, 255, 0.7);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.graff-parallax__slide--light .graff-parallax__scroll-hint-text {
|
|
353
|
+
color: rgba(18, 20, 23, 0.7);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
.graff-parallax__scroll-hint-line {
|
|
357
|
+
width: 1px;
|
|
358
|
+
height: 28px;
|
|
359
|
+
position: relative;
|
|
360
|
+
overflow: hidden;
|
|
361
|
+
background-color: rgba(255, 255, 255, 0.25);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.graff-parallax__slide--light .graff-parallax__scroll-hint-line {
|
|
365
|
+
background-color: rgba(18, 20, 23, 0.25);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.graff-parallax__scroll-hint-line::after {
|
|
369
|
+
content: "";
|
|
370
|
+
position: absolute;
|
|
371
|
+
top: 0;
|
|
372
|
+
left: 0;
|
|
373
|
+
width: 100%;
|
|
374
|
+
height: 40%;
|
|
375
|
+
background-color: #ffffff;
|
|
376
|
+
animation: graffParallaxScrollDrop 1.8s cubic-bezier(0.65, 0, 0.35, 1) infinite;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
.graff-parallax__slide--light .graff-parallax__scroll-hint-line::after {
|
|
380
|
+
background-color: #121417;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
@keyframes graffParallaxScrollDrop {
|
|
384
|
+
0% {
|
|
385
|
+
transform: translateY(-100%);
|
|
386
|
+
opacity: 0;
|
|
387
|
+
}
|
|
388
|
+
35% {
|
|
389
|
+
opacity: 1;
|
|
390
|
+
}
|
|
391
|
+
75% {
|
|
392
|
+
opacity: 1;
|
|
393
|
+
}
|
|
394
|
+
100% {
|
|
395
|
+
transform: translateY(280%);
|
|
396
|
+
opacity: 0;
|
|
397
|
+
}
|
|
398
|
+
}
|
package/src/segment.tsx
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import "./segment.css"
|
|
2
|
+
import React from "react"
|
|
3
|
+
import VerticalScrollClient from "./vertical-scroll-client"
|
|
4
|
+
import { resolveAdminCollections } from "./resolve-collection"
|
|
5
|
+
import { listCollections } from "@pradip1995/commerce-core/data/collections"
|
|
6
|
+
import type { VerticalCollectionScrollProps } from "./types"
|
|
7
|
+
import type { HttpTypes } from "@medusajs/types"
|
|
8
|
+
|
|
9
|
+
export default async function VerticalCollectionScroll({
|
|
10
|
+
collections: propCollections,
|
|
11
|
+
nav,
|
|
12
|
+
items,
|
|
13
|
+
title,
|
|
14
|
+
eyebrow,
|
|
15
|
+
ariaLabel,
|
|
16
|
+
}: VerticalCollectionScrollProps & {
|
|
17
|
+
nav?: { collections?: HttpTypes.StoreCollection[] }
|
|
18
|
+
}) {
|
|
19
|
+
let collections = propCollections || nav?.collections
|
|
20
|
+
|
|
21
|
+
// If collections are not provided in props, dynamically fetch from Medusa
|
|
22
|
+
if (!collections || collections.length === 0) {
|
|
23
|
+
try {
|
|
24
|
+
const res = await listCollections({
|
|
25
|
+
limit: "100",
|
|
26
|
+
fields: "id,handle,title,+metadata",
|
|
27
|
+
})
|
|
28
|
+
if (res && res.collections && res.collections.length > 0) {
|
|
29
|
+
collections = res.collections
|
|
30
|
+
}
|
|
31
|
+
} catch {
|
|
32
|
+
// Fallback handled in resolveAdminCollections
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Resolve collection slides dynamically from Admin or fallback defaults
|
|
37
|
+
const resolvedItems = resolveAdminCollections(collections, items)
|
|
38
|
+
|
|
39
|
+
if (resolvedItems.length === 0) return null
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<VerticalScrollClient
|
|
43
|
+
items={resolvedItems}
|
|
44
|
+
title={title}
|
|
45
|
+
eyebrow={eyebrow}
|
|
46
|
+
ariaLabel={ariaLabel}
|
|
47
|
+
/>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { HttpTypes } from "@medusajs/types"
|
|
2
|
+
|
|
3
|
+
export type VerticalCollectionSlide = {
|
|
4
|
+
id?: string
|
|
5
|
+
title?: string
|
|
6
|
+
name?: string
|
|
7
|
+
subtitle?: string
|
|
8
|
+
description?: string
|
|
9
|
+
eyebrow?: string
|
|
10
|
+
tag?: string
|
|
11
|
+
category?: string
|
|
12
|
+
buttonText?: string
|
|
13
|
+
ctaText?: string
|
|
14
|
+
buttonLink?: string
|
|
15
|
+
link?: string
|
|
16
|
+
href?: string
|
|
17
|
+
image?: string
|
|
18
|
+
imageUrl?: string
|
|
19
|
+
photo?: string
|
|
20
|
+
thumbnail?: string
|
|
21
|
+
banner?: string
|
|
22
|
+
imageAlt?: string
|
|
23
|
+
theme?: "dark" | "light"
|
|
24
|
+
handle?: string
|
|
25
|
+
collectionHandle?: string
|
|
26
|
+
accentColor?: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type VerticalCollectionScrollProps = {
|
|
30
|
+
items?: VerticalCollectionSlide[]
|
|
31
|
+
collections?: HttpTypes.StoreCollection[]
|
|
32
|
+
categories?: any[]
|
|
33
|
+
title?: string
|
|
34
|
+
eyebrow?: string
|
|
35
|
+
ariaLabel?: string
|
|
36
|
+
showPaginationDots?: boolean
|
|
37
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React, { useState, useEffect, useRef } from "react"
|
|
4
|
+
import Image from "next/image"
|
|
5
|
+
import LocalizedLink from "@pradip1995/segment-primitives/localized-link"
|
|
6
|
+
import type { VerticalCollectionSlide } from "./types"
|
|
7
|
+
|
|
8
|
+
export default function VerticalScrollClient({
|
|
9
|
+
items,
|
|
10
|
+
title,
|
|
11
|
+
eyebrow,
|
|
12
|
+
ariaLabel,
|
|
13
|
+
}: {
|
|
14
|
+
items: VerticalCollectionSlide[]
|
|
15
|
+
title?: string
|
|
16
|
+
eyebrow?: string
|
|
17
|
+
ariaLabel?: string
|
|
18
|
+
}) {
|
|
19
|
+
const [activeIndex, setActiveIndex] = useState(0)
|
|
20
|
+
const [isNavVisible, setIsNavVisible] = useState(false)
|
|
21
|
+
const [isReady, setIsReady] = useState(false)
|
|
22
|
+
const sectionRef = useRef<HTMLDivElement>(null)
|
|
23
|
+
const slideRefs = useRef<(HTMLDivElement | null)[]>([])
|
|
24
|
+
const activeIdxRef = useRef(0)
|
|
25
|
+
|
|
26
|
+
const count = items.length
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
setIsReady(true)
|
|
30
|
+
}, [])
|
|
31
|
+
|
|
32
|
+
const scrollToSlide = (index: number) => {
|
|
33
|
+
const section = sectionRef.current
|
|
34
|
+
if (!section || typeof window === "undefined") return
|
|
35
|
+
|
|
36
|
+
const vh = window.innerHeight
|
|
37
|
+
const sectionTop = section.getBoundingClientRect().top + window.scrollY
|
|
38
|
+
window.scrollTo({
|
|
39
|
+
top: sectionTop + index * vh,
|
|
40
|
+
behavior: "smooth",
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Sticky stacked slides: IntersectionObserver fires for multiple slides at once.
|
|
45
|
+
// Use scroll position within the section (same as Graff parallax pin range).
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (typeof window === "undefined" || count === 0) return
|
|
48
|
+
|
|
49
|
+
let raf = 0
|
|
50
|
+
const syncFromScroll = () => {
|
|
51
|
+
raf = 0
|
|
52
|
+
const section = sectionRef.current
|
|
53
|
+
if (!section) return
|
|
54
|
+
|
|
55
|
+
const rect = section.getBoundingClientRect()
|
|
56
|
+
const vh = window.innerHeight
|
|
57
|
+
if (vh <= 0) return
|
|
58
|
+
|
|
59
|
+
const sectionInView = rect.bottom > 0 && rect.top < vh
|
|
60
|
+
const inParallax = rect.top <= 0 && rect.bottom >= vh - 1
|
|
61
|
+
|
|
62
|
+
setIsNavVisible(sectionInView && count > 1)
|
|
63
|
+
|
|
64
|
+
const scrolled = Math.max(0, -rect.top)
|
|
65
|
+
const slideProgress = scrolled / vh
|
|
66
|
+
|
|
67
|
+
// Graff: 1:1 wheel scroll through pinned layers — stable index with hysteresis (no flicker).
|
|
68
|
+
let idx = activeIdxRef.current
|
|
69
|
+
if (inParallax) {
|
|
70
|
+
const p = slideProgress
|
|
71
|
+
if (p >= idx + 0.5 && idx < count - 1) {
|
|
72
|
+
idx += 1
|
|
73
|
+
} else if (idx > 0 && p < idx - 0.35) {
|
|
74
|
+
idx -= 1
|
|
75
|
+
}
|
|
76
|
+
if (rect.bottom <= vh + 2) {
|
|
77
|
+
idx = count - 1
|
|
78
|
+
}
|
|
79
|
+
} else if (sectionInView) {
|
|
80
|
+
idx = 0
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
activeIdxRef.current = idx
|
|
84
|
+
setActiveIndex(idx)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const onScroll = () => {
|
|
88
|
+
if (raf) return
|
|
89
|
+
raf = window.requestAnimationFrame(syncFromScroll)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
syncFromScroll()
|
|
93
|
+
window.addEventListener("scroll", onScroll, { passive: true })
|
|
94
|
+
window.addEventListener("resize", onScroll, { passive: true })
|
|
95
|
+
|
|
96
|
+
return () => {
|
|
97
|
+
if (raf) window.cancelAnimationFrame(raf)
|
|
98
|
+
window.removeEventListener("scroll", onScroll)
|
|
99
|
+
window.removeEventListener("resize", onScroll)
|
|
100
|
+
}
|
|
101
|
+
}, [count])
|
|
102
|
+
|
|
103
|
+
if (count === 0) return null
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<section
|
|
107
|
+
ref={sectionRef}
|
|
108
|
+
className={`graff-parallax-section${isReady ? " is-ready" : ""}`}
|
|
109
|
+
aria-label={ariaLabel || title || "Luxury Collections Showcase"}
|
|
110
|
+
>
|
|
111
|
+
{/* Floating Right-Side Pagination Dots (Identical to Graff .b-parallax__pagination) */}
|
|
112
|
+
{count > 1 && (
|
|
113
|
+
<nav
|
|
114
|
+
className={`graff-parallax__pagination ${
|
|
115
|
+
isNavVisible ? "is-visible" : ""
|
|
116
|
+
}`}
|
|
117
|
+
aria-label="Collection slides navigation"
|
|
118
|
+
>
|
|
119
|
+
{items.map((slide, idx) => {
|
|
120
|
+
const isActive = idx === activeIndex
|
|
121
|
+
return (
|
|
122
|
+
<button
|
|
123
|
+
key={slide.id || `dot-${idx}`}
|
|
124
|
+
type="button"
|
|
125
|
+
className={`graff-parallax__pagination-dot ${
|
|
126
|
+
isActive ? "is-active" : ""
|
|
127
|
+
}`}
|
|
128
|
+
onClick={() => scrollToSlide(idx)}
|
|
129
|
+
aria-label={`Scroll to slide ${idx + 1}: ${slide.title}`}
|
|
130
|
+
aria-current={isActive ? "true" : undefined}
|
|
131
|
+
title={slide.title}
|
|
132
|
+
/>
|
|
133
|
+
)
|
|
134
|
+
})}
|
|
135
|
+
</nav>
|
|
136
|
+
)}
|
|
137
|
+
|
|
138
|
+
{/* Multiple Layered Sticky Slides (Identical to Graff .b-parallax__container pin:true, pinSpacing:false) */}
|
|
139
|
+
{items.map((slide, idx) => {
|
|
140
|
+
const theme = slide.theme || (idx % 2 === 1 ? "light" : "dark")
|
|
141
|
+
const imgUrl = slide.image || "/images/collections/emerald-sapphire.jpg"
|
|
142
|
+
const isActive = idx === activeIndex
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<div
|
|
146
|
+
key={slide.id || `slide-${idx}`}
|
|
147
|
+
ref={(el) => {
|
|
148
|
+
slideRefs.current[idx] = el
|
|
149
|
+
}}
|
|
150
|
+
className={`graff-parallax__slide graff-parallax__slide--${theme} ${
|
|
151
|
+
isActive ? "is-active" : ""
|
|
152
|
+
}`}
|
|
153
|
+
style={{
|
|
154
|
+
zIndex: idx + 1,
|
|
155
|
+
}}
|
|
156
|
+
>
|
|
157
|
+
<div className="graff-parallax__slide-inner">
|
|
158
|
+
{/* Full Bleed Image Background */}
|
|
159
|
+
<div className="graff-parallax__bg">
|
|
160
|
+
<Image
|
|
161
|
+
src={imgUrl}
|
|
162
|
+
alt={slide.imageAlt || slide.title || "Collection image"}
|
|
163
|
+
fill
|
|
164
|
+
priority={idx <= 1}
|
|
165
|
+
quality={90}
|
|
166
|
+
sizes="100vw"
|
|
167
|
+
className="graff-parallax__image"
|
|
168
|
+
unoptimized
|
|
169
|
+
/>
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
{/* Editorial Typography Content */}
|
|
173
|
+
<div className="graff-parallax__content-wrap">
|
|
174
|
+
<div className="graff-parallax__content">
|
|
175
|
+
{slide.eyebrow ? (
|
|
176
|
+
<span className="graff-parallax__eyebrow">
|
|
177
|
+
{slide.eyebrow}
|
|
178
|
+
</span>
|
|
179
|
+
) : null}
|
|
180
|
+
|
|
181
|
+
<h2 className="graff-parallax__title">{slide.title}</h2>
|
|
182
|
+
|
|
183
|
+
{slide.subtitle ? (
|
|
184
|
+
<p className="graff-parallax__subtitle">{slide.subtitle}</p>
|
|
185
|
+
) : null}
|
|
186
|
+
|
|
187
|
+
<LocalizedLink
|
|
188
|
+
href={slide.buttonLink || "/store"}
|
|
189
|
+
className="graff-parallax__btn"
|
|
190
|
+
>
|
|
191
|
+
{slide.buttonText || "DISCOVER"}
|
|
192
|
+
</LocalizedLink>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
{/* Bottom Scroll Indicator on first slide */}
|
|
197
|
+
{idx === 0 && count > 1 && activeIndex === 0 && (
|
|
198
|
+
<button
|
|
199
|
+
type="button"
|
|
200
|
+
className="graff-parallax__scroll-hint"
|
|
201
|
+
onClick={() => scrollToSlide(1)}
|
|
202
|
+
aria-label="Scroll to next collection"
|
|
203
|
+
>
|
|
204
|
+
<span className="graff-parallax__scroll-hint-text">
|
|
205
|
+
SCROLL
|
|
206
|
+
</span>
|
|
207
|
+
<div className="graff-parallax__scroll-hint-line" />
|
|
208
|
+
</button>
|
|
209
|
+
)}
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
)
|
|
213
|
+
})}
|
|
214
|
+
</section>
|
|
215
|
+
)
|
|
216
|
+
}
|