@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.
@@ -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 || !isEnabled) {
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
- {isEnabled && (
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
- <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()}
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
- {t('common.search.image_search.select_image')}
135
- </Button>
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
- <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>
201
+ </Modal>
202
+ </>
151
203
  );
152
204
  }
@@ -194,6 +194,7 @@ export function SimilarProductsPlugin({
194
194
  handleImageFileChange={handleImageSearchFileChange}
195
195
  className={settings.customStyles?.imageSearchModal}
196
196
  clearError={clearError}
197
+ settings={settings}
197
198
  />
198
199
  </>
199
200
  );
@@ -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
- {isEnabled && (
42
+ {finalIsEnabled && (
34
43
  <>
35
44
  <SimilarProductsButton onClick={handleClick} className={className} />
36
45
 
@@ -95,7 +95,12 @@ export function SimilarProductsResultsGrid({
95
95
 
96
96
  return (
97
97
  <div className={emptyStateClassName}>
98
- <div className="text-center">
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 className="price-container">
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 className="flex flex-wrap gap-2">
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 className="flex items-center justify-between p-3 md:p-4">
352
- <div className="flex items-center gap-3">
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 className="relative">{renderSortDropdown()}</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
- {/* Main Content */}
375
- <div className="flex flex-col md:grid md:grid-cols-5 gap-0 flex-1 overflow-hidden">
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 className="flex-1 md:col-span-4 p-3 md:p-4 overflow-y-auto">
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}