@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/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
+ ]