@pradip1995/segment-powerhouse-shop 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 +40 -0
- package/src/index.ts +2 -0
- package/src/manifest.ts +10 -0
- package/src/product-card.tsx +132 -0
- package/src/segment.tsx +1 -0
- package/src/shop.css +670 -0
- package/src/shop.tsx +537 -0
- package/src/types.ts +41 -0
package/src/shop.tsx
ADDED
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useEffect, useMemo, useState, type FormEvent } from "react"
|
|
4
|
+
import { useRouter } from "next/navigation"
|
|
5
|
+
import LocalizedLink from "@pradip1995/segment-primitives/localized-link"
|
|
6
|
+
import ShopProductCard from "./product-card"
|
|
7
|
+
import {
|
|
8
|
+
DEFAULT_EMPTY,
|
|
9
|
+
DEFAULT_TITLE,
|
|
10
|
+
PAGE_SIZE,
|
|
11
|
+
SORT_OPTIONS,
|
|
12
|
+
type PowerhouseShopProps,
|
|
13
|
+
} from "./types"
|
|
14
|
+
import "./shop.css"
|
|
15
|
+
|
|
16
|
+
function paramValue(value?: string | string[]): string | undefined {
|
|
17
|
+
if (value === undefined) return undefined
|
|
18
|
+
return Array.isArray(value) ? value[0] : value
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function toStoreHref(props: PowerhouseShopProps, extras: Record<string, string | undefined> = {}) {
|
|
22
|
+
const path = props.basePath || "/store"
|
|
23
|
+
const query = buildParams(props, extras).toString()
|
|
24
|
+
return query ? `${path}?${query}` : path
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function toggleFilter(props: PowerhouseShopProps, key: string, value: string, current?: string) {
|
|
28
|
+
const extras: Record<string, string | undefined> = { page: undefined }
|
|
29
|
+
extras[key] = current === value ? "" : value
|
|
30
|
+
return toStoreHref(props, extras)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function clearFiltersHref(props: PowerhouseShopProps) {
|
|
34
|
+
return toStoreHref(props, {
|
|
35
|
+
q: "",
|
|
36
|
+
category: "",
|
|
37
|
+
collection: "",
|
|
38
|
+
type: "",
|
|
39
|
+
color: "",
|
|
40
|
+
minPrice: "",
|
|
41
|
+
maxPrice: "",
|
|
42
|
+
page: undefined,
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function pageHref(props: PowerhouseShopProps, page: number) {
|
|
47
|
+
return toStoreHref(props, { page: page > 1 ? String(page) : undefined })
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function pick(extra: string | undefined, fallback?: string) {
|
|
51
|
+
if (extra !== undefined) return extra || undefined
|
|
52
|
+
return fallback
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function buildParams(props: PowerhouseShopProps, extras: Record<string, string | undefined> = {}) {
|
|
56
|
+
const params = new URLSearchParams()
|
|
57
|
+
const sortBy = extras.sortBy ?? props.sortBy ?? "created_at_desc"
|
|
58
|
+
if (sortBy) params.set("sortBy", sortBy)
|
|
59
|
+
|
|
60
|
+
const q = pick(extras.q, props.q)
|
|
61
|
+
if (q) params.set("q", q)
|
|
62
|
+
|
|
63
|
+
const category = pick(extras.category, paramValue(props.category))
|
|
64
|
+
if (category) params.set("category", category)
|
|
65
|
+
|
|
66
|
+
const collection = pick(extras.collection, paramValue(props.collection))
|
|
67
|
+
if (collection) params.set("collection", collection)
|
|
68
|
+
|
|
69
|
+
const type = pick(extras.type, paramValue(props.type))
|
|
70
|
+
if (type) params.set("type", type)
|
|
71
|
+
|
|
72
|
+
const color = pick(extras.color, paramValue(props.color))
|
|
73
|
+
if (color) params.set("color", color)
|
|
74
|
+
|
|
75
|
+
const minPrice = pick(extras.minPrice, props.minPrice)
|
|
76
|
+
if (minPrice) params.set("min_price", minPrice)
|
|
77
|
+
|
|
78
|
+
const maxPrice = pick(extras.maxPrice, props.maxPrice)
|
|
79
|
+
if (maxPrice) params.set("max_price", maxPrice)
|
|
80
|
+
|
|
81
|
+
const view = extras.view ?? props.view
|
|
82
|
+
if (view === "list") params.set("view", "list")
|
|
83
|
+
|
|
84
|
+
const page = extras.page
|
|
85
|
+
if (page && page !== "1") params.set("page", page)
|
|
86
|
+
|
|
87
|
+
return params
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function GridIcon() {
|
|
91
|
+
return (
|
|
92
|
+
<svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor" aria-hidden>
|
|
93
|
+
<path
|
|
94
|
+
fillRule="evenodd"
|
|
95
|
+
clipRule="evenodd"
|
|
96
|
+
d="M2 7V2H7V7H2ZM0 1C0 0.447715 0.447715 0 1 0H8C8.55229 0 9 0.447715 9 1V8C9 8.55229 8.55229 9 8 9H1C0.447715 9 0 8.55229 0 8V1ZM13 7V2H18V7H13ZM11 1C11 0.447715 11.4477 0 12 0H19C19.5523 0 20 0.447715 20 1V8C20 8.55229 19.5523 9 19 9H12C11.4477 9 11 8.55229 11 8V1ZM13 13V18H18V13H13ZM12 11C11.4477 11 11 11.4477 11 12V19C11 19.5523 11.4477 20 12 20H19C19.5523 20 20 19.5523 20 19V12C20 11.4477 19.5523 11 19 11H12ZM2 18V13H7V18H2ZM0 12C0 11.4477 0.447715 11 1 11H8C8.55229 11 9 11.4477 9 12V19C9 19.5523 8.55229 20 8 20H1C0.447715 20 0 19.5523 0 19V12Z"
|
|
97
|
+
/>
|
|
98
|
+
</svg>
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function ListIcon() {
|
|
103
|
+
return (
|
|
104
|
+
<svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor" aria-hidden>
|
|
105
|
+
<path
|
|
106
|
+
fillRule="evenodd"
|
|
107
|
+
clipRule="evenodd"
|
|
108
|
+
d="M0.5 2C0.223858 2 0 2.22386 0 2.5V3.5C0 3.77614 0.223858 4 0.5 4H1.5C1.77614 4 2 3.77614 2 3.5V2.5C2 2.22386 1.77614 2 1.5 2H0.5ZM5 2C4.44772 2 4 2.44772 4 3C4 3.55228 4.44772 4 5 4H19C19.5523 4 20 3.55228 20 3C20 2.44772 19.5523 2 19 2H5ZM5 9C4.44772 9 4 9.44772 4 10C4 10.5523 4.44772 11 5 11H19C19.5523 11 20 10.5523 20 10C20 9.44772 19.5523 9 19 9H5ZM4 17C4 16.4477 4.44772 16 5 16H19C19.5523 16 20 16.4477 20 17C20 17.5523 19.5523 18 19 18H5C4.44772 18 4 17.5523 4 17ZM0 9.5C0 9.22386 0.223858 9 0.5 9H1.5C1.77614 9 2 9.22386 2 9.5V10.5C2 10.7761 1.77614 11 1.5 11H0.5C0.223858 11 0 10.7761 0 10.5V9.5ZM0.5 16C0.223858 16 0 16.2239 0 16.5V17.5C0 17.7761 0.223858 18 0.5 18H1.5C1.77614 18 2 17.7761 2 17.5V16.5C2 16.2239 1.77614 16 1.5 16H0.5Z"
|
|
109
|
+
/>
|
|
110
|
+
</svg>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function RemoveIcon() {
|
|
115
|
+
return (
|
|
116
|
+
<svg width="10" height="10" viewBox="0 0 10 10" fill="currentColor" aria-hidden>
|
|
117
|
+
<path d="M6.08785659,5 L9.77469752,1.31315906 L8.68684094,0.225302476 L5,3.91214341 L1.31315906,0.225302476 L0.225302476,1.31315906 L3.91214341,5 L0.225302476,8.68684094 L1.31315906,9.77469752 L5,6.08785659 L8.68684094,9.77469752 L9.77469752,8.68684094 L6.08785659,5 Z" />
|
|
118
|
+
</svg>
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
type AppliedFilter = { key: string; label: string; href: string }
|
|
123
|
+
|
|
124
|
+
function ActiveFilters({
|
|
125
|
+
filters,
|
|
126
|
+
clearHref,
|
|
127
|
+
countryCode,
|
|
128
|
+
}: {
|
|
129
|
+
filters: AppliedFilter[]
|
|
130
|
+
clearHref: string
|
|
131
|
+
countryCode: string
|
|
132
|
+
}) {
|
|
133
|
+
if (!filters.length) return null
|
|
134
|
+
return (
|
|
135
|
+
<ul className="powerhouse-shop__active">
|
|
136
|
+
{filters.map((filter) => (
|
|
137
|
+
<li key={filter.key}>
|
|
138
|
+
<LocalizedLink
|
|
139
|
+
href={filter.href}
|
|
140
|
+
countryCode={countryCode}
|
|
141
|
+
className="powerhouse-shop__active-item"
|
|
142
|
+
aria-label={`Remove ${filter.label} filter`}
|
|
143
|
+
>
|
|
144
|
+
<span>{filter.label}</span>
|
|
145
|
+
<span className="powerhouse-shop__active-x" aria-hidden>
|
|
146
|
+
<RemoveIcon />
|
|
147
|
+
</span>
|
|
148
|
+
</LocalizedLink>
|
|
149
|
+
</li>
|
|
150
|
+
))}
|
|
151
|
+
<li className="powerhouse-shop__active-clear">
|
|
152
|
+
<LocalizedLink href={clearHref} countryCode={countryCode}>
|
|
153
|
+
Clear all
|
|
154
|
+
</LocalizedLink>
|
|
155
|
+
</li>
|
|
156
|
+
</ul>
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const VIEW_STORAGE_KEY = "powerhouse-shop-view"
|
|
161
|
+
|
|
162
|
+
function paginationItems(current: number, total: number) {
|
|
163
|
+
if (total <= 7) return Array.from({ length: total }, (_, index) => index + 1)
|
|
164
|
+
const items: (number | "ellipsis")[] = [1]
|
|
165
|
+
const start = Math.max(2, current - 1)
|
|
166
|
+
const end = Math.min(total - 1, current + 1)
|
|
167
|
+
if (start > 2) items.push("ellipsis")
|
|
168
|
+
for (let page = start; page <= end; page += 1) items.push(page)
|
|
169
|
+
if (end < total - 1) items.push("ellipsis")
|
|
170
|
+
items.push(total)
|
|
171
|
+
return items
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export default function PowerhouseShop(props: PowerhouseShopProps) {
|
|
175
|
+
const router = useRouter()
|
|
176
|
+
const [filtersOpen, setFiltersOpen] = useState(false)
|
|
177
|
+
const [view, setView] = useState<"grid" | "list">(props.view === "list" ? "list" : "grid")
|
|
178
|
+
const countryCode = props.countryCode?.toLowerCase() || "in"
|
|
179
|
+
const title = props.title || DEFAULT_TITLE
|
|
180
|
+
const bannerImage = props.bannerImage?.trim()
|
|
181
|
+
const products = props.products ?? []
|
|
182
|
+
const totalCount = props.productCount ?? products.length
|
|
183
|
+
const currentPage = props.page ? Math.max(1, parseInt(props.page, 10) || 1) : 1
|
|
184
|
+
const totalPages = Math.max(1, Math.ceil(totalCount / PAGE_SIZE))
|
|
185
|
+
const sortBy = props.sortBy || "created_at_desc"
|
|
186
|
+
const activeCategory = paramValue(props.category)
|
|
187
|
+
const activeCollection = paramValue(props.collection)
|
|
188
|
+
const activeType = paramValue(props.type)
|
|
189
|
+
const activeColor = paramValue(props.color)
|
|
190
|
+
const propsWithView = { ...props, view }
|
|
191
|
+
|
|
192
|
+
useEffect(() => {
|
|
193
|
+
const params = new URLSearchParams(window.location.search)
|
|
194
|
+
const fromUrl = params.get("view")
|
|
195
|
+
const fromStorage = window.localStorage.getItem(VIEW_STORAGE_KEY)
|
|
196
|
+
const next = fromUrl === "list" || (!fromUrl && fromStorage === "list") ? "list" : "grid"
|
|
197
|
+
setView(next)
|
|
198
|
+
}, [])
|
|
199
|
+
|
|
200
|
+
const categories = (props.categories ?? []).filter((item) => item.id && (item.name || item.handle))
|
|
201
|
+
const collections = (props.collections ?? []).filter((item) => item.id && (item.title || item.handle))
|
|
202
|
+
const types = props.typeOptions?.filter((item) => item.value && item.label) ?? []
|
|
203
|
+
const colors = props.colorOptions?.filter((item) => item.value && item.label) ?? []
|
|
204
|
+
const selectedCategory = categories.find((item) => item.id === activeCategory || item.handle === activeCategory)
|
|
205
|
+
const selectedCollection = collections.find(
|
|
206
|
+
(item) => item.id === activeCollection || item.handle === activeCollection
|
|
207
|
+
)
|
|
208
|
+
const selectedType = types.find((item) => item.value === activeType)
|
|
209
|
+
const selectedColor = colors.find((item) => item.value === activeColor)
|
|
210
|
+
|
|
211
|
+
const applied: AppliedFilter[] = []
|
|
212
|
+
if (props.q) {
|
|
213
|
+
applied.push({
|
|
214
|
+
key: "q",
|
|
215
|
+
label: `“${props.q}”`,
|
|
216
|
+
href: toStoreHref(propsWithView, { q: "", page: undefined }),
|
|
217
|
+
})
|
|
218
|
+
}
|
|
219
|
+
if (activeCategory) {
|
|
220
|
+
applied.push({
|
|
221
|
+
key: "category",
|
|
222
|
+
label: selectedCategory?.name || selectedCategory?.handle || activeCategory,
|
|
223
|
+
href: toStoreHref(propsWithView, { category: "", page: undefined }),
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
if (activeCollection) {
|
|
227
|
+
applied.push({
|
|
228
|
+
key: "collection",
|
|
229
|
+
label: selectedCollection?.title || selectedCollection?.handle || activeCollection,
|
|
230
|
+
href: toStoreHref(propsWithView, { collection: "", page: undefined }),
|
|
231
|
+
})
|
|
232
|
+
}
|
|
233
|
+
if (activeType) {
|
|
234
|
+
applied.push({
|
|
235
|
+
key: "type",
|
|
236
|
+
label: selectedType?.label || activeType,
|
|
237
|
+
href: toStoreHref(propsWithView, { type: "", page: undefined }),
|
|
238
|
+
})
|
|
239
|
+
}
|
|
240
|
+
if (activeColor) {
|
|
241
|
+
applied.push({
|
|
242
|
+
key: "color",
|
|
243
|
+
label: selectedColor?.label || activeColor,
|
|
244
|
+
href: toStoreHref(propsWithView, { color: "", page: undefined }),
|
|
245
|
+
})
|
|
246
|
+
}
|
|
247
|
+
if (props.minPrice || props.maxPrice) {
|
|
248
|
+
const min = props.minPrice
|
|
249
|
+
const max = props.maxPrice
|
|
250
|
+
applied.push({
|
|
251
|
+
key: "price",
|
|
252
|
+
label: min && max ? `${min} – ${max}` : min ? `From ${min}` : `To ${max}`,
|
|
253
|
+
href: toStoreHref(propsWithView, { minPrice: "", maxPrice: "", page: undefined }),
|
|
254
|
+
})
|
|
255
|
+
}
|
|
256
|
+
const clearHref = clearFiltersHref(propsWithView)
|
|
257
|
+
|
|
258
|
+
const sortHref = useMemo(
|
|
259
|
+
() => (value: string) => toStoreHref(propsWithView, { sortBy: value, page: undefined }),
|
|
260
|
+
[propsWithView]
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
const handlePrice = (event: FormEvent<HTMLFormElement>) => {
|
|
264
|
+
event.preventDefault()
|
|
265
|
+
const data = new FormData(event.currentTarget)
|
|
266
|
+
const minPrice = String(data.get("min_price") || "").trim()
|
|
267
|
+
const maxPrice = String(data.get("max_price") || "").trim()
|
|
268
|
+
router.push(toStoreHref(propsWithView, { minPrice, maxPrice, page: undefined }))
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const handleSort = (value: string) => {
|
|
272
|
+
router.push(sortHref(value))
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const handleView = (next: "grid" | "list") => {
|
|
276
|
+
setView(next)
|
|
277
|
+
window.localStorage.setItem(VIEW_STORAGE_KEY, next)
|
|
278
|
+
router.replace(toStoreHref({ ...props, view: next }, { page: props.page }))
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return (
|
|
282
|
+
<section className="powerhouse-shop" data-testid="store-container">
|
|
283
|
+
<nav className="powerhouse-shop__crumbs" aria-label="Breadcrumbs">
|
|
284
|
+
<LocalizedLink href="/" countryCode={countryCode}>
|
|
285
|
+
Home
|
|
286
|
+
</LocalizedLink>
|
|
287
|
+
<span className="powerhouse-shop__crumb-sep" aria-hidden>
|
|
288
|
+
<svg width="8" height="5" viewBox="0 0 8 5">
|
|
289
|
+
<path fill="currentColor" fillRule="evenodd" d="M1.002.27L.29.982l3.712 3.712L7.714.982 7.002.27l-3 3z" />
|
|
290
|
+
</svg>
|
|
291
|
+
</span>
|
|
292
|
+
<span>{title}</span>
|
|
293
|
+
</nav>
|
|
294
|
+
|
|
295
|
+
{bannerImage ? (
|
|
296
|
+
<div className="powerhouse-shop__banner">
|
|
297
|
+
<img src={bannerImage} alt="" className="powerhouse-shop__banner-image" />
|
|
298
|
+
<div className="powerhouse-shop__banner-overlay" aria-hidden />
|
|
299
|
+
<div className="powerhouse-shop__banner-content">
|
|
300
|
+
<h1>{title}</h1>
|
|
301
|
+
{props.bannerText ? <p>{props.bannerText}</p> : null}
|
|
302
|
+
</div>
|
|
303
|
+
</div>
|
|
304
|
+
) : null}
|
|
305
|
+
|
|
306
|
+
<div className={`powerhouse-shop__layout${filtersOpen ? " is-filters-open" : ""}`}>
|
|
307
|
+
<aside className="powerhouse-shop__sidebar" aria-label="Filters">
|
|
308
|
+
<div className="powerhouse-shop__sidebar-head">
|
|
309
|
+
<h2>Filters</h2>
|
|
310
|
+
<button type="button" className="powerhouse-shop__close" onClick={() => setFiltersOpen(false)}>
|
|
311
|
+
Close
|
|
312
|
+
</button>
|
|
313
|
+
</div>
|
|
314
|
+
|
|
315
|
+
<ActiveFilters filters={applied} clearHref={clearHref} countryCode={countryCode} />
|
|
316
|
+
|
|
317
|
+
{categories.length ? (
|
|
318
|
+
<div className="powerhouse-shop__filter">
|
|
319
|
+
<h3>Product type</h3>
|
|
320
|
+
<ul>
|
|
321
|
+
{categories.map((item) => {
|
|
322
|
+
const value = item.id
|
|
323
|
+
const selected = activeCategory === value || activeCategory === item.handle
|
|
324
|
+
return (
|
|
325
|
+
<li key={value}>
|
|
326
|
+
<LocalizedLink
|
|
327
|
+
href={toggleFilter(propsWithView, "category", value, selected ? value : undefined)}
|
|
328
|
+
countryCode={countryCode}
|
|
329
|
+
className={selected ? "is-active" : undefined}
|
|
330
|
+
>
|
|
331
|
+
<span className="powerhouse-shop__check" aria-hidden />
|
|
332
|
+
{item.name || item.handle}
|
|
333
|
+
</LocalizedLink>
|
|
334
|
+
</li>
|
|
335
|
+
)
|
|
336
|
+
})}
|
|
337
|
+
</ul>
|
|
338
|
+
</div>
|
|
339
|
+
) : null}
|
|
340
|
+
|
|
341
|
+
{collections.length ? (
|
|
342
|
+
<div className="powerhouse-shop__filter">
|
|
343
|
+
<h3>Collection</h3>
|
|
344
|
+
<ul>
|
|
345
|
+
{collections.map((item) => {
|
|
346
|
+
const value = item.id
|
|
347
|
+
const selected = activeCollection === value || activeCollection === item.handle
|
|
348
|
+
return (
|
|
349
|
+
<li key={value}>
|
|
350
|
+
<LocalizedLink
|
|
351
|
+
href={toggleFilter(propsWithView, "collection", value, selected ? value : undefined)}
|
|
352
|
+
countryCode={countryCode}
|
|
353
|
+
className={selected ? "is-active" : undefined}
|
|
354
|
+
>
|
|
355
|
+
<span className="powerhouse-shop__check" aria-hidden />
|
|
356
|
+
{item.title || item.handle}
|
|
357
|
+
</LocalizedLink>
|
|
358
|
+
</li>
|
|
359
|
+
)
|
|
360
|
+
})}
|
|
361
|
+
</ul>
|
|
362
|
+
</div>
|
|
363
|
+
) : null}
|
|
364
|
+
|
|
365
|
+
{types.length ? (
|
|
366
|
+
<div className="powerhouse-shop__filter">
|
|
367
|
+
<h3>Type</h3>
|
|
368
|
+
<ul>
|
|
369
|
+
{types.map((item) => {
|
|
370
|
+
const selected = activeType === item.value
|
|
371
|
+
return (
|
|
372
|
+
<li key={item.value}>
|
|
373
|
+
<LocalizedLink
|
|
374
|
+
href={toggleFilter(propsWithView, "type", item.value, selected ? item.value : undefined)}
|
|
375
|
+
countryCode={countryCode}
|
|
376
|
+
className={selected ? "is-active" : undefined}
|
|
377
|
+
>
|
|
378
|
+
<span className="powerhouse-shop__check" aria-hidden />
|
|
379
|
+
{item.label}
|
|
380
|
+
</LocalizedLink>
|
|
381
|
+
</li>
|
|
382
|
+
)
|
|
383
|
+
})}
|
|
384
|
+
</ul>
|
|
385
|
+
</div>
|
|
386
|
+
) : null}
|
|
387
|
+
|
|
388
|
+
{colors.length ? (
|
|
389
|
+
<div className="powerhouse-shop__filter">
|
|
390
|
+
<h3>Colors</h3>
|
|
391
|
+
<ul>
|
|
392
|
+
{colors.map((item) => {
|
|
393
|
+
const selected = activeColor === item.value
|
|
394
|
+
return (
|
|
395
|
+
<li key={item.value}>
|
|
396
|
+
<LocalizedLink
|
|
397
|
+
href={toggleFilter(propsWithView, "color", item.value, selected ? item.value : undefined)}
|
|
398
|
+
countryCode={countryCode}
|
|
399
|
+
className={selected ? "is-active" : undefined}
|
|
400
|
+
>
|
|
401
|
+
<span className="powerhouse-shop__check" aria-hidden />
|
|
402
|
+
{item.label}
|
|
403
|
+
</LocalizedLink>
|
|
404
|
+
</li>
|
|
405
|
+
)
|
|
406
|
+
})}
|
|
407
|
+
</ul>
|
|
408
|
+
</div>
|
|
409
|
+
) : null}
|
|
410
|
+
|
|
411
|
+
<div className="powerhouse-shop__filter">
|
|
412
|
+
<h3>Price</h3>
|
|
413
|
+
<form className="powerhouse-shop__price-form" onSubmit={handlePrice}>
|
|
414
|
+
<label>
|
|
415
|
+
<span>From</span>
|
|
416
|
+
<input name="min_price" type="number" min={0} defaultValue={props.minPrice || ""} inputMode="decimal" />
|
|
417
|
+
</label>
|
|
418
|
+
<label>
|
|
419
|
+
<span>To</span>
|
|
420
|
+
<input name="max_price" type="number" min={0} defaultValue={props.maxPrice || ""} inputMode="decimal" />
|
|
421
|
+
</label>
|
|
422
|
+
<button type="submit">Apply</button>
|
|
423
|
+
</form>
|
|
424
|
+
</div>
|
|
425
|
+
</aside>
|
|
426
|
+
|
|
427
|
+
{filtersOpen ? (
|
|
428
|
+
<button
|
|
429
|
+
type="button"
|
|
430
|
+
className="powerhouse-shop__backdrop"
|
|
431
|
+
aria-label="Close filters"
|
|
432
|
+
onClick={() => setFiltersOpen(false)}
|
|
433
|
+
/>
|
|
434
|
+
) : null}
|
|
435
|
+
|
|
436
|
+
<div className="powerhouse-shop__main">
|
|
437
|
+
{bannerImage ? (
|
|
438
|
+
props.q ? (
|
|
439
|
+
<p className="powerhouse-shop__search">Results for “{props.q}”</p>
|
|
440
|
+
) : null
|
|
441
|
+
) : (
|
|
442
|
+
<div className="powerhouse-shop__masthead">
|
|
443
|
+
<h1>{title}</h1>
|
|
444
|
+
{props.q ? <p className="powerhouse-shop__search">Results for “{props.q}”</p> : null}
|
|
445
|
+
</div>
|
|
446
|
+
)}
|
|
447
|
+
|
|
448
|
+
<div className="powerhouse-shop__utils">
|
|
449
|
+
<button type="button" className="powerhouse-shop__filters-btn" onClick={() => setFiltersOpen(true)}>
|
|
450
|
+
{applied.length ? `Filters (${applied.length})` : "Filters"}
|
|
451
|
+
</button>
|
|
452
|
+
<div className="powerhouse-shop__utils-right">
|
|
453
|
+
<label className="powerhouse-shop__sort">
|
|
454
|
+
<span className="powerhouse-shop__utils-label">Sort</span>
|
|
455
|
+
<select value={sortBy} onChange={(event) => handleSort(event.target.value)} aria-label="Sort products">
|
|
456
|
+
{SORT_OPTIONS.map((option) => (
|
|
457
|
+
<option key={option.value} value={option.value}>
|
|
458
|
+
{option.label}
|
|
459
|
+
</option>
|
|
460
|
+
))}
|
|
461
|
+
</select>
|
|
462
|
+
</label>
|
|
463
|
+
<div className="powerhouse-shop__view" role="group" aria-label="View as">
|
|
464
|
+
<span className="powerhouse-shop__utils-label">View as</span>
|
|
465
|
+
<button
|
|
466
|
+
type="button"
|
|
467
|
+
className={view === "grid" ? "is-active" : undefined}
|
|
468
|
+
aria-label="Grid view"
|
|
469
|
+
aria-pressed={view === "grid"}
|
|
470
|
+
onClick={() => handleView("grid")}
|
|
471
|
+
>
|
|
472
|
+
<GridIcon />
|
|
473
|
+
</button>
|
|
474
|
+
<button
|
|
475
|
+
type="button"
|
|
476
|
+
className={view === "list" ? "is-active" : undefined}
|
|
477
|
+
aria-label="List view"
|
|
478
|
+
aria-pressed={view === "list"}
|
|
479
|
+
onClick={() => handleView("list")}
|
|
480
|
+
>
|
|
481
|
+
<ListIcon />
|
|
482
|
+
</button>
|
|
483
|
+
</div>
|
|
484
|
+
</div>
|
|
485
|
+
</div>
|
|
486
|
+
|
|
487
|
+
<ActiveFilters filters={applied} clearHref={clearHref} countryCode={countryCode} />
|
|
488
|
+
|
|
489
|
+
{products.length === 0 ? (
|
|
490
|
+
<p className="powerhouse-shop__empty">{props.emptyMessage || DEFAULT_EMPTY}</p>
|
|
491
|
+
) : (
|
|
492
|
+
<div className={`powerhouse-shop__grid${view === "list" ? " is-list" : ""}`}>
|
|
493
|
+
{products.map((product, index) => (
|
|
494
|
+
<ShopProductCard
|
|
495
|
+
key={product.id ?? product.handle ?? `product-${index}`}
|
|
496
|
+
product={product}
|
|
497
|
+
countryCode={countryCode}
|
|
498
|
+
/>
|
|
499
|
+
))}
|
|
500
|
+
</div>
|
|
501
|
+
)}
|
|
502
|
+
|
|
503
|
+
{totalPages > 1 ? (
|
|
504
|
+
<nav className="powerhouse-shop__pages" aria-label="Pagination">
|
|
505
|
+
<ul>
|
|
506
|
+
{paginationItems(currentPage, totalPages).map((item, index) =>
|
|
507
|
+
item === "ellipsis" ? (
|
|
508
|
+
<li key={`ellipsis-${index}`} className="is-ellipsis">
|
|
509
|
+
…
|
|
510
|
+
</li>
|
|
511
|
+
) : (
|
|
512
|
+
<li key={item} className={item === currentPage ? "is-active" : undefined}>
|
|
513
|
+
{item === currentPage ? (
|
|
514
|
+
<span aria-current="page">{item}</span>
|
|
515
|
+
) : (
|
|
516
|
+
<LocalizedLink href={pageHref(propsWithView, item)} countryCode={countryCode}>
|
|
517
|
+
{item}
|
|
518
|
+
</LocalizedLink>
|
|
519
|
+
)}
|
|
520
|
+
</li>
|
|
521
|
+
)
|
|
522
|
+
)}
|
|
523
|
+
{currentPage < totalPages ? (
|
|
524
|
+
<li className="is-next">
|
|
525
|
+
<LocalizedLink href={pageHref(propsWithView, currentPage + 1)} countryCode={countryCode}>
|
|
526
|
+
Next
|
|
527
|
+
</LocalizedLink>
|
|
528
|
+
</li>
|
|
529
|
+
) : null}
|
|
530
|
+
</ul>
|
|
531
|
+
</nav>
|
|
532
|
+
) : null}
|
|
533
|
+
</div>
|
|
534
|
+
</div>
|
|
535
|
+
</section>
|
|
536
|
+
)
|
|
537
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { HttpTypes } from "@medusajs/types"
|
|
2
|
+
|
|
3
|
+
export type FilterOption = { value: string; label: string }
|
|
4
|
+
|
|
5
|
+
export type PowerhouseShopProps = {
|
|
6
|
+
countryCode?: string
|
|
7
|
+
products?: HttpTypes.StoreProduct[]
|
|
8
|
+
productCount?: number
|
|
9
|
+
page?: string
|
|
10
|
+
sortBy?: string
|
|
11
|
+
q?: string
|
|
12
|
+
category?: string | string[]
|
|
13
|
+
collection?: string | string[]
|
|
14
|
+
categories?: { id: string; name?: string; handle?: string | null }[]
|
|
15
|
+
collections?: { id: string; title?: string; handle?: string | null }[]
|
|
16
|
+
type?: string | string[]
|
|
17
|
+
color?: string | string[]
|
|
18
|
+
typeOptions?: FilterOption[]
|
|
19
|
+
colorOptions?: FilterOption[]
|
|
20
|
+
minPrice?: string
|
|
21
|
+
maxPrice?: string
|
|
22
|
+
title?: string
|
|
23
|
+
emptyMessage?: string
|
|
24
|
+
view?: string
|
|
25
|
+
basePath?: string
|
|
26
|
+
bannerImage?: string
|
|
27
|
+
bannerText?: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const PAGE_SIZE = 12
|
|
31
|
+
|
|
32
|
+
export const DEFAULT_TITLE = "Products"
|
|
33
|
+
export const DEFAULT_EMPTY = "No products match your filters."
|
|
34
|
+
|
|
35
|
+
export const SORT_OPTIONS = [
|
|
36
|
+
{ value: "created_at_desc", label: "Featured" },
|
|
37
|
+
{ value: "bestsellers", label: "Best selling" },
|
|
38
|
+
{ value: "price_asc", label: "Price, low to high" },
|
|
39
|
+
{ value: "price_desc", label: "Price, high to low" },
|
|
40
|
+
{ value: "created_at_asc", label: "Date, old to new" },
|
|
41
|
+
]
|