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

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