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 From 5eba5c1ffcdaee9a81e965f0570b00dd26bb38c6 Mon Sep 17 00:00:00 2001 From: ilyayudovin Date: Thu, 29 Oct 2020 21:28:52 +0300 Subject: Wait till modal slide right on close --- src/components/AvatarCrop/AvatarCrop.tsx | 9 +++++++-- src/components/ModalScreen/ModalScreen.tsx | 12 ++++++++---- src/containers/PollCreation/PollCreation.tsx | 8 +++++++- src/containers/Profile/ProfileInfo.tsx | 2 +- 4 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/components/AvatarCrop/AvatarCrop.tsx b/src/components/AvatarCrop/AvatarCrop.tsx index e344edd..ef6d9c8 100644 --- a/src/components/AvatarCrop/AvatarCrop.tsx +++ b/src/components/AvatarCrop/AvatarCrop.tsx @@ -6,8 +6,8 @@ import ModalScreen from "../ModalScreen/ModalScreen"; import { getCroppedImg } from './canvasUtils' interface PropTypes { - location?: any; avatarToCrop: string; + setAvatarToCrop: (src: string) => void; callback: (file: File) => void; } @@ -23,7 +23,7 @@ const useStyles = makeStyles(theme => ({ } })); -const AvatarCrop: React.FC = ({location, avatarToCrop, callback}) => { +const AvatarCrop: React.FC = ({ avatarToCrop, setAvatarToCrop, callback }) => { const classes = useStyles(); const [crop, setCrop] = useState({x: 0, y: 0}); const [zoom, setZoom] = useState(1); @@ -42,12 +42,17 @@ const AvatarCrop: React.FC = ({location, avatarToCrop, callback}) => } }, [avatarToCrop, croppedAreaPixels]); + const handleCloseModal = useCallback( () => { + setAvatarToCrop(''); + },[]); + return ( } handleAction={handleLoadCroppedImage} isActionDisabled={false} + handleCloseModal={handleCloseModal} >
void; isActionDisabled?: boolean; + handleCloseModal: ()=> void; } const useStyles = makeStyles(theme => ({ @@ -42,24 +43,27 @@ const Transition = React.forwardRef(( ref: React.Ref ) => ); -const ModalScreen: React.FC = ({ title, actionIcon, handleAction, isActionDisabled, children }) => { +const ModalScreen: React.FC = ({ title, actionIcon, handleAction, isActionDisabled, handleCloseModal, children }) => { const [isOpen, setIsOpen] = useState(true); const classes = useStyles(); const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down('sm')); const history = useHistory(); - const handleClose = useCallback(() => history.goBack(), [history]); + + const handleClose = useCallback(() => setIsOpen(false), [setIsOpen]); + const onExited = useCallback(handleCloseModal, [history, handleAction]); const handleClickAction = useCallback(async () => { if (handleAction) await handleAction(); - return window.location.pathname.includes('/profile') ? null : handleClose(); + return handleClose(); }, [handleAction, handleClose]); return ( ({ images: { @@ -32,6 +33,7 @@ const useStyles = makeStyles(theme => ({ const PollCreation: React.FC = () => { const [description, setDescription] = useState(''); const classes = useStyles(); + const history = useHistory(); const { enqueueSnackbar } = useSnackbar(); const { user } = useAuth(); const { mutate: updateFeed } = useFeed(); @@ -71,6 +73,9 @@ const PollCreation: React.FC = () => { } }; + const handleClose = useCallback(() => history.goBack(), [history]); + + const isSubmitting = useMemo(() => leftProgress + rightProgress > 0, [leftProgress, rightProgress]); return ( @@ -79,6 +84,7 @@ const PollCreation: React.FC = () => { actionIcon={} handleAction={handleSubmit} isActionDisabled={!(left && right) || isSubmitting} + handleCloseModal={handleClose} > diff --git a/src/containers/Profile/ProfileInfo.tsx b/src/containers/Profile/ProfileInfo.tsx index 763a077..b71e6db 100644 --- a/src/containers/Profile/ProfileInfo.tsx +++ b/src/containers/Profile/ProfileInfo.tsx @@ -129,7 +129,7 @@ const ProfileInfo: React.FC = ({ return (
{ - avatarToCrop && + avatarToCrop && } { !userInfo -- cgit v1.2.3 From e086da62473bf542d972aa53e605926f9af796f5 Mon Sep 17 00:00:00 2001 From: eug-vs Date: Thu, 29 Oct 2020 23:45:19 +0300 Subject: chore: install react-easy-crop --- package-lock.json | 24 ++++++++++++++++++++++++ package.json | 2 ++ 2 files changed, 26 insertions(+) diff --git a/package-lock.json b/package-lock.json index 86992bb..7b9db26 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1714,6 +1714,15 @@ "@types/react": "*" } }, + "@types/react-easy-crop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/react-easy-crop/-/react-easy-crop-2.0.0.tgz", + "integrity": "sha512-dUkoNMW3OrXQBYgozC43d3vYPQD23uJFt3ZmM4vcVrj7UjmIc2WxCnO9Zl5fs6bp7StnCtYb3Kp7h7s5T2Xu4w==", + "dev": true, + "requires": { + "react-easy-crop": "*" + } + }, "@types/react-router": { "version": "5.1.8", "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.8.tgz", @@ -10922,6 +10931,21 @@ "scheduler": "^0.19.1" } }, + "react-easy-crop": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/react-easy-crop/-/react-easy-crop-3.2.2.tgz", + "integrity": "sha512-clDimDXVvxOAhM/jKx5RO6zrk+ODqXj1FI50Fznm/qYlsZlbhS8z1R4yk9yynTYOpt8524inTczywhGVaCGlRA==", + "requires": { + "tslib": "2.0.1" + }, + "dependencies": { + "tslib": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz", + "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==" + } + } + }, "react-error-boundary": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-2.3.1.tgz", diff --git a/package.json b/package.json index 6f0bc12..6a58a9c 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "notistack": "^0.9.17", "react": "^16.13.1", "react-dom": "^16.13.1", + "react-easy-crop": "^3.2.2", "react-error-boundary": "^2.3.1", "react-icons": "^3.10.0", "react-router-dom": "^5.2.0", @@ -49,6 +50,7 @@ "@types/node": "^12.12.44", "@types/react": "^16.9.35", "@types/react-dom": "^16.9.8", + "@types/react-easy-crop": "^2.0.0", "@types/react-router-dom": "^5.1.5", "@types/react-virtualized": "^9.21.10", "@types/yup": "^0.29.6", -- cgit v1.2.3 From 3fef7795681c405322aed6e1c876948ebc2cc932 Mon Sep 17 00:00:00 2001 From: eug-vs Date: Thu, 29 Oct 2020 23:53:13 +0300 Subject: refactor: separate ImageCropAreaSelect component --- src/components/AvatarCrop/AvatarCrop.tsx | 74 ---------------------- src/components/AvatarCrop/canvasUtils.js | 54 ---------------- .../ImageCropAreaSelect/ImageCropAreaSelect.tsx | 55 ++++++++++++++++ src/components/ModalScreen/ModalScreen.tsx | 7 +- src/containers/AvatarCropModal/AvatarCropModal.tsx | 57 +++++++++++++++++ src/containers/AvatarCropModal/canvasUtils.js | 54 ++++++++++++++++ src/containers/Profile/ProfileInfo.tsx | 15 ++--- 7 files changed, 174 insertions(+), 142 deletions(-) delete mode 100644 src/components/AvatarCrop/AvatarCrop.tsx delete mode 100644 src/components/AvatarCrop/canvasUtils.js create mode 100644 src/components/ImageCropAreaSelect/ImageCropAreaSelect.tsx create mode 100644 src/containers/AvatarCropModal/AvatarCropModal.tsx create mode 100644 src/containers/AvatarCropModal/canvasUtils.js diff --git a/src/components/AvatarCrop/AvatarCrop.tsx b/src/components/AvatarCrop/AvatarCrop.tsx deleted file mode 100644 index ef6d9c8..0000000 --- a/src/components/AvatarCrop/AvatarCrop.tsx +++ /dev/null @@ -1,74 +0,0 @@ -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 { - avatarToCrop: string; - setAvatarToCrop: (src: string) => void; - 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 = ({ avatarToCrop, setAvatarToCrop, 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]); - - const handleCloseModal = useCallback( () => { - setAvatarToCrop(''); - },[]); - - return ( - } - handleAction={handleLoadCroppedImage} - isActionDisabled={false} - handleCloseModal={handleCloseModal} - > -
- -
-
- ) -}; - -export default AvatarCrop; diff --git a/src/components/AvatarCrop/canvasUtils.js b/src/components/AvatarCrop/canvasUtils.js deleted file mode 100644 index b01aadc..0000000 --- a/src/components/AvatarCrop/canvasUtils.js +++ /dev/null @@ -1,54 +0,0 @@ -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/ImageCropAreaSelect/ImageCropAreaSelect.tsx b/src/components/ImageCropAreaSelect/ImageCropAreaSelect.tsx new file mode 100644 index 0000000..015dd47 --- /dev/null +++ b/src/components/ImageCropAreaSelect/ImageCropAreaSelect.tsx @@ -0,0 +1,55 @@ +import React, { useCallback, useState } from 'react'; +import Cropper from 'react-easy-crop'; +import { makeStyles } from '@material-ui/core/styles'; + +interface Area { + x: number; + y: number; + width: number; + height: number; +} + +interface PropTypes { + image: string; + setArea: (area: Area) => void; +} + +const useStyles = makeStyles(theme => ({ + root: { + position: 'relative', + width: '100%', + height: '100vh', + background: '#333', + [theme.breakpoints.up('sm')]: { + height: 400, + } + } +})); + +const ImageCropAreaSelect: React.FC = ({ image, setArea }) => { + const classes = useStyles(); + const [crop, setCrop] = useState({ x: 0, y: 0 }); + const [zoom, setZoom] = useState(1); + + const onCropComplete = useCallback((areaPercentage: Area, areaPixels: Area): void => { + setArea(areaPixels); + }, [setArea]); + + return ( +
+ +
+ ) +}; + +export default ImageCropAreaSelect; diff --git a/src/components/ModalScreen/ModalScreen.tsx b/src/components/ModalScreen/ModalScreen.tsx index 61cf44a..b71c2c8 100644 --- a/src/components/ModalScreen/ModalScreen.tsx +++ b/src/components/ModalScreen/ModalScreen.tsx @@ -1,5 +1,4 @@ import React, { useState, useCallback } from 'react'; -import { useHistory } from 'react-router-dom'; import { AppBar, Dialog, @@ -20,7 +19,7 @@ interface PropTypes { actionIcon?: JSX.Element; handleAction?: () => void; isActionDisabled?: boolean; - handleCloseModal: ()=> void; + handleCloseModal?: ()=> void; } const useStyles = makeStyles(theme => ({ @@ -48,11 +47,9 @@ const ModalScreen: React.FC = ({ title, actionIcon, handleAction, isA const classes = useStyles(); const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down('sm')); - const history = useHistory(); - const handleClose = useCallback(() => setIsOpen(false), [setIsOpen]); - const onExited = useCallback(handleCloseModal, [history, handleAction]); + const onExited = useCallback(() => handleCloseModal && handleCloseModal(), [handleCloseModal]); const handleClickAction = useCallback(async () => { if (handleAction) await handleAction(); diff --git a/src/containers/AvatarCropModal/AvatarCropModal.tsx b/src/containers/AvatarCropModal/AvatarCropModal.tsx new file mode 100644 index 0000000..a24d2b4 --- /dev/null +++ b/src/containers/AvatarCropModal/AvatarCropModal.tsx @@ -0,0 +1,57 @@ +import React, { useState } from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import SendIcon from "@material-ui/icons/Send"; +import { getCroppedImg } from './canvasUtils' + +import ImageCropAreaSelect from '../../components/ImageCropAreaSelect/ImageCropAreaSelect'; +import ModalScreen from '../../components/ModalScreen/ModalScreen'; + +interface Area { + x: number; + y: number; + width?: number; + height?: number; +} + +interface PropTypes { + avatar: 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 AvatarCropModal: React.FC = ({ avatar, callback }) => { + const classes = useStyles(); + const [area, setArea] = useState({ x: 0, y: 0 }); + + const handleAction = async () => getCroppedImg(avatar, area) + .then(croppedImage => callback(croppedImage)); + + return ( + } + handleAction={handleAction} + isActionDisabled={false} + > +
+ +
+
+ ) +}; + +export default AvatarCropModal; diff --git a/src/containers/AvatarCropModal/canvasUtils.js b/src/containers/AvatarCropModal/canvasUtils.js new file mode 100644 index 0000000..b01aadc --- /dev/null +++ b/src/containers/AvatarCropModal/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/containers/Profile/ProfileInfo.tsx b/src/containers/Profile/ProfileInfo.tsx index b71e6db..4c9f17e 100644 --- a/src/containers/Profile/ProfileInfo.tsx +++ b/src/containers/Profile/ProfileInfo.tsx @@ -11,7 +11,7 @@ import Avatar from '../../components/Avatar/Avatar'; import { patch } from '../../requests'; import { useAuth } from '../../hooks/useAuth'; import uploadFileToS3 from '../../utils/uploadFileToS3'; -import AvatarCrop from "../../components/AvatarCrop/AvatarCrop"; +import AvatarCropModal from '../AvatarCropModal/AvatarCropModal'; interface PropTypes { savedPolls: number; @@ -112,13 +112,10 @@ const ProfileInfo: React.FC = ({ const dateSince = useMemo(() => formatDate(userInfo?.createdAt), [userInfo]); const handleUpdateAvatar = useCallback(async (file: File) => { - if (user) { - uploadFileToS3(file, 0.8, setProgress) - .then(avatarUrl => patch(`/users/${user._id}`, { avatarUrl })) - .then(response => setUserInfo(response.data)) - .then(() => setProgress(0)); - setAvatarToCrop(''); - } + if (user) uploadFileToS3(file, 0.8, setProgress) + .then(avatarUrl => patch(`/users/${user._id}`, { avatarUrl })) + .then(response => setUserInfo(response.data)) + .then(() => setProgress(0)); }, [user, setUserInfo]); const handleCropAvatar = useCallback( async(file: File) => { @@ -129,7 +126,7 @@ const ProfileInfo: React.FC = ({ return (
{ - avatarToCrop && + avatarToCrop && } { !userInfo -- cgit v1.2.3 From 99b4e4aa53d3ade389fc270f9ba9b02904da93f6 Mon Sep 17 00:00:00 2001 From: ilyayudovin Date: Tue, 17 Nov 2020 15:19:39 +0300 Subject: Make responsive avatar crop component --- .../ImageCropAreaSelect/ImageCropAreaSelect.tsx | 55 ++++++++++++++-------- src/containers/AvatarCropModal/AvatarCropModal.tsx | 6 +-- 2 files changed, 39 insertions(+), 22 deletions(-) diff --git a/src/components/ImageCropAreaSelect/ImageCropAreaSelect.tsx b/src/components/ImageCropAreaSelect/ImageCropAreaSelect.tsx index 015dd47..b4540da 100644 --- a/src/components/ImageCropAreaSelect/ImageCropAreaSelect.tsx +++ b/src/components/ImageCropAreaSelect/ImageCropAreaSelect.tsx @@ -1,6 +1,7 @@ -import React, { useCallback, useState } from 'react'; +import React, {useCallback, useState} from 'react'; import Cropper from 'react-easy-crop'; -import { makeStyles } from '@material-ui/core/styles'; +import {makeStyles} from '@material-ui/core/styles'; +import {Slider} from "@material-ui/core"; interface Area { x: number; @@ -15,39 +16,55 @@ interface PropTypes { } const useStyles = makeStyles(theme => ({ - root: { + cropperArea: { position: 'relative', width: '100%', - height: '100vh', + height: 'calc(100vh - 130px)', background: '#333', [theme.breakpoints.up('sm')]: { height: 400, } + }, + sliderContainer: { + padding: 20, } })); -const ImageCropAreaSelect: React.FC = ({ image, setArea }) => { +const ImageCropAreaSelect: React.FC = ({image, setArea}) => { const classes = useStyles(); - const [crop, setCrop] = useState({ x: 0, y: 0 }); - const [zoom, setZoom] = useState(1); + const [crop, setCrop] = useState({x: 0, y: 0}); + const [zoom, setZoom] = useState(1); const onCropComplete = useCallback((areaPercentage: Area, areaPixels: Area): void => { setArea(areaPixels); }, [setArea]); + return ( -
- +
+
+ +
+
+ setZoom(zoom)} + /> +
) }; diff --git a/src/containers/AvatarCropModal/AvatarCropModal.tsx b/src/containers/AvatarCropModal/AvatarCropModal.tsx index a24d2b4..4decdfb 100644 --- a/src/containers/AvatarCropModal/AvatarCropModal.tsx +++ b/src/containers/AvatarCropModal/AvatarCropModal.tsx @@ -22,10 +22,10 @@ const useStyles = makeStyles(theme => ({ cropContainer: { position: 'relative', width: '100%', - height: '100vh', - background: '#333', + // height: '100vh', + // background: '#333', [theme.breakpoints.up('sm')]: { - height: 400, + // height: 400, }, } })); -- cgit v1.2.3