├── 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 |
7 | 8 |
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 | Upload 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 | 10 | 15 | 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 | Avatar 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 | --------------------------------------------------------------------------------