├── README.md
├── index.html
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
└── vite.svg
├── src
├── App.jsx
├── assets
│ └── react.svg
├── components
│ ├── CloseIcon.jsx
│ ├── ImageCropper.jsx
│ ├── Modal.jsx
│ ├── PencilIcon.jsx
│ └── Profile.jsx
├── index.css
├── main.jsx
└── setCanvasPreview.js
├── tailwind.config.js
└── vite.config.js
/README.md:
--------------------------------------------------------------------------------
1 | # React + Vite
2 |
3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4 |
5 | Currently, two official plugins are available:
6 |
7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + React
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-image-cropper-code",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "react": "^18.2.0",
14 | "react-dom": "^18.2.0",
15 | "react-image-crop": "^10.1.8"
16 | },
17 | "devDependencies": {
18 | "@types/react": "^18.2.37",
19 | "@types/react-dom": "^18.2.15",
20 | "@vitejs/plugin-react": "^4.2.0",
21 | "autoprefixer": "^10.4.16",
22 | "eslint": "^8.53.0",
23 | "eslint-plugin-react": "^7.33.2",
24 | "eslint-plugin-react-hooks": "^4.6.0",
25 | "eslint-plugin-react-refresh": "^0.4.4",
26 | "postcss": "^8.4.31",
27 | "tailwindcss": "^3.3.5",
28 | "vite": "^5.0.0"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/App.jsx:
--------------------------------------------------------------------------------
1 | import Profile from "./components/Profile";
2 | import "react-image-crop/dist/ReactCrop.css";
3 |
4 | function App() {
5 | return (
6 |
9 | );
10 | }
11 |
12 | export default App;
13 |
--------------------------------------------------------------------------------
/src/assets/react.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/CloseIcon.jsx:
--------------------------------------------------------------------------------
1 | const CloseIcon = () => (
2 |
17 | );
18 | export default CloseIcon;
19 |
--------------------------------------------------------------------------------
/src/components/ImageCropper.jsx:
--------------------------------------------------------------------------------
1 | import { useRef, useState } from "react";
2 | import ReactCrop, {
3 | centerCrop,
4 | convertToPixelCrop,
5 | makeAspectCrop,
6 | } from "react-image-crop";
7 | import setCanvasPreview from "../setCanvasPreview";
8 |
9 | const ASPECT_RATIO = 1;
10 | const MIN_DIMENSION = 150;
11 |
12 | const ImageCropper = ({ closeModal, updateAvatar }) => {
13 | const imgRef = useRef(null);
14 | const previewCanvasRef = useRef(null);
15 | const [imgSrc, setImgSrc] = useState("");
16 | const [crop, setCrop] = useState();
17 | const [error, setError] = useState("");
18 |
19 | const onSelectFile = (e) => {
20 | const file = e.target.files?.[0];
21 | if (!file) return;
22 |
23 | const reader = new FileReader();
24 | reader.addEventListener("load", () => {
25 | const imageElement = new Image();
26 | const imageUrl = reader.result?.toString() || "";
27 | imageElement.src = imageUrl;
28 |
29 | imageElement.addEventListener("load", (e) => {
30 | if (error) setError("");
31 | const { naturalWidth, naturalHeight } = e.currentTarget;
32 | if (naturalWidth < MIN_DIMENSION || naturalHeight < MIN_DIMENSION) {
33 | setError("Image must be at least 150 x 150 pixels.");
34 | return setImgSrc("");
35 | }
36 | });
37 | setImgSrc(imageUrl);
38 | });
39 | reader.readAsDataURL(file);
40 | };
41 |
42 | const onImageLoad = (e) => {
43 | const { width, height } = e.currentTarget;
44 | const cropWidthInPercent = (MIN_DIMENSION / width) * 100;
45 |
46 | const crop = makeAspectCrop(
47 | {
48 | unit: "%",
49 | width: cropWidthInPercent,
50 | },
51 | ASPECT_RATIO,
52 | width,
53 | height
54 | );
55 | const centeredCrop = centerCrop(crop, width, height);
56 | setCrop(centeredCrop);
57 | };
58 |
59 | return (
60 | <>
61 |
70 | {error && {error}
}
71 | {imgSrc && (
72 |
73 |
setCrop(percentCrop)}
76 | circularCrop
77 | keepSelection
78 | aspect={ASPECT_RATIO}
79 | minWidth={MIN_DIMENSION}
80 | >
81 |
88 |
89 |
108 |
109 | )}
110 | {crop && (
111 |
122 | )}
123 | >
124 | );
125 | };
126 | export default ImageCropper;
127 |
--------------------------------------------------------------------------------
/src/components/Modal.jsx:
--------------------------------------------------------------------------------
1 | import CloseIcon from "./CloseIcon";
2 | import ImageCropper from "./ImageCropper";
3 |
4 | const Modal = ({ updateAvatar, closeModal }) => {
5 | return (
6 |
12 |
13 |
14 |
15 |
16 |
17 |
25 |
29 |
30 |
31 |
32 |
33 |
34 | );
35 | };
36 | export default Modal;
37 |
--------------------------------------------------------------------------------
/src/components/PencilIcon.jsx:
--------------------------------------------------------------------------------
1 | const PencilIcon = () => (
2 |
16 | );
17 |
18 | export default PencilIcon;
19 |
--------------------------------------------------------------------------------
/src/components/Profile.jsx:
--------------------------------------------------------------------------------
1 | import { useRef, useState } from "react";
2 | import PencilIcon from "./PencilIcon";
3 | import Modal from "./Modal";
4 |
5 | const Profile = () => {
6 | const avatarUrl = useRef(
7 | "https://avatarfiles.alphacoders.com/161/161002.jpg"
8 | );
9 | const [modalOpen, setModalOpen] = useState(false);
10 |
11 | const updateAvatar = (imgSrc) => {
12 | avatarUrl.current = imgSrc;
13 | };
14 |
15 | return (
16 |
17 |
18 |

23 |
30 |
31 |
Mack Aroney
32 |
Software Engineer
33 | {modalOpen && (
34 |
setModalOpen(false)}
37 | />
38 | )}
39 |
40 | );
41 | };
42 |
43 | export default Profile;
44 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/src/main.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom/client'
3 | import App from './App.jsx'
4 | import './index.css'
5 |
6 | ReactDOM.createRoot(document.getElementById('root')).render(
7 |
8 |
9 | ,
10 | )
11 |
--------------------------------------------------------------------------------
/src/setCanvasPreview.js:
--------------------------------------------------------------------------------
1 | const setCanvasPreview = (
2 | image, // HTMLImageElement
3 | canvas, // HTMLCanvasElement
4 | crop // PixelCrop
5 | ) => {
6 | const ctx = canvas.getContext("2d");
7 | if (!ctx) {
8 | throw new Error("No 2d context");
9 | }
10 |
11 | // devicePixelRatio slightly increases sharpness on retina devices
12 | // at the expense of slightly slower render times and needing to
13 | // size the image back down if you want to download/upload and be
14 | // true to the images natural size.
15 | const pixelRatio = window.devicePixelRatio;
16 | const scaleX = image.naturalWidth / image.width;
17 | const scaleY = image.naturalHeight / image.height;
18 |
19 | canvas.width = Math.floor(crop.width * scaleX * pixelRatio);
20 | canvas.height = Math.floor(crop.height * scaleY * pixelRatio);
21 |
22 | ctx.scale(pixelRatio, pixelRatio);
23 | ctx.imageSmoothingQuality = "high";
24 | ctx.save();
25 |
26 | const cropX = crop.x * scaleX;
27 | const cropY = crop.y * scaleY;
28 |
29 | // Move the crop origin to the canvas origin (0,0)
30 | ctx.translate(-cropX, -cropY);
31 | ctx.drawImage(
32 | image,
33 | 0,
34 | 0,
35 | image.naturalWidth,
36 | image.naturalHeight,
37 | 0,
38 | 0,
39 | image.naturalWidth,
40 | image.naturalHeight
41 | );
42 |
43 | ctx.restore();
44 | };
45 | export default setCanvasPreview;
46 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
4 | theme: {
5 | extend: {},
6 | },
7 | plugins: [],
8 | };
9 |
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | })
8 |
--------------------------------------------------------------------------------