supporting login change as resuable component

This commit is contained in:
Roland Osborne 2022-08-18 00:45:41 -07:00
parent 4b6b981d3d
commit 37978e65b7
9 changed files with 256 additions and 49 deletions

View File

@ -2,6 +2,7 @@ import { AccountWrapper } from './Account.styled';
import { DoubleRightOutlined } from '@ant-design/icons'; import { DoubleRightOutlined } from '@ant-design/icons';
import { Checkbox } from 'antd'; import { Checkbox } from 'antd';
import { SettingOutlined, LockOutlined } from '@ant-design/icons'; import { SettingOutlined, LockOutlined } from '@ant-design/icons';
import { AccountAccess } from '../accountAccess/AccountAccess';
export function Account({ closeAccount, openProfile }) { export function Account({ closeAccount, openProfile }) {
@ -14,15 +15,11 @@ export function Account({ closeAccount, openProfile }) {
</div> </div>
</div> </div>
<div class="content"> <div class="content">
<Checkbox>Visible in Registry</Checkbox>
<div class="link" onClick={openProfile}> <div class="link" onClick={openProfile}>
<SettingOutlined /> <SettingOutlined />
<div class="label">Update Profile</div> <div class="label">Update Profile</div>
</div> </div>
<div class="link"> <AccountAccess />
<LockOutlined />
<div class="label">Change Login</div>
</div>
</div> </div>
</AccountWrapper> </AccountWrapper>
); );

View File

@ -45,6 +45,7 @@ export const AccountWrapper = styled.div`
.link { .link {
color: ${Colors.primary}; color: ${Colors.primary};
padding-top: 16px; padding-top: 16px;
padding-bottom: 8px;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;

View File

@ -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 = (
<EditFooter>
<div class="select"></div>
<Button key="back" onClick={actions.clearEditLogin}>Cancel</Button>
<Button key="save" type="primary" onClick={saveLogin} disabled={!actions.canSaveLogin()} loading={state.busy}>Save</Button>
</EditFooter>
);
return (
<AccountAccessWrapper>
<Checkbox checked={state.searchable} onChange={(e) => saveSearchable(e)}>Visible in Registry</Checkbox>
<div class="link" onClick={actions.setEditLogin}>
<LockOutlined />
<div class="label">Change Login</div>
</div>
<Modal title="Account Login" centered visible={state.editLogin} footer={editLoginFooter}
onCancel={actions.clearEditLogin}>
<AccountLogin state={state} actions={actions} />
</Modal>
</AccountAccessWrapper>
);
}

View File

@ -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;
}
`

View File

@ -0,0 +1,24 @@
import { Form, Input, Space } from 'antd';
import { LockOutlined, UserOutlined } from '@ant-design/icons';
export function AccountLogin({ state, actions }) {
return (
<Form name="basic" wrapperCol={{ span: 24, }}>
<Form.Item name="username" validateStatus={state.editStatus} help={state.editMessage}>
<Input placeholder="Username" spellCheck="false" onChange={(e) => actions.setEditHandle(e.target.value)}
defaultValue={state.editHandle} autocomplete="username" autocapitalize="none" prefix={<UserOutlined />} />
</Form.Item>
<Form.Item name="password">
<Input.Password placeholder="Password" spellCheck="false" onChange={(e) => actions.setEditPassword(e.target.value)}
autocomplete="new-password" prefix={<LockOutlined />} />
</Form.Item>
<Form.Item name="confirm">
<Input.Password placeholder="Confirm Password" spellCheck="false" onChange={(e) => actions.setEditConfirm(e.target.value)}
autocomplete="new-password" prefix={<LockOutlined />} />
</Form.Item>
</Form>
);
}

View File

@ -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 };
}

View File

@ -1,10 +1,11 @@
import { useRef } from 'react'; import { useRef } from 'react';
import { Modal, Button, Checkbox } from 'antd'; import { Modal, Button } from 'antd';
import { ProfileWrapper, EditFooter } from './Profile.styled'; import { ProfileWrapper, EditFooter } from './Profile.styled';
import { useProfile } from './useProfile.hook'; import { useProfile } from './useProfile.hook';
import { ProfileImage } from './profileImage/ProfileImage'; import { ProfileImage } from './profileImage/ProfileImage';
import { ProfileDetails } from './profileDetails/ProfileDetails'; import { ProfileDetails } from './profileDetails/ProfileDetails';
import { Logo } from 'logo/Logo'; import { Logo } from 'logo/Logo';
import { AccountAccess } from '../accountAccess/AccountAccess';
import { LogoutOutlined, 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 }) { 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 = () => { const logout = () => {
Modal.confirm({ Modal.confirm({
title: 'Are you sure you want to logout?', title: 'Are you sure you want to logout?',
@ -146,11 +134,7 @@ export function Profile({ closeProfile }) {
</div> </div>
<div class="section">Account Settings</div> <div class="section">Account Settings</div>
<div class="controls"> <div class="controls">
<Checkbox checked={state.searchable} onChange={(e) => saveSearchable(e)}>Visible in Registry</Checkbox> <AccountAccess />
<div class="link">
<LockOutlined />
<div class="label">Change Login</div>
</div>
{ state.display === 'small' && ( { state.display === 'small' && (
<div class="logout" onClick={logout}> <div class="logout" onClick={logout}>
<LogoutOutlined /> <LogoutOutlined />

View File

@ -6,15 +6,15 @@ export function ProfileDetails({ state, actions }) {
<ProfileDetailsWrapper> <ProfileDetailsWrapper>
<div class="info"> <div class="info">
<Input placeholder="Name" spellCheck="false" onChange={(e) => actions.setEditName(e.target.value)} <Input placeholder="Name" spellCheck="false" onChange={(e) => actions.setEditName(e.target.value)}
defaultValue={state.name} autocapitalizate="word" /> defaultValue={state.editName} autocapitalizate="word" />
</div> </div>
<div class="info"> <div class="info">
<Input placeholder="Location" spellCheck="false" onChange={(e) => actions.setEditLocation(e.target.value)} <Input placeholder="Location" spellCheck="false" onChange={(e) => actions.setEditLocation(e.target.value)}
defaultValue={state.location} autocapitalizate="word" /> defaultValue={state.editLocation} autocapitalizate="word" />
</div> </div>
<div class="info"> <div class="info">
<Input.TextArea placeholder="Description" onChange={(e) => actions.setEditDescription(e.target.value)} <Input.TextArea placeholder="Description" onChange={(e) => 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 }} />
</div> </div>
</ProfileDetailsWrapper> </ProfileDetailsWrapper>
); );

View File

@ -1,4 +1,4 @@
import { useState, useEffect, useContext } from 'react'; import { useRef, useState, useEffect, useContext } from 'react';
import { ProfileContext } from 'context/ProfileContext'; import { ProfileContext } from 'context/ProfileContext';
import { AccountContext } from 'context/AccountContext'; import { AccountContext } from 'context/AccountContext';
import { AppContext } from 'context/AppContext'; import { AppContext } from 'context/AppContext';
@ -10,6 +10,8 @@ export function useProfile() {
const [state, setState] = useState({ const [state, setState] = useState({
init: false, init: false,
editProfileImage: false, editProfileImage: false,
editProfileDetails: false,
handle: null,
name: null, name: null,
location: null, location: null,
description: null, description: null,
@ -20,6 +22,7 @@ export function useProfile() {
crop: { w: 0, h: 0, x: 0, y: 0 }, crop: { w: 0, h: 0, x: 0, y: 0 },
busy: false, busy: false,
searchable: null, searchable: null,
checked: true,
}); });
const IMAGE_DIM = 256; const IMAGE_DIM = 256;
@ -37,7 +40,8 @@ export function useProfile() {
const { node, name, handle, location, description, image } = profile.state.profile; const { node, name, handle, location, description, image } = profile.state.profile;
let url = !image ? null : profile.actions.profileImageUrl(); let url = !image ? null : profile.actions.profileImageUrl();
let editImage = !image ? avatar : url; 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]); }, [profile]);
@ -45,12 +49,6 @@ export function useProfile() {
updateState({ display: viewport.state.display }); updateState({ display: viewport.state.display });
}, [viewport]); }, [viewport]);
useEffect(() => {
if (account?.state?.status) {
updateState({ searchable: account.state.status.searchable });
}
}, [account]);
const actions = { const actions = {
logout: app.actions.logout, logout: app.actions.logout,
setEditImage: (value) => { setEditImage: (value) => {
@ -133,20 +131,6 @@ export function useProfile() {
throw new Error('save in progress'); 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 }; return { state, actions };