refactor: refactor profile page

This commit is contained in:
Sergey Yarkov 2021-10-30 18:45:28 +03:00
parent 1ceb083282
commit 27e1cdbfa5
29 changed files with 459 additions and 419 deletions

View File

@ -23,6 +23,8 @@ import PublicRoute from '@educt/components/PublicRoute';
import AuthPage from '@educt/pages/auth/auth'; import AuthPage from '@educt/pages/auth/auth';
import NotFoundPage from '@educt/pages/404/404'; import NotFoundPage from '@educt/pages/404/404';
import CreateCoursePage from '@educt/pages/courses/create/create'; import CreateCoursePage from '@educt/pages/courses/create/create';
import ChangePasswordPage from '@educt/pages/profile/change-password/change-password';
import ChangeEmailPage from '@educt/pages/profile/change-email/change-email';
const CoursesPage = React.lazy(() => import('@educt/pages/courses/courses')); const CoursesPage = React.lazy(() => import('@educt/pages/courses/courses'));
const MainPage = React.lazy(() => import('@educt/pages/main/main')); const MainPage = React.lazy(() => import('@educt/pages/main/main'));
const MessagesPage = React.lazy(() => import('@educt/pages/messages/messages')); const MessagesPage = React.lazy(() => import('@educt/pages/messages/messages'));
@ -46,7 +48,9 @@ const App = () => {
roles={[UserRoleEnum.ADMIN, UserRoleEnum.TEACHER]} roles={[UserRoleEnum.ADMIN, UserRoleEnum.TEACHER]}
/> />
<PrivateRoute path='/messages' component={MessagesPage} title='Messages' /> <PrivateRoute path='/messages' component={MessagesPage} title='Messages' />
<PrivateRoute path='/profile' component={ProfilePage} title='Profile' /> <PrivateRoute exact path='/profile' component={ProfilePage} title='Profile' />
<PrivateRoute path='/profile/change-password' component={ChangePasswordPage} title='Change Password' />
<PrivateRoute path='/profile/change-email' component={ChangeEmailPage} title='Change Email' />
<PrivateRoute <PrivateRoute
path='/users' path='/users'
component={UsersPage} component={UsersPage}

View File

@ -1,18 +1,24 @@
import config from '@educt/config';
import React from 'react'; import React from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { useRootStore } from '@educt/hooks/useRootStore';
import { Flex, Box, Heading, useColorMode } from '@chakra-ui/react'; import { Flex, Box, Heading, useColorMode } from '@chakra-ui/react';
import UserInfo from './UserInfo'; import { MdSchool } from 'react-icons/md';
import UserNotifications from './UserNotifications'; import config from '@educt/config';
/**
* Components
*/
import NavMobile from '@educt/components/Nav/Mobile'; import NavMobile from '@educt/components/Nav/Mobile';
import ColorModeSwitcher from '@educt/components/ColorModeSwitcher'; import ColorModeSwitcher from '@educt/components/ColorModeSwitcher';
import { MdSchool } from 'react-icons/md'; import UserInfo from './UserInfo';
import UserNotifications from './UserNotifications';
/**
* Hooks
*/
import { useMediaQuery } from '@chakra-ui/media-query';
const Header: React.FC = () => { const Header: React.FC = () => {
const { const [isDesktop] = useMediaQuery('(min-width: 992px)');
uiStore: { isDesktop },
} = useRootStore();
const { colorMode } = useColorMode(); const { colorMode } = useColorMode();
return ( return (
@ -22,7 +28,9 @@ const Header: React.FC = () => {
top={0} top={0}
left={0} left={0}
right={0} right={0}
borderBottomWidth={1} // borderBottomWidth={1}
// h='43px'
boxShadow='md'
backgroundColor={colorMode === 'dark' ? 'gray.800' : 'white'} backgroundColor={colorMode === 'dark' ? 'gray.800' : 'white'}
zIndex={4} zIndex={4}
> >

View File

@ -44,24 +44,7 @@ const Layout: React.FC<LayoutPropsType> = ({ children, roles }) => {
<Header /> <Header />
<NavDesktop /> <NavDesktop />
<Flex minH='100vh' justifyContent='center'> <Flex minH='100vh' justifyContent='center'>
<Box <Box as='main' flex='1 0' maxW='85rem' ml={{ base: '0', lg: '20rem' }} mt='7rem' mb='1rem' pr='2rem' pl='2rem'>
as='main'
flex='1 0'
maxW='85rem'
ml='20rem'
mt='7rem'
mb='1rem'
pr='2rem'
pl='2rem'
sx={{
'@media (max-width: 1400px)': {
ml: '20rem',
},
'@media (max-width: 991px)': {
ml: '0',
},
}}
>
<Suspense fallback={<LoadingPage />}> <Suspense fallback={<LoadingPage />}>
{(() => { {(() => {
/** /**

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { Flex, Box, useColorMode } from '@chakra-ui/react'; import { Flex, Box, Text, Link, IconButton } from '@chakra-ui/react';
import config from '@educt/config'; import config from '@educt/config';
/** /**
@ -11,7 +11,8 @@ import NavList from '../NavList';
/** /**
* Hooks * Hooks
*/ */
import { useRootStore } from '@educt/hooks/useRootStore'; import { useColorMode, useMediaQuery } from '@chakra-ui/react';
import { FaGithub } from 'react-icons/fa';
/** /**
* *
@ -19,14 +20,13 @@ import { useRootStore } from '@educt/hooks/useRootStore';
* Component for navigating the application. * Component for navigating the application.
*/ */
const Nav: React.FC = () => { const Nav: React.FC = () => {
const {
uiStore: { isDesktop },
} = useRootStore();
const { colorMode } = useColorMode(); const { colorMode } = useColorMode();
const [isDesktop] = useMediaQuery('(min-width: 992px)');
if (isDesktop) { if (isDesktop) {
return ( return (
<Box <Flex
flexDir='column'
borderRightWidth='1px' borderRightWidth='1px'
w='18rem' w='18rem'
position='fixed' position='fixed'
@ -49,7 +49,19 @@ const Nav: React.FC = () => {
> >
<NavList links={config.links} /> <NavList links={config.links} />
</Flex> </Flex>
</Box> <Box textAlign='center' mt='auto' mb='5'>
<Text fontSize='xs' color='gray.500' lineHeight='1.5'>
Learning Management System <br /> Made by Sergey Yarkov
</Text>
<Box textAlign='center' mt='2'>
<IconButton variant='link' aria-label='GitHub repository'>
<Link isExternal href='https://github.com/sergeyyarkov/educt-client'>
<Box as={FaGithub} />
</Link>
</IconButton>
</Box>
</Box>
</Flex>
); );
} }
return null; return null;

View File

@ -1,14 +1,14 @@
import { createContext } from 'react'; import { createContext } from 'react';
import { CoursesPageContextType, ProfilePageContextType, UsersPageContextType } from '@educt/types'; import { ChangeEmailPageContextType, CoursesPageContextType, UsersPageContextType } from '@educt/types';
import RootStore from '../stores/RootStore'; import RootStore from '../stores/RootStore';
export const StoreContext = createContext<RootStore | undefined>(undefined); export const StoreContext = createContext<RootStore | undefined>(undefined);
export const ProfilePageContext = createContext<ProfilePageContextType>({ export const ChangeEmailPageContext = createContext<ChangeEmailPageContextType>({
statusPageView: 'default', isCodeSent: false,
setStatusPageView: () => {}, setIsCodeSent: () => {},
pageData: {}, confirmEmailData: undefined,
setPageData: () => {}, setConfirmEmailData: () => {},
}); });
export const UsersPageContext = createContext<UsersPageContextType>({ export const UsersPageContext = createContext<UsersPageContextType>({

View File

@ -88,7 +88,7 @@ const CourseList: React.FC<CourseListPropsType> = ({ courses, isLoading }) => {
<> <>
{deletingCourse && <DeleteCourseDialog onClose={onCloseDeleteDialog} isOpen={isOpenDeleteDialog} />} {deletingCourse && <DeleteCourseDialog onClose={onCloseDeleteDialog} isOpen={isOpenDeleteDialog} />}
{courses.length !== 0 ? ( {courses.length !== 0 ? (
<Grid templateColumns='repeat(auto-fill, minmax(400px, 1fr))' gap='6'> <Grid templateColumns={{ sm: 'repeat(auto-fill, minmax(400px, 1fr))' }} gap='4'>
{/* Render items */} {/* Render items */}
{courses.map(course => ( {courses.map(course => (
<CourseItem key={course.id} course={course} onDelete={onDeleteCourse} onSetStatus={onSetStatus} /> <CourseItem key={course.id} course={course} onDelete={onDeleteCourse} onSetStatus={onSetStatus} />

View File

@ -0,0 +1,84 @@
import React from 'react';
import { Box, Flex, Heading, Text } from '@chakra-ui/layout';
import { IconButton } from '@chakra-ui/button';
import { ChevronLeftIcon } from '@chakra-ui/icons';
/**
* Types
*/
import { IPageProps } from '@educt/interfaces';
/**
* Components
*/
import LoadingPage from '@educt/components/LoadingPage';
import UpdateEmailForm from './components/UpdateEmailForm';
/**
* Hooks
*/
import { useHistory } from 'react-router';
import { useRootStore } from '@educt/hooks/useRootStore';
import { observer } from 'mobx-react';
/**
* Providers
*/
import { ChangeEmailPageContextProvider } from '@educt/providers';
import { ChangeEmailPageContext } from '@educt/contexts';
import ConfirmEmailContainer from './containers/ConfirmEmailContainer';
{
/* <>
{!isCodeSent ? (
<UpdateEmailForm currentEmail={me.email} />
) : (
confirmEmailData && <ConfirmEmailContainer data={confirmEmailData} />
)}
</> */
}
/**
* Change email page
*/
const ChangeEmailPage: React.FC<IPageProps> = () => {
const {
userStore: { me },
} = useRootStore();
const history = useHistory();
if (me === null) return <LoadingPage />;
return (
<ChangeEmailPageContextProvider>
<ChangeEmailPageContext.Consumer>
{({ isCodeSent, confirmEmailData }) => (
<>
{!isCodeSent ? (
<Box maxW='700px'>
<Flex alignItems='center'>
<IconButton
aria-label='Back'
borderRadius='full'
icon={<ChevronLeftIcon />}
onClick={() => history.push('/profile')}
mr='5'
/>
<Heading as='h1'>Edit email address</Heading>
</Flex>
<Box mt='3'>
<Text>Type your new email address.</Text>
</Box>
<UpdateEmailForm currentEmail={me.email} />
</Box>
) : (
confirmEmailData && <ConfirmEmailContainer data={confirmEmailData} />
)}
</>
)}
</ChangeEmailPageContext.Consumer>
</ChangeEmailPageContextProvider>
);
};
export default observer(ChangeEmailPage);

View File

@ -1,26 +1,46 @@
import React from 'react';
import { Button } from '@chakra-ui/button'; import { Button } from '@chakra-ui/button';
import { FormControl, FormHelperText, FormLabel } from '@chakra-ui/form-control'; import { FormControl, FormHelperText, FormLabel } from '@chakra-ui/form-control';
import { Input } from '@chakra-ui/input'; import { Input } from '@chakra-ui/input';
import { Box, Stack } from '@chakra-ui/layout'; import { Box, Stack } from '@chakra-ui/layout';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import { ProfilePageContext } from '@educt/contexts';
import { useRootStore } from '@educt/hooks/useRootStore'; /**
* Types
*/
import type { SubmitHandler } from 'react-hook-form';
/**
* Hooks
*/
import { useContext, useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import useUpdateUserEmailQuery from '@educt/hooks/useUpdateUserEmailQuery.ts'; import useUpdateUserEmailQuery from '@educt/hooks/useUpdateUserEmailQuery.ts';
import React, { useContext, useEffect, useState } from 'react';
import { SubmitHandler, useForm } from 'react-hook-form'; /**
* Contexts
*/
import { ChangeEmailPageContext } from '@educt/contexts';
/**
* Schema
*/
import UpdateEmailSchema from './UpdateEmailForm.validator'; import UpdateEmailSchema from './UpdateEmailForm.validator';
type UpdateEmailInputType = { type UpdateEmailInputType = {
email: string; email: string;
}; };
type UpdateEmailFormPropsType = {
currentEmail: string;
};
/** /**
* Update email using form * Send confirmation code before change email using form
*/ */
const UpdateEmailForm: React.FC = () => { const UpdateEmailForm: React.FC<UpdateEmailFormPropsType> = ({ currentEmail }) => {
const [email, setEmail] = useState<string | null>(null); const [newEmail, setNewEmail] = useState<string | null>(null);
const { setStatusPageView, setPageData } = useContext(ProfilePageContext); const { setIsCodeSent, setConfirmEmailData } = useContext(ChangeEmailPageContext);
const { userStore } = useRootStore();
const { sendConfirmationCode, loading, result } = useUpdateUserEmailQuery(); const { sendConfirmationCode, loading, result } = useUpdateUserEmailQuery();
const { const {
handleSubmit, handleSubmit,
@ -30,16 +50,25 @@ const UpdateEmailForm: React.FC = () => {
resolver: yupResolver(UpdateEmailSchema), resolver: yupResolver(UpdateEmailSchema),
}); });
/**
* Change page view in confirmation code has been sent
*/
useEffect(() => { useEffect(() => {
if (email !== null && result !== null) { if (newEmail !== null && result !== null) {
setStatusPageView('confirm-email'); setIsCodeSent(true);
setPageData({ confirmEmailData: { newEmail: email, expired_seconds: result.data.expired_seconds } }); setConfirmEmailData({ newEmail, expired_seconds: result.data.expired_seconds });
} }
}); }, [result]);
const onSubmit: SubmitHandler<UpdateEmailInputType> = ({ email }) => { /**
setEmail(email); * Submit form handler
sendConfirmationCode(email); */
const onSubmit: SubmitHandler<UpdateEmailInputType> = async ({ email }) => {
/**
* Set new email state and send confirmation code
*/
setNewEmail(email);
await sendConfirmationCode(email);
}; };
return ( return (
@ -48,7 +77,7 @@ const UpdateEmailForm: React.FC = () => {
<Stack spacing='15px' mt='10'> <Stack spacing='15px' mt='10'>
<FormControl id='email'> <FormControl id='email'>
<FormLabel>Current email</FormLabel> <FormLabel>Current email</FormLabel>
<Input type='email' size='md' variant='filled' disabled value={userStore.me?.email} /> <Input type='email' size='md' variant='filled' disabled value={currentEmail} />
</FormControl> </FormControl>
<FormControl id='new_email' isRequired> <FormControl id='new_email' isRequired>
<FormLabel>New email</FormLabel> <FormLabel>New email</FormLabel>

View File

@ -1,22 +1,30 @@
import React, { useContext, useEffect, useState } from 'react'; import React from 'react';
import { PinInput, PinInputField } from '@chakra-ui/react'; import { PinInput, PinInputField } from '@chakra-ui/react';
import { Button } from '@chakra-ui/button'; import { Button } from '@chakra-ui/button';
import { Box, Flex, Heading, HStack, Text } from '@chakra-ui/layout'; import { Box, Flex, Heading, HStack, Text } from '@chakra-ui/layout';
import { ConfirmEmailContainerDataType } from '@educt/types';
/**
* Types
*/
import { ConfirmEmailDataType } from '@educt/types';
/**
* Hooks
*/
import { useEffect, useState } from 'react';
import useUpdateUserEmailQuery from '@educt/hooks/useUpdateUserEmailQuery.ts'; import useUpdateUserEmailQuery from '@educt/hooks/useUpdateUserEmailQuery.ts';
import useIsMountedRef from '@educt/hooks/useIsMountedRef';
import { useRootStore } from '@educt/hooks/useRootStore'; import { useRootStore } from '@educt/hooks/useRootStore';
import { useToast } from '@chakra-ui/toast'; import { useToast } from '@chakra-ui/toast';
import { useErrorHandler } from 'react-error-boundary'; import { useErrorHandler } from 'react-error-boundary';
import { ProfilePageContext } from '@educt/contexts'; import { useHistory } from 'react-router';
import useIsMountedRef from '@educt/hooks/useIsMountedRef';
type ConfirmEmailContainerPropsType = { type ConfirmEmailContainerPropsType = {
data: ConfirmEmailContainerDataType; data: ConfirmEmailDataType;
}; };
const ConfirmEmailContainer: React.FC<ConfirmEmailContainerPropsType> = ({ data }) => { const ConfirmEmailContainer: React.FC<ConfirmEmailContainerPropsType> = ({ data }) => {
const { userStore } = useRootStore(); const { userStore } = useRootStore();
const { setStatusPageView } = useContext(ProfilePageContext);
const { sendConfirmationCode, loading } = useUpdateUserEmailQuery(); const { sendConfirmationCode, loading } = useUpdateUserEmailQuery();
const [isVerifying, setIsVerifying] = useState<boolean>(false); const [isVerifying, setIsVerifying] = useState<boolean>(false);
const [code, setCode] = useState<string>(''); const [code, setCode] = useState<string>('');
@ -24,6 +32,7 @@ const ConfirmEmailContainer: React.FC<ConfirmEmailContainerPropsType> = ({ data
const isMountedRef = useIsMountedRef(); const isMountedRef = useIsMountedRef();
const toast = useToast(); const toast = useToast();
const handleError = useErrorHandler(); const handleError = useErrorHandler();
const history = useHistory();
useEffect(() => { useEffect(() => {
const interval = window.setInterval(() => { const interval = window.setInterval(() => {
@ -47,7 +56,7 @@ const ConfirmEmailContainer: React.FC<ConfirmEmailContainerPropsType> = ({ data
status: 'info', status: 'info',
}); });
if (isMountedRef.current) { if (isMountedRef.current) {
setStatusPageView('default'); history.push('/profile');
} }
} catch (error: any) { } catch (error: any) {
if (error.response) { if (error.response) {

View File

@ -0,0 +1,54 @@
import React from 'react';
import { Box, Heading, Flex, Text, IconButton } from '@chakra-ui/react';
import { ChevronLeftIcon } from '@chakra-ui/icons';
/**
* Types
*/
import { IPageProps } from '@educt/interfaces';
/**
* Components
*/
import UpdatePasswordForm from './components/UpdatePasswordForm';
/**
* Hooks
*/
import { useRootStore } from '@educt/hooks/useRootStore';
import { useHistory } from 'react-router';
import LoadingPage from '@educt/components/LoadingPage';
import { observer } from 'mobx-react';
/**
* Change password page
*/
const ChangePasswordPage: React.FC<IPageProps> = () => {
const {
userStore: { me },
} = useRootStore();
const history = useHistory();
if (me === null) return <LoadingPage />;
return (
<Box maxW='700px'>
<Flex alignItems='center'>
<IconButton
aria-label='Profile page'
borderRadius='full'
icon={<ChevronLeftIcon />}
onClick={() => history.push('/profile')}
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 observer(ChangePasswordPage);

View File

@ -1,16 +1,30 @@
import React, { useContext, useState } from 'react'; import React from 'react';
import { useRootStore } from '@educt/hooks/useRootStore';
import { Box, Stack } from '@chakra-ui/layout'; import { Box, Stack } from '@chakra-ui/layout';
import { FormControl, FormLabel, FormHelperText } from '@chakra-ui/form-control'; import { FormControl, FormLabel, FormHelperText } from '@chakra-ui/form-control';
import { Button } from '@chakra-ui/button'; import { Button } from '@chakra-ui/button';
import { Input } from '@chakra-ui/input'; import { Input } from '@chakra-ui/input';
import { SubmitHandler, useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import UpdatePasswordSchema from './UpdatePasswordForm.validator';
import { useToast } from '@chakra-ui/toast'; /**
* Types
*/
import type { SubmitHandler } from 'react-hook-form';
/**
* Hooks
*/
import { useState } from 'react';
import { useHistory } from 'react-router';
import { useForm } from 'react-hook-form';
import { useErrorHandler } from 'react-error-boundary'; import { useErrorHandler } from 'react-error-boundary';
import { ProfilePageContext } from '@educt/contexts'; import { useRootStore } from '@educt/hooks/useRootStore';
import useIsMountedRef from '@educt/hooks/useIsMountedRef'; import useIsMountedRef from '@educt/hooks/useIsMountedRef';
import { useToast } from '@chakra-ui/toast';
/**
* Schema
*/
import UpdatePasswordSchema from './UpdatePasswordForm.validator';
type UpdatePasswordInputType = { type UpdatePasswordInputType = {
old_password: string; old_password: string;
@ -22,10 +36,9 @@ type UpdatePasswordInputType = {
* Update password using form * Update password using form
*/ */
const UpdatePasswordForm: React.FC = () => { const UpdatePasswordForm: React.FC = () => {
const { setStatusPageView } = useContext(ProfilePageContext);
const { userStore } = useRootStore(); const { userStore } = useRootStore();
const isMountedRef = useIsMountedRef(); const isMountedRef = useIsMountedRef();
const [loading, setLoading] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(false);
const { const {
handleSubmit, handleSubmit,
register, register,
@ -35,19 +48,25 @@ const UpdatePasswordForm: React.FC = () => {
} = useForm<UpdatePasswordInputType>({ } = useForm<UpdatePasswordInputType>({
resolver: yupResolver(UpdatePasswordSchema), resolver: yupResolver(UpdatePasswordSchema),
}); });
const history = useHistory();
const toast = useToast(); const toast = useToast();
const handleError = useErrorHandler(); const handleError = useErrorHandler();
/**
* Submit form handler
*/
const onSubmit: SubmitHandler<UpdatePasswordInputType> = async ({ old_password, new_password, confirm_password }) => { const onSubmit: SubmitHandler<UpdatePasswordInputType> = async ({ old_password, new_password, confirm_password }) => {
try { try {
setLoading(true); setIsLoading(true);
await userStore.updateCurrentUserPassword(old_password, new_password); await userStore.updateCurrentUserPassword(old_password, new_password);
toast({ title: 'Password changed.', status: 'success' }); toast({ title: 'Password changed.', status: 'success' });
reset(); reset();
setStatusPageView('default'); history.push('/profile');
} catch (error: any) { } catch (error: any) {
if (error.response) { if (error.response) {
/**
* Invalid current password handler
*/
if (error.response.status === 401) { if (error.response.status === 401) {
toast({ title: 'Invalid current password.', status: 'error', duration: 2000 }); toast({ title: 'Invalid current password.', status: 'error', duration: 2000 });
reset({ old_password: '', new_password, confirm_password }); reset({ old_password: '', new_password, confirm_password });
@ -60,7 +79,7 @@ const UpdatePasswordForm: React.FC = () => {
} }
} finally { } finally {
if (isMountedRef.current) { if (isMountedRef.current) {
setLoading(false); setIsLoading(false);
} }
} }
}; };
@ -76,7 +95,7 @@ const UpdatePasswordForm: React.FC = () => {
size='md' size='md'
variant='filled' variant='filled'
placeholder='*******' placeholder='*******'
isInvalid={errors.old_password ? true : false} isInvalid={!!errors.old_password}
{...register('old_password')} {...register('old_password')}
/> />
<FormHelperText color='gray.500'>Type your current password.</FormHelperText> <FormHelperText color='gray.500'>Type your current password.</FormHelperText>
@ -88,7 +107,7 @@ const UpdatePasswordForm: React.FC = () => {
size='md' size='md'
variant='filled' variant='filled'
placeholder='*******' placeholder='*******'
isInvalid={errors.new_password ? true : false} isInvalid={!!errors.new_password}
{...register('new_password')} {...register('new_password')}
/> />
<FormHelperText color={errors.new_password ? 'red' : 'gray.500'}> <FormHelperText color={errors.new_password ? 'red' : 'gray.500'}>
@ -102,7 +121,7 @@ const UpdatePasswordForm: React.FC = () => {
size='md' size='md'
variant='filled' variant='filled'
placeholder='*******' placeholder='*******'
isInvalid={errors.confirm_password ? true : false} isInvalid={!!errors.confirm_password}
{...register('confirm_password')} {...register('confirm_password')}
/> />
<FormHelperText color={errors.confirm_password ? 'red' : 'gray.500'}> <FormHelperText color={errors.confirm_password ? 'red' : 'gray.500'}>
@ -116,7 +135,7 @@ const UpdatePasswordForm: React.FC = () => {
type='submit' type='submit'
size='md' size='md'
variant='outline' variant='outline'
isLoading={loading} isLoading={isLoading}
loadingText='Saving...' loadingText='Saving...'
> >
Change Change

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { Link as ReactRouterLink } from 'react-router-dom'; import { Link as ReactRouterLink } from 'react-router-dom';
import { Tag, TagLabel } from '@chakra-ui/react'; import { Tag, TagLabel, LinkBox, Heading, LinkOverlay } from '@chakra-ui/react';
import { Text, Link, Box, Flex } from '@chakra-ui/layout'; import { Text, Link, Box, Flex } from '@chakra-ui/layout';
import { MdVideoLibrary, MdGroup, MdThumbUp } from 'react-icons/md'; import { MdVideoLibrary, MdGroup, MdThumbUp } from 'react-icons/md';
@ -15,24 +15,12 @@ type CourseItemPropsType = {
const CourseItem: React.FC<CourseItemPropsType> = ({ course }) => { const CourseItem: React.FC<CourseItemPropsType> = ({ course }) => {
return ( return (
<Flex flexDir='column' borderWidth='1px' borderRadius='lg' p='10px' boxShadow='sm' mb='3'> <LinkBox as='div' p='5' borderWidth='1px' rounded='lg'>
<Tag colorScheme='purple' borderRadius='full' alignSelf='flex-start'> <Heading size='md' my='2'>
<TagLabel>{course.category.title}</TagLabel> <LinkOverlay href='#'>{course.title}</LinkOverlay>
</Tag> </Heading>
<Flex justifyContent='space-between' alignItems='center'>
<Link
as={ReactRouterLink}
to={`course/${course.id}`}
overflowWrap='anywhere'
fontWeight='bold'
fontSize='xl'
mr='5'
>
{course.title}
</Link>
</Flex>
<Text>{course.description}</Text> <Text>{course.description}</Text>
<Flex mt='5' alignItems='center'> <Flex mt='2' alignItems='center'>
<MdVideoLibrary /> <MdVideoLibrary />
<Text pl='1' pr='4'> <Text pl='1' pr='4'>
{course.lessons_count}&nbsp;lessons {course.lessons_count}&nbsp;lessons
@ -46,7 +34,40 @@ const CourseItem: React.FC<CourseItemPropsType> = ({ course }) => {
{course.likes_count}&nbsp;likes {course.likes_count}&nbsp;likes
</Text> </Text>
</Flex> </Flex>
</Flex> </LinkBox>
// <Flex flexDir='column' borderWidth='1px' borderRadius='lg' p='10px' boxShadow='sm' mb='3'>
// <Tag colorScheme='purple' borderRadius='full' alignSelf='flex-start'>
// <TagLabel>{course.category.title}</TagLabel>
// </Tag>
// <Flex justifyContent='space-between' alignItems='center'>
// <Link
// as={ReactRouterLink}
// to={`course/${course.id}`}
// overflowWrap='anywhere'
// fontWeight='bold'
// fontSize='xl'
// mr='5'
// >
// {course.title}
// </Link>
// </Flex>
// <Text>{course.description}</Text>
// <Flex mt='5' alignItems='center'>
// <MdVideoLibrary />
// <Text pl='1' pr='4'>
// {course.lessons_count}&nbsp;lessons
// </Text>
// <MdGroup />
// <Text pl='1' pr='4'>
// {course.students_count}&nbsp;students
// </Text>
// <MdThumbUp />
// <Text pl='1' pr='4'>
// {course.likes_count}&nbsp;likes
// </Text>
// </Flex>
// </Flex>
); );
}; };

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { Box, Text, Flex } from '@chakra-ui/layout'; import { Box, Text, Flex, Stack } from '@chakra-ui/layout';
/** /**
* Types * Types
@ -16,9 +16,7 @@ type CourseListPropsType = {
}; };
const CourseList: React.FC<CourseListPropsType> = ({ courses }) => { const CourseList: React.FC<CourseListPropsType> = ({ courses }) => {
if (courses === undefined) { if (courses === undefined) return null;
return null;
}
if (courses.length === 0) { if (courses.length === 0) {
return ( return (
@ -31,7 +29,7 @@ const CourseList: React.FC<CourseListPropsType> = ({ courses }) => {
} }
return ( return (
<Flex flexDirection='column'> <Flex flexDir='column'>
{courses.map(course => ( {courses.map(course => (
<CourseItem course={course} key={course.id} /> <CourseItem course={course} key={course.id} />
))} ))}

View File

@ -1,76 +0,0 @@
import React from 'react';
import { Box, Heading, Stack, Flex, StackDivider, Text, Button } from '@chakra-ui/react';
/**
* Types
*/
import { IUser } from '@educt/interfaces';
/**
* Contexts
*/
import { ProfilePageContext } from '@educt/contexts';
/**
* Hooks
*/
import { useContext } from 'react';
type UserAccountInfoPropsType = {
user: IUser;
};
const UserAccountInfo: React.FC<UserAccountInfoPropsType> = ({ user }) => {
const { setStatusPageView } = useContext(ProfilePageContext);
return (
<Box borderRadius='lg' borderWidth='1px' padding='20px' boxShadow='sm'>
<Stack spacing='10px' divider={<StackDivider />}>
<Flex>
<Box>
<Text as='small' color='gray.500'>
First Name
</Text>
<Text fontWeight='medium' fontSize='lg'>
{user.first_name}
</Text>
</Box>
</Flex>
<Flex>
<Box>
<Text as='small' color='gray.500'>
Last Name
</Text>
<Text fontWeight='medium' fontSize='lg'>
{user.last_name}
</Text>
</Box>
</Flex>
<Flex justifyContent='space-between' alignItems='center'>
<Box>
<Text as='small' color='gray.500'>
Email
</Text>
<Text fontWeight='medium' fontSize='lg'>
{user.email}
</Text>
</Box>
<Button onClick={() => setStatusPageView('update-email')}>Edit</Button>
</Flex>
<Flex justifyContent='space-between' alignItems='center'>
<Box>
<Text as='small' color='gray.500'>
Password
</Text>
<Text fontWeight='medium' fontSize='lg'>
**********
</Text>
</Box>
<Button onClick={() => setStatusPageView('update-password')}>Change</Button>
</Flex>
</Stack>
</Box>
);
};
export default UserAccountInfo;

View File

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

View File

@ -1,82 +0,0 @@
import React from 'react';
import { Box, Heading, Flex, Avatar, Text, Tabs, TabList, Tab, TabPanels, TabPanel, Button } from '@chakra-ui/react';
/**
* Components
*/
import UserBadge from '@educt/components/UserBadge';
import UserAccountInfo from '@educt/pages/profile/components/UserAccountInfo';
import UpdateUserContactsForm from '@educt/pages/profile/components/UpdateUserContactsForm';
import CourseList from '@educt/pages/profile/components/CourseList';
/**
* Hooks
*/
import useLogoutQuery from '@educt/hooks/useLogoutQuery';
import { useRootStore } from '@educt/hooks/useRootStore';
import { useColorMode } from '@chakra-ui/color-mode';
const MainContainer: React.FC = () => {
const {
userStore: { me },
} = useRootStore();
const { colorMode } = useColorMode();
const { logout } = useLogoutQuery();
if (me === null) return null;
return (
<Box maxW='900px'>
<Heading as='h1'>My account</Heading>
<Text mt='2'>Update you contacts information or password here.</Text>
<Flex
alignItems='center'
flexDir={{ base: 'column', md: 'row' }}
textAlign={{ base: 'center', md: 'left' }}
mt='20'
>
<Box>
<Avatar border={`4px solid ${colorMode === 'dark' ? '#2D3748' : '#E2E8F0'}`} size='2xl' name={me.fullname} />
</Box>
<Flex ml={{ md: '50' }} mt={{ base: '2', md: '0' }} flexDirection='column'>
<Heading as='h2'>{me.fullname}</Heading>
<Flex alignItems='center' flexDir={{ base: 'column', md: 'row' }}>
<Text mr='10px' mb={{ base: '2', md: '0' }}>
{me.email}
</Text>
<UserBadge roles={me.roles} />
</Flex>
</Flex>
</Flex>
<Box mt='10'>
<Tabs>
<TabList>
<Tab>Account information</Tab>
<Tab>My contacts</Tab>
{me.isStudent && <Tab>Available courses</Tab>}
</TabList>
<TabPanels mt='2'>
<TabPanel>
<UserAccountInfo user={me} />
</TabPanel>
<TabPanel>
<UpdateUserContactsForm contacts={me.contacts} />
</TabPanel>
{me.isStudent && (
<TabPanel>
<CourseList courses={me.courses} />
</TabPanel>
)}
</TabPanels>
</Tabs>
</Box>
<Box textAlign='center' mt='3'>
<Button onClick={logout} w={{ base: 'full', sm: '150px' }}>
Sign Out
</Button>
</Box>
</Box>
);
};
export default MainContainer;

View File

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

View File

@ -1,31 +0,0 @@
import { IconButton } from '@chakra-ui/button';
import { ChevronLeftIcon } from '@chakra-ui/icons';
import { Box, Flex, Heading, Text } from '@chakra-ui/layout';
import { ProfilePageContext } from '@educt/contexts';
import React, { useContext } from 'react';
import UpdateEmailForm from './UpdateEmailForm';
const UpdateEmailContainer: React.FC = () => {
const { setStatusPageView } = useContext(ProfilePageContext);
return (
<Box maxW='700px'>
<Flex alignItems='center'>
<IconButton
aria-label='Back'
borderRadius='full'
icon={<ChevronLeftIcon />}
onClick={() => setStatusPageView('default')}
mr='5'
/>
<Heading as='h1'>Edit email address</Heading>
</Flex>
<Box mt='3'>
<Text>Type your new email address.</Text>
</Box>
<UpdateEmailForm />
</Box>
);
};
export default UpdateEmailContainer;

View File

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

View File

@ -1,30 +0,0 @@
import React, { useContext } from 'react';
import { Box, Heading, Flex, Text, IconButton } from '@chakra-ui/react';
import { ChevronLeftIcon } from '@chakra-ui/icons';
import { ProfilePageContext } from '@educt/contexts';
import UpdatePasswordForm from './UpdatePasswordForm';
const UpdatePasswordContainer: React.FC = () => {
const { setStatusPageView } = useContext(ProfilePageContext);
return (
<Box maxW='700px'>
<Flex alignItems='center'>
<IconButton
aria-label='Back'
borderRadius='full'
icon={<ChevronLeftIcon />}
onClick={() => setStatusPageView('default')}
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

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

View File

@ -1,5 +1,7 @@
import React from 'react'; import React from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { Avatar, Tabs, TabList, Tab, TabPanels, TabPanel, Button } from '@chakra-ui/react';
import { Box, Flex, Heading, Text, Stack, StackDivider } from '@chakra-ui/layout';
/** /**
* Types * Types
@ -10,31 +12,17 @@ import { IPageProps } from '@educt/interfaces';
* Components * Components
*/ */
import LoadingPage from './components/LoadingPage'; import LoadingPage from './components/LoadingPage';
import UserBadge from '@educt/components/UserBadge';
/** import UpdateUserContactsForm from '@educt/pages/profile/components/UpdateUserContactsForm';
* Containers import CourseList from '@educt/pages/profile/components/CourseList';
*/
import MainContainer from './containers/MainContainer';
import UpdatePasswordContainer from './containers/UpdatePasswordContainer';
import UpdateEmailContainer from './containers/UpdateEmailContainer';
import ConfirmEmailContainer from './containers/ConfirmEmailContainer';
/** /**
* Hooks * Hooks
*/ */
import { useEffect } from 'react';
import { useRootStore } from '@educt/hooks/useRootStore';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { useColorMode } from '@chakra-ui/color-mode';
/** import { useRootStore } from '@educt/hooks/useRootStore';
* Contexts import useLogoutQuery from '@educt/hooks/useLogoutQuery';
*/
import { ProfilePageContext } from '@educt/contexts';
/**
* Providers
*/
import { ProfilePageContextProvider } from '@educt/providers';
/** /**
* Profile page * Profile page
@ -42,39 +30,109 @@ import { ProfilePageContextProvider } from '@educt/providers';
const ProfilePage: React.FC<IPageProps> = () => { const ProfilePage: React.FC<IPageProps> = () => {
const { const {
userStore: { me }, userStore: { me },
authStore,
} = useRootStore(); } = useRootStore();
const history = useHistory(); const history = useHistory();
const { logout } = useLogoutQuery();
const { colorMode } = useColorMode();
useEffect(() => { if (me === null) return <LoadingPage />;
if (!authStore.isLoggedIn) {
history.push('/auth');
}
});
if (me === null) {
return <LoadingPage />;
}
return ( return (
<ProfilePageContextProvider> <Box maxW='900px'>
<ProfilePageContext.Consumer> <Heading as='h1'>My account</Heading>
{({ statusPageView, pageData }) => { <Text mt='2'>Update you contacts information or password here.</Text>
switch (statusPageView) { <Flex
case 'default': alignItems='center'
return <MainContainer />; flexDir={{ base: 'column', md: 'row' }}
case 'update-password': textAlign={{ base: 'center', md: 'left' }}
return <UpdatePasswordContainer />; mt='20'
case 'update-email': >
return <UpdateEmailContainer />; <Box>
case 'confirm-email': <Avatar border={`4px solid ${colorMode === 'dark' ? '#2D3748' : '#E2E8F0'}`} size='2xl' name={me.fullname} />
return pageData.confirmEmailData && <ConfirmEmailContainer data={pageData.confirmEmailData} />; </Box>
default: <Flex ml={{ md: '50' }} mt={{ base: '2', md: '0' }} flexDirection='column'>
return null; <Heading as='h2'>{me.fullname}</Heading>
} <Flex alignItems='center' flexDir={{ base: 'column', md: 'row' }}>
}} <Text mr='10px' mb={{ base: '2', md: '0' }}>
</ProfilePageContext.Consumer> {me.email}
</ProfilePageContextProvider> </Text>
<UserBadge roles={me.roles} />
</Flex>
</Flex>
</Flex>
<Box mt='10'>
<Tabs>
<TabList>
<Tab>Account information</Tab>
<Tab>My contacts</Tab>
{me.isStudent && <Tab>Available courses</Tab>}
</TabList>
<TabPanels mt='2'>
<TabPanel>
<Box borderRadius='lg' borderWidth='1px' padding='20px' boxShadow='sm'>
<Stack spacing='10px' divider={<StackDivider />}>
<Flex>
<Box>
<Text as='small' color='gray.500'>
First Name
</Text>
<Text fontWeight='medium' fontSize='lg'>
{me.first_name}
</Text>
</Box>
</Flex>
<Flex>
<Box>
<Text as='small' color='gray.500'>
Last Name
</Text>
<Text fontWeight='medium' fontSize='lg'>
{me.last_name}
</Text>
</Box>
</Flex>
<Flex justifyContent='space-between' alignItems='center'>
<Box>
<Text as='small' color='gray.500'>
Email
</Text>
<Text fontWeight='medium' fontSize='lg'>
{me.email}
</Text>
</Box>
<Button onClick={() => history.push('/profile/change-email')}>Edit</Button>
</Flex>
<Flex justifyContent='space-between' alignItems='center'>
<Box>
<Text as='small' color='gray.500'>
Password
</Text>
<Text fontWeight='medium' fontSize='lg'>
**********
</Text>
</Box>
<Button onClick={() => history.push('/profile/change-password')}>Change</Button>
</Flex>
</Stack>
</Box>
</TabPanel>
<TabPanel>
<UpdateUserContactsForm contacts={me.contacts} />
</TabPanel>
{me.isStudent && (
<TabPanel>
<CourseList courses={me.courses} />
</TabPanel>
)}
</TabPanels>
</Tabs>
</Box>
<Box textAlign='center' mt='3'>
<Button onClick={logout} w={{ base: 'full', sm: '150px' }}>
Sign Out
</Button>
</Box>
</Box>
); );
}; };

View File

@ -1,11 +1,6 @@
import React, { useState, ReactNode } from 'react'; import React, { useState, ReactNode } from 'react';
import { CoursesPageContext, ProfilePageContext, StoreContext, UsersPageContext } from '@educt/contexts'; import { ChangeEmailPageContext, CoursesPageContext, StoreContext, UsersPageContext } from '@educt/contexts';
import { import { ConfirmEmailDataType, SearchingRoleStateType } from '@educt/types';
CoursesPageContextType,
ProfilePageDataType,
ProfilePageStatusType,
SearchingRoleStateType,
} from '@educt/types';
import RootStore from '@educt/stores/RootStore'; import RootStore from '@educt/stores/RootStore';
import { ICategory, ICourse, IUser } from '@educt/interfaces'; import { ICategory, ICourse, IUser } from '@educt/interfaces';
import { CourseStatusEnum } from '@educt/enums'; import { CourseStatusEnum } from '@educt/enums';
@ -20,16 +15,16 @@ export const RootStoreProvider = ({ children }: { children: ReactNode }) => {
}; };
/** /**
* Profile page context provider * Change email page context provider
*/ */
export const ProfilePageContextProvider: React.FC = ({ children }) => { export const ChangeEmailPageContextProvider: React.FC = ({ children }) => {
const [statusPageView, setStatusPageView] = useState<ProfilePageStatusType>('default'); const [isCodeSent, setIsCodeSent] = useState<boolean>(false);
const [pageData, setPageData] = useState<ProfilePageDataType>({}); const [confirmEmailData, setConfirmEmailData] = useState<ConfirmEmailDataType | undefined>(undefined);
return ( return (
<ProfilePageContext.Provider value={{ statusPageView, setStatusPageView, pageData, setPageData }}> <ChangeEmailPageContext.Provider value={{ isCodeSent, setIsCodeSent, confirmEmailData, setConfirmEmailData }}>
{children} {children}
</ProfilePageContext.Provider> </ChangeEmailPageContext.Provider>
); );
}; };

39
src/types/index.d.ts vendored
View File

@ -1,6 +1,6 @@
import React from 'react';
import { CourseStatusEnum, UserRoleEnum } from '@educt/enums'; import { CourseStatusEnum, UserRoleEnum } from '@educt/enums';
import { ICategory, ICourse, IUser } from '@educt/interfaces'; import { ICategory, ICourse, IUser } from '@educt/interfaces';
import React from 'react';
import { IconType } from 'react-icons/lib'; import { IconType } from 'react-icons/lib';
export type LinkType = { export type LinkType = {
@ -23,35 +23,32 @@ export type AttachmentFileType = {
extname: string; extname: string;
}; };
export type ConfirmEmailContainerDataType = {
newEmail: string;
expired_seconds: number;
};
export type OptionType = { export type OptionType = {
value: string; value: string;
label: string; label: string;
}; };
export type ProfilePageStatusType = 'update-password' | 'update-email' | 'confirm-email' | 'default'; export type ConfirmEmailContainerDataType = {
export type ProfilePageDataType = { newEmail: string;
/** expired_seconds: number;
* The field that is set when trying to change the mail address through form
*/
confirmEmailData?: ConfirmEmailContainerDataType;
}; };
export type ProfilePageContextType = {
/** /**
* Render current container by status * Change email page context
*/ */
statusPageView: ProfilePageStatusType; export type ConfirmEmailDataType = {
setStatusPageView: React.Dispatch<React.SetStateAction<ProfilePageStatusType>>; newEmail: string;
expired_seconds: number;
};
export type ChangeEmailPageContextType = {
isCodeSent: boolean;
setIsCodeSent: React.Dispatch<React.SetStateAction<boolean>>;
/** /**
* Some data for other components * Set this data when confirmation code has been sent
*/ */
pageData: ProfilePageDataType; confirmEmailData?: ConfirmEmailDataType;
setPageData: React.Dispatch<React.SetStateAction<ProfilePageDataType>>; setConfirmEmailData: React.Dispatch<React.SetStateAction<ConfirmEmailDataType | undefined>>;
}; };
export type SearchingRoleStateType = UserRoleEnum | 'any' | undefined; export type SearchingRoleStateType = UserRoleEnum | 'any' | undefined;