@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/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 type="button" className="powerhouse-shop__close" onClick={() => setFiltersOpen(false)}>
311
- Close
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
- <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>
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
- <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">
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
- <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>
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
- <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>
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>