@akinon/pz-similar-products 1.92.0-snapshot-ZERO-3457-20250627111231 → 1.93.0-rc.46

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.
@@ -5,7 +5,8 @@ import {
5
5
  Button,
6
6
  Icon,
7
7
  Accordion,
8
- LoaderSpinner
8
+ LoaderSpinner,
9
+ Input
9
10
  } from '@akinon/next/components';
10
11
  import { useLocalization } from '@akinon/next/hooks';
11
12
  import { FilterSidebarProps } from '../types';
@@ -55,6 +56,8 @@ export function SimilarProductsFilterSidebar({
55
56
  isLoading,
56
57
  handleFacetChange,
57
58
  removeFacetFilter,
59
+ searchText,
60
+ setSearchText,
58
61
  currentImageUrl,
59
62
  isCropping,
60
63
  imageRef,
@@ -166,743 +169,808 @@ export function SimilarProductsFilterSidebar({
166
169
  className
167
170
  );
168
171
 
169
- return (
170
- <div
171
- className={sidebarClassName}
172
- style={{
173
- maxHeight: 'calc(100vh - 120px)'
174
- }}
175
- >
176
- {(() => {
177
- if (
178
- settings?.customRenderers?.render?.filterSidebar?.renderMobileHeader
179
- ) {
180
- return settings.customRenderers.render.filterSidebar.renderMobileHeader(
181
- {
182
- title: t('common.product.filters'),
183
- itemCount: searchResults?.pagination?.total_count || 0,
184
- onClose: () => setIsFilterMenuOpen(false)
185
- }
186
- );
187
- }
172
+ const modalCloseIconClassName = twMerge(
173
+ '',
174
+ settings?.customStyles?.modalCloseIcon
175
+ );
188
176
 
189
- const mobileHeaderClassName = twMerge(
190
- 'flex justify-between mb-6 md:hidden',
191
- settings?.customStyles?.filterSidebarMobileHeader
192
- );
177
+ const filterRemoveIconClassName = twMerge(
178
+ '',
179
+ settings?.customStyles?.filterRemoveIcon
180
+ );
193
181
 
194
- return (
195
- <>
196
- <div className={mobileHeaderClassName}>
197
- <h3
198
- className={twMerge(
199
- 'text-2xl font-bold',
200
- settings?.customStyles?.filterSidebarMobileTitle
201
- )}
202
- >
203
- {t('common.product.filters')}
204
- </h3>
205
- <Button
206
- appearance="ghost"
207
- size="sm"
208
- onClick={() => setIsFilterMenuOpen(false)}
209
- className={twMerge(
210
- 'hover:bg-gray-200 rounded-full p-0 w-6 h-6',
211
- settings?.customStyles?.filterSidebarMobileCloseButton
212
- )}
213
- >
214
- <Icon name="close" size={16} />
215
- </Button>
216
- </div>
217
- <div
218
- className={twMerge(
219
- 'flex justify-between items-center mb-6 md:hidden',
220
- settings?.customStyles?.filterSidebarMobileCounter
221
- )}
222
- >
223
- <span
224
- className={twMerge(
225
- 'text-sm',
226
- settings?.customStyles?.filterSidebarMobileCounterText
227
- )}
228
- >
229
- {searchResults?.pagination?.total_count || 0} items
230
- </span>
231
- </div>
232
- </>
233
- );
234
- })()}
235
-
236
- {(() => {
237
- if (
238
- settings?.customRenderers?.render?.filterSidebar?.renderImageSection
239
- ) {
240
- return settings.customRenderers.render.filterSidebar.renderImageSection(
241
- {
242
- currentImageUrl,
243
- isCropping,
244
- onToggleCrop: handleToggleCropMode,
245
- onFileUpload: handleFileChangeWithCropReset,
246
- onResetToOriginal: handleResetToOriginal,
247
- fileError,
248
- isLoading
249
- }
250
- );
251
- }
252
-
253
- const imageSectionClassName = twMerge(
254
- 'relative mb-2 md:mb-6 mt-4',
255
- settings?.customStyles?.imageSection
256
- );
257
-
258
- const imageContainerClassName = twMerge(
259
- 'relative bg-white overflow-hidden flex items-center justify-center transition-all duration-300 ease-in-out',
260
- isCropping ? 'border-2 border-dashed border-gray-300' : '',
261
- settings?.customStyles?.imageContainer
262
- );
263
-
264
- const cropButtonClassName = twMerge(
265
- `absolute z-10 bottom-3 left-3 p-2 rounded-full bg-white shadow-md w-10 h-10 ${
266
- isCropping ? 'text-red-500' : 'text-gray-700'
267
- } ${isLoading ? 'opacity-50 cursor-not-allowed' : ''}`,
268
- settings?.customStyles?.cropButton
269
- );
270
-
271
- const renderCropButton = () => {
272
- if (!settings?.enableCropping || settings.enableCropping === false)
273
- return null;
182
+ const modalCloseIconName = settings?.iconNames?.modalClose || 'close';
183
+ const filterRemoveIconName = settings?.iconNames?.filterRemove || 'close';
274
184
 
185
+ return (
186
+ <>
187
+ {isFilterMenuOpen && (
188
+ <div
189
+ className={twMerge(
190
+ 'fixed inset-0 bg-black bg-opacity-50 z-40 md:hidden',
191
+ settings?.customStyles?.filterSidebarMobileOverlay
192
+ )}
193
+ onClick={() => setIsFilterMenuOpen(false)}
194
+ />
195
+ )}
196
+
197
+ <div
198
+ className={sidebarClassName}
199
+ style={{
200
+ maxHeight: 'calc(100vh - 120px)'
201
+ }}
202
+ >
203
+ {(() => {
275
204
  if (
276
- settings?.customRenderers?.render?.filterSidebar?.renderCropButton
205
+ settings?.customRenderers?.render?.filterSidebar?.renderMobileHeader
277
206
  ) {
278
- return settings.customRenderers.render.filterSidebar.renderCropButton(
207
+ return settings.customRenderers.render.filterSidebar.renderMobileHeader(
279
208
  {
280
- isCropping,
281
- onClick: handleToggleCropMode,
282
- disabled: isLoading
209
+ title: t('common.product.filters'),
210
+ itemCount: searchResults?.pagination?.total_count || 0,
211
+ onClose: () => setIsFilterMenuOpen(false)
283
212
  }
284
213
  );
285
214
  }
286
215
 
216
+ const mobileHeaderClassName = twMerge(
217
+ 'flex justify-between mb-6 md:hidden',
218
+ settings?.customStyles?.filterSidebarMobileHeader
219
+ );
220
+
287
221
  return (
288
- <Button
289
- appearance="ghost"
290
- size="sm"
291
- onClick={handleToggleCropMode}
292
- disabled={isLoading}
293
- className={cropButtonClassName}
294
- >
295
- {isLoading ? (
296
- <LoaderSpinner className="w-5 h-5" />
297
- ) : isCropping ? (
298
- <svg
299
- xmlns="http://www.w3.org/2000/svg"
300
- width="20"
301
- height="20"
302
- viewBox="0 0 24 24"
303
- fill="none"
304
- stroke="currentColor"
305
- strokeWidth="2"
306
- strokeLinecap="round"
307
- strokeLinejoin="round"
222
+ <>
223
+ <div className={mobileHeaderClassName}>
224
+ <h3
225
+ className={twMerge(
226
+ 'text-2xl font-bold',
227
+ settings?.customStyles?.filterSidebarMobileTitle
228
+ )}
308
229
  >
309
- <line x1="18" y1="6" x2="6" y2="18"></line>
310
- <line x1="6" y1="6" x2="18" y2="18"></line>
311
- </svg>
312
- ) : (
313
- <svg
314
- xmlns="http://www.w3.org/2000/svg"
315
- width="20"
316
- height="20"
317
- viewBox="0 0 24 24"
318
- fill="none"
319
- stroke="currentColor"
320
- strokeWidth="2"
321
- strokeLinecap="round"
322
- strokeLinejoin="round"
230
+ {t('common.product.filters')}
231
+ </h3>
232
+ <Button
233
+ appearance="ghost"
234
+ size="sm"
235
+ onClick={() => setIsFilterMenuOpen(false)}
236
+ className={twMerge(
237
+ 'hover:bg-gray-200 rounded-full p-0 w-6 h-6',
238
+ settings?.customStyles?.filterSidebarMobileCloseButton
239
+ )}
323
240
  >
324
- <path d="M6 2v14a2 2 0 0 0 2 2h14"></path>
325
- <path d="M18 22V8a2 2 0 0 0-2-2H2"></path>
326
- </svg>
327
- )}
328
- </Button>
241
+ <Icon
242
+ name={modalCloseIconName}
243
+ size={16}
244
+ className={modalCloseIconClassName}
245
+ />
246
+ </Button>
247
+ </div>
248
+ <div
249
+ className={twMerge(
250
+ 'flex justify-between items-center mb-6 md:hidden',
251
+ settings?.customStyles?.filterSidebarMobileCounter
252
+ )}
253
+ >
254
+ <span
255
+ className={twMerge(
256
+ 'text-sm',
257
+ settings?.customStyles?.filterSidebarMobileCounterText
258
+ )}
259
+ >
260
+ {searchResults?.pagination?.total_count || 0} items
261
+ </span>
262
+ </div>
263
+ </>
329
264
  );
330
- };
265
+ })()}
331
266
 
332
- const renderTickButton = () => {
333
- if (!settings?.enableCropping || settings.enableCropping === false)
334
- return null;
267
+ {(() => {
335
268
  if (
336
- !isCropping ||
337
- !completedCrop ||
338
- completedCrop.width <= 10 ||
339
- completedCrop.height <= 10
340
- )
341
- return null;
342
-
343
- if (
344
- settings?.customRenderers?.render?.filterSidebar?.renderTickButton
269
+ settings?.customRenderers?.render?.filterSidebar?.renderImageSection
345
270
  ) {
346
- return settings.customRenderers.render.filterSidebar.renderTickButton(
271
+ return settings.customRenderers.render.filterSidebar.renderImageSection(
347
272
  {
348
- onClick: () => handleSafeProcessCrop(completedCrop),
349
- disabled: isLoading
273
+ currentImageUrl,
274
+ isCropping,
275
+ onToggleCrop: handleToggleCropMode,
276
+ onFileUpload: handleFileChangeWithCropReset,
277
+ onResetToOriginal: handleResetToOriginal,
278
+ fileError,
279
+ isLoading
350
280
  }
351
281
  );
352
282
  }
353
283
 
354
- const tickButtonClassName = twMerge(
355
- `absolute z-10 bottom-3 right-3 p-2 rounded-full bg-white shadow-md w-10 h-10 text-green-600 ${
356
- isLoading ? 'opacity-50 cursor-not-allowed' : ''
357
- }`,
358
- settings?.customStyles?.tickButton
284
+ const imageSectionClassName = twMerge(
285
+ 'relative mb-2 md:mb-6 mt-4',
286
+ settings?.customStyles?.imageSection
359
287
  );
360
288
 
361
- return (
362
- <Button
363
- appearance="ghost"
364
- size="sm"
365
- onClick={() => {
366
- if (!isLoading) {
367
- handleSafeProcessCrop(completedCrop);
368
- }
369
- }}
370
- disabled={isLoading}
371
- className={tickButtonClassName}
372
- >
373
- {isLoading ? (
374
- <LoaderSpinner className="w-5 h-5" />
375
- ) : (
376
- <svg
377
- xmlns="http://www.w3.org/2000/svg"
378
- width="20"
379
- height="20"
380
- viewBox="0 0 24 24"
381
- fill="none"
382
- stroke="currentColor"
383
- strokeWidth="2"
384
- strokeLinecap="round"
385
- strokeLinejoin="round"
386
- >
387
- <polyline points="20,6 9,17 4,12"></polyline>
388
- </svg>
389
- )}
390
- </Button>
289
+ const imageContainerClassName = twMerge(
290
+ 'relative bg-white overflow-hidden flex items-center justify-center transition-all duration-300 ease-in-out',
291
+ isCropping ? 'border-2 border-dashed border-gray-300' : '',
292
+ settings?.customStyles?.imageContainer
391
293
  );
392
- };
393
294
 
394
- return (
395
- <div className={imageSectionClassName}>
396
- <div className={imageContainerClassName}>
397
- {renderCropButton()}
398
- {renderTickButton()}
295
+ const cropButtonClassName = twMerge(
296
+ `absolute z-10 bottom-3 left-3 p-2 rounded-full bg-white shadow-md w-10 h-10 ${
297
+ isCropping ? 'text-red-500' : 'text-gray-700'
298
+ } ${isLoading ? 'opacity-50 cursor-not-allowed' : ''}`,
299
+ settings?.customStyles?.cropButton
300
+ );
399
301
 
400
- {(() => {
401
- if (
402
- settings?.customRenderers?.render?.filterSidebar
403
- ?.renderImageContainer
404
- ) {
405
- return settings.customRenderers.render.filterSidebar.renderImageContainer(
406
- {
407
- imageUrl: currentImageUrl || '',
408
- productName: product?.name || 'Product image',
409
- isCropping
410
- }
411
- );
302
+ const renderCropButton = () => {
303
+ if (!settings?.enableCropping || settings.enableCropping === false)
304
+ return null;
305
+
306
+ if (
307
+ settings?.customRenderers?.render?.filterSidebar?.renderCropButton
308
+ ) {
309
+ return settings.customRenderers.render.filterSidebar.renderCropButton(
310
+ {
311
+ isCropping,
312
+ onClick: handleToggleCropMode,
313
+ disabled: isLoading
412
314
  }
315
+ );
316
+ }
413
317
 
414
- const imageWrapperClassName = twMerge(
415
- 'w-full h-full flex items-center justify-center',
416
- settings?.customStyles?.imageWrapper
417
- );
318
+ return (
319
+ <Button
320
+ appearance="ghost"
321
+ size="sm"
322
+ onClick={handleToggleCropMode}
323
+ disabled={isLoading}
324
+ className={cropButtonClassName}
325
+ >
326
+ {isLoading ? (
327
+ <LoaderSpinner className="w-5 h-5" />
328
+ ) : isCropping ? (
329
+ <svg
330
+ xmlns="http://www.w3.org/2000/svg"
331
+ width="20"
332
+ height="20"
333
+ viewBox="0 0 24 24"
334
+ fill="none"
335
+ stroke="currentColor"
336
+ strokeWidth="2"
337
+ strokeLinecap="round"
338
+ strokeLinejoin="round"
339
+ >
340
+ <line x1="18" y1="6" x2="6" y2="18"></line>
341
+ <line x1="6" y1="6" x2="18" y2="18"></line>
342
+ </svg>
343
+ ) : (
344
+ <svg
345
+ xmlns="http://www.w3.org/2000/svg"
346
+ width="20"
347
+ height="20"
348
+ viewBox="0 0 24 24"
349
+ fill="none"
350
+ stroke="currentColor"
351
+ strokeWidth="2"
352
+ strokeLinecap="round"
353
+ strokeLinejoin="round"
354
+ >
355
+ <path d="M6 2v14a2 2 0 0 0 2 2h14"></path>
356
+ <path d="M18 22V8a2 2 0 0 0-2-2H2"></path>
357
+ </svg>
358
+ )}
359
+ </Button>
360
+ );
361
+ };
418
362
 
419
- return (
420
- <div className={imageWrapperClassName}>
421
- {isCropping ? (
422
- <ReactCrop
423
- crop={crop}
424
- onChange={(newCrop) => {
425
- if (!isLoading) {
426
- setCrop(newCrop);
427
- if (newCrop?.width > 10 && newCrop?.height > 10) {
428
- setCompletedCrop(newCrop);
429
- }
430
- }
431
- }}
432
- onComplete={handleCropComplete}
433
- onDragStart={() => {}}
434
- onDragEnd={() => {}}
435
- ruleOfThirds={false}
436
- aspect={settings?.cropAspectRatio}
437
- className="slider-crop"
438
- disabled={isLoading}
439
- keepSelection={true}
440
- >
441
- <img
442
- ref={imageRef}
443
- src={currentImageUrl || ''}
444
- alt={product?.name || 'Product image'}
445
- className="max-w-full max-h-[200px] md:max-h-[280px]"
446
- style={{ transform: `scale(1) rotate(0deg)` }}
447
- />
448
- </ReactCrop>
449
- ) : (
450
- <div className="relative w-full h-full flex items-center justify-center">
451
- <img
452
- ref={imageRef}
453
- src={currentImageUrl || ''}
454
- alt={product?.name || 'Product image'}
455
- className="max-w-full max-h-[200px] md:max-h-[280px] object-contain"
456
- />
457
- {!isCropping && completedCrop && (
458
- <div className="hidden md:block absolute inset-0 bg-black bg-opacity-50 transition-opacity duration-300 ease-in-out">
459
- <div
460
- className="absolute transition-all duration-300 ease-in-out"
461
- style={{
462
- width: `${completedCrop.width}px`,
463
- height: `${completedCrop.height}px`,
464
- left: `${completedCrop.x}px`,
465
- top: `${completedCrop.y}px`,
466
- boxShadow: '0 0 0 9999px rgba(0, 0, 0, 0.5)',
467
- border: '2px solid white'
468
- }}
469
- ></div>
470
- </div>
471
- )}
472
- </div>
473
- )}
474
- </div>
475
- );
476
- })()}
477
- </div>
363
+ const renderTickButton = () => {
364
+ if (!settings?.enableCropping || settings.enableCropping === false)
365
+ return null;
366
+ if (
367
+ !isCropping ||
368
+ !completedCrop ||
369
+ completedCrop.width <= 10 ||
370
+ completedCrop.height <= 10
371
+ )
372
+ return null;
478
373
 
479
- {!isCropping && (
480
- <div
481
- className={twMerge(
482
- 'flex flex-col md:flex-row justify-center mt-3 gap-2',
483
- settings?.customStyles?.cropControls
484
- )}
485
- >
486
- {settings?.enableFileUpload !== false && (
487
- <>
488
- <input
489
- type="file"
490
- accept="image/*"
491
- ref={fileInputRef}
492
- onChange={handleFileChangeWithCropReset}
493
- className="hidden"
494
- />
495
- {(() => {
496
- if (
497
- settings?.customRenderers?.render?.filterSidebar
498
- ?.renderUploadButton
499
- ) {
500
- return settings.customRenderers.render.filterSidebar.renderUploadButton(
501
- {
502
- onClick: handleNewImageClick,
503
- disabled: isLoading
504
- }
505
- );
506
- }
374
+ if (
375
+ settings?.customRenderers?.render?.filterSidebar?.renderTickButton
376
+ ) {
377
+ return settings.customRenderers.render.filterSidebar.renderTickButton(
378
+ {
379
+ onClick: () => handleSafeProcessCrop(completedCrop),
380
+ disabled: isLoading
381
+ }
382
+ );
383
+ }
507
384
 
508
- const uploadButtonClassName = twMerge(
509
- `flex items-center gap-2 text-xs md:text-sm justify-center bg-gray-100 hover:bg-gray-200 border-gray-200 ${
510
- isLoading ? 'opacity-50 cursor-not-allowed' : ''
511
- }`,
512
- settings?.customStyles?.uploadButton
513
- );
385
+ const tickButtonClassName = twMerge(
386
+ `absolute z-10 bottom-3 right-3 p-2 rounded-full bg-white shadow-md w-10 h-10 text-green-600 ${
387
+ isLoading ? 'opacity-50 cursor-not-allowed' : ''
388
+ }`,
389
+ settings?.customStyles?.tickButton
390
+ );
514
391
 
515
- return (
516
- <Button
517
- appearance="outlined"
518
- size="sm"
519
- onClick={handleNewImageClick}
520
- disabled={isLoading}
521
- className={uploadButtonClassName}
522
- >
523
- <svg
524
- xmlns="http://www.w3.org/2000/svg"
525
- width="20"
526
- height="20"
527
- viewBox="0 0 24 24"
528
- fill="none"
529
- stroke="currentColor"
530
- strokeWidth="2"
531
- strokeLinecap="round"
532
- strokeLinejoin="round"
533
- className="text-gray-500"
534
- >
535
- <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>
536
- <circle cx="12" cy="13" r="4"></circle>
537
- </svg>
538
- {t('common.product.new_image')}
539
- </Button>
540
- );
541
- })()}
542
- </>
392
+ return (
393
+ <Button
394
+ appearance="ghost"
395
+ size="sm"
396
+ onClick={() => {
397
+ if (!isLoading) {
398
+ handleSafeProcessCrop(completedCrop);
399
+ }
400
+ }}
401
+ disabled={isLoading}
402
+ className={tickButtonClassName}
403
+ >
404
+ {isLoading ? (
405
+ <LoaderSpinner className="w-5 h-5" />
406
+ ) : (
407
+ <svg
408
+ xmlns="http://www.w3.org/2000/svg"
409
+ width="20"
410
+ height="20"
411
+ viewBox="0 0 24 24"
412
+ fill="none"
413
+ stroke="currentColor"
414
+ strokeWidth="2"
415
+ strokeLinecap="round"
416
+ strokeLinejoin="round"
417
+ >
418
+ <polyline points="20,6 9,17 4,12"></polyline>
419
+ </svg>
543
420
  )}
421
+ </Button>
422
+ );
423
+ };
424
+
425
+ return (
426
+ <div className={imageSectionClassName}>
427
+ <div className={imageContainerClassName}>
428
+ {renderCropButton()}
429
+ {renderTickButton()}
544
430
 
545
431
  {(() => {
546
432
  if (
547
433
  settings?.customRenderers?.render?.filterSidebar
548
- ?.renderResetButton
434
+ ?.renderImageContainer
549
435
  ) {
550
- return settings.customRenderers.render.filterSidebar.renderResetButton(
436
+ return settings.customRenderers.render.filterSidebar.renderImageContainer(
551
437
  {
552
- onClick: handleResetToOriginal,
553
- disabled: isLoading,
554
- showButton:
555
- showResetButton && (hasUploadedImage || completedCrop)
438
+ imageUrl: currentImageUrl || '',
439
+ productName: product?.name || 'Product image',
440
+ isCropping
556
441
  }
557
442
  );
558
443
  }
559
444
 
560
- if (!showResetButton || (!hasUploadedImage && !completedCrop))
561
- return null;
562
-
563
- const resetButtonClassName = twMerge(
564
- `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 ${
565
- isLoading ? 'opacity-50 cursor-not-allowed' : ''
566
- }`,
567
- settings?.customStyles?.resetButton
445
+ const imageWrapperClassName = twMerge(
446
+ 'w-full h-full flex items-center justify-center',
447
+ settings?.customStyles?.imageWrapper
568
448
  );
569
449
 
570
450
  return (
571
- <Button
572
- appearance="outlined"
573
- size="sm"
574
- onClick={handleResetToOriginal}
575
- disabled={isLoading}
576
- className={resetButtonClassName}
577
- >
578
- <svg
579
- xmlns="http://www.w3.org/2000/svg"
580
- width="20"
581
- height="20"
582
- viewBox="0 0 24 24"
583
- fill="none"
584
- stroke="currentColor"
585
- strokeWidth="2"
586
- strokeLinecap="round"
587
- strokeLinejoin="round"
588
- className="text-blue-600"
589
- >
590
- <path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"></path>
591
- <path d="M21 3v5h-5"></path>
592
- <path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"></path>
593
- <path d="M3 21v-5h5"></path>
594
- </svg>
595
- {t('common.product.reset_to_original')}
596
- </Button>
451
+ <div className={imageWrapperClassName}>
452
+ {isCropping ? (
453
+ <ReactCrop
454
+ crop={crop}
455
+ onChange={(newCrop) => {
456
+ if (!isLoading) {
457
+ setCrop(newCrop);
458
+ if (newCrop?.width > 10 && newCrop?.height > 10) {
459
+ setCompletedCrop(newCrop);
460
+ }
461
+ }
462
+ }}
463
+ onComplete={handleCropComplete}
464
+ onDragStart={() => {}}
465
+ onDragEnd={() => {}}
466
+ ruleOfThirds={false}
467
+ aspect={settings?.cropAspectRatio}
468
+ className={twMerge(
469
+ 'slider-crop',
470
+ settings?.customStyles?.cropComponent
471
+ )}
472
+ disabled={isLoading}
473
+ keepSelection={true}
474
+ >
475
+ <img
476
+ ref={imageRef}
477
+ src={currentImageUrl || ''}
478
+ alt={product?.name || 'Product image'}
479
+ className={twMerge(
480
+ 'max-w-full max-h-[200px] md:max-h-[280px]',
481
+ settings?.customStyles?.cropImage,
482
+ settings?.customStyles?.cropImageActive
483
+ )}
484
+ style={{ transform: `scale(1) rotate(0deg)` }}
485
+ />
486
+ </ReactCrop>
487
+ ) : (
488
+ <div
489
+ className={twMerge(
490
+ 'relative w-full h-full flex items-center justify-center',
491
+ settings?.customStyles?.cropImageContainer
492
+ )}
493
+ >
494
+ <img
495
+ ref={imageRef}
496
+ src={currentImageUrl || ''}
497
+ alt={product?.name || 'Product image'}
498
+ className={twMerge(
499
+ 'max-w-full max-h-[200px] md:max-h-[280px] object-contain',
500
+ settings?.customStyles?.cropImage,
501
+ settings?.customStyles?.cropImageNonCropping
502
+ )}
503
+ />
504
+ {!isCropping && completedCrop && (
505
+ <div
506
+ className={twMerge(
507
+ 'hidden md:block absolute inset-0 bg-black bg-opacity-50 transition-opacity duration-300 ease-in-out',
508
+ settings?.customStyles?.cropOverlay,
509
+ settings?.customStyles?.cropOverlayBackground
510
+ )}
511
+ >
512
+ <div
513
+ className={twMerge(
514
+ 'absolute transition-all duration-300 ease-in-out',
515
+ settings?.customStyles?.cropSelection,
516
+ settings?.customStyles?.cropSelectionHighlight
517
+ )}
518
+ style={{
519
+ width: `${completedCrop.width}px`,
520
+ height: `${completedCrop.height}px`,
521
+ left: `${completedCrop.x}px`,
522
+ top: `${completedCrop.y}px`,
523
+ boxShadow: '0 0 0 9999px rgba(0, 0, 0, 0.5)',
524
+ border: '2px solid white'
525
+ }}
526
+ ></div>
527
+ </div>
528
+ )}
529
+ </div>
530
+ )}
531
+ </div>
597
532
  );
598
533
  })()}
599
534
  </div>
600
- )}
601
535
 
602
- {fileError &&
603
- !isCropping &&
604
- (() => {
605
- if (
606
- settings?.customRenderers?.render?.filterSidebar
607
- ?.renderErrorMessage
608
- ) {
609
- return settings.customRenderers.render.filterSidebar.renderErrorMessage(
610
- {
611
- error: fileError
536
+ {!isCropping && (
537
+ <div
538
+ className={twMerge(
539
+ 'flex flex-col md:flex-row justify-center mt-3 gap-2',
540
+ settings?.customStyles?.cropControls
541
+ )}
542
+ >
543
+ {settings?.enableFileUpload !== false && (
544
+ <>
545
+ <input
546
+ type="file"
547
+ accept="image/*"
548
+ ref={fileInputRef}
549
+ onChange={handleFileChangeWithCropReset}
550
+ className="hidden"
551
+ />
552
+ {(() => {
553
+ if (
554
+ settings?.customRenderers?.render?.filterSidebar
555
+ ?.renderUploadButton
556
+ ) {
557
+ return settings.customRenderers.render.filterSidebar.renderUploadButton(
558
+ {
559
+ onClick: handleNewImageClick,
560
+ disabled: isLoading
561
+ }
562
+ );
563
+ }
564
+
565
+ const uploadButtonClassName = twMerge(
566
+ `flex items-center gap-2 text-xs md:text-sm justify-center bg-gray-100 hover:bg-gray-200 border-gray-200 ${
567
+ isLoading ? 'opacity-50 cursor-not-allowed' : ''
568
+ }`,
569
+ settings?.customStyles?.uploadButton
570
+ );
571
+
572
+ return (
573
+ <Button
574
+ appearance="outlined"
575
+ size="sm"
576
+ onClick={handleNewImageClick}
577
+ disabled={isLoading}
578
+ className={uploadButtonClassName}
579
+ >
580
+ <svg
581
+ xmlns="http://www.w3.org/2000/svg"
582
+ width="20"
583
+ height="20"
584
+ viewBox="0 0 24 24"
585
+ fill="none"
586
+ stroke="currentColor"
587
+ strokeWidth="2"
588
+ strokeLinecap="round"
589
+ strokeLinejoin="round"
590
+ className="text-gray-500"
591
+ >
592
+ <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>
593
+ <circle cx="12" cy="13" r="4"></circle>
594
+ </svg>
595
+ {t('common.product.new_image')}
596
+ </Button>
597
+ );
598
+ })()}
599
+ </>
600
+ )}
601
+
602
+ {(() => {
603
+ if (
604
+ settings?.customRenderers?.render?.filterSidebar
605
+ ?.renderResetButton
606
+ ) {
607
+ return settings.customRenderers.render.filterSidebar.renderResetButton(
608
+ {
609
+ onClick: handleResetToOriginal,
610
+ disabled: isLoading,
611
+ showButton:
612
+ showResetButton &&
613
+ (hasUploadedImage || completedCrop)
614
+ }
615
+ );
612
616
  }
613
- );
614
- }
615
617
 
616
- const errorClassName = twMerge(
617
- 'mt-2 px-3 py-2 bg-red-50 border border-red-100 rounded-md',
618
- settings?.customStyles?.errorMessage
619
- );
618
+ if (
619
+ !showResetButton ||
620
+ (!hasUploadedImage && !completedCrop)
621
+ )
622
+ return null;
620
623
 
621
- return (
622
- <div className={errorClassName}>
623
- <p className="text-xs text-red-600 font-medium">
624
- {fileError}
625
- </p>
626
- </div>
627
- );
628
- })()}
629
- </div>
630
- );
631
- })()}
632
-
633
- {/* Filters */}
634
- <div className="space-y-2 md:space-y-4">
635
- {searchResults?.facets
636
- ?.filter((facet) => facet.key !== 'category_ids')
637
- ?.map((facet) => {
638
- if (
639
- settings?.customRenderers?.render?.filterSidebar
640
- ?.renderFilterGroup
641
- ) {
642
- return settings.customRenderers.render.filterSidebar.renderFilterGroup(
643
- {
644
- facet,
645
- onFacetChange: handleFacetChange,
646
- isLoading
647
- }
648
- );
649
- }
624
+ const resetButtonClassName = twMerge(
625
+ `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 ${
626
+ isLoading ? 'opacity-50 cursor-not-allowed' : ''
627
+ }`,
628
+ settings?.customStyles?.resetButton
629
+ );
650
630
 
651
- const Component = getComponentByWidgetType(
652
- facet.widget_type,
653
- facet.key
654
- );
655
- const choices = facet.data.choices || [];
631
+ return (
632
+ <Button
633
+ appearance="outlined"
634
+ size="sm"
635
+ onClick={handleResetToOriginal}
636
+ disabled={isLoading}
637
+ className={resetButtonClassName}
638
+ >
639
+ <svg
640
+ xmlns="http://www.w3.org/2000/svg"
641
+ width="20"
642
+ height="20"
643
+ viewBox="0 0 24 24"
644
+ fill="none"
645
+ stroke="currentColor"
646
+ strokeWidth="2"
647
+ strokeLinecap="round"
648
+ strokeLinejoin="round"
649
+ className="text-blue-600"
650
+ >
651
+ <path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"></path>
652
+ <path d="M21 3v5h-5"></path>
653
+ <path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"></path>
654
+ <path d="M3 21v-5h5"></path>
655
+ </svg>
656
+ {t('common.product.reset_to_original')}
657
+ </Button>
658
+ );
659
+ })()}
660
+ </div>
661
+ )}
656
662
 
657
- if (!Component) {
658
- console.warn(
659
- 'Component not found for widget type:',
660
- facet.widget_type
661
- );
662
- return null;
663
- }
663
+ {fileError &&
664
+ !isCropping &&
665
+ (() => {
666
+ if (
667
+ settings?.customRenderers?.render?.filterSidebar
668
+ ?.renderErrorMessage
669
+ ) {
670
+ return settings.customRenderers.render.filterSidebar.renderErrorMessage(
671
+ {
672
+ error: fileError
673
+ }
674
+ );
675
+ }
664
676
 
665
- const filterGroupClassName = twMerge(
666
- 'filter-group',
667
- settings?.customStyles?.filterGroup
668
- );
677
+ const errorClassName = twMerge(
678
+ 'mt-2 px-3 py-2 bg-red-50 border border-red-100 rounded-md',
679
+ settings?.customStyles?.errorMessage
680
+ );
669
681
 
670
- const filterGroupContentClassName = twMerge(
671
- clsx('flex gap-4', {
672
- 'flex-wrap flex-row': facet.key === sizeKey,
673
- 'flex-col': facet.key !== sizeKey
674
- }),
675
- settings?.customStyles?.filterGroupContent
676
- );
682
+ return (
683
+ <div className={errorClassName}>
684
+ <p className="text-xs text-red-600 font-medium">
685
+ {fileError}
686
+ </p>
687
+ </div>
688
+ );
689
+ })()}
690
+ </div>
691
+ );
692
+ })()}
677
693
 
678
- const renderFilterGroupTitle = () => {
694
+ {/* Filters */}
695
+ <div className="space-y-2 md:space-y-4">
696
+ {searchResults?.facets
697
+ ?.filter((facet) => facet.key !== 'category_ids')
698
+ ?.map((facet) => {
679
699
  if (
680
700
  settings?.customRenderers?.render?.filterSidebar
681
- ?.renderFilterGroupTitle
701
+ ?.renderFilterGroup
682
702
  ) {
683
- return settings.customRenderers.render.filterSidebar.renderFilterGroupTitle(
703
+ return settings.customRenderers.render.filterSidebar.renderFilterGroup(
684
704
  {
685
- title: facet.name,
686
- isCollapsed: choices.some((choice) => choice.is_selected),
687
- onToggle: () => {}
705
+ facet,
706
+ onFacetChange: handleFacetChange,
707
+ isLoading
688
708
  }
689
709
  );
690
710
  }
691
- return null;
692
- };
693
711
 
694
- const renderFilterItem = (choice: any, index: number) => {
695
- if (
696
- settings?.customRenderers?.render?.filterSidebar
697
- ?.renderFilterItem
698
- ) {
699
- return settings.customRenderers.render.filterSidebar.renderFilterItem(
700
- {
701
- choice,
702
- facetKey: facet.key,
703
- onFacetChange: handleFacetChange,
704
- isLoading,
705
- isSelected: choice.is_selected
706
- }
712
+ const Component = getComponentByWidgetType(
713
+ facet.widget_type,
714
+ facet.key
715
+ );
716
+ const choices = facet.data.choices || [];
717
+
718
+ if (!Component) {
719
+ console.warn(
720
+ 'Component not found for widget type:',
721
+ facet.widget_type
707
722
  );
723
+ return null;
708
724
  }
709
725
 
710
- const filterItemClassName = twMerge(
711
- 'filter-item',
712
- settings?.customStyles?.filterItem
726
+ const filterGroupClassName = twMerge(
727
+ 'filter-group',
728
+ settings?.customStyles?.filterGroup
713
729
  );
714
730
 
715
- const filterItemInputClassName = twMerge(
716
- 'filter-item-input',
717
- settings?.customStyles?.filterItemInput
731
+ const filterGroupContentClassName = twMerge(
732
+ clsx('flex gap-4', {
733
+ 'flex-wrap flex-row': facet.key === sizeKey,
734
+ 'flex-col': facet.key !== sizeKey
735
+ }),
736
+ settings?.customStyles?.filterGroupContent
718
737
  );
719
738
 
720
- const filterItemLabelClassName = twMerge(
721
- 'filter-item-label',
722
- settings?.customStyles?.filterItemLabel
723
- );
739
+ const renderFilterGroupTitle = () => {
740
+ if (
741
+ settings?.customRenderers?.render?.filterSidebar
742
+ ?.renderFilterGroupTitle
743
+ ) {
744
+ return settings.customRenderers.render.filterSidebar.renderFilterGroupTitle(
745
+ {
746
+ title: facet.name,
747
+ isCollapsed: choices.some((choice) => choice.is_selected),
748
+ onToggle: () => {}
749
+ }
750
+ );
751
+ }
752
+ return null;
753
+ };
724
754
 
725
- const filterItemCountClassName = twMerge(
726
- 'filter-item-count',
727
- settings?.customStyles?.filterItemCount
728
- );
755
+ const renderFilterItem = (choice: any, index: number) => {
756
+ if (
757
+ settings?.customRenderers?.render?.filterSidebar
758
+ ?.renderFilterItem
759
+ ) {
760
+ return settings.customRenderers.render.filterSidebar.renderFilterItem(
761
+ {
762
+ choice,
763
+ facetKey: facet.key,
764
+ onFacetChange: handleFacetChange,
765
+ isLoading,
766
+ isSelected: choice.is_selected
767
+ }
768
+ );
769
+ }
770
+
771
+ const filterItemClassName = twMerge(
772
+ 'filter-item',
773
+ settings?.customStyles?.filterItem
774
+ );
775
+
776
+ const filterItemInputClassName = twMerge(
777
+ 'filter-item-input',
778
+ settings?.customStyles?.filterItemInput
779
+ );
780
+
781
+ const filterItemLabelClassName = twMerge(
782
+ 'filter-item-label',
783
+ settings?.customStyles?.filterItemLabel
784
+ );
785
+
786
+ const filterItemCountClassName = twMerge(
787
+ 'filter-item-count',
788
+ settings?.customStyles?.filterItemCount
789
+ );
790
+
791
+ return (
792
+ <div key={choice.value} className={filterItemClassName}>
793
+ <Component
794
+ key={choice.label}
795
+ value={choice.value}
796
+ label={choice.label}
797
+ name={facet.key}
798
+ onChange={() => {
799
+ if (!isLoading) {
800
+ handleFacetChange(facet.key, choice.value);
801
+ }
802
+ }}
803
+ onClick={() => {
804
+ if (!isLoading && facet.key === sizeKey) {
805
+ handleFacetChange(facet.key, choice.value);
806
+ }
807
+ }}
808
+ checked={choice.is_selected}
809
+ data-testid={`${choice.label.trim()}`}
810
+ disabled={isLoading}
811
+ className={filterItemInputClassName}
812
+ >
813
+ <span className={filterItemLabelClassName}>
814
+ {choice.label}
815
+ </span>{' '}
816
+ (
817
+ <span
818
+ data-testid={`filter-count-${facet.name.toLowerCase()}-${index}`}
819
+ className={filterItemCountClassName}
820
+ >
821
+ {choice.quantity}
822
+ </span>
823
+ )
824
+ </Component>
825
+ </div>
826
+ );
827
+ };
729
828
 
730
829
  return (
731
- <div key={choice.value} className={filterItemClassName}>
732
- <Component
733
- key={choice.label}
734
- value={choice.value}
735
- label={choice.label}
736
- name={facet.key}
737
- onChange={() => {
738
- if (!isLoading) {
739
- handleFacetChange(facet.key, choice.value);
740
- }
741
- }}
742
- onClick={() => {
743
- if (!isLoading && facet.key === sizeKey) {
744
- handleFacetChange(facet.key, choice.value);
745
- }
746
- }}
747
- checked={choice.is_selected}
748
- data-testid={`${choice.label.trim()}`}
749
- disabled={isLoading}
750
- className={filterItemInputClassName}
830
+ <div key={facet.key} className={filterGroupClassName}>
831
+ <Accordion
832
+ title={renderFilterGroupTitle() || facet.name}
833
+ isCollapse={choices.some((choice) => choice.is_selected)}
834
+ dataTestId={`filter-${facet.name}`}
751
835
  >
752
- <span className={filterItemLabelClassName}>
753
- {choice.label}
754
- </span>{' '}
755
- (
756
- <span
757
- data-testid={`filter-count-${facet.name.toLowerCase()}-${index}`}
758
- className={filterItemCountClassName}
759
- >
760
- {choice.quantity}
761
- </span>
762
- )
763
- </Component>
836
+ <div className={filterGroupContentClassName}>
837
+ {choices
838
+ .slice(0, 10)
839
+ .map((choice, index) =>
840
+ renderFilterItem(choice, index)
841
+ )}
842
+ </div>
843
+ </Accordion>
764
844
  </div>
765
845
  );
766
- };
767
-
768
- return (
769
- <div key={facet.key} className={filterGroupClassName}>
770
- <Accordion
771
- title={renderFilterGroupTitle() || facet.name}
772
- isCollapse={choices.some((choice) => choice.is_selected)}
773
- dataTestId={`filter-${facet.name}`}
774
- >
775
- <div className={filterGroupContentClassName}>
776
- {choices
777
- .slice(0, 10)
778
- .map((choice, index) => renderFilterItem(choice, index))}
779
- </div>
780
- </Accordion>
781
- </div>
846
+ })}
847
+ </div>
848
+
849
+ {/* Mobile Active Filters and Clear Button */}
850
+ {(() => {
851
+ const hasActiveFilters = searchResults?.facets
852
+ ?.filter((facet) => facet.key !== 'category_ids')
853
+ ?.some((facet) =>
854
+ facet.data.choices?.some((choice) => choice.is_selected)
782
855
  );
783
- })}
784
- </div>
785
856
 
786
- {/* Mobile Active Filters and Clear Button */}
787
- {(() => {
788
- const hasActiveFilters = searchResults?.facets
789
- ?.filter((facet) => facet.key !== 'category_ids')
790
- ?.some((facet) =>
791
- facet.data.choices?.some((choice) => choice.is_selected)
792
- );
857
+ if (!hasActiveFilters) return null;
793
858
 
794
- if (!hasActiveFilters) return null;
795
-
796
- const activeFilters = searchResults.facets
797
- .filter((facet) => facet.key !== 'category_ids')
798
- .flatMap(
799
- (facet) =>
800
- facet.data.choices
801
- ?.filter((choice) => choice.is_selected)
802
- .map((choice) => ({
803
- key: facet.key,
804
- value: choice.value.toString(),
805
- label: choice.label
806
- })) || []
807
- );
808
-
809
- const handleClearAll = () => {
810
- if (!isLoading) {
811
- const facetsToRemove = [];
812
- searchResults?.facets
813
- ?.filter((facet) => facet.key !== 'category_ids')
814
- ?.forEach((facet) => {
859
+ const activeFilters = searchResults.facets
860
+ .filter((facet) => facet.key !== 'category_ids')
861
+ .flatMap(
862
+ (facet) =>
815
863
  facet.data.choices
816
864
  ?.filter((choice) => choice.is_selected)
817
- .forEach((choice) => {
818
- facetsToRemove.push({
819
- facetKey: facet.key,
820
- choiceValue: choice.value
865
+ .map((choice) => ({
866
+ key: facet.key,
867
+ value: choice.value.toString(),
868
+ label: choice.label
869
+ })) || []
870
+ );
871
+
872
+ const handleClearAll = () => {
873
+ if (!isLoading) {
874
+ const facetsToRemove = [];
875
+ searchResults?.facets
876
+ ?.filter((facet) => facet.key !== 'category_ids')
877
+ ?.forEach((facet) => {
878
+ facet.data.choices
879
+ ?.filter((choice) => choice.is_selected)
880
+ .forEach((choice) => {
881
+ facetsToRemove.push({
882
+ facetKey: facet.key,
883
+ choiceValue: choice.value
884
+ });
821
885
  });
822
- });
886
+ });
887
+
888
+ facetsToRemove.forEach(({ facetKey, choiceValue }) => {
889
+ removeFacetFilter(facetKey, choiceValue);
823
890
  });
891
+ }
892
+ };
824
893
 
825
- facetsToRemove.forEach(({ facetKey, choiceValue }) => {
826
- removeFacetFilter(facetKey, choiceValue);
827
- });
894
+ if (
895
+ settings?.customRenderers?.render?.filterSidebar
896
+ ?.renderMobileActiveFilters
897
+ ) {
898
+ return (
899
+ <div className="md:hidden mt-6">
900
+ {settings.customRenderers.render.filterSidebar.renderMobileActiveFilters(
901
+ {
902
+ filters: activeFilters,
903
+ onRemove: handleFacetChange,
904
+ onClearAll: handleClearAll,
905
+ isLoading
906
+ }
907
+ )}
908
+ </div>
909
+ );
828
910
  }
829
- };
830
911
 
831
- if (
832
- settings?.customRenderers?.render?.filterSidebar
833
- ?.renderMobileActiveFilters
834
- ) {
835
- return (
836
- <div className="md:hidden mt-6">
837
- {settings.customRenderers.render.filterSidebar.renderMobileActiveFilters(
838
- {
839
- filters: activeFilters,
840
- onRemove: handleFacetChange,
841
- onClearAll: handleClearAll,
842
- isLoading
843
- }
844
- )}
845
- </div>
912
+ const mobileActiveFiltersClassName = twMerge(
913
+ 'md:hidden mt-6',
914
+ settings?.customStyles?.mobileActiveFilters
846
915
  );
847
- }
848
-
849
- const mobileActiveFiltersClassName = twMerge(
850
- 'md:hidden mt-6',
851
- settings?.customStyles?.mobileActiveFilters
852
- );
853
-
854
- const mobileActiveFilterTagClassName = twMerge(
855
- 'flex items-center gap-1 px-2 py-1 bg-gray-100 rounded-full text-xs',
856
- settings?.customStyles?.mobileActiveFilterTag
857
- );
858
-
859
- const mobileClearAllButtonClassName = twMerge(
860
- 'w-full',
861
- settings?.customStyles?.mobileClearAllButton
862
- );
863
-
864
- return (
865
- <div className={mobileActiveFiltersClassName}>
866
- <div className="mb-4">
867
- <h4 className="text-sm font-medium mb-2">
868
- {t('common.product.active_filters')}
869
- </h4>
870
- <div className="flex flex-wrap gap-2">
871
- {activeFilters.map((filter) => (
872
- <div
873
- key={`${filter.key}-${filter.value}`}
874
- className={mobileActiveFilterTagClassName}
875
- >
876
- <span>{filter.label}</span>
877
- <Button
878
- appearance="ghost"
879
- size="sm"
880
- onClick={() => {
881
- if (!isLoading) {
882
- handleFacetChange(filter.key, filter.value);
883
- }
884
- }}
885
- disabled={isLoading}
886
- className="hover:bg-gray-200 rounded-full p-0 w-5 h-5 disabled:opacity-50 disabled:cursor-not-allowed ml-1"
916
+
917
+ const mobileActiveFilterTagClassName = twMerge(
918
+ 'flex items-center gap-1 px-2 py-1 bg-gray-100 rounded-full text-xs',
919
+ settings?.customStyles?.mobileActiveFilterTag
920
+ );
921
+
922
+ const mobileClearAllButtonClassName = twMerge(
923
+ 'w-full',
924
+ settings?.customStyles?.mobileClearAllButton
925
+ );
926
+
927
+ return (
928
+ <div className={mobileActiveFiltersClassName}>
929
+ <div className="mb-4">
930
+ <h4 className="text-sm font-medium mb-2">
931
+ {t('common.product.active_filters')}
932
+ </h4>
933
+ <div className="flex flex-wrap gap-2">
934
+ {activeFilters.map((filter) => (
935
+ <div
936
+ key={`${filter.key}-${filter.value}`}
937
+ className={mobileActiveFilterTagClassName}
887
938
  >
888
- <Icon name="close" size={10} />
889
- </Button>
890
- </div>
891
- ))}
939
+ <span>{filter.label}</span>
940
+ <Button
941
+ appearance="ghost"
942
+ size="sm"
943
+ onClick={() => {
944
+ if (!isLoading) {
945
+ handleFacetChange(filter.key, filter.value);
946
+ }
947
+ }}
948
+ disabled={isLoading}
949
+ className="hover:bg-gray-200 rounded-full p-0 w-5 h-5 disabled:opacity-50 disabled:cursor-not-allowed ml-1"
950
+ >
951
+ <Icon
952
+ name={filterRemoveIconName}
953
+ size={10}
954
+ className={filterRemoveIconClassName}
955
+ />
956
+ </Button>
957
+ </div>
958
+ ))}
959
+ </div>
892
960
  </div>
893
- </div>
894
961
 
895
- <Button
896
- appearance="outlined"
897
- className={mobileClearAllButtonClassName}
898
- onClick={handleClearAll}
899
- disabled={isLoading}
900
- >
901
- {t('common.product.clear_all_filters')}
902
- </Button>
903
- </div>
904
- );
905
- })()}
906
- </div>
962
+ <Button
963
+ appearance="outlined"
964
+ className={mobileClearAllButtonClassName}
965
+ onClick={handleClearAll}
966
+ disabled={isLoading}
967
+ >
968
+ {t('common.product.clear_all_filters')}
969
+ </Button>
970
+ </div>
971
+ );
972
+ })()}
973
+ </div>
974
+ </>
907
975
  );
908
976
  }