@pradip1995/segment-powerhouse-carousel 0.1.1 → 0.1.3

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-powerhouse-carousel",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -24,6 +24,7 @@
24
24
  "dependencies": {
25
25
  "@pradip1995/commerce-core": "^4.0.0",
26
26
  "@pradip1995/medusa-connector": "^0.2.0",
27
+ "@pradip1995/segment-login-popup": "^0.1.2",
27
28
  "@pradip1995/segment-primitives": "^0.4.0"
28
29
  },
29
30
  "devDependencies": {
package/src/carousel.css CHANGED
@@ -90,16 +90,18 @@
90
90
  }
91
91
 
92
92
  .powerhouse-carousel__card {
93
+ position: relative;
94
+ z-index: 1;
93
95
  display: flex;
94
96
  flex-direction: column;
95
97
  height: 100%;
96
98
  padding: 0.6875rem;
99
+ border: 1px solid #dddddd;
97
100
  background: #fff;
98
- transition: box-shadow 0.25s cubic-bezier(0.4, 0, 0.2, 1);
99
101
  }
100
102
 
101
- .powerhouse-carousel__card:hover {
102
- box-shadow: 0 2px 8px #80808033;
103
+ .powerhouse-carousel__media-wrap {
104
+ position: relative;
103
105
  }
104
106
 
105
107
  .powerhouse-carousel__card-media {
@@ -110,12 +112,63 @@
110
112
  background: #f3f3f3;
111
113
  }
112
114
 
115
+ .powerhouse-carousel__wishlist {
116
+ position: absolute;
117
+ top: 0.5rem;
118
+ right: 0.5rem;
119
+ z-index: 2;
120
+ display: inline-flex;
121
+ align-items: center;
122
+ justify-content: center;
123
+ width: 2.25rem;
124
+ height: 2.25rem;
125
+ padding: 0;
126
+ border: 1px solid rgba(29, 29, 29, 0.12);
127
+ background: rgba(255, 255, 255, 0.94);
128
+ color: #1d1d1d;
129
+ cursor: pointer;
130
+ transition: color 0.2s ease, border-color 0.2s ease;
131
+ }
132
+
133
+ .powerhouse-carousel__wishlist:hover {
134
+ border-color: #1d1d1d;
135
+ }
136
+
137
+ .powerhouse-carousel__wishlist.is-active {
138
+ color: #c1272d;
139
+ border-color: #c1272d;
140
+ }
141
+
142
+ .powerhouse-carousel__wishlist.is-loading {
143
+ opacity: 0.55;
144
+ cursor: wait;
145
+ }
146
+
147
+ .powerhouse-carousel__wishlist-icon {
148
+ width: 1.05rem;
149
+ height: 1.05rem;
150
+ fill: none;
151
+ stroke: currentColor;
152
+ stroke-width: 1.75;
153
+ stroke-linejoin: round;
154
+ }
155
+
156
+ .powerhouse-carousel__wishlist.is-active .powerhouse-carousel__wishlist-icon {
157
+ fill: currentColor;
158
+ }
159
+
113
160
  .powerhouse-carousel__card-image,
114
161
  .powerhouse-carousel__card-placeholder {
115
162
  width: 100%;
116
163
  height: 100%;
117
164
  object-fit: cover;
118
165
  display: block;
166
+ transform: scale(1);
167
+ transition: transform 0.5s cubic-bezier(0, 0, 0.2, 1);
168
+ }
169
+
170
+ .powerhouse-carousel__card:hover .powerhouse-carousel__card-image {
171
+ transform: scale(1.05);
119
172
  }
120
173
 
121
174
  .powerhouse-carousel__badge {
@@ -131,6 +184,19 @@
131
184
  line-height: 1.2;
132
185
  }
133
186
 
187
+ .powerhouse-carousel__sold {
188
+ position: absolute;
189
+ bottom: 0.6rem;
190
+ left: 0.6rem;
191
+ padding: 0.2rem 0.45rem;
192
+ background: #1d1d1d;
193
+ color: #fff;
194
+ font-family: var(--font-sans, inherit);
195
+ font-size: 0.75rem;
196
+ font-weight: 600;
197
+ line-height: 1.2;
198
+ }
199
+
134
200
  .powerhouse-carousel__card-info {
135
201
  display: flex;
136
202
  flex-direction: column;
@@ -140,6 +206,7 @@
140
206
 
141
207
  .powerhouse-carousel__price {
142
208
  margin-top: 0.25rem;
209
+ min-height: 2.7rem;
143
210
  }
144
211
 
145
212
  .powerhouse-carousel__price-compare {
@@ -163,7 +230,12 @@
163
230
  }
164
231
 
165
232
  .powerhouse-carousel__card-title {
233
+ display: -webkit-box;
234
+ -webkit-box-orient: vertical;
235
+ -webkit-line-clamp: 2;
236
+ overflow: hidden;
166
237
  margin: 0.625rem 0 0;
238
+ min-height: calc(1em * 1.3125 * 2);
167
239
  font-family: var(--font-sans, inherit);
168
240
  font-size: 1rem;
169
241
  font-weight: 300;
@@ -177,31 +249,41 @@
177
249
 
178
250
  .powerhouse-carousel__stock {
179
251
  margin: 0.5rem 0 0;
252
+ min-height: 1.2em;
180
253
  color: #688600;
181
254
  font-size: 0.875rem;
182
255
  }
183
256
 
257
+ .powerhouse-carousel__stock.is-out {
258
+ color: #6e6e6e;
259
+ }
260
+
184
261
  .powerhouse-carousel__actions {
185
262
  display: flex;
186
- flex-wrap: wrap;
187
- gap: 0.5rem;
263
+ flex-direction: column;
264
+ flex-wrap: nowrap;
265
+ gap: 0.4rem;
188
266
  margin-top: auto;
189
267
  padding-top: 0.75rem;
190
268
  }
191
269
 
192
270
  .powerhouse-carousel__quickshop,
193
271
  .powerhouse-carousel__atc {
194
- display: inline-flex;
272
+ display: flex;
195
273
  align-items: center;
196
274
  justify-content: center;
275
+ width: 100%;
197
276
  min-height: 2.5rem;
198
- padding: 0.55rem 0.9rem;
277
+ padding: 0.5rem 0.45rem;
199
278
  border-radius: 2px;
279
+ box-sizing: border-box;
200
280
  font-family: var(--font-sans, inherit);
201
- font-size: 0.875rem;
202
- font-weight: 500;
203
- letter-spacing: 0.04em;
281
+ font-size: 0.8125rem;
282
+ font-weight: 600;
283
+ letter-spacing: 0;
204
284
  text-decoration: none;
285
+ text-align: center;
286
+ white-space: nowrap;
205
287
  cursor: pointer;
206
288
  }
207
289
 
@@ -227,7 +309,7 @@
227
309
 
228
310
  .powerhouse-carousel__atc:disabled {
229
311
  opacity: 0.65;
230
- cursor: wait;
312
+ cursor: not-allowed;
231
313
  }
232
314
 
233
315
  @media screen and (min-width: 720px) {
@@ -239,16 +321,40 @@
239
321
  @media screen and (min-width: 860px) {
240
322
  .powerhouse-carousel {
241
323
  margin-top: 1.75rem;
324
+ overflow: visible;
325
+ }
326
+
327
+ .powerhouse-carousel__viewport,
328
+ .powerhouse-carousel__track {
329
+ overflow: visible;
330
+ }
331
+
332
+ .powerhouse-carousel__track {
333
+ overflow-x: auto;
334
+ padding-bottom: 4.5rem;
335
+ margin-bottom: -3rem;
242
336
  }
243
337
 
244
338
  .powerhouse-carousel__heading {
245
339
  margin-bottom: 1.75rem;
246
340
  }
247
341
 
342
+ .powerhouse-carousel__slide {
343
+ display: flex;
344
+ align-items: stretch;
345
+ }
346
+
248
347
  .powerhouse-carousel__card {
348
+ width: 100%;
249
349
  padding: 1rem;
250
350
  }
251
351
 
352
+ .powerhouse-carousel__card:hover,
353
+ .powerhouse-carousel__card:focus-within {
354
+ z-index: 8;
355
+ box-shadow: 0 6px 18px rgba(29, 29, 29, 0.12);
356
+ }
357
+
252
358
  .powerhouse-carousel__arrow--prev {
253
359
  left: 2.1875rem;
254
360
  }
@@ -256,6 +362,38 @@
256
362
  .powerhouse-carousel__arrow--next {
257
363
  right: 2.1875rem;
258
364
  }
365
+
366
+ .powerhouse-carousel__actions {
367
+ position: absolute;
368
+ left: -1px;
369
+ right: -1px;
370
+ top: calc(100% - 1px);
371
+ flex-direction: row;
372
+ margin-top: 0;
373
+ padding: 0.75rem 1rem 1rem;
374
+ border: 1px solid #dddddd;
375
+ border-top: 0;
376
+ background: #fff;
377
+ box-sizing: border-box;
378
+ opacity: 0;
379
+ visibility: hidden;
380
+ pointer-events: none;
381
+ transition: opacity 0.2s ease, visibility 0.2s ease;
382
+ }
383
+
384
+ .powerhouse-carousel__quickshop,
385
+ .powerhouse-carousel__atc {
386
+ flex: 1 1 0;
387
+ width: auto;
388
+ min-width: 0;
389
+ }
390
+
391
+ .powerhouse-carousel__card:hover .powerhouse-carousel__actions,
392
+ .powerhouse-carousel__card:focus-within .powerhouse-carousel__actions {
393
+ opacity: 1;
394
+ visibility: visible;
395
+ pointer-events: auto;
396
+ }
259
397
  }
260
398
 
261
399
  @media screen and (min-width: 1080px) {
@@ -266,17 +404,6 @@
266
404
  .powerhouse-carousel__slide {
267
405
  flex-basis: calc(25% - 15px);
268
406
  }
269
-
270
- .powerhouse-carousel__actions {
271
- opacity: 0;
272
- pointer-events: none;
273
- transition: opacity 0.2s ease;
274
- }
275
-
276
- .powerhouse-carousel__card:hover .powerhouse-carousel__actions {
277
- opacity: 1;
278
- pointer-events: auto;
279
- }
280
407
  }
281
408
 
282
409
  @media screen and (min-width: 1280px) {
package/src/carousel.tsx CHANGED
@@ -20,10 +20,12 @@ export default function ProductCarousel({
20
20
  products,
21
21
  region,
22
22
  countryCode,
23
+ wishlistProductIds,
23
24
  }: {
24
25
  products: HttpTypes.StoreProduct[]
25
26
  region?: HttpTypes.StoreRegion
26
27
  countryCode: string
28
+ wishlistProductIds?: string[]
27
29
  }) {
28
30
  const trackRef = useRef<HTMLDivElement>(null)
29
31
  const [canPrev, setCanPrev] = useState(false)
@@ -87,7 +89,12 @@ export default function ProductCarousel({
87
89
  <div ref={trackRef} className="powerhouse-carousel__track">
88
90
  {products.map((product) => (
89
91
  <div key={product.id} className="powerhouse-carousel__slide">
90
- <ProductCard product={product} region={region} countryCode={countryCode} />
92
+ <ProductCard
93
+ product={product}
94
+ region={region}
95
+ countryCode={countryCode}
96
+ wishlistProductIds={wishlistProductIds}
97
+ />
91
98
  </div>
92
99
  ))}
93
100
  </div>
@@ -7,14 +7,16 @@ import LocalizedLink from "@pradip1995/segment-primitives/localized-link"
7
7
  import { formatPrice } from "@pradip1995/segment-primitives/format-price"
8
8
  import { isNextRedirect } from "@pradip1995/segment-primitives/is-next-redirect"
9
9
  import type { HttpTypes } from "@medusajs/types"
10
+ import WishlistButton from "./wishlist-button"
10
11
 
11
12
  type Props = {
12
13
  product: HttpTypes.StoreProduct
13
14
  region?: HttpTypes.StoreRegion
14
15
  countryCode: string
16
+ wishlistProductIds?: string[]
15
17
  }
16
18
 
17
- export default function ProductCard({ product, region, countryCode }: Props) {
19
+ export default function ProductCard({ product, region, countryCode, wishlistProductIds }: Props) {
18
20
  const router = useRouter()
19
21
  const [adding, setAdding] = useState(false)
20
22
  const variant = product.variants?.[0]
@@ -24,12 +26,17 @@ export default function ProductCard({ product, region, countryCode }: Props) {
24
26
  const onSale = originalPrice != null && price != null && originalPrice > price
25
27
  const saved = onSale && originalPrice != null && price != null ? originalPrice - price : null
26
28
  const inventory = variant?.inventory_quantity
29
+ const inStock =
30
+ variant?.manage_inventory === false ||
31
+ variant?.allow_backorder === true ||
32
+ (inventory ?? 0) > 0
27
33
  const href = `/products/${product.handle}`
34
+ const hasOptions = (product.variants?.length ?? 0) > 1
28
35
 
29
36
  const handleAdd = async (event: React.MouseEvent) => {
30
37
  event.preventDefault()
31
38
  event.stopPropagation()
32
- if (!variant?.id || adding) return
39
+ if (!variant?.id || adding || !inStock || hasOptions) return
33
40
  setAdding(true)
34
41
  try {
35
42
  await addToCart({
@@ -50,16 +57,24 @@ export default function ProductCard({ product, region, countryCode }: Props) {
50
57
 
51
58
  return (
52
59
  <article className={`powerhouse-carousel__card${onSale ? " is-sale" : ""}`}>
53
- <LocalizedLink href={href} countryCode={countryCode} className="powerhouse-carousel__card-media">
54
- {product.thumbnail?.trim() ? (
55
- <img src={product.thumbnail.trim()} alt={product.title || ""} className="powerhouse-carousel__card-image" />
56
- ) : (
57
- <div className="powerhouse-carousel__card-placeholder" />
58
- )}
59
- {onSale && saved != null ? (
60
- <span className="powerhouse-carousel__badge">Save {formatPrice(saved, currency)}</span>
61
- ) : null}
62
- </LocalizedLink>
60
+ <div className="powerhouse-carousel__media-wrap">
61
+ <LocalizedLink href={href} countryCode={countryCode} className="powerhouse-carousel__card-media">
62
+ {product.thumbnail?.trim() ? (
63
+ <img src={product.thumbnail.trim()} alt={product.title || ""} className="powerhouse-carousel__card-image" />
64
+ ) : (
65
+ <div className="powerhouse-carousel__card-placeholder" />
66
+ )}
67
+ {onSale && saved != null ? (
68
+ <span className="powerhouse-carousel__badge">Save {formatPrice(saved, currency)}</span>
69
+ ) : null}
70
+ {!inStock ? <span className="powerhouse-carousel__sold">Sold out</span> : null}
71
+ </LocalizedLink>
72
+ <WishlistButton
73
+ productId={product.id}
74
+ countryCode={countryCode}
75
+ wishlistProductIds={wishlistProductIds}
76
+ />
77
+ </div>
63
78
 
64
79
  <div className="powerhouse-carousel__card-info">
65
80
  {price != null ? (
@@ -79,21 +94,30 @@ export default function ProductCard({ product, region, countryCode }: Props) {
79
94
 
80
95
  {inventory != null && inventory > 0 && inventory <= 5 ? (
81
96
  <p className="powerhouse-carousel__stock">Only {inventory} left!</p>
82
- ) : null}
97
+ ) : !inStock ? (
98
+ <p className="powerhouse-carousel__stock is-out">Out of stock</p>
99
+ ) : (
100
+ <p className="powerhouse-carousel__stock">in stock</p>
101
+ )}
83
102
 
84
103
  <div className="powerhouse-carousel__actions">
85
104
  <LocalizedLink href={href} countryCode={countryCode} className="powerhouse-carousel__quickshop">
86
105
  Quick shop
87
106
  </LocalizedLink>
88
- <button
89
- type="button"
90
- className="powerhouse-carousel__atc"
91
- onClick={handleAdd}
92
- disabled={adding || !variant?.id}
93
- aria-label="Add to cart"
94
- >
95
- {adding ? "Adding…" : "Add to cart"}
96
- </button>
107
+ {hasOptions ? (
108
+ <LocalizedLink href={href} countryCode={countryCode} className="powerhouse-carousel__atc">
109
+ Choose options
110
+ </LocalizedLink>
111
+ ) : (
112
+ <button
113
+ type="button"
114
+ className="powerhouse-carousel__atc"
115
+ onClick={handleAdd}
116
+ disabled={adding || !variant?.id || !inStock}
117
+ >
118
+ {adding ? "Adding…" : !inStock ? "Sold out" : "Add to cart"}
119
+ </button>
120
+ )}
97
121
  </div>
98
122
  </div>
99
123
  </article>
package/src/segment.tsx CHANGED
@@ -10,6 +10,7 @@ export default async function PowerhouseCarousel({
10
10
  countryCode: countryCodeProp,
11
11
  title = DEFAULT_TITLE,
12
12
  emptyMessage = DEFAULT_EMPTY,
13
+ wishlistProductIds,
13
14
  }: PowerhouseCarouselProps) {
14
15
  const countryCode =
15
16
  countryCodeProp?.toLowerCase() ||
@@ -47,7 +48,12 @@ export default async function PowerhouseCarousel({
47
48
  {list.length === 0 ? (
48
49
  <p className="powerhouse-carousel__empty">{emptyMessage}</p>
49
50
  ) : (
50
- <ProductCarousel products={list} region={displayRegion ?? undefined} countryCode={countryCode} />
51
+ <ProductCarousel
52
+ products={list}
53
+ region={displayRegion ?? undefined}
54
+ countryCode={countryCode}
55
+ wishlistProductIds={wishlistProductIds}
56
+ />
51
57
  )}
52
58
  </section>
53
59
  )
@@ -0,0 +1,93 @@
1
+ "use client"
2
+
3
+ import { useEffect, useState, type MouseEvent } from "react"
4
+ import { useRouter } from "next/navigation"
5
+ import {
6
+ addToWishlist,
7
+ getWishlistProductIds,
8
+ removeFromWishlist,
9
+ } from "@pradip1995/commerce-core/client/actions/wishlist"
10
+ import LoginRequiredModal from "@pradip1995/segment-login-popup/required"
11
+
12
+ type WishlistButtonProps = {
13
+ productId?: string | null
14
+ countryCode: string
15
+ wishlistProductIds?: string[]
16
+ className?: string
17
+ }
18
+
19
+ export default function WishlistButton({
20
+ productId,
21
+ countryCode,
22
+ wishlistProductIds,
23
+ className = "powerhouse-carousel__wishlist",
24
+ }: WishlistButtonProps) {
25
+ const router = useRouter()
26
+ const [wishlisted, setWishlisted] = useState(false)
27
+ const [loading, setLoading] = useState(false)
28
+ const [loginRequired, setLoginRequired] = useState(false)
29
+
30
+ useEffect(() => {
31
+ if (!productId || wishlistProductIds === undefined) return
32
+ setWishlisted(wishlistProductIds.includes(productId))
33
+ }, [productId, wishlistProductIds])
34
+
35
+ async function handleToggle(event: MouseEvent) {
36
+ event.preventDefault()
37
+ event.stopPropagation()
38
+ if (!productId || loading) return
39
+
40
+ setLoading(true)
41
+ try {
42
+ let currentlyWishlisted = wishlisted
43
+ if (wishlistProductIds === undefined) {
44
+ try {
45
+ const ids = await getWishlistProductIds()
46
+ currentlyWishlisted = ids.includes(productId)
47
+ setWishlisted(currentlyWishlisted)
48
+ } catch {
49
+ currentlyWishlisted = false
50
+ }
51
+ }
52
+
53
+ const result = currentlyWishlisted
54
+ ? await removeFromWishlist(productId)
55
+ : await addToWishlist(productId)
56
+
57
+ if (result.success) {
58
+ setWishlisted(!currentlyWishlisted)
59
+ router.refresh()
60
+ return
61
+ }
62
+
63
+ if (result.error?.toLowerCase().includes("login")) {
64
+ setLoginRequired(true)
65
+ }
66
+ } finally {
67
+ setLoading(false)
68
+ }
69
+ }
70
+
71
+ return (
72
+ <>
73
+ <button
74
+ type="button"
75
+ className={`${className}${wishlisted ? " is-active" : ""}${loading ? " is-loading" : ""}`}
76
+ onClick={handleToggle}
77
+ disabled={loading || !productId}
78
+ aria-label={wishlisted ? "Remove from wishlist" : "Add to wishlist"}
79
+ aria-pressed={wishlisted}
80
+ >
81
+ <svg viewBox="0 0 24 24" aria-hidden className={`${className}-icon`}>
82
+ <path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" />
83
+ </svg>
84
+ </button>
85
+ <LoginRequiredModal
86
+ isOpen={loginRequired}
87
+ onClose={() => setLoginRequired(false)}
88
+ countryCode={countryCode}
89
+ message="Please log in to save items to your wishlist."
90
+ />
91
+ </>
92
+ )
93
+ }