@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.
@@ -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 (crop: any) => {
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
- canvas.width = crop.width * scaleX;
86
- canvas.height = crop.height * scaleY;
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 = crop.width * scaleX;
98
- const sourceHeight = crop.height * scaleY;
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
 
@@ -353,6 +353,7 @@ export function useVirtualTryOnAsync(
353
353
  }, [
354
354
  uploadedImage,
355
355
  productsArray,
356
+ categoryMapping,
356
357
  startAsyncTryOn,
357
358
  getJobStatus,
358
359
  abortController
@@ -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
+ }
@@ -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(
@@ -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 } => {