feat: courses page context & categories fetch

This commit is contained in:
Sergey Yarkov 2021-10-11 20:53:45 +03:00
parent 0b080fc0be
commit b2757e1494
12 changed files with 255 additions and 45 deletions

View File

@ -1,5 +1,5 @@
import { createContext } from 'react'; import { createContext } from 'react';
import { ProfilePageContextType, UsersPageContextType } from 'types'; import { CoursesPageContextType, ProfilePageContextType, UsersPageContextType } from 'types';
import RootStore from '../stores/RootStore'; import RootStore from '../stores/RootStore';
export const StoreContext = createContext<RootStore | undefined>(undefined); export const StoreContext = createContext<RootStore | undefined>(undefined);
@ -23,3 +23,8 @@ export const UsersPageContext = createContext<UsersPageContextType>({
deletingUser: undefined, deletingUser: undefined,
setDeletingUser: () => {}, setDeletingUser: () => {},
}); });
export const CoursesPageContext = createContext<CoursesPageContextType>({
selectedCategory: undefined,
setSelectedCategory: () => {},
});

View File

@ -0,0 +1,41 @@
import React, { useContext } from 'react';
import { Tag, TagLabel } from '@chakra-ui/react';
/**
* Types
*/
import { ICategory } from 'interfaces';
/**
* Contexts
*/
import { CoursesPageContext } from 'contexts';
type CategoryItemProps = {
category: ICategory;
};
const CategoryItem: React.FC<CategoryItemProps> = ({ category }) => {
const { selectedCategory, setSelectedCategory } = useContext(CoursesPageContext);
/**
* Handle click on change category
*/
const onCategoryChange = () => setSelectedCategory(category);
return (
<Tag
borderRadius='full'
variant={selectedCategory?.id === category.id ? 'solid' : 'outline'}
colorScheme='blue'
cursor='pointer'
transition='all .1s'
_hover={{ opacity: '.8' }}
onClick={onCategoryChange}
>
<TagLabel>{category.title}</TagLabel>
</Tag>
);
};
export default CategoryItem;

View File

@ -0,0 +1,34 @@
import React, { useContext } from 'react';
import { Box, Flex, Tag, TagLabel } from '@chakra-ui/react';
import { CoursesPageContext } from 'contexts';
import CategoryItem from './CategoryItem';
import { ICategory } from 'interfaces';
type CategoryListProps = {
categories: ICategory[];
};
const CategoryList: React.FC<CategoryListProps> = ({ categories }) => {
const { selectedCategory, setSelectedCategory } = useContext(CoursesPageContext);
return (
<Box mb='5'>
<Flex sx={{ columnGap: '7px', rowGap: '5px' }} flexWrap='wrap'>
<Tag
borderRadius='full'
variant={selectedCategory === undefined ? 'solid' : 'outline'}
cursor='pointer'
transition='all .1s'
_hover={{ opacity: '.8' }}
onClick={() => setSelectedCategory(undefined)}
>
<TagLabel>All categories</TagLabel>
</Tag>
{categories.map((category, i) => {
return <CategoryItem key={category.id} category={category} />;
})}
</Flex>
</Box>
);
};
export default CategoryList;

View File

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

View File

@ -14,19 +14,36 @@ import {
IconButton, IconButton,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { Icon } from '@chakra-ui/react'; import { Icon } from '@chakra-ui/react';
import { MdNote } from 'react-icons/md'; import { MdNote, MdMoreHoriz } from 'react-icons/md';
import { MdGroup, MdThumbUp, MdVideoLibrary } from 'react-icons/md'; import { MdGroup, MdThumbUp, MdVideoLibrary } from 'react-icons/md';
import { DeleteIcon, EditIcon, HamburgerIcon } from '@chakra-ui/icons'; import { DeleteIcon, EditIcon } from '@chakra-ui/icons';
import { useColorMode } from '@chakra-ui/color-mode';
import { Link as ReactRouterLink } from 'react-router-dom'; import { Link as ReactRouterLink } from 'react-router-dom';
import { useRootStore } from 'hooks/useRootStore';
const CourseItem: React.FC = () => { const CourseItem: React.FC = () => {
const {
userStore: { me },
} = useRootStore();
const { colorMode } = useColorMode();
if (me === null) return null;
return ( return (
<Box borderWidth='1px' borderRadius='lg'> <Box borderWidth='1px' borderRadius='lg'>
{(me.isAdmin || me.isTeacher) && (
<Flex justifyContent='flex-end'> <Flex justifyContent='flex-end'>
<Box position='absolute' zIndex='1' padding='10px'> <Box position='absolute' zIndex='1' padding='10px'>
<Menu> <Menu>
<MenuButton as={IconButton} colorScheme='gray' aria-label='Actions' icon={<HamburgerIcon />} /> <MenuButton
as={IconButton}
backgroundColor={colorMode === 'dark' ? 'gray.700' : 'gray.100'}
aria-label='Actions'
icon={<MdMoreHoriz size='18px' />}
_hover={{ backgroundColor: colorMode === 'dark' ? 'gray.600' : 'gray.200' }}
_active={{ backgroundColor: colorMode === 'dark' ? 'gray.600' : 'gray.200' }}
/>
<MenuList> <MenuList>
<MenuItem icon={<EditIcon />}>Edit course</MenuItem> <MenuItem icon={<EditIcon />}>Edit course</MenuItem>
<MenuItem icon={<MdNote />}>Mark as Draft</MenuItem> <MenuItem icon={<MdNote />}>Mark as Draft</MenuItem>
@ -37,6 +54,7 @@ const CourseItem: React.FC = () => {
</Menu> </Menu>
</Box> </Box>
</Flex> </Flex>
)}
<Link <Link
to='/course/id' to='/course/id'
as={ReactRouterLink} as={ReactRouterLink}
@ -48,7 +66,7 @@ const CourseItem: React.FC = () => {
<Box <Box
position='relative' position='relative'
minHeight='200px' minHeight='200px'
backgroundColor='gray.200' backgroundColor={colorMode === 'dark' ? 'gray.700' : 'gray.200'}
borderTopLeftRadius='lg' borderTopLeftRadius='lg'
borderTopRightRadius='lg' borderTopRightRadius='lg'
p='10px' p='10px'

View File

@ -11,15 +11,12 @@ const CourseList: React.FC = () => {
<Grid <Grid
templateColumns='repeat(3, 1fr)' templateColumns='repeat(3, 1fr)'
gap='6' gap='6'
mt='12'
padding='0 20px'
sx={{ sx={{
'@media (max-width: 1280px)': { '@media (max-width: 1280px)': {
gridTemplateColumns: 'repeat(2, 1fr)', gridTemplateColumns: 'repeat(2, 1fr)',
}, },
'@media (max-width: 768px)': { '@media (max-width: 768px)': {
gridTemplateColumns: 'repeat(1, 1fr)', gridTemplateColumns: 'repeat(1, 1fr)',
padding: '10px',
}, },
}} }}
> >

View File

@ -1,4 +1,5 @@
import React from 'react'; import React, { useEffect } from 'react';
import { observer } from 'mobx-react-lite';
import { Flex, Tabs, TabList, Tab, Box, Heading, Text, Button } from '@chakra-ui/react'; import { Flex, Tabs, TabList, Tab, Box, Heading, Text, Button } from '@chakra-ui/react';
import { IPageProps } from 'interfaces'; import { IPageProps } from 'interfaces';
@ -8,23 +9,50 @@ import { AddIcon } from '@chakra-ui/icons';
* Components * Components
*/ */
import CourseList from './components/CourseList'; import CourseList from './components/CourseList';
import CategoryList from './components/CategoryList';
/**
* Hooks
*/
import { useRootStore } from 'hooks/useRootStore';
import { CoursesPageContextProvider } from 'providers';
import { useErrorHandler } from 'react-error-boundary';
/** /**
* Courses page * Courses page
*/ */
const CoursesPage: React.FC<IPageProps> = ({ title }) => { const CoursesPage: React.FC<IPageProps> = ({ title }) => {
const {
userStore: { me },
categoryStore,
} = useRootStore();
const handleError = useErrorHandler();
useEffect(() => {
categoryStore.loadCategories().catch(error => handleError(error));
}, [categoryStore, handleError]);
/**
* TODO: Loading page for courses
*/
if (me === null) return null;
return ( return (
<CoursesPageContextProvider>
<Box> <Box>
<Flex alignItems='center' justifyContent='space-between'> <Flex alignItems='center' justifyContent='space-between'>
<Box> <Box>
<Heading as='h1'>Courses</Heading> <Heading as='h1'>Courses</Heading>
<Text mt='2'>List of all available courses.</Text> <Text mt='2'>List of all available courses.</Text>
</Box> </Box>
{(me.isAdmin || me.isTeacher) && (
<Button variant='outline' colorScheme='blue' leftIcon={<AddIcon />}> <Button variant='outline' colorScheme='blue' leftIcon={<AddIcon />}>
Create new Create new
</Button> </Button>
)}
</Flex> </Flex>
<Box mt='7'> <Box mt='7'>
{(me.isAdmin || me.isTeacher) && (
<Tabs> <Tabs>
<TabList justifyContent='center'> <TabList justifyContent='center'>
<Tab>All</Tab> <Tab>All</Tab>
@ -32,10 +60,15 @@ const CoursesPage: React.FC<IPageProps> = ({ title }) => {
<Tab>Draft</Tab> <Tab>Draft</Tab>
</TabList> </TabList>
</Tabs> </Tabs>
)}
<Box mt='5'>
<CategoryList categories={categoryStore.categories} />
<CourseList /> <CourseList />
</Box> </Box>
</Box> </Box>
</Box>
</CoursesPageContextProvider>
); );
}; };
export default CoursesPage; export default observer(CoursesPage);

View File

@ -1,8 +1,8 @@
import React, { useState, ReactNode } from 'react'; import React, { useState, ReactNode } from 'react';
import { ProfilePageContext, StoreContext, UsersPageContext } from 'contexts'; import { CoursesPageContext, ProfilePageContext, StoreContext, UsersPageContext } from 'contexts';
import { ProfilePageDataType, ProfilePageStatusType, SearchingRoleStateType } from 'types'; import { ProfilePageDataType, ProfilePageStatusType, SearchingRoleStateType } from 'types';
import RootStore from 'stores/RootStore'; import RootStore from 'stores/RootStore';
import { IUser } from 'interfaces'; import { ICategory, IUser } from 'interfaces';
/** /**
* Root store context provider * Root store context provider
@ -56,3 +56,16 @@ export const UsersPageContextProvider: React.FC = ({ children }) => {
</UsersPageContext.Provider> </UsersPageContext.Provider>
); );
}; };
/**
* Courses page context provider
*/
export const CoursesPageContextProvider: React.FC = ({ children }) => {
const [selectedCategory, setSelectedCategory] = useState<ICategory | undefined>(undefined);
return (
<CoursesPageContext.Provider value={{ selectedCategory, setSelectedCategory }}>
{children}
</CoursesPageContext.Provider>
);
};

View File

@ -0,0 +1,20 @@
import { AxiosInstance } from 'axios';
import { IApiRespose, ICategory } from 'interfaces';
export default class CategoryService {
public api: AxiosInstance;
constructor(api: AxiosInstance) {
this.api = api;
}
/**
* Feach all categories
*
* @returns Array of categories
*/
public async fetchAll(): Promise<IApiRespose<ICategory[]>> {
const result = await this.api.get('/v1/categories');
return result.data;
}
}

View File

@ -0,0 +1,34 @@
import { AxiosInstance } from 'axios';
import { ICategory } from 'interfaces';
import { makeAutoObservable, runInAction } from 'mobx';
import CategoryService from 'services/CategoryService';
import RootStore from './RootStore';
export default class CategoryStore {
public root: RootStore;
public categoryService: CategoryService;
public categories: ICategory[] = [];
constructor(root: RootStore, api: AxiosInstance) {
this.root = root;
this.categoryService = new CategoryService(api);
makeAutoObservable(this);
}
public async loadCategories() {
try {
const result = await this.categoryService.fetchAll();
console.log(`[${this.constructor.name}]: ${result.message}`, result);
runInAction(() => {
this.categories = result.data;
});
return result;
} catch (error: any) {
throw error;
}
}
}

View File

@ -12,8 +12,11 @@ import ApiService from 'services/ApiService';
import AuthStore from './AuthStore'; import AuthStore from './AuthStore';
import UserStore from './UserStore'; import UserStore from './UserStore';
import UIStore from './UIStore'; import UIStore from './UIStore';
import CategoryStore from './CategoryStore';
export default class RootStore { export default class RootStore {
public categoryStore: CategoryStore;
public uiStore: UIStore; public uiStore: UIStore;
public authStore: AuthStore; public authStore: AuthStore;
@ -31,6 +34,7 @@ export default class RootStore {
/** /**
* Stores * Stores
*/ */
this.categoryStore = new CategoryStore(this, this.apiService.api);
this.uiStore = new UIStore(this); this.uiStore = new UIStore(this);
this.authStore = new AuthStore(this, this.apiService.api); this.authStore = new AuthStore(this, this.apiService.api);
this.userStore = new UserStore(this, this.apiService.api); this.userStore = new UserStore(this, this.apiService.api);

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

@ -1,5 +1,5 @@
import { UserRoleEnum } from 'enums'; import { UserRoleEnum } from 'enums';
import { IUser } from 'interfaces'; import { ICategory, IUser } from 'interfaces';
import React from 'react'; import React from 'react';
import { IconType } from 'react-icons/lib'; import { IconType } from 'react-icons/lib';
@ -73,3 +73,11 @@ export type UsersPageContextType = {
deletingUser: IUser | undefined; deletingUser: IUser | undefined;
setDeletingUser: React.Dispatch<React.SetStateAction<IUser | undefined>>; setDeletingUser: React.Dispatch<React.SetStateAction<IUser | undefined>>;
}; };
export type CoursesPageContextType = {
/**
* Filter courses by switching caterory
*/
selectedCategory: ICategory | undefined;
setSelectedCategory: React.Dispatch<React.SetStateAction<ICategory | undefined>>;
};