@akinon/pz-similar-products 1.92.0-rc.19 → 1.92.0-rc.21

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