Skip to content

Commit fea87ed

Browse files
committed
feat(image-crop): react crop component
1 parent aa4034a commit fea87ed

File tree

6 files changed

+363
-21
lines changed

6 files changed

+363
-21
lines changed

Diff for: package.json

+2
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@
2323
"postcss": "8.4.29",
2424
"react": "18.2.0",
2525
"react-dom": "18.2.0",
26+
"react-easy-crop": "^5.0.2",
2627
"react-hook-form": "^7.46.1",
28+
"react-image-crop": "^10.1.8",
2729
"react-input-mask": "3.0.0-alpha.2",
2830
"tailwindcss": "3.3.3",
2931
"typescript": "5.2.2"

Diff for: pnpm-lock.yaml

+34
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: src/components/ImageCrop.tsx

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { TypeCrop } from "@/hooks/useCrop";
2+
import Cropper from "react-easy-crop";
3+
4+
type ImageCropProps = {
5+
previewImagem: string
6+
} & TypeCrop
7+
function ImageCrop({
8+
previewImagem,
9+
crop,
10+
onCropComplete,
11+
setCrop,
12+
setZoom,
13+
zoom,
14+
}: ImageCropProps) {
15+
return (
16+
<div className="relative h-[300px] w-full rounded-sm overflow-hidden">
17+
<Cropper
18+
image={previewImagem}
19+
crop={crop}
20+
zoom={zoom}
21+
showGrid={false}
22+
aspect={1}
23+
onCropChange={setCrop}
24+
onCropComplete={onCropComplete}
25+
onZoomChange={setZoom}
26+
/>
27+
</div>
28+
);
29+
}
30+
31+
export default ImageCrop;

Diff for: src/components/UploadSticker.tsx

+156-21
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,43 @@
1-
import { ImageIcon } from "lucide-react";
1+
import {
2+
CrossIcon,
3+
FileImage,
4+
ImageIcon,
5+
RotateCcw,
6+
RotateCw,
7+
} from "lucide-react";
28
import Image from "next/image";
39
import { useCallback, useEffect, useMemo, useState } from "react";
410
import { useAuth } from "./AuthContext";
511
import { environment } from "@/utils/environment";
612
import { parseCookies } from "nookies";
713
import { event } from "@/utils/gtag";
14+
import { Controller, useForm } from "react-hook-form";
15+
import * as Dialog from "@radix-ui/react-dialog";
16+
import ImageCrop from "./ImageCrop";
17+
import { useCrop } from "@/hooks/useCrop";
818

919
export const UploadSticker = () => {
1020
const { isLogged, user, openLogin } = useAuth();
11-
const [sticker, setSticker] = useState<File | null>(null);
21+
const [sticker, setSticker] = useState<File | Blob | null>(null);
1222
const [isLoading, setIsLoading] = useState(false);
1323
const [isSubmitting, setIsSubmitting] = useState(false);
14-
24+
const [dialogSubmit, setDialogSubmit] = useState(false);
1525
const previewImagem = useMemo(
1626
() => (sticker ? URL.createObjectURL(sticker) : null),
1727
[sticker]
1828
);
29+
const crop = useCrop();
30+
31+
const {
32+
control,
33+
formState: { isValid },
34+
handleSubmit,
35+
reset
36+
} = useForm({
37+
defaultValues: {
38+
name: "",
39+
},
40+
});
1941

2042
function handleFileSelected(event: React.ChangeEvent<HTMLInputElement>) {
2143
const { files } = event.target;
@@ -33,7 +55,16 @@ export const UploadSticker = () => {
3355
const generateFormData = useCallback(async () => {
3456
try {
3557
const body = new FormData();
36-
body.append("file", sticker!);
58+
if (!sticker) {
59+
alert("Deu erro ao editar sua imagem.");
60+
return;
61+
}
62+
body.append("file", sticker);
63+
body.append("y", String(crop.croppedAreaPixels?.y));
64+
body.append("x", String(crop.croppedAreaPixels?.x));
65+
body.append("width", String(crop.croppedAreaPixels?.width));
66+
body.append("height", String(crop.croppedAreaPixels?.height));
67+
body.append("name", "bruno");
3768

3869
const cookies = parseCookies();
3970
const token = cookies.phone_token;
@@ -56,10 +87,12 @@ export const UploadSticker = () => {
5687
setIsLoading(false);
5788
setIsSubmitting(false);
5889
setSticker(null);
90+
crop.reset()
91+
reset()
5992
}
60-
}, [sticker]);
93+
}, [crop, reset, sticker]);
6194

62-
const handleSubmit = async () => {
95+
const onSubmit = async () => {
6396
try {
6497
setIsLoading(true);
6598
if (!isLogged || !user?.isAuthenticated) {
@@ -72,6 +105,8 @@ export const UploadSticker = () => {
72105
console.log(error);
73106
} finally {
74107
setIsLoading(false);
108+
crop.reset()
109+
reset()
75110
}
76111
};
77112

@@ -94,6 +129,19 @@ export const UploadSticker = () => {
94129
}
95130
}
96131
};
132+
133+
const handleDialogSubmit = (open: boolean) => {
134+
if (open) {
135+
setDialogSubmit(true)
136+
} else {
137+
setDialogSubmit(false)
138+
setSticker(null)
139+
crop.reset()
140+
reset()
141+
}
142+
}
143+
144+
97145
useEffect(() => {
98146
window.addEventListener("paste", handlePaste);
99147
return () => {
@@ -167,22 +215,109 @@ export const UploadSticker = () => {
167215
>
168216
Remover
169217
</button>
170-
<button
171-
aria-label="Gerar figurinha para receber pelo Whatsapp"
172-
className="rounded bg-indigo-600 px-2.5 py-1 text-sm font-semibold text-white shadow-sm ring-1 ring-inset ring-indigo-300 hover:bg-indigo-500 cursor-pointer disabled:cursor-not-allowed disabled:opacity-30"
173-
onClick={() => {
174-
event({
175-
action: "upload_image",
176-
label: "envio de imagem",
177-
category: "upload",
178-
value: 1,
179-
});
180-
return handleSubmit();
181-
}}
182-
disabled={!sticker || isLoading}
218+
219+
<Dialog.Root
220+
open={dialogSubmit && !!previewImagem}
221+
onOpenChange={handleDialogSubmit}
183222
>
184-
{isLoading ? "Gerando e enviando ..." : "Gerar figurinha 🪄"}
185-
</button>
223+
<Dialog.Trigger asChild>
224+
<button
225+
type="button"
226+
aria-label="Gerar figurinha para receber pelo Whatsapp"
227+
className="rounded bg-indigo-600 px-2.5 py-1 text-sm font-semibold text-white shadow-sm ring-1 ring-inset ring-indigo-300 hover:bg-indigo-500 cursor-pointer disabled:cursor-not-allowed disabled:opacity-30"
228+
onClick={() => {
229+
event({
230+
action: "upload_image",
231+
label: "envio de imagem",
232+
category: "upload",
233+
value: 1,
234+
});
235+
}}
236+
disabled={!sticker || isLoading}
237+
>
238+
{isLoading ? "Gerando e enviando ..." : "Gerar figurinha 🪄"}
239+
</button>
240+
</Dialog.Trigger>
241+
242+
<Dialog.Portal>
243+
<Dialog.Overlay className="bg-zinc-700/90 data-[state=open]:animate-overlayShow fixed inset-0" />
244+
<Dialog.Close asChild>
245+
<button
246+
className="text-violet11 hover:bg-violet4 focus:shadow-violet7 absolute top-[10px] right-[10px] inline-flex h-[25px] w-[25px] appearance-none items-center justify-center rounded-full focus:shadow-[0_0_0_2px] focus:outline-none"
247+
aria-label="Close"
248+
>
249+
<CrossIcon />
250+
</button>
251+
</Dialog.Close>
252+
<Dialog.Content
253+
className="data-[state=open]:animate-contentShow fixed top-[50%] left-[50%] max-h-[85vh] w-[90vw] max-w-[900px] translate-x-[-50%] translate-y-[-50%] rounded-[6px] bg-white p-[25px] shadow-[hsl(206_22%_7%_/_35%)_0px_10px_38px_-10px,_hsl(206_22%_7%_/_20%)_0px_10px_20px_-15px] focus:outline-none grid grid-cols-2 gap-2"
254+
asChild
255+
>
256+
{previewImagem && (
257+
<form onSubmit={handleSubmit(onSubmit)} noValidate>
258+
<ImageCrop previewImagem={previewImagem} {...crop} />
259+
<div>
260+
<div className="flex items-center gap-4">
261+
<span>Scale</span>
262+
<FileImage />
263+
<input
264+
type="range"
265+
min={1}
266+
step={0.1}
267+
max={3}
268+
onChange={(e) => crop.setZoom(Number(e.target.value))}
269+
value={crop.zoom}
270+
/>
271+
<FileImage size="30" />
272+
</div>
273+
<Controller
274+
control={control}
275+
name="name"
276+
rules={{
277+
required: false,
278+
min: 3,
279+
}}
280+
render={(props) => (
281+
<div>
282+
<label
283+
htmlFor={props.field.name}
284+
className="block text-sm font-medium leading-6 text-gray-900"
285+
>
286+
Nome do sticker
287+
</label>
288+
<div className="relative mt-2 rounded-md shadow-sm">
289+
<input
290+
type="text"
291+
id={props.field.name}
292+
className="block w-full ring-1 ring-inset ring-gray-300 border-0 rounded-md py-1.5 pr-10 sm:text-sm sm:leading-6"
293+
placeholder="Seu nome"
294+
{...props.field}
295+
/>
296+
</div>
297+
</div>
298+
)}
299+
/>
300+
<button
301+
type="submit"
302+
className="w-full rounded bg-white px-2.5 py-1 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 disabled:opacity-30 disabled:cursor-not-allowed"
303+
onClick={() => {
304+
event({
305+
action: "upload_image",
306+
label: "envio de imagem",
307+
category: "upload",
308+
value: 1,
309+
});
310+
}}
311+
disabled={!isValid || isSubmitting}
312+
>
313+
{isSubmitting ? "Enviando ..." : "Enviar"}
314+
</button>
315+
</div>
316+
</form>
317+
)}
318+
</Dialog.Content>
319+
</Dialog.Portal>
320+
</Dialog.Root>
186321
</div>
187322
</div>
188323
</div>

0 commit comments

Comments
 (0)