diff options
author | ilyayudovin <ilyayudovin123@gmail.com> | 2020-10-19 00:53:19 +0300 |
---|---|---|
committer | ilyayudovin <ilyayudovin123@gmail.com> | 2020-10-19 00:53:19 +0300 |
commit | a40961a2564738ca9f14d9e50e0d1d5c6ab7ec54 (patch) | |
tree | 62bd85a19a874c65f0261e286efb1f305fe79191 | |
parent | cda51156c20c04a20a9fcfe1e0f3aa51f54e9ad2 (diff) | |
download | which-ui-a40961a2564738ca9f14d9e50e0d1d5c6ab7ec54.tar.gz |
feat: Add avatar crop
-rw-r--r-- | src/components/AvatarCrop/AvatarCrop.tsx | 69 | ||||
-rw-r--r-- | src/components/AvatarCrop/canvasUtils.js | 54 | ||||
-rw-r--r-- | src/components/ModalScreen/ModalScreen.tsx | 8 | ||||
-rw-r--r-- | src/containers/Profile/ProfileInfo.tsx | 14 |
4 files changed, 138 insertions, 7 deletions
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<PropTypes> = ({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 (
+ <ModalScreen
+ title="Choose area"
+ actionIcon={<SendIcon />}
+ handleAction={handleLoadCroppedImage}
+ isActionDisabled={false}
+ >
+ <div className={classes.cropContainer}>
+ <Cropper
+ image={avatarToCrop}
+ crop={crop}
+ zoom={zoom}
+ aspect={1}
+ cropShape="round"
+ showGrid={false}
+ onCropChange={setCrop}
+ onCropComplete={onCropComplete}
+ onZoomChange={setZoom}
+ />
+ </div>
+ </ModalScreen>
+ )
+};
+
+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<PropTypes> = ({ 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 ( <Dialog - open={isOpen} + open={true} onClose={handleClose} - onExited={onExited} TransitionComponent={Transition} PaperProps={{ className: classes.root }} fullScreen={isMobile} diff --git a/src/containers/Profile/ProfileInfo.tsx b/src/containers/Profile/ProfileInfo.tsx index da952e9..763a077 100644 --- a/src/containers/Profile/ProfileInfo.tsx +++ b/src/containers/Profile/ProfileInfo.tsx @@ -11,6 +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"; interface PropTypes { savedPolls: number; @@ -107,7 +108,7 @@ const ProfileInfo: React.FC<PropTypes> = ({ const classes = useStyles(); const { user } = useAuth(); const [progress, setProgress] = useState<number>(0); - + const [avatarToCrop, setAvatarToCrop] = useState<string>(''); const dateSince = useMemo(() => formatDate(userInfo?.createdAt), [userInfo]); const handleUpdateAvatar = useCallback(async (file: File) => { @@ -116,12 +117,21 @@ const ProfileInfo: React.FC<PropTypes> = ({ .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 ( <div className={classes.root}> { + avatarToCrop && <AvatarCrop avatarToCrop={avatarToCrop} callback={handleUpdateAvatar}/> + } + { !userInfo ? <Skeleton animation="wave" variant="circle" width={150} height={150} className={classes.avatar} /> : userInfo?._id === user?._id @@ -135,7 +145,7 @@ const ProfileInfo: React.FC<PropTypes> = ({ }} className={classes.avatarContainer} badgeContent={( - <FileUpload callback={handleUpdateAvatar}> + <FileUpload callback={handleCropAvatar}> <div className={classes.badge}> <CameraAlt /> </div> |