mirror of
https://github.com/sergeyyarkov/educt-client.git
synced 2026-10-11 18:29:15 +03:00
refactor: page elements layout
This commit is contained in:
parent
546a92bc8c
commit
9fb8b18b23
@ -1,9 +1,22 @@
|
||||
import React from 'react';
|
||||
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 => (
|
||||
<Button variant='outline' size='sm' leftIcon={<AddIcon />} {...props}>
|
||||
<Button variant='outline' size='sm' loadingText='Creating...' leftIcon={<AddIcon />} {...props}>
|
||||
Create new
|
||||
</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>
|
||||
);
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
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';
|
||||
|
||||
/**
|
||||
@ -9,7 +10,6 @@ import { MdError } from 'react-icons/md';
|
||||
import { useEffect } from 'react';
|
||||
import { useToast } from '@chakra-ui/toast';
|
||||
import { useRootStore } from '@educt/hooks/useRootStore';
|
||||
import { Redirect } from 'react-router';
|
||||
|
||||
const ErrorFallback: React.FC<{ error: any }> = props => {
|
||||
const { userStore, authStore } = useRootStore();
|
||||
@ -30,9 +30,7 @@ const ErrorFallback: React.FC<{ error: any }> = props => {
|
||||
|
||||
return (
|
||||
<Box textAlign='center' mt={40}>
|
||||
<Flex justifyContent='center'>
|
||||
<MdError size='64px' />
|
||||
</Flex>
|
||||
<Icon as={MdError} w='16' h='16' />
|
||||
<Heading display='block' as='h1' fontSize='6xl'>
|
||||
Something went wrong
|
||||
</Heading>
|
||||
|
||||
32
src/components/PageElements/index.tsx
Normal file
32
src/components/PageElements/index.tsx
Normal 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>
|
||||
);
|
||||
@ -1,38 +1,30 @@
|
||||
import { Badge } from '@chakra-ui/react';
|
||||
import { BadgeProps, Badge, Box } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import { UserRoleEnum } from '@educt/enums';
|
||||
import { IUserRole } from '@educt/interfaces';
|
||||
import { userHasRoles } from '@educt/helpers';
|
||||
|
||||
/**
|
||||
* Returns the badge for user based on roles
|
||||
*/
|
||||
const UserBadge: React.FC<{ roles: IUserRole[] }> = ({ roles }) => {
|
||||
export interface IUserBadgeProps extends BadgeProps {
|
||||
roles: IUserRole[];
|
||||
}
|
||||
|
||||
const UserBadge: React.FC<IUserBadgeProps> = props => {
|
||||
const { roles } = props;
|
||||
const getCurrentRole = () => {
|
||||
if (userHasRoles(roles, [UserRoleEnum.ADMIN])) return { name: 'Administrator', color: 'purple' };
|
||||
if (userHasRoles(roles, [UserRoleEnum.TEACHER])) return { name: 'Teacher', color: 'orange' };
|
||||
if (userHasRoles(roles, [UserRoleEnum.STUDENT])) return { name: 'Student', color: 'blue' };
|
||||
|
||||
/**
|
||||
* The user has no roles
|
||||
*/
|
||||
return { name: 'Guest', color: 'gray' };
|
||||
};
|
||||
|
||||
const role = getCurrentRole();
|
||||
|
||||
if (role !== undefined) {
|
||||
return (
|
||||
<Badge colorScheme={role.color} variant='subtle'>
|
||||
{role.name}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cannot find any role for user
|
||||
*/
|
||||
return null;
|
||||
return (
|
||||
<Badge colorScheme={role.color} variant='subtle' {...props}>
|
||||
{role.name}
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserBadge;
|
||||
|
||||
@ -43,6 +43,7 @@ export interface ILesson {
|
||||
description: string;
|
||||
duration: string;
|
||||
color?: ColorType | undefined | null;
|
||||
materials_count?: string | undefined;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
@ -1,18 +1,25 @@
|
||||
import React from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
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';
|
||||
|
||||
/**
|
||||
* Types
|
||||
*/
|
||||
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 toast = useToast();
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
toast({ title: `Page not found`, duration: 10000, isClosable: true, status: 'warning' });
|
||||
});
|
||||
|
||||
@ -21,18 +28,18 @@ const NotFoundPage: React.FC<IPageProps> = ({ title }) => {
|
||||
<Helmet>
|
||||
<title>{title}</title>
|
||||
</Helmet>
|
||||
<Box textAlign='center' mt={40}>
|
||||
<Flex justifyContent='center'>
|
||||
<MdError size='64px' />
|
||||
</Flex>
|
||||
<Heading display='block' as='h1' fontSize='6xl'>
|
||||
Page not found
|
||||
</Heading>
|
||||
<Text mt='5'>Sorry the page you are looking for could not be found.</Text>
|
||||
<Link as={ReactRouterLink} to='/' color='blue.500'>
|
||||
Return to main page
|
||||
</Link>
|
||||
</Box>
|
||||
<PageWrapper textAlign='center' mt={40}>
|
||||
<PageContent>
|
||||
<Icon as={MdError} w='16' h='16' />
|
||||
<Heading display='block' as='h1' fontSize='6xl'>
|
||||
Page not found
|
||||
</Heading>
|
||||
<Text mt='5'>Sorry the page you are looking for could not be found.</Text>
|
||||
<Link as={ReactRouterLink} to='/' color='blue.500'>
|
||||
Return to main page
|
||||
</Link>
|
||||
</PageContent>
|
||||
</PageWrapper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
import { observer } from 'mobx-react';
|
||||
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
|
||||
|
||||
@ -23,7 +23,7 @@ const CourseStatusTabs: React.FC<CourseStatusTabsProps> = () => {
|
||||
const handleChangeStatus = (status: CourseStatusEnum | undefined) => setCourseStatus(status);
|
||||
|
||||
return (
|
||||
<Tabs>
|
||||
<Tabs variant='solid-rounded'>
|
||||
<TabList justifyContent='center'>
|
||||
<Tab onClick={() => handleChangeStatus(undefined)}>All</Tab>
|
||||
<Tab onClick={() => handleChangeStatus(CourseStatusEnum.PUBLISHED)}>Published</Tab>
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Flex, Box, Heading, Text, Button } from '@chakra-ui/react';
|
||||
import { AddIcon } from '@chakra-ui/icons';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
|
||||
/**
|
||||
* Types
|
||||
@ -10,10 +9,13 @@ import { IPageProps } from '@educt/interfaces';
|
||||
/**
|
||||
* Components
|
||||
*/
|
||||
import { PageWrapper, PageHeading, PageContent } from '@educt/components/PageElements';
|
||||
import LoadingPage from '@educt/components/LoadingPage';
|
||||
import CourseList from './components/CourseList';
|
||||
import CategoryList from './components/CategoryList';
|
||||
import CourseStatusTabs from './components/CourseStatusTabs';
|
||||
import LoadingPage from '@educt/components/LoadingPage';
|
||||
import CategoryItem from './components/CategoryList/CategoryItem';
|
||||
import CourseItem from './components/CourseList/CourseItem';
|
||||
|
||||
/**
|
||||
* Providers
|
||||
@ -24,10 +26,6 @@ import { CoursesPageContextProvider } from '@educt/providers';
|
||||
* Hooks
|
||||
*/
|
||||
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
|
||||
@ -36,28 +34,21 @@ const CoursesPage: React.FC<IPageProps> = () => {
|
||||
const {
|
||||
userStore: { me },
|
||||
} = useRootStore();
|
||||
const history = useHistory();
|
||||
|
||||
if (me === null) return <LoadingPage />;
|
||||
|
||||
return (
|
||||
<CoursesPageContextProvider>
|
||||
<Box>
|
||||
<Flex alignItems='center' justifyContent='space-between'>
|
||||
<Box>
|
||||
<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'>
|
||||
<PageWrapper>
|
||||
<PageHeading heading='Courses' description='List of all courses' />
|
||||
<PageContent mt='10'>
|
||||
{(me.isAdmin || me.isTeacher) && <CourseStatusTabs />}
|
||||
<Box mt='5'>
|
||||
<CategoryList render={CategoryItem} />
|
||||
<CourseList render={CourseItem} />
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</PageContent>
|
||||
</PageWrapper>
|
||||
</CoursesPageContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@ -35,6 +35,7 @@ import { useErrorHandler } from 'react-error-boundary';
|
||||
* Services
|
||||
*/
|
||||
import { LessonServiceInstance } from '@educt/services';
|
||||
import { CreateButton } from '@educt/components/Buttons';
|
||||
|
||||
type LessonListPropsType = {
|
||||
course: Omit<ICourse, 'students_count' | 'likes_count' | 'lessons_count'>;
|
||||
@ -169,7 +170,7 @@ const LessonList: React.FC<LessonListPropsType> = ({ course }) => {
|
||||
<Icon as={MdAttachment} />
|
||||
</Text>
|
||||
<Text as='small' verticalAlign='middle'>
|
||||
4 attachments
|
||||
{lesson.materials_count} attachments
|
||||
</Text>
|
||||
</Text>
|
||||
</Flex>
|
||||
@ -205,10 +206,10 @@ const LessonList: React.FC<LessonListPropsType> = ({ course }) => {
|
||||
<DeleteLessonDialog lesson={deleting} isOpen={isOpenDeleteDialog} onClose={onCloseDeleteDialog} />
|
||||
)}
|
||||
<Flex mt='2' mb='3' padding='0 20px' alignItems='center' justifyContent='space-between'>
|
||||
<Text fontWeight='medium'>Total: ({lessons.length})</Text>
|
||||
<Button onClick={handleCreateLesson} variant='outline' colorScheme='blue'>
|
||||
Add new
|
||||
</Button>
|
||||
<Text fontSize='sm' color='gray.500'>
|
||||
{lessons.length} lessons
|
||||
</Text>
|
||||
<CreateButton onClick={handleCreateLesson} />
|
||||
</Flex>
|
||||
<DragDropContext onDragEnd={handleDragEnd}>
|
||||
<Droppable
|
||||
|
||||
@ -18,6 +18,7 @@ import type { ICourse } from '@educt/interfaces';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useDisclosure } from '@chakra-ui/react';
|
||||
import { AddIcon } from '@chakra-ui/icons';
|
||||
import { CreateButton } from '@educt/components/Buttons';
|
||||
|
||||
type StudentTableListPropsType = {
|
||||
render: React.FC<StudentTableRowPropsType>;
|
||||
@ -89,7 +90,7 @@ const StudentTableList: React.FC<StudentTableListPropsType> = ({ render: Row, st
|
||||
<AddStudentsModal isOpen={isOpenAddStudentModal} onClose={onCloseAddStudentModal} />
|
||||
<Flex justifyContent='space-between' flexDir={{ base: 'column', lg: 'row' }}>
|
||||
<Flex mb='2'>
|
||||
<InputGroup mr='2'>
|
||||
<InputGroup mr='2' borderRadius='lg'>
|
||||
<InputLeftElement pointerEvents='none' children={<MdSearch color='gray.300' />} />
|
||||
<Input value={search} size='sm' onChange={handleSearch} type='text' placeholder='Search for student...' />
|
||||
</InputGroup>
|
||||
@ -104,9 +105,7 @@ const StudentTableList: React.FC<StudentTableListPropsType> = ({ render: Row, st
|
||||
<BulkActionsMenu selected={selected} rows={rows} setRows={setRows} />
|
||||
</>
|
||||
)}
|
||||
<Button onClick={onOpenAddStudentModal} leftIcon={<AddIcon />} size='sm' variant='outline'>
|
||||
Add new
|
||||
</Button>
|
||||
<CreateButton onClick={onOpenAddStudentModal} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Table overflow='hidden' borderRadius='lg' mt='2'>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import moment from 'moment';
|
||||
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';
|
||||
|
||||
/**
|
||||
@ -14,6 +14,7 @@ import { TimeIcon } from '@chakra-ui/icons';
|
||||
* Hooks
|
||||
*/
|
||||
import { useColorModeValue } from '@chakra-ui/color-mode';
|
||||
import { MdOutlineVideoLibrary } from 'react-icons/md';
|
||||
|
||||
type CourseItemPropsType = {
|
||||
course: UserCourseType;
|
||||
@ -22,30 +23,11 @@ type CourseItemPropsType = {
|
||||
const CourseItem: React.FC<CourseItemPropsType> = ({ course }) => {
|
||||
return (
|
||||
<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'>
|
||||
<Text>
|
||||
<LinkOverlay
|
||||
as={ReactRouterLink}
|
||||
fontSize='xl'
|
||||
fontSize='md'
|
||||
fontWeight='medium'
|
||||
to={`/course/${course.id}`}
|
||||
overflowWrap='anywhere'
|
||||
@ -53,9 +35,22 @@ const CourseItem: React.FC<CourseItemPropsType> = ({ course }) => {
|
||||
{course.title}
|
||||
</LinkOverlay>
|
||||
</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 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' />
|
||||
<Box
|
||||
as='time'
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Box, Text, Flex, Stack } from '@chakra-ui/layout';
|
||||
import { Box, Text, Stack } from '@chakra-ui/layout';
|
||||
|
||||
/**
|
||||
* Types
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import CourseList from './CourseList';
|
||||
import CourseItem from './CourseItem';
|
||||
|
||||
export default CourseList;
|
||||
export { CourseList, CourseItem };
|
||||
|
||||
@ -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;
|
||||
@ -1,3 +0,0 @@
|
||||
import LoadingPage from './LoadingPage';
|
||||
|
||||
export default LoadingPage;
|
||||
@ -23,6 +23,7 @@ import { useErrorHandler } from 'react-error-boundary';
|
||||
* Schema
|
||||
*/
|
||||
import UpdateUserContactsSchema from './UpdateUserContactsForm.validator';
|
||||
import { SaveButton } from '@educt/components/Buttons';
|
||||
|
||||
type UpdateUserContactsInputType = {
|
||||
phone_number: string | null;
|
||||
@ -152,19 +153,7 @@ const UpdateUserContactsForm: React.FC<UpdateUserContactsFormPropsType> = ({ con
|
||||
</FormControl>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Button
|
||||
colorScheme='blue'
|
||||
mt='4'
|
||||
type='submit'
|
||||
size='md'
|
||||
variant='outline'
|
||||
isLoading={isLoading}
|
||||
loadingText='Saving...'
|
||||
rightIcon={<MdSave />}
|
||||
isDisabled={!isDirty}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<SaveButton type='submit' mt='4' isLoading={isLoading} rightIcon={<MdSave />} isDisabled={!isDirty} />
|
||||
</form>
|
||||
</Box>
|
||||
);
|
||||
|
||||
98
src/pages/profile/components/index.tsx
Normal file
98
src/pages/profile/components/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@ -13,7 +13,7 @@ import {
|
||||
InputGroup,
|
||||
InputRightElement,
|
||||
} from '@chakra-ui/react';
|
||||
import { Box, Flex, Heading, Text, Stack } from '@chakra-ui/layout';
|
||||
import { Box, Stack } from '@chakra-ui/layout';
|
||||
|
||||
/**
|
||||
* Types
|
||||
@ -23,18 +23,16 @@ import { IPageProps } from '@educt/interfaces';
|
||||
/**
|
||||
* Components
|
||||
*/
|
||||
import LoadingPage from './components/LoadingPage';
|
||||
import UserBadge from '@educt/components/UserBadge';
|
||||
import { PageHeading, PageContent, PageWrapper, PageFooter } from '@educt/components/PageElements';
|
||||
import { ProfileAvatar, ProfileBaseInfo, ProfileDescription, ProfileLoading, ProfileSignOutButton } from './components';
|
||||
import { CourseList } from './components/CourseList';
|
||||
import UpdateUserContactsForm from '@educt/pages/profile/components/UpdateUserContactsForm';
|
||||
import CourseList from '@educt/pages/profile/components/CourseList';
|
||||
|
||||
/**
|
||||
* Hooks
|
||||
*/
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useColorMode } from '@chakra-ui/color-mode';
|
||||
import { useRootStore } from '@educt/hooks/useRootStore';
|
||||
import { useLogout } from '@educt/hooks/queries';
|
||||
|
||||
/**
|
||||
* Profile page
|
||||
@ -44,35 +42,17 @@ const ProfilePage: React.FC<IPageProps> = () => {
|
||||
userStore: { me },
|
||||
} = useRootStore();
|
||||
const history = useHistory();
|
||||
const { logout } = useLogout();
|
||||
const { colorMode } = useColorMode();
|
||||
|
||||
if (me === null) return <LoadingPage />;
|
||||
if (me === null) return <ProfileLoading />;
|
||||
|
||||
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'>
|
||||
<PageWrapper maxW='900px'>
|
||||
<PageHeading heading='My account' description='Update you contacts information or password here.' />
|
||||
<ProfileBaseInfo mt='20'>
|
||||
<ProfileAvatar name={me.fullname} />
|
||||
<ProfileDescription fullname={me.fullname} email={me.email} roles={me.roles} />
|
||||
</ProfileBaseInfo>
|
||||
<PageContent>
|
||||
<Tabs>
|
||||
<TabList>
|
||||
<Tab>Account information</Tab>
|
||||
@ -82,7 +62,7 @@ const ProfilePage: React.FC<IPageProps> = () => {
|
||||
<TabPanels mt='2'>
|
||||
<TabPanel>
|
||||
<form>
|
||||
<Box borderRadius='lg' borderWidth='1px' padding='5'>
|
||||
<Box borderRadius='md' borderWidth='1px' padding='5'>
|
||||
<Stack spacing='5px'>
|
||||
<FormControl isDisabled>
|
||||
<FormHelperText color='gray.500'>First name</FormHelperText>
|
||||
@ -136,13 +116,11 @@ const ProfilePage: React.FC<IPageProps> = () => {
|
||||
)}
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</Box>
|
||||
<Box textAlign='center' mt='3'>
|
||||
<Button onClick={logout} size='sm' w={{ base: 'full', sm: '150px' }}>
|
||||
Sign Out
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</PageContent>
|
||||
<PageFooter textAlign='center'>
|
||||
<ProfileSignOutButton />
|
||||
</PageFooter>
|
||||
</PageWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -30,6 +30,8 @@ import { useErrorHandler } from 'react-error-boundary';
|
||||
import { useRootStore } from '@educt/hooks/useRootStore';
|
||||
import { useDisclosure } from '@chakra-ui/hooks';
|
||||
import { IUser } from '@educt/interfaces';
|
||||
import UserSearch from '../UserSearch';
|
||||
import CreateUserForm from '../CreateUserForm';
|
||||
|
||||
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} />}
|
||||
{users.length !== 0 ? (
|
||||
<>
|
||||
<Flex justifyContent='space-between' sx={{ gap: '10px' }} flexWrap='wrap'>
|
||||
<UserSearch />
|
||||
<CreateUserForm />
|
||||
</Flex>
|
||||
<Box>
|
||||
<Flex mt='5' p='0 10px' fontWeight='bold' alignItems='center' justifyContent='space-between'>
|
||||
<Text>Total: ({pagination.total})</Text>
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import UserList from './UserList';
|
||||
import UserItem from './UserItem';
|
||||
|
||||
export default UserList;
|
||||
export { UserList, UserItem };
|
||||
|
||||
@ -1,19 +1,16 @@
|
||||
import React from 'react';
|
||||
import { Box, Flex, Heading, Text } from '@chakra-ui/react';
|
||||
|
||||
/**
|
||||
* Types
|
||||
*/
|
||||
import { IPageProps } from '@educt/interfaces';
|
||||
import type { IPageProps } from '@educt/interfaces';
|
||||
|
||||
/**
|
||||
* Components
|
||||
*/
|
||||
import { PageContent, PageHeading, PageWrapper } from '@educt/components/PageElements';
|
||||
import { UserList, UserItem } from './components/UserList';
|
||||
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
|
||||
@ -36,18 +33,14 @@ const UsersPage: React.FC<IPageProps> = () => {
|
||||
if (me === null) return <LoadingPage />;
|
||||
|
||||
return (
|
||||
// TODO: delete page context for better performance
|
||||
<UsersPageContextProvider>
|
||||
<Box>
|
||||
<Heading as='h1'>User management</Heading>
|
||||
<Text mt='2'>You can add or delete users on this page.</Text>
|
||||
<Flex mt='5' flexDir='column' h='full'>
|
||||
<Flex justifyContent='space-between' sx={{ gap: '10px' }} flexWrap='wrap'>
|
||||
<UserSearch />
|
||||
<CreateUserForm />
|
||||
</Flex>
|
||||
<PageWrapper>
|
||||
<PageHeading heading='User management' description='You can add or delete users on this page.' />
|
||||
<PageContent mt='5'>
|
||||
<UserList render={UserItem} limit={6} />
|
||||
</Flex>
|
||||
</Box>
|
||||
</PageContent>
|
||||
</PageWrapper>
|
||||
</UsersPageContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@ -60,7 +60,7 @@ export default class UserStore {
|
||||
try {
|
||||
const result = await UserServiceInstance.fetchMe();
|
||||
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;
|
||||
|
||||
runInAction(() => {
|
||||
@ -76,6 +76,8 @@ export default class UserStore {
|
||||
isAdmin: helpers.userContainRoles(roles, [UserRoleEnum.ADMIN]),
|
||||
isTeacher: helpers.userContainRoles(roles, [UserRoleEnum.TEACHER]),
|
||||
isStudent: helpers.userContainRoles(roles, [UserRoleEnum.STUDENT]),
|
||||
created_at,
|
||||
updated_at,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user