@akinon/pz-similar-products 1.92.0-rc.16

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.
@@ -0,0 +1,858 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+ import {
5
+ Button,
6
+ Icon,
7
+ Accordion,
8
+ LoaderSpinner
9
+ } from '@akinon/next/components';
10
+ import { useLocalization } from '@akinon/next/hooks';
11
+ import { FilterSidebarProps } from '../types';
12
+ import clsx from 'clsx';
13
+ import { twMerge } from 'tailwind-merge';
14
+ import dynamic from 'next/dynamic';
15
+
16
+ const ReactCrop = dynamic(
17
+ () => import('react-image-crop').then((mod) => mod.ReactCrop),
18
+ {
19
+ ssr: false,
20
+ loading: () => (
21
+ <div className="w-full h-96 bg-gray-100 flex items-center justify-center">
22
+ <LoaderSpinner />
23
+ </div>
24
+ )
25
+ }
26
+ );
27
+
28
+ const Radio = dynamic(
29
+ () =>
30
+ import('@akinon/next/components')
31
+ .then((mod) => mod.Radio)
32
+ .catch(() => import('@theme/components').then((mod) => mod.Radio))
33
+ .catch(() => () => <input type="radio" className="form-radio" />),
34
+ { ssr: false }
35
+ );
36
+
37
+ const Checkbox = dynamic(
38
+ () =>
39
+ import('@theme/components')
40
+ .then((mod) => mod.Checkbox)
41
+ .catch(() => import('@theme/components').then((mod) => mod.Checkbox))
42
+ .catch(() => () => <input type="checkbox" className="form-checkbox" />),
43
+ { ssr: false }
44
+ );
45
+
46
+ const WIDGET_TYPE = {
47
+ category: 'category',
48
+ multiselect: 'multiselect'
49
+ };
50
+
51
+ export function SimilarProductsFilterSidebar({
52
+ isFilterMenuOpen,
53
+ setIsFilterMenuOpen,
54
+ searchResults,
55
+ isLoading,
56
+ handleFacetChange,
57
+ removeFacetFilter,
58
+ currentImageUrl,
59
+ isCropping,
60
+ imageRef,
61
+ fileInputRef,
62
+ crop,
63
+ setCrop,
64
+ completedCrop,
65
+ setCompletedCrop,
66
+ handleCropComplete,
67
+ toggleCropMode,
68
+ processCompletedCrop,
69
+ processManualCrop,
70
+ resetCrop,
71
+ product,
72
+ hasUploadedImage,
73
+ handleFileUpload,
74
+ handleResetToOriginal,
75
+ fileError,
76
+ showResetButton = true,
77
+ settings,
78
+ className
79
+ }: FilterSidebarProps) {
80
+ const { t } = useLocalization();
81
+
82
+ const sizeKey = settings?.commonProductAttributes?.find(
83
+ (item: any) => item.translationKey === 'size'
84
+ )?.key;
85
+
86
+ const COMPONENT_TYPES = {
87
+ [WIDGET_TYPE.category]: Radio,
88
+ [WIDGET_TYPE.multiselect]: Checkbox
89
+ };
90
+
91
+ const getComponentByWidgetType = (widgetType: string, facetKey: string) => {
92
+ return (
93
+ COMPONENT_TYPES[widgetType] || COMPONENT_TYPES[WIDGET_TYPE.multiselect]
94
+ );
95
+ };
96
+
97
+ const handleNewImageClick = () => {
98
+ if (!isLoading && settings?.enableFileUpload !== false) {
99
+ fileInputRef.current?.click();
100
+ }
101
+ };
102
+
103
+ const handleFileChangeWithCropReset = async (
104
+ event: React.ChangeEvent<HTMLInputElement>
105
+ ) => {
106
+ if (isLoading) return;
107
+
108
+ resetCrop();
109
+ handleFileUpload(event);
110
+
111
+ if (event.target) {
112
+ event.target.value = '';
113
+ }
114
+ };
115
+
116
+ const handleToggleCropMode = () => {
117
+ if (!isLoading) {
118
+ toggleCropMode();
119
+ }
120
+ };
121
+
122
+ const handleSafeProcessCrop = async (cropData: any) => {
123
+ if (!isLoading && cropData?.width > 10 && cropData?.height > 10) {
124
+ processManualCrop(cropData);
125
+ }
126
+ };
127
+
128
+ const sidebarClassName = twMerge(
129
+ 'w-9/10 fixed left-0 top-0 bottom-0 bg-white z-50 p-6 transition-all ease-in duration-300 md:static md:mr-0 md:text-sm md:pt-4 md:col-span-1 md:bg-gray-100 md:p-4 md:flex md:flex-col md:overflow-y-auto overflow-x-hidden',
130
+ isFilterMenuOpen
131
+ ? 'flex flex-col opacity-100 overflow-auto md:opacity-100 md:visible md:translate-x-0'
132
+ : 'opacity-0 invisible absolute -translate-x-full md:opacity-100 md:visible md:translate-x-0',
133
+ settings?.customStyles?.filterSidebar,
134
+ className
135
+ );
136
+
137
+ return (
138
+ <div
139
+ className={sidebarClassName}
140
+ style={{
141
+ maxHeight: 'calc(100vh - 120px)'
142
+ }}
143
+ >
144
+ {(() => {
145
+ if (
146
+ settings?.customRenderers?.render?.filterSidebar?.renderMobileHeader
147
+ ) {
148
+ return settings.customRenderers.render.filterSidebar.renderMobileHeader(
149
+ {
150
+ title: t('common.product.filters'),
151
+ itemCount: searchResults?.pagination?.total_count || 0,
152
+ onClose: () => setIsFilterMenuOpen(false)
153
+ }
154
+ );
155
+ }
156
+
157
+ const mobileHeaderClassName = twMerge(
158
+ 'flex justify-between mb-6 md:hidden',
159
+ settings?.customStyles?.filterSidebarMobileHeader
160
+ );
161
+
162
+ return (
163
+ <>
164
+ <div className={mobileHeaderClassName}>
165
+ <h3 className="text-2xl font-bold">
166
+ {t('common.product.filters')}
167
+ </h3>
168
+ <Button
169
+ appearance="ghost"
170
+ size="sm"
171
+ onClick={() => setIsFilterMenuOpen(false)}
172
+ className="hover:bg-gray-200 rounded-full p-0 w-6 h-6"
173
+ >
174
+ <Icon name="close" size={16} />
175
+ </Button>
176
+ </div>
177
+ <div className="flex justify-between items-center mb-6 md:hidden">
178
+ <span className="text-sm">
179
+ {searchResults?.pagination?.total_count || 0} items
180
+ </span>
181
+ </div>
182
+ </>
183
+ );
184
+ })()}
185
+
186
+ {(() => {
187
+ if (
188
+ settings?.customRenderers?.render?.filterSidebar?.renderImageSection
189
+ ) {
190
+ return settings.customRenderers.render.filterSidebar.renderImageSection(
191
+ {
192
+ currentImageUrl,
193
+ isCropping,
194
+ onToggleCrop: handleToggleCropMode,
195
+ onFileUpload: handleFileChangeWithCropReset,
196
+ onResetToOriginal: handleResetToOriginal,
197
+ fileError,
198
+ isLoading
199
+ }
200
+ );
201
+ }
202
+
203
+ const imageSectionClassName = twMerge(
204
+ 'relative mb-2 md:mb-6 mt-4',
205
+ settings?.customStyles?.imageSection
206
+ );
207
+
208
+ const imageContainerClassName = twMerge(
209
+ 'relative bg-white overflow-hidden flex items-center justify-center transition-all duration-300 ease-in-out',
210
+ isCropping ? 'border-2 border-dashed border-gray-300' : '',
211
+ settings?.customStyles?.imageContainer
212
+ );
213
+
214
+ const cropButtonClassName = twMerge(
215
+ `absolute z-10 bottom-3 left-3 p-2 rounded-full bg-white shadow-md w-10 h-10 ${
216
+ isCropping ? 'text-red-500' : 'text-gray-700'
217
+ } ${isLoading ? 'opacity-50 cursor-not-allowed' : ''}`,
218
+ settings?.customStyles?.cropButton
219
+ );
220
+
221
+ const renderCropButton = () => {
222
+ if (!settings?.enableCropping || settings.enableCropping === false)
223
+ return null;
224
+
225
+ if (
226
+ settings?.customRenderers?.render?.filterSidebar?.renderCropButton
227
+ ) {
228
+ return settings.customRenderers.render.filterSidebar.renderCropButton(
229
+ {
230
+ isCropping,
231
+ onClick: handleToggleCropMode,
232
+ disabled: isLoading
233
+ }
234
+ );
235
+ }
236
+
237
+ return (
238
+ <Button
239
+ appearance="ghost"
240
+ size="sm"
241
+ onClick={handleToggleCropMode}
242
+ disabled={isLoading}
243
+ className={cropButtonClassName}
244
+ >
245
+ {isLoading ? (
246
+ <LoaderSpinner className="w-5 h-5" />
247
+ ) : isCropping ? (
248
+ <svg
249
+ xmlns="http://www.w3.org/2000/svg"
250
+ width="20"
251
+ height="20"
252
+ viewBox="0 0 24 24"
253
+ fill="none"
254
+ stroke="currentColor"
255
+ strokeWidth="2"
256
+ strokeLinecap="round"
257
+ strokeLinejoin="round"
258
+ >
259
+ <line x1="18" y1="6" x2="6" y2="18"></line>
260
+ <line x1="6" y1="6" x2="18" y2="18"></line>
261
+ </svg>
262
+ ) : (
263
+ <svg
264
+ xmlns="http://www.w3.org/2000/svg"
265
+ width="20"
266
+ height="20"
267
+ viewBox="0 0 24 24"
268
+ fill="none"
269
+ stroke="currentColor"
270
+ strokeWidth="2"
271
+ strokeLinecap="round"
272
+ strokeLinejoin="round"
273
+ >
274
+ <path d="M6 2v14a2 2 0 0 0 2 2h14"></path>
275
+ <path d="M18 22V8a2 2 0 0 0-2-2H2"></path>
276
+ </svg>
277
+ )}
278
+ </Button>
279
+ );
280
+ };
281
+
282
+ const renderTickButton = () => {
283
+ if (!settings?.enableCropping || settings.enableCropping === false)
284
+ return null;
285
+ if (
286
+ !isCropping ||
287
+ !completedCrop ||
288
+ completedCrop.width <= 10 ||
289
+ completedCrop.height <= 10
290
+ )
291
+ return null;
292
+
293
+ if (
294
+ settings?.customRenderers?.render?.filterSidebar?.renderTickButton
295
+ ) {
296
+ return settings.customRenderers.render.filterSidebar.renderTickButton(
297
+ {
298
+ onClick: () => handleSafeProcessCrop(completedCrop),
299
+ disabled: isLoading
300
+ }
301
+ );
302
+ }
303
+
304
+ const tickButtonClassName = twMerge(
305
+ `absolute z-10 bottom-3 right-3 p-2 rounded-full bg-white shadow-md w-10 h-10 text-green-600 ${
306
+ isLoading ? 'opacity-50 cursor-not-allowed' : ''
307
+ }`,
308
+ settings?.customStyles?.tickButton
309
+ );
310
+
311
+ return (
312
+ <Button
313
+ appearance="ghost"
314
+ size="sm"
315
+ onClick={() => {
316
+ if (!isLoading) {
317
+ handleSafeProcessCrop(completedCrop);
318
+ }
319
+ }}
320
+ disabled={isLoading}
321
+ className={tickButtonClassName}
322
+ >
323
+ {isLoading ? (
324
+ <LoaderSpinner className="w-5 h-5" />
325
+ ) : (
326
+ <svg
327
+ xmlns="http://www.w3.org/2000/svg"
328
+ width="20"
329
+ height="20"
330
+ viewBox="0 0 24 24"
331
+ fill="none"
332
+ stroke="currentColor"
333
+ strokeWidth="2"
334
+ strokeLinecap="round"
335
+ strokeLinejoin="round"
336
+ >
337
+ <polyline points="20,6 9,17 4,12"></polyline>
338
+ </svg>
339
+ )}
340
+ </Button>
341
+ );
342
+ };
343
+
344
+ return (
345
+ <div className={imageSectionClassName}>
346
+ <div className={imageContainerClassName}>
347
+ {renderCropButton()}
348
+ {renderTickButton()}
349
+
350
+ {(() => {
351
+ if (
352
+ settings?.customRenderers?.render?.filterSidebar
353
+ ?.renderImageContainer
354
+ ) {
355
+ return settings.customRenderers.render.filterSidebar.renderImageContainer(
356
+ {
357
+ imageUrl: currentImageUrl || '',
358
+ productName: product?.name || 'Product image',
359
+ isCropping
360
+ }
361
+ );
362
+ }
363
+
364
+ const imageWrapperClassName = twMerge(
365
+ 'w-full h-full flex items-center justify-center',
366
+ settings?.customStyles?.imageWrapper
367
+ );
368
+
369
+ return (
370
+ <div className={imageWrapperClassName}>
371
+ {isCropping ? (
372
+ <ReactCrop
373
+ crop={crop}
374
+ onChange={(newCrop) => {
375
+ if (!isLoading) {
376
+ setCrop(newCrop);
377
+ if (newCrop?.width > 10 && newCrop?.height > 10) {
378
+ setCompletedCrop(newCrop);
379
+ }
380
+ }
381
+ }}
382
+ onComplete={handleCropComplete}
383
+ onDragStart={() => {}}
384
+ onDragEnd={() => {}}
385
+ ruleOfThirds={false}
386
+ aspect={settings?.cropAspectRatio}
387
+ className="slider-crop"
388
+ disabled={isLoading}
389
+ keepSelection={true}
390
+ >
391
+ <img
392
+ ref={imageRef}
393
+ src={currentImageUrl || ''}
394
+ alt={product?.name || 'Product image'}
395
+ className="max-w-full max-h-[200px] md:max-h-[280px]"
396
+ style={{ transform: `scale(1) rotate(0deg)` }}
397
+ />
398
+ </ReactCrop>
399
+ ) : (
400
+ <div className="relative w-full h-full flex items-center justify-center">
401
+ <img
402
+ ref={imageRef}
403
+ src={currentImageUrl || ''}
404
+ alt={product?.name || 'Product image'}
405
+ className="max-w-full max-h-[200px] md:max-h-[280px] object-contain"
406
+ />
407
+ {!isCropping && completedCrop && (
408
+ <div className="hidden md:block absolute inset-0 bg-black bg-opacity-50 transition-opacity duration-300 ease-in-out">
409
+ <div
410
+ className="absolute transition-all duration-300 ease-in-out"
411
+ style={{
412
+ width: `${completedCrop.width}px`,
413
+ height: `${completedCrop.height}px`,
414
+ left: `${completedCrop.x}px`,
415
+ top: `${completedCrop.y}px`,
416
+ boxShadow: '0 0 0 9999px rgba(0, 0, 0, 0.5)',
417
+ border: '2px solid white'
418
+ }}
419
+ ></div>
420
+ </div>
421
+ )}
422
+ </div>
423
+ )}
424
+ </div>
425
+ );
426
+ })()}
427
+ </div>
428
+
429
+ {!isCropping && (
430
+ <div
431
+ className={twMerge(
432
+ 'flex flex-col md:flex-row justify-center mt-3 gap-2',
433
+ settings?.customStyles?.cropControls
434
+ )}
435
+ >
436
+ {settings?.enableFileUpload !== false && (
437
+ <>
438
+ <input
439
+ type="file"
440
+ accept="image/*"
441
+ ref={fileInputRef}
442
+ onChange={handleFileChangeWithCropReset}
443
+ className="hidden"
444
+ />
445
+ {(() => {
446
+ if (
447
+ settings?.customRenderers?.render?.filterSidebar
448
+ ?.renderUploadButton
449
+ ) {
450
+ return settings.customRenderers.render.filterSidebar.renderUploadButton(
451
+ {
452
+ onClick: handleNewImageClick,
453
+ disabled: isLoading
454
+ }
455
+ );
456
+ }
457
+
458
+ const uploadButtonClassName = twMerge(
459
+ `flex items-center gap-2 text-xs md:text-sm justify-center bg-gray-100 hover:bg-gray-200 border-gray-200 ${
460
+ isLoading ? 'opacity-50 cursor-not-allowed' : ''
461
+ }`,
462
+ settings?.customStyles?.uploadButton
463
+ );
464
+
465
+ return (
466
+ <Button
467
+ appearance="outlined"
468
+ size="sm"
469
+ onClick={handleNewImageClick}
470
+ disabled={isLoading}
471
+ className={uploadButtonClassName}
472
+ >
473
+ <svg
474
+ xmlns="http://www.w3.org/2000/svg"
475
+ width="20"
476
+ height="20"
477
+ viewBox="0 0 24 24"
478
+ fill="none"
479
+ stroke="currentColor"
480
+ strokeWidth="2"
481
+ strokeLinecap="round"
482
+ strokeLinejoin="round"
483
+ className="text-gray-500"
484
+ >
485
+ <path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"></path>
486
+ <circle cx="12" cy="13" r="4"></circle>
487
+ </svg>
488
+ {t('common.product.new_image')}
489
+ </Button>
490
+ );
491
+ })()}
492
+ </>
493
+ )}
494
+
495
+ {(() => {
496
+ if (
497
+ settings?.customRenderers?.render?.filterSidebar
498
+ ?.renderResetButton
499
+ ) {
500
+ return settings.customRenderers.render.filterSidebar.renderResetButton(
501
+ {
502
+ onClick: handleResetToOriginal,
503
+ disabled: isLoading,
504
+ showButton:
505
+ showResetButton && (hasUploadedImage || completedCrop)
506
+ }
507
+ );
508
+ }
509
+
510
+ if (!showResetButton || (!hasUploadedImage && !completedCrop))
511
+ return null;
512
+
513
+ const resetButtonClassName = twMerge(
514
+ `flex items-center gap-2 text-xs md:text-sm justify-center bg-blue-100 hover:bg-blue-200 border-blue-200 text-blue-600 ${
515
+ isLoading ? 'opacity-50 cursor-not-allowed' : ''
516
+ }`,
517
+ settings?.customStyles?.resetButton
518
+ );
519
+
520
+ return (
521
+ <Button
522
+ appearance="outlined"
523
+ size="sm"
524
+ onClick={handleResetToOriginal}
525
+ disabled={isLoading}
526
+ className={resetButtonClassName}
527
+ >
528
+ <svg
529
+ xmlns="http://www.w3.org/2000/svg"
530
+ width="20"
531
+ height="20"
532
+ viewBox="0 0 24 24"
533
+ fill="none"
534
+ stroke="currentColor"
535
+ strokeWidth="2"
536
+ strokeLinecap="round"
537
+ strokeLinejoin="round"
538
+ className="text-blue-600"
539
+ >
540
+ <path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"></path>
541
+ <path d="M21 3v5h-5"></path>
542
+ <path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"></path>
543
+ <path d="M3 21v-5h5"></path>
544
+ </svg>
545
+ {t('common.product.reset_to_original')}
546
+ </Button>
547
+ );
548
+ })()}
549
+ </div>
550
+ )}
551
+
552
+ {fileError &&
553
+ !isCropping &&
554
+ (() => {
555
+ if (
556
+ settings?.customRenderers?.render?.filterSidebar
557
+ ?.renderErrorMessage
558
+ ) {
559
+ return settings.customRenderers.render.filterSidebar.renderErrorMessage(
560
+ {
561
+ error: fileError
562
+ }
563
+ );
564
+ }
565
+
566
+ const errorClassName = twMerge(
567
+ 'mt-2 px-3 py-2 bg-red-50 border border-red-100 rounded-md',
568
+ settings?.customStyles?.errorMessage
569
+ );
570
+
571
+ return (
572
+ <div className={errorClassName}>
573
+ <p className="text-xs text-red-600 font-medium">
574
+ {fileError}
575
+ </p>
576
+ </div>
577
+ );
578
+ })()}
579
+ </div>
580
+ );
581
+ })()}
582
+
583
+ {/* Filters */}
584
+ <div className="space-y-2 md:space-y-4">
585
+ {searchResults?.facets
586
+ ?.filter((facet) => facet.key !== 'category_ids')
587
+ ?.map((facet) => {
588
+ if (
589
+ settings?.customRenderers?.render?.filterSidebar
590
+ ?.renderFilterGroup
591
+ ) {
592
+ return settings.customRenderers.render.filterSidebar.renderFilterGroup(
593
+ {
594
+ facet,
595
+ onFacetChange: handleFacetChange,
596
+ isLoading
597
+ }
598
+ );
599
+ }
600
+
601
+ const Component = getComponentByWidgetType(
602
+ facet.widget_type,
603
+ facet.key
604
+ );
605
+ const choices = facet.data.choices || [];
606
+
607
+ if (!Component) {
608
+ console.warn(
609
+ 'Component not found for widget type:',
610
+ facet.widget_type
611
+ );
612
+ return null;
613
+ }
614
+
615
+ const filterGroupClassName = twMerge(
616
+ 'filter-group',
617
+ settings?.customStyles?.filterGroup
618
+ );
619
+
620
+ const filterGroupContentClassName = twMerge(
621
+ clsx('flex gap-4', {
622
+ 'flex-wrap flex-row': facet.key === sizeKey,
623
+ 'flex-col': facet.key !== sizeKey
624
+ }),
625
+ settings?.customStyles?.filterGroupContent
626
+ );
627
+
628
+ const renderFilterGroupTitle = () => {
629
+ if (
630
+ settings?.customRenderers?.render?.filterSidebar
631
+ ?.renderFilterGroupTitle
632
+ ) {
633
+ return settings.customRenderers.render.filterSidebar.renderFilterGroupTitle(
634
+ {
635
+ title: facet.name,
636
+ isCollapsed: choices.some((choice) => choice.is_selected),
637
+ onToggle: () => {}
638
+ }
639
+ );
640
+ }
641
+ return null;
642
+ };
643
+
644
+ const renderFilterItem = (choice: any, index: number) => {
645
+ if (
646
+ settings?.customRenderers?.render?.filterSidebar
647
+ ?.renderFilterItem
648
+ ) {
649
+ return settings.customRenderers.render.filterSidebar.renderFilterItem(
650
+ {
651
+ choice,
652
+ facetKey: facet.key,
653
+ onFacetChange: handleFacetChange,
654
+ isLoading,
655
+ isSelected: choice.is_selected
656
+ }
657
+ );
658
+ }
659
+
660
+ const filterItemClassName = twMerge(
661
+ 'filter-item',
662
+ settings?.customStyles?.filterItem
663
+ );
664
+
665
+ const filterItemInputClassName = twMerge(
666
+ 'filter-item-input',
667
+ settings?.customStyles?.filterItemInput
668
+ );
669
+
670
+ const filterItemLabelClassName = twMerge(
671
+ 'filter-item-label',
672
+ settings?.customStyles?.filterItemLabel
673
+ );
674
+
675
+ const filterItemCountClassName = twMerge(
676
+ 'filter-item-count',
677
+ settings?.customStyles?.filterItemCount
678
+ );
679
+
680
+ return (
681
+ <div key={choice.value} className={filterItemClassName}>
682
+ <Component
683
+ key={choice.label}
684
+ value={choice.value}
685
+ label={choice.label}
686
+ name={facet.key}
687
+ onChange={() => {
688
+ if (!isLoading) {
689
+ handleFacetChange(facet.key, choice.value);
690
+ }
691
+ }}
692
+ onClick={() => {
693
+ if (!isLoading && facet.key === sizeKey) {
694
+ handleFacetChange(facet.key, choice.value);
695
+ }
696
+ }}
697
+ checked={choice.is_selected}
698
+ data-testid={`${choice.label.trim()}`}
699
+ disabled={isLoading}
700
+ className={filterItemInputClassName}
701
+ >
702
+ <span className={filterItemLabelClassName}>
703
+ {choice.label}
704
+ </span>{' '}
705
+ (
706
+ <span
707
+ data-testid={`filter-count-${facet.name.toLowerCase()}-${index}`}
708
+ className={filterItemCountClassName}
709
+ >
710
+ {choice.quantity}
711
+ </span>
712
+ )
713
+ </Component>
714
+ </div>
715
+ );
716
+ };
717
+
718
+ return (
719
+ <div key={facet.key} className={filterGroupClassName}>
720
+ <Accordion
721
+ title={renderFilterGroupTitle() || facet.name}
722
+ isCollapse={choices.some((choice) => choice.is_selected)}
723
+ dataTestId={`filter-${facet.name}`}
724
+ >
725
+ <div className={filterGroupContentClassName}>
726
+ {choices
727
+ .slice(0, 10)
728
+ .map((choice, index) => renderFilterItem(choice, index))}
729
+ </div>
730
+ </Accordion>
731
+ </div>
732
+ );
733
+ })}
734
+ </div>
735
+
736
+ {/* Mobile Active Filters and Clear Button */}
737
+ {(() => {
738
+ const hasActiveFilters = searchResults?.facets
739
+ ?.filter((facet) => facet.key !== 'category_ids')
740
+ ?.some((facet) =>
741
+ facet.data.choices?.some((choice) => choice.is_selected)
742
+ );
743
+
744
+ if (!hasActiveFilters) return null;
745
+
746
+ const activeFilters = searchResults.facets
747
+ .filter((facet) => facet.key !== 'category_ids')
748
+ .flatMap(
749
+ (facet) =>
750
+ facet.data.choices
751
+ ?.filter((choice) => choice.is_selected)
752
+ .map((choice) => ({
753
+ key: facet.key,
754
+ value: choice.value.toString(),
755
+ label: choice.label
756
+ })) || []
757
+ );
758
+
759
+ const handleClearAll = () => {
760
+ if (!isLoading) {
761
+ const facetsToRemove = [];
762
+ searchResults?.facets
763
+ ?.filter((facet) => facet.key !== 'category_ids')
764
+ ?.forEach((facet) => {
765
+ facet.data.choices
766
+ ?.filter((choice) => choice.is_selected)
767
+ .forEach((choice) => {
768
+ facetsToRemove.push({
769
+ facetKey: facet.key,
770
+ choiceValue: choice.value
771
+ });
772
+ });
773
+ });
774
+
775
+ facetsToRemove.forEach(({ facetKey, choiceValue }) => {
776
+ removeFacetFilter(facetKey, choiceValue);
777
+ });
778
+ }
779
+ };
780
+
781
+ if (
782
+ settings?.customRenderers?.render?.filterSidebar
783
+ ?.renderMobileActiveFilters
784
+ ) {
785
+ return (
786
+ <div className="md:hidden mt-6">
787
+ {settings.customRenderers.render.filterSidebar.renderMobileActiveFilters(
788
+ {
789
+ filters: activeFilters,
790
+ onRemove: handleFacetChange,
791
+ onClearAll: handleClearAll,
792
+ isLoading
793
+ }
794
+ )}
795
+ </div>
796
+ );
797
+ }
798
+
799
+ const mobileActiveFiltersClassName = twMerge(
800
+ 'md:hidden mt-6',
801
+ settings?.customStyles?.mobileActiveFilters
802
+ );
803
+
804
+ const mobileActiveFilterTagClassName = twMerge(
805
+ 'flex items-center gap-1 px-2 py-1 bg-gray-100 rounded-full text-xs',
806
+ settings?.customStyles?.mobileActiveFilterTag
807
+ );
808
+
809
+ const mobileClearAllButtonClassName = twMerge(
810
+ 'w-full',
811
+ settings?.customStyles?.mobileClearAllButton
812
+ );
813
+
814
+ return (
815
+ <div className={mobileActiveFiltersClassName}>
816
+ <div className="mb-4">
817
+ <h4 className="text-sm font-medium mb-2">
818
+ {t('common.product.active_filters')}
819
+ </h4>
820
+ <div className="flex flex-wrap gap-2">
821
+ {activeFilters.map((filter) => (
822
+ <div
823
+ key={`${filter.key}-${filter.value}`}
824
+ className={mobileActiveFilterTagClassName}
825
+ >
826
+ <span>{filter.label}</span>
827
+ <Button
828
+ appearance="ghost"
829
+ size="sm"
830
+ onClick={() => {
831
+ if (!isLoading) {
832
+ handleFacetChange(filter.key, filter.value);
833
+ }
834
+ }}
835
+ disabled={isLoading}
836
+ className="hover:bg-gray-200 rounded-full p-0 w-5 h-5 disabled:opacity-50 disabled:cursor-not-allowed ml-1"
837
+ >
838
+ <Icon name="close" size={10} />
839
+ </Button>
840
+ </div>
841
+ ))}
842
+ </div>
843
+ </div>
844
+
845
+ <Button
846
+ appearance="outlined"
847
+ className={mobileClearAllButtonClassName}
848
+ onClick={handleClearAll}
849
+ disabled={isLoading}
850
+ >
851
+ {t('common.product.clear_all_filters')}
852
+ </Button>
853
+ </div>
854
+ );
855
+ })()}
856
+ </div>
857
+ );
858
+ }