diff --git a/src/api.config.ts b/src/api.config.ts index dca9c91..2e0d588 100644 --- a/src/api.config.ts +++ b/src/api.config.ts @@ -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, }; diff --git a/src/components/ErrorFallback.tsx b/src/components/ErrorFallback.tsx index e0a66b2..9703a43 100644 --- a/src/components/ErrorFallback.tsx +++ b/src/components/ErrorFallback.tsx @@ -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 ( diff --git a/src/components/Header/AppName.tsx b/src/components/Header/AppName.tsx deleted file mode 100644 index 3dbd51f..0000000 --- a/src/components/Header/AppName.tsx +++ /dev/null @@ -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 ( - - - - - - - {config.metaData.appName} - - {config.metaData.appDescription} - - - ); -}; - -export default AppName; diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index 1a944ea..8c8c237 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -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,17 +33,26 @@ const Header: React.FC = () => { alignItems='center' padding='5px 1.5rem 5px 1.5rem' > - - - {isDesktop ? ( - <> - - - - ) : ( - - )} + + + + + + + {config.metaData.appName} + + {config.metaData.appDescription} + + {isDesktop ? ( + + + + + + ) : ( + + )} ); diff --git a/src/components/Layout/Layout.tsx b/src/components/Layout/Layout.tsx index 6e9541e..b59a9bc 100644 --- a/src/components/Layout/Layout.tsx +++ b/src/components/Layout/Layout.tsx @@ -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 }) => { { > {children} - ); diff --git a/src/constants.ts b/src/constants/index.ts similarity index 100% rename from src/constants.ts rename to src/constants/index.ts diff --git a/src/contexts/index.ts b/src/contexts/index.ts index 633275f..7a1e9c7 100644 --- a/src/contexts/index.ts +++ b/src/contexts/index.ts @@ -1,4 +1,10 @@ import React from 'react'; +import { ProfilePageViewStatusContextType } from 'types'; import RootStore from '../stores/RootStore'; export const StoreContext = React.createContext(undefined); + +export const ProfilePageViewContext = React.createContext({ + statusPageView: undefined, + setStatusPageView: () => {}, +}); diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index 0b10ecf..26f8f40 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -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; diff --git a/src/pages/main/index.tsx b/src/pages/main/index.tsx index 30107cb..012c5bd 100644 --- a/src/pages/main/index.tsx +++ b/src/pages/main/index.tsx @@ -18,9 +18,7 @@ const MainPage: React.FC = ({ title }) => { Main page - - ... - + diff --git a/src/pages/profile/components/UpdateUserContactsForm/UpdateUserContactsForm.tsx b/src/pages/profile/components/UpdateUserContactsForm/UpdateUserContactsForm.tsx index 4fb374c..3891d23 100644 --- a/src/pages/profile/components/UpdateUserContactsForm/UpdateUserContactsForm.tsx +++ b/src/pages/profile/components/UpdateUserContactsForm/UpdateUserContactsForm.tsx @@ -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 = ({ contact size='md' variant='flushed' placeholder='+1 (234) 555-1234' + isInvalid={errors.phone_number ? true : false} {...register('phone_number')} /> @@ -75,21 +73,42 @@ const UpdateUserContactsForm: React.FC = ({ contact Twitter - + {errors.twitter_id?.message} Telegram - + {errors.telegram_id?.message} VKontakte - + {errors.vk_id?.message} diff --git a/src/pages/profile/components/UserAccountInfo/UserAccountInfo.tsx b/src/pages/profile/components/UserAccountInfo/UserAccountInfo.tsx index 71358aa..6877836 100644 --- a/src/pages/profile/components/UserAccountInfo/UserAccountInfo.tsx +++ b/src/pages/profile/components/UserAccountInfo/UserAccountInfo.tsx @@ -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 = ({ user }) => { + const { setStatusPageView } = useContext(ProfilePageViewContext); return ( @@ -23,7 +25,6 @@ const UserAccountInfo: React.FC = ({ user }) => { - @@ -34,7 +35,6 @@ const UserAccountInfo: React.FC = ({ user }) => { - @@ -46,7 +46,6 @@ const UserAccountInfo: React.FC = ({ user }) => { - @@ -56,7 +55,7 @@ const UserAccountInfo: React.FC = ({ user }) => { ********** - + diff --git a/src/pages/profile/containers/UpdatePasswordContainer/UpdatePasswordContainer.tsx b/src/pages/profile/containers/UpdatePasswordContainer/UpdatePasswordContainer.tsx new file mode 100644 index 0000000..1a8fdcb --- /dev/null +++ b/src/pages/profile/containers/UpdatePasswordContainer/UpdatePasswordContainer.tsx @@ -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 ( + + + } + onClick={() => setStatusPageView(undefined)} + mr='5' + /> + Update password + + + Your new password must be different from the current one. + + + + ); +}; + +export default UpdatePasswordContainer; diff --git a/src/pages/profile/containers/UpdatePasswordContainer/UpdatePasswordForm/UpdatePasswordForm.tsx b/src/pages/profile/containers/UpdatePasswordContainer/UpdatePasswordForm/UpdatePasswordForm.tsx new file mode 100644 index 0000000..f236111 --- /dev/null +++ b/src/pages/profile/containers/UpdatePasswordContainer/UpdatePasswordForm/UpdatePasswordForm.tsx @@ -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({ + resolver: yupResolver(UpdatePasswordSchema), + }); + + const toast = useToast(); + const handleError = useErrorHandler(); + + const onSubmit: SubmitHandler = 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 ( + +
+ + + Old password + + Type your current password. + + + New password + + + Must be at least 6 characters. + + + + Confirm new password + + + Both passwords must match. + + + + +
+
+ ); +}; + +export default UpdatePasswordForm; diff --git a/src/pages/profile/containers/UpdatePasswordContainer/UpdatePasswordForm/UpdatePasswordForm.validator.ts b/src/pages/profile/containers/UpdatePasswordContainer/UpdatePasswordForm/UpdatePasswordForm.validator.ts new file mode 100644 index 0000000..485c887 --- /dev/null +++ b/src/pages/profile/containers/UpdatePasswordContainer/UpdatePasswordForm/UpdatePasswordForm.validator.ts @@ -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; diff --git a/src/pages/profile/containers/UpdatePasswordContainer/UpdatePasswordForm/index.ts b/src/pages/profile/containers/UpdatePasswordContainer/UpdatePasswordForm/index.ts new file mode 100644 index 0000000..46c734f --- /dev/null +++ b/src/pages/profile/containers/UpdatePasswordContainer/UpdatePasswordForm/index.ts @@ -0,0 +1,3 @@ +import UpdatePasswordForm from './UpdatePasswordForm'; + +export default UpdatePasswordForm; diff --git a/src/pages/profile/containers/UpdatePasswordContainer/index.ts b/src/pages/profile/containers/UpdatePasswordContainer/index.ts new file mode 100644 index 0000000..37feee6 --- /dev/null +++ b/src/pages/profile/containers/UpdatePasswordContainer/index.ts @@ -0,0 +1,3 @@ +import UpdatePasswordContainer from './UpdatePasswordContainer'; + +export default UpdatePasswordContainer; diff --git a/src/pages/profile/index.tsx b/src/pages/profile/index.tsx index f07a9d1..4f3a003 100644 --- a/src/pages/profile/index.tsx +++ b/src/pages/profile/index.tsx @@ -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 = ({ title }) => { +const ProfilePage: React.FC = () => { + const [statusPageView, setStatusPageView] = useState(undefined); const { colorMode } = useColorMode(); const { userStore, authStore } = useRootStore(); const history = useHistory(); @@ -42,79 +44,83 @@ const ProfilePage: React.FC = ({ title }) => { } return ( - - My account - - - - - - {`${userStore.me.first_name} ${userStore.me.last_name}`} + + {statusPageView === undefined && ( + + My account - + + + - {userStore.me.email} - - + {`${userStore.me.first_name} ${userStore.me.last_name}`} + + + {userStore.me.email} + + + + - - - - - - - Account information - My contacts - - - - - - - - - - - - + + + + Account information + My contacts + + + + + + + + + + + + + )} + {statusPageView === 'update-password' && } + ); }; diff --git a/src/services/ApiService.ts b/src/services/ApiService.ts index aa16b1b..705a9d2 100644 --- a/src/services/ApiService.ts +++ b/src/services/ApiService.ts @@ -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); diff --git a/src/services/UserService.ts b/src/services/UserService.ts index c78886b..25445fd 100644 --- a/src/services/UserService.ts +++ b/src/services/UserService.ts @@ -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 { + const result = await this.api.patch('v1/me/password', { + oldPassword, + newPassword, + }); + return result.data; + } } diff --git a/src/stores/UserStore.ts b/src/stores/UserStore.ts index 18fd794..2d041ed 100644 --- a/src/stores/UserStore.ts +++ b/src/stores/UserStore.ts @@ -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 { + try { + const result = await this.userService.updatePassword(oldPassword, newPassword); + return result; + } catch (error) { + throw error; + } + } + public reset(): void { runInAction(() => { this.me = null; diff --git a/src/types/index.d.ts b/src/types/index.d.ts new file mode 100644 index 0000000..f8e7946 --- /dev/null +++ b/src/types/index.d.ts @@ -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>; +};