@akinon/pz-similar-products 1.114.0-snapshot-ZERO-3890-20251209002835 → 1.115.0-rc.23

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.
@@ -1,43 +1,47 @@
1
1
  'use client';
2
2
 
3
3
  import React from 'react';
4
- import { Button, Icon } from '@akinon/next/components';
4
+ import { Button } from '@akinon/next/components';
5
5
  import { useLocalization } from '@akinon/next/hooks';
6
- import { twMerge } from 'tailwind-merge';
6
+ import { useImageSearchFeature } from '../hooks/use-image-search-feature';
7
7
 
8
8
  interface ImageSearchButtonProps {
9
9
  onClick: () => void;
10
10
  className?: string;
11
- settings?: any;
12
11
  }
13
12
 
14
13
  export function ImageSearchButton({
15
14
  onClick,
16
- className,
17
- settings
15
+ className
18
16
  }: ImageSearchButtonProps) {
19
17
  const { t } = useLocalization();
18
+ const { isEnabled, isLoading } = useImageSearchFeature();
20
19
 
21
- const imageSearchButtonIconName =
22
- settings?.iconNames?.imageSearchButton || 'search';
23
- const imageSearchButtonIconClassName = twMerge(
24
- 'text-black',
25
- settings?.customStyles?.imageSearchButtonIcon
26
- );
20
+ if (isLoading || !isEnabled) {
21
+ return null;
22
+ }
27
23
 
28
24
  return (
29
25
  <Button
30
26
  onClick={onClick}
31
- className={`flex items-center justify-center mr-2 text-gray-500 focus:outline-none border-none bg-transparent ${
32
- className || ''
33
- }`}
27
+ className={`flex items-center justify-center mr-2 text-gray-500 focus:outline-none border-none bg-transparent ${className || ''}`}
34
28
  title={t('common.search.image_search.title')}
35
29
  >
36
- <Icon
37
- name={imageSearchButtonIconName}
38
- size={20}
39
- className={imageSearchButtonIconClassName}
40
- />
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>
41
45
  </Button>
42
46
  );
43
47
  }
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import React, { useState, useRef, useEffect, useCallback } from 'react';
3
+ import React, { useState, useRef, useEffect } from 'react';
4
4
  import { Product } from '@akinon/next/types';
5
5
  import { useImageCropper } from '../hooks/use-image-cropper';
6
6
  import { SimilarProductsModal } from './search-modal';
@@ -37,39 +37,12 @@ export function SimilarProductsPlugin({
37
37
  const [isFilterMenuOpen, setIsFilterMenuOpen] = useState(false);
38
38
 
39
39
  const fileInputRef = useRef<HTMLInputElement>(null);
40
- const hasUploadedImageRef = useRef<boolean>(false);
41
- const [isImageUploadedViaFileInput, setIsImageUploadedViaFileInput] = useState(false);
42
-
43
- const UPLOAD_FLAG_KEY = `similar-products-upload-${product.pk}`;
44
-
45
- const setUploadFlag = (value: boolean) => {
46
- try {
47
- if (value) {
48
- sessionStorage.setItem(UPLOAD_FLAG_KEY, 'true');
49
- } else {
50
- sessionStorage.removeItem(UPLOAD_FLAG_KEY);
51
- }
52
- } catch (error) {
53
- console.error('Session storage error:', error);
54
- }
55
- };
56
-
57
- const getUploadFlag = () => {
58
- try {
59
- return sessionStorage.getItem(UPLOAD_FLAG_KEY) === 'true';
60
- } catch (error) {
61
- console.error('Session storage error:', error);
62
- return false;
63
- }
64
- };
65
40
 
66
41
  const {
67
42
  currentImageUrl,
68
43
  setCurrentImageUrl,
69
44
  isLoading,
70
45
  fileError,
71
- searchText,
72
- setSearchText,
73
46
  searchResults,
74
47
  resultsKey,
75
48
  hasUploadedImage,
@@ -84,47 +57,9 @@ export function SimilarProductsPlugin({
84
57
  fetchSimilarProductsByImageUrl,
85
58
  fetchSimilarProductsByImageCrop,
86
59
  clearError,
87
- clearResults,
88
- handleTextSearch,
89
- handleClearText,
90
- resetCropState
60
+ clearResults
91
61
  } = useSimilarProducts(product);
92
62
 
93
- useEffect(() => {
94
- if (hasUploadedImage) {
95
- setIsImageUploadedViaFileInput(true);
96
- setUploadFlag(true);
97
- } else {
98
- setIsImageUploadedViaFileInput(false);
99
- setUploadFlag(false);
100
- }
101
- }, [hasUploadedImage]);
102
-
103
- const cropProcessImageFunction = useCallback(
104
- (base64Image: string) => {
105
- let shouldExclude = true;
106
-
107
- if (base64Image.startsWith('data:application/x-cors-fallback;base64,')) {
108
- try {
109
- const fallbackDataEncoded = base64Image.replace('data:application/x-cors-fallback;base64,', '');
110
- const fallbackData = JSON.parse(atob(fallbackDataEncoded));
111
-
112
- if (fallbackData.originalUrl && fallbackData.originalUrl.startsWith('data:')) {
113
- shouldExclude = false;
114
- }
115
- } catch (error) {
116
- console.error('CORS fallback parse error:', error);
117
- }
118
- } else {
119
- const isUploadedFromStorage = getUploadFlag();
120
- shouldExclude = !isUploadedFromStorage;
121
- }
122
-
123
- fetchSimilarProductsByImageCrop(base64Image, shouldExclude);
124
- },
125
- [fetchSimilarProductsByImageCrop, hasUploadedImage, currentImageUrl, uploadedImageFile, isImageUploadedViaFileInput]
126
- );
127
-
128
63
  const {
129
64
  isCropping,
130
65
  crop,
@@ -139,7 +74,7 @@ export function SimilarProductsPlugin({
139
74
  resetCrop
140
75
  } = useImageCropper(
141
76
  (loading) => {},
142
- cropProcessImageFunction,
77
+ fetchSimilarProductsByImageCrop,
143
78
  clearError
144
79
  );
145
80
 
@@ -150,12 +85,8 @@ export function SimilarProductsPlugin({
150
85
  product.productimage_set[0].image;
151
86
  setCurrentImageUrl(originalImageUrl);
152
87
  setHasUploadedImage(false);
153
- hasUploadedImageRef.current = false;
154
- setUploadFlag(false);
155
- setSearchText('');
156
88
  resetCrop();
157
- resetCropState();
158
- fetchSimilarProductsByImageUrl(originalImageUrl, '');
89
+ fetchSimilarProductsByImageUrl(originalImageUrl);
159
90
  }
160
91
  };
161
92
 
@@ -177,8 +108,6 @@ export function SimilarProductsPlugin({
177
108
 
178
109
  if (!isOpen) {
179
110
  setHasInitialSearchDone(false);
180
- hasUploadedImageRef.current = false;
181
- setUploadFlag(false);
182
111
  clearError();
183
112
  clearResults();
184
113
  }
@@ -200,9 +129,7 @@ export function SimilarProductsPlugin({
200
129
  if (result) {
201
130
  setCurrentImageUrl(result);
202
131
  setHasUploadedImage(true);
203
- hasUploadedImageRef.current = true;
204
- setUploadFlag(true);
205
- fetchSimilarProductsByImageCrop(result, false);
132
+ fetchSimilarProductsByImageCrop(result);
206
133
  }
207
134
  };
208
135
  reader.readAsDataURL(uploadedImageFile);
@@ -266,10 +193,6 @@ export function SimilarProductsPlugin({
266
193
  settings={settings}
267
194
  className={settings.customStyles?.modal}
268
195
  showResetButton={showResetButton}
269
- searchText={searchText}
270
- setSearchText={setSearchText}
271
- handleTextSearch={handleTextSearch}
272
- handleClearText={handleClearText}
273
196
  />
274
197
 
275
198
  <ImageSearchModalComponent
@@ -5,6 +5,7 @@ import { Product } from '@akinon/next/types';
5
5
  import { SimilarProductsButton } from './search-button';
6
6
  import { SimilarProductsPlugin } from './main';
7
7
  import { mergeSettings } from '../utils';
8
+ import { useImageSearchFeature } from '../hooks/use-image-search-feature';
8
9
 
9
10
  interface ProductImageSearchFeatureProps {
10
11
  product: Product;
@@ -12,7 +13,6 @@ interface ProductImageSearchFeatureProps {
12
13
  settings?: any;
13
14
  className?: string;
14
15
  isEnabled?: boolean;
15
- enableTextSearch?: boolean;
16
16
  }
17
17
 
18
18
  export function ProductImageSearchFeature({
@@ -20,18 +20,18 @@ export function ProductImageSearchFeature({
20
20
  activeIndex = 0,
21
21
  settings: userSettings,
22
22
  className = 'absolute top-6 left-6 z-[20]',
23
- isEnabled = false,
24
- enableTextSearch = false
23
+ isEnabled: isEnabledProp
25
24
  }: ProductImageSearchFeatureProps) {
26
- const settings = mergeSettings({
27
- ...userSettings,
28
- enableTextSearch
29
- });
25
+ const settings = mergeSettings(userSettings);
30
26
  const [isModalOpen, setIsModalOpen] = useState(false);
27
+ const { isEnabled: hookIsEnabled } = useImageSearchFeature();
31
28
 
32
- if (!isEnabled) {
33
- return null;
34
- }
29
+ const envEnabled = process.env.NEXT_PUBLIC_ENABLE_IMAGE_SEARCH === 'true';
30
+ const finalIsEnabled = envEnabled
31
+ ? true
32
+ : isEnabledProp !== undefined
33
+ ? isEnabledProp
34
+ : hookIsEnabled;
35
35
 
36
36
  const handleClick = () => {
37
37
  setIsModalOpen(true);
@@ -39,19 +39,19 @@ export function ProductImageSearchFeature({
39
39
 
40
40
  return (
41
41
  <>
42
- <SimilarProductsButton
43
- onClick={handleClick}
44
- className={className}
45
- settings={settings}
46
- />
42
+ {finalIsEnabled && (
43
+ <>
44
+ <SimilarProductsButton onClick={handleClick} className={className} />
47
45
 
48
- <SimilarProductsPlugin
49
- product={product}
50
- isOpen={isModalOpen}
51
- onClose={() => setIsModalOpen(false)}
52
- activeIndex={activeIndex}
53
- settings={settings}
54
- />
46
+ <SimilarProductsPlugin
47
+ product={product}
48
+ isOpen={isModalOpen}
49
+ onClose={() => setIsModalOpen(false)}
50
+ activeIndex={activeIndex}
51
+ settings={settings}
52
+ />
53
+ </>
54
+ )}
55
55
  </>
56
56
  );
57
57
  }
@@ -3,32 +3,22 @@
3
3
  import React from 'react';
4
4
  import { Icon } from '@akinon/next/components';
5
5
  import { useLocalization } from '@akinon/next/hooks';
6
- import { twMerge } from 'tailwind-merge';
7
6
 
8
7
  interface SimilarProductsButtonProps {
9
8
  onClick: () => void;
10
9
  className?: string;
11
10
  isLoading?: boolean;
12
11
  disabled?: boolean;
13
- settings?: any;
14
12
  }
15
13
 
16
14
  export function SimilarProductsButton({
17
15
  onClick,
18
16
  className = '',
19
17
  isLoading = false,
20
- disabled = false,
21
- settings
18
+ disabled = false
22
19
  }: SimilarProductsButtonProps) {
23
20
  const { t } = useLocalization();
24
21
 
25
- const searchButtonIconClassName = twMerge(
26
- 'fill-black',
27
- settings?.customStyles?.searchButtonIcon
28
- );
29
-
30
- const searchButtonIconName = settings?.iconNames?.searchButton || 'search';
31
-
32
22
  return (
33
23
  <button
34
24
  onClick={onClick}
@@ -38,7 +28,7 @@ export function SimilarProductsButton({
38
28
  } ${className}`}
39
29
  >
40
30
  <div className="flex items-center gap-2">
41
- <Icon name={searchButtonIconName} size={16} className={searchButtonIconClassName} />
31
+ <Icon name="search" size={16} className="fill-black" />
42
32
  <span className="text-xs font-medium text-black uppercase">
43
33
  {t('common.product.view_similar_styles')}
44
34
  </span>
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import React from 'react';
4
- import { Modal, Button, Icon, Select, Input } from '@akinon/next/components';
4
+ import { Modal, Button, Icon, Select } from '@akinon/next/components';
5
5
  import { useLocalization } from '@akinon/next/hooks';
6
6
  import { SimilarProductsFilterSidebar } from './filters';
7
7
  import { SimilarProductsResultsGrid } from './results';
@@ -43,11 +43,7 @@ export function SimilarProductsModal({
43
43
  fileError,
44
44
  showResetButton = true,
45
45
  settings,
46
- className,
47
- searchText,
48
- setSearchText,
49
- handleTextSearch,
50
- handleClearText
46
+ className
51
47
  }: SimilarProductsModalProps) {
52
48
  const { t } = useLocalization();
53
49
 
@@ -168,24 +164,6 @@ export function SimilarProductsModal({
168
164
  })) || []
169
165
  ) || [];
170
166
 
171
- const modalCloseIconName = settings?.iconNames?.modalClose || 'close';
172
- const filterIconName = settings?.iconNames?.filter || 'filter';
173
- const modalSearchIconName = settings?.iconNames?.modalSearch || 'search';
174
- const modalSearchClearIconName =
175
- settings?.iconNames?.modalSearchClear || 'close';
176
-
177
- const modalCloseIconClassName = twMerge(
178
- '',
179
- settings?.customStyles?.modalCloseIcon
180
- );
181
-
182
- const filterIconClassName = twMerge('', settings?.customStyles?.filterIcon);
183
-
184
- const modalSearchIconClassName = twMerge(
185
- 'text-gray-500',
186
- settings?.customStyles?.modalSearchIcon
187
- );
188
-
189
167
  const renderHeader = () => {
190
168
  if (settings?.customRenderers?.render?.modal?.renderHeader) {
191
169
  return settings.customRenderers.render.modal.renderHeader({
@@ -268,11 +246,7 @@ export function SimilarProductsModal({
268
246
  disabled={isLoading}
269
247
  className={buttonClassName}
270
248
  >
271
- <Icon
272
- name={modalCloseIconName}
273
- size={12}
274
- className={modalCloseIconClassName}
275
- />
249
+ <Icon name="close" size={12} />
276
250
  </Button>
277
251
  </div>
278
252
  );
@@ -292,9 +266,7 @@ export function SimilarProductsModal({
292
266
  '',
293
267
  onSortChange: handleSortChange,
294
268
  onFilterMenuToggle: () => setIsFilterMenuOpen(true),
295
- isLoading,
296
- searchText,
297
- setSearchText
269
+ isLoading
298
270
  });
299
271
  }
300
272
 
@@ -309,30 +281,15 @@ export function SimilarProductsModal({
309
281
  );
310
282
 
311
283
  const filterToggleClassName = twMerge(
312
- 'md:hidden text-xs px-3 py-2',
284
+ 'md:hidden text-xs',
313
285
  settings?.customStyles?.filterToggleButton
314
286
  );
315
287
 
316
288
  const sortDropdownClassName = twMerge(
317
- 'h-10 px-3 md:px-4 text-sm md:text-xs bg-gray-200 hover:bg-gray-400 transition-colors duration-200 border-gray-300 focus:border-primary focus:ring-1 focus:ring-primary w-full md:w-40 min-w-[120px]',
289
+ 'h-10 px-4 text-md md:text-xs bg-gray-200 hover:bg-gray-400 transition-colors duration-200 border-gray-300 focus:border-primary focus:ring-1 focus:ring-primary w-full md:w-40 min-w-[120px]',
318
290
  settings?.customStyles?.sortDropdown
319
291
  );
320
292
 
321
- const modalSearchInputClassName = twMerge(
322
- 'h-10 px-3 md:px-4 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent w-48 md:w-60 min-w-[180px]',
323
- settings?.customStyles?.modalSearchInput
324
- );
325
-
326
- const modalSearchContainerClassName = twMerge(
327
- 'relative flex items-center',
328
- settings?.customStyles?.modalSearchContainer
329
- );
330
-
331
- const modalSearchButtonClassName = twMerge(
332
- 'absolute right-2 top-1/2 -translate-y-1/2 p-2 rounded-md hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed transition-colors',
333
- settings?.customStyles?.modalSearchButton
334
- );
335
-
336
293
  const renderItemCount = () => {
337
294
  if (settings?.customRenderers?.render?.modal?.renderItemCount) {
338
295
  return settings.customRenderers.render.modal.renderItemCount({
@@ -342,9 +299,7 @@ export function SimilarProductsModal({
342
299
  }
343
300
  return (
344
301
  <div className={itemCountClassName}>
345
- <span className="hidden md:inline">
346
- {searchResults?.pagination?.total_count || 0} items
347
- </span>
302
+ {searchResults?.pagination?.total_count || 0} items
348
303
  </div>
349
304
  );
350
305
  };
@@ -364,11 +319,7 @@ export function SimilarProductsModal({
364
319
  onClick={() => setIsFilterMenuOpen(true)}
365
320
  data-testid="similar-products-filter"
366
321
  >
367
- <Icon
368
- name={filterIconName}
369
- size={14}
370
- className={filterIconClassName}
371
- />
322
+ <Icon name="filter" size={14} />
372
323
  {t('common.product.filters')}
373
324
  </Button>
374
325
  );
@@ -404,134 +355,26 @@ export function SimilarProductsModal({
404
355
  );
405
356
  };
406
357
 
407
- const renderModalSearchInput = () => {
408
- if (
409
- !settings?.enableTextSearch ||
410
- searchText === undefined ||
411
- !setSearchText ||
412
- !searchResults?.products?.length ||
413
- !searchResults?.sorters?.length
414
- ) {
415
- return null;
416
- }
417
-
418
- if (settings?.customRenderers?.render?.modal?.renderModalSearchInput) {
419
- return settings.customRenderers.render.modal.renderModalSearchInput({
420
- searchText,
421
- setSearchText,
422
- isLoading,
423
- placeholder: t('common.search.placeholder'),
424
- onSearch: handleTextSearch
425
- });
426
- }
427
-
428
- return (
429
- <div className={modalSearchContainerClassName}>
430
- <Input
431
- type="text"
432
- value={searchText}
433
- onChange={(e) => setSearchText(e.currentTarget.value)}
434
- onKeyDown={(e) => {
435
- if (e.key === 'Enter' && handleTextSearch) {
436
- e.preventDefault();
437
- handleTextSearch();
438
- }
439
- }}
440
- placeholder={t('common.search.placeholder')}
441
- className={twMerge(modalSearchInputClassName, 'pr-20')}
442
- disabled={isLoading}
443
- />
444
- {searchText && (
445
- <button
446
- onClick={() => {
447
- if (handleClearText) {
448
- handleClearText();
449
- }
450
- }}
451
- disabled={isLoading}
452
- className={twMerge(
453
- 'absolute right-12 top-1/2 -translate-y-1/2 p-2 rounded-md hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed transition-colors',
454
- settings?.customStyles?.modalSearchClearButton
455
- )}
456
- >
457
- <Icon
458
- name={modalSearchClearIconName}
459
- size={16}
460
- className={twMerge(
461
- 'text-gray-400 hover:text-gray-600',
462
- settings?.customStyles?.modalSearchClearIcon
463
- )}
464
- />
465
- </button>
466
- )}
467
- <button
468
- onClick={() => {
469
- if (handleTextSearch) {
470
- handleTextSearch();
471
- }
472
- }}
473
- disabled={isLoading}
474
- className={modalSearchButtonClassName}
475
- >
476
- {settings?.customRenderers?.render?.modal?.renderSearchIcon ? (
477
- settings.customRenderers.render.modal.renderSearchIcon({
478
- disabled: isLoading,
479
- onClick: handleTextSearch
480
- })
481
- ) : (
482
- <Icon
483
- name={modalSearchIconName}
484
- size={18}
485
- className={modalSearchIconClassName}
486
- />
487
- )}
488
- </button>
489
- </div>
490
- );
491
- };
492
-
493
358
  return (
494
359
  <div className={containerClassName}>
495
360
  <div
496
361
  className={twMerge(
497
- 'p-3 md:p-4',
498
- settings?.enableTextSearch &&
499
- searchText !== undefined &&
500
- setSearchText
501
- ? 'flex gap-3 md:grid md:grid-cols-3 md:gap-4 md:items-center'
502
- : 'flex items-center justify-between',
362
+ 'flex items-center justify-between p-3 md:p-4',
503
363
  settings?.customStyles?.controlsInner
504
364
  )}
505
365
  >
506
366
  <div
507
367
  className={twMerge(
508
- 'flex items-center gap-2 md:gap-3',
368
+ 'flex items-center gap-3',
509
369
  settings?.customStyles?.controlsLeft
510
370
  )}
511
371
  >
512
372
  {renderItemCount()}
513
373
  {renderFilterToggle()}
514
374
  </div>
515
- {settings?.enableTextSearch &&
516
- searchText !== undefined &&
517
- setSearchText && (
518
- <div
519
- className={twMerge(
520
- 'flex items-center justify-center w-full',
521
- settings?.customStyles?.controlsCenter
522
- )}
523
- >
524
- {renderModalSearchInput()}
525
- </div>
526
- )}
527
375
  <div
528
376
  className={twMerge(
529
377
  'relative',
530
- settings?.enableTextSearch &&
531
- searchText !== undefined &&
532
- setSearchText
533
- ? 'flex justify-end md:mt-0'
534
- : '',
535
378
  settings?.customStyles?.controlsRight
536
379
  )}
537
380
  >
@@ -567,8 +410,6 @@ export function SimilarProductsModal({
567
410
  isLoading={isLoading}
568
411
  handleFacetChange={handleFacetChange}
569
412
  removeFacetFilter={removeFacetFilter}
570
- searchText={searchText}
571
- setSearchText={setSearchText}
572
413
  currentImageUrl={currentImageUrl}
573
414
  isCropping={isCropping}
574
415
  imageRef={imageRef}