@pradip1995/segment-product-card 0.3.16 → 0.3.18

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pradip1995/segment-product-card",
3
- "version": "0.3.16",
3
+ "version": "0.3.18",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -111,6 +111,7 @@ export function ProductRailByVariant({
111
111
  wishlistProductIds,
112
112
  productCardVariant,
113
113
  showNavButtons,
114
+ autoScroll,
114
115
  }: {
115
116
  variant: ProductRailVariant
116
117
  products: HttpTypes.StoreProduct[]
@@ -123,6 +124,7 @@ export function ProductRailByVariant({
123
124
  wishlistProductIds?: string[]
124
125
  productCardVariant?: ProductCardVariant
125
126
  showNavButtons?: boolean
127
+ autoScroll?: boolean
126
128
  }) {
127
129
  return (
128
130
  <>
@@ -136,6 +138,7 @@ export function ProductRailByVariant({
136
138
  wishlistProductIds={wishlistProductIds}
137
139
  productCardVariant={productCardVariant}
138
140
  showNavButtons={showNavButtons}
141
+ autoScroll={autoScroll}
139
142
  />
140
143
  ) : variant === "featured" ? (
141
144
  <ProductRailFeatured
@@ -1,9 +1,13 @@
1
1
  "use client"
2
2
 
3
- import { useRef } from "react"
3
+ import { useEffect, useRef, useState } from "react"
4
4
  import ProductCard, { type ProductCardVariant } from "./segment"
5
5
  import type { HttpTypes } from "@medusajs/types"
6
6
 
7
+ /** Auto-scroll only when the rail has more cards than fit in one row. */
8
+ const VISIBLE_CARDS = 4
9
+ const AUTO_SCROLL_MS = 3200
10
+
7
11
  type ProductScrollProps = {
8
12
  products: HttpTypes.StoreProduct[]
9
13
  region?: HttpTypes.StoreRegion | null
@@ -12,6 +16,8 @@ type ProductScrollProps = {
12
16
  wishlistProductIds?: string[]
13
17
  productCardVariant?: ProductCardVariant
14
18
  showNavButtons?: boolean
19
+ /** When true and there are more than 4 products, advance the rail automatically. */
20
+ autoScroll?: boolean
15
21
  }
16
22
 
17
23
  export default function ProductScroll({
@@ -22,8 +28,13 @@ export default function ProductScroll({
22
28
  wishlistProductIds,
23
29
  productCardVariant,
24
30
  showNavButtons = true,
31
+ autoScroll = false,
25
32
  }: ProductScrollProps) {
26
33
  const viewportRef = useRef<HTMLDivElement>(null)
34
+ const pauseUntilRef = useRef(0)
35
+ const [paused, setPaused] = useState(false)
36
+
37
+ const shouldAutoScroll = autoScroll && products.length > VISIBLE_CARDS
27
38
 
28
39
  const scroll = (direction: "prev" | "next") => {
29
40
  const viewport = viewportRef.current
@@ -35,15 +46,75 @@ export default function ProductScroll({
35
46
  })
36
47
  }
37
48
 
49
+ useEffect(() => {
50
+ if (!shouldAutoScroll) return
51
+ if (
52
+ typeof window !== "undefined" &&
53
+ window.matchMedia("(prefers-reduced-motion: reduce)").matches
54
+ ) {
55
+ return
56
+ }
57
+
58
+ const tick = () => {
59
+ const viewport = viewportRef.current
60
+ if (!viewport || paused) return
61
+ if (Date.now() < pauseUntilRef.current) return
62
+
63
+ const maxScroll = viewport.scrollWidth - viewport.clientWidth
64
+ if (maxScroll <= 4) return
65
+
66
+ const slide = viewport.querySelector(
67
+ ".product-scroll__slide, .bestsellers-carousel__slide"
68
+ ) as HTMLElement | null
69
+ const styles = getComputedStyle(viewport.querySelector(".product-scroll__track, .bestsellers-carousel__track") || viewport)
70
+ const gap = parseFloat(styles.columnGap || styles.gap || "16") || 16
71
+ const step = (slide?.offsetWidth || viewport.clientWidth / VISIBLE_CARDS) + gap
72
+ const next = viewport.scrollLeft + step
73
+
74
+ if (next >= maxScroll - 2) {
75
+ viewport.scrollTo({ left: 0, behavior: "smooth" })
76
+ } else {
77
+ viewport.scrollBy({ left: step, behavior: "smooth" })
78
+ }
79
+ }
80
+
81
+ const id = window.setInterval(tick, AUTO_SCROLL_MS)
82
+ return () => window.clearInterval(id)
83
+ }, [shouldAutoScroll, paused, products.length])
84
+
85
+ const pauseAutoScroll = (ms = AUTO_SCROLL_MS * 2) => {
86
+ pauseUntilRef.current = Date.now() + ms
87
+ }
88
+
38
89
  if (products.length === 0) return null
39
90
 
40
91
  return (
41
- <div className="product-scroll bestsellers-carousel">
92
+ <div
93
+ className="product-scroll bestsellers-carousel"
94
+ onMouseEnter={() => {
95
+ if (shouldAutoScroll) setPaused(true)
96
+ }}
97
+ onMouseLeave={() => {
98
+ if (shouldAutoScroll) {
99
+ setPaused(false)
100
+ pauseAutoScroll(800)
101
+ }
102
+ }}
103
+ onFocusCapture={() => {
104
+ if (shouldAutoScroll) pauseAutoScroll()
105
+ }}
106
+ onPointerDown={() => {
107
+ if (shouldAutoScroll) pauseAutoScroll()
108
+ }}
109
+ >
42
110
  {showNavButtons ? (
43
111
  <button
44
112
  type="button"
45
113
  className="product-scroll__nav bestsellers-carousel__nav product-scroll__nav--prev bestsellers-carousel__nav--prev"
46
- onClick={() => scroll("prev")}
114
+ onClick={() => {
115
+ pauseAutoScroll()
116
+ scroll("prev")
117
+ }}
47
118
  aria-label="Previous products"
48
119
  >
49
120
 
@@ -75,7 +146,10 @@ export default function ProductScroll({
75
146
  <button
76
147
  type="button"
77
148
  className="product-scroll__nav bestsellers-carousel__nav product-scroll__nav--next bestsellers-carousel__nav--next"
78
- onClick={() => scroll("next")}
149
+ onClick={() => {
150
+ pauseAutoScroll()
151
+ scroll("next")
152
+ }}
79
153
  aria-label="Next products"
80
154
  >
81
155
 
@@ -2,14 +2,19 @@ import type { HttpTypes } from "@medusajs/types"
2
2
  import { resolveMediaUrl } from "@pradip1995/segment-primitives/resolve-media-url"
3
3
 
4
4
  export function resolveProductCardImages(product: HttpTypes.StoreProduct) {
5
- const images =
5
+ const gallery =
6
6
  product.images
7
7
  ?.map((image) => resolveMediaUrl(image.url))
8
8
  .filter((url): url is string => !!url) ?? []
9
9
 
10
+ const variantImages = (product.variants ?? [])
11
+ .map((variant) => resolveMediaUrl((variant as { thumbnail?: string | null }).thumbnail))
12
+ .filter((url): url is string => !!url)
13
+
14
+ const images = [...gallery, ...variantImages]
10
15
  const thumbnail = resolveMediaUrl(product.thumbnail) || ""
11
16
  const primary = thumbnail || images[0] || ""
12
- const hover = images.find((url) => url && url !== primary) || ""
17
+ const hover = [...new Set(images)].find((url) => url && url !== primary) || ""
13
18
 
14
19
  return { primary, hover }
15
20
  }
package/src/segment.css CHANGED
@@ -246,10 +246,15 @@
246
246
  width: 100%;
247
247
  height: 100%;
248
248
  object-fit: cover;
249
- transition: opacity 0.4s ease;
249
+ transition: opacity 0.45s ease, transform 0.55s ease;
250
+ }
251
+
252
+ .jewelry-product-card__image--primary {
253
+ z-index: 1;
250
254
  }
251
255
 
252
256
  .jewelry-product-card__image--hover {
257
+ z-index: 2;
253
258
  opacity: 0;
254
259
  }
255
260
 
@@ -257,10 +262,14 @@
257
262
  opacity: 1;
258
263
  }
259
264
 
260
- .jewelry-product-card__media--has-hover:hover .jewelry-product-card__image--primary {
265
+ .jewelry-product-card:hover .jewelry-product-card__media--has-hover .jewelry-product-card__image--primary {
261
266
  opacity: 0;
262
267
  }
263
268
 
269
+ .jewelry-product-card:hover .jewelry-product-card__media:not(.jewelry-product-card__media--has-hover) .jewelry-product-card__image--primary {
270
+ transform: scale(1.06);
271
+ }
272
+
264
273
  .jewelry-product-card__placeholder {
265
274
  background: linear-gradient(135deg, var(--color-surface-muted) 0%, var(--color-surface-alt) 100%);
266
275
  }
@@ -288,14 +297,16 @@
288
297
  }
289
298
 
290
299
  .jewelry-product-card .product-card__quick-add-wrap {
291
- left: 0;
292
- right: 0;
293
- bottom: 0;
300
+ left: 0.75rem;
301
+ right: 0.75rem;
302
+ bottom: 0.75rem;
294
303
  top: auto;
304
+ z-index: 12;
305
+ padding: 0;
295
306
  opacity: 0;
296
- transform: translateY(100%);
307
+ transform: translateY(0.5rem);
297
308
  pointer-events: none;
298
- transition: opacity 0.25s ease, transform 0.25s ease;
309
+ transition: opacity 0.28s ease, transform 0.28s ease;
299
310
  }
300
311
 
301
312
  .jewelry-product-card:hover .product-card__quick-add-wrap {
@@ -306,19 +317,23 @@
306
317
 
307
318
  .jewelry-product-card .product-card__quick-add {
308
319
  width: 100%;
309
- border-radius: 0;
310
- background: rgba(26, 26, 26, 0.88);
311
- color: var(--color-text-inverse);
312
- letter-spacing: 0.08em;
320
+ border-radius: 999px;
321
+ border: 1px solid var(--color-footer-bg, #45355e);
322
+ background: var(--color-footer-bg, #45355e);
323
+ color: #ffffff;
324
+ letter-spacing: 0.12em;
313
325
  text-transform: uppercase;
314
326
  font-size: 0.6875rem;
315
327
  font-weight: 600;
316
- min-height: 2.75rem;
317
- backdrop-filter: blur(4px);
328
+ min-height: 2.5rem;
329
+ box-shadow: 0 8px 22px rgba(69, 53, 94, 0.22);
318
330
  }
319
331
 
320
- .jewelry-product-card .product-card__quick-add:hover:not(:disabled) {
321
- background: var(--color-brand-primary);
332
+ .jewelry-product-card .product-card__quick-add:hover:not(:disabled),
333
+ .jewelry-product-card .product-card__quick-add:focus-visible:not(:disabled) {
334
+ background: var(--color-footer-bg, #45355e);
335
+ border-color: var(--color-footer-bg, #45355e);
336
+ color: #ffffff;
322
337
  }
323
338
 
324
339
  .jewelry-product-card .product-card__quick-add-label {
@@ -368,12 +383,14 @@
368
383
  }
369
384
 
370
385
  .product-scroll__slide:has(.jewelry-product-card) {
371
- flex: 0 0 clamp(210px, 38vw, 280px);
386
+ flex: 0 0 calc((100cqw - 0.75rem) / 1.2);
387
+ width: calc((100cqw - 0.75rem) / 1.2);
372
388
  }
373
389
 
374
390
  @media (min-width: 768px) {
375
391
  .product-scroll__slide:has(.jewelry-product-card) {
376
392
  flex: 0 0 clamp(240px, 22vw, 280px);
393
+ width: auto;
377
394
  }
378
395
  }
379
396