mirror of
https://github.com/sergeyyarkov/educt-client.git
synced 2026-10-11 10:19:22 +03:00
feat: courses page context & categories fetch
This commit is contained in:
parent
0b080fc0be
commit
b2757e1494
@ -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: () => {},
|
||||
});
|
||||
|
||||
41
src/pages/courses/components/CategoryList/CategoryItem.tsx
Normal file
41
src/pages/courses/components/CategoryList/CategoryItem.tsx
Normal 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;
|
||||
34
src/pages/courses/components/CategoryList/CategoryList.tsx
Normal file
34
src/pages/courses/components/CategoryList/CategoryList.tsx
Normal 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;
|
||||
3
src/pages/courses/components/CategoryList/index.ts
Normal file
3
src/pages/courses/components/CategoryList/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import CategoryList from './CategoryList';
|
||||
|
||||
export default CategoryList;
|
||||
@ -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 (
|
||||
<Box borderWidth='1px' borderRadius='lg'>
|
||||
<Flex justifyContent='flex-end'>
|
||||
<Box position='absolute' zIndex='1' padding='10px'>
|
||||
<Menu>
|
||||
<MenuButton as={IconButton} colorScheme='gray' aria-label='Actions' icon={<HamburgerIcon />} />
|
||||
<MenuList>
|
||||
<MenuItem icon={<EditIcon />}>Edit course</MenuItem>
|
||||
<MenuItem icon={<MdNote />}>Mark as Draft</MenuItem>
|
||||
<MenuItem icon={<DeleteIcon />} color='red.500'>
|
||||
Delete
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</Box>
|
||||
</Flex>
|
||||
{(me.isAdmin || me.isTeacher) && (
|
||||
<Flex justifyContent='flex-end'>
|
||||
<Box position='absolute' zIndex='1' padding='10px'>
|
||||
<Menu>
|
||||
<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>
|
||||
<MenuItem icon={<DeleteIcon />} color='red.500'>
|
||||
Delete
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</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'
|
||||
|
||||
@ -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',
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
||||
@ -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<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 (
|
||||
<Box>
|
||||
<Flex alignItems='center' justifyContent='space-between'>
|
||||
<Box>
|
||||
<Heading as='h1'>Courses</Heading>
|
||||
<Text mt='2'>List of all available courses.</Text>
|
||||
<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>
|
||||
<Tab>Published</Tab>
|
||||
<Tab>Draft</Tab>
|
||||
</TabList>
|
||||
</Tabs>
|
||||
)}
|
||||
<Box mt='5'>
|
||||
<CategoryList categories={categoryStore.categories} />
|
||||
<CourseList />
|
||||
</Box>
|
||||
</Box>
|
||||
<Button variant='outline' colorScheme='blue' leftIcon={<AddIcon />}>
|
||||
Create new
|
||||
</Button>
|
||||
</Flex>
|
||||
<Box mt='7'>
|
||||
<Tabs>
|
||||
<TabList justifyContent='center'>
|
||||
<Tab>All</Tab>
|
||||
<Tab>Published</Tab>
|
||||
<Tab>Draft</Tab>
|
||||
</TabList>
|
||||
</Tabs>
|
||||
<CourseList />
|
||||
</Box>
|
||||
</Box>
|
||||
</CoursesPageContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default CoursesPage;
|
||||
export default observer(CoursesPage);
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
20
src/services/CategoryService.ts
Normal file
20
src/services/CategoryService.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
34
src/stores/CategoryStore.ts
Normal file
34
src/stores/CategoryStore.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
10
src/types/index.d.ts
vendored
@ -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>>;
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user