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 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>
|
||||||
|
);
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
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 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' {...props}>
|
||||||
<Badge colorScheme={role.color} variant='subtle'>
|
{role.name}
|
||||||
{role.name}
|
</Badge>
|
||||||
</Badge>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cannot find any role for user
|
|
||||||
*/
|
|
||||||
return null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default UserBadge;
|
export default UserBadge;
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,18 +28,18 @@ 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>
|
<Text mt='5'>Sorry the page you are looking for could not be found.</Text>
|
||||||
<Text mt='5'>Sorry the page you are looking for could not be found.</Text>
|
<Link as={ReactRouterLink} to='/' color='blue.500'>
|
||||||
<Link as={ReactRouterLink} to='/' color='blue.500'>
|
Return to main page
|
||||||
Return to main page
|
</Link>
|
||||||
</Link>
|
</PageContent>
|
||||||
</Box>
|
</PageWrapper>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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'>
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
import CourseList from './CourseList';
|
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
|
* 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>
|
||||||
);
|
);
|
||||||
|
|||||||
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,
|
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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
import UserList from './UserList';
|
import UserList from './UserList';
|
||||||
|
import UserItem from './UserItem';
|
||||||
|
|
||||||
export default UserList;
|
export { UserList, UserItem };
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user