@akinon/pz-virtual-try-on 2.0.0-beta.16 → 2.0.0-beta.18

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.
@@ -0,0 +1,232 @@
1
+ 'use client';
2
+
3
+ import React, { useState, useCallback, useMemo, useEffect } from 'react';
4
+ import { Product } from '@akinon/next/types';
5
+ import { BarcodeScannerButton } from './barcode-scanner-button';
6
+ import { BarcodeScannerModal } from './barcode-scanner-modal';
7
+ import { VirtualTryOnProductSelector } from './virtual-try-on-product-selector';
8
+ import { VirtualTryOnUploadModal } from './virtual-try-on-upload-modal';
9
+ import { BasketAsyncModal } from './basket-async-modal';
10
+ import { useVirtualTryOnAsync } from '../hooks/use-virtual-try-on-async';
11
+ import { getVirtualTryOnEnabled } from '../utils';
12
+ import type {
13
+ BarcodeScannerSettings,
14
+ VirtualTryOnPluginSettings,
15
+ BasketProduct
16
+ } from '../types';
17
+
18
+ export interface BarcodeScannerPluginProps {
19
+ className?: string;
20
+ barcodeScannerSettings?: BarcodeScannerSettings;
21
+ vtoSettings?: VirtualTryOnPluginSettings;
22
+
23
+ settings?: {
24
+ barcodeScanner?: BarcodeScannerSettings;
25
+ vto?: VirtualTryOnPluginSettings;
26
+ };
27
+
28
+ onProductsScanned?: (products: Product[]) => void;
29
+ onVTOComplete?: (result: any) => void;
30
+
31
+ showVTOContinue?: boolean;
32
+ }
33
+
34
+ export function BarcodeScannerPlugin({
35
+ className,
36
+ barcodeScannerSettings,
37
+ vtoSettings,
38
+ settings,
39
+ onProductsScanned,
40
+ onVTOComplete,
41
+ showVTOContinue = true
42
+ }: BarcodeScannerPluginProps) {
43
+ // Check if virtual try-on is enabled
44
+ const [isEnabled, setIsEnabled] = useState(false);
45
+
46
+ // Merge settings from props and PluginModule settings
47
+ const finalBarcodeScannerSettings =
48
+ barcodeScannerSettings || settings?.barcodeScanner;
49
+ const finalVtoSettings = vtoSettings || settings?.vto;
50
+
51
+ const [isScannerModalOpen, setIsScannerModalOpen] = useState(false);
52
+ const [isSelectorOpen, setIsSelectorOpen] = useState(false);
53
+ const [showUploadModal, setShowUploadModal] = useState(false);
54
+ const [showAsyncModal, setShowAsyncModal] = useState(false);
55
+ const [scannedProducts, setScannedProducts] = useState<BasketProduct[]>([]);
56
+ const [selectedProducts, setSelectedProducts] = useState<BasketProduct[]>([]);
57
+ const [categoryMapping, setCategoryMapping] = useState<
58
+ Record<string, number[]>
59
+ >({});
60
+
61
+ useEffect(() => {
62
+ setIsEnabled(getVirtualTryOnEnabled());
63
+ }, []);
64
+
65
+ // Fetch categories when products are selected
66
+ useEffect(() => {
67
+ if (selectedProducts.length === 0) {
68
+ setCategoryMapping({});
69
+ return;
70
+ }
71
+
72
+ const fetchCategories = async () => {
73
+ try {
74
+ const pks = selectedProducts.map((item) => item.pk).join(',');
75
+ const response = await fetch(`/api/product-categories?pks=${pks}`);
76
+
77
+ if (response.ok) {
78
+ const mapping = await response.json();
79
+ setCategoryMapping(mapping);
80
+ }
81
+ } catch (error) {}
82
+ };
83
+
84
+ fetchCategories();
85
+ }, [selectedProducts]);
86
+
87
+ const hookData = useVirtualTryOnAsync(
88
+ selectedProducts.length > 0
89
+ ? selectedProducts
90
+ : [{ pk: 0, sku: '', name: '', productimage_set: [], attributes: {} }],
91
+ categoryMapping
92
+ );
93
+
94
+ const handleOpenScanner = useCallback(() => {
95
+ setIsScannerModalOpen(true);
96
+ }, []);
97
+
98
+ const handleCloseScannerModal = useCallback(() => {
99
+ setIsScannerModalOpen(false);
100
+ setScannedProducts([]);
101
+ }, []);
102
+
103
+ const handleProductsFound = useCallback(
104
+ (products: Product[]) => {
105
+ onProductsScanned?.(products);
106
+ },
107
+ [onProductsScanned]
108
+ );
109
+
110
+ const handleContinueToVTO = useCallback((products: Product[]) => {
111
+ if (products.length === 0) {
112
+ return;
113
+ }
114
+
115
+ const basketProducts: BasketProduct[] = products.map((product) => ({
116
+ pk: product.pk,
117
+ sku: product.sku || '',
118
+ name: product.name,
119
+ productimage_set: product.productimage_set,
120
+ attributes: product.attributes,
121
+ category: (product as any).category
122
+ }));
123
+
124
+ setIsScannerModalOpen(false);
125
+ setScannedProducts(basketProducts);
126
+
127
+ if (basketProducts.length === 1) {
128
+ setSelectedProducts(basketProducts);
129
+ setShowUploadModal(true);
130
+ } else {
131
+ setIsSelectorOpen(true);
132
+ }
133
+ }, []);
134
+
135
+ const handleProductsSelected = useCallback((products: BasketProduct[]) => {
136
+ setSelectedProducts(products);
137
+ setIsSelectorOpen(false);
138
+ setShowUploadModal(true);
139
+ }, []);
140
+
141
+ const handleSelectorClose = useCallback(() => {
142
+ setIsSelectorOpen(false);
143
+ setScannedProducts([]);
144
+ }, []);
145
+
146
+ const handleUploadModalClose = useCallback(() => {
147
+ hookData.cancelTryOn?.();
148
+ setShowUploadModal(false);
149
+ setSelectedProducts([]);
150
+ setScannedProducts([]);
151
+ setCategoryMapping({});
152
+ }, [hookData]);
153
+
154
+ const handleAsyncModalClose = useCallback(() => {
155
+ hookData.cancelTryOn?.();
156
+ setShowAsyncModal(false);
157
+ setSelectedProducts([]);
158
+ setScannedProducts([]);
159
+ setCategoryMapping({});
160
+ hookData.reset();
161
+ }, [hookData]);
162
+
163
+ const handleProcessingStart = useCallback(() => {
164
+ setShowUploadModal(false);
165
+ setShowAsyncModal(true);
166
+ }, []);
167
+
168
+ const mergedBarcodeSettings = useMemo(
169
+ (): BarcodeScannerSettings => ({
170
+ ...finalBarcodeScannerSettings,
171
+ showContinueToVTO: showVTOContinue
172
+ }),
173
+ [finalBarcodeScannerSettings, showVTOContinue]
174
+ );
175
+
176
+ if (!isEnabled) {
177
+ return null;
178
+ }
179
+
180
+ return (
181
+ <>
182
+ <BarcodeScannerButton
183
+ onClick={handleOpenScanner}
184
+ className={className}
185
+ settings={mergedBarcodeSettings}
186
+ />
187
+
188
+ <BarcodeScannerModal
189
+ isOpen={isScannerModalOpen}
190
+ onClose={handleCloseScannerModal}
191
+ onProductFound={handleProductsFound}
192
+ onContinueToVTO={handleContinueToVTO}
193
+ settings={mergedBarcodeSettings}
194
+ />
195
+
196
+ {isSelectorOpen && scannedProducts.length > 0 && (
197
+ <VirtualTryOnProductSelector
198
+ isOpen={true}
199
+ onClose={handleSelectorClose}
200
+ products={scannedProducts}
201
+ maxSelection={3}
202
+ onConfirm={handleProductsSelected}
203
+ settings={finalVtoSettings}
204
+ />
205
+ )}
206
+
207
+ {showUploadModal && selectedProducts.length > 0 && (
208
+ <VirtualTryOnUploadModal
209
+ isOpen={true}
210
+ onClose={handleUploadModalClose}
211
+ hookData={hookData}
212
+ onProcessingStart={handleProcessingStart}
213
+ settings={finalVtoSettings}
214
+ />
215
+ )}
216
+
217
+ {showAsyncModal && selectedProducts.length > 0 && (
218
+ <BasketAsyncModal
219
+ isOpen={true}
220
+ onClose={handleAsyncModalClose}
221
+ hookData={hookData}
222
+ products={selectedProducts}
223
+ onComplete={onVTOComplete}
224
+ settings={finalVtoSettings}
225
+ source="barcode"
226
+ />
227
+ )}
228
+ </>
229
+ );
230
+ }
231
+
232
+ export default BarcodeScannerPlugin;
@@ -17,6 +17,8 @@ export interface BasketAsyncModalProps {
17
17
  hookData: ReturnType<typeof useVirtualTryOnAsync>;
18
18
  onRetryUpload?: () => void;
19
19
  settings?: VirtualTryOnPluginSettings;
20
+ /** Source of the modal - affects button text */
21
+ source?: 'basket' | 'barcode';
20
22
  }
21
23
 
22
24
  type ModalStep = 'processing' | 'results' | 'detail';
@@ -28,7 +30,8 @@ export function BasketAsyncModal({
28
30
  onComplete,
29
31
  hookData,
30
32
  onRetryUpload,
31
- settings
33
+ settings,
34
+ source = 'basket'
32
35
  }: BasketAsyncModalProps) {
33
36
  const { t } = useLocalization();
34
37
  const [currentStep, setCurrentStep] = useState<ModalStep>('processing');
@@ -516,7 +519,9 @@ export function BasketAsyncModal({
516
519
  settings?.customStyles?.basketAsyncModalBackButton
517
520
  )}
518
521
  >
519
- {t('product.virtual_try_on.back_to_basket')}
522
+ {source === 'barcode'
523
+ ? t('product.virtual_try_on.close_button')
524
+ : t('product.virtual_try_on.back_to_basket')}
520
525
  </button>
521
526
  </div>
522
527
  </div>
@@ -122,6 +122,23 @@ export function VirtualTryOnUploadModal({
122
122
  const localHookData = product ? useVirtualTryOn(product) : null;
123
123
  const activeHookData = hookData || localHookData;
124
124
 
125
+
126
+ const getImageSrc = (
127
+ settingsImage: string | undefined,
128
+ defaultImage: string | { src: string }
129
+ ): string => {
130
+ if (settingsImage) return settingsImage;
131
+ return typeof defaultImage === 'string' ? defaultImage : defaultImage.src;
132
+ };
133
+
134
+ const getText = (
135
+ settingsText: string | undefined,
136
+ localizationKey: string
137
+ ): string => {
138
+ if (settingsText) return settingsText;
139
+ return t(localizationKey);
140
+ };
141
+
125
142
  const {
126
143
  uploadedImage,
127
144
  originalImage,
@@ -424,8 +441,8 @@ export function VirtualTryOnUploadModal({
424
441
  )}
425
442
  >
426
443
  {uploadedImage
427
- ? t('product.virtual_try_on.edit_photo')
428
- : t('product.virtual_try_on.title')}
444
+ ? getText(settings?.customStyles?.uploadModalTexts?.editPhotoTitle, 'product.virtual_try_on.edit_photo')
445
+ : getText(settings?.customStyles?.uploadModalTexts?.title, 'product.virtual_try_on.title')}
429
446
  </h3>
430
447
  <button
431
448
  type="button"
@@ -498,7 +515,7 @@ export function VirtualTryOnUploadModal({
498
515
  )
499
516
  )}
500
517
  >
501
- {t('product.virtual_try_on.upload_prompt')}
518
+ {getText(settings?.customStyles?.uploadModalTexts?.uploadPrompt, 'product.virtual_try_on.upload_prompt')}
502
519
  </p>
503
520
  <p
504
521
  className={twMerge(
@@ -515,7 +532,7 @@ export function VirtualTryOnUploadModal({
515
532
  )
516
533
  )}
517
534
  >
518
- {t('product.virtual_try_on.upload_requirements')}
535
+ {getText(settings?.customStyles?.uploadRequirements, 'product.virtual_try_on.upload_requirements')}
519
536
  </p>
520
537
  </div>
521
538
  <input
@@ -542,8 +559,7 @@ export function VirtualTryOnUploadModal({
542
559
  settings?.customStyles?.rulesInfoText
543
560
  )}
544
561
  >
545
- {t('product.virtual_try_on.upload_info') ||
546
- 'Uygulamaya uygun fotoğraf yüklemeniz çok önemlidir.'}
562
+ {getText(settings?.customStyles?.uploadModalTexts?.uploadInfo, 'product.virtual_try_on.upload_info')}
547
563
  </p>
548
564
  </div>
549
565
 
@@ -567,11 +583,7 @@ export function VirtualTryOnUploadModal({
567
583
  )}
568
584
  >
569
585
  <img
570
- src={
571
- typeof rule1Image === 'string'
572
- ? rule1Image
573
- : rule1Image.src
574
- }
586
+ src={getImageSrc(settings?.customStyles?.uploadModalImages?.ruleGoodExample, rule1Image)}
575
587
  alt="Example"
576
588
  className={twMerge(
577
589
  'w-full h-full',
@@ -587,10 +599,10 @@ export function VirtualTryOnUploadModal({
587
599
  )}
588
600
  >
589
601
  {[
590
- { img: rule2Image, num: 2 },
591
- { img: rule3Image, num: 3 },
592
- { img: rule4Image, num: 4 },
593
- { img: rule5Image, num: 5 }
602
+ { img: getImageSrc(settings?.customStyles?.uploadModalImages?.ruleBadExample1, rule2Image), num: 2 },
603
+ { img: getImageSrc(settings?.customStyles?.uploadModalImages?.ruleBadExample2, rule3Image), num: 3 },
604
+ { img: getImageSrc(settings?.customStyles?.uploadModalImages?.ruleBadExample3, rule4Image), num: 4 },
605
+ { img: getImageSrc(settings?.customStyles?.uploadModalImages?.ruleBadExample4, rule5Image), num: 5 }
594
606
  ].map(({ img, num }) => (
595
607
  <div
596
608
  key={num}
@@ -601,7 +613,7 @@ export function VirtualTryOnUploadModal({
601
613
  )}
602
614
  >
603
615
  <img
604
- src={typeof img === 'string' ? img : img.src}
616
+ src={img}
605
617
  alt={`Bad example ${num}`}
606
618
  className={twMerge(
607
619
  'w-full h-full',
@@ -644,7 +656,7 @@ export function VirtualTryOnUploadModal({
644
656
  settings?.customStyles?.rulesListItemText
645
657
  }
646
658
  >
647
- {t('product.virtual_try_on.rule_1')}
659
+ {getText(settings?.customStyles?.uploadModalTexts?.rule1, 'product.virtual_try_on.rule_1')}
648
660
  </span>
649
661
  </li>
650
662
  <li
@@ -666,7 +678,7 @@ export function VirtualTryOnUploadModal({
666
678
  settings?.customStyles?.rulesListItemText
667
679
  }
668
680
  >
669
- {t('product.virtual_try_on.rule_2')}
681
+ {getText(settings?.customStyles?.uploadModalTexts?.rule2, 'product.virtual_try_on.rule_2')}
670
682
  </span>
671
683
  </li>
672
684
  <li
@@ -688,7 +700,7 @@ export function VirtualTryOnUploadModal({
688
700
  settings?.customStyles?.rulesListItemText
689
701
  }
690
702
  >
691
- {t('product.virtual_try_on.rule_3')}
703
+ {getText(settings?.customStyles?.uploadModalTexts?.rule3, 'product.virtual_try_on.rule_3')}
692
704
  </span>
693
705
  </li>
694
706
  <li
@@ -710,7 +722,7 @@ export function VirtualTryOnUploadModal({
710
722
  settings?.customStyles?.rulesListItemText
711
723
  }
712
724
  >
713
- {t('product.virtual_try_on.rule_4')}
725
+ {getText(settings?.customStyles?.uploadModalTexts?.rule4, 'product.virtual_try_on.rule_4')}
714
726
  </span>
715
727
  </li>
716
728
  <li
@@ -732,7 +744,7 @@ export function VirtualTryOnUploadModal({
732
744
  settings?.customStyles?.rulesListItemText
733
745
  }
734
746
  >
735
- {t('product.virtual_try_on.rule_5')}
747
+ {getText(settings?.customStyles?.uploadModalTexts?.rule5, 'product.virtual_try_on.rule_5')}
736
748
  </span>
737
749
  </li>
738
750
  <li
@@ -754,7 +766,7 @@ export function VirtualTryOnUploadModal({
754
766
  settings?.customStyles?.rulesListItemText
755
767
  }
756
768
  >
757
- {t('product.virtual_try_on.rule_6')}
769
+ {getText(settings?.customStyles?.uploadModalTexts?.rule6, 'product.virtual_try_on.rule_6')}
758
770
  </span>
759
771
  </li>
760
772
  </ul>
@@ -780,11 +792,7 @@ export function VirtualTryOnUploadModal({
780
792
  )}
781
793
  >
782
794
  <img
783
- src={
784
- typeof rule1Image === 'string'
785
- ? rule1Image
786
- : rule1Image.src
787
- }
795
+ src={getImageSrc(settings?.customStyles?.uploadModalImages?.ruleGoodExample, rule1Image)}
788
796
  alt="Example"
789
797
  className={twMerge(
790
798
  'relative w-full',
@@ -803,10 +811,10 @@ export function VirtualTryOnUploadModal({
803
811
  )}
804
812
  >
805
813
  {[
806
- { img: rule2Image, num: 2 },
807
- { img: rule3Image, num: 3 },
808
- { img: rule4Image, num: 4 },
809
- { img: rule5Image, num: 5 }
814
+ { img: getImageSrc(settings?.customStyles?.uploadModalImages?.ruleBadExample1, rule2Image), num: 2 },
815
+ { img: getImageSrc(settings?.customStyles?.uploadModalImages?.ruleBadExample2, rule3Image), num: 3 },
816
+ { img: getImageSrc(settings?.customStyles?.uploadModalImages?.ruleBadExample3, rule4Image), num: 4 },
817
+ { img: getImageSrc(settings?.customStyles?.uploadModalImages?.ruleBadExample4, rule5Image), num: 5 }
810
818
  ].map(({ img, num }) => (
811
819
  <div
812
820
  key={num}
@@ -817,9 +825,7 @@ export function VirtualTryOnUploadModal({
817
825
  )}
818
826
  >
819
827
  <img
820
- src={
821
- typeof img === 'string' ? img : img.src
822
- }
828
+ src={img}
823
829
  alt={`Bad example ${num}`}
824
830
  className={twMerge(
825
831
  'w-full h-full',
@@ -844,7 +850,7 @@ export function VirtualTryOnUploadModal({
844
850
  settings?.customStyles?.rulesInfoTextDesktop
845
851
  )}
846
852
  >
847
- {t('product.virtual_try_on.upload_info')}
853
+ {getText(settings?.customStyles?.uploadModalTexts?.uploadInfo, 'product.virtual_try_on.upload_info')}
848
854
  </p>
849
855
  <ul
850
856
  className={twMerge(
@@ -872,7 +878,7 @@ export function VirtualTryOnUploadModal({
872
878
  settings?.customStyles?.rulesListItemText
873
879
  }
874
880
  >
875
- {t('product.virtual_try_on.rule_1')}
881
+ {getText(settings?.customStyles?.uploadModalTexts?.rule1, 'product.virtual_try_on.rule_1')}
876
882
  </span>
877
883
  </li>
878
884
  <li
@@ -895,7 +901,7 @@ export function VirtualTryOnUploadModal({
895
901
  settings?.customStyles?.rulesListItemText
896
902
  }
897
903
  >
898
- {t('product.virtual_try_on.rule_2')}
904
+ {getText(settings?.customStyles?.uploadModalTexts?.rule2, 'product.virtual_try_on.rule_2')}
899
905
  </span>
900
906
  </li>
901
907
  <li
@@ -918,7 +924,7 @@ export function VirtualTryOnUploadModal({
918
924
  settings?.customStyles?.rulesListItemText
919
925
  }
920
926
  >
921
- {t('product.virtual_try_on.rule_3')}
927
+ {getText(settings?.customStyles?.uploadModalTexts?.rule3, 'product.virtual_try_on.rule_3')}
922
928
  </span>
923
929
  </li>
924
930
  <li
@@ -941,7 +947,7 @@ export function VirtualTryOnUploadModal({
941
947
  settings?.customStyles?.rulesListItemText
942
948
  }
943
949
  >
944
- {t('product.virtual_try_on.rule_4')}
950
+ {getText(settings?.customStyles?.uploadModalTexts?.rule4, 'product.virtual_try_on.rule_4')}
945
951
  </span>
946
952
  </li>
947
953
  <li
@@ -964,7 +970,7 @@ export function VirtualTryOnUploadModal({
964
970
  settings?.customStyles?.rulesListItemText
965
971
  }
966
972
  >
967
- {t('product.virtual_try_on.rule_5')}
973
+ {getText(settings?.customStyles?.uploadModalTexts?.rule5, 'product.virtual_try_on.rule_5')}
968
974
  </span>
969
975
  </li>
970
976
  <li
@@ -987,7 +993,7 @@ export function VirtualTryOnUploadModal({
987
993
  settings?.customStyles?.rulesListItemText
988
994
  }
989
995
  >
990
- {t('product.virtual_try_on.rule_6')}
996
+ {getText(settings?.customStyles?.uploadModalTexts?.rule6, 'product.virtual_try_on.rule_6')}
991
997
  </span>
992
998
  </li>
993
999
  </ul>