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 { 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 }) {
</div>
</div>
<div class="content">
<Checkbox>Visible in Registry</Checkbox>
<div class="link" onClick={openProfile}>
<SettingOutlined />
<div class="label">Update Profile</div>
</div>
<div class="link">
<LockOutlined />
<div class="label">Change Login</div>
</div>
<AccountAccess />
</div>
</AccountWrapper>
);

View File

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

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 { 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 }) {
</div>
<div class="section">Account Settings</div>
<div class="controls">
<Checkbox checked={state.searchable} onChange={(e) => saveSearchable(e)}>Visible in Registry</Checkbox>
<div class="link">
<LockOutlined />
<div class="label">Change Login</div>
</div>
<AccountAccess />
{ state.display === 'small' && (
<div class="logout" onClick={logout}>
<LogoutOutlined />

View File

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