@akinon/pz-similar-products 1.92.0-rc.16
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.
- package/.gitattributes +15 -0
- package/.prettierrc +13 -0
- package/CHANGELOG.md +3 -0
- package/README.md +1372 -0
- package/package.json +21 -0
- package/src/data/endpoints.ts +122 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/use-image-cropper.ts +264 -0
- package/src/hooks/use-image-search-feature.ts +32 -0
- package/src/hooks/use-similar-products.ts +939 -0
- package/src/index.ts +33 -0
- package/src/types/index.ts +419 -0
- package/src/utils/image-validation.ts +303 -0
- package/src/utils/index.ts +161 -0
- package/src/views/filters.tsx +858 -0
- package/src/views/header-image-search-feature.tsx +68 -0
- package/src/views/image-search-button.tsx +47 -0
- package/src/views/image-search.tsx +152 -0
- package/src/views/main.tsx +200 -0
- package/src/views/product-image-search-feature.tsx +48 -0
- package/src/views/results.tsx +591 -0
- package/src/views/search-button.tsx +38 -0
- package/src/views/search-modal.tsx +422 -0
|
@@ -0,0 +1,858 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import {
|
|
5
|
+
Button,
|
|
6
|
+
Icon,
|
|
7
|
+
Accordion,
|
|
8
|
+
LoaderSpinner
|
|
9
|
+
} from '@akinon/next/components';
|
|
10
|
+
import { useLocalization } from '@akinon/next/hooks';
|
|
11
|
+
import { FilterSidebarProps } from '../types';
|
|
12
|
+
import clsx from 'clsx';
|
|
13
|
+
import { twMerge } from 'tailwind-merge';
|
|
14
|
+
import dynamic from 'next/dynamic';
|
|
15
|
+
|
|
16
|
+
const ReactCrop = dynamic(
|
|
17
|
+
() => import('react-image-crop').then((mod) => mod.ReactCrop),
|
|
18
|
+
{
|
|
19
|
+
ssr: false,
|
|
20
|
+
loading: () => (
|
|
21
|
+
<div className="w-full h-96 bg-gray-100 flex items-center justify-center">
|
|
22
|
+
<LoaderSpinner />
|
|
23
|
+
</div>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const Radio = dynamic(
|
|
29
|
+
() =>
|
|
30
|
+
import('@akinon/next/components')
|
|
31
|
+
.then((mod) => mod.Radio)
|
|
32
|
+
.catch(() => import('@theme/components').then((mod) => mod.Radio))
|
|
33
|
+
.catch(() => () => <input type="radio" className="form-radio" />),
|
|
34
|
+
{ ssr: false }
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const Checkbox = dynamic(
|
|
38
|
+
() =>
|
|
39
|
+
import('@theme/components')
|
|
40
|
+
.then((mod) => mod.Checkbox)
|
|
41
|
+
.catch(() => import('@theme/components').then((mod) => mod.Checkbox))
|
|
42
|
+
.catch(() => () => <input type="checkbox" className="form-checkbox" />),
|
|
43
|
+
{ ssr: false }
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const WIDGET_TYPE = {
|
|
47
|
+
category: 'category',
|
|
48
|
+
multiselect: 'multiselect'
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export function SimilarProductsFilterSidebar({
|
|
52
|
+
isFilterMenuOpen,
|
|
53
|
+
setIsFilterMenuOpen,
|
|
54
|
+
searchResults,
|
|
55
|
+
isLoading,
|
|
56
|
+
handleFacetChange,
|
|
57
|
+
removeFacetFilter,
|
|
58
|
+
currentImageUrl,
|
|
59
|
+
isCropping,
|
|
60
|
+
imageRef,
|
|
61
|
+
fileInputRef,
|
|
62
|
+
crop,
|
|
63
|
+
setCrop,
|
|
64
|
+
completedCrop,
|
|
65
|
+
setCompletedCrop,
|
|
66
|
+
handleCropComplete,
|
|
67
|
+
toggleCropMode,
|
|
68
|
+
processCompletedCrop,
|
|
69
|
+
processManualCrop,
|
|
70
|
+
resetCrop,
|
|
71
|
+
product,
|
|
72
|
+
hasUploadedImage,
|
|
73
|
+
handleFileUpload,
|
|
74
|
+
handleResetToOriginal,
|
|
75
|
+
fileError,
|
|
76
|
+
showResetButton = true,
|
|
77
|
+
settings,
|
|
78
|
+
className
|
|
79
|
+
}: FilterSidebarProps) {
|
|
80
|
+
const { t } = useLocalization();
|
|
81
|
+
|
|
82
|
+
const sizeKey = settings?.commonProductAttributes?.find(
|
|
83
|
+
(item: any) => item.translationKey === 'size'
|
|
84
|
+
)?.key;
|
|
85
|
+
|
|
86
|
+
const COMPONENT_TYPES = {
|
|
87
|
+
[WIDGET_TYPE.category]: Radio,
|
|
88
|
+
[WIDGET_TYPE.multiselect]: Checkbox
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const getComponentByWidgetType = (widgetType: string, facetKey: string) => {
|
|
92
|
+
return (
|
|
93
|
+
COMPONENT_TYPES[widgetType] || COMPONENT_TYPES[WIDGET_TYPE.multiselect]
|
|
94
|
+
);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const handleNewImageClick = () => {
|
|
98
|
+
if (!isLoading && settings?.enableFileUpload !== false) {
|
|
99
|
+
fileInputRef.current?.click();
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const handleFileChangeWithCropReset = async (
|
|
104
|
+
event: React.ChangeEvent<HTMLInputElement>
|
|
105
|
+
) => {
|
|
106
|
+
if (isLoading) return;
|
|
107
|
+
|
|
108
|
+
resetCrop();
|
|
109
|
+
handleFileUpload(event);
|
|
110
|
+
|
|
111
|
+
if (event.target) {
|
|
112
|
+
event.target.value = '';
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const handleToggleCropMode = () => {
|
|
117
|
+
if (!isLoading) {
|
|
118
|
+
toggleCropMode();
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const handleSafeProcessCrop = async (cropData: any) => {
|
|
123
|
+
if (!isLoading && cropData?.width > 10 && cropData?.height > 10) {
|
|
124
|
+
processManualCrop(cropData);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const sidebarClassName = twMerge(
|
|
129
|
+
'w-9/10 fixed left-0 top-0 bottom-0 bg-white z-50 p-6 transition-all ease-in duration-300 md:static md:mr-0 md:text-sm md:pt-4 md:col-span-1 md:bg-gray-100 md:p-4 md:flex md:flex-col md:overflow-y-auto overflow-x-hidden',
|
|
130
|
+
isFilterMenuOpen
|
|
131
|
+
? 'flex flex-col opacity-100 overflow-auto md:opacity-100 md:visible md:translate-x-0'
|
|
132
|
+
: 'opacity-0 invisible absolute -translate-x-full md:opacity-100 md:visible md:translate-x-0',
|
|
133
|
+
settings?.customStyles?.filterSidebar,
|
|
134
|
+
className
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
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
|
+
|
|
225
|
+
if (
|
|
226
|
+
settings?.customRenderers?.render?.filterSidebar?.renderCropButton
|
|
227
|
+
) {
|
|
228
|
+
return settings.customRenderers.render.filterSidebar.renderCropButton(
|
|
229
|
+
{
|
|
230
|
+
isCropping,
|
|
231
|
+
onClick: handleToggleCropMode,
|
|
232
|
+
disabled: isLoading
|
|
233
|
+
}
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
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"
|
|
258
|
+
>
|
|
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"
|
|
273
|
+
>
|
|
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>
|
|
279
|
+
);
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const renderTickButton = () => {
|
|
283
|
+
if (!settings?.enableCropping || settings.enableCropping === false)
|
|
284
|
+
return null;
|
|
285
|
+
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
|
|
295
|
+
) {
|
|
296
|
+
return settings.customRenderers.render.filterSidebar.renderTickButton(
|
|
297
|
+
{
|
|
298
|
+
onClick: () => handleSafeProcessCrop(completedCrop),
|
|
299
|
+
disabled: isLoading
|
|
300
|
+
}
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
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
|
|
309
|
+
);
|
|
310
|
+
|
|
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>
|
|
341
|
+
);
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
return (
|
|
345
|
+
<div className={imageSectionClassName}>
|
|
346
|
+
<div className={imageContainerClassName}>
|
|
347
|
+
{renderCropButton()}
|
|
348
|
+
{renderTickButton()}
|
|
349
|
+
|
|
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
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const imageWrapperClassName = twMerge(
|
|
365
|
+
'w-full h-full flex items-center justify-center',
|
|
366
|
+
settings?.customStyles?.imageWrapper
|
|
367
|
+
);
|
|
368
|
+
|
|
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>
|
|
428
|
+
|
|
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
|
+
}
|
|
457
|
+
|
|
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
|
+
);
|
|
464
|
+
|
|
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
|
+
</>
|
|
493
|
+
)}
|
|
494
|
+
|
|
495
|
+
{(() => {
|
|
496
|
+
if (
|
|
497
|
+
settings?.customRenderers?.render?.filterSidebar
|
|
498
|
+
?.renderResetButton
|
|
499
|
+
) {
|
|
500
|
+
return settings.customRenderers.render.filterSidebar.renderResetButton(
|
|
501
|
+
{
|
|
502
|
+
onClick: handleResetToOriginal,
|
|
503
|
+
disabled: isLoading,
|
|
504
|
+
showButton:
|
|
505
|
+
showResetButton && (hasUploadedImage || completedCrop)
|
|
506
|
+
}
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
|
|
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
|
|
518
|
+
);
|
|
519
|
+
|
|
520
|
+
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>
|
|
547
|
+
);
|
|
548
|
+
})()}
|
|
549
|
+
</div>
|
|
550
|
+
)}
|
|
551
|
+
|
|
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
|
|
562
|
+
}
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
|
|
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
|
+
);
|
|
570
|
+
|
|
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
|
+
}
|
|
600
|
+
|
|
601
|
+
const Component = getComponentByWidgetType(
|
|
602
|
+
facet.widget_type,
|
|
603
|
+
facet.key
|
|
604
|
+
);
|
|
605
|
+
const choices = facet.data.choices || [];
|
|
606
|
+
|
|
607
|
+
if (!Component) {
|
|
608
|
+
console.warn(
|
|
609
|
+
'Component not found for widget type:',
|
|
610
|
+
facet.widget_type
|
|
611
|
+
);
|
|
612
|
+
return null;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const filterGroupClassName = twMerge(
|
|
616
|
+
'filter-group',
|
|
617
|
+
settings?.customStyles?.filterGroup
|
|
618
|
+
);
|
|
619
|
+
|
|
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
|
+
);
|
|
627
|
+
|
|
628
|
+
const renderFilterGroupTitle = () => {
|
|
629
|
+
if (
|
|
630
|
+
settings?.customRenderers?.render?.filterSidebar
|
|
631
|
+
?.renderFilterGroupTitle
|
|
632
|
+
) {
|
|
633
|
+
return settings.customRenderers.render.filterSidebar.renderFilterGroupTitle(
|
|
634
|
+
{
|
|
635
|
+
title: facet.name,
|
|
636
|
+
isCollapsed: choices.some((choice) => choice.is_selected),
|
|
637
|
+
onToggle: () => {}
|
|
638
|
+
}
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
return null;
|
|
642
|
+
};
|
|
643
|
+
|
|
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
|
+
}
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const filterItemClassName = twMerge(
|
|
661
|
+
'filter-item',
|
|
662
|
+
settings?.customStyles?.filterItem
|
|
663
|
+
);
|
|
664
|
+
|
|
665
|
+
const filterItemInputClassName = twMerge(
|
|
666
|
+
'filter-item-input',
|
|
667
|
+
settings?.customStyles?.filterItemInput
|
|
668
|
+
);
|
|
669
|
+
|
|
670
|
+
const filterItemLabelClassName = twMerge(
|
|
671
|
+
'filter-item-label',
|
|
672
|
+
settings?.customStyles?.filterItemLabel
|
|
673
|
+
);
|
|
674
|
+
|
|
675
|
+
const filterItemCountClassName = twMerge(
|
|
676
|
+
'filter-item-count',
|
|
677
|
+
settings?.customStyles?.filterItemCount
|
|
678
|
+
);
|
|
679
|
+
|
|
680
|
+
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}
|
|
701
|
+
>
|
|
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>
|
|
714
|
+
</div>
|
|
715
|
+
);
|
|
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>
|
|
732
|
+
);
|
|
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
|
+
|
|
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) => {
|
|
765
|
+
facet.data.choices
|
|
766
|
+
?.filter((choice) => choice.is_selected)
|
|
767
|
+
.forEach((choice) => {
|
|
768
|
+
facetsToRemove.push({
|
|
769
|
+
facetKey: facet.key,
|
|
770
|
+
choiceValue: choice.value
|
|
771
|
+
});
|
|
772
|
+
});
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
facetsToRemove.forEach(({ facetKey, choiceValue }) => {
|
|
776
|
+
removeFacetFilter(facetKey, choiceValue);
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
};
|
|
780
|
+
|
|
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>
|
|
796
|
+
);
|
|
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"
|
|
837
|
+
>
|
|
838
|
+
<Icon name="close" size={10} />
|
|
839
|
+
</Button>
|
|
840
|
+
</div>
|
|
841
|
+
))}
|
|
842
|
+
</div>
|
|
843
|
+
</div>
|
|
844
|
+
|
|
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>
|
|
857
|
+
);
|
|
858
|
+
}
|