diff --git a/src/App.tsx b/src/App.tsx index 443851c..4adffc2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import React, { useContext } from 'react'; +import React, { useContext, useEffect } from 'react'; import { Router, Switch } from 'react-router-dom'; import { routes } from './routes.config'; diff --git a/src/components/SwitchDarkThemeButton/index.tsx b/src/components/SwitchDarkThemeButton/index.tsx new file mode 100644 index 0000000..843d9ed --- /dev/null +++ b/src/components/SwitchDarkThemeButton/index.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import { Switch, SwitchProps, useColorMode } from '@chakra-ui/react'; + +const SwitchDarkThemeButton: React.FC = props => { + const { colorMode, toggleColorMode } = useColorMode(); + + return ; +}; + +export { SwitchDarkThemeButton }; diff --git a/src/components/UserProfile/Base/index.ts b/src/components/UserProfile/Base/index.ts index 5dfb06a..32893c8 100644 --- a/src/components/UserProfile/Base/index.ts +++ b/src/components/UserProfile/Base/index.ts @@ -1,3 +1 @@ -import { Base } from './Base'; - -export { Base }; +export { Base } from './Base'; diff --git a/src/components/UserProfile/Settings/index.ts b/src/components/UserProfile/Settings/index.ts index 50e0e7d..ec5d979 100644 --- a/src/components/UserProfile/Settings/index.ts +++ b/src/components/UserProfile/Settings/index.ts @@ -1,3 +1 @@ -import { Settings } from './Settings'; - -export { Settings }; +export { Settings } from './Settings'; diff --git a/src/hooks/queries/index.ts b/src/hooks/queries/index.ts index d573ed1..27ee318 100644 --- a/src/hooks/queries/index.ts +++ b/src/hooks/queries/index.ts @@ -29,6 +29,7 @@ import { useCreateUser } from './user/useCreateUser'; import { useDeleteUser } from './user/useDeleteUser'; import { useUpdateUser } from './user/useUpdateUser'; import { useUpdateUserEmail } from './user/useUpdateUserEmail'; +import { useUpdateUserInfo } from './user/useUpdateUserInfo'; /** * Category @@ -59,6 +60,7 @@ export { useCreateUser, useDeleteUser, useUpdateUser, + useUpdateUserInfo, useUpdateUserEmail, useCreateCategory, useDeleteCategory, diff --git a/src/hooks/queries/user/useUpdateUserInfo.ts b/src/hooks/queries/user/useUpdateUserInfo.ts new file mode 100644 index 0000000..fcf6e6e --- /dev/null +++ b/src/hooks/queries/user/useUpdateUserInfo.ts @@ -0,0 +1,42 @@ +import { useToast } from '@chakra-ui/react'; +import useAsync from '@educt/hooks/useAsync'; +import { IUserInfo } from '@educt/interfaces'; +import { UserServiceInstance } from '@educt/services'; +import { UpdateUserInfoParamsType } from '@educt/types'; +import axios from 'axios'; +import { useErrorHandler } from 'react-error-boundary'; + +const useUpdateUserInfo = () => { + const toast = useToast(); + const handleError = useErrorHandler(); + + const fetch = async (data: UpdateUserInfoParamsType) => { + try { + const result = await UserServiceInstance.updateInfo(data); + toast({ title: 'Info updated.', status: 'info' }); + return result.data; + } catch (error) { + if (axios.isAxiosError(error)) { + if (error.response) { + switch (error.response.status) { + case 422: + toast({ title: error.response.data.errors[0].message, status: 'error', duration: 2000 }); + break; + default: + handleError(error); + } + } else { + handleError(error); + } + } + + return Promise.reject(error); + } + }; + + const { execute, ...state } = useAsync, Parameters>(fetch); + + return { updateInfo: execute, ...state }; +}; + +export { useUpdateUserInfo }; diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index f674a5a..bbd3f0c 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -147,6 +147,10 @@ export interface IUserContacts { vk_id: string | null; } +export interface IUserInfo extends IUserContacts { + about: string | null; +} + export interface IUserRole { id: number; name: string; diff --git a/src/pages/profile/components/UpdateUserContactsForm/UpdateUserContactsForm.tsx b/src/pages/profile/components/UpdateUserContactsForm/UpdateUserContactsForm.tsx deleted file mode 100644 index 038e71d..0000000 --- a/src/pages/profile/components/UpdateUserContactsForm/UpdateUserContactsForm.tsx +++ /dev/null @@ -1,165 +0,0 @@ -import React from 'react'; -import * as helpers from '@educt/helpers'; -import { observer } from 'mobx-react'; -import { SubmitHandler, useForm } from 'react-hook-form'; -import { MdSave } from 'react-icons/md'; -import { Text, Input } from '@chakra-ui/react'; -import { Box, Stack } from '@chakra-ui/layout'; -import { FormControl, FormHelperText } from '@chakra-ui/form-control'; -import { yupResolver } from '@hookform/resolvers/yup'; - -/** - * Types - */ -import { IUserContacts } from '@educt/interfaces'; - -/** - * Hooks - */ -import { useState } from 'react'; -import { useToast } from '@chakra-ui/toast'; -import { useRootStore } from '@educt/hooks/useRootStore'; -import { useErrorHandler } from 'react-error-boundary'; - -/** - * Schema - */ -import UpdateUserContactsSchema from './UpdateUserContactsForm.validator'; -import { SaveButton } from '@educt/components/Buttons'; - -type UpdateUserContactsInputType = { - phone_number: string | null; - twitter_id: string | null; - telegram_id: string | null; - vk_id: string | null; -}; - -type UpdateUserContactsFormPropsType = { - contacts: IUserContacts | null; -}; - -const UpdateUserContactsForm: React.FC = ({ contacts }) => { - const { - register, - formState: { errors, dirtyFields, isDirty }, - handleSubmit, - reset, - } = useForm({ - resolver: yupResolver(UpdateUserContactsSchema), - defaultValues: { - phone_number: contacts && (contacts.phone_number ?? ''), - twitter_id: contacts && (contacts.twitter_id?.replace(/^/, '@') ?? ''), - telegram_id: contacts && (contacts.telegram_id?.replace(/^/, '@') ?? ''), - vk_id: contacts && (contacts.vk_id ?? ''), - }, - }); - const [isLoading, setIsLoading] = useState(false); - const { userStore } = useRootStore(); - const toast = useToast(); - const handleError = useErrorHandler(); - - /** - * Sumbit form handler - */ - const onSubmit: SubmitHandler = async data => { - try { - setIsLoading(true); - const updatedFields = helpers.getDirtyFields(dirtyFields, data); - await userStore.updateCurrentUserContacts(updatedFields); - toast({ title: 'Contacts saved!', status: 'info' }); - reset({ - phone_number: data.phone_number ?? '', - twitter_id: data.twitter_id ?? '', - telegram_id: data.telegram_id ?? '', - vk_id: data.vk_id ?? '', - }); - } catch (error: any) { - if (error.response) { - switch (error.response.status) { - case 422: - toast({ title: error.response.data.errors[0].message, status: 'error', duration: 2000 }); - break; - default: - handleError(error); - } - } else { - handleError(error); - } - } finally { - setIsLoading(false); - } - }; - - return ( - -
- - - - Phone number with country code - - - {errors.phone_number?.message} - - - - - Twitter - - - {errors.twitter_id?.message} - - - - - Telegram - - - {errors.telegram_id?.message} - - - - - VKontakte - - - {errors.vk_id?.message} - - - - - } isDisabled={!isDirty} /> - -
- ); -}; - -export default observer(UpdateUserContactsForm); diff --git a/src/pages/profile/components/UpdateUserContactsForm/UpdateUserContactsForm.validator.ts b/src/pages/profile/components/UpdateUserContactsForm/UpdateUserContactsForm.validator.ts deleted file mode 100644 index 1d5a861..0000000 --- a/src/pages/profile/components/UpdateUserContactsForm/UpdateUserContactsForm.validator.ts +++ /dev/null @@ -1,26 +0,0 @@ -import yup from '@educt/schema'; - -const UpdateUserContactsSchema = yup.object().shape({ - phone_number: yup - .string() - .isMobileValid(['en-US', 'ru-RU'], { strictMode: true }) - .transform(val => (!val ? null : val)) - .nullable(true), - twitter_id: yup - .string() - .matches(/(^|[^@\w])@(\w{1,15})\b/, 'Twitter username is not valid') - .transform(val => (!val ? null : val)) - .nullable(true), - telegram_id: yup - .string() - .matches(/(^|[^@\w])@(\w{1,64})\b/, 'Telegram username is not valid') - .transform(val => (!val ? null : val)) - .nullable(true), - vk_id: yup - .string() - .matches(/^([a-zA-Z0-9_]){1,64}$/, 'VKontakte username is not valid') - .transform(val => (!val ? null : val)) - .nullable(true), -}); - -export default UpdateUserContactsSchema; diff --git a/src/pages/profile/components/UpdateUserContactsForm/index.ts b/src/pages/profile/components/UpdateUserContactsForm/index.ts deleted file mode 100644 index b90a73c..0000000 --- a/src/pages/profile/components/UpdateUserContactsForm/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import UpdateUserContactsForm from './UpdateUserContactsForm'; - -export default UpdateUserContactsForm; diff --git a/src/pages/profile/components/index.tsx b/src/pages/profile/components/index.tsx deleted file mode 100644 index 882843c..0000000 --- a/src/pages/profile/components/index.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import React from 'react'; -import { FlexProps, Flex, BoxProps, Box, Heading, Text } from '@chakra-ui/layout'; -import { Avatar } from '@chakra-ui/avatar'; -import { Button, ButtonProps } from '@chakra-ui/button'; -import { Skeleton, SkeletonCircle, Spinner } from '@chakra-ui/react'; - -/** - * Types - */ -import { IUserRole } from '@educt/interfaces'; - -/** - * Components - */ -import UserBadge from '@educt/components/UserBadge'; - -/** - * Hooks - */ -import { useColorMode } from '@chakra-ui/react'; -import { useLogout } from '@educt/hooks/queries'; - -export const ProfileBaseInfo: React.FC = props => { - return ( - - {props.children} - - ); -}; - -interface IProfileAvatarProps extends BoxProps { - name: string; -} -export const ProfileAvatar: React.FC = props => { - const { colorMode } = useColorMode(); - const isDark = colorMode === 'dark'; - - return ( - - - - ); -}; - -interface IProfileDescriptionProps extends BoxProps { - fullname: string; - email: string; - roles: IUserRole[]; -} -export const ProfileDescription: React.FC = props => { - const { fullname, email, roles, ...boxProps } = props; - return ( - - - - - {fullname} -
- - {email} - -
- - - -
-
-
- ); -}; - -export const ProfileLoading: React.FC = props => { - return ( - - - - - - - - - - - - - - - - ); -}; diff --git a/src/pages/profile/profile.tsx b/src/pages/profile/profile.tsx index 3a75bb2..907e979 100644 --- a/src/pages/profile/profile.tsx +++ b/src/pages/profile/profile.tsx @@ -1,13 +1,17 @@ import React from 'react'; +import { observer } from 'mobx-react'; +import { runInAction } from 'mobx'; +import yup from '@educt/schema'; +import { yupResolver } from '@hookform/resolvers/yup'; +import * as helpers from '@educt/helpers'; import { InputGroup, InputRightElement, - Flex, - Switch, FormControl, FormLabel, Input, Textarea, + Text, FormHelperText, Select, Button, @@ -17,21 +21,54 @@ import { Box, Stack } from '@chakra-ui/layout'; /** * Types */ -import { IPageProps } from '@educt/interfaces'; +import type { IPageProps } from '@educt/interfaces'; /** * Components */ import { Page } from '@educt/components/PageElements'; -import { ProfileLoading } from './components'; import { UserProfile } from '@educt/components/UserProfile'; +import { SwitchDarkThemeButton } from '@educt/components/SwitchDarkThemeButton'; +import { SaveButton } from '@educt/components/Buttons'; +import LoadingPage from '@educt/components/LoadingPage'; /** * Hooks */ +import { useHistory } from 'react-router-dom'; +import { useForm } from 'react-hook-form'; import { useRootStore } from '@educt/hooks/useRootStore'; -import { observer } from 'mobx-react'; -import { MdOutlineSave } from 'react-icons/md'; +import { useUpdateUserInfo } from '@educt/hooks/queries'; + +type UpdateProfileInputType = { + about: string; + phone_number: string; + twitter_id: string; + telegram_id: string; +}; + +const schema = yup.object().shape({ + about: yup + .string() + .max(250, 'Description must be at most 250 characters') + .transform(val => (!val ? null : val)) + .nullable(true), + phone_number: yup + .string() + .isMobileValid(['en-US', 'ru-RU'], { strictMode: true }) + .transform(val => (!val ? null : val)) + .nullable(true), + twitter_id: yup + .string() + .matches(/(^|[^@\w])@(\w{1,15})\b/, 'Twitter username is not valid') + .transform(val => (!val ? null : val)) + .nullable(true), + telegram_id: yup + .string() + .matches(/(^|[^@\w])@(\w{1,64})\b/, 'Telegram username is not valid') + .transform(val => (!val ? null : val)) + .nullable(true), +}); /** * Profile page @@ -40,12 +77,58 @@ const ProfilePage: React.FC = () => { const { userStore: { me }, } = useRootStore(); + const { updateInfo, isLoading } = useUpdateUserInfo(); + const history = useHistory(); - if (me === null) return ; + const { + register, + handleSubmit, + reset, + formState: { errors, isDirty, dirtyFields }, + } = useForm({ + resolver: yupResolver(schema), + defaultValues: { + about: me?.about || '', + phone_number: me?.contacts?.phone_number || '', + twitter_id: me?.contacts?.twitter_id || '', + telegram_id: me?.contacts?.telegram_id || '', + }, + }); + + if (me === null) return ; + + const handleChangePassword = () => history.push(`/profile/change-password`); + + const handleChangeEmail = () => history.push(`/profile/change-email`); + + const handleUpdateProfile = async (data: UpdateProfileInputType) => { + try { + const updatedFields = helpers.getDirtyFields(dirtyFields, data); + const { about, phone_number, telegram_id, twitter_id } = await updateInfo(updatedFields); + + /** + * Update info in store + */ + runInAction(() => { + if (me !== null) { + me.about = about === undefined ? me.about : about; + if (me.contacts) { + me.contacts.phone_number = phone_number === undefined ? me.contacts.phone_number : phone_number; + me.contacts.twitter_id = twitter_id === undefined ? me.contacts.twitter_id : twitter_id; + me.contacts.telegram_id = telegram_id === undefined ? me.contacts.telegram_id : telegram_id; + } + } + }); + + reset(data); + } catch (error) { + console.error(error); + } + }; return ( - + @@ -57,109 +140,126 @@ const ProfilePage: React.FC = () => { - - {/* Personal Info */} - - - - - - Fullname - - +
+ + {/* Personal Info */} + + + + + + Fullname + + + + Email + + + + + + + + + About +