refactor: page elements layout

This commit is contained in:
Sergey Yarkov 2021-12-07 23:00:23 +03:00
parent 546a92bc8c
commit 9fb8b18b23
23 changed files with 264 additions and 194 deletions

View File

@ -1,9 +1,22 @@
import React from 'react'; import React from 'react';
import { ButtonProps, Button } from '@chakra-ui/button'; import { ButtonProps, Button } from '@chakra-ui/button';
import { AddIcon } from '@chakra-ui/icons'; import { AddIcon, EditIcon } from '@chakra-ui/icons';
import { MdSave } from 'react-icons/md';
export const CreateButton: React.FC<ButtonProps> = props => ( export const CreateButton: React.FC<ButtonProps> = props => (
<Button variant='outline' size='sm' leftIcon={<AddIcon />} {...props}> <Button variant='outline' size='sm' loadingText='Creating...' leftIcon={<AddIcon />} {...props}>
Create new Create new
</Button> </Button>
); );
export const EditButton: React.FC<ButtonProps> = props => (
<Button leftIcon={<EditIcon />} variant='outline' size='sm' {...props}>
Edit
</Button>
);
export const SaveButton: React.FC<ButtonProps> = props => (
<Button size='sm' variant='outline' loadingText='Saving...' rightIcon={<MdSave />} {...props}>
Save
</Button>
);

View File

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { Flex, Box, Heading, Text } from '@chakra-ui/react'; import { Redirect } from 'react-router';
import { Icon, Box, Heading, Text } from '@chakra-ui/react';
import { MdError } from 'react-icons/md'; import { MdError } from 'react-icons/md';
/** /**
@ -9,7 +10,6 @@ import { MdError } from 'react-icons/md';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useToast } from '@chakra-ui/toast'; import { useToast } from '@chakra-ui/toast';
import { useRootStore } from '@educt/hooks/useRootStore'; import { useRootStore } from '@educt/hooks/useRootStore';
import { Redirect } from 'react-router';
const ErrorFallback: React.FC<{ error: any }> = props => { const ErrorFallback: React.FC<{ error: any }> = props => {
const { userStore, authStore } = useRootStore(); const { userStore, authStore } = useRootStore();
@ -30,9 +30,7 @@ const ErrorFallback: React.FC<{ error: any }> = props => {
return ( return (
<Box textAlign='center' mt={40}> <Box textAlign='center' mt={40}>
<Flex justifyContent='center'> <Icon as={MdError} w='16' h='16' />
<MdError size='64px' />
</Flex>
<Heading display='block' as='h1' fontSize='6xl'> <Heading display='block' as='h1' fontSize='6xl'>
Something went wrong Something went wrong
</Heading> </Heading>

View File

@ -0,0 +1,32 @@
import React from 'react';
import { BoxProps, Box, Heading, Text, Divider } from '@chakra-ui/react';
interface IPageHeadingProps extends BoxProps {
heading: string;
description?: string;
}
export const PageWrapper: React.FC<BoxProps> = props => {
return <Box {...props}>{props.children}</Box>;
};
export const PageHeading: React.FC<IPageHeadingProps> = props => {
const { heading, description, ...boxProps } = props;
return (
<Box {...boxProps}>
<Heading as='h1'>{heading}</Heading>
{description && <Text mt='2'>{description}</Text>}
{props.children}
<Divider mt='5' />
</Box>
);
};
export const PageContent: React.FC<BoxProps> = props => <Box {...props}>{props.children}</Box>;
export const PageFooter: React.FC<BoxProps> = props => (
<Box mt='4' {...props}>
<Divider m='20px 0' />
{props.children}
</Box>
);

View File

@ -1,38 +1,30 @@
import { Badge } from '@chakra-ui/react'; import { BadgeProps, Badge, Box } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { UserRoleEnum } from '@educt/enums'; import { UserRoleEnum } from '@educt/enums';
import { IUserRole } from '@educt/interfaces'; import { IUserRole } from '@educt/interfaces';
import { userHasRoles } from '@educt/helpers'; import { userHasRoles } from '@educt/helpers';
/** export interface IUserBadgeProps extends BadgeProps {
* Returns the badge for user based on roles roles: IUserRole[];
*/ }
const UserBadge: React.FC<{ roles: IUserRole[] }> = ({ roles }) => {
const UserBadge: React.FC<IUserBadgeProps> = props => {
const { roles } = props;
const getCurrentRole = () => { const getCurrentRole = () => {
if (userHasRoles(roles, [UserRoleEnum.ADMIN])) return { name: 'Administrator', color: 'purple' }; if (userHasRoles(roles, [UserRoleEnum.ADMIN])) return { name: 'Administrator', color: 'purple' };
if (userHasRoles(roles, [UserRoleEnum.TEACHER])) return { name: 'Teacher', color: 'orange' }; if (userHasRoles(roles, [UserRoleEnum.TEACHER])) return { name: 'Teacher', color: 'orange' };
if (userHasRoles(roles, [UserRoleEnum.STUDENT])) return { name: 'Student', color: 'blue' }; if (userHasRoles(roles, [UserRoleEnum.STUDENT])) return { name: 'Student', color: 'blue' };
/**
* The user has no roles
*/
return { name: 'Guest', color: 'gray' }; return { name: 'Guest', color: 'gray' };
}; };
const role = getCurrentRole(); const role = getCurrentRole();
if (role !== undefined) {
return ( return (
<Badge colorScheme={role.color} variant='subtle'> <Badge colorScheme={role.color} variant='subtle' {...props}>
{role.name} {role.name}
</Badge> </Badge>
); );
}
/**
* Cannot find any role for user
*/
return null;
}; };
export default UserBadge; export default UserBadge;

View File

@ -43,6 +43,7 @@ export interface ILesson {
description: string; description: string;
duration: string; duration: string;
color?: ColorType | undefined | null; color?: ColorType | undefined | null;
materials_count?: string | undefined;
created_at: string; created_at: string;
updated_at: string; updated_at: string;
} }

View File

@ -1,18 +1,25 @@
import React from 'react'; import React from 'react';
import Helmet from 'react-helmet'; import Helmet from 'react-helmet';
import { Link as ReactRouterLink } from 'react-router-dom'; import { Link as ReactRouterLink } from 'react-router-dom';
import { Flex, Box, Heading, Text, useToast, Link } from '@chakra-ui/react'; import { Flex, Heading, Text, Link, Icon } from '@chakra-ui/react';
import { MdError } from 'react-icons/md'; import { MdError } from 'react-icons/md';
/** /**
* Types * Types
*/ */
import { IPageProps } from '@educt/interfaces'; import { IPageProps } from '@educt/interfaces';
import { PageContent, PageWrapper } from '@educt/components/PageElements';
/**
* Hooks
*/
import { useEffect } from 'react';
import { useToast } from '@chakra-ui/react';
const NotFoundPage: React.FC<IPageProps> = ({ title }) => { const NotFoundPage: React.FC<IPageProps> = ({ title }) => {
const toast = useToast(); const toast = useToast();
React.useEffect(() => { useEffect(() => {
toast({ title: `Page not found`, duration: 10000, isClosable: true, status: 'warning' }); toast({ title: `Page not found`, duration: 10000, isClosable: true, status: 'warning' });
}); });
@ -21,10 +28,9 @@ const NotFoundPage: React.FC<IPageProps> = ({ title }) => {
<Helmet> <Helmet>
<title>{title}</title> <title>{title}</title>
</Helmet> </Helmet>
<Box textAlign='center' mt={40}> <PageWrapper textAlign='center' mt={40}>
<Flex justifyContent='center'> <PageContent>
<MdError size='64px' /> <Icon as={MdError} w='16' h='16' />
</Flex>
<Heading display='block' as='h1' fontSize='6xl'> <Heading display='block' as='h1' fontSize='6xl'>
Page not found Page not found
</Heading> </Heading>
@ -32,7 +38,8 @@ const NotFoundPage: React.FC<IPageProps> = ({ title }) => {
<Link as={ReactRouterLink} to='/' color='blue.500'> <Link as={ReactRouterLink} to='/' color='blue.500'>
Return to main page Return to main page
</Link> </Link>
</Box> </PageContent>
</PageWrapper>
</> </>
); );
}; };

View File

@ -1,8 +1,8 @@
import React from 'react'; import React from 'react';
import Helmet from 'react-helmet'; import Helmet from 'react-helmet';
import { observer } from 'mobx-react';
import { Redirect } from 'react-router-dom'; import { Redirect } from 'react-router-dom';
import { Box, Heading, Text, Link } from '@chakra-ui/react'; import { observer } from 'mobx-react';
import { Box, Heading } from '@chakra-ui/react';
/** /**
* Types * Types

View File

@ -23,7 +23,7 @@ const CourseStatusTabs: React.FC<CourseStatusTabsProps> = () => {
const handleChangeStatus = (status: CourseStatusEnum | undefined) => setCourseStatus(status); const handleChangeStatus = (status: CourseStatusEnum | undefined) => setCourseStatus(status);
return ( return (
<Tabs> <Tabs variant='solid-rounded'>
<TabList justifyContent='center'> <TabList justifyContent='center'>
<Tab onClick={() => handleChangeStatus(undefined)}>All</Tab> <Tab onClick={() => handleChangeStatus(undefined)}>All</Tab>
<Tab onClick={() => handleChangeStatus(CourseStatusEnum.PUBLISHED)}>Published</Tab> <Tab onClick={() => handleChangeStatus(CourseStatusEnum.PUBLISHED)}>Published</Tab>

View File

@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import { Flex, Box, Heading, Text, Button } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import { AddIcon } from '@chakra-ui/icons';
/** /**
* Types * Types
@ -10,10 +9,13 @@ import { IPageProps } from '@educt/interfaces';
/** /**
* Components * Components
*/ */
import { PageWrapper, PageHeading, PageContent } from '@educt/components/PageElements';
import LoadingPage from '@educt/components/LoadingPage';
import CourseList from './components/CourseList'; import CourseList from './components/CourseList';
import CategoryList from './components/CategoryList'; import CategoryList from './components/CategoryList';
import CourseStatusTabs from './components/CourseStatusTabs'; import CourseStatusTabs from './components/CourseStatusTabs';
import LoadingPage from '@educt/components/LoadingPage'; import CategoryItem from './components/CategoryList/CategoryItem';
import CourseItem from './components/CourseList/CourseItem';
/** /**
* Providers * Providers
@ -24,10 +26,6 @@ import { CoursesPageContextProvider } from '@educt/providers';
* Hooks * Hooks
*/ */
import { useRootStore } from '@educt/hooks/useRootStore'; import { useRootStore } from '@educt/hooks/useRootStore';
import { useHistory } from 'react-router';
import CategoryItem from './components/CategoryList/CategoryItem';
import CourseItem from './components/CourseList/CourseItem';
import { CreateButton } from '@educt/components/Buttons';
/** /**
* Courses page * Courses page
@ -36,28 +34,21 @@ const CoursesPage: React.FC<IPageProps> = () => {
const { const {
userStore: { me }, userStore: { me },
} = useRootStore(); } = useRootStore();
const history = useHistory();
if (me === null) return <LoadingPage />; if (me === null) return <LoadingPage />;
return ( return (
<CoursesPageContextProvider> <CoursesPageContextProvider>
<Box> <PageWrapper>
<Flex alignItems='center' justifyContent='space-between'> <PageHeading heading='Courses' description='List of all courses' />
<Box> <PageContent mt='10'>
<Heading as='h1'>Courses</Heading>
<Text mt='2'>List of all courses.</Text>
</Box>
{(me.isAdmin || me.isTeacher) && <CreateButton onClick={() => history.push('/courses/create')} />}
</Flex>
<Box mt='7'>
{(me.isAdmin || me.isTeacher) && <CourseStatusTabs />} {(me.isAdmin || me.isTeacher) && <CourseStatusTabs />}
<Box mt='5'> <Box mt='5'>
<CategoryList render={CategoryItem} /> <CategoryList render={CategoryItem} />
<CourseList render={CourseItem} /> <CourseList render={CourseItem} />
</Box> </Box>
</Box> </PageContent>
</Box> </PageWrapper>
</CoursesPageContextProvider> </CoursesPageContextProvider>
); );
}; };

View File

@ -35,6 +35,7 @@ import { useErrorHandler } from 'react-error-boundary';
* Services * Services
*/ */
import { LessonServiceInstance } from '@educt/services'; import { LessonServiceInstance } from '@educt/services';
import { CreateButton } from '@educt/components/Buttons';
type LessonListPropsType = { type LessonListPropsType = {
course: Omit<ICourse, 'students_count' | 'likes_count' | 'lessons_count'>; course: Omit<ICourse, 'students_count' | 'likes_count' | 'lessons_count'>;
@ -169,7 +170,7 @@ const LessonList: React.FC<LessonListPropsType> = ({ course }) => {
<Icon as={MdAttachment} /> <Icon as={MdAttachment} />
</Text> </Text>
<Text as='small' verticalAlign='middle'> <Text as='small' verticalAlign='middle'>
4 attachments {lesson.materials_count} attachments
</Text> </Text>
</Text> </Text>
</Flex> </Flex>
@ -205,10 +206,10 @@ const LessonList: React.FC<LessonListPropsType> = ({ course }) => {
<DeleteLessonDialog lesson={deleting} isOpen={isOpenDeleteDialog} onClose={onCloseDeleteDialog} /> <DeleteLessonDialog lesson={deleting} isOpen={isOpenDeleteDialog} onClose={onCloseDeleteDialog} />
)} )}
<Flex mt='2' mb='3' padding='0 20px' alignItems='center' justifyContent='space-between'> <Flex mt='2' mb='3' padding='0 20px' alignItems='center' justifyContent='space-between'>
<Text fontWeight='medium'>Total: ({lessons.length})</Text> <Text fontSize='sm' color='gray.500'>
<Button onClick={handleCreateLesson} variant='outline' colorScheme='blue'> {lessons.length} lessons
Add new </Text>
</Button> <CreateButton onClick={handleCreateLesson} />
</Flex> </Flex>
<DragDropContext onDragEnd={handleDragEnd}> <DragDropContext onDragEnd={handleDragEnd}>
<Droppable <Droppable

View File

@ -18,6 +18,7 @@ import type { ICourse } from '@educt/interfaces';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { useDisclosure } from '@chakra-ui/react'; import { useDisclosure } from '@chakra-ui/react';
import { AddIcon } from '@chakra-ui/icons'; import { AddIcon } from '@chakra-ui/icons';
import { CreateButton } from '@educt/components/Buttons';
type StudentTableListPropsType = { type StudentTableListPropsType = {
render: React.FC<StudentTableRowPropsType>; render: React.FC<StudentTableRowPropsType>;
@ -89,7 +90,7 @@ const StudentTableList: React.FC<StudentTableListPropsType> = ({ render: Row, st
<AddStudentsModal isOpen={isOpenAddStudentModal} onClose={onCloseAddStudentModal} /> <AddStudentsModal isOpen={isOpenAddStudentModal} onClose={onCloseAddStudentModal} />
<Flex justifyContent='space-between' flexDir={{ base: 'column', lg: 'row' }}> <Flex justifyContent='space-between' flexDir={{ base: 'column', lg: 'row' }}>
<Flex mb='2'> <Flex mb='2'>
<InputGroup mr='2'> <InputGroup mr='2' borderRadius='lg'>
<InputLeftElement pointerEvents='none' children={<MdSearch color='gray.300' />} /> <InputLeftElement pointerEvents='none' children={<MdSearch color='gray.300' />} />
<Input value={search} size='sm' onChange={handleSearch} type='text' placeholder='Search for student...' /> <Input value={search} size='sm' onChange={handleSearch} type='text' placeholder='Search for student...' />
</InputGroup> </InputGroup>
@ -104,9 +105,7 @@ const StudentTableList: React.FC<StudentTableListPropsType> = ({ render: Row, st
<BulkActionsMenu selected={selected} rows={rows} setRows={setRows} /> <BulkActionsMenu selected={selected} rows={rows} setRows={setRows} />
</> </>
)} )}
<Button onClick={onOpenAddStudentModal} leftIcon={<AddIcon />} size='sm' variant='outline'> <CreateButton onClick={onOpenAddStudentModal} />
Add new
</Button>
</Flex> </Flex>
</Flex> </Flex>
<Table overflow='hidden' borderRadius='lg' mt='2'> <Table overflow='hidden' borderRadius='lg' mt='2'>

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import moment from 'moment'; import moment from 'moment';
import { Link as ReactRouterLink } from 'react-router-dom'; import { Link as ReactRouterLink } from 'react-router-dom';
import { Tag, TagLabel, LinkBox, Text, LinkOverlay, Icon, Link } from '@chakra-ui/react'; import { Tag, TagLabel, LinkBox, Text, LinkOverlay, Icon } from '@chakra-ui/react';
import { Box, Flex } from '@chakra-ui/layout'; import { Box, Flex } from '@chakra-ui/layout';
/** /**
@ -14,6 +14,7 @@ import { TimeIcon } from '@chakra-ui/icons';
* Hooks * Hooks
*/ */
import { useColorModeValue } from '@chakra-ui/color-mode'; import { useColorModeValue } from '@chakra-ui/color-mode';
import { MdOutlineVideoLibrary } from 'react-icons/md';
type CourseItemPropsType = { type CourseItemPropsType = {
course: UserCourseType; course: UserCourseType;
@ -22,30 +23,11 @@ type CourseItemPropsType = {
const CourseItem: React.FC<CourseItemPropsType> = ({ course }) => { const CourseItem: React.FC<CourseItemPropsType> = ({ course }) => {
return ( return (
<LinkBox as='div' p='3' borderBottomWidth='1px' borderRadius='lg' bg={useColorModeValue('gray.50', 'gray.700')}> <LinkBox as='div' p='3' borderBottomWidth='1px' borderRadius='lg' bg={useColorModeValue('gray.50', 'gray.700')}>
<Flex justifyContent='space-between'>
<Box>
<Tag variant='outline' borderRadius='full' alignSelf='flex-start'>
<TagLabel>{course.lessons_count} lessons</TagLabel>
</Tag>
</Box>
<Box>
<Tag
color='white'
boxShadow={`inset 0 0 0px 1px ${course.category.color?.hex}`}
bg={course.category.color?.hex}
borderRadius='full'
alignSelf='flex-start'
>
<TagLabel>{course.category.title}</TagLabel>
</Tag>
</Box>
</Flex>
<Flex justifyContent='space-between' mt='2'> <Flex justifyContent='space-between' mt='2'>
<Text> <Text>
<LinkOverlay <LinkOverlay
as={ReactRouterLink} as={ReactRouterLink}
fontSize='xl' fontSize='md'
fontWeight='medium' fontWeight='medium'
to={`/course/${course.id}`} to={`/course/${course.id}`}
overflowWrap='anywhere' overflowWrap='anywhere'
@ -53,9 +35,22 @@ const CourseItem: React.FC<CourseItemPropsType> = ({ course }) => {
{course.title} {course.title}
</LinkOverlay> </LinkOverlay>
</Text> </Text>
<Box>
<Tag
color='white'
boxShadow={`inset 0 0 0px 1px ${course.category.color?.hex}`}
bg={course.category.color?.hex}
alignSelf='flex-start'
variant='outline'
>
<TagLabel>{course.category.title}</TagLabel>
</Tag>
</Box>
</Flex> </Flex>
<Flex mt='2' alignItems='center' sx={{ gap: '9px' }}> <Flex mt='2' alignItems='center' fontSize='sm' sx={{ gap: '9px' }}>
<Icon as={MdOutlineVideoLibrary} w='14px' h='14px' />
<Text mr='1'>{course.lessons_count} lessons</Text>
<Icon as={TimeIcon} w='14px' h='14px' /> <Icon as={TimeIcon} w='14px' h='14px' />
<Box <Box
as='time' as='time'

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { Box, Text, Flex, Stack } from '@chakra-ui/layout'; import { Box, Text, Stack } from '@chakra-ui/layout';
/** /**
* Types * Types

View File

@ -1,3 +1,4 @@
import CourseList from './CourseList'; import CourseList from './CourseList';
import CourseItem from './CourseItem';
export default CourseList; export { CourseList, CourseItem };

View File

@ -1,24 +0,0 @@
import React from 'react';
import { Box, Skeleton, SkeletonCircle, Flex, Spinner } from '@chakra-ui/react';
const LoadingPage: React.FC = () => {
return (
<Box maxW='900px'>
<Skeleton height='35px' width='250px' borderRadius='md' />
<Flex alignItems='center' mt='20'>
<Box>
<SkeletonCircle size='128px' />
</Box>
<Flex ml='50' flexDirection='column'>
<Skeleton height='25px' width='200px' mb='5' borderRadius='md' />
<Skeleton height='25px' width='300px' borderRadius='md' />
</Flex>
</Flex>
<Flex mt='20' justifyContent='center'>
<Spinner size='xl' />
</Flex>
</Box>
);
};
export default LoadingPage;

View File

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

View File

@ -23,6 +23,7 @@ import { useErrorHandler } from 'react-error-boundary';
* Schema * Schema
*/ */
import UpdateUserContactsSchema from './UpdateUserContactsForm.validator'; import UpdateUserContactsSchema from './UpdateUserContactsForm.validator';
import { SaveButton } from '@educt/components/Buttons';
type UpdateUserContactsInputType = { type UpdateUserContactsInputType = {
phone_number: string | null; phone_number: string | null;
@ -152,19 +153,7 @@ const UpdateUserContactsForm: React.FC<UpdateUserContactsFormPropsType> = ({ con
</FormControl> </FormControl>
</Stack> </Stack>
</Box> </Box>
<Button <SaveButton type='submit' mt='4' isLoading={isLoading} rightIcon={<MdSave />} isDisabled={!isDirty} />
colorScheme='blue'
mt='4'
type='submit'
size='md'
variant='outline'
isLoading={isLoading}
loadingText='Saving...'
rightIcon={<MdSave />}
isDisabled={!isDirty}
>
Save
</Button>
</form> </form>
</Box> </Box>
); );

View File

@ -0,0 +1,98 @@
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<FlexProps> = props => {
return (
<Flex flexDir={{ base: 'column', md: 'row' }} textAlign={{ base: 'center', md: 'left' }} {...props}>
{props.children}
</Flex>
);
};
interface IProfileAvatarProps extends BoxProps {
name: string;
}
export const ProfileAvatar: React.FC<IProfileAvatarProps> = props => {
const { colorMode } = useColorMode();
const isDark = colorMode === 'dark';
return (
<Box>
<Avatar border={`4px solid ${isDark ? '#2D3748' : '#E2E8F0'}`} size='2xl' name={props.name} />
</Box>
);
};
interface IProfileDescriptionProps extends BoxProps {
fullname: string;
email: string;
roles: IUserRole[];
}
export const ProfileDescription: React.FC<IProfileDescriptionProps> = props => {
const { fullname, email, roles, ...boxProps } = props;
return (
<Box w='full' ml={{ md: '50' }} mt={{ base: '2', md: '3' }} {...boxProps}>
<Flex alignItems='center' justifyContent='space-between'>
<Box>
<Heading lineHeight='1.5rem' fontWeight='bold' fontSize='24px'>
{fullname}
<br />
<Text as='span' fontSize='sm' fontWeight='normal'>
{email}
</Text>
</Heading>
<UserBadge mt='2' roles={roles} />
</Box>
</Flex>
</Box>
);
};
export const ProfileSignOutButton: React.FC<ButtonProps> = props => {
const { logout } = useLogout();
return (
<Button onClick={logout} size='sm' w={{ base: 'full', sm: '150px' }}>
Sign Out
</Button>
);
};
export const ProfileLoading: React.FC<BoxProps> = props => {
return (
<Box maxW='900px' {...props}>
<Skeleton height='35px' width='250px' borderRadius='md' />
<Flex alignItems='center' mt='20'>
<Box>
<SkeletonCircle size='128px' />
</Box>
<Flex ml='50' flexDirection='column'>
<Skeleton height='25px' width='200px' mb='5' borderRadius='md' />
<Skeleton height='25px' width='300px' borderRadius='md' />
</Flex>
</Flex>
<Flex mt='20' justifyContent='center'>
<Spinner size='xl' />
</Flex>
</Box>
);
};

View File

@ -13,7 +13,7 @@ import {
InputGroup, InputGroup,
InputRightElement, InputRightElement,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { Box, Flex, Heading, Text, Stack } from '@chakra-ui/layout'; import { Box, Stack } from '@chakra-ui/layout';
/** /**
* Types * Types
@ -23,18 +23,16 @@ import { IPageProps } from '@educt/interfaces';
/** /**
* Components * Components
*/ */
import LoadingPage from './components/LoadingPage'; import { PageHeading, PageContent, PageWrapper, PageFooter } from '@educt/components/PageElements';
import UserBadge from '@educt/components/UserBadge'; import { ProfileAvatar, ProfileBaseInfo, ProfileDescription, ProfileLoading, ProfileSignOutButton } from './components';
import { CourseList } from './components/CourseList';
import UpdateUserContactsForm from '@educt/pages/profile/components/UpdateUserContactsForm'; import UpdateUserContactsForm from '@educt/pages/profile/components/UpdateUserContactsForm';
import CourseList from '@educt/pages/profile/components/CourseList';
/** /**
* Hooks * Hooks
*/ */
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'; import { useRootStore } from '@educt/hooks/useRootStore';
import { useLogout } from '@educt/hooks/queries';
/** /**
* Profile page * Profile page
@ -44,35 +42,17 @@ const ProfilePage: React.FC<IPageProps> = () => {
userStore: { me }, userStore: { me },
} = useRootStore(); } = useRootStore();
const history = useHistory(); const history = useHistory();
const { logout } = useLogout();
const { colorMode } = useColorMode();
if (me === null) return <LoadingPage />; if (me === null) return <ProfileLoading />;
return ( return (
<Box maxW='900px'> <PageWrapper maxW='900px'>
<Heading as='h1'>My account</Heading> <PageHeading heading='My account' description='Update you contacts information or password here.' />
<Text mt='2'>Update you contacts information or password here.</Text> <ProfileBaseInfo mt='20'>
<Flex <ProfileAvatar name={me.fullname} />
alignItems='center' <ProfileDescription fullname={me.fullname} email={me.email} roles={me.roles} />
flexDir={{ base: 'column', md: 'row' }} </ProfileBaseInfo>
textAlign={{ base: 'center', md: 'left' }} <PageContent>
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> <Tabs>
<TabList> <TabList>
<Tab>Account information</Tab> <Tab>Account information</Tab>
@ -82,7 +62,7 @@ const ProfilePage: React.FC<IPageProps> = () => {
<TabPanels mt='2'> <TabPanels mt='2'>
<TabPanel> <TabPanel>
<form> <form>
<Box borderRadius='lg' borderWidth='1px' padding='5'> <Box borderRadius='md' borderWidth='1px' padding='5'>
<Stack spacing='5px'> <Stack spacing='5px'>
<FormControl isDisabled> <FormControl isDisabled>
<FormHelperText color='gray.500'>First name</FormHelperText> <FormHelperText color='gray.500'>First name</FormHelperText>
@ -136,13 +116,11 @@ const ProfilePage: React.FC<IPageProps> = () => {
)} )}
</TabPanels> </TabPanels>
</Tabs> </Tabs>
</Box> </PageContent>
<Box textAlign='center' mt='3'> <PageFooter textAlign='center'>
<Button onClick={logout} size='sm' w={{ base: 'full', sm: '150px' }}> <ProfileSignOutButton />
Sign Out </PageFooter>
</Button> </PageWrapper>
</Box>
</Box>
); );
}; };

View File

@ -30,6 +30,8 @@ import { useErrorHandler } from 'react-error-boundary';
import { useRootStore } from '@educt/hooks/useRootStore'; import { useRootStore } from '@educt/hooks/useRootStore';
import { useDisclosure } from '@chakra-ui/hooks'; import { useDisclosure } from '@chakra-ui/hooks';
import { IUser } from '@educt/interfaces'; import { IUser } from '@educt/interfaces';
import UserSearch from '../UserSearch';
import CreateUserForm from '../CreateUserForm';
type UserListPropsType = { render: React.FC<UserItemPropsType>; limit?: number }; type UserListPropsType = { render: React.FC<UserItemPropsType>; limit?: number };
@ -76,6 +78,10 @@ const UserList: React.FC<UserListPropsType> = ({ render: Item, limit }) => {
{deleting && <DeleteUserDialog user={deleting} isOpen={isOpenDeleteDialog} onClose={onCloseDeleteDialog} />} {deleting && <DeleteUserDialog user={deleting} isOpen={isOpenDeleteDialog} onClose={onCloseDeleteDialog} />}
{users.length !== 0 ? ( {users.length !== 0 ? (
<> <>
<Flex justifyContent='space-between' sx={{ gap: '10px' }} flexWrap='wrap'>
<UserSearch />
<CreateUserForm />
</Flex>
<Box> <Box>
<Flex mt='5' p='0 10px' fontWeight='bold' alignItems='center' justifyContent='space-between'> <Flex mt='5' p='0 10px' fontWeight='bold' alignItems='center' justifyContent='space-between'>
<Text>Total: ({pagination.total})</Text> <Text>Total: ({pagination.total})</Text>

View File

@ -1,3 +1,4 @@
import UserList from './UserList'; import UserList from './UserList';
import UserItem from './UserItem';
export default UserList; export { UserList, UserItem };

View File

@ -1,19 +1,16 @@
import React from 'react'; import React from 'react';
import { Box, Flex, Heading, Text } from '@chakra-ui/react';
/** /**
* Types * Types
*/ */
import { IPageProps } from '@educt/interfaces'; import type { IPageProps } from '@educt/interfaces';
/** /**
* Components * Components
*/ */
import { PageContent, PageHeading, PageWrapper } from '@educt/components/PageElements';
import { UserList, UserItem } from './components/UserList';
import LoadingPage from '@educt/components/LoadingPage'; import LoadingPage from '@educt/components/LoadingPage';
import UserSearch from './components/UserSearch';
import UserList from './components/UserList';
import UserItem from './components/UserList/UserItem';
import CreateUserForm from './components/CreateUserForm';
/** /**
* Hooks * Hooks
@ -36,18 +33,14 @@ const UsersPage: React.FC<IPageProps> = () => {
if (me === null) return <LoadingPage />; if (me === null) return <LoadingPage />;
return ( return (
// TODO: delete page context for better performance
<UsersPageContextProvider> <UsersPageContextProvider>
<Box> <PageWrapper>
<Heading as='h1'>User management</Heading> <PageHeading heading='User management' description='You can add or delete users on this page.' />
<Text mt='2'>You can add or delete users on this page.</Text> <PageContent mt='5'>
<Flex mt='5' flexDir='column' h='full'>
<Flex justifyContent='space-between' sx={{ gap: '10px' }} flexWrap='wrap'>
<UserSearch />
<CreateUserForm />
</Flex>
<UserList render={UserItem} limit={6} /> <UserList render={UserItem} limit={6} />
</Flex> </PageContent>
</Box> </PageWrapper>
</UsersPageContextProvider> </UsersPageContextProvider>
); );
}; };

View File

@ -60,7 +60,7 @@ export default class UserStore {
try { try {
const result = await UserServiceInstance.fetchMe(); const result = await UserServiceInstance.fetchMe();
const { const {
data: { id, first_name, last_name, fullname, email, roles, contacts, courses }, data: { id, first_name, last_name, fullname, email, roles, contacts, courses, created_at, updated_at },
} = result; } = result;
runInAction(() => { runInAction(() => {
@ -76,6 +76,8 @@ export default class UserStore {
isAdmin: helpers.userContainRoles(roles, [UserRoleEnum.ADMIN]), isAdmin: helpers.userContainRoles(roles, [UserRoleEnum.ADMIN]),
isTeacher: helpers.userContainRoles(roles, [UserRoleEnum.TEACHER]), isTeacher: helpers.userContainRoles(roles, [UserRoleEnum.TEACHER]),
isStudent: helpers.userContainRoles(roles, [UserRoleEnum.STUDENT]), isStudent: helpers.userContainRoles(roles, [UserRoleEnum.STUDENT]),
created_at,
updated_at,
}; };
}); });