@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 +5 -5
- package/src/beauty-product-card.tsx +1 -1
- package/src/default-product-card.tsx +1 -1
- package/src/jewelry-product-card.tsx +5 -2
- package/src/product-card-actions-class.test.ts +67 -0
- package/src/product-card-actions-class.ts +16 -0
- package/src/product-card-actions.tsx +13 -4
- package/src/resolve-product-images.ts +9 -15
- package/src/segment.css +20 -3
- package/src/segment.tsx +2 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pradip1995/segment-product-card",
|
|
3
|
-
"version": "0.3.
|
|
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-
|
|
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
|
-
|
|
185
|
-
{
|
|
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
|
-
|
|
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
|
-
|
|
7
|
-
?.map((image
|
|
8
|
-
.filter((url
|
|
6
|
+
product.images
|
|
7
|
+
?.map((image) => resolveMediaUrl(image.url))
|
|
8
|
+
.filter((url): url is string => !!url) ?? []
|
|
9
9
|
|
|
10
|
-
const variantImages = (
|
|
11
|
-
.map((variant
|
|
12
|
-
|
|
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
|
|
18
|
-
const thumbnail = resolveMediaUrl(
|
|
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
|
|
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
|
-
|
|
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"
|
|
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} />
|