feat: added courses page store

This commit is contained in:
Sergey Yarkov 2021-12-08 20:47:06 +03:00
parent 9fb8b18b23
commit d1d345ca88
13 changed files with 168 additions and 130 deletions

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { ButtonProps, Button } from '@chakra-ui/button'; import { ButtonProps, Button } from '@chakra-ui/button';
import { AddIcon, EditIcon } from '@chakra-ui/icons'; import { AddIcon, DeleteIcon, EditIcon } from '@chakra-ui/icons';
import { MdSave } from 'react-icons/md'; import { MdSave } from 'react-icons/md';
export const CreateButton: React.FC<ButtonProps> = props => ( export const CreateButton: React.FC<ButtonProps> = props => (
@ -9,6 +9,12 @@ export const CreateButton: React.FC<ButtonProps> = props => (
</Button> </Button>
); );
export const DeleteButton: React.FC<ButtonProps> = props => (
<Button variant='outline' leftIcon={<DeleteIcon />} colorScheme='red' size='sm' {...props}>
Delete
</Button>
);
export const EditButton: React.FC<ButtonProps> = props => ( export const EditButton: React.FC<ButtonProps> = props => (
<Button leftIcon={<EditIcon />} variant='outline' size='sm' {...props}> <Button leftIcon={<EditIcon />} variant='outline' size='sm' {...props}>
Edit Edit

View File

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

View File

@ -15,19 +15,23 @@ import { CoursesPageContext } from '@educt/contexts';
* Hooks * Hooks
*/ */
import { useContext } from 'react'; import { useContext } from 'react';
import { useRootStore } from '@educt/hooks/useRootStore';
export type CategoryItemPropsType = { export type CategoryItemPropsType = {
category: ICategory; category: ICategory;
}; };
const CategoryItem: React.FC<CategoryItemPropsType> = ({ category }) => { const CategoryItem: React.FC<CategoryItemPropsType> = ({ category }) => {
const { selectedCategory, setSelectedCategory } = useContext(CoursesPageContext); const {
const isSelected = selectedCategory?.id === category.id; pageStore: { coursesStore },
} = useRootStore();
const isSelected = coursesStore.selectedCategory?.id === category.id;
/** /**
* Handle click on change category * Handle click on change category
*/ */
const onCategoryChange = () => setSelectedCategory(category); const onCategoryChange = () => coursesStore.setSelectedCategory(category);
return ( return (
<Tag <Tag

View File

@ -29,8 +29,10 @@ type CategoryListPropsType = {
}; };
const CategoryList: React.FC<CategoryListPropsType> = ({ render: Item }) => { const CategoryList: React.FC<CategoryListPropsType> = ({ render: Item }) => {
const { categoryStore } = useRootStore(); const {
const { selectedCategory, setSelectedCategory } = useContext(CoursesPageContext); categoryStore,
pageStore: { coursesStore },
} = useRootStore();
const handleError = useErrorHandler(); const handleError = useErrorHandler();
const { categories, isLoading } = categoryStore; const { categories, isLoading } = categoryStore;
@ -51,11 +53,11 @@ const CategoryList: React.FC<CategoryListPropsType> = ({ render: Item }) => {
<Flex sx={{ columnGap: '7px', rowGap: '5px' }} flexWrap='wrap'> <Flex sx={{ columnGap: '7px', rowGap: '5px' }} flexWrap='wrap'>
<Tag <Tag
borderRadius='full' borderRadius='full'
variant={selectedCategory === undefined ? 'solid' : 'outline'} variant={coursesStore.selectedCategory === null ? 'solid' : 'outline'}
cursor='pointer' cursor='pointer'
transition='all .1s' transition='all .1s'
_hover={{ opacity: '.8' }} _hover={{ opacity: '.8' }}
onClick={() => setSelectedCategory(undefined)} onClick={() => coursesStore.setSelectedCategory(null)}
> >
<TagLabel>All categories</TagLabel> <TagLabel>All categories</TagLabel>
</Tag> </Tag>

View File

@ -36,12 +36,12 @@ const CourseList: React.FC<CourseListPropsType> = ({ render: Item }) => {
const { const {
userStore: { me }, userStore: { me },
courseStore, courseStore,
pageStore: { coursesStore },
} = useRootStore(); } = useRootStore();
const [deleting, setDeleting] = useState<Pick<ICourse, 'id' | 'title'> | null>(null); const [deleting, setDeleting] = useState<Pick<ICourse, 'id' | 'title'> | null>(null);
const { selectedCategory, courseStatus } = useContext(CoursesPageContext);
const { onOpen: onOpenDeleteDialog, onClose: onCloseDeleteDialog, isOpen: isOpenDeleteDialog } = useDisclosure(); const { onOpen: onOpenDeleteDialog, onClose: onCloseDeleteDialog, isOpen: isOpenDeleteDialog } = useDisclosure();
const { setCourseStatus } = useSetCourseStatus(); const { setCourseStatus } = useSetCourseStatus();
const { courses, isLoading } = courseStore; const { courses, isLoading, loadCourses } = courseStore;
const handleDeleteCourse = (course: Pick<ICourse, 'id' | 'title'>) => { const handleDeleteCourse = (course: Pick<ICourse, 'id' | 'title'>) => {
setDeleting(course); setDeleting(course);
@ -54,11 +54,11 @@ const CourseList: React.FC<CourseListPropsType> = ({ render: Item }) => {
useEffect(() => { useEffect(() => {
if (me !== null) { if (me !== null) {
courseStore.loadCourses({ courseStore.loadCourses({
category_id: selectedCategory?.id, category_id: coursesStore.selectedCategory?.id,
status: me.isAdmin || me.isTeacher ? courseStatus : CourseStatusEnum.PUBLISHED, status: me.isAdmin || me.isTeacher ? coursesStore.showedStatus : CourseStatusEnum.PUBLISHED,
}); });
} }
}, [courseStore, selectedCategory, courseStatus]); }, [courseStore, coursesStore.selectedCategory, coursesStore.showedStatus]);
if (courses === null || isLoading) return <LoadingList />; if (courses === null || isLoading) return <LoadingList />;
@ -80,9 +80,9 @@ const CourseList: React.FC<CourseListPropsType> = ({ render: Item }) => {
<Box textAlign='center' mt='10' userSelect='none'> <Box textAlign='center' mt='10' userSelect='none'>
<Text color='gray.500'> <Text color='gray.500'>
There are no courses&nbsp; There are no courses&nbsp;
{!selectedCategory && !courseStatus {!coursesStore.selectedCategory && !coursesStore.showedStatus
? 'in the system yet' ? 'in the system yet'
: `in this category${courseStatus ? ' or with this status' : ''}`} : `in this category${coursesStore.showedStatus ? ' or with this status' : ''}`}
. .
</Text> </Text>
</Box> </Box>

View File

@ -1,36 +0,0 @@
import React from 'react';
import { Tabs, TabList, Tab } from '@chakra-ui/react';
/**
* Types
*/
import { CourseStatusEnum } from '@educt/enums';
/**
* Contexts
*/
import { CoursesPageContext } from '@educt/contexts';
/**
* Hooks
*/
import { useContext } from 'react';
type CourseStatusTabsProps = {};
const CourseStatusTabs: React.FC<CourseStatusTabsProps> = () => {
const { setCourseStatus } = useContext(CoursesPageContext);
const handleChangeStatus = (status: CourseStatusEnum | undefined) => setCourseStatus(status);
return (
<Tabs variant='solid-rounded'>
<TabList justifyContent='center'>
<Tab onClick={() => handleChangeStatus(undefined)}>All</Tab>
<Tab onClick={() => handleChangeStatus(CourseStatusEnum.PUBLISHED)}>Published</Tab>
<Tab onClick={() => handleChangeStatus(CourseStatusEnum.DRAFT)}>Unpublished Drafts</Tab>
</TabList>
</Tabs>
);
};
export default CourseStatusTabs;

View File

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

View File

@ -0,0 +1,31 @@
import React from 'react';
import { BoxProps, Box } from '@chakra-ui/layout';
import { Tabs, TabList, Tab } from '@chakra-ui/tabs';
import { CourseStatusEnum } from '@educt/enums';
/**
* Hooks
*/
import { useRootStore } from '@educt/hooks/useRootStore';
export const CoursesTabs: React.FC<BoxProps> = props => {
const {
userStore: { me },
pageStore: { coursesStore },
} = useRootStore();
return (
me &&
(me.isAdmin || me.isTeacher ? (
<Box {...props}>
<Tabs variant='solid-rounded'>
<TabList justifyContent='center' {...props}>
<Tab onClick={() => coursesStore.setShowedStatus(undefined)}>All</Tab>
<Tab onClick={() => coursesStore.setShowedStatus(CourseStatusEnum.PUBLISHED)}>Published</Tab>
<Tab onClick={() => coursesStore.setShowedStatus(CourseStatusEnum.DRAFT)}>Unpublished Drafts</Tab>
</TabList>
</Tabs>
</Box>
) : null)
);
};

View File

@ -1,5 +1,4 @@
import React from 'react'; import React from 'react';
import { Box } from '@chakra-ui/react';
/** /**
* Types * Types
@ -10,22 +9,19 @@ import { IPageProps } from '@educt/interfaces';
* Components * Components
*/ */
import { PageWrapper, PageHeading, PageContent } from '@educt/components/PageElements'; import { PageWrapper, PageHeading, PageContent } from '@educt/components/PageElements';
import { CreateButton } from '@educt/components/Buttons';
import LoadingPage from '@educt/components/LoadingPage'; 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 CategoryItem from './components/CategoryList/CategoryItem'; import CategoryItem from './components/CategoryList/CategoryItem';
import CourseItem from './components/CourseList/CourseItem'; import CourseItem from './components/CourseList/CourseItem';
/**
* Providers
*/
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 { CoursesTabs } from './components';
/** /**
* Courses page * Courses page
@ -34,22 +30,20 @@ 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>
<PageWrapper> <PageWrapper>
<PageHeading heading='Courses' description='List of all courses' /> <PageHeading heading='Courses' description='List of all courses' />
{(me.isAdmin || me.isTeacher) && <CreateButton onClick={() => history.push('/courses/create')} />}
<PageContent mt='10'> <PageContent mt='10'>
{(me.isAdmin || me.isTeacher) && <CourseStatusTabs />} <CoursesTabs mb='7' />
<Box mt='5'>
<CategoryList render={CategoryItem} /> <CategoryList render={CategoryItem} />
<CourseList render={CourseItem} /> <CourseList render={CourseItem} />
</Box>
</PageContent> </PageContent>
</PageWrapper> </PageWrapper>
</CoursesPageContextProvider>
); );
}; };

View File

@ -1,20 +1,21 @@
import React, { useState } from 'react'; import React from 'react';
import { Button } from '@chakra-ui/react'; import { ButtonProps, Button } from '@chakra-ui/button';
import { AiOutlineEye, AiOutlineEyeInvisible } from 'react-icons/ai';
import { CourseStatusEnum } from '@educt/enums'; import { CourseStatusEnum } from '@educt/enums';
import { AiOutlineEye, AiOutlineEyeInvisible } from 'react-icons/ai';
/**
* Hooks
*/
import { useState } from 'react';
import { useSetCourseStatus } from '@educt/hooks/queries'; import { useSetCourseStatus } from '@educt/hooks/queries';
type SetCourseStatusButtonPropsType = { interface IStatusButtonProps extends ButtonProps {
courseId: string; courseId: string;
currentStatus: CourseStatusEnum; currentStatus: CourseStatusEnum;
loadingText?: string | undefined; loadingText?: string | undefined;
}; }
export const StatusButton: React.FC<IStatusButtonProps> = props => {
const SetCourseStatusButton: React.FC<SetCourseStatusButtonPropsType> = ({ const { courseId, currentStatus, loadingText, ...btnProps } = props;
courseId,
currentStatus,
loadingText = 'Wait a second...',
}) => {
const [status, setStatus] = useState<keyof typeof CourseStatusEnum>(currentStatus); const [status, setStatus] = useState<keyof typeof CourseStatusEnum>(currentStatus);
const { setCourseStatus, isLoading } = useSetCourseStatus(); const { setCourseStatus, isLoading } = useSetCourseStatus();
@ -28,8 +29,11 @@ const SetCourseStatusButton: React.FC<SetCourseStatusButtonPropsType> = ({
<Button <Button
onClick={() => handleChangeStatus(CourseStatusEnum.DRAFT)} onClick={() => handleChangeStatus(CourseStatusEnum.DRAFT)}
isLoading={isLoading} isLoading={isLoading}
loadingText={loadingText} loadingText={loadingText || 'Saving...'}
leftIcon={<AiOutlineEyeInvisible />} leftIcon={<AiOutlineEyeInvisible />}
size='sm'
variant='outline'
{...btnProps}
> >
Mark as Draft Mark as Draft
</Button> </Button>
@ -38,8 +42,11 @@ const SetCourseStatusButton: React.FC<SetCourseStatusButtonPropsType> = ({
<Button <Button
onClick={() => handleChangeStatus(CourseStatusEnum.PUBLISHED)} onClick={() => handleChangeStatus(CourseStatusEnum.PUBLISHED)}
isLoading={isLoading} isLoading={isLoading}
loadingText={loadingText} loadingText={loadingText || 'Saving...'}
leftIcon={<AiOutlineEye />} leftIcon={<AiOutlineEye />}
size='sm'
variant='outline'
{...btnProps}
> >
Publish course Publish course
</Button> </Button>
@ -48,5 +55,3 @@ const SetCourseStatusButton: React.FC<SetCourseStatusButtonPropsType> = ({
return Components[status]; return Components[status];
}; };
export default SetCourseStatusButton;

View File

@ -1,8 +1,7 @@
import React from 'react'; import React from 'react';
import Helmet from 'react-helmet'; import Helmet from 'react-helmet';
import { Redirect } from 'react-router-dom'; import { Redirect } from 'react-router-dom';
import { Box, Heading, Text, Tab, TabList, Flex, Tabs, TabPanels, TabPanel, Button } from '@chakra-ui/react'; import { Box, Heading, Text, Tab, TabList, Flex, Tabs, TabPanels, TabPanel } from '@chakra-ui/react';
import { DeleteIcon } from '@chakra-ui/icons';
/** /**
* Types * Types
@ -14,7 +13,6 @@ import type { IPageProps } from '@educt/interfaces';
*/ */
import LoadingPage from '@educt/components/LoadingPage'; import LoadingPage from '@educt/components/LoadingPage';
import { EditCourseForm } from '@educt/components/Forms/CourseForm'; import { EditCourseForm } from '@educt/components/Forms/CourseForm';
import SetCourseStatusButton from '@educt/components/SetCourseStatusButton';
import DeleteCourseDialog from '@educt/components/Dialogs/DeleteCourseDialog'; import DeleteCourseDialog from '@educt/components/Dialogs/DeleteCourseDialog';
import { StudentTableList, StudentTableRow } from './components/StudentTableList'; import { StudentTableList, StudentTableRow } from './components/StudentTableList';
import LessonList from './components/LessonList'; import LessonList from './components/LessonList';
@ -26,6 +24,9 @@ import { useParams } from 'react-router-dom';
import { useRootStore } from '@educt/hooks/useRootStore'; import { useRootStore } from '@educt/hooks/useRootStore';
import { useFetchCourse } from '@educt/hooks/queries'; import { useFetchCourse } from '@educt/hooks/queries';
import { useDisclosure } from '@chakra-ui/hooks'; import { useDisclosure } from '@chakra-ui/hooks';
import { PageContent, PageWrapper } from '@educt/components/PageElements';
import { DeleteButton } from '@educt/components/Buttons';
import { StatusButton } from './components';
/** /**
* Course editor * Course editor
@ -53,23 +54,25 @@ const EditCoursePage: React.FC<IPageProps> = () => {
if (!course || isLoading) return <LoadingPage />; if (!course || isLoading) return <LoadingPage />;
return ( return (
<> <PageWrapper>
<Helmet> <Helmet>
<title>Editing {course.title}</title> <title>Editing {course.title}</title>
</Helmet> </Helmet>
<DeleteCourseDialog course={course} onClose={onCloseDeleteDialog} isOpen={isOpenDeleteDialog} /> <DeleteCourseDialog course={course} onClose={onCloseDeleteDialog} isOpen={isOpenDeleteDialog} />
<Flex justifyContent='space-between' alignItems='center' flexWrap='wrap'> <Flex justifyContent='space-between' alignItems='center' flexWrap='wrap'>
<Box mr='3'> <Box mr='3'>
<Heading as='h1'>Course editor</Heading> <Heading as='h1'>Course editor</Heading>
<Text mt='2'>{course.title}</Text> <Text mt='2'>{course.title}</Text>
</Box> </Box>
<Box ml='auto' alignSelf='flex-end' justifySelf='flex-end' mt={{ base: '7', md: '0' }}> <Box ml='auto' alignSelf='flex-end' justifySelf='flex-end' mt={{ base: '7', md: '0' }}>
<SetCourseStatusButton courseId={course.id} currentStatus={course.status} /> <StatusButton courseId={course.id} currentStatus={course.status} />
<Button onClick={onOpenDeleteDialog} ml='2' variant='outline' leftIcon={<DeleteIcon />} colorScheme='red'> <DeleteButton onClick={onOpenDeleteDialog} ml='2' />
Delete
</Button>
</Box> </Box>
</Flex> </Flex>
<PageContent>
<Box> <Box>
<Tabs isLazy mt='8'> <Tabs isLazy mt='8'>
<TabList> <TabList>
@ -101,7 +104,8 @@ const EditCoursePage: React.FC<IPageProps> = () => {
</TabPanels> </TabPanels>
</Tabs> </Tabs>
</Box> </Box>
</> </PageContent>
</PageWrapper>
); );
}; };

View File

@ -1,10 +1,13 @@
import { makeAutoObservable } from 'mobx'; import { makeAutoObservable } from 'mobx';
import CoursesStore from './pages/CoursesStore';
import EditCourseStore from './pages/EditCourseStore'; import EditCourseStore from './pages/EditCourseStore';
import RootStore from './RootStore'; import RootStore from './RootStore';
export default class PageStore { export default class PageStore {
public root: RootStore; public root: RootStore;
public coursesStore: CoursesStore;
public editCourseStore: EditCourseStore; public editCourseStore: EditCourseStore;
constructor(root: RootStore) { constructor(root: RootStore) {
@ -13,6 +16,7 @@ export default class PageStore {
/** /**
* Pages * Pages
*/ */
this.coursesStore = new CoursesStore(this);
this.editCourseStore = new EditCourseStore(this); this.editCourseStore = new EditCourseStore(this);
makeAutoObservable(this); makeAutoObservable(this);

View File

@ -0,0 +1,30 @@
import { makeAutoObservable } from 'mobx';
import { CourseStatusEnum } from '@educt/enums';
import PageStore from '../PageStore';
/**
* Types
*/
import type { ICategory } from '@educt/interfaces';
export default class CoursesStore {
public pageStore: PageStore;
public selectedCategory: ICategory | null = null;
public showedStatus: CourseStatusEnum | undefined;
constructor(pageStore: PageStore) {
this.pageStore = pageStore;
makeAutoObservable(this);
}
public setSelectedCategory(category: ICategory | null): void {
this.selectedCategory = category;
}
public setShowedStatus(newStatus: CourseStatusEnum | undefined): void {
this.showedStatus = newStatus;
}
}