feat: password update form

This commit is contained in:
Sergey Yarkov 2021-09-03 23:53:46 +03:00
parent add3b1bb24
commit fe00bb5e72
21 changed files with 347 additions and 133 deletions

View File

@ -1,8 +1,8 @@
import { AxiosRequestConfig } from 'axios';
import { API_BASE_URL } from './constants';
import * as constants from './constants';
const apiConfig: AxiosRequestConfig = {
baseURL: API_BASE_URL,
baseURL: constants.API_BASE_URL,
responseType: 'json',
withCredentials: true,
};

View File

@ -1,8 +1,11 @@
import React from 'react';
import Cookies from 'js-cookie';
import { useToast, Flex, Box, Heading, Text } from '@chakra-ui/react';
import { MdError } from 'react-icons/md';
import { useRootStore } from 'hooks/useRootStore';
const ErrorFallback: React.FC<{ error: any }> = props => {
const { userStore, authStore } = useRootStore();
const { error } = props;
const toast = useToast();
@ -10,6 +13,14 @@ const ErrorFallback: React.FC<{ error: any }> = props => {
toast({ title: `${error.message}`, duration: 10000, isClosable: true, status: 'error' });
});
if (error.response) {
if (error.response.status === 401) {
userStore.me = null;
authStore.setIsLoggedIn(false);
Cookies.remove('logged_in');
}
}
return (
<Box textAlign='center' mt={40}>
<Flex justifyContent='center'>

View File

@ -1,22 +0,0 @@
import React from 'react';
import { Flex, Box, Text, Heading } from '@chakra-ui/react';
import { MdSchool } from 'react-icons/md';
import config from 'config';
const AppName: React.FC = () => {
return (
<Flex alignItems='center'>
<Box mr='4'>
<Box as={MdSchool} color='blue.500' size='36px' />
</Box>
<Box>
<Heading as='p' fontSize='2xl' lineHeight='1'>
{config.metaData.appName}
</Heading>
<Text as='small'>{config.metaData.appDescription}</Text>
</Box>
</Flex>
);
};
export default AppName;

View File

@ -1,11 +1,13 @@
import config from 'config';
import React from 'react';
import { observer } from 'mobx-react';
import { useRootStore } from 'hooks/useRootStore';
import { Flex, Box, useColorMode } from '@chakra-ui/react';
import AppName from './AppName';
import { Flex, Box, Heading, Text, useColorMode } from '@chakra-ui/react';
import UserInfo from './UserInfo';
import UserNotifications from './UserNotifications';
import NavMobile from 'components/Nav/Mobile';
import ColorModeSwitcher from 'components/ColorModeSwitcher';
import { MdSchool } from 'react-icons/md';
const Header: React.FC = () => {
const {
@ -31,18 +33,27 @@ const Header: React.FC = () => {
alignItems='center'
padding='5px 1.5rem 5px 1.5rem'
>
<AppName />
<Flex>
<Flex alignItems='center'>
<Box mr='4'>
<Box as={MdSchool} color='blue.500' size='36px' />
</Box>
<Box>
<Heading as='p' fontSize='2xl' lineHeight='1'>
{config.metaData.appName}
</Heading>
<Text as='small'>{config.metaData.appDescription}</Text>
</Box>
</Flex>
{isDesktop ? (
<>
<Flex>
<ColorModeSwitcher />
<UserNotifications />
<UserInfo />
</>
</Flex>
) : (
<NavMobile />
)}
</Flex>
</Flex>
</Box>
);
};

View File

@ -5,7 +5,6 @@ import { Flex, Box } from '@chakra-ui/react';
import { useRootStore } from 'hooks/useRootStore';
import Header from 'components/Header';
import NavDesktop from 'components/Nav/Desktop';
import ColorModeSwitcher from 'components/ColorModeSwitcher';
/**
*
@ -27,7 +26,7 @@ const Layout: React.FC = ({ children }) => {
<NavDesktop />
<Box
as='main'
flex='1 0 auto'
flex='1 0'
maxW='85rem'
ml='25rem'
mt='7rem'
@ -45,7 +44,6 @@ const Layout: React.FC = ({ children }) => {
>
{children}
</Box>
<ColorModeSwitcher position='absolute' right={0} bottom={0} margin={5} />
</Flex>
</>
);

View File

@ -1,4 +1,10 @@
import React from 'react';
import { ProfilePageViewStatusContextType } from 'types';
import RootStore from '../stores/RootStore';
export const StoreContext = React.createContext<RootStore | undefined>(undefined);
export const ProfilePageViewContext = React.createContext<ProfilePageViewStatusContextType>({
statusPageView: undefined,
setStatusPageView: () => {},
});

View File

@ -1,15 +1,8 @@
import { IconType } from 'react-icons/lib';
import type { RouteProps } from 'react-router-dom';
import type { RouteComponentProps } from 'react-router-dom';
import { LinkType } from 'types';
import { UserRoleEnum } from '../enums';
export type LinkType = {
location: string;
title: string;
public: boolean;
icon: IconType;
};
export interface IAppConfig {
metaData: {
appName: string;

View File

@ -18,9 +18,7 @@ const MainPage: React.FC<IPageProps> = ({ title }) => {
<Box mt='10'>
<Box>
<Heading as='h1'>Main page</Heading>
<Grid templateColumns='3fr 1fr' mt='25px'>
...
</Grid>
<Grid templateColumns='3fr 1fr' mt='25px'></Grid>
</Box>
</Box>
</>

View File

@ -1,17 +1,14 @@
import React from 'react';
import { observer } from 'mobx-react';
import { SubmitHandler, useForm } from 'react-hook-form';
import { MdSave } from 'react-icons/md';
import { IUserContacts } from 'interfaces';
import { Box, Heading, Stack } from '@chakra-ui/layout';
import { Text, Input, Button } from '@chakra-ui/react';
import { FormControl, FormHelperText } from '@chakra-ui/form-control';
import { Input } from '@chakra-ui/input';
import { Text } from '@chakra-ui/react';
import { Button } from '@chakra-ui/button';
import { yupResolver } from '@hookform/resolvers/yup';
import UpdateUserContactsSchema from 'validators/UpdateUserContactsSchema';
import useUpdateUserContactsQuery from 'hooks/useUpdateUserContactsQuery';
import { observer } from 'mobx-react';
import UpdateUserContactsSchema from 'validators/UpdateUserContactsSchema';
type UpdateUserContactsInputType = {
phone_number: string | null;
@ -67,6 +64,7 @@ const UpdateUserContactsForm: React.FC<UpdateUserContactsFormProps> = ({ contact
size='md'
variant='flushed'
placeholder='+1 (234) 555-1234'
isInvalid={errors.phone_number ? true : false}
{...register('phone_number')}
/>
<Text as='small' color='red.500'>
@ -75,21 +73,42 @@ const UpdateUserContactsForm: React.FC<UpdateUserContactsFormProps> = ({ contact
</FormControl>
<FormControl id='twitter_id'>
<FormHelperText color='gray.500'>Twitter</FormHelperText>
<Input type='text' size='md' variant='flushed' placeholder='@tw_username' {...register('twitter_id')} />
<Input
type='text'
size='md'
variant='flushed'
placeholder='@tw_username'
isInvalid={errors.twitter_id ? true : false}
{...register('twitter_id')}
/>
<Text as='small' color='red.500'>
{errors.twitter_id?.message}
</Text>
</FormControl>
<FormControl id='telegram'>
<FormHelperText color='gray.500'>Telegram</FormHelperText>
<Input type='text' size='md' variant='flushed' placeholder='@tg_username' {...register('telegram_id')} />
<Input
type='text'
size='md'
variant='flushed'
placeholder='@tg_username'
isInvalid={errors.telegram_id ? true : false}
{...register('telegram_id')}
/>
<Text as='small' color='red.500'>
{errors.telegram_id?.message}
</Text>
</FormControl>
<FormControl id='vk'>
<FormHelperText color='gray.500'>VKontakte</FormHelperText>
<Input type='text' size='md' variant='flushed' placeholder='VKontakte id' {...register('vk_id')} />
<Input
type='text'
size='md'
variant='flushed'
placeholder='VKontakte id'
isInvalid={errors.vk_id ? true : false}
{...register('vk_id')}
/>
<Text as='small' color='red.500'>
{errors.vk_id?.message}
</Text>

View File

@ -1,12 +1,14 @@
import React from 'react';
import React, { useContext } from 'react';
import { Box, Heading, Stack, Flex, StackDivider, Text, Button } from '@chakra-ui/react';
import { IUser } from 'interfaces';
import { ProfilePageViewContext } from 'contexts';
type UserAccountInfoPropsType = {
user: IUser;
};
const UserAccountInfo: React.FC<UserAccountInfoPropsType> = ({ user }) => {
const { setStatusPageView } = useContext(ProfilePageViewContext);
return (
<Box borderRadius='md' borderWidth='1px' padding='20px'>
<Heading as='h3' size='lg'>
@ -23,7 +25,6 @@ const UserAccountInfo: React.FC<UserAccountInfoPropsType> = ({ user }) => {
</Text>
</Box>
</Flex>
<Flex>
<Box>
<Text as='small' color='gray.500'>
@ -34,7 +35,6 @@ const UserAccountInfo: React.FC<UserAccountInfoPropsType> = ({ user }) => {
</Text>
</Box>
</Flex>
<Flex justifyContent='space-between' alignItems='center'>
<Box>
<Text as='small' color='gray.500'>
@ -46,7 +46,6 @@ const UserAccountInfo: React.FC<UserAccountInfoPropsType> = ({ user }) => {
</Box>
<Button>Edit</Button>
</Flex>
<Flex justifyContent='space-between' alignItems='center'>
<Box>
<Text as='small' color='gray.500'>
@ -56,7 +55,7 @@ const UserAccountInfo: React.FC<UserAccountInfoPropsType> = ({ user }) => {
**********
</Text>
</Box>
<Button>Change</Button>
<Button onClick={() => setStatusPageView('update-password')}>Change</Button>
</Flex>
</Stack>
</Box>

View File

@ -0,0 +1,30 @@
import React, { useContext } from 'react';
import { Box, Heading, Flex, Text, IconButton } from '@chakra-ui/react';
import { ChevronLeftIcon } from '@chakra-ui/icons';
import { ProfilePageViewContext } from 'contexts';
import UpdatePasswordForm from './UpdatePasswordForm';
const UpdatePasswordContainer: React.FC = () => {
const { setStatusPageView } = useContext(ProfilePageViewContext);
return (
<Box maxW='700px'>
<Flex alignItems='center'>
<IconButton
aria-label='Back'
borderRadius='full'
icon={<ChevronLeftIcon />}
onClick={() => setStatusPageView(undefined)}
mr='5'
/>
<Heading as='h1'>Update password</Heading>
</Flex>
<Box mt='3'>
<Text>Your new password must be different from the current one.</Text>
</Box>
<UpdatePasswordForm />
</Box>
);
};
export default UpdatePasswordContainer;

View File

@ -0,0 +1,108 @@
import React, { useContext } from 'react';
import { Box, Stack } from '@chakra-ui/layout';
import { FormControl, FormLabel, FormHelperText } from '@chakra-ui/form-control';
import { Button } from '@chakra-ui/button';
import { Input } from '@chakra-ui/input';
import { SubmitHandler, useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import UpdatePasswordSchema from './UpdatePasswordForm.validator';
import { useRootStore } from 'hooks/useRootStore';
import { useToast } from '@chakra-ui/toast';
import { useErrorHandler } from 'react-error-boundary';
import { ProfilePageViewContext } from 'contexts';
type UpdatePasswordInputType = {
old_password: string;
new_password: string;
confirm_password: string;
};
/**
* Update password using form
*/
const UpdatePasswordForm: React.FC = () => {
const { setStatusPageView } = useContext(ProfilePageViewContext);
const { userStore } = useRootStore();
const {
handleSubmit,
register,
reset,
formState: { errors },
} = useForm<UpdatePasswordInputType>({
resolver: yupResolver(UpdatePasswordSchema),
});
const toast = useToast();
const handleError = useErrorHandler();
const onSubmit: SubmitHandler<UpdatePasswordInputType> = async data => {
try {
await userStore.updateCurrentUserPassword(data.old_password, data.new_password);
toast({ title: 'Password changed.', status: 'success' });
reset();
setStatusPageView(undefined);
} catch (error: any) {
if (error.response) {
if (error.response.status === 401) {
toast({ title: 'Invalid current password.', status: 'error', duration: 2000 });
}
} else {
handleError(error);
}
}
};
return (
<Box maxW='700px'>
<form onSubmit={handleSubmit(onSubmit)}>
<Stack spacing='15px' mt='10'>
<FormControl id='old_password' isRequired>
<FormLabel>Old password</FormLabel>
<Input
type='text'
size='md'
variant='filled'
placeholder='*******'
isInvalid={errors.old_password ? true : false}
{...register('old_password')}
/>
<FormHelperText color='gray.500'>Type your current password.</FormHelperText>
</FormControl>
<FormControl id='new_password' isRequired>
<FormLabel>New password</FormLabel>
<Input
type='text'
size='md'
variant='filled'
placeholder='*******'
isInvalid={errors.new_password ? true : false}
{...register('new_password')}
/>
<FormHelperText color={errors.new_password ? 'red' : 'gray.500'}>
Must be at least 6 characters.
</FormHelperText>
</FormControl>
<FormControl id='confirm_password' isRequired>
<FormLabel>Confirm new password</FormLabel>
<Input
type='text'
size='md'
variant='filled'
placeholder='*******'
isInvalid={errors.confirm_password ? true : false}
{...register('confirm_password')}
/>
<FormHelperText color={errors.confirm_password ? 'red' : 'gray.500'}>
Both passwords must match.
</FormHelperText>
</FormControl>
</Stack>
<Button colorScheme='blue' mt='4' type='submit' size='md' variant='outline' loadingText='Saving...'>
Change
</Button>
</form>
</Box>
);
};
export default UpdatePasswordForm;

View File

@ -0,0 +1,14 @@
import yup from 'validators';
const UpdatePasswordSchema = yup.object().shape({
old_password: yup.string().required('Old password is required'),
new_password: yup.string().required('New password is required').min(6, 'Password bust be at least 6 characters.'),
confirm_password: yup
.string()
.required('Confirm password is required')
.test('passwords-match', 'Make sure your passwords match.', function (value) {
return this.parent.new_password === value;
}),
});
export default UpdatePasswordSchema;

View File

@ -0,0 +1,3 @@
import UpdatePasswordForm from './UpdatePasswordForm';
export default UpdatePasswordForm;

View File

@ -0,0 +1,3 @@
import UpdatePasswordContainer from './UpdatePasswordContainer';
export default UpdatePasswordContainer;

View File

@ -1,4 +1,4 @@
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import {
Box,
Heading,
@ -12,21 +12,23 @@ import {
TabPanels,
TabPanel,
} from '@chakra-ui/react';
import { IPageProps } from 'interfaces';
import { ProfilePageStatusType } from 'types';
import { useRootStore } from 'hooks/useRootStore';
import { useHistory } from 'react-router-dom';
import { observer } from 'mobx-react';
import { ProfilePageViewContext } from 'contexts';
import UserBadge from 'components/UserBadge';
import LoadingPage from './components/LoadingPage';
import UserAccountInfo from './components/UserAccountInfo';
import UpdateUserContactsForm from './components/UpdateUserContactsForm';
import UpdatePasswordContainer from './containers/UpdatePasswordContainer';
/**
* Profile page
*/
const ProfilePage: React.FC<IPageProps> = ({ title }) => {
const ProfilePage: React.FC<IPageProps> = () => {
const [statusPageView, setStatusPageView] = useState<ProfilePageStatusType>(undefined);
const { colorMode } = useColorMode();
const { userStore, authStore } = useRootStore();
const history = useHistory();
@ -42,6 +44,8 @@ const ProfilePage: React.FC<IPageProps> = ({ title }) => {
}
return (
<ProfilePageViewContext.Provider value={{ statusPageView, setStatusPageView }}>
{statusPageView === undefined && (
<Box maxW='900px'>
<Heading as='h1'>My account</Heading>
<Flex
@ -97,7 +101,6 @@ const ProfilePage: React.FC<IPageProps> = ({ title }) => {
</Flex>
</Flex>
</Flex>
<Box mt='10'>
<Tabs>
<TabList>
@ -115,6 +118,9 @@ const ProfilePage: React.FC<IPageProps> = ({ title }) => {
</Tabs>
</Box>
</Box>
)}
{statusPageView === 'update-password' && <UpdatePasswordContainer />}
</ProfilePageViewContext.Provider>
);
};

View File

@ -1,5 +1,4 @@
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
import Cookies from 'js-cookie';
import RootStore from 'stores/RootStore';
export default class ApiService {
@ -26,9 +25,9 @@ export default class ApiService {
* Unauthorized Error 401
*/
if (error.response.status === 401) {
this.root.userStore.me = null;
this.root.authStore.setIsLoggedIn(false);
Cookies.remove('logged_in');
// this.root.userStore.me = null;
// this.root.authStore.setIsLoggedIn(false);
// Cookies.remove('logged_in');
}
}
return Promise.reject(error);

View File

@ -1,5 +1,5 @@
import { AxiosInstance } from 'axios';
import { IUpdatedContactsResult, IUserContacts, IUserResult } from 'interfaces';
import { IDataResult, IUpdatedContactsResult, IUserContacts, IUserResult } from 'interfaces';
import * as helpers from 'helpers';
export default class UserService {
@ -29,4 +29,19 @@ export default class UserService {
const result = await this.api.put('v1/me/contacts', helpers.removeEmptyValues(data));
return result.data;
}
/**
* Update password of authorized user
*
* @param oldPassword string
* @param newPassword string
* @returns Result data
*/
public async updatePassword(oldPassword: string, newPassword: string): Promise<IDataResult> {
const result = await this.api.patch('v1/me/password', {
oldPassword,
newPassword,
});
return result.data;
}
}

View File

@ -1,6 +1,6 @@
import { AxiosInstance } from 'axios';
import { makeAutoObservable, runInAction } from 'mobx';
import { IUserResult, IUser, IUserContacts, IUpdatedContactsResult } from 'interfaces';
import { IUserResult, IUser, IUserContacts, IUpdatedContactsResult, IDataResult } from 'interfaces';
/**
* Services
@ -57,6 +57,15 @@ export default class UserStore {
}
}
public async updateCurrentUserPassword(oldPassword: string, newPassword: string): Promise<IDataResult> {
try {
const result = await this.userService.updatePassword(oldPassword, newPassword);
return result;
} catch (error) {
throw error;
}
}
public reset(): void {
runInAction(() => {
this.me = null;

14
src/types/index.d.ts vendored Normal file
View File

@ -0,0 +1,14 @@
import { IconType } from 'react-icons/lib';
export type LinkType = {
location: string;
title: string;
public: boolean;
icon: IconType;
};
export type ProfilePageStatusType = 'update-password' | 'update-email' | undefined;
export type ProfilePageViewStatusContextType = {
statusPageView: ProfilePageStatusType;
setStatusPageView: React.Dispatch<React.SetStateAction<ProfilePageStatusType>>;
};