From e3de0a43b47f97797c450b890d3d10ed5cb52594 Mon Sep 17 00:00:00 2001 From: Roland Osborne Date: Wed, 17 Aug 2022 14:57:08 -0700 Subject: [PATCH] added profile image editing --- net/web/src/session/profile/Profile.jsx | 54 +++++++++++++++-- net/web/src/session/profile/Profile.styled.js | 52 +++++++++++++--- .../profile/profileImage/ProfileImage.jsx | 22 +++++++ .../profileImage/ProfileImage.styled.js | 11 ++++ .../src/session/profile/useProfile.hook.js | 60 ++++++++++++++++++- 5 files changed, 186 insertions(+), 13 deletions(-) create mode 100644 net/web/src/session/profile/profileImage/ProfileImage.jsx create mode 100644 net/web/src/session/profile/profileImage/ProfileImage.styled.js diff --git a/net/web/src/session/profile/Profile.jsx b/net/web/src/session/profile/Profile.jsx index e64fbc02..e40b54fe 100644 --- a/net/web/src/session/profile/Profile.jsx +++ b/net/web/src/session/profile/Profile.jsx @@ -1,15 +1,40 @@ -import { Checkbox } from 'antd'; -import { ProfileWrapper } from './Profile.styled'; +import { useRef } from 'react'; +import { Modal, Button, Checkbox } from 'antd'; +import { ProfileWrapper, EditImageFooter } from './Profile.styled'; import { useProfile } from './useProfile.hook'; +import { ProfileImage } from './profileImage/ProfileImage'; import { Logo } from 'logo/Logo'; -import { DatabaseOutlined, LockOutlined, RightOutlined, EditOutlined, BookOutlined, EnvironmentOutlined } from '@ant-design/icons'; +import { LogoutOutlined, DatabaseOutlined, LockOutlined, RightOutlined, EditOutlined, BookOutlined, EnvironmentOutlined } from '@ant-design/icons'; export function Profile({ closeProfile }) { const { state, actions } = useProfile(); + const imageFile = useRef(null); + + const selected = (e) => { + var reader = new FileReader(); + reader.onload = () => { + actions.setEditImage(reader.result); + } + reader.readAsDataURL(e.target.files[0]); + } + + const saveImage = async () => { + try { + await actions.setProfileImage(); + actions.clearEditProfileImage(); + } + catch(err) { + console.log(err); + Modal.error({ + title: 'Failed to Save', + content: 'Please try again.', + }); + } + } const Image = ( - )} + + + ); } diff --git a/net/web/src/session/profile/Profile.styled.js b/net/web/src/session/profile/Profile.styled.js index 99af33e2..f2fbc30b 100644 --- a/net/web/src/session/profile/Profile.styled.js +++ b/net/web/src/session/profile/Profile.styled.js @@ -46,20 +46,25 @@ export const ProfileWrapper = styled.div` position: relative; width: 20vw; margin-right: 32px; + cursor: pointer; + + &:hover .edit { + opacity: 1; + } .edit { position: absolute; display: flex; align-items: center; justify-content: center; - border-radius: 4px; + border-radius: 8px; width: 24px; height: 24px; bottom: 0; right: 0; color: ${Colors.link}; - background-color: ${Colors.mask}; - cursor: pointer; + background-color: ${Colors.white}; + opacity: 0.7; } } @@ -118,22 +123,27 @@ export const ProfileWrapper = styled.div` .logo { position: relative; - width: 80%; + width: 60%; margin-bottom: 16px; + cursor: pointer; + + &:hover .edit { + opacity: 1; + } .edit { position: absolute; display: flex; align-items: center; justify-content: center; - border-radius: 4px; + border-radius: 8px; width: 24px; height: 24px; bottom: 0; right: 0; color: ${Colors.link}; - background-color: ${Colors.mask}; - cursor: pointer; + background-color: ${Colors.white}; + opacity: 0.7; } } @@ -180,7 +190,7 @@ export const ProfileWrapper = styled.div` .section { width: 100%; color: ${Colors.grey}; - padding-top: 32px; + padding-top: 24px; font-size: 12px; display: flex; justify-content: center; @@ -209,5 +219,31 @@ export const ProfileWrapper = styled.div` padding-left: 8px; } } + + .logout { + display: flex; + flex-direction: row; + align-items: center; + cursor: pointer; + color: ${Colors.white}; + background-color: ${Colors.primary}; + margin-top: 8px; + padding: 8px; + border-radius: 4px; + + .label { + padding-left: 8px; + } + } } ` +export const EditImageFooter = styled.div` + width: 100%; + display: flex; + + .select { + display: flex; + flex-grow: 1; + } +` + diff --git a/net/web/src/session/profile/profileImage/ProfileImage.jsx b/net/web/src/session/profile/profileImage/ProfileImage.jsx new file mode 100644 index 00000000..eeb78f84 --- /dev/null +++ b/net/web/src/session/profile/profileImage/ProfileImage.jsx @@ -0,0 +1,22 @@ +import avatar from 'images/avatar.png'; +import { useState, useCallback } from 'react'; +import Cropper from 'react-easy-crop' +import { ProfileImageWrapper } from './ProfileImage.styled'; + +export function ProfileImage({ state, actions }) { + + const [crop, setCrop] = useState({ x: 0, y: 0 }) + const [zoom, setZoom] = useState(1) + + const onCropComplete = useCallback((area, crop) => { + actions.setEditImageCrop(crop.width, crop.height, crop.x, crop.y) + }); + + return ( + + + + ) +} + diff --git a/net/web/src/session/profile/profileImage/ProfileImage.styled.js b/net/web/src/session/profile/profileImage/ProfileImage.styled.js new file mode 100644 index 00000000..e59cfce1 --- /dev/null +++ b/net/web/src/session/profile/profileImage/ProfileImage.styled.js @@ -0,0 +1,11 @@ +import styled from 'styled-components'; + +export const ProfileImageWrapper = styled.div` + position: relative; + height: 256px; + width: 100%; + display: flex; + align-items: center; + justify-content: center; +`; + diff --git a/net/web/src/session/profile/useProfile.hook.js b/net/web/src/session/profile/useProfile.hook.js index 5979a719..6e94aa47 100644 --- a/net/web/src/session/profile/useProfile.hook.js +++ b/net/web/src/session/profile/useProfile.hook.js @@ -2,13 +2,19 @@ import { useState, useEffect, useContext } from 'react'; import { ProfileContext } from 'context/ProfileContext'; import { AppContext } from 'context/AppContext'; import { ViewportContext } from 'context/ViewportContext'; +import avatar from 'images/avatar.png'; export function useProfile() { const [state, setState] = useState({ init: false, + editProfileImage: false, + editImage: null, + crop: { w: 0, h: 0, x: 0, y: 0 }, + busy: false, }); + const IMAGE_DIM = 256; const app = useContext(AppContext); const viewport = useContext(ViewportContext); const profile = useContext(ProfileContext); @@ -21,7 +27,8 @@ export function useProfile() { if (profile.state.init) { const { node, name, handle, location, description, image } = profile.state.profile; let url = !image ? null : profile.actions.profileImageUrl(); - updateState({ init: true, name, node, handle, url, location, description }); + let editImage = !image ? avatar : url; + updateState({ init: true, name, node, handle, url, editImage, location, description }); } }, [profile]); @@ -31,6 +38,57 @@ export function useProfile() { const actions = { logout: app.actions.logout, + setEditImage: (value) => { + updateState({ editImage: value }); + }, + setEditProfileImage: () => { + updateState({ editProfileImage: true }); + }, + clearEditProfileImage: () => { + updateState({ editProfileImage: false }); + }, + setEditImageCrop: (w, h, x, y) => { + updateState({ crop: { w, h, x, y }}); + }, + setProfileImage: async () => { +console.log("CHECK1"); + if(!state.busy) { + updateState({ busy: true }); + try { + const processImg = () => { + return new Promise((resolve, reject) => { + let img = new Image(); + img.onload = () => { + var canvas = document.createElement("canvas"); + var context = canvas.getContext('2d'); + canvas.width = IMAGE_DIM; + canvas.height = IMAGE_DIM; + context.imageSmoothingQuality = "medium"; + context.drawImage(img, state.crop.x, state.crop.y, state.crop.w, state.crop.h, + 0, 0, IMAGE_DIM, IMAGE_DIM); + resolve(canvas.toDataURL()); + } + img.onerror = reject; + img.src = state.editImage; + }); + }; + let dataUrl = await processImg(); + let data = dataUrl.split(",")[1]; +console.log("CHECK2"); + await profile.actions.setProfileImage(data); +console.log("CHECK3"); + updateState({ busy: false }); + } + catch (err) { + console.log(err); + updateState({ busy: false }); + throw new Error('failed to save profile image'); + } + } + else { + throw new Error('save in progress'); + } + }, }; return { state, actions };