@greatapps/common 1.1.448 → 1.1.449

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.
Files changed (46) hide show
  1. package/dist/components/modals/cards/AddCardModal.mjs +5 -2
  2. package/dist/components/modals/cards/AddCardModal.mjs.map +1 -1
  3. package/dist/components/widgets/ImageUpload/ImageCropModal.mjs +76 -0
  4. package/dist/components/widgets/ImageUpload/ImageCropModal.mjs.map +1 -0
  5. package/dist/components/widgets/ImageUpload/ImageUpload.mjs +121 -0
  6. package/dist/components/widgets/ImageUpload/ImageUpload.mjs.map +1 -0
  7. package/dist/components/widgets/ImageUpload/index.mjs +7 -0
  8. package/dist/components/widgets/ImageUpload/index.mjs.map +1 -0
  9. package/dist/index.mjs +29 -1
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/infra/utils/date.mjs +11 -0
  12. package/dist/infra/utils/date.mjs.map +1 -1
  13. package/dist/modules/images/actions/process-image.action.mjs +31 -0
  14. package/dist/modules/images/actions/process-image.action.mjs.map +1 -0
  15. package/dist/modules/images/constants/image.constants.mjs +14 -0
  16. package/dist/modules/images/constants/image.constants.mjs.map +1 -0
  17. package/dist/modules/images/hooks/use-image-upload.hook.mjs +96 -0
  18. package/dist/modules/images/hooks/use-image-upload.hook.mjs.map +1 -0
  19. package/dist/modules/images/services/image-processing.service.mjs +70 -0
  20. package/dist/modules/images/services/image-processing.service.mjs.map +1 -0
  21. package/dist/modules/images/types/image.type.mjs +1 -0
  22. package/dist/modules/images/types/image.type.mjs.map +1 -0
  23. package/dist/modules/images/utils/compress-image.mjs +18 -0
  24. package/dist/modules/images/utils/compress-image.mjs.map +1 -0
  25. package/dist/modules/images/utils/crop-image.mjs +29 -0
  26. package/dist/modules/images/utils/crop-image.mjs.map +1 -0
  27. package/dist/modules/images/utils/validate-image.mjs +50 -0
  28. package/dist/modules/images/utils/validate-image.mjs.map +1 -0
  29. package/dist/server.mjs +4 -0
  30. package/dist/server.mjs.map +1 -1
  31. package/package.json +4 -1
  32. package/src/components/modals/cards/AddCardModal.tsx +2 -1
  33. package/src/components/widgets/ImageUpload/ImageCropModal.tsx +97 -0
  34. package/src/components/widgets/ImageUpload/ImageUpload.tsx +125 -0
  35. package/src/components/widgets/ImageUpload/index.ts +2 -0
  36. package/src/index.ts +24 -1
  37. package/src/infra/utils/date.ts +11 -0
  38. package/src/modules/images/actions/process-image.action.ts +38 -0
  39. package/src/modules/images/constants/image.constants.ts +9 -0
  40. package/src/modules/images/hooks/use-image-upload.hook.ts +111 -0
  41. package/src/modules/images/services/image-processing.service.ts +76 -0
  42. package/src/modules/images/types/image.type.ts +54 -0
  43. package/src/modules/images/utils/compress-image.ts +21 -0
  44. package/src/modules/images/utils/crop-image.ts +41 -0
  45. package/src/modules/images/utils/validate-image.ts +59 -0
  46. package/src/server.ts +10 -0
@@ -0,0 +1,96 @@
1
+ "use client";
2
+ import { useState, useCallback } from "react";
3
+ import { useMutation } from "@tanstack/react-query";
4
+ import { compressImage } from "../utils/compress-image";
5
+ import { cropImageToCanvas } from "../utils/crop-image";
6
+ import { validateImage } from "../utils/validate-image";
7
+ import { processImageAction } from "../actions/process-image.action";
8
+ function useImageUpload(options) {
9
+ const { config, onSuccess, onError } = options;
10
+ const [originalImage, setOriginalImage] = useState(null);
11
+ const [croppedPreview, setCroppedPreview] = useState(null);
12
+ const [showCropModal, setShowCropModal] = useState(false);
13
+ const processMutation = useMutation({
14
+ mutationFn: processImageAction,
15
+ onSuccess: (result) => {
16
+ if (result.success && result.base64Image) {
17
+ setCroppedPreview(result.base64Image);
18
+ onSuccess?.(result.base64Image);
19
+ } else {
20
+ onError?.(result.error || "Erro desconhecido");
21
+ }
22
+ },
23
+ onError: (error) => {
24
+ onError?.(error instanceof Error ? error.message : "Erro ao processar");
25
+ }
26
+ });
27
+ const handleFileSelect = useCallback(
28
+ async (file) => {
29
+ const validation = validateImage(file);
30
+ if (!validation.valid) {
31
+ onError?.(validation.error || "Arquivo inv\xE1lido");
32
+ return;
33
+ }
34
+ try {
35
+ const compressed = await compressImage(file);
36
+ const reader = new FileReader();
37
+ reader.onload = (e) => {
38
+ setOriginalImage(e.target?.result);
39
+ setShowCropModal(true);
40
+ };
41
+ reader.readAsDataURL(compressed);
42
+ } catch {
43
+ onError?.("Erro ao processar imagem");
44
+ }
45
+ },
46
+ [onError]
47
+ );
48
+ const handleCropConfirm = useCallback(
49
+ async (imageElement, cropArea) => {
50
+ try {
51
+ const croppedBase64 = await cropImageToCanvas(
52
+ imageElement,
53
+ cropArea,
54
+ config.width,
55
+ config.height
56
+ );
57
+ setCroppedPreview(croppedBase64);
58
+ setShowCropModal(false);
59
+ processMutation.mutate({
60
+ base64Image: croppedBase64,
61
+ targetWidth: config.width,
62
+ targetHeight: config.height,
63
+ cropArea: {
64
+ x: cropArea.x,
65
+ y: cropArea.y,
66
+ width: cropArea.width,
67
+ height: cropArea.height,
68
+ unit: cropArea.unit
69
+ }
70
+ });
71
+ } catch {
72
+ onError?.("Erro ao aplicar crop");
73
+ }
74
+ },
75
+ [config, processMutation, onError]
76
+ );
77
+ const clear = useCallback(() => {
78
+ setOriginalImage(null);
79
+ setCroppedPreview(null);
80
+ }, []);
81
+ return {
82
+ originalImage,
83
+ croppedPreview,
84
+ showCropModal,
85
+ config,
86
+ isProcessing: processMutation.isPending,
87
+ handleFileSelect,
88
+ handleCropConfirm,
89
+ setShowCropModal,
90
+ clear
91
+ };
92
+ }
93
+ export {
94
+ useImageUpload
95
+ };
96
+ //# sourceMappingURL=use-image-upload.hook.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/modules/images/hooks/use-image-upload.hook.ts"],"sourcesContent":["'use client'\n\nimport { useState, useCallback } from 'react'\nimport { useMutation } from '@tanstack/react-query'\nimport type { Crop } from 'react-image-crop'\nimport { compressImage } from '../utils/compress-image'\nimport { cropImageToCanvas } from '../utils/crop-image'\nimport { validateImage } from '../utils/validate-image'\nimport { processImageAction } from '../actions/process-image.action'\nimport type { ImageConfig } from '../types/image.type'\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\n const processMutation = useMutation({\n mutationFn: processImageAction,\n onSuccess: (result) => {\n if (result.success && result.base64Image) {\n setCroppedPreview(result.base64Image)\n onSuccess?.(result.base64Image)\n } else {\n onError?.(result.error || 'Erro desconhecido')\n }\n },\n onError: (error) => {\n onError?.(error instanceof Error ? error.message : 'Erro ao processar')\n },\n })\n\n const handleFileSelect = useCallback(\n async (file: File) => {\n const validation = validateImage(file)\n if (!validation.valid) {\n onError?.(validation.error || 'Arquivo inválido')\n return\n }\n\n try {\n const compressed = await compressImage(file)\n const reader = new FileReader()\n reader.onload = (e) => {\n setOriginalImage(e.target?.result as string)\n setShowCropModal(true)\n }\n reader.readAsDataURL(compressed)\n } catch {\n onError?.('Erro ao processar imagem')\n }\n },\n [onError]\n )\n\n const handleCropConfirm = useCallback(\n async (imageElement: HTMLImageElement, cropArea: Crop) => {\n try {\n const croppedBase64 = await cropImageToCanvas(\n imageElement,\n cropArea,\n config.width,\n config.height\n )\n\n setCroppedPreview(croppedBase64)\n setShowCropModal(false)\n\n processMutation.mutate({\n base64Image: croppedBase64,\n targetWidth: config.width,\n targetHeight: config.height,\n cropArea: {\n x: cropArea.x,\n y: cropArea.y,\n width: cropArea.width,\n height: cropArea.height,\n unit: cropArea.unit,\n },\n })\n } catch {\n onError?.('Erro ao aplicar crop')\n }\n },\n [config, processMutation, onError]\n )\n\n const clear = useCallback(() => {\n setOriginalImage(null)\n setCroppedPreview(null)\n }, [])\n\n return {\n originalImage,\n croppedPreview,\n showCropModal,\n config,\n isProcessing: processMutation.isPending,\n handleFileSelect,\n handleCropConfirm,\n setShowCropModal,\n clear,\n }\n}\n"],"mappings":";AAEA,SAAS,UAAU,mBAAmB;AACtC,SAAS,mBAAmB;AAE5B,SAAS,qBAAqB;AAC9B,SAAS,yBAAyB;AAClC,SAAS,qBAAqB;AAC9B,SAAS,0BAA0B;AAS5B,SAAS,eAAe,SAAgC;AAC7D,QAAM,EAAE,QAAQ,WAAW,QAAQ,IAAI;AAEvC,QAAM,CAAC,eAAe,gBAAgB,IAAI,SAAwB,IAAI;AACtE,QAAM,CAAC,gBAAgB,iBAAiB,IAAI,SAAwB,IAAI;AACxE,QAAM,CAAC,eAAe,gBAAgB,IAAI,SAAS,KAAK;AAExD,QAAM,kBAAkB,YAAY;AAAA,IAClC,YAAY;AAAA,IACZ,WAAW,CAAC,WAAW;AACrB,UAAI,OAAO,WAAW,OAAO,aAAa;AACxC,0BAAkB,OAAO,WAAW;AACpC,oBAAY,OAAO,WAAW;AAAA,MAChC,OAAO;AACL,kBAAU,OAAO,SAAS,mBAAmB;AAAA,MAC/C;AAAA,IACF;AAAA,IACA,SAAS,CAAC,UAAU;AAClB,gBAAU,iBAAiB,QAAQ,MAAM,UAAU,mBAAmB;AAAA,IACxE;AAAA,EACF,CAAC;AAED,QAAM,mBAAmB;AAAA,IACvB,OAAO,SAAe;AACpB,YAAM,aAAa,cAAc,IAAI;AACrC,UAAI,CAAC,WAAW,OAAO;AACrB,kBAAU,WAAW,SAAS,qBAAkB;AAChD;AAAA,MACF;AAEA,UAAI;AACF,cAAM,aAAa,MAAM,cAAc,IAAI;AAC3C,cAAM,SAAS,IAAI,WAAW;AAC9B,eAAO,SAAS,CAAC,MAAM;AACrB,2BAAiB,EAAE,QAAQ,MAAgB;AAC3C,2BAAiB,IAAI;AAAA,QACvB;AACA,eAAO,cAAc,UAAU;AAAA,MACjC,QAAQ;AACN,kBAAU,0BAA0B;AAAA,MACtC;AAAA,IACF;AAAA,IACA,CAAC,OAAO;AAAA,EACV;AAEA,QAAM,oBAAoB;AAAA,IACxB,OAAO,cAAgC,aAAmB;AACxD,UAAI;AACF,cAAM,gBAAgB,MAAM;AAAA,UAC1B;AAAA,UACA;AAAA,UACA,OAAO;AAAA,UACP,OAAO;AAAA,QACT;AAEA,0BAAkB,aAAa;AAC/B,yBAAiB,KAAK;AAEtB,wBAAgB,OAAO;AAAA,UACrB,aAAa;AAAA,UACb,aAAa,OAAO;AAAA,UACpB,cAAc,OAAO;AAAA,UACrB,UAAU;AAAA,YACR,GAAG,SAAS;AAAA,YACZ,GAAG,SAAS;AAAA,YACZ,OAAO,SAAS;AAAA,YAChB,QAAQ,SAAS;AAAA,YACjB,MAAM,SAAS;AAAA,UACjB;AAAA,QACF,CAAC;AAAA,MACH,QAAQ;AACN,kBAAU,sBAAsB;AAAA,MAClC;AAAA,IACF;AAAA,IACA,CAAC,QAAQ,iBAAiB,OAAO;AAAA,EACnC;AAEA,QAAM,QAAQ,YAAY,MAAM;AAC9B,qBAAiB,IAAI;AACrB,sBAAkB,IAAI;AAAA,EACxB,GAAG,CAAC,CAAC;AAEL,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,cAAc,gBAAgB;AAAA,IAC9B;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,70 @@
1
+ import {
2
+ PhotonImage,
3
+ resize,
4
+ crop,
5
+ SamplingFilter
6
+ } from "@cf-wasm/photon/edge-light";
7
+ function processImage(input) {
8
+ const {
9
+ imageBytes,
10
+ targetWidth,
11
+ targetHeight,
12
+ outputFormat = "jpeg",
13
+ quality = 85
14
+ } = input;
15
+ const image = PhotonImage.new_from_byteslice(imageBytes);
16
+ try {
17
+ const sourceWidth = image.get_width();
18
+ const sourceHeight = image.get_height();
19
+ const targetAspect = targetWidth / targetHeight;
20
+ const sourceAspect = sourceWidth / sourceHeight;
21
+ let cropX1 = 0;
22
+ let cropY1 = 0;
23
+ let cropX2 = sourceWidth;
24
+ let cropY2 = sourceHeight;
25
+ if (sourceAspect > targetAspect) {
26
+ const cropWidth = Math.round(sourceHeight * targetAspect);
27
+ cropX1 = Math.round((sourceWidth - cropWidth) / 2);
28
+ cropX2 = cropX1 + cropWidth;
29
+ } else if (sourceAspect < targetAspect) {
30
+ const cropHeight = Math.round(sourceWidth / targetAspect);
31
+ cropY1 = Math.round((sourceHeight - cropHeight) / 2);
32
+ cropY2 = cropY1 + cropHeight;
33
+ }
34
+ const cropped = crop(image, cropX1, cropY1, cropX2, cropY2);
35
+ const resized = resize(
36
+ cropped,
37
+ targetWidth,
38
+ targetHeight,
39
+ SamplingFilter.Triangle
40
+ );
41
+ let outputBytes;
42
+ switch (outputFormat) {
43
+ case "jpeg":
44
+ outputBytes = resized.get_bytes_jpeg(quality);
45
+ break;
46
+ case "png":
47
+ outputBytes = resized.get_bytes();
48
+ break;
49
+ case "webp":
50
+ outputBytes = resized.get_bytes_webp();
51
+ break;
52
+ default:
53
+ outputBytes = resized.get_bytes_jpeg(quality);
54
+ }
55
+ cropped.free();
56
+ resized.free();
57
+ return {
58
+ bytes: outputBytes,
59
+ width: targetWidth,
60
+ height: targetHeight,
61
+ format: `image/${outputFormat}`
62
+ };
63
+ } finally {
64
+ image.free();
65
+ }
66
+ }
67
+ export {
68
+ processImage
69
+ };
70
+ //# sourceMappingURL=image-processing.service.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/modules/images/services/image-processing.service.ts"],"sourcesContent":["import {\n PhotonImage,\n resize,\n crop,\n SamplingFilter,\n} from '@cf-wasm/photon/edge-light'\nimport type { ProcessImageInput, ProcessImageOutput } from '../types/image.type'\n\nexport function processImage(input: ProcessImageInput): ProcessImageOutput {\n const {\n imageBytes,\n targetWidth,\n targetHeight,\n outputFormat = 'jpeg',\n quality = 85,\n } = input\n\n const image = PhotonImage.new_from_byteslice(imageBytes)\n\n try {\n const sourceWidth = image.get_width()\n const sourceHeight = image.get_height()\n const targetAspect = targetWidth / targetHeight\n const sourceAspect = sourceWidth / sourceHeight\n\n let cropX1 = 0\n let cropY1 = 0\n let cropX2 = sourceWidth\n let cropY2 = sourceHeight\n\n if (sourceAspect > targetAspect) {\n const cropWidth = Math.round(sourceHeight * targetAspect)\n cropX1 = Math.round((sourceWidth - cropWidth) / 2)\n cropX2 = cropX1 + cropWidth\n } else if (sourceAspect < targetAspect) {\n const cropHeight = Math.round(sourceWidth / targetAspect)\n cropY1 = Math.round((sourceHeight - cropHeight) / 2)\n cropY2 = cropY1 + cropHeight\n }\n\n const cropped = crop(image, cropX1, cropY1, cropX2, cropY2)\n const resized = resize(\n cropped,\n targetWidth,\n targetHeight,\n SamplingFilter.Triangle\n )\n\n let outputBytes: Uint8Array\n switch (outputFormat) {\n case 'jpeg':\n outputBytes = resized.get_bytes_jpeg(quality)\n break\n case 'png':\n outputBytes = resized.get_bytes()\n break\n case 'webp':\n outputBytes = resized.get_bytes_webp()\n break\n default:\n outputBytes = resized.get_bytes_jpeg(quality)\n }\n\n cropped.free()\n resized.free()\n\n return {\n bytes: outputBytes,\n width: targetWidth,\n height: targetHeight,\n format: `image/${outputFormat}`,\n }\n } finally {\n image.free()\n }\n}\n"],"mappings":"AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGA,SAAS,aAAa,OAA8C;AACzE,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA,eAAe;AAAA,IACf,UAAU;AAAA,EACZ,IAAI;AAEJ,QAAM,QAAQ,YAAY,mBAAmB,UAAU;AAEvD,MAAI;AACF,UAAM,cAAc,MAAM,UAAU;AACpC,UAAM,eAAe,MAAM,WAAW;AACtC,UAAM,eAAe,cAAc;AACnC,UAAM,eAAe,cAAc;AAEnC,QAAI,SAAS;AACb,QAAI,SAAS;AACb,QAAI,SAAS;AACb,QAAI,SAAS;AAEb,QAAI,eAAe,cAAc;AAC/B,YAAM,YAAY,KAAK,MAAM,eAAe,YAAY;AACxD,eAAS,KAAK,OAAO,cAAc,aAAa,CAAC;AACjD,eAAS,SAAS;AAAA,IACpB,WAAW,eAAe,cAAc;AACtC,YAAM,aAAa,KAAK,MAAM,cAAc,YAAY;AACxD,eAAS,KAAK,OAAO,eAAe,cAAc,CAAC;AACnD,eAAS,SAAS;AAAA,IACpB;AAEA,UAAM,UAAU,KAAK,OAAO,QAAQ,QAAQ,QAAQ,MAAM;AAC1D,UAAM,UAAU;AAAA,MACd;AAAA,MACA;AAAA,MACA;AAAA,MACA,eAAe;AAAA,IACjB;AAEA,QAAI;AACJ,YAAQ,cAAc;AAAA,MACpB,KAAK;AACH,sBAAc,QAAQ,eAAe,OAAO;AAC5C;AAAA,MACF,KAAK;AACH,sBAAc,QAAQ,UAAU;AAChC;AAAA,MACF,KAAK;AACH,sBAAc,QAAQ,eAAe;AACrC;AAAA,MACF;AACE,sBAAc,QAAQ,eAAe,OAAO;AAAA,IAChD;AAEA,YAAQ,KAAK;AACb,YAAQ,KAAK;AAEb,WAAO;AAAA,MACL,OAAO;AAAA,MACP,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,QAAQ,SAAS,YAAY;AAAA,IAC/B;AAAA,EACF,UAAE;AACA,UAAM,KAAK;AAAA,EACb;AACF;","names":[]}
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=image.type.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,18 @@
1
+ import imageCompression from "browser-image-compression";
2
+ import { TARGET_FILE_SIZE_MB } from "../constants/image.constants";
3
+ async function compressImage(file, options = {}) {
4
+ const { maxSizeMB = TARGET_FILE_SIZE_MB, maxWidthOrHeight = 1920 } = options;
5
+ if (file.size <= maxSizeMB * 1024 * 1024) {
6
+ return file;
7
+ }
8
+ return imageCompression(file, {
9
+ maxSizeMB,
10
+ maxWidthOrHeight,
11
+ useWebWorker: true,
12
+ fileType: file.type
13
+ });
14
+ }
15
+ export {
16
+ compressImage
17
+ };
18
+ //# sourceMappingURL=compress-image.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/modules/images/utils/compress-image.ts"],"sourcesContent":["import imageCompression from 'browser-image-compression'\nimport type { CompressImageOptions } from '../types/image.type'\nimport { TARGET_FILE_SIZE_MB } from '../constants/image.constants'\n\nexport async function compressImage(\n file: File,\n options: CompressImageOptions = {}\n): Promise<File> {\n const { maxSizeMB = TARGET_FILE_SIZE_MB, maxWidthOrHeight = 1920 } = options\n\n if (file.size <= maxSizeMB * 1024 * 1024) {\n return file\n }\n\n return imageCompression(file, {\n maxSizeMB,\n maxWidthOrHeight,\n useWebWorker: true,\n fileType: file.type as 'image/jpeg' | 'image/png' | 'image/webp',\n })\n}\n"],"mappings":"AAAA,OAAO,sBAAsB;AAE7B,SAAS,2BAA2B;AAEpC,eAAsB,cACpB,MACA,UAAgC,CAAC,GAClB;AACf,QAAM,EAAE,YAAY,qBAAqB,mBAAmB,KAAK,IAAI;AAErE,MAAI,KAAK,QAAQ,YAAY,OAAO,MAAM;AACxC,WAAO;AAAA,EACT;AAEA,SAAO,iBAAiB,MAAM;AAAA,IAC5B;AAAA,IACA;AAAA,IACA,cAAc;AAAA,IACd,UAAU,KAAK;AAAA,EACjB,CAAC;AACH;","names":[]}
@@ -0,0 +1,29 @@
1
+ async function cropImageToCanvas(image, crop, targetWidth, targetHeight) {
2
+ const canvas = document.createElement("canvas");
3
+ const ctx = canvas.getContext("2d");
4
+ if (!ctx) throw new Error("Canvas n\xE3o suportado");
5
+ const pixelCrop = {
6
+ x: crop.unit === "%" ? crop.x / 100 * image.naturalWidth : crop.x,
7
+ y: crop.unit === "%" ? crop.y / 100 * image.naturalHeight : crop.y,
8
+ width: crop.unit === "%" ? crop.width / 100 * image.naturalWidth : crop.width,
9
+ height: crop.unit === "%" ? crop.height / 100 * image.naturalHeight : crop.height
10
+ };
11
+ canvas.width = targetWidth;
12
+ canvas.height = targetHeight;
13
+ ctx.drawImage(
14
+ image,
15
+ pixelCrop.x,
16
+ pixelCrop.y,
17
+ pixelCrop.width,
18
+ pixelCrop.height,
19
+ 0,
20
+ 0,
21
+ targetWidth,
22
+ targetHeight
23
+ );
24
+ return canvas.toDataURL("image/jpeg", 0.9);
25
+ }
26
+ export {
27
+ cropImageToCanvas
28
+ };
29
+ //# sourceMappingURL=crop-image.mjs.map
@@ -0,0 +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/jpeg', 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":[]}
@@ -0,0 +1,50 @@
1
+ import {
2
+ ACCEPTED_IMAGE_FORMATS,
3
+ MAX_FILE_SIZE_MB
4
+ } from "../constants/image.constants";
5
+ function validateImageFormat(file) {
6
+ if (!ACCEPTED_IMAGE_FORMATS.includes(file.type)) {
7
+ return {
8
+ valid: false,
9
+ error: "Formato n\xE3o suportado. Use PNG, JPEG ou WebP."
10
+ };
11
+ }
12
+ return { valid: true };
13
+ }
14
+ function validateImageSize(file, maxSizeMB = MAX_FILE_SIZE_MB) {
15
+ if (file.size > maxSizeMB * 1024 * 1024) {
16
+ return {
17
+ valid: false,
18
+ error: `Arquivo muito grande. M\xE1ximo ${maxSizeMB}MB.`
19
+ };
20
+ }
21
+ return { valid: true };
22
+ }
23
+ function validateImage(file) {
24
+ const formatValidation = validateImageFormat(file);
25
+ if (!formatValidation.valid) return formatValidation;
26
+ const sizeValidation = validateImageSize(file);
27
+ if (!sizeValidation.valid) return sizeValidation;
28
+ return { valid: true };
29
+ }
30
+ async function getImageDimensions(file) {
31
+ return new Promise((resolve, reject) => {
32
+ const img = new Image();
33
+ img.onload = () => {
34
+ resolve({ width: img.naturalWidth, height: img.naturalHeight });
35
+ URL.revokeObjectURL(img.src);
36
+ };
37
+ img.onerror = () => {
38
+ reject(new Error("Falha ao carregar imagem"));
39
+ URL.revokeObjectURL(img.src);
40
+ };
41
+ img.src = URL.createObjectURL(file);
42
+ });
43
+ }
44
+ export {
45
+ getImageDimensions,
46
+ validateImage,
47
+ validateImageFormat,
48
+ validateImageSize
49
+ };
50
+ //# sourceMappingURL=validate-image.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/modules/images/utils/validate-image.ts"],"sourcesContent":["import {\n ACCEPTED_IMAGE_FORMATS,\n MAX_FILE_SIZE_MB,\n} from '../constants/image.constants'\n\nexport interface ImageValidationResult {\n valid: boolean\n error?: string\n}\n\nexport function validateImageFormat(file: File): ImageValidationResult {\n if (!ACCEPTED_IMAGE_FORMATS.includes(file.type as (typeof ACCEPTED_IMAGE_FORMATS)[number])) {\n return {\n valid: false,\n error: 'Formato não suportado. Use PNG, JPEG ou WebP.',\n }\n }\n return { valid: true }\n}\n\nexport function validateImageSize(\n file: File,\n maxSizeMB: number = MAX_FILE_SIZE_MB\n): ImageValidationResult {\n if (file.size > maxSizeMB * 1024 * 1024) {\n return {\n valid: false,\n error: `Arquivo muito grande. Máximo ${maxSizeMB}MB.`,\n }\n }\n return { valid: true }\n}\n\nexport function validateImage(file: File): ImageValidationResult {\n const formatValidation = validateImageFormat(file)\n if (!formatValidation.valid) return formatValidation\n\n const sizeValidation = validateImageSize(file)\n if (!sizeValidation.valid) return sizeValidation\n\n return { valid: true }\n}\n\nexport async function getImageDimensions(\n file: File\n): Promise<{ width: number; height: number }> {\n return new Promise((resolve, reject) => {\n const img = new Image()\n img.onload = () => {\n resolve({ width: img.naturalWidth, height: img.naturalHeight })\n URL.revokeObjectURL(img.src)\n }\n img.onerror = () => {\n reject(new Error('Falha ao carregar imagem'))\n URL.revokeObjectURL(img.src)\n }\n img.src = URL.createObjectURL(file)\n })\n}\n"],"mappings":"AAAA;AAAA,EACE;AAAA,EACA;AAAA,OACK;AAOA,SAAS,oBAAoB,MAAmC;AACrE,MAAI,CAAC,uBAAuB,SAAS,KAAK,IAA+C,GAAG;AAC1F,WAAO;AAAA,MACL,OAAO;AAAA,MACP,OAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO,EAAE,OAAO,KAAK;AACvB;AAEO,SAAS,kBACd,MACA,YAAoB,kBACG;AACvB,MAAI,KAAK,OAAO,YAAY,OAAO,MAAM;AACvC,WAAO;AAAA,MACL,OAAO;AAAA,MACP,OAAO,mCAAgC,SAAS;AAAA,IAClD;AAAA,EACF;AACA,SAAO,EAAE,OAAO,KAAK;AACvB;AAEO,SAAS,cAAc,MAAmC;AAC/D,QAAM,mBAAmB,oBAAoB,IAAI;AACjD,MAAI,CAAC,iBAAiB,MAAO,QAAO;AAEpC,QAAM,iBAAiB,kBAAkB,IAAI;AAC7C,MAAI,CAAC,eAAe,MAAO,QAAO;AAElC,SAAO,EAAE,OAAO,KAAK;AACvB;AAEA,eAAsB,mBACpB,MAC4C;AAC5C,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAM,MAAM,IAAI,MAAM;AACtB,QAAI,SAAS,MAAM;AACjB,cAAQ,EAAE,OAAO,IAAI,cAAc,QAAQ,IAAI,cAAc,CAAC;AAC9D,UAAI,gBAAgB,IAAI,GAAG;AAAA,IAC7B;AACA,QAAI,UAAU,MAAM;AAClB,aAAO,IAAI,MAAM,0BAA0B,CAAC;AAC5C,UAAI,gBAAgB,IAAI,GAAG;AAAA,IAC7B;AACA,QAAI,MAAM,IAAI,gBAAgB,IAAI;AAAA,EACpC,CAAC;AACH;","names":[]}
package/dist/server.mjs CHANGED
@@ -53,6 +53,8 @@ import { getClientInfoFromRequest } from "./infra/utils/client-info";
53
53
  import { getUserContext } from "./modules/auth/utils/get-user-context";
54
54
  import { buildWlOverride, buildWlOverrideFromJwt } from "./modules/auth/utils/build-wl-override";
55
55
  import { safeServerAction } from "./utils/safeServerAction";
56
+ import { processImageAction } from "./modules/images/actions/process-image.action";
57
+ import { processImage } from "./modules/images/services/image-processing.service";
56
58
  export {
57
59
  ApiClient,
58
60
  accountService,
@@ -94,6 +96,8 @@ export {
94
96
  listProjectUsersAction,
95
97
  listSubscriptionsAction,
96
98
  plansService,
99
+ processImage,
100
+ processImageAction,
97
101
  projectUsersService,
98
102
  removeProjectUserAction,
99
103
  requestEmailChangeAction,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/server.ts"],"sourcesContent":["import 'server-only';\r\n\r\n// API Client\r\nexport { ApiClient, api, apiClient } from './infra/api/client';\r\n\r\n// Services\r\nexport { authService } from './modules/auth/services/auth.service';\r\nexport { whitelabelService } from './modules/whitelabel/services/whitelabel.service';\r\nexport { revalidateWhitelabelAction } from './modules/whitelabel/actions/revalidate-whitelabel.action';\r\nexport { assertManagementPermission } from './modules/auth/utils/assert-management-permission';\r\nexport { assertManagementSubscription } from './modules/auth/utils/assert-management-subscription';\r\nexport { assertSubscriptionAddon } from './modules/auth/utils/assert-subscription-addon';\r\nexport { assertPaidSubscription } from './modules/auth/utils/assert-paid-subscription';\r\nexport { subscriptionsService } from './modules/subscriptions/services/subscriptions.service';\r\nexport { plansService } from './modules/plans/services/plans.service';\r\nexport { cardsService } from './modules/cards/services/cards.service';\r\nexport { iaCreditsService } from './modules/ia-credits/services/ia-credits.service';\r\n\r\n// Actions\r\nexport { findWhitelabel } from './modules/whitelabel/actions/find-whitelabel.action';\r\nexport { validateSessionAction } from './modules/auth/actions/validate-session.action';\r\nexport { findUserById } from './modules/users/action/find-user-by-id.action';\r\nexport { findCurrentAccount } from './modules/accounts/actions/find-current-account.action';\r\nexport { listSubscriptionsAction } from './modules/subscriptions/actions/list-subscriptions.action';\r\nexport { findPlanByIdAction } from './modules/plans/actions/find-plan-by-id.action';\r\nexport { listPlansAction } from './modules/plans/actions/list-plans.action';\r\nexport { calculateSubscriptionAction } from './modules/subscriptions/actions/calculate-subscription.action';\r\nexport { updateSubscriptionPlanAction } from './modules/subscriptions/actions/update-subscription-plan.action';\r\nexport { listCardsAction } from './modules/cards/actions/list-cards.action';\r\nexport { createCardAction } from './modules/cards/actions/create-card.action';\r\nexport { resolvePlanExtrasPrices } from './modules/subscriptions/utils/resolve-plan-extras-prices';\r\nexport { hasSubscriptionExpired } from './modules/subscriptions/utils/has-subscription-expired';\r\nexport { listIaCreditsAction } from './modules/ia-credits/actions/list-ia-credits.action';\r\n\r\n// Project Users Services & Actions (server-only)\r\nexport { projectUsersService } from './modules/projects/services/project-users.service';\r\nexport { listProjectUsersAction } from './modules/projects/actions/list-project-users.action';\r\nexport { listAvailableUsersAction } from './modules/projects/actions/list-available-users.action';\r\nexport { addProjectUserAction } from './modules/projects/actions/add-project-user.action';\r\nexport { removeProjectUserAction } from './modules/projects/actions/remove-project-user.action';\r\n\r\n// Account Services & Actions (server-only)\r\nexport { accountService } from './modules/accounts/services/account.service';\r\nexport { twoFactorService } from './modules/accounts/services/two-factor.service';\r\nexport {\r\n listAccountUsersAction,\r\n updateUserAction,\r\n updateAccountAction,\r\n updateAccountUserByIdAction,\r\n deleteAccountUserAction,\r\n deleteAccountAction,\r\n createAccountUserAction,\r\n changePasswordAction,\r\n requestEmailChangeAction,\r\n confirmEmailChangeAction,\r\n requestPhoneChangeAction,\r\n confirmPhoneChangeAction,\r\n generateTwoFactorAction,\r\n confirmTwoFactorAction,\r\n disableTwoFactorAction,\r\n} from './modules/accounts/actions/account-management.action';\r\n\r\n// Server Utils\r\nexport { getClientInfoFromRequest } from './infra/utils/client-info';\r\nexport { getUserContext } from './modules/auth/utils/get-user-context';\r\nexport { buildWlOverride, buildWlOverrideFromJwt } from './modules/auth/utils/build-wl-override';\r\nexport { safeServerAction } from './utils/safeServerAction';\r\n"],"mappings":"AAAA,OAAO;AAGP,SAAS,WAAW,KAAK,iBAAiB;AAG1C,SAAS,mBAAmB;AAC5B,SAAS,yBAAyB;AAClC,SAAS,kCAAkC;AAC3C,SAAS,kCAAkC;AAC3C,SAAS,oCAAoC;AAC7C,SAAS,+BAA+B;AACxC,SAAS,8BAA8B;AACvC,SAAS,4BAA4B;AACrC,SAAS,oBAAoB;AAC7B,SAAS,oBAAoB;AAC7B,SAAS,wBAAwB;AAGjC,SAAS,sBAAsB;AAC/B,SAAS,6BAA6B;AACtC,SAAS,oBAAoB;AAC7B,SAAS,0BAA0B;AACnC,SAAS,+BAA+B;AACxC,SAAS,0BAA0B;AACnC,SAAS,uBAAuB;AAChC,SAAS,mCAAmC;AAC5C,SAAS,oCAAoC;AAC7C,SAAS,uBAAuB;AAChC,SAAS,wBAAwB;AACjC,SAAS,+BAA+B;AACxC,SAAS,8BAA8B;AACvC,SAAS,2BAA2B;AAGpC,SAAS,2BAA2B;AACpC,SAAS,8BAA8B;AACvC,SAAS,gCAAgC;AACzC,SAAS,4BAA4B;AACrC,SAAS,+BAA+B;AAGxC,SAAS,sBAAsB;AAC/B,SAAS,wBAAwB;AACjC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP,SAAS,gCAAgC;AACzC,SAAS,sBAAsB;AAC/B,SAAS,iBAAiB,8BAA8B;AACxD,SAAS,wBAAwB;","names":[]}
1
+ {"version":3,"sources":["../src/server.ts"],"sourcesContent":["import 'server-only';\r\n\r\n// API Client\r\nexport { ApiClient, api, apiClient } from './infra/api/client';\r\n\r\n// Services\r\nexport { authService } from './modules/auth/services/auth.service';\r\nexport { whitelabelService } from './modules/whitelabel/services/whitelabel.service';\r\nexport { revalidateWhitelabelAction } from './modules/whitelabel/actions/revalidate-whitelabel.action';\r\nexport { assertManagementPermission } from './modules/auth/utils/assert-management-permission';\r\nexport { assertManagementSubscription } from './modules/auth/utils/assert-management-subscription';\r\nexport { assertSubscriptionAddon } from './modules/auth/utils/assert-subscription-addon';\r\nexport { assertPaidSubscription } from './modules/auth/utils/assert-paid-subscription';\r\nexport { subscriptionsService } from './modules/subscriptions/services/subscriptions.service';\r\nexport { plansService } from './modules/plans/services/plans.service';\r\nexport { cardsService } from './modules/cards/services/cards.service';\r\nexport { iaCreditsService } from './modules/ia-credits/services/ia-credits.service';\r\n\r\n// Actions\r\nexport { findWhitelabel } from './modules/whitelabel/actions/find-whitelabel.action';\r\nexport { validateSessionAction } from './modules/auth/actions/validate-session.action';\r\nexport { findUserById } from './modules/users/action/find-user-by-id.action';\r\nexport { findCurrentAccount } from './modules/accounts/actions/find-current-account.action';\r\nexport { listSubscriptionsAction } from './modules/subscriptions/actions/list-subscriptions.action';\r\nexport { findPlanByIdAction } from './modules/plans/actions/find-plan-by-id.action';\r\nexport { listPlansAction } from './modules/plans/actions/list-plans.action';\r\nexport { calculateSubscriptionAction } from './modules/subscriptions/actions/calculate-subscription.action';\r\nexport { updateSubscriptionPlanAction } from './modules/subscriptions/actions/update-subscription-plan.action';\r\nexport { listCardsAction } from './modules/cards/actions/list-cards.action';\r\nexport { createCardAction } from './modules/cards/actions/create-card.action';\r\nexport { resolvePlanExtrasPrices } from './modules/subscriptions/utils/resolve-plan-extras-prices';\r\nexport { hasSubscriptionExpired } from './modules/subscriptions/utils/has-subscription-expired';\r\nexport { listIaCreditsAction } from './modules/ia-credits/actions/list-ia-credits.action';\r\n\r\n// Project Users Services & Actions (server-only)\r\nexport { projectUsersService } from './modules/projects/services/project-users.service';\r\nexport { listProjectUsersAction } from './modules/projects/actions/list-project-users.action';\r\nexport { listAvailableUsersAction } from './modules/projects/actions/list-available-users.action';\r\nexport { addProjectUserAction } from './modules/projects/actions/add-project-user.action';\r\nexport { removeProjectUserAction } from './modules/projects/actions/remove-project-user.action';\r\n\r\n// Account Services & Actions (server-only)\r\nexport { accountService } from './modules/accounts/services/account.service';\r\nexport { twoFactorService } from './modules/accounts/services/two-factor.service';\r\nexport {\r\n listAccountUsersAction,\r\n updateUserAction,\r\n updateAccountAction,\r\n updateAccountUserByIdAction,\r\n deleteAccountUserAction,\r\n deleteAccountAction,\r\n createAccountUserAction,\r\n changePasswordAction,\r\n requestEmailChangeAction,\r\n confirmEmailChangeAction,\r\n requestPhoneChangeAction,\r\n confirmPhoneChangeAction,\r\n generateTwoFactorAction,\r\n confirmTwoFactorAction,\r\n disableTwoFactorAction,\r\n} from './modules/accounts/actions/account-management.action';\r\n\r\n// Server Utils\r\nexport { getClientInfoFromRequest } from './infra/utils/client-info';\r\nexport { getUserContext } from './modules/auth/utils/get-user-context';\r\nexport { buildWlOverride, buildWlOverrideFromJwt } from './modules/auth/utils/build-wl-override';\r\nexport { safeServerAction } from './utils/safeServerAction';\r\n\r\n// Image Processing (server-side)\r\nexport { processImageAction } from './modules/images/actions/process-image.action';\r\nexport { processImage } from './modules/images/services/image-processing.service';\r\nexport type {\r\n ProcessImageInput,\r\n ProcessImageOutput,\r\n ProcessImageActionInput,\r\n ProcessImageActionOutput,\r\n} from './modules/images/types/image.type';\r\n"],"mappings":"AAAA,OAAO;AAGP,SAAS,WAAW,KAAK,iBAAiB;AAG1C,SAAS,mBAAmB;AAC5B,SAAS,yBAAyB;AAClC,SAAS,kCAAkC;AAC3C,SAAS,kCAAkC;AAC3C,SAAS,oCAAoC;AAC7C,SAAS,+BAA+B;AACxC,SAAS,8BAA8B;AACvC,SAAS,4BAA4B;AACrC,SAAS,oBAAoB;AAC7B,SAAS,oBAAoB;AAC7B,SAAS,wBAAwB;AAGjC,SAAS,sBAAsB;AAC/B,SAAS,6BAA6B;AACtC,SAAS,oBAAoB;AAC7B,SAAS,0BAA0B;AACnC,SAAS,+BAA+B;AACxC,SAAS,0BAA0B;AACnC,SAAS,uBAAuB;AAChC,SAAS,mCAAmC;AAC5C,SAAS,oCAAoC;AAC7C,SAAS,uBAAuB;AAChC,SAAS,wBAAwB;AACjC,SAAS,+BAA+B;AACxC,SAAS,8BAA8B;AACvC,SAAS,2BAA2B;AAGpC,SAAS,2BAA2B;AACpC,SAAS,8BAA8B;AACvC,SAAS,gCAAgC;AACzC,SAAS,4BAA4B;AACrC,SAAS,+BAA+B;AAGxC,SAAS,sBAAsB;AAC/B,SAAS,wBAAwB;AACjC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP,SAAS,gCAAgC;AACzC,SAAS,sBAAsB;AAC/B,SAAS,iBAAiB,8BAA8B;AACxD,SAAS,wBAAwB;AAGjC,SAAS,0BAA0B;AACnC,SAAS,oBAAoB;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@greatapps/common",
3
- "version": "1.1.448",
3
+ "version": "1.1.449",
4
4
  "description": "Shared library for GreatApps frontend applications",
5
5
  "main": "./dist/index.mjs",
6
6
  "types": "./src/index.ts",
@@ -31,6 +31,7 @@
31
31
  "author": "",
32
32
  "license": "ISC",
33
33
  "dependencies": {
34
+ "@cf-wasm/photon": "^0.3.4",
34
35
  "@greatapps/cache": "^2.0.1",
35
36
  "@radix-ui/react-accordion": "^1.2.12",
36
37
  "@radix-ui/react-checkbox": "^1.3.3",
@@ -47,6 +48,7 @@
47
48
  "@tanstack/query-broadcast-client-experimental": "^5.95.2",
48
49
  "@tanstack/react-query-persist-client": "^5.95.2",
49
50
  "axios": "^1.13.6",
51
+ "browser-image-compression": "^2.0.2",
50
52
  "class-variance-authority": "^0.7.1",
51
53
  "clsx": "^2.1.1",
52
54
  "cmdk": "^1.1.1",
@@ -55,6 +57,7 @@
55
57
  "input-otp": "^1.4.2",
56
58
  "libphonenumber-js": "^1.12.40",
57
59
  "react-day-picker": "^9.14.0",
60
+ "react-image-crop": "^11.0.10",
58
61
  "server-only": "^0.0.1",
59
62
  "sonner": ">=2.0.0",
60
63
  "tailwind-merge": "^3.5.0",
@@ -9,7 +9,7 @@ import { useCreateCard } from '../../../modules/cards/hooks/create-card.hook';
9
9
  import { useUpdateAccount } from '../../../modules/accounts/hooks/useAccountManagement';
10
10
  import { CardFormFields, BillingFormFields, cardFormSchema } from './CardFormFields';
11
11
  import type { CardFormData, StripeElementsStatus } from './CardFormFields';
12
- import { IconX } from '@tabler/icons-react';
12
+ import { IconLoader2, IconX } from '@tabler/icons-react';
13
13
  import { useForm } from 'react-hook-form';
14
14
  import { zodResolver } from '@hookform/resolvers/zod';
15
15
  import { CardNumberElement, useElements, useStripe } from '@stripe/react-stripe-js';
@@ -248,6 +248,7 @@ export default function AddCardModal() {
248
248
  Voltar
249
249
  </Button>
250
250
  <Button type="submit" className="h-10!" disabled={isSubmitting || !isStep2Valid}>
251
+ {isSubmitting && <IconLoader2 className="size-4 animate-spin" />}
251
252
  {isSubmitting ? 'Adicionando cartão...' : 'Adicionar cartão'}
252
253
  </Button>
253
254
  </>
@@ -0,0 +1,97 @@
1
+ 'use client'
2
+
3
+ import { useRef, useState } from 'react'
4
+ import ReactCrop, {
5
+ type Crop,
6
+ centerCrop,
7
+ makeAspectCrop,
8
+ } from 'react-image-crop'
9
+ import 'react-image-crop/dist/ReactCrop.css'
10
+ import {
11
+ Dialog,
12
+ DialogContent,
13
+ DialogFooter,
14
+ DialogHeader,
15
+ DialogTitle,
16
+ } from '../../ui/overlay/Dialog'
17
+ import { Button } from '../../ui/buttons/Button'
18
+
19
+ interface ImageCropModalProps {
20
+ open: boolean
21
+ onOpenChange: (open: boolean) => void
22
+ imageSrc: string
23
+ aspectRatio: number
24
+ onConfirm: (image: HTMLImageElement, crop: Crop) => void
25
+ }
26
+
27
+ export function ImageCropModal({
28
+ open,
29
+ onOpenChange,
30
+ imageSrc,
31
+ aspectRatio,
32
+ onConfirm,
33
+ }: ImageCropModalProps) {
34
+ const imgRef = useRef<HTMLImageElement>(null)
35
+ const [crop, setCrop] = useState<Crop>()
36
+
37
+ const onImageLoad = (e: React.SyntheticEvent<HTMLImageElement>) => {
38
+ const { naturalWidth, naturalHeight } = e.currentTarget
39
+
40
+ const initialCrop = centerCrop(
41
+ makeAspectCrop(
42
+ { unit: '%', width: 90 },
43
+ aspectRatio,
44
+ naturalWidth,
45
+ naturalHeight
46
+ ),
47
+ naturalWidth,
48
+ naturalHeight
49
+ )
50
+
51
+ setCrop(initialCrop)
52
+ }
53
+
54
+ const handleConfirm = () => {
55
+ if (imgRef.current && crop) {
56
+ onConfirm(imgRef.current, crop)
57
+ }
58
+ }
59
+
60
+ return (
61
+ <Dialog open={open} onOpenChange={onOpenChange}>
62
+ <DialogContent className="max-w-2xl">
63
+ <DialogHeader>
64
+ <DialogTitle>Ajustar imagem</DialogTitle>
65
+ </DialogHeader>
66
+
67
+ <div className="flex justify-center py-4">
68
+ <ReactCrop
69
+ crop={crop}
70
+ onChange={(c) => setCrop(c)}
71
+ aspect={aspectRatio}
72
+ className="max-h-[60vh]"
73
+ >
74
+ <img
75
+ ref={imgRef}
76
+ src={imageSrc}
77
+ alt="Crop"
78
+ onLoad={onImageLoad}
79
+ className="max-h-[60vh] object-contain"
80
+ />
81
+ </ReactCrop>
82
+ </div>
83
+
84
+ <p className="paragraph-small-regular text-gray-500 text-center">
85
+ Arraste para ajustar a área de corte
86
+ </p>
87
+
88
+ <DialogFooter>
89
+ <Button variant="secondary" onClick={() => onOpenChange(false)}>
90
+ Cancelar
91
+ </Button>
92
+ <Button onClick={handleConfirm}>Confirmar</Button>
93
+ </DialogFooter>
94
+ </DialogContent>
95
+ </Dialog>
96
+ )
97
+ }
@@ -0,0 +1,125 @@
1
+ 'use client'
2
+
3
+ import { useRef } from 'react'
4
+ import { useImageUpload } from '../../../modules/images/hooks/use-image-upload.hook'
5
+ import { ImageCropModal } from './ImageCropModal'
6
+ import { Button } from '../../ui/buttons/Button'
7
+ import type { ImageConfig } from '../../../modules/images/types/image.type'
8
+
9
+ interface ImageUploadProps {
10
+ config: ImageConfig
11
+ value?: string
12
+ onChange?: (value: string) => void
13
+ onError?: (error: string) => void
14
+ className?: string
15
+ }
16
+
17
+ export function ImageUpload({
18
+ config,
19
+ value,
20
+ onChange,
21
+ onError,
22
+ className,
23
+ }: ImageUploadProps) {
24
+ const inputRef = useRef<HTMLInputElement>(null)
25
+
26
+ const {
27
+ originalImage,
28
+ croppedPreview,
29
+ showCropModal,
30
+ isProcessing,
31
+ handleFileSelect,
32
+ handleCropConfirm,
33
+ setShowCropModal,
34
+ clear,
35
+ } = useImageUpload({
36
+ config,
37
+ onSuccess: (base64) => onChange?.(base64),
38
+ onError,
39
+ })
40
+
41
+ const displayImage = croppedPreview || value
42
+
43
+ return (
44
+ <div className={className}>
45
+ <input
46
+ ref={inputRef}
47
+ type="file"
48
+ accept="image/png,image/jpeg,image/webp,image/gif"
49
+ className="hidden"
50
+ onChange={(e) => {
51
+ const file = e.target.files?.[0]
52
+ if (file) handleFileSelect(file)
53
+ e.target.value = ''
54
+ }}
55
+ />
56
+
57
+ {displayImage ? (
58
+ <div className="relative">
59
+ <div
60
+ className="overflow-hidden rounded-lg border border-gray-200"
61
+ style={{ aspectRatio: config.aspectRatio }}
62
+ >
63
+ <img
64
+ src={displayImage}
65
+ alt="Preview"
66
+ className="w-full h-full object-cover"
67
+ />
68
+ </div>
69
+
70
+ <div className="flex gap-2 mt-2">
71
+ <Button
72
+ variant="secondary"
73
+ size="sm"
74
+ onClick={() => inputRef.current?.click()}
75
+ disabled={isProcessing}
76
+ >
77
+ Trocar
78
+ </Button>
79
+ <Button
80
+ variant="secondary"
81
+ size="sm"
82
+ onClick={() => {
83
+ clear()
84
+ onChange?.('')
85
+ }}
86
+ disabled={isProcessing}
87
+ >
88
+ Remover
89
+ </Button>
90
+ </div>
91
+
92
+ {isProcessing && (
93
+ <p className="paragraph-small-regular text-gray-500 mt-2">
94
+ Processando...
95
+ </p>
96
+ )}
97
+ </div>
98
+ ) : (
99
+ <button
100
+ type="button"
101
+ onClick={() => inputRef.current?.click()}
102
+ className="w-full border-2 border-dashed border-gray-300 rounded-lg p-8 text-center hover:border-gray-400 transition-colors"
103
+ style={{ aspectRatio: config.aspectRatio }}
104
+ >
105
+ <p className="paragraph-medium-regular text-gray-600">
106
+ Clique para enviar imagem
107
+ </p>
108
+ <p className="paragraph-small-regular text-gray-400 mt-1">
109
+ Tamanho recomendado: {config.width} x {config.height} pixels
110
+ </p>
111
+ </button>
112
+ )}
113
+
114
+ {originalImage && (
115
+ <ImageCropModal
116
+ open={showCropModal}
117
+ onOpenChange={setShowCropModal}
118
+ imageSrc={originalImage}
119
+ aspectRatio={config.aspectRatio}
120
+ onConfirm={handleCropConfirm}
121
+ />
122
+ )}
123
+ </div>
124
+ )
125
+ }
@@ -0,0 +1,2 @@
1
+ export { ImageUpload } from './ImageUpload'
2
+ export { ImageCropModal } from './ImageCropModal'