@pradip1995/segment-powerhouse-carousel 0.1.2 → 0.1.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pradip1995/segment-powerhouse-carousel",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
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
@@ -100,6 +100,10 @@
100
100
  background: #fff;
101
101
  }
102
102
 
103
+ .powerhouse-carousel__media-wrap {
104
+ position: relative;
105
+ }
106
+
103
107
  .powerhouse-carousel__card-media {
104
108
  position: relative;
105
109
  display: block;
@@ -108,6 +112,51 @@
108
112
  background: #f3f3f3;
109
113
  }
110
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
+
111
160
  .powerhouse-carousel__card-image,
112
161
  .powerhouse-carousel__card-placeholder {
113
162
  width: 100%;
@@ -242,9 +291,15 @@
242
291
  background: #f1eeae;
243
292
  border: 1px solid #f2f682;
244
293
  color: #1d1d1d;
294
+ cursor: pointer;
295
+ }
296
+
297
+ .powerhouse-carousel__quickshop:disabled {
298
+ opacity: 0.55;
299
+ cursor: not-allowed;
245
300
  }
246
301
 
247
- .powerhouse-carousel__quickshop:hover {
302
+ .powerhouse-carousel__quickshop:hover:not(:disabled) {
248
303
  background: #f4f1be;
249
304
  }
250
305
 
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>
@@ -2,21 +2,24 @@
2
2
 
3
3
  import { useState } from "react"
4
4
  import { useRouter } from "next/navigation"
5
- import { addToCart } from "@pradip1995/commerce-core/client/actions/cart"
5
+ import { addToCart, buyNow } from "@pradip1995/commerce-core/client/actions/cart"
6
6
  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)
22
+ const [buying, setBuying] = useState(false)
20
23
  const variant = product.variants?.[0]
21
24
  const price = variant?.calculated_price?.calculated_amount
22
25
  const originalPrice = variant?.calculated_price?.original_amount
@@ -30,17 +33,19 @@ export default function ProductCard({ product, region, countryCode }: Props) {
30
33
  (inventory ?? 0) > 0
31
34
  const href = `/products/${product.handle}`
32
35
  const hasOptions = (product.variants?.length ?? 0) > 1
36
+ const busy = adding || buying
37
+ const code = countryCode.toLowerCase()
33
38
 
34
39
  const handleAdd = async (event: React.MouseEvent) => {
35
40
  event.preventDefault()
36
41
  event.stopPropagation()
37
- if (!variant?.id || adding || !inStock || hasOptions) return
42
+ if (!variant?.id || busy || !inStock || hasOptions) return
38
43
  setAdding(true)
39
44
  try {
40
45
  await addToCart({
41
46
  variantId: variant.id,
42
47
  quantity: 1,
43
- countryCode: countryCode.toLowerCase(),
48
+ countryCode: code,
44
49
  })
45
50
  window.dispatchEvent(new CustomEvent("cart:item-added", { detail: { message: "Added to cart" } }))
46
51
  router.refresh()
@@ -53,19 +58,46 @@ export default function ProductCard({ product, region, countryCode }: Props) {
53
58
  }
54
59
  }
55
60
 
61
+ const handleBuyNow = async (event: React.MouseEvent) => {
62
+ event.preventDefault()
63
+ event.stopPropagation()
64
+ if (!variant?.id || busy || !inStock) return
65
+ setBuying(true)
66
+ try {
67
+ await buyNow({
68
+ variantId: variant.id,
69
+ quantity: 1,
70
+ countryCode: code,
71
+ })
72
+ router.refresh()
73
+ } catch (err) {
74
+ if (isNextRedirect(err)) throw err
75
+ const message = err instanceof Error ? err.message : "Buy now failed"
76
+ window.dispatchEvent(new CustomEvent("cart:item-added", { detail: { message, variant: "error" } }))
77
+ setBuying(false)
78
+ }
79
+ }
80
+
56
81
  return (
57
82
  <article className={`powerhouse-carousel__card${onSale ? " is-sale" : ""}`}>
58
- <LocalizedLink href={href} countryCode={countryCode} className="powerhouse-carousel__card-media">
59
- {product.thumbnail?.trim() ? (
60
- <img src={product.thumbnail.trim()} alt={product.title || ""} className="powerhouse-carousel__card-image" />
61
- ) : (
62
- <div className="powerhouse-carousel__card-placeholder" />
63
- )}
64
- {onSale && saved != null ? (
65
- <span className="powerhouse-carousel__badge">Save {formatPrice(saved, currency)}</span>
66
- ) : null}
67
- {!inStock ? <span className="powerhouse-carousel__sold">Sold out</span> : null}
68
- </LocalizedLink>
83
+ <div className="powerhouse-carousel__media-wrap">
84
+ <LocalizedLink href={href} countryCode={countryCode} className="powerhouse-carousel__card-media">
85
+ {product.thumbnail?.trim() ? (
86
+ <img src={product.thumbnail.trim()} alt={product.title || ""} className="powerhouse-carousel__card-image" />
87
+ ) : (
88
+ <div className="powerhouse-carousel__card-placeholder" />
89
+ )}
90
+ {onSale && saved != null ? (
91
+ <span className="powerhouse-carousel__badge">Save {formatPrice(saved, currency)}</span>
92
+ ) : null}
93
+ {!inStock ? <span className="powerhouse-carousel__sold">Sold out</span> : null}
94
+ </LocalizedLink>
95
+ <WishlistButton
96
+ productId={product.id}
97
+ countryCode={countryCode}
98
+ wishlistProductIds={wishlistProductIds}
99
+ />
100
+ </div>
69
101
 
70
102
  <div className="powerhouse-carousel__card-info">
71
103
  {price != null ? (
@@ -92,9 +124,16 @@ export default function ProductCard({ product, region, countryCode }: Props) {
92
124
  )}
93
125
 
94
126
  <div className="powerhouse-carousel__actions">
95
- <LocalizedLink href={href} countryCode={countryCode} className="powerhouse-carousel__quickshop">
96
- Quick shop
97
- </LocalizedLink>
127
+ {inStock ? (
128
+ <button
129
+ type="button"
130
+ className="powerhouse-carousel__quickshop"
131
+ onClick={handleBuyNow}
132
+ disabled={busy || !variant?.id}
133
+ >
134
+ {buying ? "Processing…" : "Buy now"}
135
+ </button>
136
+ ) : null}
98
137
  {hasOptions ? (
99
138
  <LocalizedLink href={href} countryCode={countryCode} className="powerhouse-carousel__atc">
100
139
  Choose options
@@ -104,7 +143,7 @@ export default function ProductCard({ product, region, countryCode }: Props) {
104
143
  type="button"
105
144
  className="powerhouse-carousel__atc"
106
145
  onClick={handleAdd}
107
- disabled={adding || !variant?.id || !inStock}
146
+ disabled={busy || !variant?.id || !inStock}
108
147
  >
109
148
  {adding ? "Adding…" : !inStock ? "Sold out" : "Add to cart"}
110
149
  </button>
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
+ }