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}
+
+
+ } variant='ghost'>
+ {likes}
+
+
+
+ );
+};
+
+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',
+ },
+ },
+ },
+});