@greatapps/common 1.1.455 → 1.1.457

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.
@@ -77,13 +77,17 @@ function ImageCropModal({
77
77
  imageSrc,
78
78
  aspectRatio,
79
79
  onConfirm,
80
- showCircleGuide
80
+ showCircleGuide,
81
+ minWidth,
82
+ minHeight
81
83
  }) {
82
84
  const imgRef = useRef(null);
83
85
  const containerRef = useRef(null);
84
86
  const [crop, setCrop] = useState();
87
+ const [imageSize, setImageSize] = useState({ width: 0, height: 0 });
85
88
  const onImageLoad = (e) => {
86
89
  const { naturalWidth, naturalHeight } = e.currentTarget;
90
+ setImageSize({ width: naturalWidth, height: naturalHeight });
87
91
  const initialCrop = centerCrop(
88
92
  makeAspectCrop(
89
93
  { unit: "%", width: 90 },
@@ -96,6 +100,8 @@ function ImageCropModal({
96
100
  );
97
101
  setCrop(initialCrop);
98
102
  };
103
+ const minCropWidth = minWidth && imageSize.width > 0 ? minWidth / imageSize.width * 100 : void 0;
104
+ const minCropHeight = minHeight && imageSize.height > 0 ? minHeight / imageSize.height * 100 : void 0;
99
105
  const handleConfirm = () => {
100
106
  if (imgRef.current && crop) {
101
107
  onConfirm(imgRef.current, crop);
@@ -123,6 +129,8 @@ function ImageCropModal({
123
129
  aspect: aspectRatio,
124
130
  className: `max-h-[60vh] custom-crop-area ${shouldShowCircle ? "with-circle" : ""}`,
125
131
  keepSelection: true,
132
+ minWidth: minCropWidth,
133
+ minHeight: minCropHeight,
126
134
  renderSelectionAddon,
127
135
  children: /* @__PURE__ */ jsx(
128
136
  "img",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/components/widgets/ImageUpload/ImageCropModal.tsx"],"sourcesContent":["'use client'\n\nimport { useRef, useState } from 'react'\nimport ReactCrop, {\n type Crop,\n centerCrop,\n makeAspectCrop,\n} from 'react-image-crop'\nimport 'react-image-crop/dist/ReactCrop.css'\nimport {\n Dialog,\n DialogContent,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '../../ui/overlay/Dialog'\nimport { Button } from '../../ui/buttons/Button'\n\n// CSS customizado para grid pontilhado fino e círculo\nconst baseCropStyles = `\n .custom-crop-area .ReactCrop__crop-selection {\n border: 2px solid white !important;\n box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.5) !important;\n }\n\n /* Grid de 9 quadrados - linhas pontilhadas finas */\n .custom-crop-area .ReactCrop__crop-selection .crop-grid {\n position: absolute;\n inset: 0;\n pointer-events: none;\n }\n\n .custom-crop-area .ReactCrop__crop-selection .crop-grid-line {\n position: absolute;\n background: repeating-linear-gradient(\n to right,\n rgba(255, 255, 255, 0.5) 0,\n rgba(255, 255, 255, 0.5) 4px,\n transparent 4px,\n transparent 8px\n );\n }\n\n .custom-crop-area .ReactCrop__crop-selection .crop-grid-line.horizontal {\n left: 0;\n right: 0;\n height: 1px;\n }\n\n .custom-crop-area .ReactCrop__crop-selection .crop-grid-line.vertical {\n top: 0;\n bottom: 0;\n width: 1px;\n background: repeating-linear-gradient(\n to bottom,\n rgba(255, 255, 255, 0.5) 0,\n rgba(255, 255, 255, 0.5) 4px,\n transparent 4px,\n transparent 8px\n );\n }\n\n .custom-crop-area .ReactCrop__crop-selection .crop-grid-line.h1 { top: 33.33%; }\n .custom-crop-area .ReactCrop__crop-selection .crop-grid-line.h2 { top: 66.66%; }\n .custom-crop-area .ReactCrop__crop-selection .crop-grid-line.v1 { left: 33.33%; }\n .custom-crop-area .ReactCrop__crop-selection .crop-grid-line.v2 { left: 66.66%; }\n\n /* Círculo inscrito */\n .custom-crop-area.with-circle .ReactCrop__crop-selection .crop-circle {\n position: absolute;\n inset: 0;\n border: 1px dashed rgba(255, 255, 255, 0.6);\n border-radius: 50%;\n pointer-events: none;\n }\n`\n\ninterface ImageCropModalProps {\n open: boolean\n onOpenChange: (open: boolean) => void\n imageSrc: string\n aspectRatio: number\n onConfirm: (image: HTMLImageElement, crop: Crop) => void\n showCircleGuide?: boolean\n}\n\nexport function ImageCropModal({\n open,\n onOpenChange,\n imageSrc,\n aspectRatio,\n onConfirm,\n showCircleGuide,\n}: ImageCropModalProps) {\n const imgRef = useRef<HTMLImageElement>(null)\n const containerRef = useRef<HTMLDivElement>(null)\n const [crop, setCrop] = useState<Crop>()\n\n const onImageLoad = (e: React.SyntheticEvent<HTMLImageElement>) => {\n const { naturalWidth, naturalHeight } = e.currentTarget\n\n const initialCrop = centerCrop(\n makeAspectCrop(\n { unit: '%', width: 90 },\n aspectRatio,\n naturalWidth,\n naturalHeight\n ),\n naturalWidth,\n naturalHeight\n )\n\n setCrop(initialCrop)\n }\n\n const handleConfirm = () => {\n if (imgRef.current && crop) {\n onConfirm(imgRef.current, crop)\n }\n }\n\n // Determina se deve mostrar círculo (aspect ratio 1:1 por padrão)\n const shouldShowCircle = showCircleGuide ?? aspectRatio === 1\n\n // Renderiza grid e círculo dentro da área de crop\n const renderSelectionAddon = () => (\n <>\n {/* Grid de 9 quadrados com linhas pontilhadas */}\n <div className=\"crop-grid\">\n <div className=\"crop-grid-line horizontal h1\" />\n <div className=\"crop-grid-line horizontal h2\" />\n <div className=\"crop-grid-line vertical v1\" />\n <div className=\"crop-grid-line vertical v2\" />\n </div>\n {/* Círculo inscrito */}\n {shouldShowCircle && <div className=\"crop-circle\" />}\n </>\n )\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <style>{baseCropStyles}</style>\n <DialogContent className=\"max-w-2xl\">\n <DialogHeader>\n <DialogTitle>Ajustar imagem</DialogTitle>\n </DialogHeader>\n\n <div ref={containerRef} className=\"flex justify-center py-4 relative\">\n <ReactCrop\n crop={crop}\n onChange={(c) => setCrop(c)}\n aspect={aspectRatio}\n className={`max-h-[60vh] custom-crop-area ${shouldShowCircle ? 'with-circle' : ''}`}\n keepSelection\n renderSelectionAddon={renderSelectionAddon}\n >\n <img\n ref={imgRef}\n src={imageSrc}\n alt=\"Crop\"\n onLoad={onImageLoad}\n className=\"max-h-[60vh] object-contain\"\n />\n </ReactCrop>\n </div>\n\n <p className=\"paragraph-small-regular text-zinc-500 text-center\">\n Arraste para ajustar a área de corte\n </p>\n\n <DialogFooter>\n <Button variant=\"secondary\" onClick={() => onOpenChange(false)}>\n Cancelar\n </Button>\n <Button onClick={handleConfirm}>Confirmar</Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n )\n}\n"],"mappings":";AA8HI,mBAGI,KADF,YAFF;AA5HJ,SAAS,QAAQ,gBAAgB;AACjC,OAAO;AAAA,EAEL;AAAA,EACA;AAAA,OACK;AACP,OAAO;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,cAAc;AAGvB,MAAM,iBAAiB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAmEhB,SAAS,eAAe;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAwB;AACtB,QAAM,SAAS,OAAyB,IAAI;AAC5C,QAAM,eAAe,OAAuB,IAAI;AAChD,QAAM,CAAC,MAAM,OAAO,IAAI,SAAe;AAEvC,QAAM,cAAc,CAAC,MAA8C;AACjE,UAAM,EAAE,cAAc,cAAc,IAAI,EAAE;AAE1C,UAAM,cAAc;AAAA,MAClB;AAAA,QACE,EAAE,MAAM,KAAK,OAAO,GAAG;AAAA,QACvB;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAEA,YAAQ,WAAW;AAAA,EACrB;AAEA,QAAM,gBAAgB,MAAM;AAC1B,QAAI,OAAO,WAAW,MAAM;AAC1B,gBAAU,OAAO,SAAS,IAAI;AAAA,IAChC;AAAA,EACF;AAGA,QAAM,mBAAmB,mBAAmB,gBAAgB;AAG5D,QAAM,uBAAuB,MAC3B,iCAEE;AAAA,yBAAC,SAAI,WAAU,aACb;AAAA,0BAAC,SAAI,WAAU,gCAA+B;AAAA,MAC9C,oBAAC,SAAI,WAAU,gCAA+B;AAAA,MAC9C,oBAAC,SAAI,WAAU,8BAA6B;AAAA,MAC5C,oBAAC,SAAI,WAAU,8BAA6B;AAAA,OAC9C;AAAA,IAEC,oBAAoB,oBAAC,SAAI,WAAU,eAAc;AAAA,KACpD;AAGF,SACE,qBAAC,UAAO,MAAY,cAClB;AAAA,wBAAC,WAAO,0BAAe;AAAA,IACvB,qBAAC,iBAAc,WAAU,aACvB;AAAA,0BAAC,gBACC,8BAAC,eAAY,4BAAc,GAC7B;AAAA,MAEA,oBAAC,SAAI,KAAK,cAAc,WAAU,qCAChC;AAAA,QAAC;AAAA;AAAA,UACC;AAAA,UACA,UAAU,CAAC,MAAM,QAAQ,CAAC;AAAA,UAC1B,QAAQ;AAAA,UACR,WAAW,iCAAiC,mBAAmB,gBAAgB,EAAE;AAAA,UACjF,eAAa;AAAA,UACb;AAAA,UAEA;AAAA,YAAC;AAAA;AAAA,cACC,KAAK;AAAA,cACL,KAAK;AAAA,cACL,KAAI;AAAA,cACJ,QAAQ;AAAA,cACR,WAAU;AAAA;AAAA,UACZ;AAAA;AAAA,MACF,GACF;AAAA,MAEA,oBAAC,OAAE,WAAU,qDAAoD,qDAEjE;AAAA,MAEA,qBAAC,gBACC;AAAA,4BAAC,UAAO,SAAQ,aAAY,SAAS,MAAM,aAAa,KAAK,GAAG,sBAEhE;AAAA,QACA,oBAAC,UAAO,SAAS,eAAe,uBAAS;AAAA,SAC3C;AAAA,OACF;AAAA,KACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../../src/components/widgets/ImageUpload/ImageCropModal.tsx"],"sourcesContent":["'use client'\n\nimport { useRef, useState } from 'react'\nimport ReactCrop, {\n type Crop,\n centerCrop,\n makeAspectCrop,\n} from 'react-image-crop'\nimport 'react-image-crop/dist/ReactCrop.css'\nimport {\n Dialog,\n DialogContent,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '../../ui/overlay/Dialog'\nimport { Button } from '../../ui/buttons/Button'\n\n// CSS customizado para grid pontilhado fino e círculo\nconst baseCropStyles = `\n .custom-crop-area .ReactCrop__crop-selection {\n border: 2px solid white !important;\n box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.5) !important;\n }\n\n /* Grid de 9 quadrados - linhas pontilhadas finas */\n .custom-crop-area .ReactCrop__crop-selection .crop-grid {\n position: absolute;\n inset: 0;\n pointer-events: none;\n }\n\n .custom-crop-area .ReactCrop__crop-selection .crop-grid-line {\n position: absolute;\n background: repeating-linear-gradient(\n to right,\n rgba(255, 255, 255, 0.5) 0,\n rgba(255, 255, 255, 0.5) 4px,\n transparent 4px,\n transparent 8px\n );\n }\n\n .custom-crop-area .ReactCrop__crop-selection .crop-grid-line.horizontal {\n left: 0;\n right: 0;\n height: 1px;\n }\n\n .custom-crop-area .ReactCrop__crop-selection .crop-grid-line.vertical {\n top: 0;\n bottom: 0;\n width: 1px;\n background: repeating-linear-gradient(\n to bottom,\n rgba(255, 255, 255, 0.5) 0,\n rgba(255, 255, 255, 0.5) 4px,\n transparent 4px,\n transparent 8px\n );\n }\n\n .custom-crop-area .ReactCrop__crop-selection .crop-grid-line.h1 { top: 33.33%; }\n .custom-crop-area .ReactCrop__crop-selection .crop-grid-line.h2 { top: 66.66%; }\n .custom-crop-area .ReactCrop__crop-selection .crop-grid-line.v1 { left: 33.33%; }\n .custom-crop-area .ReactCrop__crop-selection .crop-grid-line.v2 { left: 66.66%; }\n\n /* Círculo inscrito */\n .custom-crop-area.with-circle .ReactCrop__crop-selection .crop-circle {\n position: absolute;\n inset: 0;\n border: 1px dashed rgba(255, 255, 255, 0.6);\n border-radius: 50%;\n pointer-events: none;\n }\n`\n\ninterface ImageCropModalProps {\n open: boolean\n onOpenChange: (open: boolean) => void\n imageSrc: string\n aspectRatio: number\n onConfirm: (image: HTMLImageElement, crop: Crop) => void\n showCircleGuide?: boolean\n minWidth?: number\n minHeight?: number\n}\n\nexport function ImageCropModal({\n open,\n onOpenChange,\n imageSrc,\n aspectRatio,\n onConfirm,\n showCircleGuide,\n minWidth,\n minHeight,\n}: ImageCropModalProps) {\n const imgRef = useRef<HTMLImageElement>(null)\n const containerRef = useRef<HTMLDivElement>(null)\n const [crop, setCrop] = useState<Crop>()\n const [imageSize, setImageSize] = useState({ width: 0, height: 0 })\n\n const onImageLoad = (e: React.SyntheticEvent<HTMLImageElement>) => {\n const { naturalWidth, naturalHeight } = e.currentTarget\n setImageSize({ width: naturalWidth, height: naturalHeight })\n\n const initialCrop = centerCrop(\n makeAspectCrop(\n { unit: '%', width: 90 },\n aspectRatio,\n naturalWidth,\n naturalHeight\n ),\n naturalWidth,\n naturalHeight\n )\n\n setCrop(initialCrop)\n }\n\n // Calcula tamanho mínimo em porcentagem baseado nas dimensões mínimas em pixels\n const minCropWidth = minWidth && imageSize.width > 0\n ? (minWidth / imageSize.width) * 100\n : undefined\n const minCropHeight = minHeight && imageSize.height > 0\n ? (minHeight / imageSize.height) * 100\n : undefined\n\n const handleConfirm = () => {\n if (imgRef.current && crop) {\n onConfirm(imgRef.current, crop)\n }\n }\n\n // Determina se deve mostrar círculo (aspect ratio 1:1 por padrão)\n const shouldShowCircle = showCircleGuide ?? aspectRatio === 1\n\n // Renderiza grid e círculo dentro da área de crop\n const renderSelectionAddon = () => (\n <>\n {/* Grid de 9 quadrados com linhas pontilhadas */}\n <div className=\"crop-grid\">\n <div className=\"crop-grid-line horizontal h1\" />\n <div className=\"crop-grid-line horizontal h2\" />\n <div className=\"crop-grid-line vertical v1\" />\n <div className=\"crop-grid-line vertical v2\" />\n </div>\n {/* Círculo inscrito */}\n {shouldShowCircle && <div className=\"crop-circle\" />}\n </>\n )\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <style>{baseCropStyles}</style>\n <DialogContent className=\"max-w-2xl\">\n <DialogHeader>\n <DialogTitle>Ajustar imagem</DialogTitle>\n </DialogHeader>\n\n <div ref={containerRef} className=\"flex justify-center py-4 relative\">\n <ReactCrop\n crop={crop}\n onChange={(c) => setCrop(c)}\n aspect={aspectRatio}\n className={`max-h-[60vh] custom-crop-area ${shouldShowCircle ? 'with-circle' : ''}`}\n keepSelection\n minWidth={minCropWidth}\n minHeight={minCropHeight}\n renderSelectionAddon={renderSelectionAddon}\n >\n <img\n ref={imgRef}\n src={imageSrc}\n alt=\"Crop\"\n onLoad={onImageLoad}\n className=\"max-h-[60vh] object-contain\"\n />\n </ReactCrop>\n </div>\n\n <p className=\"paragraph-small-regular text-zinc-500 text-center\">\n Arraste para ajustar a área de corte\n </p>\n\n <DialogFooter>\n <Button variant=\"secondary\" onClick={() => onOpenChange(false)}>\n Cancelar\n </Button>\n <Button onClick={handleConfirm}>Confirmar</Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n )\n}\n"],"mappings":"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","names":[]}
@@ -1,7 +1,7 @@
1
1
  "use server";
2
- import { processImage } from "../services/image-processing.service";
3
2
  async function processImageAction(input) {
4
3
  try {
4
+ const { processImage } = await import("../services/image-processing.service");
5
5
  const base64Data = input.base64Image.replace(/^data:image\/\w+;base64,/, "");
6
6
  const binaryString = atob(base64Data);
7
7
  const bytes = new Uint8Array(binaryString.length);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/modules/images/actions/process-image.action.ts"],"sourcesContent":["'use server'\n\nimport { processImage } from '../services/image-processing.service'\nimport type {\n ProcessImageActionInput,\n ProcessImageActionOutput,\n} from '../types/image.type'\n\nexport async function processImageAction(\n input: ProcessImageActionInput\n): Promise<ProcessImageActionOutput> {\n try {\n const base64Data = input.base64Image.replace(/^data:image\\/\\w+;base64,/, '')\n const binaryString = atob(base64Data)\n const bytes = new Uint8Array(binaryString.length)\n for (let i = 0; i < binaryString.length; i++) {\n bytes[i] = binaryString.charCodeAt(i)\n }\n\n const result = await processImage({\n imageBytes: bytes,\n targetWidth: input.targetWidth,\n targetHeight: input.targetHeight,\n })\n\n const base64Output = btoa(String.fromCharCode(...result.bytes))\n\n return {\n success: true,\n base64Image: `data:${result.format};base64,${base64Output}`,\n }\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Erro ao processar imagem',\n }\n }\n}\n"],"mappings":";AAEA,SAAS,oBAAoB;AAM7B,eAAsB,mBACpB,OACmC;AACnC,MAAI;AACF,UAAM,aAAa,MAAM,YAAY,QAAQ,4BAA4B,EAAE;AAC3E,UAAM,eAAe,KAAK,UAAU;AACpC,UAAM,QAAQ,IAAI,WAAW,aAAa,MAAM;AAChD,aAAS,IAAI,GAAG,IAAI,aAAa,QAAQ,KAAK;AAC5C,YAAM,CAAC,IAAI,aAAa,WAAW,CAAC;AAAA,IACtC;AAEA,UAAM,SAAS,MAAM,aAAa;AAAA,MAChC,YAAY;AAAA,MACZ,aAAa,MAAM;AAAA,MACnB,cAAc,MAAM;AAAA,IACtB,CAAC;AAED,UAAM,eAAe,KAAK,OAAO,aAAa,GAAG,OAAO,KAAK,CAAC;AAE9D,WAAO;AAAA,MACL,SAAS;AAAA,MACT,aAAa,QAAQ,OAAO,MAAM,WAAW,YAAY;AAAA,IAC3D;AAAA,EACF,SAAS,OAAO;AACd,WAAO;AAAA,MACL,SAAS;AAAA,MACT,OAAO,iBAAiB,QAAQ,MAAM,UAAU;AAAA,IAClD;AAAA,EACF;AACF;","names":[]}
1
+ {"version":3,"sources":["../../../../src/modules/images/actions/process-image.action.ts"],"sourcesContent":["'use server'\n\nimport type {\n ProcessImageActionInput,\n ProcessImageActionOutput,\n} from '../types/image.type'\n\nexport async function processImageAction(\n input: ProcessImageActionInput\n): Promise<ProcessImageActionOutput> {\n try {\n // Import dinâmico para evitar erro de fs.createReadStream no edge runtime\n const { processImage } = await import('../services/image-processing.service')\n\n const base64Data = input.base64Image.replace(/^data:image\\/\\w+;base64,/, '')\n const binaryString = atob(base64Data)\n const bytes = new Uint8Array(binaryString.length)\n for (let i = 0; i < binaryString.length; i++) {\n bytes[i] = binaryString.charCodeAt(i)\n }\n\n const result = await processImage({\n imageBytes: bytes,\n targetWidth: input.targetWidth,\n targetHeight: input.targetHeight,\n })\n\n const base64Output = btoa(String.fromCharCode(...result.bytes))\n\n return {\n success: true,\n base64Image: `data:${result.format};base64,${base64Output}`,\n }\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Erro ao processar imagem',\n }\n }\n}\n"],"mappings":";AAOA,eAAsB,mBACpB,OACmC;AACnC,MAAI;AAEF,UAAM,EAAE,aAAa,IAAI,MAAM,OAAO,sCAAsC;AAE5E,UAAM,aAAa,MAAM,YAAY,QAAQ,4BAA4B,EAAE;AAC3E,UAAM,eAAe,KAAK,UAAU;AACpC,UAAM,QAAQ,IAAI,WAAW,aAAa,MAAM;AAChD,aAAS,IAAI,GAAG,IAAI,aAAa,QAAQ,KAAK;AAC5C,YAAM,CAAC,IAAI,aAAa,WAAW,CAAC;AAAA,IACtC;AAEA,UAAM,SAAS,MAAM,aAAa;AAAA,MAChC,YAAY;AAAA,MACZ,aAAa,MAAM;AAAA,MACnB,cAAc,MAAM;AAAA,IACtB,CAAC;AAED,UAAM,eAAe,KAAK,OAAO,aAAa,GAAG,OAAO,KAAK,CAAC;AAE9D,WAAO;AAAA,MACL,SAAS;AAAA,MACT,aAAa,QAAQ,OAAO,MAAM,WAAW,YAAY;AAAA,IAC3D;AAAA,EACF,SAAS,OAAO;AACd,WAAO;AAAA,MACL,SAAS;AAAA,MACT,OAAO,iBAAiB,QAAQ,MAAM,UAAU;AAAA,IAClD;AAAA,EACF;AACF;","names":[]}
@@ -1,29 +1,14 @@
1
1
  "use client";
2
2
  import { useState, useCallback } from "react";
3
- import { useMutation } from "@tanstack/react-query";
4
3
  import { compressImage } from "../utils/compress-image";
5
4
  import { cropImageToCanvas } from "../utils/crop-image";
6
5
  import { validateImage } from "../utils/validate-image";
7
- import { processImageAction } from "../actions/process-image.action";
8
6
  function useImageUpload(options) {
9
7
  const { config, onSuccess, onError } = options;
10
8
  const [originalImage, setOriginalImage] = useState(null);
11
9
  const [croppedPreview, setCroppedPreview] = useState(null);
12
10
  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
- });
11
+ const [isProcessing, setIsProcessing] = useState(false);
27
12
  const handleFileSelect = useCallback(
28
13
  async (file) => {
29
14
  const validation = validateImage(file);
@@ -47,6 +32,7 @@ function useImageUpload(options) {
47
32
  );
48
33
  const handleCropConfirm = useCallback(
49
34
  async (imageElement, cropArea) => {
35
+ setIsProcessing(true);
50
36
  try {
51
37
  const croppedBase64 = await cropImageToCanvas(
52
38
  imageElement,
@@ -56,23 +42,14 @@ function useImageUpload(options) {
56
42
  );
57
43
  setCroppedPreview(croppedBase64);
58
44
  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
- });
45
+ onSuccess?.(croppedBase64);
71
46
  } catch {
72
47
  onError?.("Erro ao aplicar crop");
48
+ } finally {
49
+ setIsProcessing(false);
73
50
  }
74
51
  },
75
- [config, processMutation, onError]
52
+ [config, onSuccess, onError]
76
53
  );
77
54
  const clear = useCallback(() => {
78
55
  setOriginalImage(null);
@@ -83,7 +60,7 @@ function useImageUpload(options) {
83
60
  croppedPreview,
84
61
  showCropModal,
85
62
  config,
86
- isProcessing: processMutation.isPending,
63
+ isProcessing,
87
64
  handleFileSelect,
88
65
  handleCropConfirm,
89
66
  setShowCropModal,
@@ -1 +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":[]}
1
+ {"version":3,"sources":["../../../../src/modules/images/hooks/use-image-upload.hook.ts"],"sourcesContent":["'use client'\n\nimport { useState, useCallback } from 'react'\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 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 const [isProcessing, setIsProcessing] = useState(false)\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 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 config,\n isProcessing,\n handleFileSelect,\n handleCropConfirm,\n setShowCropModal,\n clear,\n }\n}\n"],"mappings":";AAEA,SAAS,UAAU,mBAAmB;AAEtC,SAAS,qBAAqB;AAC9B,SAAS,yBAAyB;AAClC,SAAS,qBAAqB;AASvB,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;AACxD,QAAM,CAAC,cAAc,eAAe,IAAI,SAAS,KAAK;AAEtD,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,sBAAgB,IAAI;AACpB,UAAI;AAGF,cAAM,gBAAgB,MAAM;AAAA,UAC1B;AAAA,UACA;AAAA,UACA,OAAO;AAAA,UACP,OAAO;AAAA,QACT;AAEA,0BAAkB,aAAa;AAC/B,yBAAiB,KAAK;AACtB,oBAAY,aAAa;AAAA,MAC3B,QAAQ;AACN,kBAAU,sBAAsB;AAAA,MAClC,UAAE;AACA,wBAAgB,KAAK;AAAA,MACvB;AAAA,IACF;AAAA,IACA,CAAC,QAAQ,WAAW,OAAO;AAAA,EAC7B;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;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@greatapps/common",
3
- "version": "1.1.455",
3
+ "version": "1.1.457",
4
4
  "description": "Shared library for GreatApps frontend applications",
5
5
  "main": "./dist/index.mjs",
6
6
  "types": "./src/index.ts",
@@ -82,6 +82,8 @@ interface ImageCropModalProps {
82
82
  aspectRatio: number
83
83
  onConfirm: (image: HTMLImageElement, crop: Crop) => void
84
84
  showCircleGuide?: boolean
85
+ minWidth?: number
86
+ minHeight?: number
85
87
  }
86
88
 
87
89
  export function ImageCropModal({
@@ -91,13 +93,17 @@ export function ImageCropModal({
91
93
  aspectRatio,
92
94
  onConfirm,
93
95
  showCircleGuide,
96
+ minWidth,
97
+ minHeight,
94
98
  }: ImageCropModalProps) {
95
99
  const imgRef = useRef<HTMLImageElement>(null)
96
100
  const containerRef = useRef<HTMLDivElement>(null)
97
101
  const [crop, setCrop] = useState<Crop>()
102
+ const [imageSize, setImageSize] = useState({ width: 0, height: 0 })
98
103
 
99
104
  const onImageLoad = (e: React.SyntheticEvent<HTMLImageElement>) => {
100
105
  const { naturalWidth, naturalHeight } = e.currentTarget
106
+ setImageSize({ width: naturalWidth, height: naturalHeight })
101
107
 
102
108
  const initialCrop = centerCrop(
103
109
  makeAspectCrop(
@@ -113,6 +119,14 @@ export function ImageCropModal({
113
119
  setCrop(initialCrop)
114
120
  }
115
121
 
122
+ // Calcula tamanho mínimo em porcentagem baseado nas dimensões mínimas em pixels
123
+ const minCropWidth = minWidth && imageSize.width > 0
124
+ ? (minWidth / imageSize.width) * 100
125
+ : undefined
126
+ const minCropHeight = minHeight && imageSize.height > 0
127
+ ? (minHeight / imageSize.height) * 100
128
+ : undefined
129
+
116
130
  const handleConfirm = () => {
117
131
  if (imgRef.current && crop) {
118
132
  onConfirm(imgRef.current, crop)
@@ -152,6 +166,8 @@ export function ImageCropModal({
152
166
  aspect={aspectRatio}
153
167
  className={`max-h-[60vh] custom-crop-area ${shouldShowCircle ? 'with-circle' : ''}`}
154
168
  keepSelection
169
+ minWidth={minCropWidth}
170
+ minHeight={minCropHeight}
155
171
  renderSelectionAddon={renderSelectionAddon}
156
172
  >
157
173
  <img
@@ -1,6 +1,5 @@
1
1
  'use server'
2
2
 
3
- import { processImage } from '../services/image-processing.service'
4
3
  import type {
5
4
  ProcessImageActionInput,
6
5
  ProcessImageActionOutput,
@@ -10,6 +9,9 @@ export async function processImageAction(
10
9
  input: ProcessImageActionInput
11
10
  ): Promise<ProcessImageActionOutput> {
12
11
  try {
12
+ // Import dinâmico para evitar erro de fs.createReadStream no edge runtime
13
+ const { processImage } = await import('../services/image-processing.service')
14
+
13
15
  const base64Data = input.base64Image.replace(/^data:image\/\w+;base64,/, '')
14
16
  const binaryString = atob(base64Data)
15
17
  const bytes = new Uint8Array(binaryString.length)
@@ -1,12 +1,10 @@
1
1
  'use client'
2
2
 
3
3
  import { useState, useCallback } from 'react'
4
- import { useMutation } from '@tanstack/react-query'
5
4
  import type { Crop } from 'react-image-crop'
6
5
  import { compressImage } from '../utils/compress-image'
7
6
  import { cropImageToCanvas } from '../utils/crop-image'
8
7
  import { validateImage } from '../utils/validate-image'
9
- import { processImageAction } from '../actions/process-image.action'
10
8
  import type { ImageConfig } from '../types/image.type'
11
9
 
12
10
  export interface UseImageUploadOptions {
@@ -21,21 +19,7 @@ export function useImageUpload(options: UseImageUploadOptions) {
21
19
  const [originalImage, setOriginalImage] = useState<string | null>(null)
22
20
  const [croppedPreview, setCroppedPreview] = useState<string | null>(null)
23
21
  const [showCropModal, setShowCropModal] = useState(false)
24
-
25
- const processMutation = useMutation({
26
- mutationFn: processImageAction,
27
- onSuccess: (result) => {
28
- if (result.success && result.base64Image) {
29
- setCroppedPreview(result.base64Image)
30
- onSuccess?.(result.base64Image)
31
- } else {
32
- onError?.(result.error || 'Erro desconhecido')
33
- }
34
- },
35
- onError: (error) => {
36
- onError?.(error instanceof Error ? error.message : 'Erro ao processar')
37
- },
38
- })
22
+ const [isProcessing, setIsProcessing] = useState(false)
39
23
 
40
24
  const handleFileSelect = useCallback(
41
25
  async (file: File) => {
@@ -62,7 +46,10 @@ export function useImageUpload(options: UseImageUploadOptions) {
62
46
 
63
47
  const handleCropConfirm = useCallback(
64
48
  async (imageElement: HTMLImageElement, cropArea: Crop) => {
49
+ setIsProcessing(true)
65
50
  try {
51
+ // Crop é feito inteiramente no cliente via Canvas
52
+ // Já produz imagem no tamanho final (config.width x config.height)
66
53
  const croppedBase64 = await cropImageToCanvas(
67
54
  imageElement,
68
55
  cropArea,
@@ -72,24 +59,14 @@ export function useImageUpload(options: UseImageUploadOptions) {
72
59
 
73
60
  setCroppedPreview(croppedBase64)
74
61
  setShowCropModal(false)
75
-
76
- processMutation.mutate({
77
- base64Image: croppedBase64,
78
- targetWidth: config.width,
79
- targetHeight: config.height,
80
- cropArea: {
81
- x: cropArea.x,
82
- y: cropArea.y,
83
- width: cropArea.width,
84
- height: cropArea.height,
85
- unit: cropArea.unit,
86
- },
87
- })
62
+ onSuccess?.(croppedBase64)
88
63
  } catch {
89
64
  onError?.('Erro ao aplicar crop')
65
+ } finally {
66
+ setIsProcessing(false)
90
67
  }
91
68
  },
92
- [config, processMutation, onError]
69
+ [config, onSuccess, onError]
93
70
  )
94
71
 
95
72
  const clear = useCallback(() => {
@@ -102,7 +79,7 @@ export function useImageUpload(options: UseImageUploadOptions) {
102
79
  croppedPreview,
103
80
  showCropModal,
104
81
  config,
105
- isProcessing: processMutation.isPending,
82
+ isProcessing,
106
83
  handleFileSelect,
107
84
  handleCropConfirm,
108
85
  setShowCropModal,