From 37978e65b7cf8dc5fc7add72fa4c990d20afe905 Mon Sep 17 00:00:00 2001 From: Roland Osborne Date: Thu, 18 Aug 2022 00:45:41 -0700 Subject: [PATCH] supporting login change as resuable component --- net/web/src/session/account/Account.jsx | 7 +- net/web/src/session/account/Account.styled.js | 1 + .../session/accountAccess/AccountAccess.jsx | 60 +++++++++ .../accountAccess/AccountAccess.styled.js | 33 +++++ .../accountLogin/AccountLogin.jsx | 24 ++++ .../accountAccess/useAccountAccess.hook.js | 124 ++++++++++++++++++ net/web/src/session/profile/Profile.jsx | 22 +--- .../profile/profileDetails/ProfileDetails.jsx | 6 +- .../src/session/profile/useProfile.hook.js | 28 +--- 9 files changed, 256 insertions(+), 49 deletions(-) create mode 100644 net/web/src/session/accountAccess/AccountAccess.jsx create mode 100644 net/web/src/session/accountAccess/AccountAccess.styled.js create mode 100644 net/web/src/session/accountAccess/accountLogin/AccountLogin.jsx create mode 100644 net/web/src/session/accountAccess/useAccountAccess.hook.js diff --git a/net/web/src/session/account/Account.jsx b/net/web/src/session/account/Account.jsx index c574fbb5..0eab8fd5 100644 --- a/net/web/src/session/account/Account.jsx +++ b/net/web/src/session/account/Account.jsx @@ -2,6 +2,7 @@ import { AccountWrapper } from './Account.styled'; import { DoubleRightOutlined } from '@ant-design/icons'; import { Checkbox } from 'antd'; import { SettingOutlined, LockOutlined } from '@ant-design/icons'; +import { AccountAccess } from '../accountAccess/AccountAccess'; export function Account({ closeAccount, openProfile }) { @@ -14,15 +15,11 @@ export function Account({ closeAccount, openProfile }) {
- Visible in Registry - +
); diff --git a/net/web/src/session/account/Account.styled.js b/net/web/src/session/account/Account.styled.js index 981c59f9..d3be6bff 100644 --- a/net/web/src/session/account/Account.styled.js +++ b/net/web/src/session/account/Account.styled.js @@ -45,6 +45,7 @@ export const AccountWrapper = styled.div` .link { color: ${Colors.primary}; padding-top: 16px; + padding-bottom: 8px; display: flex; flex-direction: row; align-items: center; diff --git a/net/web/src/session/accountAccess/AccountAccess.jsx b/net/web/src/session/accountAccess/AccountAccess.jsx new file mode 100644 index 00000000..38427896 --- /dev/null +++ b/net/web/src/session/accountAccess/AccountAccess.jsx @@ -0,0 +1,60 @@ +import { AccountAccessWrapper, EditFooter } from './AccountAccess.styled'; +import { useAccountAccess } from './useAccountAccess.hook'; +import { AccountLogin } from './accountLogin/AccountLogin'; +import { Button, Modal, Checkbox } from 'antd'; +import { LockOutlined } from '@ant-design/icons'; + +export function AccountAccess() { + + const { state, actions } = useAccountAccess(); + + const saveSearchable = async (e) => { + try { + await actions.setSearchable(e.target.checked); + } + catch (err) { + console.log(err); + Modal.error({ + title: 'Update Registry Failed', + content: 'Please try again.', + }); + } + }; + + const saveLogin = async () => { + try { + await actions.setLogin(); + actions.clearEditLogin(); + } + catch (err) { + console.log(err); + Modal.error({ + title: 'Failed to Save', + comment: 'Please try again.', + }); + } + } + + const editLoginFooter = ( + +
+ + +
+ ); + + return ( + + saveSearchable(e)}>Visible in Registry + + + + + + ); +} + diff --git a/net/web/src/session/accountAccess/AccountAccess.styled.js b/net/web/src/session/accountAccess/AccountAccess.styled.js new file mode 100644 index 00000000..6b7a270a --- /dev/null +++ b/net/web/src/session/accountAccess/AccountAccess.styled.js @@ -0,0 +1,33 @@ +import styled from 'styled-components'; +import Colors from 'constants/Colors'; + +export const AccountAccessWrapper = styled.div` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + .link { + display: flex; + flex-direction: row; + align-items: center; + cursor: pointer; + color: ${Colors.primary}; + padding-top: 8px; + + .label { + padding-left: 8px; + } + } +`; + +export const EditFooter = styled.div` + width: 100%; + display: flex; + + .select { + display: flex; + flex-grow: 1; + } +` + diff --git a/net/web/src/session/accountAccess/accountLogin/AccountLogin.jsx b/net/web/src/session/accountAccess/accountLogin/AccountLogin.jsx new file mode 100644 index 00000000..3c03a8be --- /dev/null +++ b/net/web/src/session/accountAccess/accountLogin/AccountLogin.jsx @@ -0,0 +1,24 @@ +import { Form, Input, Space } from 'antd'; +import { LockOutlined, UserOutlined } from '@ant-design/icons'; + +export function AccountLogin({ state, actions }) { + return ( +
+ + actions.setEditHandle(e.target.value)} + defaultValue={state.editHandle} autocomplete="username" autocapitalize="none" prefix={} /> + + + + actions.setEditPassword(e.target.value)} + autocomplete="new-password" prefix={} /> + + + + actions.setEditConfirm(e.target.value)} + autocomplete="new-password" prefix={} /> + +
+ ); +} + diff --git a/net/web/src/session/accountAccess/useAccountAccess.hook.js b/net/web/src/session/accountAccess/useAccountAccess.hook.js new file mode 100644 index 00000000..0f76a68c --- /dev/null +++ b/net/web/src/session/accountAccess/useAccountAccess.hook.js @@ -0,0 +1,124 @@ +import { useRef, useState, useEffect, useContext } from 'react'; +import { AccountContext } from 'context/AccountContext'; +import { ProfileContext } from 'context/ProfileContext'; +import { getUsername } from 'api/getUsername'; + +export function useAccountAccess() { + + const [state, setState] = useState({ + editLogin: false, + handle: null, + editHandle: null, + editStatus: null, + editMessage: null, + editPassword: null, + editConfirm: null, + busy: false, + searchable: null, + checked: true, + }); + + const profile = useContext(ProfileContext); + const account = useContext(AccountContext); + const debounce = useRef(null); + + const updateState = (value) => { + setState((s) => ({ ...s, ...value })); + } + + useEffect(() => { + if (profile.state.init) { + const { handle } = profile.state.profile; + updateState({ handle, editHandle: handle }); + } + }, [profile]); + + useEffect(() => { + if (account?.state?.status) { + updateState({ searchable: account.state.status.searchable }); + } + }, [account]); + + const actions = { + setEditLogin: () => { + updateState({ editLogin: true }); + }, + clearEditLogin: () => { + updateState({ editLogin: false }); + }, + setEditHandle: (editHandle) => { + updateState({ checked: false, editHandle }); + clearTimeout(debounce.current); + debounce.current = setTimeout(async () => { + if (editHandle.toLowerCase() === state.handle.toLowerCase()) { + updateState({ checked: true, editStatus: 'success', editMessage: '' }); + } + try { + let valid = await getUsername(editHandle); + if (valid) { + updateState({ checked: true, editStatus: 'success', editMessage: '' }); + } + else { + updateState({ checked: true, editStatus: 'error', editMessage: 'Username is not available' }); + } + } + catch(err) { + console.log(err); + updateState({ checked: true, editStatus: 'success', editMessage: '' }); + } + }, 500); + }, + setEditPassword: (editPassword) => { + updateState({ editPassword }); + }, + setEditConfirm: (editConfirm) => { + updateState({ editConfirm }); + }, + canSaveLogin: () => { + if(state.editStatus === 'error' || !state.checked) { + return false; + } + if(state.editHandle && state.editPassword && state.editPassword === state.editConfirm) { + return true; + } + return false; + }, + setLogin: async () => { + if (!state.editHandle || !state.editPassword || state.editPassword !== state.editConfirm) { + throw new Error("Invalid login credentials"); + } + if (!state.busy) { + try { + updateState({ busy: true }); + await account.actions.setLogin(state.editHandle, state.editPassword); + updateState({ busy: false }); + } + catch(err) { + console.log(err); + updateState({ busy: false }); + throw new Error("failed to update login"); + } + } + else { + throw new Error("save in progress"); + } + }, + setSearchable: async (flag) => { + if (!state.busy) { + try { + updateState({ busy: true }); + await account.actions.setSearchable(flag); + updateState({ busy: false }); + } + catch (err) { + console.log(err); + throw new Error('failed to set searchable'); + updateState({ busy: false }); + } + } + }, + }; + + return { state, actions }; +} + diff --git a/net/web/src/session/profile/Profile.jsx b/net/web/src/session/profile/Profile.jsx index 87159abb..ed761c08 100644 --- a/net/web/src/session/profile/Profile.jsx +++ b/net/web/src/session/profile/Profile.jsx @@ -1,10 +1,11 @@ import { useRef } from 'react'; -import { Modal, Button, Checkbox } from 'antd'; +import { Modal, Button } from 'antd'; import { ProfileWrapper, EditFooter } from './Profile.styled'; import { useProfile } from './useProfile.hook'; import { ProfileImage } from './profileImage/ProfileImage'; import { ProfileDetails } from './profileDetails/ProfileDetails'; import { Logo } from 'logo/Logo'; +import { AccountAccess } from '../accountAccess/AccountAccess'; import { LogoutOutlined, DatabaseOutlined, LockOutlined, RightOutlined, EditOutlined, BookOutlined, EnvironmentOutlined } from '@ant-design/icons'; export function Profile({ closeProfile }) { @@ -48,19 +49,6 @@ export function Profile({ closeProfile }) { } } - const saveSearchable = async (e) => { - try { - await actions.setSearchable(e.target.checked); - } - catch (err) { - console.log(err); - Modal.error({ - title: 'Update Registry Failed', - content: 'Please try again.', - }); - } - }; - const logout = () => { Modal.confirm({ title: 'Are you sure you want to logout?', @@ -146,11 +134,7 @@ export function Profile({ closeProfile }) {
Account Settings
- saveSearchable(e)}>Visible in Registry - + { state.display === 'small' && (
diff --git a/net/web/src/session/profile/profileDetails/ProfileDetails.jsx b/net/web/src/session/profile/profileDetails/ProfileDetails.jsx index e168c921..e4608937 100644 --- a/net/web/src/session/profile/profileDetails/ProfileDetails.jsx +++ b/net/web/src/session/profile/profileDetails/ProfileDetails.jsx @@ -6,15 +6,15 @@ export function ProfileDetails({ state, actions }) {
actions.setEditName(e.target.value)} - defaultValue={state.name} autocapitalizate="word" /> + defaultValue={state.editName} autocapitalizate="word" />
actions.setEditLocation(e.target.value)} - defaultValue={state.location} autocapitalizate="word" /> + defaultValue={state.editLocation} autocapitalizate="word" />
actions.setEditDescription(e.target.value)} - spellCheck="false" defaultValue={state.description} autoSize={{ minRows: 2, maxRows: 6 }} /> + spellCheck="false" defaultValue={state.editDescription} autoSize={{ minRows: 2, maxRows: 6 }} />
); diff --git a/net/web/src/session/profile/useProfile.hook.js b/net/web/src/session/profile/useProfile.hook.js index f90ebd40..5fd6fac5 100644 --- a/net/web/src/session/profile/useProfile.hook.js +++ b/net/web/src/session/profile/useProfile.hook.js @@ -1,4 +1,4 @@ -import { useState, useEffect, useContext } from 'react'; +import { useRef, useState, useEffect, useContext } from 'react'; import { ProfileContext } from 'context/ProfileContext'; import { AccountContext } from 'context/AccountContext'; import { AppContext } from 'context/AppContext'; @@ -10,6 +10,8 @@ export function useProfile() { const [state, setState] = useState({ init: false, editProfileImage: false, + editProfileDetails: false, + handle: null, name: null, location: null, description: null, @@ -20,6 +22,7 @@ export function useProfile() { crop: { w: 0, h: 0, x: 0, y: 0 }, busy: false, searchable: null, + checked: true, }); const IMAGE_DIM = 256; @@ -37,7 +40,8 @@ export function useProfile() { const { node, name, handle, location, description, image } = profile.state.profile; let url = !image ? null : profile.actions.profileImageUrl(); let editImage = !image ? avatar : url; - updateState({ init: true, name, node, handle, url, editImage, location, description }); + updateState({ init: true, name, location, description, node, handle, url, + editName: name, editLocation: location, editDescription: description, editHandle: handle, editImage }); } }, [profile]); @@ -45,12 +49,6 @@ export function useProfile() { updateState({ display: viewport.state.display }); }, [viewport]); - useEffect(() => { - if (account?.state?.status) { - updateState({ searchable: account.state.status.searchable }); - } - }, [account]); - const actions = { logout: app.actions.logout, setEditImage: (value) => { @@ -133,20 +131,6 @@ export function useProfile() { throw new Error('save in progress'); } }, - setSearchable: async (flag) => { - if (!state.busy) { - try { - updateState({ busy: true }); - await account.actions.setSearchable(flag); - updateState({ busy: false }); - } - catch (err) { - console.log(err); - throw new Error('failed to set searchable'); - updateState({ busy: false }); - } - } - }, }; return { state, actions };