Skip to content

Commit 03e0536

Browse files
committed
cat identifier: feat crop image
1 parent 09d0ff0 commit 03e0536

File tree

14 files changed

+277
-93
lines changed

14 files changed

+277
-93
lines changed

digit-recognition/frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"@tanstack/react-query": "^5.59.20",
1717
"react": "^18.3.1",
1818
"react-dom": "^18.3.1",
19+
"react-easy-crop": "^5.4.1",
1920
"wagmi": "^2.12.29"
2021
},
2122
"devDependencies": {

digit-recognition/frontend/src/components/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@ export { Layout } from "./layout/Layout";
33
export { WalletButton } from "./wallet/WalletButton";
44
export { Button } from "./ui/button/Button";
55
export { Card } from "./ui/card/Card";
6+
export { Modal } from "./ui/modal";
7+
export { ImageCrop } from "./ui/image-crop";

digit-recognition/frontend/src/components/ui/button/Button.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ const Button: React.FC<ButtonProps> = ({
3838
onClick={onClick}
3939
disabled={isDisabled}
4040
aria-disabled={isDisabled}
41+
type="button"
4142
>
4243
{isLoading ? (
4344
<LoadingIcon className={styles["animate-spin"]} />
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
.crop-modal {
2+
display: flex;
3+
flex-direction: column;
4+
align-items: center;
5+
padding: 16px;
6+
}
7+
8+
.cropContainer {
9+
position: relative;
10+
width: 100%;
11+
height: 300px;;
12+
background: #000;
13+
margin-bottom: 16px;
14+
}
15+
16+
.cropControls {
17+
width: 100%;
18+
display: flex;
19+
flex-direction: column;
20+
align-items: center;
21+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import React, { useState, useCallback } from "react";
2+
import Cropper, { Area } from "react-easy-crop";
3+
4+
import { getCroppedImg } from "@/lib/utils";
5+
6+
import styles from "./image-crop.module.scss";
7+
import { Button } from "../button/Button";
8+
9+
type Props = {
10+
image: string;
11+
onClose: () => void;
12+
onCropComplete: (croppedImage: Blob) => void;
13+
};
14+
15+
const ImageCrop: React.FC<Props> = ({ image, onClose, onCropComplete }) => {
16+
const [crop, setCrop] = useState({ x: 0, y: 0 });
17+
const [zoom, setZoom] = useState(1);
18+
const [croppedAreaPixels, setCroppedAreaPixels] = useState<Area | null>(null);
19+
20+
const onCropDone = async () => {
21+
if (!croppedAreaPixels) return;
22+
const croppedImage = await getCroppedImg(image, croppedAreaPixels);
23+
onCropComplete(croppedImage);
24+
onClose();
25+
};
26+
27+
const onCropCompleteHandler = useCallback(
28+
(_croppedArea: Area, croppedPixels: Area) => {
29+
setCroppedAreaPixels(croppedPixels);
30+
},
31+
[]
32+
);
33+
34+
return (
35+
<div className={styles.cropModal}>
36+
<div className={styles.cropContainer}>
37+
<Cropper
38+
image={image}
39+
crop={crop}
40+
zoom={zoom}
41+
aspect={1}
42+
onCropChange={setCrop}
43+
onZoomChange={setZoom}
44+
onCropComplete={onCropCompleteHandler}
45+
46+
/>
47+
</div>
48+
<div className={styles.cropControls}>
49+
<Button variant="outline" onClick={onCropDone}>
50+
CROP
51+
</Button>
52+
</div>
53+
</div>
54+
);
55+
};
56+
57+
export { ImageCrop };
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { ImageCrop } from "./image-crop";
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { Modal } from "./modal";
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
.dialog {
2+
padding: 16px;
3+
width: 438px;
4+
background-color: #000;
5+
border: 1px solid #ffffff80;
6+
color: #fff;
7+
8+
&::backdrop {
9+
backdrop-filter: blur(4px);
10+
background-color: rgba(0, 0, 0, 0.6);
11+
}
12+
}
13+
14+
.header {
15+
display: flex;
16+
justify-content: space-between;
17+
align-items: flex-start;
18+
width: 100%;
19+
margin-bottom: 16px;
20+
21+
h2 {
22+
margin: 0;
23+
font-size: 16px;
24+
}
25+
}

mandelbrot-set/frontend/src/components/ui/Modal.tsx renamed to digit-recognition/frontend/src/components/ui/modal/modal.tsx

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,26 @@
1+
import clsx from "clsx";
12
import { ReactNode, useEffect, useRef, MouseEvent } from "react";
3+
24
import CrossIcon from "@/assets/icons/cross.svg?react";
35
import { Button } from "@/components";
4-
import { cn } from "@/lib/utils";
6+
7+
import styles from "./modal.module.scss";
58

69
type Props = {
710
heading?: string;
811
children: ReactNode;
912
onClose: () => void;
1013
className?: string;
14+
closeOnBackdropClick?: boolean;
1115
};
1216

13-
function Modal({ heading, children, onClose, className }: Props) {
17+
function Modal({
18+
heading,
19+
children,
20+
onClose,
21+
className,
22+
closeOnBackdropClick = false,
23+
}: Props) {
1424
const ref = useRef<HTMLDialogElement>(null);
1525

1626
const disableScroll = () => document.body.classList.add("modal-open");
@@ -36,30 +46,24 @@ function Modal({ heading, children, onClose, className }: Props) {
3646
const handleClick = ({ target }: MouseEvent) => {
3747
const isBackdropClick = target === ref.current;
3848

39-
if (isBackdropClick) onClose();
49+
if (isBackdropClick && closeOnBackdropClick) onClose();
4050
};
4151

4252
return (
43-
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions
4453
<dialog
4554
ref={ref}
4655
onClick={handleClick}
47-
className={cn(
48-
"flex justify-center items-center backdrop:backdrop-filter backdrop:backdrop-blur backdrop:bg-[#00000099]",
49-
className
50-
)}
56+
className={clsx(styles.dialog, className)}
5157
>
52-
<div className="p-4 w-[438px] bg-black border border-muted-foreground text-white">
53-
<header className="flex justify-between items-start mb-4 w-full">
54-
<h2>{heading}</h2>
58+
<header className={styles.header}>
59+
<h2>{heading}</h2>
5560

56-
<Button variant="link" onClick={onClose}>
57-
<CrossIcon />
58-
</Button>
59-
</header>
61+
<Button variant="link" size="icon" onClick={onClose}>
62+
<CrossIcon />
63+
</Button>
64+
</header>
6065

61-
{children}
62-
</div>
66+
{children}
6367
</dialog>
6468
);
6569
}

0 commit comments

Comments
 (0)