@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.
- package/CHANGELOG.md +12 -0
- package/README.md +286 -20
- package/package.json +1 -1
- package/src/hooks/use-similar-products.ts +18 -12
- package/src/types/index.ts +23 -0
- package/src/utils/image-conversion.ts +44 -0
- package/src/utils/index.ts +6 -1
- package/src/views/filters.tsx +696 -628
- package/src/views/header-image-search-feature.tsx +16 -4
- package/src/views/image-search.tsx +136 -84
- package/src/views/main.tsx +1 -0
- package/src/views/product-image-search-feature.tsx +12 -3
- package/src/views/results.tsx +12 -2
- package/src/views/search-modal.tsx +38 -7
|
@@ -8,18 +8,29 @@ import { SimilarProductsPlugin } from './main';
|
|
|
8
8
|
|
|
9
9
|
interface HeaderImageSearchFeatureProps {
|
|
10
10
|
className?: string;
|
|
11
|
+
isEnabled?: boolean;
|
|
12
|
+
settings?: any;
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
export function HeaderImageSearchFeature({
|
|
14
|
-
className
|
|
16
|
+
className,
|
|
17
|
+
isEnabled: isEnabledProp,
|
|
18
|
+
settings
|
|
15
19
|
}: HeaderImageSearchFeatureProps) {
|
|
16
|
-
const { isEnabled, isLoading } = useImageSearchFeature();
|
|
20
|
+
const { isEnabled: hookIsEnabled, isLoading } = useImageSearchFeature();
|
|
21
|
+
|
|
22
|
+
const envEnabled = process.env.NEXT_PUBLIC_ENABLE_IMAGE_SEARCH === 'true';
|
|
23
|
+
const finalIsEnabled = envEnabled
|
|
24
|
+
? true
|
|
25
|
+
: isEnabledProp !== undefined
|
|
26
|
+
? isEnabledProp
|
|
27
|
+
: hookIsEnabled;
|
|
17
28
|
|
|
18
29
|
const [isImageSearchModalOpen, setIsImageSearchModalOpen] = useState(false);
|
|
19
30
|
const [isResultsModalOpen, setIsResultsModalOpen] = useState(false);
|
|
20
31
|
const [uploadedImageFile, setUploadedImageFile] = useState<File | null>(null);
|
|
21
32
|
|
|
22
|
-
if (isLoading || !
|
|
33
|
+
if (isLoading || !finalIsEnabled) {
|
|
23
34
|
return null;
|
|
24
35
|
}
|
|
25
36
|
|
|
@@ -44,7 +55,7 @@ export function HeaderImageSearchFeature({
|
|
|
44
55
|
|
|
45
56
|
return (
|
|
46
57
|
<>
|
|
47
|
-
{
|
|
58
|
+
{finalIsEnabled && (
|
|
48
59
|
<>
|
|
49
60
|
<ImageSearchButton
|
|
50
61
|
onClick={handleOpenImageSearch}
|
|
@@ -60,6 +71,7 @@ export function HeaderImageSearchFeature({
|
|
|
60
71
|
uploadedImageFile={uploadedImageFile}
|
|
61
72
|
onImageUpload={handleImageUpload}
|
|
62
73
|
showResetButton={false}
|
|
74
|
+
settings={settings}
|
|
63
75
|
/>
|
|
64
76
|
</>
|
|
65
77
|
)}
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
import React, { useEffect } from 'react';
|
|
4
4
|
import { Modal, Button, Input } from '@akinon/next/components';
|
|
5
5
|
import { useLocalization } from '@akinon/next/hooks';
|
|
6
|
+
import { twMerge } from 'tailwind-merge';
|
|
7
|
+
import { convertWebPToJPEG } from '../utils/image-conversion';
|
|
6
8
|
|
|
7
9
|
interface ImageSearchModalProps {
|
|
8
10
|
isOpen: boolean;
|
|
@@ -11,63 +13,33 @@ interface ImageSearchModalProps {
|
|
|
11
13
|
handleImageFileChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
|
12
14
|
className?: string;
|
|
13
15
|
clearError?: () => void;
|
|
16
|
+
settings?: any;
|
|
14
17
|
}
|
|
15
18
|
|
|
16
|
-
const convertWebPToJPEG = (file: File): Promise<File> => {
|
|
17
|
-
return new Promise((resolve, reject) => {
|
|
18
|
-
const canvas = document.createElement('canvas');
|
|
19
|
-
const ctx = canvas.getContext('2d');
|
|
20
|
-
const img = new Image();
|
|
21
|
-
|
|
22
|
-
img.onload = () => {
|
|
23
|
-
canvas.width = img.naturalWidth;
|
|
24
|
-
canvas.height = img.naturalHeight;
|
|
25
|
-
|
|
26
|
-
if (!ctx) {
|
|
27
|
-
reject(new Error('Canvas context not available'));
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
ctx.drawImage(img, 0, 0);
|
|
32
|
-
|
|
33
|
-
canvas.toBlob(
|
|
34
|
-
(blob) => {
|
|
35
|
-
if (!blob) {
|
|
36
|
-
reject(new Error('Failed to convert image'));
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const convertedFile = new File(
|
|
41
|
-
[blob],
|
|
42
|
-
file.name.replace(/\.webp$/i, '.jpg'),
|
|
43
|
-
{
|
|
44
|
-
type: 'image/jpeg',
|
|
45
|
-
lastModified: Date.now()
|
|
46
|
-
}
|
|
47
|
-
);
|
|
48
|
-
|
|
49
|
-
resolve(convertedFile);
|
|
50
|
-
},
|
|
51
|
-
'image/jpeg',
|
|
52
|
-
0.9
|
|
53
|
-
);
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
img.onerror = () => reject(new Error('Failed to load image'));
|
|
57
|
-
img.src = URL.createObjectURL(file);
|
|
58
|
-
});
|
|
59
|
-
};
|
|
60
|
-
|
|
61
19
|
export function ImageSearchModal({
|
|
62
20
|
isOpen,
|
|
63
21
|
setIsOpen,
|
|
64
22
|
fileInputRef,
|
|
65
23
|
handleImageFileChange,
|
|
66
24
|
className,
|
|
67
|
-
clearError
|
|
25
|
+
clearError,
|
|
26
|
+
settings
|
|
68
27
|
}: ImageSearchModalProps) {
|
|
69
28
|
const { t } = useLocalization();
|
|
70
29
|
|
|
30
|
+
// Check for custom modal renderer first
|
|
31
|
+
if (settings?.customRenderers?.render?.imageSearchModal?.renderModal) {
|
|
32
|
+
return settings.customRenderers.render.imageSearchModal.renderModal({
|
|
33
|
+
isOpen,
|
|
34
|
+
setIsOpen,
|
|
35
|
+
fileInputRef,
|
|
36
|
+
handleImageFileChange,
|
|
37
|
+
className,
|
|
38
|
+
clearError,
|
|
39
|
+
settings
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
71
43
|
useEffect(() => {
|
|
72
44
|
if (isOpen && clearError) {
|
|
73
45
|
clearError();
|
|
@@ -107,46 +79,126 @@ export function ImageSearchModal({
|
|
|
107
79
|
};
|
|
108
80
|
|
|
109
81
|
return (
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
82
|
+
<>
|
|
83
|
+
{isOpen && (
|
|
84
|
+
<div
|
|
85
|
+
className={twMerge(
|
|
86
|
+
'fixed inset-0 bg-black/50 z-[60]',
|
|
87
|
+
settings?.customStyles?.imageSearchModalOverlay
|
|
88
|
+
)}
|
|
89
|
+
onClick={() => setIsOpen(false)}
|
|
90
|
+
/>
|
|
91
|
+
)}
|
|
92
|
+
|
|
93
|
+
<Modal
|
|
94
|
+
portalId="image-search-modal"
|
|
95
|
+
title={t('common.search.image_search.title')}
|
|
96
|
+
open={isOpen}
|
|
97
|
+
setOpen={setIsOpen}
|
|
98
|
+
className={`w-full max-w-2xl max-h-[90vh] overflow-auto ${
|
|
99
|
+
className || ''
|
|
100
|
+
}`}
|
|
101
|
+
>
|
|
102
|
+
<div
|
|
103
|
+
className={twMerge(
|
|
104
|
+
'grid grid-cols-1 md:grid-cols-2 gap-4',
|
|
105
|
+
settings?.customStyles?.imageSearchContent
|
|
106
|
+
)}
|
|
107
|
+
>
|
|
108
|
+
<div
|
|
109
|
+
className={twMerge(
|
|
110
|
+
'md:col-span-1 p-4',
|
|
111
|
+
settings?.customStyles?.imageSearchUploadSection
|
|
112
|
+
)}
|
|
113
|
+
>
|
|
114
|
+
<p
|
|
115
|
+
className={twMerge(
|
|
116
|
+
'mb-4 text-lg font-medium',
|
|
117
|
+
settings?.customStyles?.imageSearchUploadTitle
|
|
118
|
+
)}
|
|
119
|
+
>
|
|
120
|
+
{t('common.search.image_search.upload_image')}
|
|
121
|
+
</p>
|
|
122
|
+
<div
|
|
123
|
+
className={twMerge(
|
|
124
|
+
'border-2 border-dashed border-gray-300 rounded-lg p-8 text-center',
|
|
125
|
+
settings?.customStyles?.imageSearchUploadArea
|
|
126
|
+
)}
|
|
133
127
|
>
|
|
134
|
-
|
|
135
|
-
|
|
128
|
+
<Input
|
|
129
|
+
type="file"
|
|
130
|
+
ref={fileInputRef}
|
|
131
|
+
onChange={handleFileSelect}
|
|
132
|
+
accept="image/*"
|
|
133
|
+
className="hidden"
|
|
134
|
+
/>
|
|
135
|
+
<Button
|
|
136
|
+
className={twMerge(
|
|
137
|
+
'w-full py-4 px-6 border-2 border-primary text-white hover:bg-primary hover:text-white transition-colors rounded-md',
|
|
138
|
+
settings?.customStyles?.imageSearchUploadButton
|
|
139
|
+
)}
|
|
140
|
+
onClick={() => fileInputRef.current?.click()}
|
|
141
|
+
>
|
|
142
|
+
{t('common.search.image_search.select_image')}
|
|
143
|
+
</Button>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
<div
|
|
147
|
+
className={twMerge(
|
|
148
|
+
'md:col-span-1 bg-gray-50 p-4 rounded-md h-fit my-4',
|
|
149
|
+
settings?.customStyles?.imageSearchTipsSection
|
|
150
|
+
)}
|
|
151
|
+
>
|
|
152
|
+
<p
|
|
153
|
+
className={twMerge(
|
|
154
|
+
'mb-2 font-medium',
|
|
155
|
+
settings?.customStyles?.imageSearchTipsTitle
|
|
156
|
+
)}
|
|
157
|
+
>
|
|
158
|
+
{t('common.search.image_search.best_results')}
|
|
159
|
+
</p>
|
|
160
|
+
<ul
|
|
161
|
+
className={twMerge(
|
|
162
|
+
'text-left list-disc pl-5 text-sm text-gray-600',
|
|
163
|
+
settings?.customStyles?.imageSearchTipsList
|
|
164
|
+
)}
|
|
165
|
+
>
|
|
166
|
+
<li
|
|
167
|
+
className={twMerge(
|
|
168
|
+
'mb-2',
|
|
169
|
+
settings?.customStyles?.imageSearchTipsItem
|
|
170
|
+
)}
|
|
171
|
+
>
|
|
172
|
+
{t('common.search.image_search.tip_1')}
|
|
173
|
+
</li>
|
|
174
|
+
<li
|
|
175
|
+
className={twMerge(
|
|
176
|
+
'mb-2',
|
|
177
|
+
settings?.customStyles?.imageSearchTipsItem
|
|
178
|
+
)}
|
|
179
|
+
>
|
|
180
|
+
{t('common.search.image_search.tip_2')}
|
|
181
|
+
</li>
|
|
182
|
+
<li
|
|
183
|
+
className={twMerge(
|
|
184
|
+
'mb-2',
|
|
185
|
+
settings?.customStyles?.imageSearchTipsItem
|
|
186
|
+
)}
|
|
187
|
+
>
|
|
188
|
+
{t('common.search.image_search.tip_3')}
|
|
189
|
+
</li>
|
|
190
|
+
<li
|
|
191
|
+
className={twMerge(
|
|
192
|
+
'mb-2',
|
|
193
|
+
settings?.customStyles?.imageSearchTipsItem
|
|
194
|
+
)}
|
|
195
|
+
>
|
|
196
|
+
{t('common.search.image_search.tip_4')}
|
|
197
|
+
</li>
|
|
198
|
+
</ul>
|
|
136
199
|
</div>
|
|
137
200
|
</div>
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
{t('common.search.image_search.best_results')}
|
|
141
|
-
</p>
|
|
142
|
-
<ul className="text-left list-disc pl-5 text-sm text-gray-600">
|
|
143
|
-
<li className="mb-2">{t('common.search.image_search.tip_1')}</li>
|
|
144
|
-
<li className="mb-2">{t('common.search.image_search.tip_2')}</li>
|
|
145
|
-
<li className="mb-2">{t('common.search.image_search.tip_3')}</li>
|
|
146
|
-
<li className="mb-2">{t('common.search.image_search.tip_4')}</li>
|
|
147
|
-
</ul>
|
|
148
|
-
</div>
|
|
149
|
-
</div>
|
|
150
|
-
</Modal>
|
|
201
|
+
</Modal>
|
|
202
|
+
</>
|
|
151
203
|
);
|
|
152
204
|
}
|
package/src/views/main.tsx
CHANGED
|
@@ -12,17 +12,26 @@ interface ProductImageSearchFeatureProps {
|
|
|
12
12
|
activeIndex?: number;
|
|
13
13
|
settings?: any;
|
|
14
14
|
className?: string;
|
|
15
|
+
isEnabled?: boolean;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
export function ProductImageSearchFeature({
|
|
18
19
|
product,
|
|
19
20
|
activeIndex = 0,
|
|
20
21
|
settings: userSettings,
|
|
21
|
-
className = 'absolute top-6 left-6 z-[20]'
|
|
22
|
+
className = 'absolute top-6 left-6 z-[20]',
|
|
23
|
+
isEnabled: isEnabledProp
|
|
22
24
|
}: ProductImageSearchFeatureProps) {
|
|
23
25
|
const settings = mergeSettings(userSettings);
|
|
24
26
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
25
|
-
const { isEnabled } = useImageSearchFeature();
|
|
27
|
+
const { isEnabled: hookIsEnabled } = useImageSearchFeature();
|
|
28
|
+
|
|
29
|
+
const envEnabled = process.env.NEXT_PUBLIC_ENABLE_IMAGE_SEARCH === 'true';
|
|
30
|
+
const finalIsEnabled = envEnabled
|
|
31
|
+
? true
|
|
32
|
+
: isEnabledProp !== undefined
|
|
33
|
+
? isEnabledProp
|
|
34
|
+
: hookIsEnabled;
|
|
26
35
|
|
|
27
36
|
const handleClick = () => {
|
|
28
37
|
setIsModalOpen(true);
|
|
@@ -30,7 +39,7 @@ export function ProductImageSearchFeature({
|
|
|
30
39
|
|
|
31
40
|
return (
|
|
32
41
|
<>
|
|
33
|
-
{
|
|
42
|
+
{finalIsEnabled && (
|
|
34
43
|
<>
|
|
35
44
|
<SimilarProductsButton onClick={handleClick} className={className} />
|
|
36
45
|
|
package/src/views/results.tsx
CHANGED
|
@@ -95,7 +95,12 @@ export function SimilarProductsResultsGrid({
|
|
|
95
95
|
|
|
96
96
|
return (
|
|
97
97
|
<div className={emptyStateClassName}>
|
|
98
|
-
<div
|
|
98
|
+
<div
|
|
99
|
+
className={twMerge(
|
|
100
|
+
'text-center',
|
|
101
|
+
settings?.customStyles?.emptyStateInner
|
|
102
|
+
)}
|
|
103
|
+
>
|
|
99
104
|
<svg
|
|
100
105
|
className={emptyStateIconClassName}
|
|
101
106
|
fill="none"
|
|
@@ -229,7 +234,12 @@ export function SimilarProductsResultsGrid({
|
|
|
229
234
|
)}
|
|
230
235
|
|
|
231
236
|
{renderProductPrice() || (
|
|
232
|
-
<div
|
|
237
|
+
<div
|
|
238
|
+
className={twMerge(
|
|
239
|
+
'price-container',
|
|
240
|
+
settings?.customStyles?.productPriceContainer
|
|
241
|
+
)}
|
|
242
|
+
>
|
|
233
243
|
<span className={productPriceClassName}>
|
|
234
244
|
{product.price} ₺
|
|
235
245
|
</span>
|
|
@@ -196,7 +196,12 @@ export function SimilarProductsModal({
|
|
|
196
196
|
|
|
197
197
|
return (
|
|
198
198
|
<div className={containerClassName}>
|
|
199
|
-
<div
|
|
199
|
+
<div
|
|
200
|
+
className={twMerge(
|
|
201
|
+
'flex flex-wrap gap-2',
|
|
202
|
+
settings?.customStyles?.activeFiltersWrapper
|
|
203
|
+
)}
|
|
204
|
+
>
|
|
200
205
|
{activeFilters.map((filter) => {
|
|
201
206
|
if (
|
|
202
207
|
settings?.customRenderers?.render?.modal?.renderActiveFilterTag
|
|
@@ -348,12 +353,29 @@ export function SimilarProductsModal({
|
|
|
348
353
|
|
|
349
354
|
return (
|
|
350
355
|
<div className={containerClassName}>
|
|
351
|
-
<div
|
|
352
|
-
|
|
356
|
+
<div
|
|
357
|
+
className={twMerge(
|
|
358
|
+
'flex items-center justify-between p-3 md:p-4',
|
|
359
|
+
settings?.customStyles?.controlsInner
|
|
360
|
+
)}
|
|
361
|
+
>
|
|
362
|
+
<div
|
|
363
|
+
className={twMerge(
|
|
364
|
+
'flex items-center gap-3',
|
|
365
|
+
settings?.customStyles?.controlsLeft
|
|
366
|
+
)}
|
|
367
|
+
>
|
|
353
368
|
{renderItemCount()}
|
|
354
369
|
{renderFilterToggle()}
|
|
355
370
|
</div>
|
|
356
|
-
<div
|
|
371
|
+
<div
|
|
372
|
+
className={twMerge(
|
|
373
|
+
'relative',
|
|
374
|
+
settings?.customStyles?.controlsRight
|
|
375
|
+
)}
|
|
376
|
+
>
|
|
377
|
+
{renderSortDropdown()}
|
|
378
|
+
</div>
|
|
357
379
|
</div>
|
|
358
380
|
</div>
|
|
359
381
|
);
|
|
@@ -371,8 +393,12 @@ export function SimilarProductsModal({
|
|
|
371
393
|
{renderActiveFilters()}
|
|
372
394
|
{renderControls()}
|
|
373
395
|
|
|
374
|
-
|
|
375
|
-
|
|
396
|
+
<div
|
|
397
|
+
className={twMerge(
|
|
398
|
+
'flex flex-col md:grid md:grid-cols-5 gap-0 flex-1 overflow-hidden',
|
|
399
|
+
settings?.customStyles?.modalContent
|
|
400
|
+
)}
|
|
401
|
+
>
|
|
376
402
|
<FilterSidebarComponent
|
|
377
403
|
isFilterMenuOpen={isFilterMenuOpen}
|
|
378
404
|
setIsFilterMenuOpen={setIsFilterMenuOpen}
|
|
@@ -403,7 +429,12 @@ export function SimilarProductsModal({
|
|
|
403
429
|
className={settings?.customStyles?.filterSidebar}
|
|
404
430
|
/>
|
|
405
431
|
|
|
406
|
-
<div
|
|
432
|
+
<div
|
|
433
|
+
className={twMerge(
|
|
434
|
+
'flex-1 md:col-span-4 p-3 md:p-4 overflow-y-auto',
|
|
435
|
+
settings?.customStyles?.resultsContainer
|
|
436
|
+
)}
|
|
437
|
+
>
|
|
407
438
|
<ResultsGridComponent
|
|
408
439
|
searchResults={searchResults}
|
|
409
440
|
resultsKey={resultsKey}
|