@pradip1995/segment-powerhouse-shop 0.1.1 → 0.1.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 +1 -1
- package/src/add-to-cart.ts +24 -0
- package/src/product-card.tsx +80 -44
- package/src/shop.css +387 -78
- package/src/shop.tsx +270 -145
package/src/shop.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
-
import { useEffect, useMemo, useState, type FormEvent } from "react"
|
|
3
|
+
import { useEffect, useMemo, useState, type FormEvent, type ReactNode } from "react"
|
|
4
4
|
import { useRouter } from "next/navigation"
|
|
5
5
|
import LocalizedLink from "@pradip1995/segment-primitives/localized-link"
|
|
6
6
|
import ShopProductCard from "./product-card"
|
|
@@ -87,6 +87,23 @@ function buildParams(props: PowerhouseShopProps, extras: Record<string, string |
|
|
|
87
87
|
return params
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
function FilterIcon() {
|
|
91
|
+
return (
|
|
92
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden>
|
|
93
|
+
<path d="M4 6h16M7 12h10M10 18h4" stroke="currentColor" strokeWidth="1.75" strokeLinecap="square" />
|
|
94
|
+
</svg>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function SortCaret() {
|
|
99
|
+
return (
|
|
100
|
+
<svg className="powerhouse-shop__sort-caret" width="10" height="6" viewBox="0 0 8 6" fill="none" aria-hidden>
|
|
101
|
+
<path d="M4 4.5L7 1.5" stroke="currentColor" strokeWidth="1.25" strokeLinecap="square" />
|
|
102
|
+
<path d="M4 4.5L1 1.5" stroke="currentColor" strokeWidth="1.25" strokeLinecap="square" />
|
|
103
|
+
</svg>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
90
107
|
function GridIcon() {
|
|
91
108
|
return (
|
|
92
109
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor" aria-hidden>
|
|
@@ -157,6 +174,71 @@ function ActiveFilters({
|
|
|
157
174
|
)
|
|
158
175
|
}
|
|
159
176
|
|
|
177
|
+
function colorSwatch(value?: string, label?: string) {
|
|
178
|
+
const raw = (value || label || "").trim()
|
|
179
|
+
if (/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(raw)) return raw
|
|
180
|
+
if (/^(rgb|hsl)a?\(/i.test(raw)) return raw
|
|
181
|
+
const named = raw.toLowerCase().replace(/[\s_-]+/g, "")
|
|
182
|
+
const map: Record<string, string> = {
|
|
183
|
+
black: "#111111",
|
|
184
|
+
white: "#ffffff",
|
|
185
|
+
red: "#c62828",
|
|
186
|
+
blue: "#1565c0",
|
|
187
|
+
green: "#2e7d32",
|
|
188
|
+
yellow: "#f1c40f",
|
|
189
|
+
pink: "#e91e63",
|
|
190
|
+
purple: "#6a1b9a",
|
|
191
|
+
orange: "#ef6c00",
|
|
192
|
+
brown: "#6d4c41",
|
|
193
|
+
grey: "#9e9e9e",
|
|
194
|
+
gray: "#9e9e9e",
|
|
195
|
+
gold: "#c9a86c",
|
|
196
|
+
beige: "#d7ccc8",
|
|
197
|
+
navy: "#1a237e",
|
|
198
|
+
maroon: "#7b1e3a",
|
|
199
|
+
}
|
|
200
|
+
return map[named] || null
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function CloseIcon() {
|
|
204
|
+
return (
|
|
205
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden>
|
|
206
|
+
<path d="M5 5L19 19M19 5L5 19" stroke="currentColor" strokeWidth="1.75" strokeLinecap="square" />
|
|
207
|
+
</svg>
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function FilterSection({
|
|
212
|
+
id,
|
|
213
|
+
title,
|
|
214
|
+
children,
|
|
215
|
+
}: {
|
|
216
|
+
id: string
|
|
217
|
+
title: string
|
|
218
|
+
children: ReactNode
|
|
219
|
+
}) {
|
|
220
|
+
const [open, setOpen] = useState(true)
|
|
221
|
+
return (
|
|
222
|
+
<div className={`powerhouse-shop__filter${open ? " is-open" : ""}`}>
|
|
223
|
+
<button
|
|
224
|
+
type="button"
|
|
225
|
+
className="powerhouse-shop__filter-toggle"
|
|
226
|
+
aria-expanded={open}
|
|
227
|
+
aria-controls={`filter-${id}`}
|
|
228
|
+
onClick={() => setOpen((value) => !value)}
|
|
229
|
+
>
|
|
230
|
+
<span>{title}</span>
|
|
231
|
+
<span className="powerhouse-shop__filter-icon" aria-hidden>
|
|
232
|
+
{open ? "−" : "+"}
|
|
233
|
+
</span>
|
|
234
|
+
</button>
|
|
235
|
+
<div id={`filter-${id}`} className="powerhouse-shop__filter-body" hidden={!open}>
|
|
236
|
+
{children}
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
|
|
160
242
|
const VIEW_STORAGE_KEY = "powerhouse-shop-view"
|
|
161
243
|
|
|
162
244
|
function paginationItems(current: number, total: number) {
|
|
@@ -197,6 +279,19 @@ export default function PowerhouseShop(props: PowerhouseShopProps) {
|
|
|
197
279
|
setView(next)
|
|
198
280
|
}, [])
|
|
199
281
|
|
|
282
|
+
useEffect(() => {
|
|
283
|
+
document.documentElement.classList.toggle("powerhouse-filters-open", filtersOpen)
|
|
284
|
+
if (!filtersOpen) return undefined
|
|
285
|
+
const onKey = (event: KeyboardEvent) => {
|
|
286
|
+
if (event.key === "Escape") setFiltersOpen(false)
|
|
287
|
+
}
|
|
288
|
+
window.addEventListener("keydown", onKey)
|
|
289
|
+
return () => {
|
|
290
|
+
window.removeEventListener("keydown", onKey)
|
|
291
|
+
document.documentElement.classList.remove("powerhouse-filters-open")
|
|
292
|
+
}
|
|
293
|
+
}, [filtersOpen])
|
|
294
|
+
|
|
200
295
|
const categories = (props.categories ?? []).filter((item) => item.id && (item.name || item.handle))
|
|
201
296
|
const collections = (props.collections ?? []).filter((item) => item.id && (item.title || item.handle))
|
|
202
297
|
const types = props.typeOptions?.filter((item) => item.value && item.label) ?? []
|
|
@@ -307,120 +402,142 @@ export default function PowerhouseShop(props: PowerhouseShopProps) {
|
|
|
307
402
|
<aside className="powerhouse-shop__sidebar" aria-label="Filters">
|
|
308
403
|
<div className="powerhouse-shop__sidebar-head">
|
|
309
404
|
<h2>Filters</h2>
|
|
310
|
-
<button
|
|
311
|
-
|
|
405
|
+
<button
|
|
406
|
+
type="button"
|
|
407
|
+
className="powerhouse-shop__close"
|
|
408
|
+
aria-label="Close filters"
|
|
409
|
+
onClick={() => setFiltersOpen(false)}
|
|
410
|
+
>
|
|
411
|
+
<CloseIcon />
|
|
312
412
|
</button>
|
|
313
413
|
</div>
|
|
314
414
|
|
|
315
|
-
<
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
<
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
</
|
|
358
|
-
|
|
359
|
-
)
|
|
360
|
-
|
|
361
|
-
</
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
<
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
415
|
+
<div className="powerhouse-shop__sidebar-body">
|
|
416
|
+
<ActiveFilters filters={applied} clearHref={clearHref} countryCode={countryCode} />
|
|
417
|
+
|
|
418
|
+
{categories.length ? (
|
|
419
|
+
<FilterSection id="product-type" title="Product type">
|
|
420
|
+
<ul>
|
|
421
|
+
{categories.map((item) => {
|
|
422
|
+
const value = item.id
|
|
423
|
+
const selected = activeCategory === value || activeCategory === item.handle
|
|
424
|
+
return (
|
|
425
|
+
<li key={value}>
|
|
426
|
+
<LocalizedLink
|
|
427
|
+
href={toggleFilter(propsWithView, "category", value, selected ? value : undefined)}
|
|
428
|
+
countryCode={countryCode}
|
|
429
|
+
className={selected ? "is-active" : undefined}
|
|
430
|
+
>
|
|
431
|
+
<span className="powerhouse-shop__check" aria-hidden />
|
|
432
|
+
{item.name || item.handle}
|
|
433
|
+
</LocalizedLink>
|
|
434
|
+
</li>
|
|
435
|
+
)
|
|
436
|
+
})}
|
|
437
|
+
</ul>
|
|
438
|
+
</FilterSection>
|
|
439
|
+
) : null}
|
|
440
|
+
|
|
441
|
+
{collections.length ? (
|
|
442
|
+
<FilterSection id="collection" title="Collection">
|
|
443
|
+
<ul>
|
|
444
|
+
{collections.map((item) => {
|
|
445
|
+
const value = item.id
|
|
446
|
+
const selected = activeCollection === value || activeCollection === item.handle
|
|
447
|
+
return (
|
|
448
|
+
<li key={value}>
|
|
449
|
+
<LocalizedLink
|
|
450
|
+
href={toggleFilter(propsWithView, "collection", value, selected ? value : undefined)}
|
|
451
|
+
countryCode={countryCode}
|
|
452
|
+
className={selected ? "is-active" : undefined}
|
|
453
|
+
>
|
|
454
|
+
<span className="powerhouse-shop__check" aria-hidden />
|
|
455
|
+
{item.title || item.handle}
|
|
456
|
+
</LocalizedLink>
|
|
457
|
+
</li>
|
|
458
|
+
)
|
|
459
|
+
})}
|
|
460
|
+
</ul>
|
|
461
|
+
</FilterSection>
|
|
462
|
+
) : null}
|
|
463
|
+
|
|
464
|
+
{types.length ? (
|
|
465
|
+
<FilterSection id="type" title="Type">
|
|
466
|
+
<ul>
|
|
467
|
+
{types.map((item) => {
|
|
468
|
+
const selected = activeType === item.value
|
|
469
|
+
return (
|
|
470
|
+
<li key={item.value}>
|
|
471
|
+
<LocalizedLink
|
|
472
|
+
href={toggleFilter(propsWithView, "type", item.value, selected ? item.value : undefined)}
|
|
473
|
+
countryCode={countryCode}
|
|
474
|
+
className={selected ? "is-active" : undefined}
|
|
475
|
+
>
|
|
476
|
+
<span className="powerhouse-shop__check" aria-hidden />
|
|
477
|
+
{item.label}
|
|
478
|
+
</LocalizedLink>
|
|
479
|
+
</li>
|
|
480
|
+
)
|
|
481
|
+
})}
|
|
482
|
+
</ul>
|
|
483
|
+
</FilterSection>
|
|
484
|
+
) : null}
|
|
485
|
+
|
|
486
|
+
{colors.length ? (
|
|
487
|
+
<FilterSection id="colors" title="Colors">
|
|
488
|
+
<ul className="powerhouse-shop__swatches">
|
|
489
|
+
{colors.map((item) => {
|
|
490
|
+
const selected = activeColor === item.value
|
|
491
|
+
const swatch = colorSwatch(item.value, item.label)
|
|
492
|
+
return (
|
|
493
|
+
<li key={item.value}>
|
|
494
|
+
<LocalizedLink
|
|
495
|
+
href={toggleFilter(propsWithView, "color", item.value, selected ? item.value : undefined)}
|
|
496
|
+
countryCode={countryCode}
|
|
497
|
+
className={selected ? "is-active" : undefined}
|
|
498
|
+
aria-label={item.label}
|
|
499
|
+
>
|
|
500
|
+
{swatch ? (
|
|
501
|
+
<span className="powerhouse-shop__swatch" style={{ background: swatch }} aria-hidden />
|
|
502
|
+
) : (
|
|
503
|
+
<span className="powerhouse-shop__check" aria-hidden />
|
|
504
|
+
)}
|
|
505
|
+
{swatch ? <span className="sr-only">{item.label}</span> : item.label}
|
|
506
|
+
</LocalizedLink>
|
|
507
|
+
</li>
|
|
508
|
+
)
|
|
509
|
+
})}
|
|
510
|
+
</ul>
|
|
511
|
+
</FilterSection>
|
|
512
|
+
) : null}
|
|
513
|
+
|
|
514
|
+
<FilterSection id="price" title="Price">
|
|
515
|
+
<form className="powerhouse-shop__price-form" onSubmit={handlePrice}>
|
|
516
|
+
<label className="powerhouse-shop__price-field">
|
|
517
|
+
<span>₹</span>
|
|
518
|
+
<input
|
|
519
|
+
name="min_price"
|
|
520
|
+
type="number"
|
|
521
|
+
min={0}
|
|
522
|
+
defaultValue={props.minPrice || ""}
|
|
523
|
+
inputMode="decimal"
|
|
524
|
+
placeholder="From"
|
|
525
|
+
/>
|
|
526
|
+
</label>
|
|
527
|
+
<label className="powerhouse-shop__price-field">
|
|
528
|
+
<span>₹</span>
|
|
529
|
+
<input
|
|
530
|
+
name="max_price"
|
|
531
|
+
type="number"
|
|
532
|
+
min={0}
|
|
533
|
+
defaultValue={props.maxPrice || ""}
|
|
534
|
+
inputMode="decimal"
|
|
535
|
+
placeholder="To"
|
|
536
|
+
/>
|
|
537
|
+
</label>
|
|
538
|
+
<button type="submit">Apply</button>
|
|
539
|
+
</form>
|
|
540
|
+
</FilterSection>
|
|
424
541
|
</div>
|
|
425
542
|
</aside>
|
|
426
543
|
|
|
@@ -446,41 +563,48 @@ export default function PowerhouseShop(props: PowerhouseShopProps) {
|
|
|
446
563
|
)}
|
|
447
564
|
|
|
448
565
|
<div className="powerhouse-shop__utils">
|
|
449
|
-
<
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
566
|
+
<div className="powerhouse-shop__utils-left">
|
|
567
|
+
<button type="button" className="powerhouse-shop__filters-btn" onClick={() => setFiltersOpen(true)}>
|
|
568
|
+
<FilterIcon />
|
|
569
|
+
{applied.length ? `Filters (${applied.length})` : "Filters"}
|
|
570
|
+
</button>
|
|
453
571
|
<label className="powerhouse-shop__sort">
|
|
454
|
-
<span className="powerhouse-shop__utils-label">Sort</span>
|
|
455
|
-
<
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
))}
|
|
461
|
-
|
|
572
|
+
<span className="powerhouse-shop__utils-label">Sort by</span>
|
|
573
|
+
<span className="powerhouse-shop__sort-control">
|
|
574
|
+
<span className="powerhouse-shop__sort-value">
|
|
575
|
+
{SORT_OPTIONS.find((option) => option.value === sortBy)?.label || "Featured"}
|
|
576
|
+
</span>
|
|
577
|
+
<SortCaret />
|
|
578
|
+
<select value={sortBy} onChange={(event) => handleSort(event.target.value)} aria-label="Sort products">
|
|
579
|
+
{SORT_OPTIONS.map((option) => (
|
|
580
|
+
<option key={option.value} value={option.value}>
|
|
581
|
+
{option.label}
|
|
582
|
+
</option>
|
|
583
|
+
))}
|
|
584
|
+
</select>
|
|
585
|
+
</span>
|
|
462
586
|
</label>
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
</
|
|
587
|
+
</div>
|
|
588
|
+
<div className="powerhouse-shop__view" role="group" aria-label="View as">
|
|
589
|
+
<span className="powerhouse-shop__utils-label powerhouse-shop__view-label">View as</span>
|
|
590
|
+
<button
|
|
591
|
+
type="button"
|
|
592
|
+
className={view === "grid" ? "is-active" : undefined}
|
|
593
|
+
aria-label="Grid view"
|
|
594
|
+
aria-pressed={view === "grid"}
|
|
595
|
+
onClick={() => handleView("grid")}
|
|
596
|
+
>
|
|
597
|
+
<GridIcon />
|
|
598
|
+
</button>
|
|
599
|
+
<button
|
|
600
|
+
type="button"
|
|
601
|
+
className={view === "list" ? "is-active" : undefined}
|
|
602
|
+
aria-label="List view"
|
|
603
|
+
aria-pressed={view === "list"}
|
|
604
|
+
onClick={() => handleView("list")}
|
|
605
|
+
>
|
|
606
|
+
<ListIcon />
|
|
607
|
+
</button>
|
|
484
608
|
</div>
|
|
485
609
|
</div>
|
|
486
610
|
|
|
@@ -495,6 +619,7 @@ export default function PowerhouseShop(props: PowerhouseShopProps) {
|
|
|
495
619
|
key={product.id ?? product.handle ?? `product-${index}`}
|
|
496
620
|
product={product}
|
|
497
621
|
countryCode={countryCode}
|
|
622
|
+
layout={view}
|
|
498
623
|
/>
|
|
499
624
|
))}
|
|
500
625
|
</div>
|