@pradip1995/segment-product-card 0.3.2 → 0.3.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,11 +1,13 @@
1
1
  {
2
2
  "name": "@pradip1995/segment-product-card",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
8
- "sideEffects": false,
8
+ "sideEffects": [
9
+ "src/segment.css"
10
+ ],
9
11
  "files": [
10
12
  "src"
11
13
  ],
@@ -0,0 +1,108 @@
1
+ "use client"
2
+
3
+ import LocalizedLink from "@pradip1995/segment-primitives/localized-link"
4
+ import { formatPrice } from "@pradip1995/segment-primitives/format-price"
5
+ import type { HttpTypes } from "@medusajs/types"
6
+ import ProductCardActions from "./product-card-actions"
7
+
8
+ type Props = {
9
+ product: HttpTypes.StoreProduct
10
+ region?: HttpTypes.StoreRegion
11
+ countryCode?: string
12
+ rating?: { rating?: number }
13
+ showWishlist?: boolean
14
+ }
15
+
16
+ function productSubtitle(product: HttpTypes.StoreProduct) {
17
+ const category = product.categories?.[0]?.name?.trim()
18
+ if (category) return category
19
+ const collection = (product as HttpTypes.StoreProduct & { collection?: { title?: string } }).collection
20
+ ?.title
21
+ return collection?.trim() || "Beauty"
22
+ }
23
+
24
+ export default function BeautyProductCard({
25
+ product,
26
+ region,
27
+ countryCode: countryCodeProp,
28
+ rating,
29
+ showWishlist = true,
30
+ }: Props) {
31
+ const variant = product.variants?.[0]
32
+ const price = variant?.calculated_price?.calculated_amount
33
+ const originalPrice = variant?.calculated_price?.original_amount
34
+ const currency = region?.currency_code || variant?.calculated_price?.currency_code || "inr"
35
+ const countryCode =
36
+ countryCodeProp?.toLowerCase() ??
37
+ region?.countries?.[0]?.iso_2?.toLowerCase() ??
38
+ process.env.NEXT_PUBLIC_DEFAULT_REGION?.toLowerCase() ??
39
+ "in"
40
+
41
+ const discountPct =
42
+ originalPrice && price && originalPrice > price
43
+ ? Math.round(((originalPrice - price) / originalPrice) * 100)
44
+ : null
45
+
46
+ const subtitle = productSubtitle(product)
47
+
48
+ return (
49
+ <article
50
+ data-product-card-id={product.id}
51
+ className="beauty-product-card group flex flex-col w-full h-full relative"
52
+ >
53
+ <LocalizedLink
54
+ href={`/products/${product.handle}`}
55
+ className="beauty-product-card__link flex flex-col h-full"
56
+ >
57
+ <div className="beauty-product-card__media relative aspect-square bg-[#faf7f5] overflow-hidden">
58
+ {product.thumbnail?.trim() ? (
59
+ <img
60
+ src={product.thumbnail.trim()}
61
+ alt={product.title || ""}
62
+ className="beauty-product-card__image w-full h-full object-cover"
63
+ />
64
+ ) : (
65
+ <div className="beauty-product-card__placeholder w-full h-full" />
66
+ )}
67
+
68
+ {discountPct != null ? (
69
+ <span className="beauty-product-card__badge beauty-product-card__badge--sale">
70
+ {discountPct}% off
71
+ </span>
72
+ ) : null}
73
+
74
+ {showWishlist && product.id ? (
75
+ <ProductCardActions product={product} countryCode={countryCode} />
76
+ ) : null}
77
+ </div>
78
+
79
+ <div className="beauty-product-card__body">
80
+ <p className="beauty-product-card__eyebrow">{subtitle}</p>
81
+ <h3 className="beauty-product-card__title">{product.title}</h3>
82
+
83
+ <div className="beauty-product-card__footer">
84
+ {rating?.rating != null && rating.rating > 0 ? (
85
+ <p className="beauty-product-card__rating" aria-label={`${rating.rating} out of 5 stars`}>
86
+ <span className="beauty-product-card__rating-stars" aria-hidden="true">
87
+
88
+ </span>
89
+ {rating.rating.toFixed(1)}
90
+ </p>
91
+ ) : (
92
+ <span />
93
+ )}
94
+
95
+ {price != null ? (
96
+ <div className="beauty-product-card__prices">
97
+ <span className="beauty-product-card__price">{formatPrice(price, currency)}</span>
98
+ {originalPrice && price && originalPrice > price ? (
99
+ <span className="beauty-product-card__compare">{formatPrice(originalPrice, currency)}</span>
100
+ ) : null}
101
+ </div>
102
+ ) : null}
103
+ </div>
104
+ </div>
105
+ </LocalizedLink>
106
+ </article>
107
+ )
108
+ }
@@ -0,0 +1,86 @@
1
+ "use client"
2
+
3
+ import LocalizedLink from "@pradip1995/segment-primitives/localized-link"
4
+ import { formatPrice } from "@pradip1995/segment-primitives/format-price"
5
+ import type { HttpTypes } from "@medusajs/types"
6
+ import ProductCardActions from "./product-card-actions"
7
+
8
+ type Props = {
9
+ product: HttpTypes.StoreProduct
10
+ region?: HttpTypes.StoreRegion
11
+ countryCode?: string
12
+ rating?: { rating?: number }
13
+ showWishlist?: boolean
14
+ }
15
+
16
+ export default function DefaultProductCard({
17
+ product,
18
+ region,
19
+ countryCode: countryCodeProp,
20
+ rating,
21
+ showWishlist = true,
22
+ }: Props) {
23
+ const variant = product.variants?.[0]
24
+ const price = variant?.calculated_price?.calculated_amount
25
+ const originalPrice = variant?.calculated_price?.original_amount
26
+ const currency = region?.currency_code || variant?.calculated_price?.currency_code || "inr"
27
+ const countryCode =
28
+ countryCodeProp?.toLowerCase() ??
29
+ region?.countries?.[0]?.iso_2?.toLowerCase() ??
30
+ process.env.NEXT_PUBLIC_DEFAULT_REGION?.toLowerCase() ??
31
+ "in"
32
+
33
+ const discountPct =
34
+ originalPrice && price && originalPrice > price
35
+ ? Math.round(((originalPrice - price) / originalPrice) * 100)
36
+ : null
37
+
38
+ return (
39
+ <article
40
+ data-product-card-id={product.id}
41
+ className="product-card group flex flex-col w-full h-full relative"
42
+ >
43
+ <LocalizedLink href={`/products/${product.handle}`} className="product-card__link flex flex-col h-full">
44
+ <div className="product-card__media relative aspect-[3/4] bg-surface-muted overflow-hidden rounded-lg">
45
+ {product.thumbnail?.trim() ? (
46
+ <img
47
+ src={product.thumbnail.trim()}
48
+ alt={product.title || ""}
49
+ className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
50
+ />
51
+ ) : (
52
+ <div className="w-full h-full bg-surface-muted" />
53
+ )}
54
+
55
+ {showWishlist && product.id ? (
56
+ <ProductCardActions product={product} countryCode={countryCode} />
57
+ ) : null}
58
+ </div>
59
+
60
+ <div className="product-card__info mt-3 flex flex-col gap-1 min-w-0">
61
+ <h3 className="product-card__title text-sm font-medium text-heading group-hover:text-brand-accent transition-colors line-clamp-2">
62
+ {product.title}
63
+ </h3>
64
+
65
+ {rating?.rating != null && rating.rating > 0 ? (
66
+ <p className="product-card__rating text-xs text-muted">★ {rating.rating.toFixed(1)}</p>
67
+ ) : null}
68
+
69
+ {price != null ? (
70
+ <div className="product-card__prices">
71
+ <span className="product-card__price">{formatPrice(price, currency)}</span>
72
+ {originalPrice && price && originalPrice > price ? (
73
+ <>
74
+ <span className="product-card__compare">{formatPrice(originalPrice, currency)}</span>
75
+ {discountPct != null ? (
76
+ <span className="product-card__discount">{discountPct}% OFF</span>
77
+ ) : null}
78
+ </>
79
+ ) : null}
80
+ </div>
81
+ ) : null}
82
+ </div>
83
+ </LocalizedLink>
84
+ </article>
85
+ )
86
+ }
@@ -0,0 +1,225 @@
1
+ /* Beauty product card — Lumière cosmetics */
2
+
3
+ .beauty-product-card {
4
+ background: #ffffff;
5
+ border: 1px solid rgba(139, 58, 98, 0.1);
6
+ box-shadow: 0 10px 28px rgba(42, 16, 32, 0.05);
7
+ transition: transform 0.25s ease, box-shadow 0.25s ease, border-color 0.25s ease;
8
+ }
9
+
10
+ .beauty-product-card:hover {
11
+ transform: translateY(-0.2rem);
12
+ border-color: rgba(139, 58, 98, 0.22);
13
+ box-shadow: 0 16px 36px rgba(42, 16, 32, 0.09);
14
+ }
15
+
16
+ .beauty-product-card__link {
17
+ min-width: 0;
18
+ text-decoration: none;
19
+ color: inherit;
20
+ }
21
+
22
+ .beauty-product-card__media {
23
+ position: relative;
24
+ }
25
+
26
+ .beauty-product-card__image {
27
+ transition: transform 0.45s ease;
28
+ }
29
+
30
+ .beauty-product-card:hover .beauty-product-card__image {
31
+ transform: scale(1.04);
32
+ }
33
+
34
+ .beauty-product-card__placeholder {
35
+ background: linear-gradient(135deg, #faf7f5 0%, #f5ebe3 100%);
36
+ }
37
+
38
+ .beauty-product-card__badge {
39
+ position: absolute;
40
+ top: 0.625rem;
41
+ left: 0.625rem;
42
+ z-index: 12;
43
+ padding: 0.25rem 0.55rem;
44
+ font-size: 0.625rem;
45
+ font-weight: 600;
46
+ letter-spacing: 0.12em;
47
+ text-transform: uppercase;
48
+ }
49
+
50
+ .beauty-product-card__badge--sale {
51
+ background: #000000;
52
+ color: #ffffff;
53
+ }
54
+
55
+ .beauty-product-card .product-card__actions {
56
+ top: 0.5rem;
57
+ right: 0.5rem;
58
+ }
59
+
60
+ .beauty-product-card .product-card__wishlist-btn,
61
+ .beauty-product-card .product-card__cart-btn {
62
+ width: 2.125rem;
63
+ height: 2.125rem;
64
+ border-radius: 999px;
65
+ border: 1px solid rgba(139, 58, 98, 0.14);
66
+ background: rgba(255, 255, 255, 0.95);
67
+ }
68
+
69
+ .beauty-product-card .product-card__wishlist-btn--active {
70
+ background: var(--color-brand-accent-muted);
71
+ }
72
+
73
+ .beauty-product-card .product-card__quick-add-wrap {
74
+ left: 0;
75
+ right: 0;
76
+ bottom: 0;
77
+ top: auto;
78
+ opacity: 1;
79
+ transform: none;
80
+ pointer-events: auto;
81
+ transition: opacity 0.25s ease, transform 0.25s ease;
82
+ }
83
+
84
+ .beauty-product-card .product-card__quick-add {
85
+ width: 100%;
86
+ border-radius: 0;
87
+ background: rgba(0, 0, 0, 0.82);
88
+ color: #ffffff;
89
+ letter-spacing: 0.14em;
90
+ text-transform: uppercase;
91
+ font-size: 0.6875rem;
92
+ font-weight: 600;
93
+ min-height: 2.5rem;
94
+ backdrop-filter: blur(4px);
95
+ }
96
+
97
+ .beauty-product-card .product-card__quick-add:hover:not(:disabled) {
98
+ background: #000000;
99
+ }
100
+
101
+ .beauty-product-card:not(:hover) .product-card__quick-add-wrap {
102
+ opacity: 0;
103
+ transform: translateY(100%);
104
+ pointer-events: none;
105
+ }
106
+
107
+ .beauty-product-card:hover .product-card__quick-add-wrap {
108
+ opacity: 1;
109
+ transform: translateY(0);
110
+ }
111
+
112
+ .beauty-product-card .product-card__cart-btn {
113
+ display: none;
114
+ }
115
+
116
+ .beauty-product-card .product-card__quick-add-label {
117
+ font-size: 0;
118
+ }
119
+
120
+ .beauty-product-card .product-card__quick-add-label::after {
121
+ content: "Add to bag";
122
+ font-size: 0.6875rem;
123
+ }
124
+
125
+ .beauty-product-card__body {
126
+ display: flex;
127
+ flex-direction: column;
128
+ gap: 0.4rem;
129
+ padding: 0.875rem 0.875rem 1rem;
130
+ min-width: 0;
131
+ }
132
+
133
+ .beauty-product-card__eyebrow {
134
+ margin: 0;
135
+ font-size: 0.625rem;
136
+ font-weight: 600;
137
+ letter-spacing: 0.14em;
138
+ text-transform: uppercase;
139
+ color: var(--color-brand-accent);
140
+ }
141
+
142
+ .beauty-product-card__title {
143
+ margin: 0;
144
+ font-family: var(--font-heading);
145
+ font-size: 0.9375rem;
146
+ font-weight: 500;
147
+ line-height: 1.35;
148
+ color: var(--color-text-heading);
149
+ display: -webkit-box;
150
+ -webkit-line-clamp: 2;
151
+ -webkit-box-orient: vertical;
152
+ overflow: hidden;
153
+ min-height: 2.5rem;
154
+ }
155
+
156
+ .beauty-product-card__footer {
157
+ display: flex;
158
+ align-items: flex-end;
159
+ justify-content: space-between;
160
+ gap: 0.75rem;
161
+ margin-top: 0.25rem;
162
+ }
163
+
164
+ .beauty-product-card__rating {
165
+ margin: 0;
166
+ display: inline-flex;
167
+ align-items: center;
168
+ gap: 0.2rem;
169
+ font-size: 0.75rem;
170
+ font-weight: 600;
171
+ color: var(--color-text-heading);
172
+ }
173
+
174
+ .beauty-product-card__rating-stars {
175
+ color: #c9a86c;
176
+ font-size: 0.8125rem;
177
+ }
178
+
179
+ .beauty-product-card__prices {
180
+ display: flex;
181
+ flex-direction: column;
182
+ align-items: flex-end;
183
+ gap: 0.125rem;
184
+ }
185
+
186
+ .beauty-product-card__price {
187
+ font-size: 0.875rem;
188
+ font-weight: 600;
189
+ color: var(--color-text-heading);
190
+ }
191
+
192
+ .beauty-product-card__compare {
193
+ font-size: 0.75rem;
194
+ color: var(--color-text-muted);
195
+ text-decoration: line-through;
196
+ }
197
+
198
+ .product-grid .beauty-product-card,
199
+ .product-rail__grid .beauty-product-card {
200
+ height: 100%;
201
+ }
202
+
203
+ .product-scroll__slide:has(.beauty-product-card) {
204
+ flex: 0 0 clamp(220px, 42vw, 260px);
205
+ }
206
+
207
+ @media (min-width: 768px) {
208
+ .product-scroll__slide:has(.beauty-product-card) {
209
+ flex: 0 0 260px;
210
+ }
211
+ }
212
+
213
+ @media (prefers-reduced-motion: reduce) {
214
+ .beauty-product-card,
215
+ .beauty-product-card__image,
216
+ .beauty-product-card .product-card__quick-add-wrap {
217
+ transition: none;
218
+ }
219
+
220
+ .beauty-product-card:not(:hover) .product-card__quick-add-wrap {
221
+ opacity: 1;
222
+ transform: none;
223
+ pointer-events: auto;
224
+ }
225
+ }
package/src/segment.tsx CHANGED
@@ -1,9 +1,11 @@
1
1
  "use client"
2
2
 
3
- import LocalizedLink from "@pradip1995/segment-primitives/localized-link"
4
- import { formatPrice } from "@pradip1995/segment-primitives/format-price"
3
+ import "./segment.css"
5
4
  import type { HttpTypes } from "@medusajs/types"
6
- import ProductCardActions from "./product-card-actions"
5
+ import BeautyProductCard from "./beauty-product-card"
6
+ import DefaultProductCard from "./default-product-card"
7
+
8
+ export type ProductCardVariant = "default" | "beauty"
7
9
 
8
10
  type Props = {
9
11
  product: HttpTypes.StoreProduct
@@ -11,76 +13,17 @@ type Props = {
11
13
  countryCode?: string
12
14
  rating?: { rating?: number }
13
15
  showWishlist?: boolean
16
+ variant?: ProductCardVariant
14
17
  }
15
18
 
16
- export default function ProductCard({
17
- product,
18
- region,
19
- countryCode: countryCodeProp,
20
- rating,
21
- showWishlist = true,
22
- }: Props) {
23
- const variant = product.variants?.[0]
24
- const price = variant?.calculated_price?.calculated_amount
25
- const originalPrice = variant?.calculated_price?.original_amount
26
- const currency = region?.currency_code || variant?.calculated_price?.currency_code || "inr"
27
- const countryCode =
28
- countryCodeProp?.toLowerCase() ??
29
- region?.countries?.[0]?.iso_2?.toLowerCase() ??
30
- process.env.NEXT_PUBLIC_DEFAULT_REGION?.toLowerCase() ??
31
- "in"
32
-
33
- const discountPct =
34
- originalPrice && price && originalPrice > price
35
- ? Math.round(((originalPrice - price) / originalPrice) * 100)
36
- : null
37
-
38
- return (
39
- <article
40
- data-product-card-id={product.id}
41
- className="product-card group flex flex-col w-full h-full relative"
42
- >
43
- <LocalizedLink href={`/products/${product.handle}`} className="product-card__link flex flex-col h-full">
44
- <div className="product-card__media relative aspect-[3/4] bg-surface-muted overflow-hidden rounded-lg">
45
- {product.thumbnail?.trim() ? (
46
- <img
47
- src={product.thumbnail.trim()}
48
- alt={product.title || ""}
49
- className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
50
- />
51
- ) : (
52
- <div className="w-full h-full bg-surface-muted" />
53
- )}
54
-
55
- {showWishlist && product.id ? (
56
- <ProductCardActions product={product} countryCode={countryCode} />
57
- ) : null}
58
- </div>
59
-
60
- <div className="product-card__info mt-3 flex flex-col gap-1 min-w-0">
61
- <h3 className="product-card__title text-sm font-medium text-heading group-hover:text-brand-accent transition-colors line-clamp-2">
62
- {product.title}
63
- </h3>
64
-
65
- {rating?.rating != null && rating.rating > 0 ? (
66
- <p className="product-card__rating text-xs text-muted">★ {rating.rating.toFixed(1)}</p>
67
- ) : null}
19
+ function resolveProductCardVariant(variant?: ProductCardVariant): ProductCardVariant {
20
+ if (variant === "beauty" || variant === "default") return variant
21
+ const envVariant = process.env.NEXT_PUBLIC_PRODUCT_CARD_VARIANT?.toLowerCase()
22
+ return envVariant === "beauty" ? "beauty" : "default"
23
+ }
68
24
 
69
- {price != null ? (
70
- <div className="product-card__prices">
71
- <span className="product-card__price">{formatPrice(price, currency)}</span>
72
- {originalPrice && price && originalPrice > price ? (
73
- <>
74
- <span className="product-card__compare">{formatPrice(originalPrice, currency)}</span>
75
- {discountPct != null ? (
76
- <span className="product-card__discount">{discountPct}% OFF</span>
77
- ) : null}
78
- </>
79
- ) : null}
80
- </div>
81
- ) : null}
82
- </div>
83
- </LocalizedLink>
84
- </article>
85
- )
25
+ export default function ProductCard({ variant, ...props }: Props) {
26
+ const layout = resolveProductCardVariant(variant)
27
+ if (layout === "beauty") return <BeautyProductCard {...props} />
28
+ return <DefaultProductCard {...props} />
86
29
  }