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 { 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: () => {},
|
||||||
|
});
|
||||||
|
|||||||
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,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'
|
||||||
|
|||||||
@ -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',
|
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
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 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
10
src/types/index.d.ts
vendored
@ -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>>;
|
||||||
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user