@greatapps/common 1.1.460 → 1.1.462
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 +40 -31
- package/dist/components/widgets/ImageUpload/ImageCropModal.mjs.map +1 -1
- package/dist/components/widgets/ImageUpload/ImageTooSmallModal.mjs +26 -19
- 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 +12 -4
- package/src/components/widgets/ImageUpload/ImageTooSmallModal.tsx +6 -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,6 +113,7 @@ function ImageCropModal({
|
|
|
113
113
|
aspectRatio,
|
|
114
114
|
onConfirm,
|
|
115
115
|
onCancel,
|
|
116
|
+
onImageLoad: onImageLoadCallback,
|
|
116
117
|
showCircleGuide,
|
|
117
118
|
minWidth,
|
|
118
119
|
minHeight
|
|
@@ -123,6 +124,7 @@ function ImageCropModal({
|
|
|
123
124
|
const onImageLoad = (e) => {
|
|
124
125
|
const { naturalWidth, naturalHeight } = e.currentTarget;
|
|
125
126
|
setImageSize({ width: naturalWidth, height: naturalHeight });
|
|
127
|
+
onImageLoadCallback?.(naturalWidth, naturalHeight);
|
|
126
128
|
const initialCrop = centerCrop(
|
|
127
129
|
makeAspectCrop(
|
|
128
130
|
{ unit: "%", width: 90 },
|
|
@@ -162,39 +164,46 @@ function ImageCropModal({
|
|
|
162
164
|
] });
|
|
163
165
|
return /* @__PURE__ */ jsxs(Dialog, { open, onOpenChange, children: [
|
|
164
166
|
/* @__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",
|
|
167
|
+
/* @__PURE__ */ jsxs(
|
|
168
|
+
DialogContent,
|
|
169
|
+
{
|
|
170
|
+
className: "w-full md:w-[650px]! lg:w-[650px]! max-w-[650px]! max-[650px]:w-[calc(100%-32px)] p-0 gap-0 z-[1003]!",
|
|
171
|
+
overlayClassName: "z-[1002]!",
|
|
172
|
+
children: [
|
|
173
|
+
/* @__PURE__ */ jsx(DialogHeader, { className: "p-5", children: /* @__PURE__ */ jsx(DialogTitle, { className: "text-center text-[16px]!", children: "Ajustar imagem" }) }),
|
|
174
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-4 px-5 pb-5", children: [
|
|
175
|
+
/* @__PURE__ */ jsx("div", { className: "flex justify-center overflow-hidden rounded-lg", children: /* @__PURE__ */ jsx(
|
|
176
|
+
ReactCrop,
|
|
181
177
|
{
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
178
|
+
crop,
|
|
179
|
+
onChange: (c) => setCrop(c),
|
|
180
|
+
aspect: aspectRatio,
|
|
181
|
+
className: `max-h-[50vh] custom-crop-area ${shouldShowCircle ? "with-circle" : ""}`,
|
|
182
|
+
keepSelection: true,
|
|
183
|
+
minWidth: minCropWidth,
|
|
184
|
+
minHeight: minCropHeight,
|
|
185
|
+
renderSelectionAddon,
|
|
186
|
+
children: /* @__PURE__ */ jsx(
|
|
187
|
+
"img",
|
|
188
|
+
{
|
|
189
|
+
ref: imgRef,
|
|
190
|
+
src: imageSrc,
|
|
191
|
+
alt: "Crop",
|
|
192
|
+
onLoad: onImageLoad,
|
|
193
|
+
className: "max-h-[50vh] object-contain"
|
|
194
|
+
}
|
|
195
|
+
)
|
|
187
196
|
}
|
|
188
|
-
)
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
197
|
+
) }),
|
|
198
|
+
/* @__PURE__ */ jsx("p", { className: "paragraph-small-regular text-zinc-500 text-center", children: "Arraste para ajustar a \xE1rea de corte" })
|
|
199
|
+
] }),
|
|
200
|
+
/* @__PURE__ */ jsxs(DialogFooter, { className: "flex-row! items-center justify-start! gap-3 p-5 border-t border-zinc-200", children: [
|
|
201
|
+
/* @__PURE__ */ jsx(Button, { onClick: handleConfirm, children: "Confirmar" }),
|
|
202
|
+
/* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: handleCancel, children: "Cancelar" })
|
|
203
|
+
] })
|
|
204
|
+
]
|
|
205
|
+
}
|
|
206
|
+
)
|
|
198
207
|
] });
|
|
199
208
|
}
|
|
200
209
|
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}\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}: 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 z-[1003]!\"\n overlayClassName=\"z-[1002]!\"\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":[]}
|
|
@@ -18,25 +18,32 @@ function ImageTooSmallModal({
|
|
|
18
18
|
onCancel
|
|
19
19
|
}) {
|
|
20
20
|
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
|
-
|
|
21
|
+
return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsxs(
|
|
22
|
+
DialogContent,
|
|
23
|
+
{
|
|
24
|
+
className: "w-full md:w-[480px]! lg:w-[480px]! max-w-[480px]! max-[480px]:w-[calc(100%-32px)] p-0 gap-0 z-[1003]!",
|
|
25
|
+
overlayClassName: "z-[1002]!",
|
|
26
|
+
children: [
|
|
27
|
+
/* @__PURE__ */ jsx(DialogHeader, { className: "p-5", children: /* @__PURE__ */ jsxs(DialogTitle, { className: "text-center text-[16px]!", children: [
|
|
28
|
+
"Foto menor que ",
|
|
29
|
+
dimensionText
|
|
30
|
+
] }) }),
|
|
31
|
+
/* @__PURE__ */ jsx("div", { className: "px-5 pb-5", children: /* @__PURE__ */ jsxs("p", { className: "paragraph-small-regular text-zinc-600 text-center", children: [
|
|
32
|
+
'O arquivo "',
|
|
33
|
+
fileName,
|
|
34
|
+
'" \xE9 menor que ',
|
|
35
|
+
dimensionText,
|
|
36
|
+
". Fa\xE7a envio de uma imagem com pelo menos ",
|
|
37
|
+
dimensionText,
|
|
38
|
+
" para continuar."
|
|
39
|
+
] }) }),
|
|
40
|
+
/* @__PURE__ */ jsxs(DialogFooter, { className: "flex-row! items-center justify-start! gap-3 p-5 border-t border-zinc-200", children: [
|
|
41
|
+
/* @__PURE__ */ jsx(Button, { onClick: onSelectAnother, children: "Enviar outra" }),
|
|
42
|
+
/* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: onCancel, children: "Cancelar" })
|
|
43
|
+
] })
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
) });
|
|
40
47
|
}
|
|
41
48
|
export {
|
|
42
49
|
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}\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\n className=\"w-full md:w-[480px]! lg:w-[480px]! max-w-[480px]! max-[480px]:w-[calc(100%-32px)] p-0 gap-0 z-[1003]!\"\n overlayClassName=\"z-[1002]!\"\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":";AAsCQ,cACE,YADF;AApCR;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,cAAc;AAYhB,SAAS,mBAAmB;AAAA,EACjC;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,kBAAiB;AAAA,MAEjB;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,4EACtB;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,6 +117,7 @@ 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
|
|
@@ -129,6 +130,7 @@ export function ImageCropModal({
|
|
|
129
130
|
aspectRatio,
|
|
130
131
|
onConfirm,
|
|
131
132
|
onCancel,
|
|
133
|
+
onImageLoad: onImageLoadCallback,
|
|
132
134
|
showCircleGuide,
|
|
133
135
|
minWidth,
|
|
134
136
|
minHeight,
|
|
@@ -141,6 +143,9 @@ export function ImageCropModal({
|
|
|
141
143
|
const { naturalWidth, naturalHeight } = e.currentTarget
|
|
142
144
|
setImageSize({ width: naturalWidth, height: naturalHeight })
|
|
143
145
|
|
|
146
|
+
// Notifica o hook para verificar dimensões mínimas
|
|
147
|
+
onImageLoadCallback?.(naturalWidth, naturalHeight)
|
|
148
|
+
|
|
144
149
|
const initialCrop = centerCrop(
|
|
145
150
|
makeAspectCrop(
|
|
146
151
|
{ unit: '%', width: 90 },
|
|
@@ -200,7 +205,10 @@ export function ImageCropModal({
|
|
|
200
205
|
return (
|
|
201
206
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
202
207
|
<style>{baseCropStyles}</style>
|
|
203
|
-
<DialogContent
|
|
208
|
+
<DialogContent
|
|
209
|
+
className="w-full md:w-[650px]! lg:w-[650px]! max-w-[650px]! max-[650px]:w-[calc(100%-32px)] p-0 gap-0 z-[1003]!"
|
|
210
|
+
overlayClassName="z-[1002]!"
|
|
211
|
+
>
|
|
204
212
|
<DialogHeader className="p-5">
|
|
205
213
|
<DialogTitle className="text-center text-[16px]!">
|
|
206
214
|
Ajustar imagem
|
|
@@ -208,7 +216,7 @@ export function ImageCropModal({
|
|
|
208
216
|
</DialogHeader>
|
|
209
217
|
|
|
210
218
|
<div className="flex flex-col gap-4 px-5 pb-5">
|
|
211
|
-
<div className="flex justify-center">
|
|
219
|
+
<div className="flex justify-center overflow-hidden rounded-lg">
|
|
212
220
|
<ReactCrop
|
|
213
221
|
crop={crop}
|
|
214
222
|
onChange={(c) => setCrop(c)}
|
|
@@ -234,11 +242,11 @@ export function ImageCropModal({
|
|
|
234
242
|
</p>
|
|
235
243
|
</div>
|
|
236
244
|
|
|
237
|
-
<DialogFooter className="flex-row items-center justify-
|
|
245
|
+
<DialogFooter className="flex-row! items-center justify-start! gap-3 p-5 border-t border-zinc-200">
|
|
246
|
+
<Button onClick={handleConfirm}>Confirmar</Button>
|
|
238
247
|
<Button variant="secondary" onClick={handleCancel}>
|
|
239
248
|
Cancelar
|
|
240
249
|
</Button>
|
|
241
|
-
<Button onClick={handleConfirm}>Confirmar</Button>
|
|
242
250
|
</DialogFooter>
|
|
243
251
|
</DialogContent>
|
|
244
252
|
</Dialog>
|
|
@@ -32,7 +32,10 @@ export function ImageTooSmallModal({
|
|
|
32
32
|
|
|
33
33
|
return (
|
|
34
34
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
35
|
-
<DialogContent
|
|
35
|
+
<DialogContent
|
|
36
|
+
className="w-full md:w-[480px]! lg:w-[480px]! max-w-[480px]! max-[480px]:w-[calc(100%-32px)] p-0 gap-0 z-[1003]!"
|
|
37
|
+
overlayClassName="z-[1002]!"
|
|
38
|
+
>
|
|
36
39
|
<DialogHeader className="p-5">
|
|
37
40
|
<DialogTitle className="text-center text-[16px]!">
|
|
38
41
|
Foto menor que {dimensionText}
|
|
@@ -46,11 +49,11 @@ export function ImageTooSmallModal({
|
|
|
46
49
|
</p>
|
|
47
50
|
</div>
|
|
48
51
|
|
|
49
|
-
<DialogFooter className="flex-row items-center justify-
|
|
52
|
+
<DialogFooter className="flex-row! items-center justify-start! gap-3 p-5 border-t border-zinc-200">
|
|
53
|
+
<Button onClick={onSelectAnother}>Enviar outra</Button>
|
|
50
54
|
<Button variant="secondary" onClick={onCancel}>
|
|
51
55
|
Cancelar
|
|
52
56
|
</Button>
|
|
53
|
-
<Button onClick={onSelectAnother}>Enviar outra</Button>
|
|
54
57
|
</DialogFooter>
|
|
55
58
|
</DialogContent>
|
|
56
59
|
</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,
|