@akinon/pz-similar-products 1.92.0-rc.20 → 1.92.0-rc.22

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,786 @@ 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={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>
547
512
  );
548
513
  })()}
549
514
  </div>
550
- )}
551
515
 
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
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
+ );
562
596
  }
563
- );
564
- }
565
597
 
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
- );
598
+ if (
599
+ !showResetButton ||
600
+ (!hasUploadedImage && !completedCrop)
601
+ )
602
+ return null;
570
603
 
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
- }
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
+ );
600
610
 
601
- const Component = getComponentByWidgetType(
602
- facet.widget_type,
603
- facet.key
604
- );
605
- const choices = facet.data.choices || [];
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
+ )}
606
642
 
607
- if (!Component) {
608
- console.warn(
609
- 'Component not found for widget type:',
610
- facet.widget_type
611
- );
612
- return null;
613
- }
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
+ }
614
656
 
615
- const filterGroupClassName = twMerge(
616
- 'filter-group',
617
- settings?.customStyles?.filterGroup
618
- );
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
+ );
619
661
 
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
- );
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
+ })()}
627
673
 
628
- const renderFilterGroupTitle = () => {
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) => {
629
679
  if (
630
680
  settings?.customRenderers?.render?.filterSidebar
631
- ?.renderFilterGroupTitle
681
+ ?.renderFilterGroup
632
682
  ) {
633
- return settings.customRenderers.render.filterSidebar.renderFilterGroupTitle(
683
+ return settings.customRenderers.render.filterSidebar.renderFilterGroup(
634
684
  {
635
- title: facet.name,
636
- isCollapsed: choices.some((choice) => choice.is_selected),
637
- onToggle: () => {}
685
+ facet,
686
+ onFacetChange: handleFacetChange,
687
+ isLoading
638
688
  }
639
689
  );
640
690
  }
641
- return null;
642
- };
643
691
 
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
- }
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
657
702
  );
703
+ return null;
658
704
  }
659
705
 
660
- const filterItemClassName = twMerge(
661
- 'filter-item',
662
- settings?.customStyles?.filterItem
706
+ const filterGroupClassName = twMerge(
707
+ 'filter-group',
708
+ settings?.customStyles?.filterGroup
663
709
  );
664
710
 
665
- const filterItemInputClassName = twMerge(
666
- 'filter-item-input',
667
- settings?.customStyles?.filterItemInput
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
668
717
  );
669
718
 
670
- const filterItemLabelClassName = twMerge(
671
- 'filter-item-label',
672
- settings?.customStyles?.filterItemLabel
673
- );
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
+ };
674
734
 
675
- const filterItemCountClassName = twMerge(
676
- 'filter-item-count',
677
- settings?.customStyles?.filterItemCount
678
- );
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
+ );
770
+
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
+ };
679
808
 
680
809
  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}
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}`}
701
815
  >
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>
816
+ <div className={filterGroupContentClassName}>
817
+ {choices
818
+ .slice(0, 10)
819
+ .map((choice, index) =>
820
+ renderFilterItem(choice, index)
821
+ )}
822
+ </div>
823
+ </Accordion>
714
824
  </div>
715
825
  );
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>
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)
732
835
  );
733
- })}
734
- </div>
735
836
 
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
- );
837
+ if (!hasActiveFilters) return null;
743
838
 
744
- if (!hasActiveFilters) return null;
745
-
746
- const activeFilters = searchResults.facets
747
- .filter((facet) => facet.key !== 'category_ids')
748
- .flatMap(
749
- (facet) =>
750
- facet.data.choices
751
- ?.filter((choice) => choice.is_selected)
752
- .map((choice) => ({
753
- key: facet.key,
754
- value: choice.value.toString(),
755
- label: choice.label
756
- })) || []
757
- );
758
-
759
- const handleClearAll = () => {
760
- if (!isLoading) {
761
- const facetsToRemove = [];
762
- searchResults?.facets
763
- ?.filter((facet) => facet.key !== 'category_ids')
764
- ?.forEach((facet) => {
839
+ const activeFilters = searchResults.facets
840
+ .filter((facet) => facet.key !== 'category_ids')
841
+ .flatMap(
842
+ (facet) =>
765
843
  facet.data.choices
766
844
  ?.filter((choice) => choice.is_selected)
767
- .forEach((choice) => {
768
- facetsToRemove.push({
769
- facetKey: facet.key,
770
- choiceValue: choice.value
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
+ });
771
865
  });
772
- });
866
+ });
867
+
868
+ facetsToRemove.forEach(({ facetKey, choiceValue }) => {
869
+ removeFacetFilter(facetKey, choiceValue);
773
870
  });
871
+ }
872
+ };
774
873
 
775
- facetsToRemove.forEach(({ facetKey, choiceValue }) => {
776
- removeFacetFilter(facetKey, choiceValue);
777
- });
874
+ if (
875
+ settings?.customRenderers?.render?.filterSidebar
876
+ ?.renderMobileActiveFilters
877
+ ) {
878
+ 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
+ )}
888
+ </div>
889
+ );
778
890
  }
779
- };
780
891
 
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>
892
+ const mobileActiveFiltersClassName = twMerge(
893
+ 'md:hidden mt-6',
894
+ settings?.customStyles?.mobileActiveFilters
796
895
  );
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"
896
+
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
900
+ );
901
+
902
+ const mobileClearAllButtonClassName = twMerge(
903
+ 'w-full',
904
+ settings?.customStyles?.mobileClearAllButton
905
+ );
906
+
907
+ 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}
837
918
  >
838
- <Icon name="close" size={10} />
839
- </Button>
840
- </div>
841
- ))}
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>
842
936
  </div>
843
- </div>
844
937
 
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>
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
+ </div>
947
+ );
948
+ })()}
949
+ </div>
950
+ </>
857
951
  );
858
952
  }