diff --git a/src/contexts/index.ts b/src/contexts/index.ts index dd2ff8d..af23126 100644 --- a/src/contexts/index.ts +++ b/src/contexts/index.ts @@ -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(undefined); @@ -23,3 +23,8 @@ export const UsersPageContext = createContext({ deletingUser: undefined, setDeletingUser: () => {}, }); + +export const CoursesPageContext = createContext({ + selectedCategory: undefined, + setSelectedCategory: () => {}, +}); diff --git a/src/pages/courses/components/CategoryList/CategoryItem.tsx b/src/pages/courses/components/CategoryList/CategoryItem.tsx new file mode 100644 index 0000000..3042c2d --- /dev/null +++ b/src/pages/courses/components/CategoryList/CategoryItem.tsx @@ -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 = ({ category }) => { + const { selectedCategory, setSelectedCategory } = useContext(CoursesPageContext); + + /** + * Handle click on change category + */ + const onCategoryChange = () => setSelectedCategory(category); + + return ( + + {category.title} + + ); +}; + +export default CategoryItem; diff --git a/src/pages/courses/components/CategoryList/CategoryList.tsx b/src/pages/courses/components/CategoryList/CategoryList.tsx new file mode 100644 index 0000000..b4daf42 --- /dev/null +++ b/src/pages/courses/components/CategoryList/CategoryList.tsx @@ -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 = ({ categories }) => { + const { selectedCategory, setSelectedCategory } = useContext(CoursesPageContext); + return ( + + + setSelectedCategory(undefined)} + > + All categories + + {categories.map((category, i) => { + return ; + })} + + + ); +}; + +export default CategoryList; diff --git a/src/pages/courses/components/CategoryList/index.ts b/src/pages/courses/components/CategoryList/index.ts new file mode 100644 index 0000000..62fa346 --- /dev/null +++ b/src/pages/courses/components/CategoryList/index.ts @@ -0,0 +1,3 @@ +import CategoryList from './CategoryList'; + +export default CategoryList; diff --git a/src/pages/courses/components/CourseList/CourseItem.tsx b/src/pages/courses/components/CourseList/CourseItem.tsx index b68135d..992627f 100644 --- a/src/pages/courses/components/CourseList/CourseItem.tsx +++ b/src/pages/courses/components/CourseList/CourseItem.tsx @@ -14,29 +14,47 @@ 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 ( - - - - } /> - - }>Edit course - }>Mark as Draft - } color='red.500'> - Delete - - - - - + {(me.isAdmin || me.isTeacher) && ( + + + + } + _hover={{ backgroundColor: colorMode === 'dark' ? 'gray.600' : 'gray.200' }} + _active={{ backgroundColor: colorMode === 'dark' ? 'gray.600' : 'gray.200' }} + /> + + }>Edit course + }>Mark as Draft + } color='red.500'> + Delete + + + + + + )} { { diff --git a/src/pages/courses/index.tsx b/src/pages/courses/index.tsx index 96f0484..1aa131e 100644 --- a/src/pages/courses/index.tsx +++ b/src/pages/courses/index.tsx @@ -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,34 +9,66 @@ 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 = ({ 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 ( - - - - Courses - List of all available courses. + + + + + Courses + List of all available courses. + + {(me.isAdmin || me.isTeacher) && ( + + )} + + + {(me.isAdmin || me.isTeacher) && ( + + + All + Published + Draft + + + )} + + + + - - - - - - All - Published - Draft - - - - + ); }; -export default CoursesPage; +export default observer(CoursesPage); diff --git a/src/providers/index.tsx b/src/providers/index.tsx index bbaa867..9719064 100644 --- a/src/providers/index.tsx +++ b/src/providers/index.tsx @@ -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 }) => { ); }; + +/** + * Courses page context provider + */ +export const CoursesPageContextProvider: React.FC = ({ children }) => { + const [selectedCategory, setSelectedCategory] = useState(undefined); + + return ( + + {children} + + ); +}; diff --git a/src/services/CategoryService.ts b/src/services/CategoryService.ts new file mode 100644 index 0000000..f06f802 --- /dev/null +++ b/src/services/CategoryService.ts @@ -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> { + const result = await this.api.get('/v1/categories'); + return result.data; + } +} diff --git a/src/stores/CategoryStore.ts b/src/stores/CategoryStore.ts new file mode 100644 index 0000000..0457faa --- /dev/null +++ b/src/stores/CategoryStore.ts @@ -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; + } + } +} diff --git a/src/stores/RootStore.ts b/src/stores/RootStore.ts index 6340de6..26db41f 100644 --- a/src/stores/RootStore.ts +++ b/src/stores/RootStore.ts @@ -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); diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 60cc5d5..c843a08 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -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>; }; + +export type CoursesPageContextType = { + /** + * Filter courses by switching caterory + */ + selectedCategory: ICategory | undefined; + setSelectedCategory: React.Dispatch>; +};