@pradip1995/segment-powerhouse-shop 0.1.4 → 0.1.6

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-shop",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -24,6 +24,7 @@
24
24
  },
25
25
  "dependencies": {
26
26
  "@pradip1995/commerce-core": "^4.0.0",
27
+ "@pradip1995/segment-login-popup": "^0.1.2",
27
28
  "@pradip1995/segment-primitives": "^0.4.0"
28
29
  },
29
30
  "devDependencies": {
@@ -2,11 +2,13 @@
2
2
 
3
3
  import { useState, type MouseEvent } from "react"
4
4
  import { useRouter } from "next/navigation"
5
+ import { buyNow } from "@pradip1995/commerce-core/client/actions/cart"
5
6
  import { addToCart } from "./add-to-cart"
6
7
  import LocalizedLink from "@pradip1995/segment-primitives/localized-link"
7
8
  import { formatPrice } from "@pradip1995/segment-primitives/format-price"
8
9
  import { isNextRedirect } from "@pradip1995/segment-primitives/is-next-redirect"
9
10
  import type { HttpTypes } from "@medusajs/types"
11
+ import WishlistButton from "./wishlist-button"
10
12
 
11
13
  function stripHtml(value?: string | null) {
12
14
  if (!value) return ""
@@ -23,13 +25,16 @@ export default function ShopProductCard({
23
25
  product,
24
26
  countryCode,
25
27
  layout = "grid",
28
+ wishlistProductIds,
26
29
  }: {
27
30
  product: HttpTypes.StoreProduct
28
31
  countryCode: string
29
32
  layout?: "grid" | "list"
33
+ wishlistProductIds?: string[]
30
34
  }) {
31
35
  const router = useRouter()
32
36
  const [adding, setAdding] = useState(false)
37
+ const [buying, setBuying] = useState(false)
33
38
  const variants = product.variants ?? []
34
39
  const variant = variants[0]
35
40
  const price = variant?.calculated_price?.calculated_amount
@@ -47,11 +52,12 @@ export default function ShopProductCard({
47
52
  const hasOptions = variants.length > 1
48
53
  const copy = snippet(product.description, layout === "list" ? 220 : 140)
49
54
  const isList = layout === "list"
55
+ const busy = adding || buying
50
56
 
51
57
  const handleAdd = async (event: MouseEvent) => {
52
58
  event.preventDefault()
53
59
  event.stopPropagation()
54
- if (!variant?.id || adding || !inStock || hasOptions) return
60
+ if (!variant?.id || busy || !inStock || hasOptions) return
55
61
  setAdding(true)
56
62
  try {
57
63
  await addToCart({
@@ -70,6 +76,26 @@ export default function ShopProductCard({
70
76
  }
71
77
  }
72
78
 
79
+ const handleBuyNow = async (event: MouseEvent) => {
80
+ event.preventDefault()
81
+ event.stopPropagation()
82
+ if (!variant?.id || busy || !inStock) return
83
+ setBuying(true)
84
+ try {
85
+ await buyNow({
86
+ variantId: variant.id,
87
+ quantity: 1,
88
+ countryCode,
89
+ })
90
+ router.refresh()
91
+ } catch (err) {
92
+ if (isNextRedirect(err)) throw err
93
+ const message = err instanceof Error ? err.message : "Buy now failed"
94
+ window.dispatchEvent(new CustomEvent("cart:item-added", { detail: { message, variant: "error" } }))
95
+ setBuying(false)
96
+ }
97
+ }
98
+
73
99
  const priceBlock =
74
100
  price != null ? (
75
101
  <div className="powerhouse-shop__price">
@@ -80,13 +106,20 @@ export default function ShopProductCard({
80
106
  </div>
81
107
  ) : null
82
108
 
109
+ const buyNowControl = (
110
+ <button
111
+ type="button"
112
+ className="powerhouse-shop__quickshop"
113
+ onClick={handleBuyNow}
114
+ disabled={busy || !variant?.id}
115
+ >
116
+ {buying ? "Processing…" : "Buy now"}
117
+ </button>
118
+ )
119
+
83
120
  const actionBlock = (
84
121
  <div className="powerhouse-shop__actions">
85
- {inStock ? (
86
- <LocalizedLink href={href} countryCode={countryCode} className="powerhouse-shop__quickshop">
87
- Quick shop
88
- </LocalizedLink>
89
- ) : null}
122
+ {inStock ? buyNowControl : null}
90
123
  {!inStock ? (
91
124
  <button type="button" className="powerhouse-shop__atc" disabled>
92
125
  Sold out
@@ -100,7 +133,7 @@ export default function ShopProductCard({
100
133
  type="button"
101
134
  className="powerhouse-shop__atc"
102
135
  onClick={handleAdd}
103
- disabled={adding || !variant?.id}
136
+ disabled={busy || !variant?.id}
104
137
  >
105
138
  {adding ? "Adding…" : "Add to cart"}
106
139
  </button>
@@ -110,17 +143,24 @@ export default function ShopProductCard({
110
143
 
111
144
  return (
112
145
  <article className={`powerhouse-shop__card${onSale ? " is-sale" : ""}${isList ? " is-list-card" : ""}`}>
113
- <LocalizedLink href={href} countryCode={countryCode} className="powerhouse-shop__media" tabIndex={-1}>
114
- {product.thumbnail?.trim() ? (
115
- <img src={product.thumbnail.trim()} alt={product.title || ""} />
116
- ) : (
117
- <div className="powerhouse-shop__placeholder" />
118
- )}
119
- {onSale && saved != null ? (
120
- <span className="powerhouse-shop__badge">Save {formatPrice(saved, currency)}</span>
121
- ) : null}
122
- {!inStock && !isList ? <span className="powerhouse-shop__sold">Sold out</span> : null}
123
- </LocalizedLink>
146
+ <div className="powerhouse-shop__media-wrap">
147
+ <LocalizedLink href={href} countryCode={countryCode} className="powerhouse-shop__media" tabIndex={-1}>
148
+ {product.thumbnail?.trim() ? (
149
+ <img src={product.thumbnail.trim()} alt={product.title || ""} />
150
+ ) : (
151
+ <div className="powerhouse-shop__placeholder" />
152
+ )}
153
+ {onSale && saved != null ? (
154
+ <span className="powerhouse-shop__badge">Save {formatPrice(saved, currency)}</span>
155
+ ) : null}
156
+ {!inStock && !isList ? <span className="powerhouse-shop__sold">Sold out</span> : null}
157
+ </LocalizedLink>
158
+ <WishlistButton
159
+ productId={product.id}
160
+ countryCode={countryCode}
161
+ wishlistProductIds={wishlistProductIds}
162
+ />
163
+ </div>
124
164
 
125
165
  {isList ? (
126
166
  <>
package/src/shop.css CHANGED
@@ -458,7 +458,7 @@ html.powerhouse-filters-open body {
458
458
  border: 1px solid #dddddd;
459
459
  }
460
460
 
461
- .powerhouse-shop__grid.is-list .powerhouse-shop__media {
461
+ .powerhouse-shop__grid.is-list .powerhouse-shop__media-wrap {
462
462
  grid-row: 1 / 3;
463
463
  }
464
464
 
@@ -557,6 +557,10 @@ html.powerhouse-filters-open body {
557
557
  background: #fff;
558
558
  }
559
559
 
560
+ .powerhouse-shop__media-wrap {
561
+ position: relative;
562
+ }
563
+
560
564
  .powerhouse-shop__media {
561
565
  position: relative;
562
566
  display: block;
@@ -566,6 +570,51 @@ html.powerhouse-filters-open body {
566
570
  aspect-ratio: 1;
567
571
  }
568
572
 
573
+ .powerhouse-shop__wishlist {
574
+ position: absolute;
575
+ top: 0.5rem;
576
+ right: 0.5rem;
577
+ z-index: 2;
578
+ display: inline-flex;
579
+ align-items: center;
580
+ justify-content: center;
581
+ width: 2.25rem;
582
+ height: 2.25rem;
583
+ padding: 0;
584
+ border: 1px solid rgba(29, 29, 29, 0.12);
585
+ background: rgba(255, 255, 255, 0.94);
586
+ color: #1d1d1d;
587
+ cursor: pointer;
588
+ transition: color 0.2s ease, background 0.2s ease, border-color 0.2s ease;
589
+ }
590
+
591
+ .powerhouse-shop__wishlist:hover {
592
+ border-color: #1d1d1d;
593
+ }
594
+
595
+ .powerhouse-shop__wishlist.is-active {
596
+ color: #c1272d;
597
+ border-color: #c1272d;
598
+ }
599
+
600
+ .powerhouse-shop__wishlist.is-loading {
601
+ opacity: 0.55;
602
+ cursor: wait;
603
+ }
604
+
605
+ .powerhouse-shop__wishlist-icon {
606
+ width: 1.05rem;
607
+ height: 1.05rem;
608
+ fill: none;
609
+ stroke: currentColor;
610
+ stroke-width: 1.75;
611
+ stroke-linejoin: round;
612
+ }
613
+
614
+ .powerhouse-shop__wishlist.is-active .powerhouse-shop__wishlist-icon {
615
+ fill: currentColor;
616
+ }
617
+
569
618
  .powerhouse-shop__media img,
570
619
  .powerhouse-shop__placeholder {
571
620
  position: absolute;
@@ -592,8 +641,10 @@ html.powerhouse-filters-open body {
592
641
  }
593
642
 
594
643
  .powerhouse-shop__sold {
595
- left: auto;
596
- right: 0.5rem;
644
+ left: 0.5rem;
645
+ right: auto;
646
+ top: auto;
647
+ bottom: 0.5rem;
597
648
  background: #1d1d1d;
598
649
  color: #fff;
599
650
  }
@@ -699,6 +750,12 @@ html.powerhouse-filters-open body {
699
750
  border: 1px solid #f2f682;
700
751
  background: #f1eeae;
701
752
  color: #1d1d1d;
753
+ cursor: pointer;
754
+ }
755
+
756
+ .powerhouse-shop__quickshop:disabled {
757
+ opacity: 0.55;
758
+ cursor: not-allowed;
702
759
  }
703
760
 
704
761
  .powerhouse-shop__atc {
@@ -857,7 +914,7 @@ html.powerhouse-filters-open body {
857
914
  padding: 1.25rem;
858
915
  }
859
916
 
860
- .powerhouse-shop__grid.is-list .powerhouse-shop__media {
917
+ .powerhouse-shop__grid.is-list .powerhouse-shop__media-wrap {
861
918
  grid-row: auto;
862
919
  }
863
920
 
package/src/shop.tsx CHANGED
@@ -620,6 +620,7 @@ export default function PowerhouseShop(props: PowerhouseShopProps) {
620
620
  product={product}
621
621
  countryCode={countryCode}
622
622
  layout={view}
623
+ wishlistProductIds={props.wishlistProductIds}
623
624
  />
624
625
  ))}
625
626
  </div>
package/src/types.ts CHANGED
@@ -25,6 +25,7 @@ export type PowerhouseShopProps = {
25
25
  basePath?: string
26
26
  bannerImage?: string
27
27
  bannerText?: string
28
+ wishlistProductIds?: string[]
28
29
  }
29
30
 
30
31
  export const PAGE_SIZE = 12
@@ -0,0 +1,94 @@
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-shop__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
+ return
66
+ }
67
+ } finally {
68
+ setLoading(false)
69
+ }
70
+ }
71
+
72
+ return (
73
+ <>
74
+ <button
75
+ type="button"
76
+ className={`${className}${wishlisted ? " is-active" : ""}${loading ? " is-loading" : ""}`}
77
+ onClick={handleToggle}
78
+ disabled={loading || !productId}
79
+ aria-label={wishlisted ? "Remove from wishlist" : "Add to wishlist"}
80
+ aria-pressed={wishlisted}
81
+ >
82
+ <svg viewBox="0 0 24 24" aria-hidden className={`${className}-icon`}>
83
+ <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" />
84
+ </svg>
85
+ </button>
86
+ <LoginRequiredModal
87
+ isOpen={loginRequired}
88
+ onClose={() => setLoginRequired(false)}
89
+ countryCode={countryCode}
90
+ message="Please log in to save items to your wishlist."
91
+ />
92
+ </>
93
+ )
94
+ }