@akinon/pz-virtual-try-on 2.0.0-beta.16 → 2.0.0-beta.17
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 +18 -0
- package/README.md +403 -7
- package/package.json +4 -3
- package/src/components/barcode-scanner.tsx +422 -0
- package/src/data/barcode-endpoints.ts +34 -0
- package/src/hooks/use-barcode-search.ts +172 -0
- package/src/hooks/use-image-cropper.ts +38 -7
- package/src/hooks/use-virtual-try-on-async.ts +1 -0
- package/src/hooks/use-virtual-try-on.ts +14 -2
- package/src/index.ts +26 -1
- package/src/types/barcode.ts +308 -0
- package/src/types/index.ts +32 -0
- package/src/utils/error-mapping.ts +3 -1
- package/src/utils/index.ts +115 -0
- package/src/views/barcode-scanner-button.tsx +63 -0
- package/src/views/barcode-scanner-modal.tsx +632 -0
- package/src/views/barcode-scanner-plugin.tsx +232 -0
- package/src/views/basket-async-modal.tsx +7 -2
- package/src/views/virtual-try-on-upload-modal.tsx +47 -41
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useState, useRef, useCallback } from 'react';
|
|
2
2
|
import 'react-image-crop/dist/ReactCrop.css';
|
|
3
|
+
import { validateAspectRatioFromDimensions } from '../utils';
|
|
3
4
|
|
|
4
5
|
type CropType = {
|
|
5
6
|
unit: 'px' | '%';
|
|
@@ -9,10 +10,16 @@ type CropType = {
|
|
|
9
10
|
height: number;
|
|
10
11
|
};
|
|
11
12
|
|
|
13
|
+
export interface CropResult {
|
|
14
|
+
success: boolean;
|
|
15
|
+
error?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
12
18
|
export function useImageCropper(
|
|
13
19
|
setIsLoading: (loading: boolean) => void,
|
|
14
20
|
processImage: (base64: string) => void,
|
|
15
|
-
clearError?: () => void
|
|
21
|
+
clearError?: () => void,
|
|
22
|
+
onAspectRatioError?: (error: string) => void
|
|
16
23
|
) {
|
|
17
24
|
const [isCropping, setIsCropping] = useState(false);
|
|
18
25
|
const [crop, setCrop] = useState<CropType>({
|
|
@@ -64,10 +71,12 @@ export function useImageCropper(
|
|
|
64
71
|
}
|
|
65
72
|
};
|
|
66
73
|
|
|
67
|
-
const processCompletedCropImmediate = async (
|
|
74
|
+
const processCompletedCropImmediate = async (
|
|
75
|
+
crop: any
|
|
76
|
+
): Promise<CropResult> => {
|
|
68
77
|
if (!imageRef.current || !crop?.width || !crop?.height) {
|
|
69
78
|
setIsLoading(false);
|
|
70
|
-
return;
|
|
79
|
+
return { success: false, error: 'Invalid crop dimensions' };
|
|
71
80
|
}
|
|
72
81
|
|
|
73
82
|
if (clearError) {
|
|
@@ -82,8 +91,28 @@ export function useImageCropper(
|
|
|
82
91
|
const scaleX = image.naturalWidth / image.width;
|
|
83
92
|
const scaleY = image.naturalHeight / image.height;
|
|
84
93
|
|
|
85
|
-
|
|
86
|
-
|
|
94
|
+
const croppedWidth = crop.width * scaleX;
|
|
95
|
+
const croppedHeight = crop.height * scaleY;
|
|
96
|
+
|
|
97
|
+
// Validate aspect ratio before processing
|
|
98
|
+
const aspectRatioValidation = validateAspectRatioFromDimensions(
|
|
99
|
+
croppedWidth,
|
|
100
|
+
croppedHeight
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
if (!aspectRatioValidation.isValid) {
|
|
104
|
+
setIsLoading(false);
|
|
105
|
+
if (onAspectRatioError && aspectRatioValidation.error) {
|
|
106
|
+
onAspectRatioError(aspectRatioValidation.error);
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
success: false,
|
|
110
|
+
error: aspectRatioValidation.error
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
canvas.width = croppedWidth;
|
|
115
|
+
canvas.height = croppedHeight;
|
|
87
116
|
|
|
88
117
|
const ctx = canvas.getContext('2d');
|
|
89
118
|
|
|
@@ -94,8 +123,8 @@ export function useImageCropper(
|
|
|
94
123
|
|
|
95
124
|
const sourceX = crop.x * scaleX;
|
|
96
125
|
const sourceY = crop.y * scaleY;
|
|
97
|
-
const sourceWidth =
|
|
98
|
-
const sourceHeight =
|
|
126
|
+
const sourceWidth = croppedWidth;
|
|
127
|
+
const sourceHeight = croppedHeight;
|
|
99
128
|
|
|
100
129
|
ctx.drawImage(
|
|
101
130
|
image,
|
|
@@ -115,9 +144,11 @@ export function useImageCropper(
|
|
|
115
144
|
setCropProcessed(true);
|
|
116
145
|
setLastSuccessfulCrop(crop);
|
|
117
146
|
setIsLoading(false);
|
|
147
|
+
return { success: true };
|
|
118
148
|
} catch (error) {
|
|
119
149
|
console.error('❌ processCompletedCrop failed:', error);
|
|
120
150
|
setIsLoading(false);
|
|
151
|
+
return { success: false, error: 'Failed to process crop' };
|
|
121
152
|
}
|
|
122
153
|
};
|
|
123
154
|
|
|
@@ -15,7 +15,8 @@ import {
|
|
|
15
15
|
isVirtualTryOnEnabled,
|
|
16
16
|
VirtualTryOnCache,
|
|
17
17
|
compressImageToUnder1MB,
|
|
18
|
-
convertWebPToJPEG
|
|
18
|
+
convertWebPToJPEG,
|
|
19
|
+
validateImageAspectRatio
|
|
19
20
|
} from '../utils';
|
|
20
21
|
import { useImageCropper } from './use-image-cropper';
|
|
21
22
|
import type { VirtualTryOnResponse } from '../types';
|
|
@@ -112,7 +113,10 @@ export function useVirtualTryOn(product: Product) {
|
|
|
112
113
|
(croppedBase64) => {
|
|
113
114
|
setUploadedImage(croppedBase64);
|
|
114
115
|
},
|
|
115
|
-
() => setFileError('')
|
|
116
|
+
() => setFileError(''),
|
|
117
|
+
(aspectRatioError) => {
|
|
118
|
+
setFileError(aspectRatioError);
|
|
119
|
+
}
|
|
116
120
|
);
|
|
117
121
|
|
|
118
122
|
useEffect(() => {
|
|
@@ -178,6 +182,14 @@ export function useVirtualTryOn(product: Product) {
|
|
|
178
182
|
return;
|
|
179
183
|
}
|
|
180
184
|
|
|
185
|
+
const aspectRatioValidation = await validateImageAspectRatio(
|
|
186
|
+
processedFile
|
|
187
|
+
);
|
|
188
|
+
if (!aspectRatioValidation.isValid) {
|
|
189
|
+
setFileError(aspectRatioValidation.error || 'Invalid aspect ratio');
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
181
193
|
const base64Image = await fileToBase64(processedFile);
|
|
182
194
|
setUploadedImage(base64Image);
|
|
183
195
|
setOriginalImage(base64Image);
|
package/src/index.ts
CHANGED
|
@@ -9,10 +9,16 @@ export { VirtualTryOnProductSelector } from './views/virtual-try-on-product-sele
|
|
|
9
9
|
export { VirtualTryOnAsyncModal } from './views/virtual-try-on-async-modal';
|
|
10
10
|
export { BasketAsyncModal } from './views/basket-async-modal';
|
|
11
11
|
|
|
12
|
+
export { BarcodeScannerPlugin } from './views/barcode-scanner-plugin';
|
|
13
|
+
export { BarcodeScannerModal } from './views/barcode-scanner-modal';
|
|
14
|
+
export { BarcodeScannerButton } from './views/barcode-scanner-button';
|
|
15
|
+
export { BarcodeScanner } from './components/barcode-scanner';
|
|
16
|
+
|
|
12
17
|
export { ProcessingSpinner } from './components/processing-spinner';
|
|
13
18
|
|
|
14
19
|
export { useVirtualTryOn } from './hooks/use-virtual-try-on';
|
|
15
20
|
export { useImageCropper } from './hooks/use-image-cropper';
|
|
21
|
+
export { useBarcodeSearch } from './hooks/use-barcode-search';
|
|
16
22
|
|
|
17
23
|
export { useVirtualTryOnAsync } from './hooks/use-virtual-try-on-async';
|
|
18
24
|
|
|
@@ -24,6 +30,11 @@ export {
|
|
|
24
30
|
useSubmitVirtualTryOnFeedbackMutation
|
|
25
31
|
} from './data/endpoints';
|
|
26
32
|
|
|
33
|
+
export {
|
|
34
|
+
useSearchProductByBarcodeQuery,
|
|
35
|
+
useLazySearchProductByBarcodeQuery
|
|
36
|
+
} from './data/barcode-endpoints';
|
|
37
|
+
|
|
27
38
|
export type {
|
|
28
39
|
VirtualTryOnResponse,
|
|
29
40
|
VirtualTryOnModalProps,
|
|
@@ -38,7 +49,21 @@ export type {
|
|
|
38
49
|
VirtualTryOnMultipleResult,
|
|
39
50
|
VirtualTryOnCompatibilityError,
|
|
40
51
|
BasketProduct,
|
|
41
|
-
VirtualTryOnMultipleModalProps
|
|
52
|
+
VirtualTryOnMultipleModalProps,
|
|
53
|
+
BarcodeFormat,
|
|
54
|
+
BarcodeScanResult,
|
|
55
|
+
BarcodeSearchResponse,
|
|
56
|
+
BarcodeSearchRequest,
|
|
57
|
+
BarcodeScannerState,
|
|
58
|
+
ScannedProductState,
|
|
59
|
+
BarcodeScannerButtonProps,
|
|
60
|
+
BarcodeScannerModalProps,
|
|
61
|
+
BarcodeScannerProps,
|
|
62
|
+
ScannedProductCardProps,
|
|
63
|
+
BarcodeScannerSettings,
|
|
64
|
+
BarcodeScannerTheme,
|
|
65
|
+
BarcodeScannerCustomStyles,
|
|
66
|
+
BarcodeScannerCustomRenderers
|
|
42
67
|
} from './types';
|
|
43
68
|
|
|
44
69
|
export * from './utils';
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Product, Pagination, Facet, SortOption } from '@akinon/next/types';
|
|
3
|
+
|
|
4
|
+
// ==================== BARCODE SCANNER TYPES ====================
|
|
5
|
+
|
|
6
|
+
export type BarcodeFormat =
|
|
7
|
+
| 'QR_CODE'
|
|
8
|
+
| 'DATA_MATRIX'
|
|
9
|
+
| 'AZTEC'
|
|
10
|
+
| 'PDF_417'
|
|
11
|
+
| 'EAN_8'
|
|
12
|
+
| 'EAN_13'
|
|
13
|
+
| 'UPC_A'
|
|
14
|
+
| 'UPC_E'
|
|
15
|
+
| 'CODE_39'
|
|
16
|
+
| 'CODE_93'
|
|
17
|
+
| 'CODE_128'
|
|
18
|
+
| 'CODABAR'
|
|
19
|
+
| 'ITF'
|
|
20
|
+
| 'RSS_14'
|
|
21
|
+
| 'RSS_EXPANDED';
|
|
22
|
+
|
|
23
|
+
export interface BarcodeScanResult {
|
|
24
|
+
text: string;
|
|
25
|
+
format: BarcodeFormat;
|
|
26
|
+
timestamp: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface BarcodeSearchResponse {
|
|
30
|
+
pagination: Pagination;
|
|
31
|
+
facets: Facet[];
|
|
32
|
+
sorters: SortOption[];
|
|
33
|
+
search_text: string | null;
|
|
34
|
+
products: Product[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface BarcodeSearchRequest {
|
|
38
|
+
barcode: string;
|
|
39
|
+
page?: number;
|
|
40
|
+
limit?: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface BarcodeScannerState {
|
|
44
|
+
isScanning: boolean;
|
|
45
|
+
hasPermission: boolean | null;
|
|
46
|
+
error: string | null;
|
|
47
|
+
lastScanResult: BarcodeScanResult | null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface ScannedProductState {
|
|
51
|
+
products: Product[];
|
|
52
|
+
isLoading: boolean;
|
|
53
|
+
error: string | null;
|
|
54
|
+
searchedBarcode: string | null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ==================== BARCODE COMPONENT PROPS ====================
|
|
58
|
+
|
|
59
|
+
export interface BarcodeScannerButtonProps {
|
|
60
|
+
onClick: () => void;
|
|
61
|
+
className?: string;
|
|
62
|
+
disabled?: boolean;
|
|
63
|
+
settings?: BarcodeScannerSettings;
|
|
64
|
+
style?: React.CSSProperties;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface BarcodeScannerModalProps {
|
|
68
|
+
isOpen: boolean;
|
|
69
|
+
onClose: () => void;
|
|
70
|
+
onProductFound?: (products: Product[]) => void;
|
|
71
|
+
onContinueToVTO?: (products: Product[]) => void;
|
|
72
|
+
className?: string;
|
|
73
|
+
settings?: BarcodeScannerSettings;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface BarcodeScannerProps {
|
|
77
|
+
onScan: (result: BarcodeScanResult) => void;
|
|
78
|
+
onError?: (error: Error) => void;
|
|
79
|
+
onPermissionDenied?: () => void;
|
|
80
|
+
isActive: boolean;
|
|
81
|
+
className?: string;
|
|
82
|
+
settings?: BarcodeScannerSettings;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface ScannedProductCardProps {
|
|
86
|
+
product: Product;
|
|
87
|
+
isSelected?: boolean;
|
|
88
|
+
onSelect?: (product: Product) => void;
|
|
89
|
+
onRemove?: (product: Product) => void;
|
|
90
|
+
className?: string;
|
|
91
|
+
settings?: BarcodeScannerSettings;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ==================== BARCODE SETTINGS ====================
|
|
95
|
+
|
|
96
|
+
export interface BarcodeScannerSettings {
|
|
97
|
+
customStyles?: BarcodeScannerCustomStyles;
|
|
98
|
+
customRenderers?: BarcodeScannerCustomRenderers;
|
|
99
|
+
theme?: BarcodeScannerTheme;
|
|
100
|
+
cssVariables?: Record<string, string>;
|
|
101
|
+
// Supported barcode formats - defaults to all
|
|
102
|
+
supportedFormats?: BarcodeFormat[];
|
|
103
|
+
// Maximum number of products that can be scanned
|
|
104
|
+
maxProducts?: number;
|
|
105
|
+
// Whether to show the continue to VTO button
|
|
106
|
+
showContinueToVTO?: boolean;
|
|
107
|
+
// Sound on successful scan
|
|
108
|
+
playScanSound?: boolean;
|
|
109
|
+
// Vibrate on successful scan (mobile)
|
|
110
|
+
vibrateOnScan?: boolean;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface BarcodeScannerTheme {
|
|
114
|
+
colors?: {
|
|
115
|
+
primary?: string;
|
|
116
|
+
secondary?: string;
|
|
117
|
+
background?: string;
|
|
118
|
+
text?: string;
|
|
119
|
+
border?: string;
|
|
120
|
+
success?: string;
|
|
121
|
+
error?: string;
|
|
122
|
+
scannerOverlay?: string;
|
|
123
|
+
scannerFrame?: string;
|
|
124
|
+
scannerCorner?: string;
|
|
125
|
+
};
|
|
126
|
+
spacing?: {
|
|
127
|
+
xs?: string;
|
|
128
|
+
sm?: string;
|
|
129
|
+
md?: string;
|
|
130
|
+
lg?: string;
|
|
131
|
+
xl?: string;
|
|
132
|
+
};
|
|
133
|
+
borderRadius?: {
|
|
134
|
+
sm?: string;
|
|
135
|
+
md?: string;
|
|
136
|
+
lg?: string;
|
|
137
|
+
full?: string;
|
|
138
|
+
};
|
|
139
|
+
fonts?: {
|
|
140
|
+
primary?: string;
|
|
141
|
+
secondary?: string;
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface BarcodeScannerCustomStyles {
|
|
146
|
+
// Button styles
|
|
147
|
+
floatingButton?: string;
|
|
148
|
+
floatingButtonIcon?: string;
|
|
149
|
+
|
|
150
|
+
// Modal styles
|
|
151
|
+
modalOverlay?: string;
|
|
152
|
+
modalContainer?: string;
|
|
153
|
+
modalHeader?: string;
|
|
154
|
+
modalTitle?: string;
|
|
155
|
+
modalCloseButton?: string;
|
|
156
|
+
modalCloseIcon?: string;
|
|
157
|
+
modalContent?: string;
|
|
158
|
+
|
|
159
|
+
// Scanner styles
|
|
160
|
+
scannerContainer?: string;
|
|
161
|
+
scannerVideo?: string;
|
|
162
|
+
scannerOverlay?: string;
|
|
163
|
+
scannerFrame?: string;
|
|
164
|
+
scannerCornerTopLeft?: string;
|
|
165
|
+
scannerCornerTopRight?: string;
|
|
166
|
+
scannerCornerBottomLeft?: string;
|
|
167
|
+
scannerCornerBottomRight?: string;
|
|
168
|
+
scannerHelpText?: string;
|
|
169
|
+
scannerLine?: string;
|
|
170
|
+
|
|
171
|
+
// Product display styles
|
|
172
|
+
productListContainer?: string;
|
|
173
|
+
productList?: string;
|
|
174
|
+
productCard?: string;
|
|
175
|
+
productCardSelected?: string;
|
|
176
|
+
productImage?: string;
|
|
177
|
+
productImageWrapper?: string;
|
|
178
|
+
productInfo?: string;
|
|
179
|
+
productName?: string;
|
|
180
|
+
productPrice?: string;
|
|
181
|
+
productCheckmark?: string;
|
|
182
|
+
productRemoveButton?: string;
|
|
183
|
+
|
|
184
|
+
// Actions styles
|
|
185
|
+
actionsContainer?: string;
|
|
186
|
+
continueButton?: string;
|
|
187
|
+
continueButtonDisabled?: string;
|
|
188
|
+
scanMoreText?: string;
|
|
189
|
+
|
|
190
|
+
// Error and loading styles
|
|
191
|
+
errorContainer?: string;
|
|
192
|
+
errorMessage?: string;
|
|
193
|
+
errorIcon?: string;
|
|
194
|
+
errorRetryButton?: string;
|
|
195
|
+
loadingContainer?: string;
|
|
196
|
+
loadingSpinner?: string;
|
|
197
|
+
loadingText?: string;
|
|
198
|
+
|
|
199
|
+
// Permission denied styles
|
|
200
|
+
permissionDeniedContainer?: string;
|
|
201
|
+
permissionDeniedIcon?: string;
|
|
202
|
+
permissionDeniedTitle?: string;
|
|
203
|
+
permissionDeniedMessage?: string;
|
|
204
|
+
permissionDeniedButton?: string;
|
|
205
|
+
|
|
206
|
+
// Empty state styles
|
|
207
|
+
emptyStateContainer?: string;
|
|
208
|
+
emptyStateIcon?: string;
|
|
209
|
+
emptyStateTitle?: string;
|
|
210
|
+
emptyStateMessage?: string;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export interface BarcodeScannerCustomRenderers {
|
|
214
|
+
// Button renderers
|
|
215
|
+
renderFloatingButton?: (props: {
|
|
216
|
+
onClick: () => void;
|
|
217
|
+
className?: string;
|
|
218
|
+
}) => React.ReactNode;
|
|
219
|
+
renderFloatingButtonIcon?: () => React.ReactNode;
|
|
220
|
+
|
|
221
|
+
// Modal renderers
|
|
222
|
+
renderModal?: (props: BarcodeScannerModalProps) => React.ReactNode;
|
|
223
|
+
renderModalHeader?: (props: {
|
|
224
|
+
title: string;
|
|
225
|
+
onClose: () => void;
|
|
226
|
+
}) => React.ReactNode;
|
|
227
|
+
renderModalCloseButton?: (props: {
|
|
228
|
+
onClick: () => void;
|
|
229
|
+
}) => React.ReactNode;
|
|
230
|
+
renderModalCloseIcon?: () => React.ReactNode;
|
|
231
|
+
|
|
232
|
+
// Scanner renderers
|
|
233
|
+
renderScanner?: (props: BarcodeScannerProps) => React.ReactNode;
|
|
234
|
+
renderScannerOverlay?: (props: {
|
|
235
|
+
isScanning: boolean;
|
|
236
|
+
}) => React.ReactNode;
|
|
237
|
+
renderScannerFrame?: () => React.ReactNode;
|
|
238
|
+
renderScannerCorners?: () => React.ReactNode;
|
|
239
|
+
renderScannerHelpText?: (props: { text: string }) => React.ReactNode;
|
|
240
|
+
renderScannerLine?: () => React.ReactNode;
|
|
241
|
+
|
|
242
|
+
// Product renderers
|
|
243
|
+
renderProductList?: (props: {
|
|
244
|
+
products: Product[];
|
|
245
|
+
onRemove: (product: Product) => void;
|
|
246
|
+
}) => React.ReactNode;
|
|
247
|
+
renderProductCard?: (props: ScannedProductCardProps) => React.ReactNode;
|
|
248
|
+
renderProductImage?: (props: {
|
|
249
|
+
imageUrl: string;
|
|
250
|
+
alt: string;
|
|
251
|
+
className?: string;
|
|
252
|
+
}) => React.ReactNode;
|
|
253
|
+
renderProductInfo?: (props: {
|
|
254
|
+
product: Product;
|
|
255
|
+
}) => React.ReactNode;
|
|
256
|
+
renderProductCheckmark?: () => React.ReactNode;
|
|
257
|
+
renderProductRemoveButton?: (props: {
|
|
258
|
+
onClick: () => void;
|
|
259
|
+
}) => React.ReactNode;
|
|
260
|
+
|
|
261
|
+
// Actions renderers
|
|
262
|
+
renderActionsContainer?: (props: {
|
|
263
|
+
children: React.ReactNode;
|
|
264
|
+
}) => React.ReactNode;
|
|
265
|
+
renderContinueButton?: (props: {
|
|
266
|
+
onClick: () => void;
|
|
267
|
+
disabled: boolean;
|
|
268
|
+
text: string;
|
|
269
|
+
}) => React.ReactNode;
|
|
270
|
+
renderScanMoreText?: (props: { text: string }) => React.ReactNode;
|
|
271
|
+
|
|
272
|
+
// Error renderers
|
|
273
|
+
renderError?: (props: {
|
|
274
|
+
error: string;
|
|
275
|
+
onRetry?: () => void;
|
|
276
|
+
}) => React.ReactNode;
|
|
277
|
+
renderErrorIcon?: () => React.ReactNode;
|
|
278
|
+
renderErrorMessage?: (props: { message: string }) => React.ReactNode;
|
|
279
|
+
renderErrorRetryButton?: (props: {
|
|
280
|
+
onClick: () => void;
|
|
281
|
+
text: string;
|
|
282
|
+
}) => React.ReactNode;
|
|
283
|
+
|
|
284
|
+
// Loading renderers
|
|
285
|
+
renderLoading?: (props: { text?: string }) => React.ReactNode;
|
|
286
|
+
renderLoadingSpinner?: () => React.ReactNode;
|
|
287
|
+
renderLoadingText?: (props: { text: string }) => React.ReactNode;
|
|
288
|
+
|
|
289
|
+
// Permission denied renderers
|
|
290
|
+
renderPermissionDenied?: (props: {
|
|
291
|
+
onRequestPermission?: () => void;
|
|
292
|
+
}) => React.ReactNode;
|
|
293
|
+
renderPermissionDeniedIcon?: () => React.ReactNode;
|
|
294
|
+
renderPermissionDeniedTitle?: (props: { title: string }) => React.ReactNode;
|
|
295
|
+
renderPermissionDeniedMessage?: (props: {
|
|
296
|
+
message: string;
|
|
297
|
+
}) => React.ReactNode;
|
|
298
|
+
renderPermissionDeniedButton?: (props: {
|
|
299
|
+
onClick: () => void;
|
|
300
|
+
text: string;
|
|
301
|
+
}) => React.ReactNode;
|
|
302
|
+
|
|
303
|
+
// Empty state renderers
|
|
304
|
+
renderEmptyState?: () => React.ReactNode;
|
|
305
|
+
renderEmptyStateIcon?: () => React.ReactNode;
|
|
306
|
+
renderEmptyStateTitle?: (props: { title: string }) => React.ReactNode;
|
|
307
|
+
renderEmptyStateMessage?: (props: { message: string }) => React.ReactNode;
|
|
308
|
+
}
|
package/src/types/index.ts
CHANGED
|
@@ -2,6 +2,9 @@ import React from 'react';
|
|
|
2
2
|
import { Product } from '@akinon/next/types';
|
|
3
3
|
import { useVirtualTryOn } from '../hooks/use-virtual-try-on';
|
|
4
4
|
|
|
5
|
+
// Re-export barcode types
|
|
6
|
+
export * from './barcode';
|
|
7
|
+
|
|
5
8
|
export interface VirtualTryOnResponse {
|
|
6
9
|
reference: string;
|
|
7
10
|
generated: string;
|
|
@@ -561,6 +564,32 @@ export interface CustomRendererProps {
|
|
|
561
564
|
};
|
|
562
565
|
}
|
|
563
566
|
|
|
567
|
+
export interface VirtualTryOnModalImages {
|
|
568
|
+
ruleGoodExample?: string;
|
|
569
|
+
ruleBadExample1?: string;
|
|
570
|
+
ruleBadExample2?: string;
|
|
571
|
+
ruleBadExample3?: string;
|
|
572
|
+
ruleBadExample4?: string;
|
|
573
|
+
uploadIcon?: string;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
export interface VirtualTryOnModalTexts {
|
|
577
|
+
title?: string;
|
|
578
|
+
editPhotoTitle?: string;
|
|
579
|
+
uploadPrompt?: string;
|
|
580
|
+
uploadRequirements?: string;
|
|
581
|
+
uploadInfo?: string;
|
|
582
|
+
rule1?: string;
|
|
583
|
+
rule2?: string;
|
|
584
|
+
rule3?: string;
|
|
585
|
+
rule4?: string;
|
|
586
|
+
rule5?: string;
|
|
587
|
+
rule6?: string;
|
|
588
|
+
processingTitle?: string;
|
|
589
|
+
processingMessage?: string;
|
|
590
|
+
retryUpload?: string;
|
|
591
|
+
}
|
|
592
|
+
|
|
564
593
|
export interface VirtualTryOnPluginSettings {
|
|
565
594
|
maxFileSize?: number;
|
|
566
595
|
allowedFormats?: string[];
|
|
@@ -571,6 +600,7 @@ export interface VirtualTryOnPluginSettings {
|
|
|
571
600
|
buttonSize?: 'sm' | 'md' | 'lg';
|
|
572
601
|
legal_text?: string;
|
|
573
602
|
instructions?: string;
|
|
603
|
+
|
|
574
604
|
customStyles?: {
|
|
575
605
|
plugin?: string;
|
|
576
606
|
button?: string;
|
|
@@ -609,6 +639,8 @@ export interface VirtualTryOnPluginSettings {
|
|
|
609
639
|
uploadRequirements?: string;
|
|
610
640
|
uploadRequirementsError?: string;
|
|
611
641
|
uploadButton?: string;
|
|
642
|
+
uploadModalImages?: VirtualTryOnModalImages;
|
|
643
|
+
uploadModalTexts?: VirtualTryOnModalTexts;
|
|
612
644
|
fileInput?: string;
|
|
613
645
|
|
|
614
646
|
rulesInfoText?: string;
|
|
@@ -11,7 +11,9 @@ export const ERROR_MESSAGES: Record<string, string> = {
|
|
|
11
11
|
background: 'Görüntü arka planı uygun değil.',
|
|
12
12
|
political: 'Siyasi içerik tespit edildi.',
|
|
13
13
|
others: 'Görüntü uygun değil.',
|
|
14
|
-
empty: 'Görüntüde insan tespit edilemedi.'
|
|
14
|
+
empty: 'Görüntüde insan tespit edilemedi.',
|
|
15
|
+
aspect_ratio_too_tall: `Görsel çok uzun. Dikey görseller için izin verilen maksimum oran 9:16'dır. Lütfen kırpın veya farklı bir görsel seçin.`,
|
|
16
|
+
aspect_ratio_too_wide: `Görsel çok geniş. Yatay görseller için izin verilen maksimum oran 21:9'dur. Lütfen kırpın veya farklı bir görsel seçin.`
|
|
15
17
|
};
|
|
16
18
|
|
|
17
19
|
export function getErrorMessage(
|
package/src/utils/index.ts
CHANGED
|
@@ -1,5 +1,120 @@
|
|
|
1
1
|
export { parseVirtualTryOnError } from './error-parser';
|
|
2
2
|
|
|
3
|
+
export const ASPECT_RATIO_LIMITS = {
|
|
4
|
+
MIN_RATIO: 9 / 16,
|
|
5
|
+
MAX_RATIO: 21 / 9
|
|
6
|
+
} as const;
|
|
7
|
+
|
|
8
|
+
export interface AspectRatioValidation {
|
|
9
|
+
isValid: boolean;
|
|
10
|
+
error?: string;
|
|
11
|
+
ratio?: number;
|
|
12
|
+
width?: number;
|
|
13
|
+
height?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const getImageDimensions = (
|
|
17
|
+
source: string | File
|
|
18
|
+
): Promise<{ width: number; height: number }> => {
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
const img = new Image();
|
|
21
|
+
|
|
22
|
+
img.onload = () => {
|
|
23
|
+
resolve({
|
|
24
|
+
width: img.naturalWidth,
|
|
25
|
+
height: img.naturalHeight
|
|
26
|
+
});
|
|
27
|
+
URL.revokeObjectURL(img.src);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
img.onerror = () => {
|
|
31
|
+
reject(new Error('Failed to load image for dimension check'));
|
|
32
|
+
URL.revokeObjectURL(img.src);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
if (typeof source === 'string') {
|
|
36
|
+
img.src = source;
|
|
37
|
+
} else {
|
|
38
|
+
img.src = URL.createObjectURL(source);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const validateImageAspectRatio = async (
|
|
44
|
+
source: string | File
|
|
45
|
+
): Promise<AspectRatioValidation> => {
|
|
46
|
+
try {
|
|
47
|
+
const { width, height } = await getImageDimensions(source);
|
|
48
|
+
const ratio = width / height;
|
|
49
|
+
|
|
50
|
+
if (ratio < ASPECT_RATIO_LIMITS.MIN_RATIO) {
|
|
51
|
+
return {
|
|
52
|
+
isValid: false,
|
|
53
|
+
error: 'aspect_ratio_too_tall',
|
|
54
|
+
ratio,
|
|
55
|
+
width,
|
|
56
|
+
height
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (ratio > ASPECT_RATIO_LIMITS.MAX_RATIO) {
|
|
61
|
+
return {
|
|
62
|
+
isValid: false,
|
|
63
|
+
error: 'aspect_ratio_too_wide',
|
|
64
|
+
ratio,
|
|
65
|
+
width,
|
|
66
|
+
height
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
isValid: true,
|
|
72
|
+
ratio,
|
|
73
|
+
width,
|
|
74
|
+
height
|
|
75
|
+
};
|
|
76
|
+
} catch (error) {
|
|
77
|
+
return {
|
|
78
|
+
isValid: false,
|
|
79
|
+
error: 'Failed to validate image dimensions'
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const validateAspectRatioFromDimensions = (
|
|
85
|
+
width: number,
|
|
86
|
+
height: number
|
|
87
|
+
): AspectRatioValidation => {
|
|
88
|
+
const ratio = width / height;
|
|
89
|
+
|
|
90
|
+
if (ratio < ASPECT_RATIO_LIMITS.MIN_RATIO) {
|
|
91
|
+
return {
|
|
92
|
+
isValid: false,
|
|
93
|
+
error: 'aspect_ratio_too_tall',
|
|
94
|
+
ratio,
|
|
95
|
+
width,
|
|
96
|
+
height
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (ratio > ASPECT_RATIO_LIMITS.MAX_RATIO) {
|
|
101
|
+
return {
|
|
102
|
+
isValid: false,
|
|
103
|
+
error: 'aspect_ratio_too_wide',
|
|
104
|
+
ratio,
|
|
105
|
+
width,
|
|
106
|
+
height
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
isValid: true,
|
|
112
|
+
ratio,
|
|
113
|
+
width,
|
|
114
|
+
height
|
|
115
|
+
};
|
|
116
|
+
};
|
|
117
|
+
|
|
3
118
|
export const validateImageFile = (
|
|
4
119
|
file: File
|
|
5
120
|
): { isValid: boolean; error?: string } => {
|