@greatapps/common 1.1.460 → 1.1.461
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/ui/overlay/Dialog.mjs +2 -1
- package/dist/components/ui/overlay/Dialog.mjs.map +1 -1
- package/dist/components/widgets/ImageUpload/ImageCropModal.mjs +42 -32
- package/dist/components/widgets/ImageUpload/ImageCropModal.mjs.map +1 -1
- package/dist/components/widgets/ImageUpload/ImageTooSmallModal.mjs +28 -20
- package/dist/components/widgets/ImageUpload/ImageTooSmallModal.mjs.map +1 -1
- package/dist/modules/images/hooks/use-image-upload.hook.mjs +35 -41
- package/dist/modules/images/hooks/use-image-upload.hook.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/ui/overlay/Dialog.tsx +3 -1
- package/src/components/widgets/ImageUpload/ImageCropModal.tsx +14 -4
- package/src/components/widgets/ImageUpload/ImageTooSmallModal.tsx +8 -3
- package/src/modules/images/hooks/use-image-upload.hook.ts +43 -54
|
@@ -44,10 +44,11 @@ function DialogContent({
|
|
|
44
44
|
children,
|
|
45
45
|
showCloseButton = true,
|
|
46
46
|
overlayClassName,
|
|
47
|
+
hideOverlay = false,
|
|
47
48
|
...props
|
|
48
49
|
}) {
|
|
49
50
|
return /* @__PURE__ */ jsxs(DialogPortal, { "data-slot": "dialog-portal", children: [
|
|
50
|
-
/* @__PURE__ */ jsx(DialogOverlay, { className: overlayClassName }),
|
|
51
|
+
!hideOverlay && /* @__PURE__ */ jsx(DialogOverlay, { className: overlayClassName }),
|
|
51
52
|
/* @__PURE__ */ jsxs(
|
|
52
53
|
DialogPrimitive.Content,
|
|
53
54
|
{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/components/ui/overlay/Dialog.tsx"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\";\nimport { IconX } from '@tabler/icons-react';\nimport { cn } from \"../../../infra/utils/clsx\";\n\nfunction Dialog({\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Root>) {\n return <DialogPrimitive.Root data-slot=\"dialog\" {...props} />;\n}\n\nfunction DialogTrigger({\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {\n return <DialogPrimitive.Trigger data-slot=\"dialog-trigger\" {...props} />;\n}\n\nfunction DialogPortal({\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Portal>) {\n return <DialogPrimitive.Portal data-slot=\"dialog-portal\" {...props} />;\n}\n\nfunction DialogClose({\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Close>) {\n return <DialogPrimitive.Close data-slot=\"dialog-close\" {...props} />;\n}\n\nfunction DialogOverlay({\n className,\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {\n return (\n <DialogPrimitive.Overlay\n data-slot=\"dialog-overlay\"\n className={cn(\n \"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-[1000] bg-black/50\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction DialogContent({\n className,\n children,\n showCloseButton = true,\n overlayClassName,\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Content> & {\n showCloseButton?: boolean;\n overlayClassName?: string;\n}) {\n return (\n <DialogPortal data-slot=\"dialog-portal\">\n <DialogOverlay className={overlayClassName}
|
|
1
|
+
{"version":3,"sources":["../../../../src/components/ui/overlay/Dialog.tsx"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\";\nimport { IconX } from '@tabler/icons-react';\nimport { cn } from \"../../../infra/utils/clsx\";\n\nfunction Dialog({\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Root>) {\n return <DialogPrimitive.Root data-slot=\"dialog\" {...props} />;\n}\n\nfunction DialogTrigger({\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {\n return <DialogPrimitive.Trigger data-slot=\"dialog-trigger\" {...props} />;\n}\n\nfunction DialogPortal({\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Portal>) {\n return <DialogPrimitive.Portal data-slot=\"dialog-portal\" {...props} />;\n}\n\nfunction DialogClose({\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Close>) {\n return <DialogPrimitive.Close data-slot=\"dialog-close\" {...props} />;\n}\n\nfunction DialogOverlay({\n className,\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {\n return (\n <DialogPrimitive.Overlay\n data-slot=\"dialog-overlay\"\n className={cn(\n \"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-[1000] bg-black/50\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction DialogContent({\n className,\n children,\n showCloseButton = true,\n overlayClassName,\n hideOverlay = false,\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Content> & {\n showCloseButton?: boolean;\n overlayClassName?: string;\n hideOverlay?: boolean;\n}) {\n return (\n <DialogPortal data-slot=\"dialog-portal\">\n {!hideOverlay && <DialogOverlay className={overlayClassName} />}\n <DialogPrimitive.Content\n data-slot=\"dialog-content\"\n className={cn(\n \"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-[1001] grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg\",\n className,\n )}\n {...props}\n >\n {children}\n {showCloseButton && (\n <div className=\"absolute top-0.5 right-0\">\n <div className=\"p-4\">\n <DialogPrimitive.Close\n data-slot=\"dialog-close\"\n className=\"hover:cursor-pointer flex size-8 items-center justify-center rounded-lg p-2 text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-950 focus:outline-none\"\n >\n <IconX className=\"size-[18px]\" />\n <span className=\"sr-only\">Close</span>\n </DialogPrimitive.Close>\n </div>\n </div>\n )}\n </DialogPrimitive.Content>\n </DialogPortal>\n );\n}\n\nfunction DialogHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"dialog-header\"\n className={cn(\n \"p-5 flex flex-col gap-2 text-center sm:text-left\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction DialogFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"dialog-footer\"\n className={cn(\n \"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction DialogTitle({\n className,\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Title>) {\n return (\n <DialogPrimitive.Title\n data-slot=\"dialog-title\"\n className={cn(\"text-lg leading-none font-semibold\", className)}\n {...props}\n />\n );\n}\n\nfunction DialogDescription({\n className,\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Description>) {\n return (\n <DialogPrimitive.Description\n data-slot=\"dialog-description\"\n className={cn(\"text-muted-foreground text-sm\", className)}\n {...props}\n />\n );\n}\n\nexport {\n Dialog,\n DialogClose,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogOverlay,\n DialogPortal,\n DialogTitle,\n DialogTrigger,\n};\n"],"mappings":";AAUS,cAgEK,YAhEL;AAPT,YAAY,qBAAqB;AACjC,SAAS,aAAa;AACtB,SAAS,UAAU;AAEnB,SAAS,OAAO;AAAA,EACd,GAAG;AACL,GAAsD;AACpD,SAAO,oBAAC,gBAAgB,MAAhB,EAAqB,aAAU,UAAU,GAAG,OAAO;AAC7D;AAEA,SAAS,cAAc;AAAA,EACrB,GAAG;AACL,GAAyD;AACvD,SAAO,oBAAC,gBAAgB,SAAhB,EAAwB,aAAU,kBAAkB,GAAG,OAAO;AACxE;AAEA,SAAS,aAAa;AAAA,EACpB,GAAG;AACL,GAAwD;AACtD,SAAO,oBAAC,gBAAgB,QAAhB,EAAuB,aAAU,iBAAiB,GAAG,OAAO;AACtE;AAEA,SAAS,YAAY;AAAA,EACnB,GAAG;AACL,GAAuD;AACrD,SAAO,oBAAC,gBAAgB,OAAhB,EAAsB,aAAU,gBAAgB,GAAG,OAAO;AACpE;AAEA,SAAS,cAAc;AAAA,EACrB;AAAA,EACA,GAAG;AACL,GAAyD;AACvD,SACE;AAAA,IAAC,gBAAgB;AAAA,IAAhB;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAEA,SAAS,cAAc;AAAA,EACrB;AAAA,EACA;AAAA,EACA,kBAAkB;AAAA,EAClB;AAAA,EACA,cAAc;AAAA,EACd,GAAG;AACL,GAIG;AACD,SACE,qBAAC,gBAAa,aAAU,iBACrB;AAAA,KAAC,eAAe,oBAAC,iBAAc,WAAW,kBAAkB;AAAA,IAC7D;AAAA,MAAC,gBAAgB;AAAA,MAAhB;AAAA,QACC,aAAU;AAAA,QACV,WAAW;AAAA,UACT;AAAA,UACA;AAAA,QACF;AAAA,QACC,GAAG;AAAA,QAEH;AAAA;AAAA,UACA,mBACC,oBAAC,SAAI,WAAU,4BACb,8BAAC,SAAI,WAAU,OACb;AAAA,YAAC,gBAAgB;AAAA,YAAhB;AAAA,cACC,aAAU;AAAA,cACV,WAAU;AAAA,cAEV;AAAA,oCAAC,SAAM,WAAU,eAAc;AAAA,gBAC/B,oBAAC,UAAK,WAAU,WAAU,mBAAK;AAAA;AAAA;AAAA,UACjC,GACF,GACF;AAAA;AAAA;AAAA,IAEJ;AAAA,KACF;AAEJ;AAEA,SAAS,aAAa,EAAE,WAAW,GAAG,MAAM,GAAgC;AAC1E,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAEA,SAAS,aAAa,EAAE,WAAW,GAAG,MAAM,GAAgC;AAC1E,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAEA,SAAS,YAAY;AAAA,EACnB;AAAA,EACA,GAAG;AACL,GAAuD;AACrD,SACE;AAAA,IAAC,gBAAgB;AAAA,IAAhB;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,sCAAsC,SAAS;AAAA,MAC5D,GAAG;AAAA;AAAA,EACN;AAEJ;AAEA,SAAS,kBAAkB;AAAA,EACzB;AAAA,EACA,GAAG;AACL,GAA6D;AAC3D,SACE;AAAA,IAAC,gBAAgB;AAAA,IAAhB;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,iCAAiC,SAAS;AAAA,MACvD,GAAG;AAAA;AAAA,EACN;AAEJ;","names":[]}
|
|
@@ -113,9 +113,11 @@ function ImageCropModal({
|
|
|
113
113
|
aspectRatio,
|
|
114
114
|
onConfirm,
|
|
115
115
|
onCancel,
|
|
116
|
+
onImageLoad: onImageLoadCallback,
|
|
116
117
|
showCircleGuide,
|
|
117
118
|
minWidth,
|
|
118
|
-
minHeight
|
|
119
|
+
minHeight,
|
|
120
|
+
hideOverlay
|
|
119
121
|
}) {
|
|
120
122
|
const imgRef = useRef(null);
|
|
121
123
|
const [crop, setCrop] = useState();
|
|
@@ -123,6 +125,7 @@ function ImageCropModal({
|
|
|
123
125
|
const onImageLoad = (e) => {
|
|
124
126
|
const { naturalWidth, naturalHeight } = e.currentTarget;
|
|
125
127
|
setImageSize({ width: naturalWidth, height: naturalHeight });
|
|
128
|
+
onImageLoadCallback?.(naturalWidth, naturalHeight);
|
|
126
129
|
const initialCrop = centerCrop(
|
|
127
130
|
makeAspectCrop(
|
|
128
131
|
{ unit: "%", width: 90 },
|
|
@@ -162,39 +165,46 @@ function ImageCropModal({
|
|
|
162
165
|
] });
|
|
163
166
|
return /* @__PURE__ */ jsxs(Dialog, { open, onOpenChange, children: [
|
|
164
167
|
/* @__PURE__ */ jsx("style", { children: baseCropStyles }),
|
|
165
|
-
/* @__PURE__ */ jsxs(
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
keepSelection: true,
|
|
176
|
-
minWidth: minCropWidth,
|
|
177
|
-
minHeight: minCropHeight,
|
|
178
|
-
renderSelectionAddon,
|
|
179
|
-
children: /* @__PURE__ */ jsx(
|
|
180
|
-
"img",
|
|
168
|
+
/* @__PURE__ */ jsxs(
|
|
169
|
+
DialogContent,
|
|
170
|
+
{
|
|
171
|
+
className: "w-full md:w-[650px]! lg:w-[650px]! max-w-[650px]! max-[650px]:w-[calc(100%-32px)] p-0 gap-0",
|
|
172
|
+
hideOverlay,
|
|
173
|
+
children: [
|
|
174
|
+
/* @__PURE__ */ jsx(DialogHeader, { className: "p-5", children: /* @__PURE__ */ jsx(DialogTitle, { className: "text-center text-[16px]!", children: "Ajustar imagem" }) }),
|
|
175
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-4 px-5 pb-5", children: [
|
|
176
|
+
/* @__PURE__ */ jsx("div", { className: "flex justify-center overflow-hidden rounded-lg", children: /* @__PURE__ */ jsx(
|
|
177
|
+
ReactCrop,
|
|
181
178
|
{
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
179
|
+
crop,
|
|
180
|
+
onChange: (c) => setCrop(c),
|
|
181
|
+
aspect: aspectRatio,
|
|
182
|
+
className: `max-h-[50vh] custom-crop-area ${shouldShowCircle ? "with-circle" : ""}`,
|
|
183
|
+
keepSelection: true,
|
|
184
|
+
minWidth: minCropWidth,
|
|
185
|
+
minHeight: minCropHeight,
|
|
186
|
+
renderSelectionAddon,
|
|
187
|
+
children: /* @__PURE__ */ jsx(
|
|
188
|
+
"img",
|
|
189
|
+
{
|
|
190
|
+
ref: imgRef,
|
|
191
|
+
src: imageSrc,
|
|
192
|
+
alt: "Crop",
|
|
193
|
+
onLoad: onImageLoad,
|
|
194
|
+
className: "max-h-[50vh] object-contain"
|
|
195
|
+
}
|
|
196
|
+
)
|
|
187
197
|
}
|
|
188
|
-
)
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
+
) }),
|
|
199
|
+
/* @__PURE__ */ jsx("p", { className: "paragraph-small-regular text-zinc-500 text-center", children: "Arraste para ajustar a \xE1rea de corte" })
|
|
200
|
+
] }),
|
|
201
|
+
/* @__PURE__ */ jsxs(DialogFooter, { className: "flex-row items-center justify-start gap-3 p-5 border-t border-zinc-200", children: [
|
|
202
|
+
/* @__PURE__ */ jsx(Button, { onClick: handleConfirm, children: "Confirmar" }),
|
|
203
|
+
/* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: handleCancel, children: "Cancelar" })
|
|
204
|
+
] })
|
|
205
|
+
]
|
|
206
|
+
}
|
|
207
|
+
)
|
|
198
208
|
] });
|
|
199
209
|
}
|
|
200
210
|
export {
|
|
@@ -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 /* Bolinhas centrais nas bordas */\n .custom-crop-area .ReactCrop__crop-selection .crop-handle {\n position: absolute;\n width: 10px;\n height: 10px;\n background: white;\n border-radius: 50%;\n pointer-events: none;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);\n }\n\n .custom-crop-area .ReactCrop__crop-selection .crop-handle.top {\n top: -5px;\n left: 50%;\n transform: translateX(-50%);\n }\n\n .custom-crop-area .ReactCrop__crop-selection .crop-handle.bottom {\n bottom: -5px;\n left: 50%;\n transform: translateX(-50%);\n }\n\n .custom-crop-area .ReactCrop__crop-selection .crop-handle.left {\n left: -5px;\n top: 50%;\n transform: translateY(-50%);\n }\n\n .custom-crop-area .ReactCrop__crop-selection .crop-handle.right {\n right: -5px;\n top: 50%;\n transform: translateY(-50%);\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 onCancel?: () => 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 onCancel,\n showCircleGuide,\n minWidth,\n minHeight,\n}: ImageCropModalProps) {\n const imgRef = useRef<HTMLImageElement>(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 ? Math.min((minWidth / imageSize.width) * 100, 100)\n : undefined\n const minCropHeight = minHeight && imageSize.height > 0\n ? Math.min((minHeight / imageSize.height) * 100, 100)\n : undefined\n\n const handleConfirm = () => {\n if (imgRef.current && crop) {\n onConfirm(imgRef.current, crop)\n }\n }\n\n const handleCancel = () => {\n onCancel?.()\n onOpenChange(false)\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, círculo e bolinhas 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 {/* Bolinhas centrais nas bordas */}\n <div className=\"crop-handle top\" />\n <div className=\"crop-handle bottom\" />\n <div className=\"crop-handle left\" />\n <div className=\"crop-handle right\" />\n </>\n )\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <style>{baseCropStyles}</style>\n <DialogContent className=\"w-full md:w-[650px]! lg:w-[650px]! max-w-[650px]! max-[650px]:w-[calc(100%-32px)] p-0 gap-0\">\n <DialogHeader className=\"p-5\">\n <DialogTitle className=\"text-center text-[16px]!\">\n Ajustar imagem\n </DialogTitle>\n </DialogHeader>\n\n <div className=\"flex flex-col gap-4 px-5 pb-5\">\n <div className=\"flex justify-center\">\n <ReactCrop\n crop={crop}\n onChange={(c) => setCrop(c)}\n aspect={aspectRatio}\n className={`max-h-[50vh] 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-[50vh] 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 </div>\n\n <DialogFooter className=\"flex-row items-center justify-end gap-3 p-5 border-t border-zinc-200\">\n <Button variant=\"secondary\" onClick={handleCancel}>\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
|
+
{"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 /* Bolinhas centrais nas bordas */\n .custom-crop-area .ReactCrop__crop-selection .crop-handle {\n position: absolute;\n width: 10px;\n height: 10px;\n background: white;\n border-radius: 50%;\n pointer-events: none;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);\n }\n\n .custom-crop-area .ReactCrop__crop-selection .crop-handle.top {\n top: -5px;\n left: 50%;\n transform: translateX(-50%);\n }\n\n .custom-crop-area .ReactCrop__crop-selection .crop-handle.bottom {\n bottom: -5px;\n left: 50%;\n transform: translateX(-50%);\n }\n\n .custom-crop-area .ReactCrop__crop-selection .crop-handle.left {\n left: -5px;\n top: 50%;\n transform: translateY(-50%);\n }\n\n .custom-crop-area .ReactCrop__crop-selection .crop-handle.right {\n right: -5px;\n top: 50%;\n transform: translateY(-50%);\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 onCancel?: () => void\n onImageLoad?: (naturalWidth: number, naturalHeight: number) => void\n showCircleGuide?: boolean\n minWidth?: number\n minHeight?: number\n hideOverlay?: boolean\n}\n\nexport function ImageCropModal({\n open,\n onOpenChange,\n imageSrc,\n aspectRatio,\n onConfirm,\n onCancel,\n onImageLoad: onImageLoadCallback,\n showCircleGuide,\n minWidth,\n minHeight,\n hideOverlay,\n}: ImageCropModalProps) {\n const imgRef = useRef<HTMLImageElement>(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 // Notifica o hook para verificar dimensões mínimas\n onImageLoadCallback?.(naturalWidth, 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 ? Math.min((minWidth / imageSize.width) * 100, 100)\n : undefined\n const minCropHeight = minHeight && imageSize.height > 0\n ? Math.min((minHeight / imageSize.height) * 100, 100)\n : undefined\n\n const handleConfirm = () => {\n if (imgRef.current && crop) {\n onConfirm(imgRef.current, crop)\n }\n }\n\n const handleCancel = () => {\n onCancel?.()\n onOpenChange(false)\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, círculo e bolinhas 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 {/* Bolinhas centrais nas bordas */}\n <div className=\"crop-handle top\" />\n <div className=\"crop-handle bottom\" />\n <div className=\"crop-handle left\" />\n <div className=\"crop-handle right\" />\n </>\n )\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <style>{baseCropStyles}</style>\n <DialogContent\n className=\"w-full md:w-[650px]! lg:w-[650px]! max-w-[650px]! max-[650px]:w-[calc(100%-32px)] p-0 gap-0\"\n hideOverlay={hideOverlay}\n >\n <DialogHeader className=\"p-5\">\n <DialogTitle className=\"text-center text-[16px]!\">\n Ajustar imagem\n </DialogTitle>\n </DialogHeader>\n\n <div className=\"flex flex-col gap-4 px-5 pb-5\">\n <div className=\"flex justify-center overflow-hidden rounded-lg\">\n <ReactCrop\n crop={crop}\n onChange={(c) => setCrop(c)}\n aspect={aspectRatio}\n className={`max-h-[50vh] 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-[50vh] 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 </div>\n\n <DialogFooter className=\"flex-row items-center justify-start gap-3 p-5 border-t border-zinc-200\">\n <Button onClick={handleConfirm}>Confirmar</Button>\n <Button variant=\"secondary\" onClick={handleCancel}>\n Cancelar\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n )\n}\n"],"mappings":"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","names":[]}
|
|
@@ -15,28 +15,36 @@ function ImageTooSmallModal({
|
|
|
15
15
|
minWidth,
|
|
16
16
|
minHeight,
|
|
17
17
|
onSelectAnother,
|
|
18
|
-
onCancel
|
|
18
|
+
onCancel,
|
|
19
|
+
hideOverlay
|
|
19
20
|
}) {
|
|
20
21
|
const dimensionText = `${minWidth}x${minHeight}px`;
|
|
21
|
-
return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsxs(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
22
|
+
return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsxs(
|
|
23
|
+
DialogContent,
|
|
24
|
+
{
|
|
25
|
+
className: "w-full md:w-[480px]! lg:w-[480px]! max-w-[480px]! max-[480px]:w-[calc(100%-32px)] p-0 gap-0",
|
|
26
|
+
hideOverlay,
|
|
27
|
+
children: [
|
|
28
|
+
/* @__PURE__ */ jsx(DialogHeader, { className: "p-5", children: /* @__PURE__ */ jsxs(DialogTitle, { className: "text-center text-[16px]!", children: [
|
|
29
|
+
"Foto menor que ",
|
|
30
|
+
dimensionText
|
|
31
|
+
] }) }),
|
|
32
|
+
/* @__PURE__ */ jsx("div", { className: "px-5 pb-5", children: /* @__PURE__ */ jsxs("p", { className: "paragraph-small-regular text-zinc-600 text-center", children: [
|
|
33
|
+
'O arquivo "',
|
|
34
|
+
fileName,
|
|
35
|
+
'" \xE9 menor que ',
|
|
36
|
+
dimensionText,
|
|
37
|
+
". Fa\xE7a envio de uma imagem com pelo menos ",
|
|
38
|
+
dimensionText,
|
|
39
|
+
" para continuar."
|
|
40
|
+
] }) }),
|
|
41
|
+
/* @__PURE__ */ jsxs(DialogFooter, { className: "flex-row items-center justify-start gap-3 p-5 border-t border-zinc-200", children: [
|
|
42
|
+
/* @__PURE__ */ jsx(Button, { onClick: onSelectAnother, children: "Enviar outra" }),
|
|
43
|
+
/* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: onCancel, children: "Cancelar" })
|
|
44
|
+
] })
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
) });
|
|
40
48
|
}
|
|
41
49
|
export {
|
|
42
50
|
ImageTooSmallModal
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/components/widgets/ImageUpload/ImageTooSmallModal.tsx"],"sourcesContent":["'use client'\n\nimport {\n Dialog,\n DialogContent,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '../../ui/overlay/Dialog'\nimport { Button } from '../../ui/buttons/Button'\n\ninterface ImageTooSmallModalProps {\n open: boolean\n onOpenChange: (open: boolean) => void\n fileName: string\n minWidth: number\n minHeight: number\n onSelectAnother: () => void\n onCancel: () => void\n}\n\nexport function ImageTooSmallModal({\n open,\n onOpenChange,\n fileName,\n minWidth,\n minHeight,\n onSelectAnother,\n onCancel,\n}: ImageTooSmallModalProps) {\n const dimensionText = `${minWidth}x${minHeight}px`\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent
|
|
1
|
+
{"version":3,"sources":["../../../../src/components/widgets/ImageUpload/ImageTooSmallModal.tsx"],"sourcesContent":["'use client'\n\nimport {\n Dialog,\n DialogContent,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '../../ui/overlay/Dialog'\nimport { Button } from '../../ui/buttons/Button'\n\ninterface ImageTooSmallModalProps {\n open: boolean\n onOpenChange: (open: boolean) => void\n fileName: string\n minWidth: number\n minHeight: number\n onSelectAnother: () => void\n onCancel: () => void\n hideOverlay?: boolean\n}\n\nexport function ImageTooSmallModal({\n open,\n onOpenChange,\n fileName,\n minWidth,\n minHeight,\n onSelectAnother,\n onCancel,\n hideOverlay,\n}: ImageTooSmallModalProps) {\n const dimensionText = `${minWidth}x${minHeight}px`\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent\n className=\"w-full md:w-[480px]! lg:w-[480px]! max-w-[480px]! max-[480px]:w-[calc(100%-32px)] p-0 gap-0\"\n hideOverlay={hideOverlay}\n >\n <DialogHeader className=\"p-5\">\n <DialogTitle className=\"text-center text-[16px]!\">\n Foto menor que {dimensionText}\n </DialogTitle>\n </DialogHeader>\n\n <div className=\"px-5 pb-5\">\n <p className=\"paragraph-small-regular text-zinc-600 text-center\">\n O arquivo \"{fileName}\" é menor que {dimensionText}. Faça envio de\n uma imagem com pelo menos {dimensionText} para continuar.\n </p>\n </div>\n\n <DialogFooter className=\"flex-row items-center justify-start gap-3 p-5 border-t border-zinc-200\">\n <Button onClick={onSelectAnother}>Enviar outra</Button>\n <Button variant=\"secondary\" onClick={onCancel}>\n Cancelar\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n )\n}\n"],"mappings":";AAwCQ,cACE,YADF;AAtCR;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,cAAc;AAahB,SAAS,mBAAmB;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA4B;AAC1B,QAAM,gBAAgB,GAAG,QAAQ,IAAI,SAAS;AAE9C,SACE,oBAAC,UAAO,MAAY,cAClB;AAAA,IAAC;AAAA;AAAA,MACC,WAAU;AAAA,MACV;AAAA,MAEA;AAAA,4BAAC,gBAAa,WAAU,OACtB,+BAAC,eAAY,WAAU,4BAA2B;AAAA;AAAA,UAChC;AAAA,WAClB,GACF;AAAA,QAEA,oBAAC,SAAI,WAAU,aACb,+BAAC,OAAE,WAAU,qDAAoD;AAAA;AAAA,UACnD;AAAA,UAAS;AAAA,UAAe;AAAA,UAAc;AAAA,UACvB;AAAA,UAAc;AAAA,WAC3C,GACF;AAAA,QAEA,qBAAC,gBAAa,WAAU,0EACtB;AAAA,8BAAC,UAAO,SAAS,iBAAiB,0BAAY;AAAA,UAC9C,oBAAC,UAAO,SAAQ,aAAY,SAAS,UAAU,sBAE/C;AAAA,WACF;AAAA;AAAA;AAAA,EACF,GACF;AAEJ;","names":[]}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { useState, useCallback, useRef } from "react";
|
|
3
|
-
import { compressImage } from "../utils/compress-image";
|
|
4
3
|
import { cropImageToCanvas } from "../utils/crop-image";
|
|
5
4
|
import { validateImage } from "../utils/validate-image";
|
|
6
5
|
function useImageUpload(options) {
|
|
@@ -12,55 +11,49 @@ function useImageUpload(options) {
|
|
|
12
11
|
const [tooSmallError, setTooSmallError] = useState(null);
|
|
13
12
|
const [isProcessing, setIsProcessing] = useState(false);
|
|
14
13
|
const fileInputRef = useRef(null);
|
|
15
|
-
const
|
|
14
|
+
const currentFileName = useRef("");
|
|
16
15
|
const handleFileSelect = useCallback(
|
|
17
|
-
|
|
16
|
+
(file) => {
|
|
18
17
|
const validation = validateImage(file);
|
|
19
18
|
if (!validation.valid) {
|
|
20
19
|
onError?.(validation.error || "Arquivo inv\xE1lido");
|
|
21
20
|
return;
|
|
22
21
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
22
|
+
currentFileName.current = file.name;
|
|
23
|
+
const reader = new FileReader();
|
|
24
|
+
reader.onload = (e) => {
|
|
25
|
+
setOriginalImage(e.target?.result);
|
|
26
|
+
setShowCropModal(true);
|
|
27
|
+
};
|
|
28
|
+
reader.readAsDataURL(file);
|
|
29
|
+
},
|
|
30
|
+
[onError]
|
|
31
|
+
);
|
|
32
|
+
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
|
|
31
52
|
});
|
|
32
|
-
|
|
33
|
-
const imgAspect = imageDimensions.width / imageDimensions.height;
|
|
34
|
-
let canFitMinCrop = false;
|
|
35
|
-
if (imgAspect >= aspectRatio) {
|
|
36
|
-
const cropHeight = imageDimensions.height;
|
|
37
|
-
const cropWidth = cropHeight * aspectRatio;
|
|
38
|
-
canFitMinCrop = cropWidth >= minW && cropHeight >= minH;
|
|
39
|
-
} else {
|
|
40
|
-
const cropWidth = imageDimensions.width;
|
|
41
|
-
const cropHeight = cropWidth / aspectRatio;
|
|
42
|
-
canFitMinCrop = cropWidth >= minW && cropHeight >= minH;
|
|
43
|
-
}
|
|
44
|
-
if (!canFitMinCrop) {
|
|
45
|
-
setTooSmallError({
|
|
46
|
-
fileName: file.name,
|
|
47
|
-
minWidth: minW,
|
|
48
|
-
minHeight: minH
|
|
49
|
-
});
|
|
50
|
-
setShowTooSmallModal(true);
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
const reader = new FileReader();
|
|
54
|
-
reader.onload = (e) => {
|
|
55
|
-
setOriginalImage(e.target?.result);
|
|
56
|
-
setShowCropModal(true);
|
|
57
|
-
};
|
|
58
|
-
reader.readAsDataURL(compressed);
|
|
59
|
-
} catch {
|
|
60
|
-
onError?.("Erro ao processar imagem");
|
|
53
|
+
setShowTooSmallModal(true);
|
|
61
54
|
}
|
|
62
55
|
},
|
|
63
|
-
[config
|
|
56
|
+
[config]
|
|
64
57
|
);
|
|
65
58
|
const handleSelectAnotherFile = useCallback(() => {
|
|
66
59
|
setShowTooSmallModal(false);
|
|
@@ -109,6 +102,7 @@ function useImageUpload(options) {
|
|
|
109
102
|
fileInputRef,
|
|
110
103
|
handleFileSelect,
|
|
111
104
|
handleCropConfirm,
|
|
105
|
+
handleImageLoadInModal,
|
|
112
106
|
handleSelectAnotherFile,
|
|
113
107
|
handleCancelTooSmall,
|
|
114
108
|
setShowCropModal,
|
|
@@ -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 {
|
|
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":[]}
|
package/package.json
CHANGED
|
@@ -50,14 +50,16 @@ function DialogContent({
|
|
|
50
50
|
children,
|
|
51
51
|
showCloseButton = true,
|
|
52
52
|
overlayClassName,
|
|
53
|
+
hideOverlay = false,
|
|
53
54
|
...props
|
|
54
55
|
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
|
55
56
|
showCloseButton?: boolean;
|
|
56
57
|
overlayClassName?: string;
|
|
58
|
+
hideOverlay?: boolean;
|
|
57
59
|
}) {
|
|
58
60
|
return (
|
|
59
61
|
<DialogPortal data-slot="dialog-portal">
|
|
60
|
-
<DialogOverlay className={overlayClassName} />
|
|
62
|
+
{!hideOverlay && <DialogOverlay className={overlayClassName} />}
|
|
61
63
|
<DialogPrimitive.Content
|
|
62
64
|
data-slot="dialog-content"
|
|
63
65
|
className={cn(
|
|
@@ -117,9 +117,11 @@ interface ImageCropModalProps {
|
|
|
117
117
|
aspectRatio: number
|
|
118
118
|
onConfirm: (image: HTMLImageElement, crop: Crop) => void
|
|
119
119
|
onCancel?: () => void
|
|
120
|
+
onImageLoad?: (naturalWidth: number, naturalHeight: number) => void
|
|
120
121
|
showCircleGuide?: boolean
|
|
121
122
|
minWidth?: number
|
|
122
123
|
minHeight?: number
|
|
124
|
+
hideOverlay?: boolean
|
|
123
125
|
}
|
|
124
126
|
|
|
125
127
|
export function ImageCropModal({
|
|
@@ -129,9 +131,11 @@ export function ImageCropModal({
|
|
|
129
131
|
aspectRatio,
|
|
130
132
|
onConfirm,
|
|
131
133
|
onCancel,
|
|
134
|
+
onImageLoad: onImageLoadCallback,
|
|
132
135
|
showCircleGuide,
|
|
133
136
|
minWidth,
|
|
134
137
|
minHeight,
|
|
138
|
+
hideOverlay,
|
|
135
139
|
}: ImageCropModalProps) {
|
|
136
140
|
const imgRef = useRef<HTMLImageElement>(null)
|
|
137
141
|
const [crop, setCrop] = useState<Crop>()
|
|
@@ -141,6 +145,9 @@ export function ImageCropModal({
|
|
|
141
145
|
const { naturalWidth, naturalHeight } = e.currentTarget
|
|
142
146
|
setImageSize({ width: naturalWidth, height: naturalHeight })
|
|
143
147
|
|
|
148
|
+
// Notifica o hook para verificar dimensões mínimas
|
|
149
|
+
onImageLoadCallback?.(naturalWidth, naturalHeight)
|
|
150
|
+
|
|
144
151
|
const initialCrop = centerCrop(
|
|
145
152
|
makeAspectCrop(
|
|
146
153
|
{ unit: '%', width: 90 },
|
|
@@ -200,7 +207,10 @@ export function ImageCropModal({
|
|
|
200
207
|
return (
|
|
201
208
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
202
209
|
<style>{baseCropStyles}</style>
|
|
203
|
-
<DialogContent
|
|
210
|
+
<DialogContent
|
|
211
|
+
className="w-full md:w-[650px]! lg:w-[650px]! max-w-[650px]! max-[650px]:w-[calc(100%-32px)] p-0 gap-0"
|
|
212
|
+
hideOverlay={hideOverlay}
|
|
213
|
+
>
|
|
204
214
|
<DialogHeader className="p-5">
|
|
205
215
|
<DialogTitle className="text-center text-[16px]!">
|
|
206
216
|
Ajustar imagem
|
|
@@ -208,7 +218,7 @@ export function ImageCropModal({
|
|
|
208
218
|
</DialogHeader>
|
|
209
219
|
|
|
210
220
|
<div className="flex flex-col gap-4 px-5 pb-5">
|
|
211
|
-
<div className="flex justify-center">
|
|
221
|
+
<div className="flex justify-center overflow-hidden rounded-lg">
|
|
212
222
|
<ReactCrop
|
|
213
223
|
crop={crop}
|
|
214
224
|
onChange={(c) => setCrop(c)}
|
|
@@ -234,11 +244,11 @@ export function ImageCropModal({
|
|
|
234
244
|
</p>
|
|
235
245
|
</div>
|
|
236
246
|
|
|
237
|
-
<DialogFooter className="flex-row items-center justify-
|
|
247
|
+
<DialogFooter className="flex-row items-center justify-start gap-3 p-5 border-t border-zinc-200">
|
|
248
|
+
<Button onClick={handleConfirm}>Confirmar</Button>
|
|
238
249
|
<Button variant="secondary" onClick={handleCancel}>
|
|
239
250
|
Cancelar
|
|
240
251
|
</Button>
|
|
241
|
-
<Button onClick={handleConfirm}>Confirmar</Button>
|
|
242
252
|
</DialogFooter>
|
|
243
253
|
</DialogContent>
|
|
244
254
|
</Dialog>
|
|
@@ -17,6 +17,7 @@ interface ImageTooSmallModalProps {
|
|
|
17
17
|
minHeight: number
|
|
18
18
|
onSelectAnother: () => void
|
|
19
19
|
onCancel: () => void
|
|
20
|
+
hideOverlay?: boolean
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
export function ImageTooSmallModal({
|
|
@@ -27,12 +28,16 @@ export function ImageTooSmallModal({
|
|
|
27
28
|
minHeight,
|
|
28
29
|
onSelectAnother,
|
|
29
30
|
onCancel,
|
|
31
|
+
hideOverlay,
|
|
30
32
|
}: ImageTooSmallModalProps) {
|
|
31
33
|
const dimensionText = `${minWidth}x${minHeight}px`
|
|
32
34
|
|
|
33
35
|
return (
|
|
34
36
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
35
|
-
<DialogContent
|
|
37
|
+
<DialogContent
|
|
38
|
+
className="w-full md:w-[480px]! lg:w-[480px]! max-w-[480px]! max-[480px]:w-[calc(100%-32px)] p-0 gap-0"
|
|
39
|
+
hideOverlay={hideOverlay}
|
|
40
|
+
>
|
|
36
41
|
<DialogHeader className="p-5">
|
|
37
42
|
<DialogTitle className="text-center text-[16px]!">
|
|
38
43
|
Foto menor que {dimensionText}
|
|
@@ -46,11 +51,11 @@ export function ImageTooSmallModal({
|
|
|
46
51
|
</p>
|
|
47
52
|
</div>
|
|
48
53
|
|
|
49
|
-
<DialogFooter className="flex-row items-center justify-
|
|
54
|
+
<DialogFooter className="flex-row items-center justify-start gap-3 p-5 border-t border-zinc-200">
|
|
55
|
+
<Button onClick={onSelectAnother}>Enviar outra</Button>
|
|
50
56
|
<Button variant="secondary" onClick={onCancel}>
|
|
51
57
|
Cancelar
|
|
52
58
|
</Button>
|
|
53
|
-
<Button onClick={onSelectAnother}>Enviar outra</Button>
|
|
54
59
|
</DialogFooter>
|
|
55
60
|
</DialogContent>
|
|
56
61
|
</Dialog>
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useCallback, useRef } from 'react'
|
|
4
4
|
import type { Crop } from 'react-image-crop'
|
|
5
|
-
import { compressImage } from '../utils/compress-image'
|
|
6
5
|
import { cropImageToCanvas } from '../utils/crop-image'
|
|
7
6
|
import { validateImage } from '../utils/validate-image'
|
|
8
7
|
import type { ImageConfig } from '../types/image.type'
|
|
@@ -29,70 +28,59 @@ export function useImageUpload(options: UseImageUploadOptions) {
|
|
|
29
28
|
const [tooSmallError, setTooSmallError] = useState<ImageTooSmallError | null>(null)
|
|
30
29
|
const [isProcessing, setIsProcessing] = useState(false)
|
|
31
30
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
32
|
-
const
|
|
31
|
+
const currentFileName = useRef<string>('')
|
|
33
32
|
|
|
33
|
+
// Abre modal IMEDIATAMENTE sem processar - muito mais rápido
|
|
34
34
|
const handleFileSelect = useCallback(
|
|
35
|
-
|
|
35
|
+
(file: File) => {
|
|
36
36
|
const validation = validateImage(file)
|
|
37
37
|
if (!validation.valid) {
|
|
38
38
|
onError?.(validation.error || 'Arquivo inválido')
|
|
39
39
|
return
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
const compressed = await compressImage(file)
|
|
44
|
-
lastCompressedFile.current = compressed
|
|
45
|
-
|
|
46
|
-
// Verificar dimensões da imagem antes de abrir modal
|
|
47
|
-
const imageDimensions = await new Promise<{
|
|
48
|
-
width: number
|
|
49
|
-
height: number
|
|
50
|
-
}>((resolve, reject) => {
|
|
51
|
-
const img = new Image()
|
|
52
|
-
img.onload = () =>
|
|
53
|
-
resolve({ width: img.naturalWidth, height: img.naturalHeight })
|
|
54
|
-
img.onerror = reject
|
|
55
|
-
img.src = URL.createObjectURL(compressed)
|
|
56
|
-
})
|
|
42
|
+
currentFileName.current = file.name
|
|
57
43
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
44
|
+
// Converte para base64 e abre modal instantaneamente
|
|
45
|
+
const reader = new FileReader()
|
|
46
|
+
reader.onload = (e) => {
|
|
47
|
+
setOriginalImage(e.target?.result as string)
|
|
48
|
+
setShowCropModal(true)
|
|
49
|
+
}
|
|
50
|
+
reader.readAsDataURL(file)
|
|
51
|
+
},
|
|
52
|
+
[onError]
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
// Chamado pelo ImageCropModal quando a imagem carrega - verifica dimensões
|
|
56
|
+
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)
|
|
93
81
|
}
|
|
94
82
|
},
|
|
95
|
-
[config
|
|
83
|
+
[config]
|
|
96
84
|
)
|
|
97
85
|
|
|
98
86
|
const handleSelectAnotherFile = useCallback(() => {
|
|
@@ -149,6 +137,7 @@ export function useImageUpload(options: UseImageUploadOptions) {
|
|
|
149
137
|
fileInputRef,
|
|
150
138
|
handleFileSelect,
|
|
151
139
|
handleCropConfirm,
|
|
140
|
+
handleImageLoadInModal,
|
|
152
141
|
handleSelectAnotherFile,
|
|
153
142
|
handleCancelTooSmall,
|
|
154
143
|
setShowCropModal,
|