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 { ProfilePageContextType, UsersPageContextType } from 'types';
import { CoursesPageContextType, ProfilePageContextType, UsersPageContextType } from 'types';
import RootStore from '../stores/RootStore';
export const StoreContext = createContext<RootStore | undefined>(undefined);
@ -23,3 +23,8 @@ export const UsersPageContext = createContext<UsersPageContextType>({
deletingUser: undefined,
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,
} 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 { 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 { useRootStore } from 'hooks/useRootStore';
const CourseItem: React.FC = () => {
const {
userStore: { me },
} = useRootStore();
const { colorMode } = useColorMode();
if (me === null) return null;
return (
<Box borderWidth='1px' borderRadius='lg'>
{(me.isAdmin || me.isTeacher) && (
<Flex justifyContent='flex-end'>
<Box position='absolute' zIndex='1' padding='10px'>
<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>
<MenuItem icon={<EditIcon />}>Edit course</MenuItem>
<MenuItem icon={<MdNote />}>Mark as Draft</MenuItem>
@ -37,6 +54,7 @@ const CourseItem: React.FC = () => {
</Menu>
</Box>
</Flex>
)}
<Link
to='/course/id'
as={ReactRouterLink}
@ -48,7 +66,7 @@ const CourseItem: React.FC = () => {
<Box
position='relative'
minHeight='200px'
backgroundColor='gray.200'
backgroundColor={colorMode === 'dark' ? 'gray.700' : 'gray.200'}
borderTopLeftRadius='lg'
borderTopRightRadius='lg'
p='10px'

View File

@ -11,15 +11,12 @@ const CourseList: React.FC = () => {
<Grid
templateColumns='repeat(3, 1fr)'
gap='6'
mt='12'
padding='0 20px'
sx={{
'@media (max-width: 1280px)': {
gridTemplateColumns: 'repeat(2, 1fr)',
},
'@media (max-width: 768px)': {
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 { IPageProps } from 'interfaces';
@ -8,23 +9,50 @@ import { AddIcon } from '@chakra-ui/icons';
* Components
*/
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
*/
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 (
<CoursesPageContextProvider>
<Box>
<Flex alignItems='center' justifyContent='space-between'>
<Box>
<Heading as='h1'>Courses</Heading>
<Text mt='2'>List of all available courses.</Text>
</Box>
{(me.isAdmin || me.isTeacher) && (
<Button variant='outline' colorScheme='blue' leftIcon={<AddIcon />}>
Create new
</Button>
)}
</Flex>
<Box mt='7'>
{(me.isAdmin || me.isTeacher) && (
<Tabs>
<TabList justifyContent='center'>
<Tab>All</Tab>
@ -32,10 +60,15 @@ const CoursesPage: React.FC<IPageProps> = ({ title }) => {
<Tab>Draft</Tab>
</TabList>
</Tabs>
)}
<Box mt='5'>
<CategoryList categories={categoryStore.categories} />
<CourseList />
</Box>
</Box>
</Box>
</CoursesPageContextProvider>
);
};
export default CoursesPage;
export default observer(CoursesPage);

View File

@ -1,8 +1,8 @@
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 RootStore from 'stores/RootStore';
import { IUser } from 'interfaces';
import { ICategory, IUser } from 'interfaces';
/**
* Root store context provider
@ -56,3 +56,16 @@ export const UsersPageContextProvider: React.FC = ({ children }) => {
</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 UserStore from './UserStore';
import UIStore from './UIStore';
import CategoryStore from './CategoryStore';
export default class RootStore {
public categoryStore: CategoryStore;
public uiStore: UIStore;
public authStore: AuthStore;
@ -31,6 +34,7 @@ export default class RootStore {
/**
* Stores
*/
this.categoryStore = new CategoryStore(this, this.apiService.api);
this.uiStore = new UIStore(this);
this.authStore = new AuthStore(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 { IUser } from 'interfaces';
import { ICategory, IUser } from 'interfaces';
import React from 'react';
import { IconType } from 'react-icons/lib';
@ -73,3 +73,11 @@ export type UsersPageContextType = {
deletingUser: 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>>;
};