@pradip1995/segment-product-card 0.3.23 → 0.3.25

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.23",
3
+ "version": "0.3.25",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -20,12 +20,12 @@
20
20
  "peerDependencies": {
21
21
  "@pradip1995/commerce-core": "^4.0.0",
22
22
  "@pradip1995/plugin-sdk": "^0.2.0",
23
- "@pradip1995/segment-animation-login-popup": "*",
24
23
  "next": ">=15",
25
24
  "react": ">=19",
26
25
  "react-dom": ">=19"
27
26
  },
28
27
  "dependencies": {
28
+ "@pradip1995/segment-login-popup": "^0.1.3",
29
29
  "@pradip1995/segment-primitives": "^0.4.2",
30
30
  "@pradip1995/segment-tokens": "^0.3.7"
31
31
  },
@@ -36,11 +36,11 @@
36
36
  "@types/react-dom": "^19",
37
37
  "next": ">=15",
38
38
  "react": "19.0.3",
39
- "typescript": "^5.7.2",
40
- "@pradip1995/segment-animation-login-popup": "0.1.1"
39
+ "typescript": "^5.7.2"
41
40
  },
42
41
  "scripts": {
43
42
  "typecheck": "tsc --noEmit",
44
- "lint": "tsc --noEmit"
43
+ "lint": "tsc --noEmit",
44
+ "test": "node --experimental-strip-types --test src/*.test.ts"
45
45
  }
46
46
  }
@@ -61,7 +61,7 @@ export default function BeautyProductCard({
61
61
  href={`/products/${product.handle}`}
62
62
  className="beauty-product-card__link flex flex-col h-full"
63
63
  >
64
- <div className="beauty-product-card__media relative aspect-square bg-[var(--color-surface-muted)] overflow-hidden">
64
+ <div className="beauty-product-card__media group/media relative aspect-square bg-[var(--color-surface-muted)] overflow-hidden">
65
65
  {product.thumbnail?.trim() ? (
66
66
  <Image
67
67
  src={product.thumbnail.trim()}
@@ -43,7 +43,7 @@ export default function DefaultProductCard({
43
43
  className="product-card group flex flex-col w-full h-full relative"
44
44
  >
45
45
  <LocalizedLink href={`/products/${product.handle}`} className="product-card__link flex flex-col h-full">
46
- <div className="product-card__media relative aspect-[3/4] bg-surface-muted overflow-hidden rounded-lg">
46
+ <div className="product-card__media group/media relative aspect-[3/4] bg-surface-muted overflow-hidden rounded-lg">
47
47
  {product.thumbnail?.trim() ? (
48
48
  <Image
49
49
  src={product.thumbnail.trim()}
@@ -11,6 +11,7 @@ type Props = {
11
11
  product: HttpTypes.StoreProduct
12
12
  region?: HttpTypes.StoreRegion
13
13
  countryCode?: string
14
+ showWishlist?: boolean
14
15
  wishlistProductIds?: string[]
15
16
  }
16
17
 
@@ -18,6 +19,7 @@ export default function JewelryProductCard({
18
19
  product,
19
20
  region,
20
21
  countryCode: countryCodeProp,
22
+ showWishlist = true,
21
23
  wishlistProductIds,
22
24
  }: Props) {
23
25
  const variant = product.variants?.[0]
@@ -40,7 +42,7 @@ export default function JewelryProductCard({
40
42
  className="jewelry-product-card__link flex flex-col h-full"
41
43
  >
42
44
  <div
43
- className={`jewelry-product-card__media relative aspect-square overflow-hidden rounded-2xl bg-[var(--color-surface-muted)]${
45
+ className={`jewelry-product-card__media group/media relative aspect-square overflow-hidden rounded-2xl bg-[var(--color-surface-muted)]${
44
46
  hover ? " jewelry-product-card__media--has-hover" : ""
45
47
  }`}
46
48
  >
@@ -70,11 +72,12 @@ export default function JewelryProductCard({
70
72
  <div className="jewelry-product-card__placeholder w-full h-full" />
71
73
  )}
72
74
 
73
- {product.id ? (
75
+ {showWishlist && product.id ? (
74
76
  <ProductCardActions
75
77
  product={product}
76
78
  countryCode={countryCode}
77
79
  wishlistProductIds={wishlistProductIds}
80
+ showWishlist={showWishlist}
78
81
  />
79
82
  ) : null}
80
83
  </div>
@@ -0,0 +1,67 @@
1
+ import { describe, it } from "node:test"
2
+ import assert from "node:assert/strict"
3
+ import {
4
+ productCardActionsClass,
5
+ productCardWishlistClass,
6
+ } from "./product-card-actions-class.ts"
7
+
8
+ const WISHLIST_CASES = [
9
+ {
10
+ name: "wishlist wrap is visible without hover",
11
+ wantIncludes: ["product-card__wishlist-wrap", "top-3.5", "right-3.5", "pointer-events-auto"],
12
+ wantExcludes: ["opacity-0", "group-hover/media"],
13
+ },
14
+ ]
15
+
16
+ const CART_CASES = [
17
+ {
18
+ name: "hover hides cart actions until named media group hover",
19
+ visibility: "hover" as const,
20
+ wantIncludes: [
21
+ "opacity-0",
22
+ "group-hover/media:opacity-100",
23
+ "group-hover/media:pointer-events-auto",
24
+ ],
25
+ wantExcludes: ["top-3.5"],
26
+ },
27
+ {
28
+ name: "always keeps cart actions visible",
29
+ visibility: "always" as const,
30
+ wantIncludes: ["top-3.5", "right-3.5", "pointer-events-auto"],
31
+ wantExcludes: ["opacity-0", "group-hover/media"],
32
+ },
33
+ {
34
+ name: "unknown visibility falls back to hover",
35
+ visibility: undefined,
36
+ wantIncludes: ["opacity-0", "group-hover/media:opacity-100"],
37
+ wantExcludes: ["top-3.5"],
38
+ },
39
+ ]
40
+
41
+ describe("productCardWishlistClass", () => {
42
+ for (const tc of WISHLIST_CASES) {
43
+ it(tc.name, () => {
44
+ const className = productCardWishlistClass()
45
+ for (const snippet of tc.wantIncludes) {
46
+ assert.match(className, new RegExp(snippet.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")))
47
+ }
48
+ for (const snippet of tc.wantExcludes) {
49
+ assert.doesNotMatch(className, new RegExp(snippet.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")))
50
+ }
51
+ })
52
+ }
53
+ })
54
+
55
+ describe("productCardActionsClass", () => {
56
+ for (const tc of CART_CASES) {
57
+ it(tc.name, () => {
58
+ const className = productCardActionsClass(tc.visibility)
59
+ for (const snippet of tc.wantIncludes) {
60
+ assert.match(className, new RegExp(snippet.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")))
61
+ }
62
+ for (const snippet of tc.wantExcludes) {
63
+ assert.doesNotMatch(className, new RegExp(snippet.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")))
64
+ }
65
+ })
66
+ }
67
+ })
@@ -0,0 +1,16 @@
1
+ export type ProductCardActionsVisibility = "hover" | "always"
2
+
3
+ export function productCardWishlistClass(): string {
4
+ return "product-card__wishlist-wrap absolute top-3.5 right-3.5 z-30 pointer-events-auto"
5
+ }
6
+
7
+ export function productCardActionsClass(
8
+ visibility?: ProductCardActionsVisibility
9
+ ): string {
10
+ const mode = visibility === "always" ? "always" : "hover"
11
+ const base = "product-card__actions absolute z-30"
12
+ if (mode === "always") {
13
+ return `${base} top-3.5 right-3.5 pointer-events-auto`
14
+ }
15
+ return `${base} bottom-3.5 right-3.5 opacity-0 group-hover/media:opacity-100 transition-opacity duration-300 pointer-events-none group-hover/media:pointer-events-auto`
16
+ }
@@ -10,14 +10,20 @@ import {
10
10
  } from "@pradip1995/commerce-core/client/actions/wishlist"
11
11
  import type { HttpTypes } from "@medusajs/types"
12
12
  import { isNextRedirect } from "@pradip1995/segment-primitives/is-next-redirect"
13
- import LoginRequiredModal from "@pradip1995/segment-animation-login-popup/required"
13
+ import LoginRequiredModal from "@pradip1995/segment-login-popup/required"
14
14
  import QuickAddPanel from "./quick-add-panel"
15
+ import {
16
+ productCardActionsClass,
17
+ productCardWishlistClass,
18
+ type ProductCardActionsVisibility,
19
+ } from "./product-card-actions-class"
15
20
 
16
21
  type Props = {
17
22
  product: HttpTypes.StoreProduct
18
23
  countryCode: string
19
24
  wishlistProductIds?: string[]
20
25
  showWishlist?: boolean
26
+ actionsVisible?: ProductCardActionsVisibility
21
27
  }
22
28
 
23
29
  function findVariantForOptions(
@@ -44,6 +50,7 @@ export default function ProductCardActions({
44
50
  countryCode,
45
51
  wishlistProductIds,
46
52
  showWishlist = true,
53
+ actionsVisible = "hover",
47
54
  }: Props) {
48
55
  const router = useRouter()
49
56
  const [wishlisted, setWishlisted] = useState(false)
@@ -181,8 +188,8 @@ export default function ProductCardActions({
181
188
 
182
189
  return (
183
190
  <>
184
- <div className="product-card__actions absolute bottom-3.5 right-3.5 z-30 opacity-0 group-hover/media:opacity-100 transition-opacity duration-300 pointer-events-none group-hover/media:pointer-events-auto" onClick={stopCardNav}>
185
- {showWishlist ? (
191
+ {showWishlist ? (
192
+ <div className={productCardWishlistClass()} onClick={stopCardNav}>
186
193
  <button
187
194
  type="button"
188
195
  className={`product-card__wishlist-btn${wishlisted ? " product-card__wishlist-btn--active" : ""}${
@@ -200,8 +207,10 @@ export default function ProductCardActions({
200
207
  <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" />
201
208
  </svg>
202
209
  </button>
203
- ) : null}
210
+ </div>
211
+ ) : null}
204
212
 
213
+ <div className={productCardActionsClass(actionsVisible)} onClick={stopCardNav}>
205
214
  <button
206
215
  type="button"
207
216
  className="product-card__cart-btn"
@@ -3,24 +3,18 @@ import { resolveMediaUrl } from "@pradip1995/segment-primitives/resolve-media-ur
3
3
 
4
4
  export function resolveProductCardImages(product: HttpTypes.StoreProduct) {
5
5
  const gallery =
6
- (product.images as { url?: string | null }[] | undefined)
7
- ?.map((image: { url?: string | null }) => resolveMediaUrl(image.url) || image.url?.trim())
8
- .filter((url: string | undefined): url is string => Boolean(url)) ?? []
6
+ product.images
7
+ ?.map((image) => resolveMediaUrl(image.url))
8
+ .filter((url): url is string => !!url) ?? []
9
9
 
10
- const variantImages = ((product.variants as { thumbnail?: string | null }[] | undefined) ?? [])
11
- .map((variant: { thumbnail?: string | null }) => {
12
- const raw = variant.thumbnail
13
- return resolveMediaUrl(raw) || raw?.trim()
14
- })
15
- .filter((url: string | undefined): url is string => Boolean(url))
10
+ const variantImages = (product.variants ?? [])
11
+ .map((variant) => resolveMediaUrl((variant as { thumbnail?: string | null }).thumbnail))
12
+ .filter((url): url is string => !!url)
16
13
 
17
- const rawThumb = product.thumbnail?.trim() || ""
18
- const thumbnail = resolveMediaUrl(rawThumb) || rawThumb
19
-
20
- const images = Array.from(new Set([thumbnail, ...gallery, ...variantImages].filter((url: string): url is string => Boolean(url))))
21
-
14
+ const images = [...gallery, ...variantImages]
15
+ const thumbnail = resolveMediaUrl(product.thumbnail) || ""
22
16
  const primary = thumbnail || images[0] || ""
23
- const hover = images.find((url: string) => Boolean(url) && url !== primary) || ""
17
+ const hover = [...new Set(images)].find((url) => url && url !== primary) || ""
24
18
 
25
19
  return { primary, hover }
26
20
  }
package/src/segment.css CHANGED
@@ -58,6 +58,11 @@
58
58
  right: 0.875rem;
59
59
  }
60
60
 
61
+ .beauty-product-card .product-card__wishlist-wrap {
62
+ top: 0.625rem;
63
+ right: 0.625rem;
64
+ }
65
+
61
66
  .beauty-product-card .product-card__wishlist-btn,
62
67
  .beauty-product-card .product-card__cart-btn {
63
68
  width: 2.125rem;
@@ -304,10 +309,22 @@
304
309
  background: linear-gradient(135deg, var(--color-surface-muted) 0%, var(--color-surface-alt) 100%);
305
310
  }
306
311
 
312
+ .product-card__wishlist-wrap {
313
+ position: absolute;
314
+ top: 0.75rem;
315
+ right: 0.75rem;
316
+ z-index: 30;
317
+ opacity: 1;
318
+ pointer-events: auto;
319
+ }
320
+
321
+ .jewelry-product-card .product-card__wishlist-wrap {
322
+ top: 0.75rem;
323
+ right: 0.75rem;
324
+ }
325
+
307
326
  .jewelry-product-card .product-card__actions {
308
- bottom: 0.875rem;
309
- top: auto;
310
- right: 0.875rem;
327
+ display: none;
311
328
  }
312
329
 
313
330
  .jewelry-product-card .product-card__wishlist-btn {
package/src/segment.tsx CHANGED
@@ -5,10 +5,8 @@ import type { HttpTypes } from "@medusajs/types"
5
5
  import BeautyProductCard from "./beauty-product-card"
6
6
  import DefaultProductCard from "./default-product-card"
7
7
  import JewelryProductCard from "./jewelry-product-card"
8
- import MotionProductCard from "./motion-product-card"
9
- import ShopProductCard from "./shop-product-card"
10
8
 
11
- export type ProductCardVariant = "default" | "beauty" | "jewelry" | "motion" | "shop"
9
+ export type ProductCardVariant = "default" | "beauty" | "jewelry"
12
10
 
13
11
  type Props = {
14
12
  product: HttpTypes.StoreProduct
@@ -21,21 +19,12 @@ type Props = {
21
19
  }
22
20
 
23
21
  function resolveProductCardVariant(variant?: ProductCardVariant): ProductCardVariant {
24
- if (
25
- variant === "beauty" ||
26
- variant === "default" ||
27
- variant === "jewelry" ||
28
- variant === "motion" ||
29
- variant === "shop"
30
- )
31
- return variant
22
+ if (variant === "beauty" || variant === "default" || variant === "jewelry") return variant
32
23
  return "default"
33
24
  }
34
25
 
35
26
  export default function ProductCard({ variant, ...props }: Props) {
36
27
  const layout = resolveProductCardVariant(variant)
37
- if (layout === "shop") return <ShopProductCard {...props} />
38
- if (layout === "motion") return <MotionProductCard {...props} />
39
28
  if (layout === "beauty") return <BeautyProductCard {...props} />
40
29
  if (layout === "jewelry") return <JewelryProductCard {...props} />
41
30
  return <DefaultProductCard {...props} />