diff --git a/index.html b/index.html index cd4d8a1..39f5472 100644 --- a/index.html +++ b/index.html @@ -1,5 +1,5 @@ - + diff --git a/src/components/Modals/EditUserModal/EditUserModal.tsx b/src/components/Modals/EditUserModal/EditUserModal.tsx index ceb6611..64d371f 100644 --- a/src/components/Modals/EditUserModal/EditUserModal.tsx +++ b/src/components/Modals/EditUserModal/EditUserModal.tsx @@ -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 = 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 = props => { } = useForm({ resolver: yupResolver(EditUserSchema), defaultValues: useMemo(() => { - return props.user; + return defaultValues; }, [props]), }); @@ -78,7 +84,7 @@ const EditUserModal: React.FC = props => { }; useEffect(() => { - reset(props.user); + reset(defaultValues); }, [props.user]); return ( diff --git a/src/helpers/index.ts b/src/helpers/index.ts index 8b65582..9cf8a9e 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -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(array: T[], from: number, to: number) { export function getDirtyFields(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 | undefined) { + if (!userCourses) return true; + return userCourses.some(course => course.id === findingCourse.id); +} diff --git a/src/hooks/queries/course/useFetchCourse.ts b/src/hooks/queries/course/useFetchCourse.ts index 7f00f53..636ad5a 100644 --- a/src/hooks/queries/course/useFetchCourse.ts +++ b/src/hooks/queries/course/useFetchCourse.ts @@ -10,15 +10,13 @@ import { CourseServiceInstance } from '@educt/services'; import { useEffect } from 'react'; import useAsync from '../../useAsync'; -type CourseResultDataType = Omit; - const useFetchCourse = (id: string) => { const fetch = async (id: string) => { const { data } = await CourseServiceInstance.fetchById(id); return data; }; - const { execute: fetchCourseById, ...state } = useAsync>(fetch); + const { execute: fetchCourseById, ...state } = useAsync>(fetch); useEffect(() => { fetchCourseById(id); diff --git a/src/index.tsx b/src/index.tsx index c2f00a8..085d093 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -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( diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index 3d36eaf..636fde6 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -107,6 +107,7 @@ export interface IMe extends IUser { * does not have the STUDENT role */ courses?: UserCourseType[]; + likes: Array>; isAdmin: boolean; isTeacher: boolean; isStudent: boolean; diff --git a/src/pages/404/404.tsx b/src/pages/404/404.tsx index 53d58a9..2d67e41 100644 --- a/src/pages/404/404.tsx +++ b/src/pages/404/404.tsx @@ -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 = ({ title }) => { useEffect(() => { toast({ title: `Page not found`, duration: 10000, isClosable: true, status: 'warning' }); - }); + }, []); return ( <> diff --git a/src/pages/auth/auth.tsx b/src/pages/auth/auth.tsx index e7f4288..1a28914 100644 --- a/src/pages/auth/auth.tsx +++ b/src/pages/auth/auth.tsx @@ -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 = ({ title }) => { {title} - - - - Login to your account - + + diff --git a/src/pages/auth/components/AuthForm/AuthForm.tsx b/src/pages/auth/components/AuthForm/AuthForm.tsx index 74186b2..60dd7e2 100644 --- a/src/pages/auth/components/AuthForm/AuthForm.tsx +++ b/src/pages/auth/components/AuthForm/AuthForm.tsx @@ -84,7 +84,7 @@ const AuthForm: React.FC = () => { + + + + + + + About + + + + + + + About this course + + {course.description} + + {!isEmptyStudents ? ( + + + Students: + + + {course.students.map(student => ( + + ))} + + + ) : ( + + + 0 students + + + )} + + + + + + + + + + + What you'll learn. + + + Lorem, ipsum dolor sit amet consectetur adipisicing elit. Iste temporibus harum eos praesentium hic, + sunt corporis deserunt impedit natus consequuntur? + + + + + + + + + Content + + + {course.lessons_count} lessons + + + {!isEmptyLessons ? ( + + ) : ( + + + There are no lessons here yet + + + )} + + + + + + + ); +}; + +export default observer(CoursePage); diff --git a/src/pages/course/index.tsx b/src/pages/course/index.tsx new file mode 100644 index 0000000..79d0d75 --- /dev/null +++ b/src/pages/course/index.tsx @@ -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 = props => ( + + {props.children} + +); + +interface ICourseHeadingProps extends BoxProps { + title: string; + teacher: ICourse['teacher']; + category: ICourse['category']; +} +export const CourseHeading: React.FC = props => { + const { title, teacher, category, ...boxProps } = props; + + return ( + + + {title} + + + {teacher.fullname} •  + + {category.title} + + + + ); +}; + +interface ICourseInfoProps extends BoxProps { + id: string; + lessonsCount: string; + studentsCount: string; + likesCount: string; +} +export const CourseInfo: React.FC = props => { + const { userStore } = useRootStore(); + const { me } = userStore; + const { id, lessonsCount, studentsCount, likesCount, ...boxProps } = props; + const [isLoading, setIsLoading] = useState(false); + const [likes, setLikes] = useState(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 ( + + + + + {lessonsCount} + + + + + {studentsCount} + + + + + + ); +}; + +export const CourseBackgroundImage: React.FC = props => { + return ( + + ); +}; + +interface ILessonListProps extends StackProps { + course: ICourse; + render: React.FC; +} +export const LessonList: React.FC = 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 ( + + {course.lessons.map(lesson => ( + + ))} + + ); +}; + +interface ILessonItemProps extends LinkBoxProps { + id: string; + title: string; + duration: string; + isDisabled?: boolean | undefined; +} +export const LessonItem: React.FC = props => { + const { id, title, duration, isDisabled, ...linkBoxProps } = props; + return ( + + + + {!isDisabled ? ( + + {title} + + ) : ( + + {title} + + )} + + {moment.duration(duration).asMinutes()}min + + ); +}; diff --git a/src/routes.config.ts b/src/routes.config.ts index dc7357d..75b90ba 100644 --- a/src/routes.config.ts +++ b/src/routes.config.ts @@ -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', diff --git a/src/services/CourseService.ts b/src/services/CourseService.ts index 2078ad6..82cad95 100644 --- a/src/services/CourseService.ts +++ b/src/services/CourseService.ts @@ -35,9 +35,7 @@ class CourseService { * @param id Course id * @returns Course */ - public async fetchById( - id: string - ): Promise>> { + public async fetchById(id: string): Promise> { 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>> { + 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>> { + const result = await this.api.delete(`/v1/courses/${id}/likes`); + return result.data; + } + /** * * @param data Data for creating course diff --git a/src/stores/UserStore.ts b/src/stores/UserStore.ts index 423e175..aef29e3 100644 --- a/src/stores/UserStore.ts +++ b/src/stores/UserStore.ts @@ -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]), diff --git a/src/theme.ts b/src/theme.ts new file mode 100644 index 0000000..2af47ec --- /dev/null +++ b/src/theme.ts @@ -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', + }, + }, + }, +});