@pradip1995/segment-powerhouse-shop 0.1.1 → 0.1.2

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pradip1995/segment-powerhouse-shop",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -0,0 +1,24 @@
1
+ "use server"
2
+
3
+ import { addToCart as addToCartCore } from "@pradip1995/commerce-core/data/cart"
4
+ import { removeAuthToken, removeCartId } from "@pradip1995/commerce-core/data/cookies"
5
+
6
+ function isStaleCustomerError(err: unknown) {
7
+ const message = err instanceof Error ? err.message : String(err)
8
+ return /customer with id/i.test(message) && /not found/i.test(message)
9
+ }
10
+
11
+ export async function addToCart(input: {
12
+ variantId: string
13
+ quantity: number
14
+ countryCode: string
15
+ }) {
16
+ try {
17
+ await addToCartCore(input)
18
+ } catch (err) {
19
+ if (!isStaleCustomerError(err)) throw err
20
+ await removeAuthToken()
21
+ await removeCartId()
22
+ await addToCartCore(input)
23
+ }
24
+ }
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { useState, type MouseEvent } from "react"
4
4
  import { useRouter } from "next/navigation"
5
- import { addToCart } from "@pradip1995/commerce-core/client/actions/cart"
5
+ import { addToCart } from "./add-to-cart"
6
6
  import LocalizedLink from "@pradip1995/segment-primitives/localized-link"
7
7
  import { formatPrice } from "@pradip1995/segment-primitives/format-price"
8
8
  import { isNextRedirect } from "@pradip1995/segment-primitives/is-next-redirect"
@@ -82,6 +82,15 @@ export default function ShopProductCard({
82
82
  </LocalizedLink>
83
83
 
84
84
  <div className="powerhouse-shop__info">
85
+ {price != null ? (
86
+ <div className="powerhouse-shop__price">
87
+ {onSale && originalPrice != null ? (
88
+ <span className="powerhouse-shop__compare">{formatPrice(originalPrice, currency)}</span>
89
+ ) : null}
90
+ <span className="powerhouse-shop__current">{formatPrice(price, currency)}</span>
91
+ </div>
92
+ ) : null}
93
+
85
94
  <h2 className="powerhouse-shop__name">
86
95
  <LocalizedLink href={href} countryCode={countryCode}>
87
96
  {product.title}
@@ -98,15 +107,6 @@ export default function ShopProductCard({
98
107
 
99
108
  {copy ? <p className="powerhouse-shop__excerpt">{copy}</p> : null}
100
109
 
101
- {price != null ? (
102
- <div className="powerhouse-shop__price">
103
- {onSale && originalPrice != null ? (
104
- <span className="powerhouse-shop__compare">{formatPrice(originalPrice, currency)}</span>
105
- ) : null}
106
- <span className="powerhouse-shop__current">{formatPrice(price, currency)}</span>
107
- </div>
108
- ) : null}
109
-
110
110
  <div className="powerhouse-shop__actions">
111
111
  <LocalizedLink href={href} countryCode={countryCode} className="powerhouse-shop__quickshop">
112
112
  Quick shop
package/src/shop.css CHANGED
@@ -1,3 +1,20 @@
1
+ html.powerhouse-filters-open,
2
+ html.powerhouse-filters-open body {
3
+ overflow: hidden;
4
+ }
5
+
6
+ .powerhouse-shop .sr-only {
7
+ position: absolute;
8
+ width: 1px;
9
+ height: 1px;
10
+ padding: 0;
11
+ margin: -1px;
12
+ overflow: hidden;
13
+ clip: rect(0, 0, 0, 0);
14
+ white-space: nowrap;
15
+ border: 0;
16
+ }
17
+
1
18
  .powerhouse-shop {
2
19
  --layout-container-max-width: 1400px;
3
20
  max-width: var(--layout-container-max-width);
@@ -94,29 +111,52 @@
94
111
  align-items: center;
95
112
  justify-content: space-between;
96
113
  gap: 1rem;
97
- margin-bottom: 1rem;
114
+ margin: 0;
115
+ padding: 1.1rem 1.15rem;
116
+ border-bottom: 1px solid #e8e8e8;
117
+ background: #fff;
98
118
  }
99
119
 
100
120
  .powerhouse-shop__sidebar h2,
101
- .powerhouse-shop__filter h3 {
121
+ .powerhouse-shop__filter-toggle {
102
122
  margin: 0;
103
123
  font-family: var(--font-sans, inherit);
104
124
  font-weight: 700;
125
+ color: #1d1d1d;
105
126
  }
106
127
 
107
128
  .powerhouse-shop__sidebar h2 {
108
129
  font-size: 1.25rem;
130
+ line-height: 1.2;
109
131
  }
110
132
 
111
133
  .powerhouse-shop__filter {
112
- margin: 0 0 1.5rem;
113
- padding-bottom: 1.25rem;
114
- border-bottom: 1px solid #dddddd;
134
+ margin: 0;
135
+ padding: 0;
136
+ border-bottom: 1px solid #e8e8e8;
115
137
  }
116
138
 
117
- .powerhouse-shop__filter h3 {
118
- margin-bottom: 0.75rem;
139
+ .powerhouse-shop__filter-toggle {
140
+ display: flex;
141
+ align-items: center;
142
+ justify-content: space-between;
143
+ width: 100%;
144
+ padding: 1.05rem 0 0.85rem;
145
+ border: 0;
146
+ background: transparent;
119
147
  font-size: 0.9375rem;
148
+ text-align: left;
149
+ cursor: pointer;
150
+ }
151
+
152
+ .powerhouse-shop__filter-icon {
153
+ font-size: 1.25rem;
154
+ line-height: 1;
155
+ font-weight: 400;
156
+ }
157
+
158
+ .powerhouse-shop__filter-body {
159
+ padding: 0 0 1.15rem;
120
160
  }
121
161
 
122
162
  .powerhouse-shop__filter ul {
@@ -126,21 +166,21 @@
126
166
  }
127
167
 
128
168
  .powerhouse-shop__filter li + li {
129
- margin-top: 0.45rem;
169
+ margin-top: 0.7rem;
130
170
  }
131
171
 
132
172
  .powerhouse-shop__filter a {
133
173
  display: flex;
134
174
  align-items: center;
135
- gap: 0.55rem;
175
+ gap: 0.7rem;
136
176
  color: #1d1d1d;
137
177
  font-size: 0.9375rem;
138
178
  text-decoration: none;
139
179
  }
140
180
 
141
181
  .powerhouse-shop__check {
142
- width: 1rem;
143
- height: 1rem;
182
+ width: 1.05rem;
183
+ height: 1.05rem;
144
184
  flex-shrink: 0;
145
185
  border: 1px solid #1d1d1d;
146
186
  background: #fff;
@@ -151,38 +191,64 @@
151
191
  box-shadow: inset 0 0 0 2px #fff;
152
192
  }
153
193
 
194
+ .powerhouse-shop__swatches {
195
+ display: flex;
196
+ flex-wrap: wrap;
197
+ gap: 0.55rem;
198
+ }
199
+
200
+ .powerhouse-shop__swatches li + li {
201
+ margin-top: 0;
202
+ }
203
+
204
+ .powerhouse-shop__swatch {
205
+ display: block;
206
+ width: 1.35rem;
207
+ height: 1.35rem;
208
+ border: 1px solid #cfcfcf;
209
+ }
210
+
211
+ .powerhouse-shop__swatches a.is-active .powerhouse-shop__swatch {
212
+ outline: 1px solid #1d1d1d;
213
+ outline-offset: 2px;
214
+ }
215
+
154
216
  .powerhouse-shop__price-form {
155
217
  display: grid;
156
218
  grid-template-columns: 1fr 1fr;
157
- gap: 0.5rem;
219
+ gap: 0.65rem;
158
220
  }
159
221
 
160
- .powerhouse-shop__price-form label {
222
+ .powerhouse-shop__price-field {
161
223
  display: flex;
162
- flex-direction: column;
163
- gap: 0.3rem;
164
- font-size: 0.8125rem;
224
+ align-items: center;
225
+ gap: 0.35rem;
226
+ min-width: 0;
227
+ padding: 0 0.65rem;
228
+ border: 1px solid #dddddd;
229
+ background: #fff;
230
+ font-size: 0.9375rem;
231
+ color: #6e6e6e;
165
232
  }
166
233
 
167
- .powerhouse-shop__price-form input,
168
- .powerhouse-shop__sort select {
234
+ .powerhouse-shop__price-field input {
169
235
  width: 100%;
170
- padding: 0.6rem 0.7rem;
171
- border: 1px solid #dddddd;
172
- background: #fff;
236
+ min-width: 0;
237
+ padding: 0.7rem 0;
238
+ border: 0;
239
+ background: transparent;
173
240
  color: #1d1d1d;
174
241
  font-family: var(--font-sans, inherit);
175
242
  font-size: 0.9375rem;
176
243
  }
177
244
 
178
- .powerhouse-shop__sort select {
179
- width: auto;
180
- min-width: 12rem;
245
+ .powerhouse-shop__price-field input:focus {
246
+ outline: none;
181
247
  }
182
248
 
183
249
  .powerhouse-shop__price-form button {
184
250
  grid-column: 1 / -1;
185
- padding: 0.65rem 0.9rem;
251
+ padding: 0.7rem 0.9rem;
186
252
  border: 1px solid #1d1d1d;
187
253
  background: #1d1d1d;
188
254
  color: #fff;
@@ -212,37 +278,57 @@
212
278
  display: flex;
213
279
  align-items: center;
214
280
  justify-content: space-between;
215
- gap: 0.75rem;
216
- margin: 1rem 0 1.25rem;
281
+ gap: 0.5rem;
282
+ margin: 0.35rem 0 1rem;
283
+ flex-wrap: nowrap;
284
+ }
285
+
286
+ .powerhouse-shop__utils-left {
287
+ display: flex;
288
+ align-items: center;
289
+ min-width: 0;
290
+ gap: 0.15rem 1.15rem;
217
291
  }
218
292
 
219
293
  .powerhouse-shop__filters-btn,
220
294
  .powerhouse-shop__sort {
221
- display: flex;
295
+ display: inline-flex;
222
296
  align-items: center;
223
- gap: 0.5rem;
297
+ gap: 0.45rem;
224
298
  font-family: var(--font-sans, inherit);
299
+ font-size: 0.9375rem;
300
+ color: #1d1d1d;
225
301
  }
226
302
 
227
303
  .powerhouse-shop__filters-btn {
228
- padding: 0.65rem 0.9rem;
229
- border: 1px solid #dddddd;
230
- background: #fff;
231
- color: #1d1d1d;
304
+ padding: 0.45rem 0.15rem;
305
+ border: 0;
306
+ background: transparent;
232
307
  cursor: pointer;
308
+ white-space: nowrap;
233
309
  }
234
310
 
235
- .powerhouse-shop__utils-right {
236
- display: flex;
237
- align-items: center;
238
- justify-content: flex-end;
239
- flex-wrap: wrap;
240
- gap: 0.85rem 1.25rem;
241
- margin-left: auto;
311
+ .powerhouse-shop__sort {
312
+ position: relative;
313
+ min-width: 0;
314
+ }
315
+
316
+ .powerhouse-shop__sort select {
317
+ position: absolute;
318
+ inset: 0;
319
+ width: 100%;
320
+ min-width: 0;
321
+ opacity: 0;
322
+ cursor: pointer;
242
323
  }
243
324
 
244
325
  .powerhouse-shop__utils-label {
245
326
  font-size: 0.9375rem;
327
+ white-space: nowrap;
328
+ }
329
+
330
+ .powerhouse-shop__view-label {
331
+ display: none;
246
332
  }
247
333
 
248
334
  .powerhouse-shop__view {
@@ -325,6 +411,7 @@
325
411
  display: grid;
326
412
  grid-template-columns: 7.5rem minmax(0, 1fr);
327
413
  align-items: start;
414
+ height: auto;
328
415
  gap: 1rem;
329
416
  padding: 1.25rem 0;
330
417
  border-bottom: 1px solid #dddddd;
@@ -332,6 +419,18 @@
332
419
 
333
420
  .powerhouse-shop__grid.is-list .powerhouse-shop__info {
334
421
  padding-top: 0;
422
+ flex: none;
423
+ }
424
+
425
+ .powerhouse-shop__grid.is-list .powerhouse-shop__name {
426
+ display: block;
427
+ -webkit-line-clamp: unset;
428
+ overflow: visible;
429
+ min-height: 0;
430
+ }
431
+
432
+ .powerhouse-shop__grid.is-list .powerhouse-shop__price {
433
+ min-height: 0;
335
434
  }
336
435
 
337
436
  .powerhouse-shop__grid.is-list .powerhouse-shop__excerpt,
@@ -351,25 +450,35 @@
351
450
  .powerhouse-shop__grid {
352
451
  display: grid;
353
452
  grid-template-columns: repeat(2, minmax(0, 1fr));
354
- gap: 1.25rem 10px;
453
+ align-items: stretch;
454
+ gap: 2rem 10px;
455
+ width: 100%;
355
456
  }
356
457
 
357
458
  .powerhouse-shop__card {
459
+ display: flex;
460
+ flex-direction: column;
461
+ height: 100%;
358
462
  min-width: 0;
463
+ max-width: 100%;
359
464
  }
360
465
 
361
466
  .powerhouse-shop__media {
362
467
  position: relative;
363
468
  display: block;
469
+ width: 100%;
364
470
  overflow: hidden;
365
471
  background: #f3f3f3;
472
+ aspect-ratio: 1;
366
473
  }
367
474
 
368
475
  .powerhouse-shop__media img,
369
476
  .powerhouse-shop__placeholder {
477
+ position: absolute;
478
+ inset: 0;
370
479
  display: block;
371
480
  width: 100%;
372
- aspect-ratio: 1;
481
+ height: 100%;
373
482
  object-fit: cover;
374
483
  }
375
484
 
@@ -396,15 +505,24 @@
396
505
  }
397
506
 
398
507
  .powerhouse-shop__info {
508
+ display: flex;
509
+ flex-direction: column;
510
+ flex: 1 1 auto;
511
+ min-width: 0;
399
512
  padding-top: 0.75rem;
400
513
  }
401
514
 
402
515
  .powerhouse-shop__name {
516
+ display: -webkit-box;
517
+ -webkit-box-orient: vertical;
518
+ -webkit-line-clamp: 2;
519
+ overflow: hidden;
403
520
  margin: 0;
521
+ min-height: calc(1em * 1.35 * 2);
404
522
  font-family: var(--font-sans, inherit);
405
523
  font-size: 1rem;
406
524
  font-weight: 400;
407
- line-height: 1.3;
525
+ line-height: 1.35;
408
526
  }
409
527
 
410
528
  .powerhouse-shop__name a {
@@ -414,6 +532,7 @@
414
532
 
415
533
  .powerhouse-shop__stock {
416
534
  margin: 0.35rem 0 0;
535
+ min-height: 1.2em;
417
536
  color: #688600;
418
537
  font-size: 0.8125rem;
419
538
  }
@@ -431,7 +550,8 @@
431
550
  }
432
551
 
433
552
  .powerhouse-shop__price {
434
- margin-top: 0.4rem;
553
+ margin: 0 0 0.4rem;
554
+ min-height: 2.7rem;
435
555
  }
436
556
 
437
557
  .powerhouse-shop__compare {
@@ -455,22 +575,26 @@
455
575
 
456
576
  .powerhouse-shop__actions {
457
577
  display: flex;
458
- flex-wrap: wrap;
459
- gap: 0.5rem;
460
- margin-top: 0.75rem;
578
+ flex-direction: column;
579
+ flex-wrap: nowrap;
580
+ gap: 0.45rem;
581
+ margin-top: auto;
582
+ padding-top: 0.75rem;
461
583
  }
462
584
 
463
585
  .powerhouse-shop__quickshop,
464
586
  .powerhouse-shop__atc {
465
- display: inline-flex;
587
+ display: flex;
466
588
  align-items: center;
467
589
  justify-content: center;
590
+ width: 100%;
468
591
  min-height: 2.5rem;
469
- padding: 0.5rem 0.85rem;
592
+ padding: 0.5rem 0.65rem;
470
593
  border-radius: 2px;
594
+ box-sizing: border-box;
471
595
  font-family: var(--font-sans, inherit);
472
- font-size: 0.8125rem;
473
- font-weight: 500;
596
+ font-size: 0.875rem;
597
+ font-weight: 600;
474
598
  text-decoration: none;
475
599
  cursor: pointer;
476
600
  }
@@ -536,40 +660,68 @@
536
660
  @media screen and (max-width: 859px) {
537
661
  .powerhouse-shop.is-filters-open .powerhouse-shop__sidebar,
538
662
  .powerhouse-shop__layout.is-filters-open .powerhouse-shop__sidebar {
539
- display: block;
663
+ display: flex;
664
+ flex-direction: column;
540
665
  position: fixed;
541
666
  inset: 0 auto 0 0;
542
- z-index: 40;
543
- width: min(22rem, 88vw);
544
- padding: 1.25rem;
545
- overflow: auto;
667
+ z-index: 80;
668
+ width: min(26.5rem, 100vw);
669
+ max-width: 100%;
670
+ padding: 0;
671
+ overflow: hidden;
546
672
  background: #fff;
547
- box-shadow: 8px 0 24px rgb(0 0 0 / 12%);
673
+ box-shadow: 8px 0 28px rgb(0 0 0 / 18%);
674
+ }
675
+
676
+ .powerhouse-shop__layout.is-filters-open .powerhouse-shop__sidebar-head {
677
+ position: sticky;
678
+ top: 0;
679
+ z-index: 1;
680
+ flex-shrink: 0;
681
+ }
682
+
683
+ .powerhouse-shop__layout.is-filters-open .powerhouse-shop__sidebar-body {
684
+ flex: 1;
685
+ min-height: 0;
686
+ overflow: auto;
687
+ padding: 0 1.15rem 2rem;
688
+ -webkit-overflow-scrolling: touch;
548
689
  }
549
690
 
550
691
  .powerhouse-shop__layout.is-filters-open .powerhouse-shop__close,
551
692
  .powerhouse-shop__layout.is-filters-open .powerhouse-shop__backdrop {
552
- display: block;
693
+ display: flex;
553
694
  }
554
695
 
555
696
  .powerhouse-shop__backdrop {
556
697
  position: fixed;
557
698
  inset: 0;
558
- z-index: 30;
699
+ z-index: 75;
559
700
  border: 0;
560
- background: rgb(0 0 0 / 35%);
701
+ background: rgb(0 0 0 / 45%);
561
702
  }
562
703
 
563
704
  .powerhouse-shop__close {
705
+ align-items: center;
706
+ justify-content: center;
707
+ width: 2.5rem;
708
+ height: 2.5rem;
709
+ margin-right: -0.45rem;
564
710
  padding: 0;
565
711
  border: 0;
566
712
  background: none;
567
713
  color: #1d1d1d;
568
- text-decoration: underline;
569
714
  cursor: pointer;
570
715
  }
571
716
  }
572
717
 
718
+ @media screen and (max-width: 719px) {
719
+ .powerhouse-shop__crumbs,
720
+ .powerhouse-shop__masthead {
721
+ display: none;
722
+ }
723
+ }
724
+
573
725
  @media screen and (min-width: 720px) {
574
726
  .powerhouse-shop {
575
727
  padding: 0 20px 4rem;
@@ -593,7 +745,6 @@
593
745
  }
594
746
 
595
747
  .powerhouse-shop__grid {
596
- grid-template-columns: repeat(3, minmax(0, 1fr));
597
748
  gap: 1.75rem 20px;
598
749
  }
599
750
 
@@ -606,6 +757,13 @@
606
757
  gap: 1.5rem;
607
758
  padding: 1.5rem 0;
608
759
  }
760
+
761
+ .powerhouse-shop__sort select {
762
+ position: static;
763
+ opacity: 1;
764
+ width: auto;
765
+ min-width: 10.5rem;
766
+ }
609
767
  }
610
768
 
611
769
  @media screen and (min-width: 860px) {
@@ -621,6 +779,11 @@
621
779
  flex-shrink: 0;
622
780
  }
623
781
 
782
+ .powerhouse-shop__sidebar-head {
783
+ padding: 0 0 0.85rem;
784
+ border-bottom: 0;
785
+ }
786
+
624
787
  .powerhouse-shop__sidebar-head .powerhouse-shop__close,
625
788
  .powerhouse-shop__filters-btn {
626
789
  display: none;
@@ -641,10 +804,15 @@
641
804
 
642
805
  .powerhouse-shop__utils {
643
806
  justify-content: flex-end;
807
+ margin-top: 0.75rem;
808
+ }
809
+
810
+ .powerhouse-shop__view-label {
811
+ display: inline;
644
812
  }
645
813
 
646
814
  .powerhouse-shop__grid {
647
- grid-template-columns: repeat(4, minmax(0, 1fr));
815
+ grid-template-columns: repeat(3, minmax(0, 1fr));
648
816
  }
649
817
 
650
818
  .powerhouse-shop__grid.is-list {
@@ -668,3 +836,13 @@
668
836
  pointer-events: auto;
669
837
  }
670
838
  }
839
+
840
+ @media screen and (min-width: 1100px) {
841
+ .powerhouse-shop__grid {
842
+ grid-template-columns: repeat(4, minmax(0, 1fr));
843
+ }
844
+
845
+ .powerhouse-shop__grid.is-list {
846
+ grid-template-columns: 1fr;
847
+ }
848
+ }
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,22 @@ 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 SortIcon() {
99
+ return (
100
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" aria-hidden>
101
+ <path d="M8 7V21M8 7L4.5 10.5M8 7L11.5 10.5M16 17V3M16 17L12.5 13.5M16 17L19.5 13.5" stroke="currentColor" strokeWidth="1.75" strokeLinecap="square" />
102
+ </svg>
103
+ )
104
+ }
105
+
90
106
  function GridIcon() {
91
107
  return (
92
108
  <svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor" aria-hidden>
@@ -157,6 +173,71 @@ function ActiveFilters({
157
173
  )
158
174
  }
159
175
 
176
+ function colorSwatch(value?: string, label?: string) {
177
+ const raw = (value || label || "").trim()
178
+ if (/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(raw)) return raw
179
+ if (/^(rgb|hsl)a?\(/i.test(raw)) return raw
180
+ const named = raw.toLowerCase().replace(/[\s_-]+/g, "")
181
+ const map: Record<string, string> = {
182
+ black: "#111111",
183
+ white: "#ffffff",
184
+ red: "#c62828",
185
+ blue: "#1565c0",
186
+ green: "#2e7d32",
187
+ yellow: "#f1c40f",
188
+ pink: "#e91e63",
189
+ purple: "#6a1b9a",
190
+ orange: "#ef6c00",
191
+ brown: "#6d4c41",
192
+ grey: "#9e9e9e",
193
+ gray: "#9e9e9e",
194
+ gold: "#c9a86c",
195
+ beige: "#d7ccc8",
196
+ navy: "#1a237e",
197
+ maroon: "#7b1e3a",
198
+ }
199
+ return map[named] || null
200
+ }
201
+
202
+ function CloseIcon() {
203
+ return (
204
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden>
205
+ <path d="M5 5L19 19M19 5L5 19" stroke="currentColor" strokeWidth="1.75" strokeLinecap="square" />
206
+ </svg>
207
+ )
208
+ }
209
+
210
+ function FilterSection({
211
+ id,
212
+ title,
213
+ children,
214
+ }: {
215
+ id: string
216
+ title: string
217
+ children: ReactNode
218
+ }) {
219
+ const [open, setOpen] = useState(true)
220
+ return (
221
+ <div className={`powerhouse-shop__filter${open ? " is-open" : ""}`}>
222
+ <button
223
+ type="button"
224
+ className="powerhouse-shop__filter-toggle"
225
+ aria-expanded={open}
226
+ aria-controls={`filter-${id}`}
227
+ onClick={() => setOpen((value) => !value)}
228
+ >
229
+ <span>{title}</span>
230
+ <span className="powerhouse-shop__filter-icon" aria-hidden>
231
+ {open ? "−" : "+"}
232
+ </span>
233
+ </button>
234
+ <div id={`filter-${id}`} className="powerhouse-shop__filter-body" hidden={!open}>
235
+ {children}
236
+ </div>
237
+ </div>
238
+ )
239
+ }
240
+
160
241
  const VIEW_STORAGE_KEY = "powerhouse-shop-view"
161
242
 
162
243
  function paginationItems(current: number, total: number) {
@@ -197,6 +278,19 @@ export default function PowerhouseShop(props: PowerhouseShopProps) {
197
278
  setView(next)
198
279
  }, [])
199
280
 
281
+ useEffect(() => {
282
+ document.documentElement.classList.toggle("powerhouse-filters-open", filtersOpen)
283
+ if (!filtersOpen) return undefined
284
+ const onKey = (event: KeyboardEvent) => {
285
+ if (event.key === "Escape") setFiltersOpen(false)
286
+ }
287
+ window.addEventListener("keydown", onKey)
288
+ return () => {
289
+ window.removeEventListener("keydown", onKey)
290
+ document.documentElement.classList.remove("powerhouse-filters-open")
291
+ }
292
+ }, [filtersOpen])
293
+
200
294
  const categories = (props.categories ?? []).filter((item) => item.id && (item.name || item.handle))
201
295
  const collections = (props.collections ?? []).filter((item) => item.id && (item.title || item.handle))
202
296
  const types = props.typeOptions?.filter((item) => item.value && item.label) ?? []
@@ -307,120 +401,142 @@ export default function PowerhouseShop(props: PowerhouseShopProps) {
307
401
  <aside className="powerhouse-shop__sidebar" aria-label="Filters">
308
402
  <div className="powerhouse-shop__sidebar-head">
309
403
  <h2>Filters</h2>
310
- <button type="button" className="powerhouse-shop__close" onClick={() => setFiltersOpen(false)}>
311
- Close
404
+ <button
405
+ type="button"
406
+ className="powerhouse-shop__close"
407
+ aria-label="Close filters"
408
+ onClick={() => setFiltersOpen(false)}
409
+ >
410
+ <CloseIcon />
312
411
  </button>
313
412
  </div>
314
413
 
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>
414
+ <div className="powerhouse-shop__sidebar-body">
415
+ <ActiveFilters filters={applied} clearHref={clearHref} countryCode={countryCode} />
416
+
417
+ {categories.length ? (
418
+ <FilterSection id="product-type" title="Product type">
419
+ <ul>
420
+ {categories.map((item) => {
421
+ const value = item.id
422
+ const selected = activeCategory === value || activeCategory === item.handle
423
+ return (
424
+ <li key={value}>
425
+ <LocalizedLink
426
+ href={toggleFilter(propsWithView, "category", value, selected ? value : undefined)}
427
+ countryCode={countryCode}
428
+ className={selected ? "is-active" : undefined}
429
+ >
430
+ <span className="powerhouse-shop__check" aria-hidden />
431
+ {item.name || item.handle}
432
+ </LocalizedLink>
433
+ </li>
434
+ )
435
+ })}
436
+ </ul>
437
+ </FilterSection>
438
+ ) : null}
439
+
440
+ {collections.length ? (
441
+ <FilterSection id="collection" title="Collection">
442
+ <ul>
443
+ {collections.map((item) => {
444
+ const value = item.id
445
+ const selected = activeCollection === value || activeCollection === item.handle
446
+ return (
447
+ <li key={value}>
448
+ <LocalizedLink
449
+ href={toggleFilter(propsWithView, "collection", value, selected ? value : undefined)}
450
+ countryCode={countryCode}
451
+ className={selected ? "is-active" : undefined}
452
+ >
453
+ <span className="powerhouse-shop__check" aria-hidden />
454
+ {item.title || item.handle}
455
+ </LocalizedLink>
456
+ </li>
457
+ )
458
+ })}
459
+ </ul>
460
+ </FilterSection>
461
+ ) : null}
462
+
463
+ {types.length ? (
464
+ <FilterSection id="type" title="Type">
465
+ <ul>
466
+ {types.map((item) => {
467
+ const selected = activeType === item.value
468
+ return (
469
+ <li key={item.value}>
470
+ <LocalizedLink
471
+ href={toggleFilter(propsWithView, "type", item.value, selected ? item.value : undefined)}
472
+ countryCode={countryCode}
473
+ className={selected ? "is-active" : undefined}
474
+ >
475
+ <span className="powerhouse-shop__check" aria-hidden />
476
+ {item.label}
477
+ </LocalizedLink>
478
+ </li>
479
+ )
480
+ })}
481
+ </ul>
482
+ </FilterSection>
483
+ ) : null}
484
+
485
+ {colors.length ? (
486
+ <FilterSection id="colors" title="Colors">
487
+ <ul className="powerhouse-shop__swatches">
488
+ {colors.map((item) => {
489
+ const selected = activeColor === item.value
490
+ const swatch = colorSwatch(item.value, item.label)
491
+ return (
492
+ <li key={item.value}>
493
+ <LocalizedLink
494
+ href={toggleFilter(propsWithView, "color", item.value, selected ? item.value : undefined)}
495
+ countryCode={countryCode}
496
+ className={selected ? "is-active" : undefined}
497
+ aria-label={item.label}
498
+ >
499
+ {swatch ? (
500
+ <span className="powerhouse-shop__swatch" style={{ background: swatch }} aria-hidden />
501
+ ) : (
502
+ <span className="powerhouse-shop__check" aria-hidden />
503
+ )}
504
+ {swatch ? <span className="sr-only">{item.label}</span> : item.label}
505
+ </LocalizedLink>
506
+ </li>
507
+ )
508
+ })}
509
+ </ul>
510
+ </FilterSection>
511
+ ) : null}
512
+
513
+ <FilterSection id="price" title="Price">
514
+ <form className="powerhouse-shop__price-form" onSubmit={handlePrice}>
515
+ <label className="powerhouse-shop__price-field">
516
+ <span>₹</span>
517
+ <input
518
+ name="min_price"
519
+ type="number"
520
+ min={0}
521
+ defaultValue={props.minPrice || ""}
522
+ inputMode="decimal"
523
+ placeholder="From"
524
+ />
525
+ </label>
526
+ <label className="powerhouse-shop__price-field">
527
+ <span>₹</span>
528
+ <input
529
+ name="max_price"
530
+ type="number"
531
+ min={0}
532
+ defaultValue={props.maxPrice || ""}
533
+ inputMode="decimal"
534
+ placeholder="To"
535
+ />
536
+ </label>
537
+ <button type="submit">Apply</button>
538
+ </form>
539
+ </FilterSection>
424
540
  </div>
425
541
  </aside>
426
542
 
@@ -446,12 +562,14 @@ export default function PowerhouseShop(props: PowerhouseShopProps) {
446
562
  )}
447
563
 
448
564
  <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">
565
+ <div className="powerhouse-shop__utils-left">
566
+ <button type="button" className="powerhouse-shop__filters-btn" onClick={() => setFiltersOpen(true)}>
567
+ <FilterIcon />
568
+ {applied.length ? `Filters (${applied.length})` : "Filters"}
569
+ </button>
453
570
  <label className="powerhouse-shop__sort">
454
- <span className="powerhouse-shop__utils-label">Sort</span>
571
+ <SortIcon />
572
+ <span className="powerhouse-shop__utils-label">Sort by</span>
455
573
  <select value={sortBy} onChange={(event) => handleSort(event.target.value)} aria-label="Sort products">
456
574
  {SORT_OPTIONS.map((option) => (
457
575
  <option key={option.value} value={option.value}>
@@ -460,27 +578,27 @@ export default function PowerhouseShop(props: PowerhouseShopProps) {
460
578
  ))}
461
579
  </select>
462
580
  </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>
581
+ </div>
582
+ <div className="powerhouse-shop__view" role="group" aria-label="View as">
583
+ <span className="powerhouse-shop__utils-label powerhouse-shop__view-label">View as</span>
584
+ <button
585
+ type="button"
586
+ className={view === "grid" ? "is-active" : undefined}
587
+ aria-label="Grid view"
588
+ aria-pressed={view === "grid"}
589
+ onClick={() => handleView("grid")}
590
+ >
591
+ <GridIcon />
592
+ </button>
593
+ <button
594
+ type="button"
595
+ className={view === "list" ? "is-active" : undefined}
596
+ aria-label="List view"
597
+ aria-pressed={view === "list"}
598
+ onClick={() => handleView("list")}
599
+ >
600
+ <ListIcon />
601
+ </button>
484
602
  </div>
485
603
  </div>
486
604