@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,68 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState, useRef } from 'react';
|
|
4
|
+
import { Product } from '@akinon/next/types';
|
|
5
|
+
import { useImageSearchFeature } from '../hooks/use-image-search-feature';
|
|
6
|
+
import { ImageSearchButton } from './image-search-button';
|
|
7
|
+
import { SimilarProductsPlugin } from './main';
|
|
8
|
+
|
|
9
|
+
interface HeaderImageSearchFeatureProps {
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function HeaderImageSearchFeature({
|
|
14
|
+
className
|
|
15
|
+
}: HeaderImageSearchFeatureProps) {
|
|
16
|
+
const { isEnabled, isLoading } = useImageSearchFeature();
|
|
17
|
+
|
|
18
|
+
const [isImageSearchModalOpen, setIsImageSearchModalOpen] = useState(false);
|
|
19
|
+
const [isResultsModalOpen, setIsResultsModalOpen] = useState(false);
|
|
20
|
+
const [uploadedImageFile, setUploadedImageFile] = useState<File | null>(null);
|
|
21
|
+
|
|
22
|
+
if (isLoading || !isEnabled) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const handleOpenImageSearch = () => {
|
|
27
|
+
setIsImageSearchModalOpen(true);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const handleImageUpload = (file: File) => {
|
|
31
|
+
setUploadedImageFile(file);
|
|
32
|
+
setIsImageSearchModalOpen(false);
|
|
33
|
+
setIsResultsModalOpen(true);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const handleResultsModalClose = () => {
|
|
37
|
+
setIsResultsModalOpen(false);
|
|
38
|
+
setUploadedImageFile(null);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const handleImageSearchModalClose = () => {
|
|
42
|
+
setIsImageSearchModalOpen(false);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<>
|
|
47
|
+
{isEnabled && (
|
|
48
|
+
<>
|
|
49
|
+
<ImageSearchButton
|
|
50
|
+
onClick={handleOpenImageSearch}
|
|
51
|
+
className={className}
|
|
52
|
+
/>
|
|
53
|
+
<SimilarProductsPlugin
|
|
54
|
+
product={{} as Product}
|
|
55
|
+
isOpen={isResultsModalOpen}
|
|
56
|
+
onClose={handleResultsModalClose}
|
|
57
|
+
activeIndex={0}
|
|
58
|
+
showImageSearchModal={isImageSearchModalOpen}
|
|
59
|
+
onImageSearchModalClose={handleImageSearchModalClose}
|
|
60
|
+
uploadedImageFile={uploadedImageFile}
|
|
61
|
+
onImageUpload={handleImageUpload}
|
|
62
|
+
showResetButton={false}
|
|
63
|
+
/>
|
|
64
|
+
</>
|
|
65
|
+
)}
|
|
66
|
+
</>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { Button } from '@akinon/next/components';
|
|
5
|
+
import { useLocalization } from '@akinon/next/hooks';
|
|
6
|
+
import { useImageSearchFeature } from '../hooks/use-image-search-feature';
|
|
7
|
+
|
|
8
|
+
interface ImageSearchButtonProps {
|
|
9
|
+
onClick: () => void;
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function ImageSearchButton({
|
|
14
|
+
onClick,
|
|
15
|
+
className
|
|
16
|
+
}: ImageSearchButtonProps) {
|
|
17
|
+
const { t } = useLocalization();
|
|
18
|
+
const { isEnabled, isLoading } = useImageSearchFeature();
|
|
19
|
+
|
|
20
|
+
if (isLoading || !isEnabled) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<Button
|
|
26
|
+
onClick={onClick}
|
|
27
|
+
className={`flex items-center justify-center mr-2 text-gray-500 focus:outline-none border-none bg-transparent ${className || ''}`}
|
|
28
|
+
title={t('common.search.image_search.title')}
|
|
29
|
+
>
|
|
30
|
+
<svg
|
|
31
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
32
|
+
width="20"
|
|
33
|
+
height="20"
|
|
34
|
+
viewBox="0 0 24 24"
|
|
35
|
+
fill="none"
|
|
36
|
+
stroke="currentColor"
|
|
37
|
+
strokeWidth="2"
|
|
38
|
+
strokeLinecap="round"
|
|
39
|
+
strokeLinejoin="round"
|
|
40
|
+
className="text-gray-500"
|
|
41
|
+
>
|
|
42
|
+
<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>
|
|
43
|
+
<circle cx="12" cy="13" r="4"></circle>
|
|
44
|
+
</svg>
|
|
45
|
+
</Button>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useEffect } from 'react';
|
|
4
|
+
import { Modal, Button, Input } from '@akinon/next/components';
|
|
5
|
+
import { useLocalization } from '@akinon/next/hooks';
|
|
6
|
+
|
|
7
|
+
interface ImageSearchModalProps {
|
|
8
|
+
isOpen: boolean;
|
|
9
|
+
setIsOpen: (open: boolean) => void;
|
|
10
|
+
fileInputRef: React.RefObject<HTMLInputElement>;
|
|
11
|
+
handleImageFileChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
|
12
|
+
className?: string;
|
|
13
|
+
clearError?: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
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
|
+
export function ImageSearchModal({
|
|
62
|
+
isOpen,
|
|
63
|
+
setIsOpen,
|
|
64
|
+
fileInputRef,
|
|
65
|
+
handleImageFileChange,
|
|
66
|
+
className,
|
|
67
|
+
clearError
|
|
68
|
+
}: ImageSearchModalProps) {
|
|
69
|
+
const { t } = useLocalization();
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (isOpen && clearError) {
|
|
73
|
+
clearError();
|
|
74
|
+
}
|
|
75
|
+
}, [isOpen, clearError]);
|
|
76
|
+
|
|
77
|
+
const handleFileSelect = async (
|
|
78
|
+
event: React.ChangeEvent<HTMLInputElement>
|
|
79
|
+
) => {
|
|
80
|
+
const file = event.target.files?.[0];
|
|
81
|
+
|
|
82
|
+
if (!file) return;
|
|
83
|
+
|
|
84
|
+
if (file.type === 'image/webp') {
|
|
85
|
+
try {
|
|
86
|
+
const convertedFile = await convertWebPToJPEG(file);
|
|
87
|
+
|
|
88
|
+
const modifiedEvent = {
|
|
89
|
+
...event,
|
|
90
|
+
target: {
|
|
91
|
+
...event.target,
|
|
92
|
+
files: [convertedFile] as any
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
handleImageFileChange(
|
|
97
|
+
modifiedEvent as React.ChangeEvent<HTMLInputElement>
|
|
98
|
+
);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
console.error('WebP conversion failed:', error);
|
|
101
|
+
|
|
102
|
+
handleImageFileChange(event);
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
handleImageFileChange(event);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<Modal
|
|
111
|
+
portalId="image-search-modal"
|
|
112
|
+
title={t('common.search.image_search.title')}
|
|
113
|
+
open={isOpen}
|
|
114
|
+
setOpen={setIsOpen}
|
|
115
|
+
className={`w-full max-w-2xl max-h-[90vh] overflow-auto ${className || ''}`}
|
|
116
|
+
>
|
|
117
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
118
|
+
<div className="md:col-span-1 p-4">
|
|
119
|
+
<p className="mb-4 text-lg font-medium">
|
|
120
|
+
{t('common.search.image_search.upload_image')}
|
|
121
|
+
</p>
|
|
122
|
+
<div className="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center">
|
|
123
|
+
<Input
|
|
124
|
+
type="file"
|
|
125
|
+
ref={fileInputRef}
|
|
126
|
+
onChange={handleFileSelect}
|
|
127
|
+
accept="image/*"
|
|
128
|
+
className="hidden"
|
|
129
|
+
/>
|
|
130
|
+
<Button
|
|
131
|
+
className="w-full py-4 px-6 border-2 border-primary text-white hover:bg-primary hover:text-white transition-colors rounded-md"
|
|
132
|
+
onClick={() => fileInputRef.current?.click()}
|
|
133
|
+
>
|
|
134
|
+
{t('common.search.image_search.select_image')}
|
|
135
|
+
</Button>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
<div className="md:col-span-1 bg-gray-50 p-4 rounded-md h-fit my-4">
|
|
139
|
+
<p className="mb-2 font-medium">
|
|
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>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState, useRef, useEffect } from 'react';
|
|
4
|
+
import { Product } from '@akinon/next/types';
|
|
5
|
+
import { useImageCropper } from '../hooks/use-image-cropper';
|
|
6
|
+
import { SimilarProductsModal } from './search-modal';
|
|
7
|
+
import { ImageSearchModal } from './image-search';
|
|
8
|
+
import { useSimilarProducts } from '../hooks/use-similar-products';
|
|
9
|
+
import { mergeSettings } from '../utils';
|
|
10
|
+
|
|
11
|
+
interface SimilarProductsPluginProps {
|
|
12
|
+
product: Product;
|
|
13
|
+
isOpen?: boolean;
|
|
14
|
+
onClose?: () => void;
|
|
15
|
+
activeIndex?: number;
|
|
16
|
+
settings?: any;
|
|
17
|
+
showImageSearchModal?: boolean;
|
|
18
|
+
onImageSearchModalClose?: () => void;
|
|
19
|
+
uploadedImageFile?: File | null;
|
|
20
|
+
onImageUpload?: (file: File) => void;
|
|
21
|
+
showResetButton?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function SimilarProductsPlugin({
|
|
25
|
+
product,
|
|
26
|
+
isOpen = false,
|
|
27
|
+
onClose = () => {},
|
|
28
|
+
activeIndex = 0,
|
|
29
|
+
settings: userSettings,
|
|
30
|
+
showImageSearchModal = false,
|
|
31
|
+
onImageSearchModalClose = () => {},
|
|
32
|
+
uploadedImageFile = null,
|
|
33
|
+
onImageUpload = () => {},
|
|
34
|
+
showResetButton = true
|
|
35
|
+
}: SimilarProductsPluginProps) {
|
|
36
|
+
const settings = mergeSettings(userSettings);
|
|
37
|
+
const [isFilterMenuOpen, setIsFilterMenuOpen] = useState(false);
|
|
38
|
+
|
|
39
|
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
40
|
+
|
|
41
|
+
const {
|
|
42
|
+
currentImageUrl,
|
|
43
|
+
setCurrentImageUrl,
|
|
44
|
+
isLoading,
|
|
45
|
+
fileError,
|
|
46
|
+
searchResults,
|
|
47
|
+
resultsKey,
|
|
48
|
+
hasUploadedImage,
|
|
49
|
+
setHasUploadedImage,
|
|
50
|
+
handleFileUpload,
|
|
51
|
+
handleSortChange,
|
|
52
|
+
handlePageChange,
|
|
53
|
+
handleFacetChange,
|
|
54
|
+
removeFacetFilter,
|
|
55
|
+
handleLoadMore,
|
|
56
|
+
loadedPages,
|
|
57
|
+
fetchSimilarProductsByImageUrl,
|
|
58
|
+
fetchSimilarProductsByImageCrop,
|
|
59
|
+
clearError
|
|
60
|
+
} = useSimilarProducts(product);
|
|
61
|
+
|
|
62
|
+
const {
|
|
63
|
+
isCropping,
|
|
64
|
+
crop,
|
|
65
|
+
setCrop,
|
|
66
|
+
completedCrop,
|
|
67
|
+
setCompletedCrop,
|
|
68
|
+
imageRef,
|
|
69
|
+
handleCropComplete,
|
|
70
|
+
toggleCropMode,
|
|
71
|
+
processCompletedCrop,
|
|
72
|
+
processManualCrop,
|
|
73
|
+
resetCrop
|
|
74
|
+
} = useImageCropper(
|
|
75
|
+
(loading) => {},
|
|
76
|
+
fetchSimilarProductsByImageCrop,
|
|
77
|
+
clearError
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const handleResetToOriginal = () => {
|
|
81
|
+
if (product?.productimage_set?.length > 0) {
|
|
82
|
+
const originalImageUrl =
|
|
83
|
+
product.productimage_set[activeIndex]?.image ||
|
|
84
|
+
product.productimage_set[0].image;
|
|
85
|
+
setCurrentImageUrl(originalImageUrl);
|
|
86
|
+
setHasUploadedImage(false);
|
|
87
|
+
resetCrop();
|
|
88
|
+
fetchSimilarProductsByImageUrl(originalImageUrl);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const [hasInitialSearchDone, setHasInitialSearchDone] = useState(false);
|
|
93
|
+
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
if (
|
|
96
|
+
isOpen &&
|
|
97
|
+
product?.productimage_set?.length > 0 &&
|
|
98
|
+
!hasUploadedImage &&
|
|
99
|
+
!hasInitialSearchDone
|
|
100
|
+
) {
|
|
101
|
+
const initialImageUrl =
|
|
102
|
+
product.productimage_set[activeIndex]?.image ||
|
|
103
|
+
product.productimage_set[0].image;
|
|
104
|
+
fetchSimilarProductsByImageUrl(initialImageUrl);
|
|
105
|
+
setHasInitialSearchDone(true);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!isOpen) {
|
|
109
|
+
setHasInitialSearchDone(false);
|
|
110
|
+
clearError();
|
|
111
|
+
}
|
|
112
|
+
}, [
|
|
113
|
+
isOpen,
|
|
114
|
+
product?.productimage_set?.length,
|
|
115
|
+
activeIndex,
|
|
116
|
+
hasUploadedImage,
|
|
117
|
+
hasInitialSearchDone,
|
|
118
|
+
clearError
|
|
119
|
+
]);
|
|
120
|
+
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
if (uploadedImageFile && isOpen) {
|
|
123
|
+
const reader = new FileReader();
|
|
124
|
+
reader.onload = (e) => {
|
|
125
|
+
const result = e.target?.result as string;
|
|
126
|
+
if (result) {
|
|
127
|
+
setCurrentImageUrl(result);
|
|
128
|
+
setHasUploadedImage(true);
|
|
129
|
+
fetchSimilarProductsByImageCrop(result);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
reader.readAsDataURL(uploadedImageFile);
|
|
133
|
+
}
|
|
134
|
+
}, [uploadedImageFile, isOpen]);
|
|
135
|
+
|
|
136
|
+
const handleImageSearchFileChange = (
|
|
137
|
+
event: React.ChangeEvent<HTMLInputElement>
|
|
138
|
+
) => {
|
|
139
|
+
const file = event.target.files?.[0];
|
|
140
|
+
if (file) {
|
|
141
|
+
onImageUpload(file);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const ModalComponent =
|
|
146
|
+
settings.customRenderers?.Modal || SimilarProductsModal;
|
|
147
|
+
const ImageSearchModalComponent =
|
|
148
|
+
settings.customRenderers?.ImageSearchModal || ImageSearchModal;
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<>
|
|
152
|
+
<ModalComponent
|
|
153
|
+
isOpen={isOpen}
|
|
154
|
+
onClose={onClose}
|
|
155
|
+
searchResults={searchResults}
|
|
156
|
+
resultsKey={resultsKey}
|
|
157
|
+
isLoading={isLoading}
|
|
158
|
+
isFilterMenuOpen={isFilterMenuOpen}
|
|
159
|
+
setIsFilterMenuOpen={setIsFilterMenuOpen}
|
|
160
|
+
handleSortChange={handleSortChange}
|
|
161
|
+
handlePageChange={handlePageChange}
|
|
162
|
+
handleFacetChange={handleFacetChange}
|
|
163
|
+
removeFacetFilter={removeFacetFilter}
|
|
164
|
+
handleLoadMore={handleLoadMore}
|
|
165
|
+
loadedPages={loadedPages}
|
|
166
|
+
currentImageUrl={currentImageUrl}
|
|
167
|
+
isCropping={isCropping}
|
|
168
|
+
imageRef={imageRef}
|
|
169
|
+
fileInputRef={fileInputRef}
|
|
170
|
+
crop={crop}
|
|
171
|
+
setCrop={setCrop}
|
|
172
|
+
completedCrop={completedCrop}
|
|
173
|
+
setCompletedCrop={setCompletedCrop}
|
|
174
|
+
handleCropComplete={handleCropComplete}
|
|
175
|
+
toggleCropMode={toggleCropMode}
|
|
176
|
+
processCompletedCrop={processCompletedCrop}
|
|
177
|
+
processManualCrop={processManualCrop}
|
|
178
|
+
resetCrop={resetCrop}
|
|
179
|
+
product={product}
|
|
180
|
+
activeIndex={activeIndex}
|
|
181
|
+
hasUploadedImage={hasUploadedImage}
|
|
182
|
+
handleFileUpload={handleFileUpload}
|
|
183
|
+
handleResetToOriginal={handleResetToOriginal}
|
|
184
|
+
fileError={fileError}
|
|
185
|
+
settings={settings}
|
|
186
|
+
className={settings.customStyles?.modal}
|
|
187
|
+
showResetButton={showResetButton}
|
|
188
|
+
/>
|
|
189
|
+
|
|
190
|
+
<ImageSearchModalComponent
|
|
191
|
+
isOpen={showImageSearchModal}
|
|
192
|
+
setIsOpen={onImageSearchModalClose}
|
|
193
|
+
fileInputRef={fileInputRef}
|
|
194
|
+
handleImageFileChange={handleImageSearchFileChange}
|
|
195
|
+
className={settings.customStyles?.imageSearchModal}
|
|
196
|
+
clearError={clearError}
|
|
197
|
+
/>
|
|
198
|
+
</>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
import { Product } from '@akinon/next/types';
|
|
5
|
+
import { SimilarProductsButton } from './search-button';
|
|
6
|
+
import { SimilarProductsPlugin } from './main';
|
|
7
|
+
import { mergeSettings } from '../utils';
|
|
8
|
+
import { useImageSearchFeature } from '../hooks/use-image-search-feature';
|
|
9
|
+
|
|
10
|
+
interface ProductImageSearchFeatureProps {
|
|
11
|
+
product: Product;
|
|
12
|
+
activeIndex?: number;
|
|
13
|
+
settings?: any;
|
|
14
|
+
className?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function ProductImageSearchFeature({
|
|
18
|
+
product,
|
|
19
|
+
activeIndex = 0,
|
|
20
|
+
settings: userSettings,
|
|
21
|
+
className = 'absolute top-6 left-6 z-[20]'
|
|
22
|
+
}: ProductImageSearchFeatureProps) {
|
|
23
|
+
const settings = mergeSettings(userSettings);
|
|
24
|
+
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
25
|
+
const { isEnabled } = useImageSearchFeature();
|
|
26
|
+
|
|
27
|
+
const handleClick = () => {
|
|
28
|
+
setIsModalOpen(true);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<>
|
|
33
|
+
{isEnabled && (
|
|
34
|
+
<>
|
|
35
|
+
<SimilarProductsButton onClick={handleClick} className={className} />
|
|
36
|
+
|
|
37
|
+
<SimilarProductsPlugin
|
|
38
|
+
product={product}
|
|
39
|
+
isOpen={isModalOpen}
|
|
40
|
+
onClose={() => setIsModalOpen(false)}
|
|
41
|
+
activeIndex={activeIndex}
|
|
42
|
+
settings={settings}
|
|
43
|
+
/>
|
|
44
|
+
</>
|
|
45
|
+
)}
|
|
46
|
+
</>
|
|
47
|
+
);
|
|
48
|
+
}
|