From a40961a2564738ca9f14d9e50e0d1d5c6ab7ec54 Mon Sep 17 00:00:00 2001 From: ilyayudovin Date: Mon, 19 Oct 2020 00:53:19 +0300 Subject: feat: Add avatar crop --- src/components/AvatarCrop/AvatarCrop.tsx | 69 ++++++++++++++++++++++++++++++ src/components/AvatarCrop/canvasUtils.js | 54 +++++++++++++++++++++++ src/components/ModalScreen/ModalScreen.tsx | 8 ++-- src/containers/Profile/ProfileInfo.tsx | 14 +++++- 4 files changed, 138 insertions(+), 7 deletions(-) create mode 100644 src/components/AvatarCrop/AvatarCrop.tsx create mode 100644 src/components/AvatarCrop/canvasUtils.js diff --git a/src/components/AvatarCrop/AvatarCrop.tsx b/src/components/AvatarCrop/AvatarCrop.tsx new file mode 100644 index 0000000..e344edd --- /dev/null +++ b/src/components/AvatarCrop/AvatarCrop.tsx @@ -0,0 +1,69 @@ +import React, {useCallback, useContext, useState} from 'react'; +import Cropper from 'react-easy-crop'; +import {makeStyles} from '@material-ui/core/styles'; +import SendIcon from "@material-ui/icons/Send"; +import ModalScreen from "../ModalScreen/ModalScreen"; +import { getCroppedImg } from './canvasUtils' + +interface PropTypes { + location?: any; + avatarToCrop: string; + callback: (file: File) => void; +} + +const useStyles = makeStyles(theme => ({ + cropContainer: { + position: 'relative', + width: '100%', + height: '100vh', + background: '#333', + [theme.breakpoints.up('sm')]: { + height: 400, + }, + } +})); + +const AvatarCrop: React.FC = ({location, avatarToCrop, callback}) => { + const classes = useStyles(); + const [crop, setCrop] = useState({x: 0, y: 0}); + const [zoom, setZoom] = useState(1); + const [croppedAreaPixels, setCroppedAreaPixels] = useState(null); + + const onCropComplete = useCallback((croppedArea, croppedAreaPixels) => { + setCroppedAreaPixels(croppedAreaPixels) + }, []); + + const handleLoadCroppedImage = useCallback(async () => { + try { + const croppedImage = await getCroppedImg(avatarToCrop, croppedAreaPixels); + callback(croppedImage); + } catch (e) { + console.error(e) + } + }, [avatarToCrop, croppedAreaPixels]); + + return ( + } + handleAction={handleLoadCroppedImage} + isActionDisabled={false} + > +
+ +
+
+ ) +}; + +export default AvatarCrop; diff --git a/src/components/AvatarCrop/canvasUtils.js b/src/components/AvatarCrop/canvasUtils.js new file mode 100644 index 0000000..b01aadc --- /dev/null +++ b/src/components/AvatarCrop/canvasUtils.js @@ -0,0 +1,54 @@ +const createImage = url => + new Promise((resolve, reject) => { + const image = new Image(); + image.addEventListener('load', () => resolve(image)); + image.addEventListener('error', error => reject(error)); + image.setAttribute('crossOrigin', 'anonymous') ;// needed to avoid cross-origin issues on CodeSandbox + image.src = url + }); + +export async function getCroppedImg(imageSrc, pixelCrop) { + const image = await createImage(imageSrc); + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + + const maxSize = Math.max(image.width, image.height); + const safeArea = 2 * ((maxSize / 2) * Math.sqrt(2)); + + // set each dimensions to double largest dimension to allow for a safe area for the + // image to rotate in without being clipped by canvas context + canvas.width = safeArea; + canvas.height = safeArea; + + // translate canvas context to a central location on image to allow rotating around the center. + ctx.translate(safeArea / 2, safeArea / 2); + ctx.translate(-safeArea / 2, -safeArea / 2); + + // draw rotated image and store data. + ctx.drawImage( + image, + safeArea / 2 - image.width * 0.5, + safeArea / 2 - image.height * 0.5 + ); + const data = ctx.getImageData(0, 0, safeArea, safeArea); + + // set canvas width to final desired crop size - this will clear existing context + canvas.width = pixelCrop.width; + canvas.height = pixelCrop.height; + + // paste generated rotate image with correct offsets for x,y crop values. + ctx.putImageData( + data, + Math.round(0 - safeArea / 2 + image.width * 0.5 - pixelCrop.x), + Math.round(0 - safeArea / 2 + image.height * 0.5 - pixelCrop.y) + ); + + // As Base64 string + // return canvas.toDataURL('image/jpeg'); + + // As a blob + + return new Promise(resolve => { + canvas.toBlob(file => resolve(file)) + }) +} diff --git a/src/components/ModalScreen/ModalScreen.tsx b/src/components/ModalScreen/ModalScreen.tsx index cf76272..0f2c96f 100644 --- a/src/components/ModalScreen/ModalScreen.tsx +++ b/src/components/ModalScreen/ModalScreen.tsx @@ -49,19 +49,17 @@ const ModalScreen: React.FC = ({ title, actionIcon, handleAction, isA const isMobile = useMediaQuery(theme.breakpoints.down('sm')); const history = useHistory(); - const handleClose = useCallback(() => setIsOpen(false), [setIsOpen]); - const onExited = useCallback(() => history.goBack(), [history]); + const handleClose = useCallback(() => history.goBack(), [history]); const handleClickAction = useCallback(async () => { if (handleAction) await handleAction(); - return handleClose(); + return window.location.pathname.includes('/profile') ? null : handleClose(); }, [handleAction, handleClose]); return ( = ({ const classes = useStyles(); const { user } = useAuth(); const [progress, setProgress] = useState(0); - + const [avatarToCrop, setAvatarToCrop] = useState(''); const dateSince = useMemo(() => formatDate(userInfo?.createdAt), [userInfo]); const handleUpdateAvatar = useCallback(async (file: File) => { @@ -116,11 +117,20 @@ const ProfileInfo: React.FC = ({ .then(avatarUrl => patch(`/users/${user._id}`, { avatarUrl })) .then(response => setUserInfo(response.data)) .then(() => setProgress(0)); + setAvatarToCrop(''); } }, [user, setUserInfo]); + const handleCropAvatar = useCallback( async(file: File) => { + const imageSrc = URL.createObjectURL(file); + setAvatarToCrop(imageSrc); + },[]); + return (
+ { + avatarToCrop && + } { !userInfo ? @@ -135,7 +145,7 @@ const ProfileInfo: React.FC = ({ }} className={classes.avatarContainer} badgeContent={( - +
-- cgit v1.2.3