@akinon/pz-similar-products 1.92.0-snapshot-ZERO-3457-20250627111231 → 1.92.0-snapshot-ZERO-3457-20250627121541

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