@pradip1995/segment-powerhouse-carousel 0.1.1

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 ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@pradip1995/segment-powerhouse-carousel",
3
+ "version": "0.1.1",
4
+ "license": "MIT",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "sideEffects": [
9
+ "./src/carousel.css"
10
+ ],
11
+ "files": [
12
+ "src"
13
+ ],
14
+ "exports": {
15
+ ".": "./src/index.ts",
16
+ "./manifest": "./src/manifest.ts"
17
+ },
18
+ "peerDependencies": {
19
+ "@pradip1995/plugin-sdk": "^0.2.0",
20
+ "next": ">=15",
21
+ "react": ">=19",
22
+ "react-dom": ">=19"
23
+ },
24
+ "dependencies": {
25
+ "@pradip1995/commerce-core": "^4.0.0",
26
+ "@pradip1995/medusa-connector": "^0.2.0",
27
+ "@pradip1995/segment-primitives": "^0.4.0"
28
+ },
29
+ "devDependencies": {
30
+ "@medusajs/types": ">=2",
31
+ "@pradip1995/plugin-sdk": "^0.2.0",
32
+ "@types/react": "^19",
33
+ "react": "19.0.3",
34
+ "typescript": "^5.7.2"
35
+ },
36
+ "scripts": {
37
+ "typecheck": "tsc --noEmit",
38
+ "lint": "tsc --noEmit"
39
+ }
40
+ }
@@ -0,0 +1,296 @@
1
+ .powerhouse-carousel {
2
+ --layout-container-max-width: 1400px;
3
+ position: relative;
4
+ z-index: 1;
5
+ max-width: var(--layout-container-max-width);
6
+ margin: 1.25rem auto 0;
7
+ padding: 0 10px;
8
+ text-align: center;
9
+ }
10
+
11
+ .powerhouse-carousel:hover {
12
+ z-index: 2;
13
+ }
14
+
15
+ .powerhouse-carousel__heading {
16
+ margin: 0 0 1.25rem;
17
+ color: #1d1d1d;
18
+ font-family: var(--font-sans, inherit);
19
+ font-size: 1.5rem;
20
+ font-weight: 700;
21
+ letter-spacing: 0.08em;
22
+ line-height: 1.2;
23
+ text-transform: uppercase;
24
+ }
25
+
26
+ .powerhouse-carousel__empty {
27
+ margin: 0;
28
+ padding: 2rem 0;
29
+ color: #6b6b6b;
30
+ font-size: 0.9375rem;
31
+ }
32
+
33
+ .powerhouse-carousel__viewport {
34
+ position: relative;
35
+ }
36
+
37
+ .powerhouse-carousel__track {
38
+ display: flex;
39
+ gap: 10px;
40
+ overflow-x: auto;
41
+ scroll-snap-type: x mandatory;
42
+ scrollbar-width: none;
43
+ -ms-overflow-style: none;
44
+ }
45
+
46
+ .powerhouse-carousel__track::-webkit-scrollbar {
47
+ display: none;
48
+ }
49
+
50
+ .powerhouse-carousel__slide {
51
+ flex: 0 0 204px;
52
+ min-width: 0;
53
+ scroll-snap-align: start;
54
+ text-align: left;
55
+ }
56
+
57
+ .powerhouse-carousel__arrow {
58
+ position: absolute;
59
+ top: 28%;
60
+ z-index: 3;
61
+ display: flex;
62
+ align-items: center;
63
+ justify-content: center;
64
+ width: 40px;
65
+ height: 40px;
66
+ padding: 5px;
67
+ border: 0;
68
+ background: transparent;
69
+ color: #1d1d1d;
70
+ opacity: 0.6;
71
+ cursor: pointer;
72
+ transform: translateY(-50%);
73
+ }
74
+
75
+ .powerhouse-carousel__arrow:hover:not(:disabled) {
76
+ opacity: 0.8;
77
+ }
78
+
79
+ .powerhouse-carousel__arrow:disabled {
80
+ opacity: 0.25;
81
+ cursor: default;
82
+ }
83
+
84
+ .powerhouse-carousel__arrow--prev {
85
+ left: 0.35rem;
86
+ }
87
+
88
+ .powerhouse-carousel__arrow--next {
89
+ right: 0.35rem;
90
+ }
91
+
92
+ .powerhouse-carousel__card {
93
+ display: flex;
94
+ flex-direction: column;
95
+ height: 100%;
96
+ padding: 0.6875rem;
97
+ background: #fff;
98
+ transition: box-shadow 0.25s cubic-bezier(0.4, 0, 0.2, 1);
99
+ }
100
+
101
+ .powerhouse-carousel__card:hover {
102
+ box-shadow: 0 2px 8px #80808033;
103
+ }
104
+
105
+ .powerhouse-carousel__card-media {
106
+ position: relative;
107
+ display: block;
108
+ aspect-ratio: 1;
109
+ overflow: hidden;
110
+ background: #f3f3f3;
111
+ }
112
+
113
+ .powerhouse-carousel__card-image,
114
+ .powerhouse-carousel__card-placeholder {
115
+ width: 100%;
116
+ height: 100%;
117
+ object-fit: cover;
118
+ display: block;
119
+ }
120
+
121
+ .powerhouse-carousel__badge {
122
+ position: absolute;
123
+ top: 0.6rem;
124
+ left: 0.6rem;
125
+ padding: 0.2rem 0.45rem;
126
+ background: #688600;
127
+ color: #fff;
128
+ font-family: var(--font-sans, inherit);
129
+ font-size: 0.75rem;
130
+ font-weight: 600;
131
+ line-height: 1.2;
132
+ }
133
+
134
+ .powerhouse-carousel__card-info {
135
+ display: flex;
136
+ flex-direction: column;
137
+ flex: 1;
138
+ min-width: 0;
139
+ }
140
+
141
+ .powerhouse-carousel__price {
142
+ margin-top: 0.25rem;
143
+ }
144
+
145
+ .powerhouse-carousel__price-compare {
146
+ display: block;
147
+ color: #949494;
148
+ font-size: 0.875rem;
149
+ text-decoration: line-through;
150
+ }
151
+
152
+ .powerhouse-carousel__price-current {
153
+ display: block;
154
+ margin-top: 0.125rem;
155
+ color: #1d1d1d;
156
+ font-size: 1.375rem;
157
+ font-weight: 500;
158
+ line-height: 1.2;
159
+ }
160
+
161
+ .powerhouse-carousel__card.is-sale .powerhouse-carousel__price-current {
162
+ color: #688600;
163
+ }
164
+
165
+ .powerhouse-carousel__card-title {
166
+ margin: 0.625rem 0 0;
167
+ font-family: var(--font-sans, inherit);
168
+ font-size: 1rem;
169
+ font-weight: 300;
170
+ line-height: 1.3125;
171
+ }
172
+
173
+ .powerhouse-carousel__card-title a {
174
+ color: #1d1d1d;
175
+ text-decoration: none;
176
+ }
177
+
178
+ .powerhouse-carousel__stock {
179
+ margin: 0.5rem 0 0;
180
+ color: #688600;
181
+ font-size: 0.875rem;
182
+ }
183
+
184
+ .powerhouse-carousel__actions {
185
+ display: flex;
186
+ flex-wrap: wrap;
187
+ gap: 0.5rem;
188
+ margin-top: auto;
189
+ padding-top: 0.75rem;
190
+ }
191
+
192
+ .powerhouse-carousel__quickshop,
193
+ .powerhouse-carousel__atc {
194
+ display: inline-flex;
195
+ align-items: center;
196
+ justify-content: center;
197
+ min-height: 2.5rem;
198
+ padding: 0.55rem 0.9rem;
199
+ border-radius: 2px;
200
+ font-family: var(--font-sans, inherit);
201
+ font-size: 0.875rem;
202
+ font-weight: 500;
203
+ letter-spacing: 0.04em;
204
+ text-decoration: none;
205
+ cursor: pointer;
206
+ }
207
+
208
+ .powerhouse-carousel__quickshop {
209
+ background: #f1eeae;
210
+ border: 1px solid #f2f682;
211
+ color: #1d1d1d;
212
+ }
213
+
214
+ .powerhouse-carousel__quickshop:hover {
215
+ background: #f4f1be;
216
+ }
217
+
218
+ .powerhouse-carousel__atc {
219
+ background: #2c2b2b;
220
+ border: 1px solid #2c2b2b;
221
+ color: #fff;
222
+ }
223
+
224
+ .powerhouse-carousel__atc:hover {
225
+ background: #414040;
226
+ }
227
+
228
+ .powerhouse-carousel__atc:disabled {
229
+ opacity: 0.65;
230
+ cursor: wait;
231
+ }
232
+
233
+ @media screen and (min-width: 720px) {
234
+ .powerhouse-carousel {
235
+ padding: 0 20px;
236
+ }
237
+ }
238
+
239
+ @media screen and (min-width: 860px) {
240
+ .powerhouse-carousel {
241
+ margin-top: 1.75rem;
242
+ }
243
+
244
+ .powerhouse-carousel__heading {
245
+ margin-bottom: 1.75rem;
246
+ }
247
+
248
+ .powerhouse-carousel__card {
249
+ padding: 1rem;
250
+ }
251
+
252
+ .powerhouse-carousel__arrow--prev {
253
+ left: 2.1875rem;
254
+ }
255
+
256
+ .powerhouse-carousel__arrow--next {
257
+ right: 2.1875rem;
258
+ }
259
+ }
260
+
261
+ @media screen and (min-width: 1080px) {
262
+ .powerhouse-carousel__track {
263
+ gap: 20px;
264
+ }
265
+
266
+ .powerhouse-carousel__slide {
267
+ flex-basis: calc(25% - 15px);
268
+ }
269
+
270
+ .powerhouse-carousel__actions {
271
+ opacity: 0;
272
+ pointer-events: none;
273
+ transition: opacity 0.2s ease;
274
+ }
275
+
276
+ .powerhouse-carousel__card:hover .powerhouse-carousel__actions {
277
+ opacity: 1;
278
+ pointer-events: auto;
279
+ }
280
+ }
281
+
282
+ @media screen and (min-width: 1280px) {
283
+ .powerhouse-carousel__slide {
284
+ flex-basis: calc(20% - 16px);
285
+ }
286
+ }
287
+
288
+ @media screen and (max-width: 859px) {
289
+ .powerhouse-carousel__price-current {
290
+ font-size: 1.2890625rem;
291
+ }
292
+
293
+ .powerhouse-carousel__card-title {
294
+ font-size: 0.9375rem;
295
+ }
296
+ }
@@ -0,0 +1,96 @@
1
+ "use client"
2
+
3
+ import { useCallback, useEffect, useRef, useState } from "react"
4
+ import type { HttpTypes } from "@medusajs/types"
5
+ import ProductCard from "./product-card"
6
+
7
+ function ArrowIcon({ direction }: { direction: "prev" | "next" }) {
8
+ return (
9
+ <svg width="30" height="30" viewBox="0 0 30 30" fill="none" aria-hidden="true">
10
+ {direction === "prev" ? (
11
+ <path d="M18.5 6.5L10.5 15L18.5 23.5" stroke="currentColor" strokeWidth="2" strokeLinecap="square" />
12
+ ) : (
13
+ <path d="M11.5 6.5L19.5 15L11.5 23.5" stroke="currentColor" strokeWidth="2" strokeLinecap="square" />
14
+ )}
15
+ </svg>
16
+ )
17
+ }
18
+
19
+ export default function ProductCarousel({
20
+ products,
21
+ region,
22
+ countryCode,
23
+ }: {
24
+ products: HttpTypes.StoreProduct[]
25
+ region?: HttpTypes.StoreRegion
26
+ countryCode: string
27
+ }) {
28
+ const trackRef = useRef<HTMLDivElement>(null)
29
+ const [canPrev, setCanPrev] = useState(false)
30
+ const [canNext, setCanNext] = useState(false)
31
+
32
+ const updateButtons = useCallback(() => {
33
+ const track = trackRef.current
34
+ if (!track) return
35
+ const max = track.scrollWidth - track.clientWidth
36
+ setCanPrev(track.scrollLeft > 4)
37
+ setCanNext(track.scrollLeft < max - 4)
38
+ }, [])
39
+
40
+ useEffect(() => {
41
+ const track = trackRef.current
42
+ if (!track) return
43
+ updateButtons()
44
+ track.addEventListener("scroll", updateButtons, { passive: true })
45
+ window.addEventListener("resize", updateButtons)
46
+ return () => {
47
+ track.removeEventListener("scroll", updateButtons)
48
+ window.removeEventListener("resize", updateButtons)
49
+ }
50
+ }, [products.length, updateButtons])
51
+
52
+ const scrollByCard = (direction: -1 | 1) => {
53
+ const track = trackRef.current
54
+ const card = track?.querySelector<HTMLElement>(".powerhouse-carousel__slide")
55
+ if (!track || !card) return
56
+ const gap = Number.parseFloat(getComputedStyle(track).columnGap || getComputedStyle(track).gap) || 10
57
+ track.scrollBy({ left: direction * (card.offsetWidth + gap), behavior: "smooth" })
58
+ }
59
+
60
+ const showArrows = products.length > 1 && (canPrev || canNext)
61
+
62
+ return (
63
+ <div className="powerhouse-carousel__viewport">
64
+ {showArrows ? (
65
+ <>
66
+ <button
67
+ type="button"
68
+ className="powerhouse-carousel__arrow powerhouse-carousel__arrow--prev"
69
+ aria-label="Previous products"
70
+ disabled={!canPrev}
71
+ onClick={() => scrollByCard(-1)}
72
+ >
73
+ <ArrowIcon direction="prev" />
74
+ </button>
75
+ <button
76
+ type="button"
77
+ className="powerhouse-carousel__arrow powerhouse-carousel__arrow--next"
78
+ aria-label="Next products"
79
+ disabled={!canNext}
80
+ onClick={() => scrollByCard(1)}
81
+ >
82
+ <ArrowIcon direction="next" />
83
+ </button>
84
+ </>
85
+ ) : null}
86
+
87
+ <div ref={trackRef} className="powerhouse-carousel__track">
88
+ {products.map((product) => (
89
+ <div key={product.id} className="powerhouse-carousel__slide">
90
+ <ProductCard product={product} region={region} countryCode={countryCode} />
91
+ </div>
92
+ ))}
93
+ </div>
94
+ </div>
95
+ )
96
+ }
@@ -0,0 +1,2 @@
1
+ export const DEFAULT_TITLE = ""
2
+ export const DEFAULT_EMPTY = "No products right now. Check back soon!"
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { default } from "./segment"
2
+ export { default as manifest } from "./manifest"
@@ -0,0 +1,10 @@
1
+ import type { SegmentManifest } from "@pradip1995/plugin-sdk"
2
+
3
+ const manifest: SegmentManifest = {
4
+ id: "powerhouse-carousel",
5
+ type: "segment",
6
+ version: "0.1.0",
7
+ compatibleFramework: ["^1.0.0"],
8
+ dataKey: "powerhouseCarousel",
9
+ }
10
+ export default manifest
@@ -0,0 +1,101 @@
1
+ "use client"
2
+
3
+ import { useState } from "react"
4
+ import { useRouter } from "next/navigation"
5
+ import { addToCart } from "@pradip1995/commerce-core/client/actions/cart"
6
+ import LocalizedLink from "@pradip1995/segment-primitives/localized-link"
7
+ import { formatPrice } from "@pradip1995/segment-primitives/format-price"
8
+ import { isNextRedirect } from "@pradip1995/segment-primitives/is-next-redirect"
9
+ import type { HttpTypes } from "@medusajs/types"
10
+
11
+ type Props = {
12
+ product: HttpTypes.StoreProduct
13
+ region?: HttpTypes.StoreRegion
14
+ countryCode: string
15
+ }
16
+
17
+ export default function ProductCard({ product, region, countryCode }: Props) {
18
+ const router = useRouter()
19
+ const [adding, setAdding] = useState(false)
20
+ const variant = product.variants?.[0]
21
+ const price = variant?.calculated_price?.calculated_amount
22
+ const originalPrice = variant?.calculated_price?.original_amount
23
+ const currency = region?.currency_code || variant?.calculated_price?.currency_code || "inr"
24
+ const onSale = originalPrice != null && price != null && originalPrice > price
25
+ const saved = onSale && originalPrice != null && price != null ? originalPrice - price : null
26
+ const inventory = variant?.inventory_quantity
27
+ const href = `/products/${product.handle}`
28
+
29
+ const handleAdd = async (event: React.MouseEvent) => {
30
+ event.preventDefault()
31
+ event.stopPropagation()
32
+ if (!variant?.id || adding) return
33
+ setAdding(true)
34
+ try {
35
+ await addToCart({
36
+ variantId: variant.id,
37
+ quantity: 1,
38
+ countryCode: countryCode.toLowerCase(),
39
+ })
40
+ window.dispatchEvent(new CustomEvent("cart:item-added", { detail: { message: "Added to cart" } }))
41
+ router.refresh()
42
+ } catch (err) {
43
+ if (isNextRedirect(err)) throw err
44
+ const message = err instanceof Error ? err.message : "Could not add to cart"
45
+ window.dispatchEvent(new CustomEvent("cart:item-added", { detail: { message, variant: "error" } }))
46
+ } finally {
47
+ setAdding(false)
48
+ }
49
+ }
50
+
51
+ return (
52
+ <article className={`powerhouse-carousel__card${onSale ? " is-sale" : ""}`}>
53
+ <LocalizedLink href={href} countryCode={countryCode} className="powerhouse-carousel__card-media">
54
+ {product.thumbnail?.trim() ? (
55
+ <img src={product.thumbnail.trim()} alt={product.title || ""} className="powerhouse-carousel__card-image" />
56
+ ) : (
57
+ <div className="powerhouse-carousel__card-placeholder" />
58
+ )}
59
+ {onSale && saved != null ? (
60
+ <span className="powerhouse-carousel__badge">Save {formatPrice(saved, currency)}</span>
61
+ ) : null}
62
+ </LocalizedLink>
63
+
64
+ <div className="powerhouse-carousel__card-info">
65
+ {price != null ? (
66
+ <div className="powerhouse-carousel__price">
67
+ {onSale && originalPrice != null ? (
68
+ <span className="powerhouse-carousel__price-compare">{formatPrice(originalPrice, currency)}</span>
69
+ ) : null}
70
+ <span className="powerhouse-carousel__price-current">{formatPrice(price, currency)}</span>
71
+ </div>
72
+ ) : null}
73
+
74
+ <h3 className="powerhouse-carousel__card-title">
75
+ <LocalizedLink href={href} countryCode={countryCode}>
76
+ {product.title}
77
+ </LocalizedLink>
78
+ </h3>
79
+
80
+ {inventory != null && inventory > 0 && inventory <= 5 ? (
81
+ <p className="powerhouse-carousel__stock">Only {inventory} left!</p>
82
+ ) : null}
83
+
84
+ <div className="powerhouse-carousel__actions">
85
+ <LocalizedLink href={href} countryCode={countryCode} className="powerhouse-carousel__quickshop">
86
+ Quick shop
87
+ </LocalizedLink>
88
+ <button
89
+ type="button"
90
+ className="powerhouse-carousel__atc"
91
+ onClick={handleAdd}
92
+ disabled={adding || !variant?.id}
93
+ aria-label="Add to cart"
94
+ >
95
+ {adding ? "Adding…" : "Add to cart"}
96
+ </button>
97
+ </div>
98
+ </div>
99
+ </article>
100
+ )
101
+ }
@@ -0,0 +1,54 @@
1
+ import { listStoreProductsWithSort } from "@pradip1995/medusa-connector"
2
+ import ProductCarousel from "./carousel"
3
+ import { DEFAULT_EMPTY, DEFAULT_TITLE } from "./defaults"
4
+ import type { PowerhouseCarouselProps } from "./types"
5
+ import "./carousel.css"
6
+
7
+ export default async function PowerhouseCarousel({
8
+ products: fallbackProducts,
9
+ region,
10
+ countryCode: countryCodeProp,
11
+ title = DEFAULT_TITLE,
12
+ emptyMessage = DEFAULT_EMPTY,
13
+ }: PowerhouseCarouselProps) {
14
+ const countryCode =
15
+ countryCodeProp?.toLowerCase() ||
16
+ region?.countries?.[0]?.iso_2?.toLowerCase() ||
17
+ "in"
18
+
19
+ let products = fallbackProducts ?? []
20
+ let displayRegion = region ?? null
21
+
22
+ if (products.length === 0) {
23
+ try {
24
+ const result = await listStoreProductsWithSort({
25
+ page: 1,
26
+ queryParams: { limit: 12 },
27
+ sortBy: "created_at_desc",
28
+ countryCode,
29
+ region,
30
+ })
31
+ if (result.response.products?.length) {
32
+ products = result.response.products
33
+ }
34
+ if (result.region) {
35
+ displayRegion = result.region
36
+ }
37
+ } catch {
38
+ products = fallbackProducts ?? []
39
+ }
40
+ }
41
+
42
+ const list = products.slice(0, 9)
43
+
44
+ return (
45
+ <section className="powerhouse-carousel" aria-label={title || "Featured products"}>
46
+ {title.trim() ? <h2 className="powerhouse-carousel__heading">{title}</h2> : null}
47
+ {list.length === 0 ? (
48
+ <p className="powerhouse-carousel__empty">{emptyMessage}</p>
49
+ ) : (
50
+ <ProductCarousel products={list} region={displayRegion ?? undefined} countryCode={countryCode} />
51
+ )}
52
+ </section>
53
+ )
54
+ }
package/src/types.ts ADDED
@@ -0,0 +1,10 @@
1
+ import type { HttpTypes } from "@medusajs/types"
2
+
3
+ export type PowerhouseCarouselProps = {
4
+ products?: HttpTypes.StoreProduct[]
5
+ region?: HttpTypes.StoreRegion | null
6
+ countryCode?: string
7
+ title?: string
8
+ emptyMessage?: string
9
+ wishlistProductIds?: string[]
10
+ }