mirror of
https://github.com/sergeyyarkov/educt-client.git
synced 2026-10-11 18:29:15 +03:00
Merge branch 'feature/course-page' into develop
This commit is contained in:
commit
329995fcda
@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="en" translate="no">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
|
||||
@ -1,11 +1,9 @@
|
||||
import React from 'react';
|
||||
import BaseModal from '@educt/components/Modal';
|
||||
|
||||
import * as helpers from '@educt/helpers';
|
||||
import { FormControl, FormLabel, FormHelperText, Input, Select, Stack } from '@chakra-ui/react';
|
||||
import { CheckIcon } from '@chakra-ui/icons';
|
||||
import { MdModeEdit } from 'react-icons/md';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import BaseModal from '@educt/components/Modal';
|
||||
|
||||
/**
|
||||
* Types
|
||||
@ -49,6 +47,14 @@ const EditUserModal: React.FC<EditUserModalPropsType> = props => {
|
||||
userStore: { me },
|
||||
} = useRootStore();
|
||||
const { updateUser, isLoading } = useUpdateUser();
|
||||
const defaultValues = {
|
||||
login: null,
|
||||
first_name: user.first_name,
|
||||
last_name: user.last_name,
|
||||
email: user.email,
|
||||
role: user.roles[0].slug,
|
||||
password: null,
|
||||
};
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
@ -57,7 +63,7 @@ const EditUserModal: React.FC<EditUserModalPropsType> = props => {
|
||||
} = useForm<UpdateUserInputType>({
|
||||
resolver: yupResolver(EditUserSchema),
|
||||
defaultValues: useMemo(() => {
|
||||
return props.user;
|
||||
return defaultValues;
|
||||
}, [props]),
|
||||
});
|
||||
|
||||
@ -78,7 +84,7 @@ const EditUserModal: React.FC<EditUserModalPropsType> = props => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
reset(props.user);
|
||||
reset(defaultValues);
|
||||
}, [props.user]);
|
||||
|
||||
return (
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { UserRoleEnum } from '@educt/enums';
|
||||
import { IUserRole } from '@educt/interfaces';
|
||||
import { ICourse, IUserRole, UserCourseType } from '@educt/interfaces';
|
||||
|
||||
export function userHasRoles(userRoles: IUserRole[], roles: UserRoleEnum[]): boolean {
|
||||
for (let i = 0; i < roles.length; i += 1) {
|
||||
@ -97,3 +97,8 @@ export function arrayMove<T>(array: T[], from: number, to: number) {
|
||||
export function getDirtyFields<T>(dirtyFields: { [key: string]: boolean | undefined }, data: T) {
|
||||
return Object.fromEntries(Object.keys(dirtyFields).map(k => [k, data[k as keyof T]]));
|
||||
}
|
||||
|
||||
export function userHasCourse(findingCourse: ICourse, userCourses?: Array<UserCourseType> | undefined) {
|
||||
if (!userCourses) return true;
|
||||
return userCourses.some(course => course.id === findingCourse.id);
|
||||
}
|
||||
|
||||
@ -10,15 +10,13 @@ import { CourseServiceInstance } from '@educt/services';
|
||||
import { useEffect } from 'react';
|
||||
import useAsync from '../../useAsync';
|
||||
|
||||
type CourseResultDataType = Omit<ICourse, 'students_count' | 'likes_count' | 'lessons_count'>;
|
||||
|
||||
const useFetchCourse = (id: string) => {
|
||||
const fetch = async (id: string) => {
|
||||
const { data } = await CourseServiceInstance.fetchById(id);
|
||||
return data;
|
||||
};
|
||||
|
||||
const { execute: fetchCourseById, ...state } = useAsync<CourseResultDataType, Parameters<typeof fetch>>(fetch);
|
||||
const { execute: fetchCourseById, ...state } = useAsync<ICourse, Parameters<typeof fetch>>(fetch);
|
||||
|
||||
useEffect(() => {
|
||||
fetchCourseById(id);
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import { ChakraProvider, ColorModeScript, theme } from '@chakra-ui/react';
|
||||
import * as React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { ChakraProvider, ColorModeScript } from '@chakra-ui/react';
|
||||
import App from './App';
|
||||
import { theme } from './theme';
|
||||
import { RootStoreProvider } from './providers';
|
||||
|
||||
ReactDOM.render(
|
||||
|
||||
@ -107,6 +107,7 @@ export interface IMe extends IUser {
|
||||
* does not have the STUDENT role
|
||||
*/
|
||||
courses?: UserCourseType[];
|
||||
likes: Array<Pick<ICourse, 'id'>>;
|
||||
isAdmin: boolean;
|
||||
isTeacher: boolean;
|
||||
isStudent: boolean;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
import { Link as ReactRouterLink } from 'react-router-dom';
|
||||
import { Flex, Heading, Text, Link, Icon } from '@chakra-ui/react';
|
||||
import { Heading, Text, Link, Icon } from '@chakra-ui/react';
|
||||
import { MdError } from 'react-icons/md';
|
||||
|
||||
/**
|
||||
@ -21,7 +21,7 @@ const NotFoundPage: React.FC<IPageProps> = ({ title }) => {
|
||||
|
||||
useEffect(() => {
|
||||
toast({ title: `Page not found`, duration: 10000, isClosable: true, status: 'warning' });
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@ -2,7 +2,7 @@ import React from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Box, Heading } from '@chakra-ui/react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
|
||||
/**
|
||||
* Types
|
||||
@ -41,11 +41,8 @@ const AuthPage: React.FC<IPageProps> = ({ title }) => {
|
||||
<Helmet>
|
||||
<title>{title}</title>
|
||||
</Helmet>
|
||||
<Box maxW='md' mx='auto'>
|
||||
<Logo mx='auto' h='14' mb={{ base: '10', md: '20' }} />
|
||||
<Heading mb='8' textAlign='center' size='xl' fontWeight='extrabold'>
|
||||
Login to your account
|
||||
</Heading>
|
||||
<Box maxW='md' mx='auto' mt='20'>
|
||||
<Logo mx='auto' h='14' mb={{ base: '10', md: '10' }} />
|
||||
<Card>
|
||||
<AuthForm />
|
||||
</Card>
|
||||
|
||||
@ -84,7 +84,7 @@ const AuthForm: React.FC = () => {
|
||||
<Button
|
||||
type='submit'
|
||||
colorScheme='blue'
|
||||
size='lg'
|
||||
size='md'
|
||||
fontSize='md'
|
||||
loadingText='Logging in...'
|
||||
isLoading={isLoading}
|
||||
|
||||
157
src/pages/course/course.tsx
Normal file
157
src/pages/course/course.tsx
Normal file
@ -0,0 +1,157 @@
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as helpres from '@educt/helpers';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
import { Flex, Box, Text, Heading, Button, Grid, GridItem, Divider, AvatarGroup, Avatar } from '@chakra-ui/react';
|
||||
import { MdPlayCircleOutline } from 'react-icons/md';
|
||||
import { Tabs, TabList, TabPanels, Tab, TabPanel } from '@chakra-ui/react';
|
||||
|
||||
/**
|
||||
* Types
|
||||
*/
|
||||
import type { IPageProps } from '@educt/interfaces';
|
||||
|
||||
/**
|
||||
* Components
|
||||
*/
|
||||
import { CourseBackgroundImage, CourseHeader, CourseHeading, CourseInfo, LessonItem, LessonList } from '.';
|
||||
import { PageWrapper, PageContent } from '@educt/components/PageElements';
|
||||
import LoadingPage from '@educt/components/LoadingPage';
|
||||
|
||||
/**
|
||||
* Hooks
|
||||
*/
|
||||
import { useHistory, useParams } from 'react-router-dom';
|
||||
import { useFetchCourse } from '@educt/hooks/queries';
|
||||
import { useRootStore } from '@educt/hooks/useRootStore';
|
||||
|
||||
const CoursePage: React.FC<IPageProps> = () => {
|
||||
const {
|
||||
userStore: { me },
|
||||
} = useRootStore();
|
||||
const params = useParams<{ id: string }>();
|
||||
const { error, data: course, isLoading } = useFetchCourse(params.id);
|
||||
const history = useHistory();
|
||||
|
||||
if (error?.response?.status === 404) {
|
||||
return <Redirect to='/404' />;
|
||||
}
|
||||
|
||||
if (!me || !course || isLoading) return <LoadingPage />;
|
||||
|
||||
const isCourseAvailable = helpres.userHasCourse(course, me.courses);
|
||||
const isEmptyLessons = course.lessons.length === 0;
|
||||
const isEmptyStudents = course.students.length === 0;
|
||||
|
||||
const handleWatchCourse = () => history.push(`/lesson/${course.lessons[0].id}`);
|
||||
|
||||
return (
|
||||
<PageWrapper maxW='1200px'>
|
||||
<CourseHeader>
|
||||
<CourseHeading title={course.title} teacher={course.teacher} category={course.category} />
|
||||
<CourseInfo
|
||||
id={course.id}
|
||||
lessonsCount={course.lessons_count}
|
||||
studentsCount={course.students_count}
|
||||
likesCount={course.likes_count}
|
||||
/>
|
||||
</CourseHeader>
|
||||
|
||||
<PageContent mt={{ base: '5', sm: '2' }}>
|
||||
<Grid templateColumns={{ base: '1fr', xl: '3fr 1fr' }} gap={'7'}>
|
||||
<GridItem>
|
||||
<Box>
|
||||
<Flex justifyContent={'space-between'} alignItems={'center'} mb='2'>
|
||||
<Text fontSize={'sm'}>Last update: {new Date(course.updated_at).toLocaleDateString()}</Text>
|
||||
<Button
|
||||
isDisabled={!isCourseAvailable}
|
||||
onClick={handleWatchCourse}
|
||||
leftIcon={<MdPlayCircleOutline />}
|
||||
size='sm'
|
||||
variant={'outline'}
|
||||
>
|
||||
Watch
|
||||
</Button>
|
||||
</Flex>
|
||||
<CourseBackgroundImage bg={course.color?.hex} src={course.image?.url} />
|
||||
</Box>
|
||||
<Box mt='4'>
|
||||
<Tabs>
|
||||
<TabList>
|
||||
<Tab>About</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
<Box mt='6'>
|
||||
<Heading as='h2' fontSize='2xl' fontWeight={'medium'}>
|
||||
About this course
|
||||
</Heading>
|
||||
<Text mt='2'>{course.description}</Text>
|
||||
</Box>
|
||||
{!isEmptyStudents ? (
|
||||
<Flex justifyContent={'flex-end'} alignItems={'center'} mt='5'>
|
||||
<Text fontSize={'sm'} mr='2'>
|
||||
Students:
|
||||
</Text>
|
||||
<AvatarGroup size='sm' max={2}>
|
||||
{course.students.map(student => (
|
||||
<Avatar key={student.id} name={student.fullname} />
|
||||
))}
|
||||
</AvatarGroup>
|
||||
</Flex>
|
||||
) : (
|
||||
<Box textAlign={'right'}>
|
||||
<Text fontSize={'sm'} userSelect={'none'} color='gray.500'>
|
||||
0 students
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
<Divider mt='5' />
|
||||
</GridItem>
|
||||
<GridItem borderRadius={'lg'} minWidth={'290px'} h='550px'>
|
||||
<Box>
|
||||
<Text fontSize={'2xl'} fontWeight={'medium'}>
|
||||
What you'll learn.
|
||||
</Text>
|
||||
<Text fontSize={'sm'} mt='2'>
|
||||
Lorem, ipsum dolor sit amet consectetur adipisicing elit. Iste temporibus harum eos praesentium hic,
|
||||
sunt corporis deserunt impedit natus consequuntur?
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Divider mt='5' />
|
||||
|
||||
<Box mt='4'>
|
||||
<Flex alignItems={'center'} justifyContent={'space-between'}>
|
||||
<Text fontSize={'2xl'} fontWeight={'medium'}>
|
||||
Content
|
||||
</Text>
|
||||
<Text as='span' color='gray.500' fontSize={'sm'}>
|
||||
{course.lessons_count} lessons
|
||||
</Text>
|
||||
</Flex>
|
||||
{!isEmptyLessons ? (
|
||||
<LessonList course={course} render={LessonItem} />
|
||||
) : (
|
||||
<Box textAlign={'center'} p='15px 0'>
|
||||
<Text fontSize={'sm'} userSelect={'none'} color='gray.500'>
|
||||
There are no lessons here yet
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
<Divider mt='5' />
|
||||
</Box>
|
||||
</GridItem>
|
||||
</Grid>
|
||||
</PageContent>
|
||||
</PageWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default observer(CoursePage);
|
||||
223
src/pages/course/index.tsx
Normal file
223
src/pages/course/index.tsx
Normal file
@ -0,0 +1,223 @@
|
||||
import React from 'react';
|
||||
import { runInAction } from 'mobx';
|
||||
import moment from 'moment';
|
||||
import * as helpers from '@educt/helpers';
|
||||
import { Link as ReactRouterLink } from 'react-router-dom';
|
||||
import {
|
||||
Box,
|
||||
BoxProps,
|
||||
Flex,
|
||||
FlexProps,
|
||||
Button,
|
||||
Icon,
|
||||
Image,
|
||||
Text,
|
||||
Heading,
|
||||
Tag,
|
||||
Stack,
|
||||
ImageProps,
|
||||
LinkBox,
|
||||
LinkOverlay,
|
||||
LinkBoxProps,
|
||||
StackProps,
|
||||
} from '@chakra-ui/react';
|
||||
import {
|
||||
MdThumbUpOffAlt,
|
||||
MdPlayCircleOutline,
|
||||
MdOutlineGroup,
|
||||
MdOutlineVideoLibrary,
|
||||
MdOutlineLock,
|
||||
} from 'react-icons/md';
|
||||
|
||||
/**
|
||||
* Types
|
||||
*/
|
||||
import type { ICourse } from '@educt/interfaces';
|
||||
|
||||
/**
|
||||
* Hooks
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
import { useColorModeValue } from '@chakra-ui/react';
|
||||
import { useRootStore } from '@educt/hooks/useRootStore';
|
||||
import { CourseServiceInstance } from '@educt/services';
|
||||
|
||||
export const CourseHeader: React.FC<FlexProps> = props => (
|
||||
<Flex justifyContent='space-between' flexDir={{ base: 'column', xl: 'row' }} {...props}>
|
||||
{props.children}
|
||||
</Flex>
|
||||
);
|
||||
|
||||
interface ICourseHeadingProps extends BoxProps {
|
||||
title: string;
|
||||
teacher: ICourse['teacher'];
|
||||
category: ICourse['category'];
|
||||
}
|
||||
export const CourseHeading: React.FC<ICourseHeadingProps> = props => {
|
||||
const { title, teacher, category, ...boxProps } = props;
|
||||
|
||||
return (
|
||||
<Box {...boxProps}>
|
||||
<Heading as='h1' fontSize='3xl' mr='4'>
|
||||
{title}
|
||||
</Heading>
|
||||
<Flex mt='3' alignItems={'center'}>
|
||||
<Text fontSize='sm'>{teacher.fullname}</Text> •
|
||||
<Tag size='sm' variant='solid' bg={category.color?.hex}>
|
||||
{category.title}
|
||||
</Tag>
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
interface ICourseInfoProps extends BoxProps {
|
||||
id: string;
|
||||
lessonsCount: string;
|
||||
studentsCount: string;
|
||||
likesCount: string;
|
||||
}
|
||||
export const CourseInfo: React.FC<ICourseInfoProps> = props => {
|
||||
const { userStore } = useRootStore();
|
||||
const { me } = userStore;
|
||||
const { id, lessonsCount, studentsCount, likesCount, ...boxProps } = props;
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [likes, setLikes] = useState<number>(Number.parseInt(likesCount, 10));
|
||||
|
||||
const handleLikeCourse = (id: string) => async () => {
|
||||
if (isLoading || me === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const isCourseLiked = me.likes.some(course => course.id === id);
|
||||
const setCourseLike = () => me.likes.push({ id });
|
||||
const unsetCourseLike = () => (me.likes = me.likes.filter(course => course.id !== id));
|
||||
|
||||
if (!isCourseLiked) {
|
||||
setIsLoading(true);
|
||||
setLikes(likes => likes + 1);
|
||||
await CourseServiceInstance.setLike(id);
|
||||
runInAction(setCourseLike);
|
||||
} else {
|
||||
setIsLoading(true);
|
||||
setLikes(likes => likes - 1);
|
||||
await CourseServiceInstance.unsetLike(id);
|
||||
runInAction(unsetCourseLike);
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box mt={{ base: '2', xl: '0' }} {...boxProps}>
|
||||
<Flex alignItems={'center'}>
|
||||
<Icon as={MdOutlineVideoLibrary} />
|
||||
<Text as='span' fontSize={'sm'} ml='1' mr='4'>
|
||||
{lessonsCount}
|
||||
</Text>
|
||||
|
||||
<Icon as={MdOutlineGroup} />
|
||||
<Text as='span' fontSize={'sm'} ml='1' mr='2'>
|
||||
{studentsCount}
|
||||
</Text>
|
||||
|
||||
<Button onClick={handleLikeCourse(id)} leftIcon={<MdThumbUpOffAlt />} variant='ghost'>
|
||||
{likes}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export const CourseBackgroundImage: React.FC<ImageProps> = props => {
|
||||
return (
|
||||
<Image
|
||||
borderRadius={'lg'}
|
||||
objectFit='cover'
|
||||
w='full'
|
||||
h={{ base: '300px', sm: '300px', md: '300px', lg: '400px' }}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface ILessonListProps extends StackProps {
|
||||
course: ICourse;
|
||||
render: React.FC<ILessonItemProps>;
|
||||
}
|
||||
export const LessonList: React.FC<ILessonListProps> = props => {
|
||||
const { course, render: Item, ...stackProps } = props;
|
||||
const {
|
||||
userStore: { me },
|
||||
} = useRootStore();
|
||||
const bg = useColorModeValue('gray.50', 'gray.700');
|
||||
|
||||
if (!me) return null;
|
||||
|
||||
const isCourseAvailable = helpers.userHasCourse(course, me.courses);
|
||||
|
||||
return (
|
||||
<Stack spacing={'1'} mt='2' overflowY={'auto'} maxH={'445px'} pr='5px' {...stackProps}>
|
||||
{course.lessons.map(lesson => (
|
||||
<Item
|
||||
key={lesson.id}
|
||||
id={lesson.id}
|
||||
title={lesson.title}
|
||||
duration={lesson.duration}
|
||||
isDisabled={!isCourseAvailable}
|
||||
bg={bg}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
interface ILessonItemProps extends LinkBoxProps {
|
||||
id: string;
|
||||
title: string;
|
||||
duration: string;
|
||||
isDisabled?: boolean | undefined;
|
||||
}
|
||||
export const LessonItem: React.FC<ILessonItemProps> = props => {
|
||||
const { id, title, duration, isDisabled, ...linkBoxProps } = props;
|
||||
return (
|
||||
<LinkBox
|
||||
as='div'
|
||||
display={'flex'}
|
||||
fontSize={'sm'}
|
||||
justifyContent={'space-between'}
|
||||
pl='5'
|
||||
pr='5'
|
||||
pb='5'
|
||||
pt='5'
|
||||
minH='85px'
|
||||
borderRadius={'xl'}
|
||||
transition={'ease-in-out .1s'}
|
||||
_hover={
|
||||
!isDisabled
|
||||
? {
|
||||
bg: 'blue.500',
|
||||
color: 'white',
|
||||
}
|
||||
: {}
|
||||
}
|
||||
{...linkBoxProps}
|
||||
>
|
||||
<Flex alignItems={'center'} fontWeight='medium' userSelect={'none'}>
|
||||
<Icon as={isDisabled ? MdOutlineLock : MdPlayCircleOutline} w={5} h={5} mr='2' />
|
||||
{!isDisabled ? (
|
||||
<LinkOverlay as={ReactRouterLink} to={`/lesson/${id}`} overflowY={'hidden'} maxH='60px'>
|
||||
{title}
|
||||
</LinkOverlay>
|
||||
) : (
|
||||
<Text overflowY={'hidden'} maxH='60px'>
|
||||
{title}
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
<Text as='span'>{moment.duration(duration).asMinutes()}min</Text>
|
||||
</LinkBox>
|
||||
);
|
||||
};
|
||||
@ -8,6 +8,7 @@ import AuthPage from '@educt/pages/auth/auth';
|
||||
import NotFoundPage from '@educt/pages/404/404';
|
||||
import CreateCoursePage from '@educt/pages/courses/create/create';
|
||||
import CreateLessonPage from '@educt/pages/courses/create-lesson/create-lesson';
|
||||
import CoursePage from '@educt/pages/course/course';
|
||||
import EditCoursePage from '@educt/pages/courses/edit/edit';
|
||||
import ChangePasswordPage from '@educt/pages/profile/change-password/change-password';
|
||||
import ChangeEmailPage from '@educt/pages/profile/change-email/change-email';
|
||||
@ -32,6 +33,13 @@ export const routes: IRouteOption[] = [
|
||||
exact: true,
|
||||
isPrivate: true,
|
||||
},
|
||||
{
|
||||
path: '/course/:id',
|
||||
title: 'Course',
|
||||
component: CoursePage,
|
||||
isPrivate: true,
|
||||
exact: true,
|
||||
},
|
||||
{
|
||||
path: '/courses',
|
||||
title: 'Courses',
|
||||
|
||||
@ -35,9 +35,7 @@ class CourseService {
|
||||
* @param id Course id
|
||||
* @returns Course
|
||||
*/
|
||||
public async fetchById(
|
||||
id: string
|
||||
): Promise<IApiRespose<Omit<ICourse, 'students_count' | 'likes_count' | 'lessons_count'>>> {
|
||||
public async fetchById(id: string): Promise<IApiRespose<ICourse>> {
|
||||
const result = await this.api.get(`/v1/courses/${id}`);
|
||||
return result.data;
|
||||
}
|
||||
@ -56,6 +54,28 @@ class CourseService {
|
||||
return result.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set course like
|
||||
*
|
||||
* @param id Course id
|
||||
* @returns Empty data
|
||||
*/
|
||||
public async setLike(id: string): Promise<IApiRespose<Record<string, never>>> {
|
||||
const result = await this.api.put(`/v1/courses/${id}/likes`);
|
||||
return result.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unset course like
|
||||
*
|
||||
* @param id Course id
|
||||
* @returns Empty data
|
||||
*/
|
||||
public async unsetLike(id: string): Promise<IApiRespose<Record<string, never>>> {
|
||||
const result = await this.api.delete(`/v1/courses/${id}/likes`);
|
||||
return result.data;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param data Data for creating course
|
||||
|
||||
@ -62,7 +62,7 @@ export default class UserStore {
|
||||
public async loadCurrentUserData() {
|
||||
const result = await UserServiceInstance.fetchMe();
|
||||
const {
|
||||
data: { id, first_name, last_name, fullname, email, roles, contacts, courses, created_at, updated_at },
|
||||
data: { id, first_name, last_name, fullname, email, roles, likes, contacts, courses, created_at, updated_at },
|
||||
} = result;
|
||||
|
||||
runInAction(() => {
|
||||
@ -73,6 +73,7 @@ export default class UserStore {
|
||||
email,
|
||||
roles,
|
||||
fullname,
|
||||
likes,
|
||||
contacts,
|
||||
courses,
|
||||
isAdmin: helpers.userContainRoles(roles, [UserRoleEnum.ADMIN]),
|
||||
|
||||
24
src/theme.ts
Normal file
24
src/theme.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { extendTheme } from '@chakra-ui/react';
|
||||
|
||||
export const theme = extendTheme({
|
||||
styles: {
|
||||
global: {
|
||||
body: {
|
||||
scrollbarWidth: 'thin',
|
||||
scrollbarColor: '#000',
|
||||
},
|
||||
'::-webkit-scrollbar': {
|
||||
width: '5px',
|
||||
height: '5px',
|
||||
},
|
||||
'::-webkit-scrollbar-track': {
|
||||
backgroundColor: 'gray.100',
|
||||
borderRadius: '100px',
|
||||
},
|
||||
'::-webkit-scrollbar-thumb': {
|
||||
background: 'blue.600',
|
||||
borderRadius: '100px',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user