@greatapps/common 1.1.462 → 1.1.464

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,9 +1,15 @@
1
1
  "use client";
2
- import { useState, useCallback, useRef } from "react";
2
+ import { useState, useCallback, useRef, useEffect } from "react";
3
3
  import { cropImageToCanvas } from "../utils/crop-image";
4
4
  import { validateImage } from "../utils/validate-image";
5
5
  function useImageUpload(options) {
6
6
  const { config, onSuccess, onError } = options;
7
+ const onSuccessRef = useRef(onSuccess);
8
+ const onErrorRef = useRef(onError);
9
+ useEffect(() => {
10
+ onSuccessRef.current = onSuccess;
11
+ onErrorRef.current = onError;
12
+ }, [onSuccess, onError]);
7
13
  const [originalImage, setOriginalImage] = useState(null);
8
14
  const [croppedPreview, setCroppedPreview] = useState(null);
9
15
  const [showCropModal, setShowCropModal] = useState(false);
@@ -12,48 +18,58 @@ function useImageUpload(options) {
12
18
  const [isProcessing, setIsProcessing] = useState(false);
13
19
  const fileInputRef = useRef(null);
14
20
  const currentFileName = useRef("");
21
+ const checkMinimumDimensions = useCallback(
22
+ (naturalWidth, naturalHeight) => {
23
+ const { width: minW, height: minH, aspectRatio } = config;
24
+ const imgAspect = naturalWidth / naturalHeight;
25
+ if (imgAspect >= aspectRatio) {
26
+ const cropHeight = naturalHeight;
27
+ const cropWidth = cropHeight * aspectRatio;
28
+ return cropWidth >= minW && cropHeight >= minH;
29
+ } else {
30
+ const cropWidth = naturalWidth;
31
+ const cropHeight = cropWidth / aspectRatio;
32
+ return cropWidth >= minW && cropHeight >= minH;
33
+ }
34
+ },
35
+ [config]
36
+ );
15
37
  const handleFileSelect = useCallback(
16
38
  (file) => {
17
39
  const validation = validateImage(file);
18
40
  if (!validation.valid) {
19
- onError?.(validation.error || "Arquivo inv\xE1lido");
41
+ onErrorRef.current?.(validation.error || "Arquivo inv\xE1lido");
20
42
  return;
21
43
  }
22
44
  currentFileName.current = file.name;
23
45
  const reader = new FileReader();
24
46
  reader.onload = (e) => {
25
- setOriginalImage(e.target?.result);
26
- setShowCropModal(true);
47
+ const base64 = e.target?.result;
48
+ setOriginalImage(base64);
49
+ const img = new Image();
50
+ img.onload = () => {
51
+ const { naturalWidth, naturalHeight } = img;
52
+ if (checkMinimumDimensions(naturalWidth, naturalHeight)) {
53
+ setShowCropModal(true);
54
+ } else {
55
+ setTooSmallError({
56
+ fileName: currentFileName.current,
57
+ minWidth: config.width,
58
+ minHeight: config.height
59
+ });
60
+ setShowTooSmallModal(true);
61
+ }
62
+ };
63
+ img.src = base64;
27
64
  };
28
65
  reader.readAsDataURL(file);
29
66
  },
30
- [onError]
67
+ [config, checkMinimumDimensions]
31
68
  );
32
69
  const handleImageLoadInModal = useCallback(
33
- (naturalWidth, naturalHeight) => {
34
- const { width: minW, height: minH, aspectRatio } = config;
35
- const imgAspect = naturalWidth / naturalHeight;
36
- let canFitMinCrop = false;
37
- if (imgAspect >= aspectRatio) {
38
- const cropHeight = naturalHeight;
39
- const cropWidth = cropHeight * aspectRatio;
40
- canFitMinCrop = cropWidth >= minW && cropHeight >= minH;
41
- } else {
42
- const cropWidth = naturalWidth;
43
- const cropHeight = cropWidth / aspectRatio;
44
- canFitMinCrop = cropWidth >= minW && cropHeight >= minH;
45
- }
46
- if (!canFitMinCrop) {
47
- setShowCropModal(false);
48
- setTooSmallError({
49
- fileName: currentFileName.current,
50
- minWidth: minW,
51
- minHeight: minH
52
- });
53
- setShowTooSmallModal(true);
54
- }
70
+ (_naturalWidth, _naturalHeight) => {
55
71
  },
56
- [config]
72
+ []
57
73
  );
58
74
  const handleSelectAnotherFile = useCallback(() => {
59
75
  setShowTooSmallModal(false);
@@ -78,14 +94,14 @@ function useImageUpload(options) {
78
94
  );
79
95
  setCroppedPreview(croppedBase64);
80
96
  setShowCropModal(false);
81
- onSuccess?.(croppedBase64);
97
+ onSuccessRef.current?.(croppedBase64);
82
98
  } catch {
83
- onError?.("Erro ao aplicar crop");
99
+ onErrorRef.current?.("Erro ao aplicar crop");
84
100
  } finally {
85
101
  setIsProcessing(false);
86
102
  }
87
103
  },
88
- [config, onSuccess, onError]
104
+ [config]
89
105
  );
90
106
  const clear = useCallback(() => {
91
107
  setOriginalImage(null);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/modules/images/hooks/use-image-upload.hook.ts"],"sourcesContent":["'use client'\n\nimport { useState, useCallback, useRef } from 'react'\nimport type { Crop } from 'react-image-crop'\nimport { cropImageToCanvas } from '../utils/crop-image'\nimport { validateImage } from '../utils/validate-image'\nimport type { ImageConfig } from '../types/image.type'\n\nexport interface ImageTooSmallError {\n fileName: string\n minWidth: number\n minHeight: number\n}\n\nexport interface UseImageUploadOptions {\n config: ImageConfig\n onSuccess?: (base64Image: string) => void\n onError?: (error: string) => void\n}\n\nexport function useImageUpload(options: UseImageUploadOptions) {\n const { config, onSuccess, onError } = options\n\n const [originalImage, setOriginalImage] = useState<string | null>(null)\n const [croppedPreview, setCroppedPreview] = useState<string | null>(null)\n const [showCropModal, setShowCropModal] = useState(false)\n const [showTooSmallModal, setShowTooSmallModal] = useState(false)\n const [tooSmallError, setTooSmallError] = useState<ImageTooSmallError | null>(null)\n const [isProcessing, setIsProcessing] = useState(false)\n const fileInputRef = useRef<HTMLInputElement>(null)\n const currentFileName = useRef<string>('')\n\n // Abre modal IMEDIATAMENTE sem processar - muito mais rápido\n const handleFileSelect = useCallback(\n (file: File) => {\n const validation = validateImage(file)\n if (!validation.valid) {\n onError?.(validation.error || 'Arquivo inválido')\n return\n }\n\n currentFileName.current = file.name\n\n // Converte para base64 e abre modal instantaneamente\n const reader = new FileReader()\n reader.onload = (e) => {\n setOriginalImage(e.target?.result as string)\n setShowCropModal(true)\n }\n reader.readAsDataURL(file)\n },\n [onError]\n )\n\n // Chamado pelo ImageCropModal quando a imagem carrega - verifica dimensões\n const handleImageLoadInModal = useCallback(\n (naturalWidth: number, naturalHeight: number) => {\n const { width: minW, height: minH, aspectRatio } = config\n const imgAspect = naturalWidth / naturalHeight\n\n let canFitMinCrop = false\n if (imgAspect >= aspectRatio) {\n const cropHeight = naturalHeight\n const cropWidth = cropHeight * aspectRatio\n canFitMinCrop = cropWidth >= minW && cropHeight >= minH\n } else {\n const cropWidth = naturalWidth\n const cropHeight = cropWidth / aspectRatio\n canFitMinCrop = cropWidth >= minW && cropHeight >= minH\n }\n\n if (!canFitMinCrop) {\n // Fecha modal de crop e abre modal de erro\n setShowCropModal(false)\n setTooSmallError({\n fileName: currentFileName.current,\n minWidth: minW,\n minHeight: minH,\n })\n setShowTooSmallModal(true)\n }\n },\n [config]\n )\n\n const handleSelectAnotherFile = useCallback(() => {\n setShowTooSmallModal(false)\n setTooSmallError(null)\n fileInputRef.current?.click()\n }, [])\n\n const handleCancelTooSmall = useCallback(() => {\n setShowTooSmallModal(false)\n setShowCropModal(false)\n setTooSmallError(null)\n setOriginalImage(null)\n }, [])\n\n const handleCropConfirm = useCallback(\n async (imageElement: HTMLImageElement, cropArea: Crop) => {\n setIsProcessing(true)\n try {\n // Crop é feito inteiramente no cliente via Canvas\n // Já produz imagem no tamanho final (config.width x config.height)\n const croppedBase64 = await cropImageToCanvas(\n imageElement,\n cropArea,\n config.width,\n config.height\n )\n\n setCroppedPreview(croppedBase64)\n setShowCropModal(false)\n onSuccess?.(croppedBase64)\n } catch {\n onError?.('Erro ao aplicar crop')\n } finally {\n setIsProcessing(false)\n }\n },\n [config, onSuccess, onError]\n )\n\n const clear = useCallback(() => {\n setOriginalImage(null)\n setCroppedPreview(null)\n }, [])\n\n return {\n originalImage,\n croppedPreview,\n showCropModal,\n showTooSmallModal,\n tooSmallError,\n config,\n isProcessing,\n fileInputRef,\n handleFileSelect,\n handleCropConfirm,\n handleImageLoadInModal,\n handleSelectAnotherFile,\n handleCancelTooSmall,\n setShowCropModal,\n setShowTooSmallModal,\n clear,\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../../src/modules/images/hooks/use-image-upload.hook.ts"],"sourcesContent":["'use client'\n\nimport { useState, useCallback, useRef, useEffect } from 'react'\nimport type { Crop } from 'react-image-crop'\nimport { cropImageToCanvas } from '../utils/crop-image'\nimport { validateImage } from '../utils/validate-image'\nimport type { ImageConfig } from '../types/image.type'\n\nexport interface ImageTooSmallError {\n fileName: string\n minWidth: number\n minHeight: number\n}\n\nexport interface UseImageUploadOptions {\n config: ImageConfig\n onSuccess?: (base64Image: string) => void\n onError?: (error: string) => void\n}\n\nexport function useImageUpload(options: UseImageUploadOptions) {\n const { config, onSuccess, onError } = options\n\n // Refs para manter callbacks sempre atualizados (evita stale closures)\n const onSuccessRef = useRef(onSuccess)\n const onErrorRef = useRef(onError)\n useEffect(() => {\n onSuccessRef.current = onSuccess\n onErrorRef.current = onError\n }, [onSuccess, onError])\n\n const [originalImage, setOriginalImage] = useState<string | null>(null)\n const [croppedPreview, setCroppedPreview] = useState<string | null>(null)\n const [showCropModal, setShowCropModal] = useState(false)\n const [showTooSmallModal, setShowTooSmallModal] = useState(false)\n const [tooSmallError, setTooSmallError] = useState<ImageTooSmallError | null>(null)\n const [isProcessing, setIsProcessing] = useState(false)\n const fileInputRef = useRef<HTMLInputElement>(null)\n const currentFileName = useRef<string>('')\n\n // Verifica se a imagem cabe o crop mínimo\n const checkMinimumDimensions = useCallback(\n (naturalWidth: number, naturalHeight: number): boolean => {\n const { width: minW, height: minH, aspectRatio } = config\n const imgAspect = naturalWidth / naturalHeight\n\n if (imgAspect >= aspectRatio) {\n const cropHeight = naturalHeight\n const cropWidth = cropHeight * aspectRatio\n return cropWidth >= minW && cropHeight >= minH\n } else {\n const cropWidth = naturalWidth\n const cropHeight = cropWidth / aspectRatio\n return cropWidth >= minW && cropHeight >= minH\n }\n },\n [config]\n )\n\n // Valida dimensões ANTES de abrir qualquer modal\n const handleFileSelect = useCallback(\n (file: File) => {\n const validation = validateImage(file)\n if (!validation.valid) {\n onErrorRef.current?.(validation.error || 'Arquivo inválido')\n return\n }\n\n currentFileName.current = file.name\n\n // Converte para base64\n const reader = new FileReader()\n reader.onload = (e) => {\n const base64 = e.target?.result as string\n setOriginalImage(base64)\n\n // Carrega imagem em memória para verificar dimensões ANTES de abrir modal\n const img = new Image()\n img.onload = () => {\n const { naturalWidth, naturalHeight } = img\n\n if (checkMinimumDimensions(naturalWidth, naturalHeight)) {\n // Dimensões OK - abre modal de crop\n setShowCropModal(true)\n } else {\n // Imagem muito pequena - abre modal de erro diretamente\n setTooSmallError({\n fileName: currentFileName.current,\n minWidth: config.width,\n minHeight: config.height,\n })\n setShowTooSmallModal(true)\n }\n }\n img.src = base64\n }\n reader.readAsDataURL(file)\n },\n [config, checkMinimumDimensions]\n )\n\n // Mantido para compatibilidade, mas não mais necessário para validação\n const handleImageLoadInModal = useCallback(\n (_naturalWidth: number, _naturalHeight: number) => {\n // Validação agora é feita antes de abrir a modal\n // Este callback pode ser usado para outros propósitos se necessário\n },\n []\n )\n\n const handleSelectAnotherFile = useCallback(() => {\n setShowTooSmallModal(false)\n setTooSmallError(null)\n fileInputRef.current?.click()\n }, [])\n\n const handleCancelTooSmall = useCallback(() => {\n setShowTooSmallModal(false)\n setShowCropModal(false)\n setTooSmallError(null)\n setOriginalImage(null)\n }, [])\n\n const handleCropConfirm = useCallback(\n async (imageElement: HTMLImageElement, cropArea: Crop) => {\n setIsProcessing(true)\n try {\n // Crop é feito inteiramente no cliente via Canvas\n // Já produz imagem no tamanho final (config.width x config.height)\n const croppedBase64 = await cropImageToCanvas(\n imageElement,\n cropArea,\n config.width,\n config.height\n )\n\n setCroppedPreview(croppedBase64)\n setShowCropModal(false)\n onSuccessRef.current?.(croppedBase64)\n } catch {\n onErrorRef.current?.('Erro ao aplicar crop')\n } finally {\n setIsProcessing(false)\n }\n },\n [config]\n )\n\n const clear = useCallback(() => {\n setOriginalImage(null)\n setCroppedPreview(null)\n }, [])\n\n return {\n originalImage,\n croppedPreview,\n showCropModal,\n showTooSmallModal,\n tooSmallError,\n config,\n isProcessing,\n fileInputRef,\n handleFileSelect,\n handleCropConfirm,\n handleImageLoadInModal,\n handleSelectAnotherFile,\n handleCancelTooSmall,\n setShowCropModal,\n setShowTooSmallModal,\n clear,\n }\n}\n"],"mappings":"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","names":[]}
@@ -21,7 +21,7 @@ async function cropImageToCanvas(image, crop, targetWidth, targetHeight) {
21
21
  targetWidth,
22
22
  targetHeight
23
23
  );
24
- return canvas.toDataURL("image/webp", 0.9);
24
+ return canvas.toDataURL("image/jpeg", 0.92);
25
25
  }
26
26
  export {
27
27
  cropImageToCanvas
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/modules/images/utils/crop-image.ts"],"sourcesContent":["import type { Crop } from 'react-image-crop'\n\nexport async function cropImageToCanvas(\n image: HTMLImageElement,\n crop: Crop,\n targetWidth: number,\n targetHeight: number\n): Promise<string> {\n const canvas = document.createElement('canvas')\n const ctx = canvas.getContext('2d')\n\n if (!ctx) throw new Error('Canvas não suportado')\n\n const pixelCrop = {\n x: crop.unit === '%' ? (crop.x / 100) * image.naturalWidth : crop.x,\n y: crop.unit === '%' ? (crop.y / 100) * image.naturalHeight : crop.y,\n width:\n crop.unit === '%' ? (crop.width / 100) * image.naturalWidth : crop.width,\n height:\n crop.unit === '%'\n ? (crop.height / 100) * image.naturalHeight\n : crop.height,\n }\n\n canvas.width = targetWidth\n canvas.height = targetHeight\n\n ctx.drawImage(\n image,\n pixelCrop.x,\n pixelCrop.y,\n pixelCrop.width,\n pixelCrop.height,\n 0,\n 0,\n targetWidth,\n targetHeight\n )\n\n return canvas.toDataURL('image/webp', 0.9)\n}\n"],"mappings":"AAEA,eAAsB,kBACpB,OACA,MACA,aACA,cACiB;AACjB,QAAM,SAAS,SAAS,cAAc,QAAQ;AAC9C,QAAM,MAAM,OAAO,WAAW,IAAI;AAElC,MAAI,CAAC,IAAK,OAAM,IAAI,MAAM,yBAAsB;AAEhD,QAAM,YAAY;AAAA,IAChB,GAAG,KAAK,SAAS,MAAO,KAAK,IAAI,MAAO,MAAM,eAAe,KAAK;AAAA,IAClE,GAAG,KAAK,SAAS,MAAO,KAAK,IAAI,MAAO,MAAM,gBAAgB,KAAK;AAAA,IACnE,OACE,KAAK,SAAS,MAAO,KAAK,QAAQ,MAAO,MAAM,eAAe,KAAK;AAAA,IACrE,QACE,KAAK,SAAS,MACT,KAAK,SAAS,MAAO,MAAM,gBAC5B,KAAK;AAAA,EACb;AAEA,SAAO,QAAQ;AACf,SAAO,SAAS;AAEhB,MAAI;AAAA,IACF;AAAA,IACA,UAAU;AAAA,IACV,UAAU;AAAA,IACV,UAAU;AAAA,IACV,UAAU;AAAA,IACV;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,SAAO,OAAO,UAAU,cAAc,GAAG;AAC3C;","names":[]}
1
+ {"version":3,"sources":["../../../../src/modules/images/utils/crop-image.ts"],"sourcesContent":["import type { Crop } from 'react-image-crop'\n\nexport async function cropImageToCanvas(\n image: HTMLImageElement,\n crop: Crop,\n targetWidth: number,\n targetHeight: number\n): Promise<string> {\n const canvas = document.createElement('canvas')\n const ctx = canvas.getContext('2d')\n\n if (!ctx) throw new Error('Canvas não suportado')\n\n const pixelCrop = {\n x: crop.unit === '%' ? (crop.x / 100) * image.naturalWidth : crop.x,\n y: crop.unit === '%' ? (crop.y / 100) * image.naturalHeight : crop.y,\n width:\n crop.unit === '%' ? (crop.width / 100) * image.naturalWidth : crop.width,\n height:\n crop.unit === '%'\n ? (crop.height / 100) * image.naturalHeight\n : crop.height,\n }\n\n canvas.width = targetWidth\n canvas.height = targetHeight\n\n ctx.drawImage(\n image,\n pixelCrop.x,\n pixelCrop.y,\n pixelCrop.width,\n pixelCrop.height,\n 0,\n 0,\n targetWidth,\n targetHeight\n )\n\n // Usa JPEG para compatibilidade universal (Safari tem problemas com WebP)\n return canvas.toDataURL('image/jpeg', 0.92)\n}\n"],"mappings":"AAEA,eAAsB,kBACpB,OACA,MACA,aACA,cACiB;AACjB,QAAM,SAAS,SAAS,cAAc,QAAQ;AAC9C,QAAM,MAAM,OAAO,WAAW,IAAI;AAElC,MAAI,CAAC,IAAK,OAAM,IAAI,MAAM,yBAAsB;AAEhD,QAAM,YAAY;AAAA,IAChB,GAAG,KAAK,SAAS,MAAO,KAAK,IAAI,MAAO,MAAM,eAAe,KAAK;AAAA,IAClE,GAAG,KAAK,SAAS,MAAO,KAAK,IAAI,MAAO,MAAM,gBAAgB,KAAK;AAAA,IACnE,OACE,KAAK,SAAS,MAAO,KAAK,QAAQ,MAAO,MAAM,eAAe,KAAK;AAAA,IACrE,QACE,KAAK,SAAS,MACT,KAAK,SAAS,MAAO,MAAM,gBAC5B,KAAK;AAAA,EACb;AAEA,SAAO,QAAQ;AACf,SAAO,SAAS;AAEhB,MAAI;AAAA,IACF;AAAA,IACA,UAAU;AAAA,IACV,UAAU;AAAA,IACV,UAAU;AAAA,IACV,UAAU;AAAA,IACV;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAGA,SAAO,OAAO,UAAU,cAAc,IAAI;AAC5C;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@greatapps/common",
3
- "version": "1.1.462",
3
+ "version": "1.1.464",
4
4
  "description": "Shared library for GreatApps frontend applications",
5
5
  "main": "./dist/index.mjs",
6
6
  "types": "./src/index.ts",
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { useState, useCallback, useRef } from 'react'
3
+ import { useState, useCallback, useRef, useEffect } from 'react'
4
4
  import type { Crop } from 'react-image-crop'
5
5
  import { cropImageToCanvas } from '../utils/crop-image'
6
6
  import { validateImage } from '../utils/validate-image'
@@ -21,6 +21,14 @@ export interface UseImageUploadOptions {
21
21
  export function useImageUpload(options: UseImageUploadOptions) {
22
22
  const { config, onSuccess, onError } = options
23
23
 
24
+ // Refs para manter callbacks sempre atualizados (evita stale closures)
25
+ const onSuccessRef = useRef(onSuccess)
26
+ const onErrorRef = useRef(onError)
27
+ useEffect(() => {
28
+ onSuccessRef.current = onSuccess
29
+ onErrorRef.current = onError
30
+ }, [onSuccess, onError])
31
+
24
32
  const [originalImage, setOriginalImage] = useState<string | null>(null)
25
33
  const [croppedPreview, setCroppedPreview] = useState<string | null>(null)
26
34
  const [showCropModal, setShowCropModal] = useState(false)
@@ -30,57 +38,74 @@ export function useImageUpload(options: UseImageUploadOptions) {
30
38
  const fileInputRef = useRef<HTMLInputElement>(null)
31
39
  const currentFileName = useRef<string>('')
32
40
 
33
- // Abre modal IMEDIATAMENTE sem processar - muito mais rápido
41
+ // Verifica se a imagem cabe o crop mínimo
42
+ const checkMinimumDimensions = useCallback(
43
+ (naturalWidth: number, naturalHeight: number): boolean => {
44
+ const { width: minW, height: minH, aspectRatio } = config
45
+ const imgAspect = naturalWidth / naturalHeight
46
+
47
+ if (imgAspect >= aspectRatio) {
48
+ const cropHeight = naturalHeight
49
+ const cropWidth = cropHeight * aspectRatio
50
+ return cropWidth >= minW && cropHeight >= minH
51
+ } else {
52
+ const cropWidth = naturalWidth
53
+ const cropHeight = cropWidth / aspectRatio
54
+ return cropWidth >= minW && cropHeight >= minH
55
+ }
56
+ },
57
+ [config]
58
+ )
59
+
60
+ // Valida dimensões ANTES de abrir qualquer modal
34
61
  const handleFileSelect = useCallback(
35
62
  (file: File) => {
36
63
  const validation = validateImage(file)
37
64
  if (!validation.valid) {
38
- onError?.(validation.error || 'Arquivo inválido')
65
+ onErrorRef.current?.(validation.error || 'Arquivo inválido')
39
66
  return
40
67
  }
41
68
 
42
69
  currentFileName.current = file.name
43
70
 
44
- // Converte para base64 e abre modal instantaneamente
71
+ // Converte para base64
45
72
  const reader = new FileReader()
46
73
  reader.onload = (e) => {
47
- setOriginalImage(e.target?.result as string)
48
- setShowCropModal(true)
74
+ const base64 = e.target?.result as string
75
+ setOriginalImage(base64)
76
+
77
+ // Carrega imagem em memória para verificar dimensões ANTES de abrir modal
78
+ const img = new Image()
79
+ img.onload = () => {
80
+ const { naturalWidth, naturalHeight } = img
81
+
82
+ if (checkMinimumDimensions(naturalWidth, naturalHeight)) {
83
+ // Dimensões OK - abre modal de crop
84
+ setShowCropModal(true)
85
+ } else {
86
+ // Imagem muito pequena - abre modal de erro diretamente
87
+ setTooSmallError({
88
+ fileName: currentFileName.current,
89
+ minWidth: config.width,
90
+ minHeight: config.height,
91
+ })
92
+ setShowTooSmallModal(true)
93
+ }
94
+ }
95
+ img.src = base64
49
96
  }
50
97
  reader.readAsDataURL(file)
51
98
  },
52
- [onError]
99
+ [config, checkMinimumDimensions]
53
100
  )
54
101
 
55
- // Chamado pelo ImageCropModal quando a imagem carrega - verifica dimensões
102
+ // Mantido para compatibilidade, mas não mais necessário para validação
56
103
  const handleImageLoadInModal = useCallback(
57
- (naturalWidth: number, naturalHeight: number) => {
58
- const { width: minW, height: minH, aspectRatio } = config
59
- const imgAspect = naturalWidth / naturalHeight
60
-
61
- let canFitMinCrop = false
62
- if (imgAspect >= aspectRatio) {
63
- const cropHeight = naturalHeight
64
- const cropWidth = cropHeight * aspectRatio
65
- canFitMinCrop = cropWidth >= minW && cropHeight >= minH
66
- } else {
67
- const cropWidth = naturalWidth
68
- const cropHeight = cropWidth / aspectRatio
69
- canFitMinCrop = cropWidth >= minW && cropHeight >= minH
70
- }
71
-
72
- if (!canFitMinCrop) {
73
- // Fecha modal de crop e abre modal de erro
74
- setShowCropModal(false)
75
- setTooSmallError({
76
- fileName: currentFileName.current,
77
- minWidth: minW,
78
- minHeight: minH,
79
- })
80
- setShowTooSmallModal(true)
81
- }
104
+ (_naturalWidth: number, _naturalHeight: number) => {
105
+ // Validação agora é feita antes de abrir a modal
106
+ // Este callback pode ser usado para outros propósitos se necessário
82
107
  },
83
- [config]
108
+ []
84
109
  )
85
110
 
86
111
  const handleSelectAnotherFile = useCallback(() => {
@@ -111,14 +136,14 @@ export function useImageUpload(options: UseImageUploadOptions) {
111
136
 
112
137
  setCroppedPreview(croppedBase64)
113
138
  setShowCropModal(false)
114
- onSuccess?.(croppedBase64)
139
+ onSuccessRef.current?.(croppedBase64)
115
140
  } catch {
116
- onError?.('Erro ao aplicar crop')
141
+ onErrorRef.current?.('Erro ao aplicar crop')
117
142
  } finally {
118
143
  setIsProcessing(false)
119
144
  }
120
145
  },
121
- [config, onSuccess, onError]
146
+ [config]
122
147
  )
123
148
 
124
149
  const clear = useCallback(() => {
@@ -37,5 +37,6 @@ export async function cropImageToCanvas(
37
37
  targetHeight
38
38
  )
39
39
 
40
- return canvas.toDataURL('image/webp', 0.9)
40
+ // Usa JPEG para compatibilidade universal (Safari tem problemas com WebP)
41
+ return canvas.toDataURL('image/jpeg', 0.92)
41
42
  }