diff --git a/src/components/ErrorFallback.tsx b/src/components/ErrorFallback.tsx
index 6a1cac2..eb76168 100644
--- a/src/components/ErrorFallback.tsx
+++ b/src/components/ErrorFallback.tsx
@@ -1,15 +1,22 @@
import React from 'react';
import Cookies from 'js-cookie';
-import { useToast, Flex, Box, Heading, Text } from '@chakra-ui/react';
+import { Flex, Box, Heading, Text } from '@chakra-ui/react';
import { MdError } from 'react-icons/md';
+
+/**
+ * Hooks
+ */
+import { useEffect } from 'react';
+import { useToast } from '@chakra-ui/toast';
import { useRootStore } from '@educt/hooks/useRootStore';
+import { Redirect } from 'react-router';
const ErrorFallback: React.FC<{ error: any }> = props => {
const { userStore, authStore } = useRootStore();
const { error } = props;
const toast = useToast();
- React.useEffect(() => {
+ useEffect(() => {
toast({ title: `${error.message}`, duration: 10000, isClosable: true, status: 'error' });
});
@@ -18,7 +25,7 @@ const ErrorFallback: React.FC<{ error: any }> = props => {
userStore.me = null;
authStore.setIsLoggedIn(false);
Cookies.remove('logged_in');
- }
+ } else if (error.response.status === 404) return ;
}
return (
diff --git a/src/components/Forms/CourseForm/CourseForm.tsx b/src/components/Forms/CourseForm/CourseForm.tsx
new file mode 100644
index 0000000..9183e10
--- /dev/null
+++ b/src/components/Forms/CourseForm/CourseForm.tsx
@@ -0,0 +1,210 @@
+import React from 'react';
+import { Button, Textarea, InputGroup, InputRightElement, Input } from '@chakra-ui/react';
+import { FormControl, FormLabel, FormHelperText } from '@chakra-ui/form-control';
+import { Flex, Box, Text, Divider, Stack } from '@chakra-ui/layout';
+import { Controller, UseFormReturn } from 'react-hook-form';
+
+/**
+ * Types
+ */
+import { OptionType } from '@educt/types';
+import { UserRoleEnum } from '@educt/enums';
+
+/**
+ * Components
+ */
+import AsyncSelect from '@educt/components/AsyncSelect';
+import FileSelect from '@educt/components/FileSelect';
+
+/**
+ * Hooks
+ */
+import { useToast } from '@chakra-ui/toast';
+import { useRootStore } from '@educt/hooks/useRootStore';
+import { useErrorHandler } from 'react-error-boundary';
+
+export type InputFields = {
+ title: string;
+ image?: File | undefined;
+ category_id: string;
+ description: string;
+ teacher_id: string;
+};
+
+export type CourseFormPropsType = {
+ onSubmit: () => Promise;
+ buttonLabel: string;
+ isLoading: boolean;
+ reactHookForm: UseFormReturn;
+};
+
+const CourseForm: React.FC = ({ onSubmit, buttonLabel, isLoading, reactHookForm }) => {
+ const {
+ control,
+ register,
+ watch,
+ formState: { errors, isDirty },
+ } = reactHookForm;
+
+ const {
+ userStore: { userService },
+ categoryStore: { categoryService },
+ } = useRootStore();
+ const handleError = useErrorHandler();
+ const toast = useToast();
+
+ /**
+ * Load users into select field
+ */
+ const loadUsersOptions = async (): Promise => {
+ try {
+ const users = await userService.fetchAll({ limit: 12, role: UserRoleEnum.TEACHER });
+
+ return users.data.map(user => ({
+ label: user.fullname,
+ value: user.id,
+ }));
+ } catch (error: any) {
+ if (error.response) {
+ toast({ title: 'Error' });
+ } else {
+ handleError(error);
+ }
+ }
+ };
+
+ /**
+ * Load categories into select filed
+ */
+ const loadCategoriesOptions = async (): Promise => {
+ try {
+ const categories = await categoryService.fetchAll();
+ return categories.data.map(category => ({
+ label: category.title,
+ value: category.id,
+ }));
+ } catch (error: any) {
+ if (error.response) {
+ toast({ title: 'Error' });
+ } else {
+ handleError(error);
+ }
+ }
+ };
+
+ const watchTitle = watch('title');
+
+ return (
+
+ );
+};
+
+export default CourseForm;
diff --git a/src/pages/courses/create/components/CreateCourseForm.validator.ts b/src/components/Forms/CourseForm/CourseForm.validator.ts
similarity index 100%
rename from src/pages/courses/create/components/CreateCourseForm.validator.ts
rename to src/components/Forms/CourseForm/CourseForm.validator.ts
diff --git a/src/components/Forms/CourseForm/CreateCourseForm.tsx b/src/components/Forms/CourseForm/CreateCourseForm.tsx
new file mode 100644
index 0000000..e98400c
--- /dev/null
+++ b/src/components/Forms/CourseForm/CreateCourseForm.tsx
@@ -0,0 +1,91 @@
+import React from 'react';
+import { Box } from '@chakra-ui/layout';
+import { yupResolver } from '@hookform/resolvers/yup';
+import { SubmitHandler } from 'react-hook-form';
+
+/**
+ * Types
+ */
+import { InputFields } from './CourseForm';
+import { CourseStatusEnum } from '@educt/enums';
+
+/**
+ * Components
+ */
+import CourseForm from './CourseForm';
+
+/**
+ * Hooks
+ */
+import { useState } from 'react';
+import { useHistory } from 'react-router';
+import { useForm } from 'react-hook-form';
+import { useToast } from '@chakra-ui/toast';
+import { useRootStore } from '@educt/hooks/useRootStore';
+import useIsMountedRef from '@educt/hooks/useIsMountedRef';
+import { useErrorHandler } from 'react-error-boundary';
+
+/**
+ * Schema
+ */
+import CourseFormSchema from './CourseForm.validator';
+
+type CreateFormCoursePropsType = {};
+
+const CreateFormCourse: React.FC = () => {
+ const [isLoading, setIsLoading] = useState(false);
+ const {
+ courseStore: { courseService },
+ } = useRootStore();
+ const form = useForm({ resolver: yupResolver(CourseFormSchema) });
+ const isMountedRef = useIsMountedRef();
+ const history = useHistory();
+ const toast = useToast();
+ const handleError = useErrorHandler();
+
+ /**
+ * Submit handler
+ */
+ const onSubmit: SubmitHandler = async data => {
+ try {
+ setIsLoading(true);
+ const course = await courseService.create({
+ title: data.title,
+ description: data.description,
+ teacher_id: data.teacher_id,
+ category_id: data.category_id,
+ image: data.image,
+ status: CourseStatusEnum.DRAFT,
+ });
+ toast({ title: `Course successfully created.`, status: 'success' });
+ history.push(`/courses/edit/${course.data.id}`);
+ } catch (error: any) {
+ if (error.response) {
+ if (error.response.status === 422) {
+ toast({ title: `${error.response.data.errors[0].message}`, status: 'error' });
+ } else {
+ toast({ title: `${error.message}`, status: 'error' });
+ }
+ } else {
+ handleError(error);
+ }
+ } finally {
+ if (isMountedRef.current) {
+ setIsLoading(false);
+ }
+ }
+ };
+
+ return (
+
+
+
+ );
+};
+
+export default CreateFormCourse;
diff --git a/src/components/Forms/CourseForm/EditCourseForm.tsx b/src/components/Forms/CourseForm/EditCourseForm.tsx
new file mode 100644
index 0000000..9026d7a
--- /dev/null
+++ b/src/components/Forms/CourseForm/EditCourseForm.tsx
@@ -0,0 +1,108 @@
+import React from 'react';
+import { Box } from '@chakra-ui/layout';
+import { yupResolver } from '@hookform/resolvers/yup';
+import { SubmitHandler } from 'react-hook-form';
+
+/**
+ * Types
+ */
+import { InputFields } from './CourseForm';
+
+/**
+ * Components
+ */
+import CourseForm from './CourseForm';
+
+/**
+ * Hooks
+ */
+import { useState } from 'react';
+import { useParams } from 'react-router';
+import { useForm } from 'react-hook-form';
+import { useRootStore } from '@educt/hooks/useRootStore';
+import { useErrorHandler } from 'react-error-boundary';
+import { useToast } from '@chakra-ui/toast';
+
+/**
+ * Schema
+ */
+import CourseFormSchema from './CourseForm.validator';
+
+type EditFormCoursePropsType = {
+ defaultValues?:
+ | {
+ title?: string | undefined;
+ image?: File | undefined;
+ category_id?: string | undefined;
+ description?: string | undefined;
+ teacher_id?: string | undefined;
+ }
+ | undefined;
+};
+
+const EditFormCourse: React.FC = ({ defaultValues }) => {
+ const [isLoading, setIsLoading] = useState(false);
+ const {
+ courseStore: { courseService },
+ } = useRootStore();
+ const form = useForm({
+ resolver: yupResolver(CourseFormSchema),
+ defaultValues: {
+ title: defaultValues?.title || '',
+ description: defaultValues?.description || '',
+ category_id: defaultValues?.category_id || '',
+ teacher_id: defaultValues?.teacher_id || '',
+ image: undefined,
+ },
+ });
+ const toast = useToast();
+ const handleError = useErrorHandler();
+ const { id } = useParams<{ id: string }>();
+
+ /**
+ * Submit handler
+ */
+ const onSubmit: SubmitHandler = async data => {
+ try {
+ setIsLoading(true);
+ const course = await courseService.update(id, {
+ title: data.title,
+ description: data.description,
+ teacher_id: data.teacher_id,
+ category_id: data.category_id,
+ });
+ toast({ title: `Course updated.`, status: 'info' });
+
+ /**
+ * Update fields with new values
+ */
+ form.reset({
+ title: course.data.title,
+ description: course.data.description,
+ teacher_id: course.data.teacher.id,
+ category_id: course.data.category.id,
+ });
+ } catch (error: any) {
+ if (error.response) {
+ toast({ title: `${error.message}`, status: 'error' });
+ } else {
+ handleError(error);
+ }
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return (
+
+
+
+ );
+};
+
+export default EditFormCourse;
diff --git a/src/components/Forms/CourseForm/index.ts b/src/components/Forms/CourseForm/index.ts
new file mode 100644
index 0000000..4b5f23d
--- /dev/null
+++ b/src/components/Forms/CourseForm/index.ts
@@ -0,0 +1,4 @@
+import CreateCourseForm from './CreateCourseForm';
+import EditCourseForm from './EditCourseForm';
+
+export { CreateCourseForm, EditCourseForm };
diff --git a/src/hooks/useFetchCourseQuery.ts b/src/hooks/useFetchCourseQuery.ts
new file mode 100644
index 0000000..c88147d
--- /dev/null
+++ b/src/hooks/useFetchCourseQuery.ts
@@ -0,0 +1,49 @@
+import { ICourse } from '@educt/interfaces';
+
+/**
+ * Hooks
+ */
+import { useEffect, useState } from 'react';
+import { useErrorHandler } from 'react-error-boundary';
+import { useRootStore } from './useRootStore';
+
+type CourseStateType = {
+ data: Omit | null;
+ error: any;
+ loading: boolean;
+ fetched: boolean;
+};
+
+const useFetchCourseQuery = (id: string) => {
+ const {
+ courseStore: { courseService },
+ } = useRootStore();
+ const [state, setState] = useState({ data: null, error: null, loading: false, fetched: false });
+ const handleError = useErrorHandler();
+
+ /**
+ * Fetch course handler
+ */
+ useEffect(() => {
+ setState(s => ({ ...s, loading: true }));
+ courseService
+ .fetchById(id)
+ .then(data => {
+ setState(s => ({ ...s, data: data.data }));
+ })
+ .catch(error => {
+ if (error.response) {
+ setState(s => ({ ...s, error }));
+ } else {
+ handleError(error);
+ }
+ })
+ .finally(() => {
+ setState(s => ({ ...s, loading: false, fetched: true }));
+ });
+ }, [id]);
+
+ return { ...state };
+};
+
+export default useFetchCourseQuery;
diff --git a/src/pages/courses/components/CourseList/CourseItem.tsx b/src/pages/courses/components/CourseList/CourseItem.tsx
index f352282..b171747 100644
--- a/src/pages/courses/components/CourseList/CourseItem.tsx
+++ b/src/pages/courses/components/CourseList/CourseItem.tsx
@@ -21,6 +21,7 @@ import CourseStatusBadge from '@educt/components/CourseStatusBadge';
/**
* Hooks
*/
+import { useHistory } from 'react-router';
import { useColorMode } from '@chakra-ui/color-mode';
import { useRootStore } from '@educt/hooks/useRootStore';
@@ -35,6 +36,7 @@ const CourseItem: React.FC = ({ course, onDelete, onSetStat
userStore: { me },
} = useRootStore();
const { colorMode } = useColorMode();
+ const history = useHistory();
if (me === null) return null;
@@ -54,7 +56,9 @@ const CourseItem: React.FC = ({ course, onDelete, onSetStat
_active={{ backgroundColor: colorMode === 'dark' ? 'gray.600' : 'gray.200' }}
/>
- }>Edit course
+
{(() => {
switch (course.status) {
case CourseStatusEnum.DRAFT:
diff --git a/src/pages/courses/create/components/CreateCourseForm.tsx b/src/pages/courses/create/components/CreateCourseForm.tsx
deleted file mode 100644
index eb77ecc..0000000
--- a/src/pages/courses/create/components/CreateCourseForm.tsx
+++ /dev/null
@@ -1,266 +0,0 @@
-import React, { useState } from 'react';
-import {
- Box,
- Grid,
- useToast,
- Text,
- Flex,
- Stack,
- Button,
- Textarea,
- FormControl,
- FormLabel,
- InputGroup,
- InputRightElement,
- Input,
- FormHelperText,
- Divider,
-} from '@chakra-ui/react';
-import { SubmitHandler, useForm, useWatch, Controller } from 'react-hook-form';
-import { yupResolver } from '@hookform/resolvers/yup';
-import AsyncSelect from '@educt/components/AsyncSelect';
-import FileSelect from '@educt/components/FileSelect';
-
-/**
- * Types
- */
-import { OptionType } from '@educt/types';
-import { CourseStatusEnum, UserRoleEnum } from '@educt/enums';
-
-/**
- * Hooks
- */
-import { useHistory } from 'react-router';
-import { useRootStore } from '@educt/hooks/useRootStore';
-import { useErrorHandler } from 'react-error-boundary';
-import useIsMountedRef from '@educt/hooks/useIsMountedRef';
-
-/**
- * Schema
- */
-import CreateCourseSchema from './CreateCourseForm.validator';
-
-type CreateCourseInputType = {
- title: string;
- image: File;
- category_id: string;
- description: string;
- teacher_id: string;
-};
-type CreateCourseFormPropsType = {};
-
-const CreateCourseForm: React.FC = () => {
- const {
- userStore: { userService },
- categoryStore: { categoryService },
- courseStore: { courseService },
- } = useRootStore();
- const {
- register,
- handleSubmit,
- formState: { errors },
- control,
- } = useForm({
- resolver: yupResolver(CreateCourseSchema),
- });
- const handleError = useErrorHandler();
- const toast = useToast();
- const history = useHistory();
- const isMountedRef = useIsMountedRef();
- const [isLoading, setIsLoading] = useState(false);
- const [status, setStatus] = useState(undefined);
-
- const title = useWatch({
- control,
- name: 'title',
- defaultValue: '',
- });
-
- /**
- * Load users into select field
- */
- const loadUsersOptions = async (): Promise => {
- try {
- const users = await userService.fetchAll({ limit: 12, role: UserRoleEnum.TEACHER });
-
- return users.data.map(user => ({
- label: user.fullname,
- value: user.id,
- }));
- } catch (error: any) {
- if (error.response) {
- toast({ title: 'Error' });
- } else {
- handleError(error);
- }
- }
- };
-
- /**
- * Load categories into select filed
- */
- const loadCategoriesOptions = async (): Promise => {
- try {
- const categories = await categoryService.fetchAll();
- return categories.data.map(category => ({
- label: category.title,
- value: category.id,
- }));
- } catch (error: any) {
- if (error.response) {
- toast({ title: 'Error' });
- } else {
- handleError(error);
- }
- }
- };
-
- /**
- * Submit form handler
- */
- const onSubmit: SubmitHandler = async data => {
- try {
- setIsLoading(true);
- await courseService.create({
- title: data.title,
- description: data.description,
- teacher_id: data.teacher_id,
- category_id: data.category_id,
- image: data.image,
- status: status ?? CourseStatusEnum.DRAFT,
- });
- toast({ title: `Course successfully created.`, status: 'success' });
- history.push('/courses');
- } catch (error: any) {
- if (error.response) {
- if (error.response.status === 422) {
- toast({ title: `${error.response.data.errors[0].message}`, status: 'error' });
- } else {
- toast({ title: `${error.message}`, status: 'error' });
- }
- } else {
- handleError(error);
- }
- } finally {
- if (isMountedRef.current) {
- setIsLoading(false);
- setStatus(undefined);
- }
- }
- };
-
- return (
-
-
-
- );
-};
-
-export default CreateCourseForm;
diff --git a/src/pages/courses/create/components/index.ts b/src/pages/courses/create/components/index.ts
deleted file mode 100644
index 158a67b..0000000
--- a/src/pages/courses/create/components/index.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import CreateCourseForm from './CreateCourseForm';
-
-export default CreateCourseForm;
diff --git a/src/pages/courses/create/create.tsx b/src/pages/courses/create/create.tsx
index 817c900..7ca1583 100644
--- a/src/pages/courses/create/create.tsx
+++ b/src/pages/courses/create/create.tsx
@@ -10,7 +10,7 @@ import { IPageProps } from '@educt/interfaces';
* Components
*/
import PrevPageButton from '@educt/components/PrevPageButton';
-import CreateCourseForm from './components';
+import { CreateCourseForm } from '@educt/components/Forms/CourseForm';
/**
* Create course page
diff --git a/src/pages/courses/edit/edit.tsx b/src/pages/courses/edit/edit.tsx
new file mode 100644
index 0000000..2feb454
--- /dev/null
+++ b/src/pages/courses/edit/edit.tsx
@@ -0,0 +1,87 @@
+import React from 'react';
+import Helmet from 'react-helmet';
+import { Redirect } from 'react-router-dom';
+import { Box, Heading, Text, Tab, TabList, Tabs, TabPanels, TabPanel, Button } from '@chakra-ui/react';
+
+/**
+ * Types
+ */
+import { IPageProps } from '@educt/interfaces';
+
+/**
+ * Components
+ */
+import LoadingPage from '@educt/components/LoadingPage';
+import { EditCourseForm } from '@educt/components/Forms/CourseForm';
+
+/**
+ * Hooks
+ */
+import useFetchCourseQuery from '@educt/hooks/useFetchCourseQuery';
+import { useParams } from 'react-router-dom';
+
+/**
+ * Course editor
+ */
+const EditCoursePage: React.FC = () => {
+ const params = useParams<{ id: string }>();
+ const { data: course, loading, error } = useFetchCourseQuery(params.id);
+
+ /**
+ * Not Found
+ */
+ if (error?.response?.status === 404) return ;
+
+ /**
+ * Loading
+ */
+ if (loading || course === null) return ;
+
+ return (
+ <>
+
+ Editing {course.title}
+
+
+ Course editor
+ {course.title}
+
+
+
+
+ Info
+ Lessons
+ Students
+
+
+
+
+
+
+
+ {course.lessons.length === 0 ? (
+
+ No lessons have been added to this course yet
+
+
+ ) : null}
+
+
+
+
+
+ >
+ );
+};
+
+export default EditCoursePage;
diff --git a/src/pages/profile/components/UpdateUserContactsForm/UpdateUserContactsForm.tsx b/src/pages/profile/components/UpdateUserContactsForm/UpdateUserContactsForm.tsx
index 1de20c8..ff9c601 100644
--- a/src/pages/profile/components/UpdateUserContactsForm/UpdateUserContactsForm.tsx
+++ b/src/pages/profile/components/UpdateUserContactsForm/UpdateUserContactsForm.tsx
@@ -69,7 +69,6 @@ const UpdateUserContactsForm: React.FC = ({ con
telegram_id: data.telegram_id ?? '',
vk_id: data.vk_id ?? '',
});
- setIsLoading(false);
} catch (error: any) {
if (error.response) {
switch (error.response.status) {
diff --git a/src/routes.config.ts b/src/routes.config.ts
index 7011a1f..cf0e786 100644
--- a/src/routes.config.ts
+++ b/src/routes.config.ts
@@ -7,6 +7,7 @@ import { IPrivateRouteProps } from './components/PrivateRoute/PrivateRoute';
import AuthPage from '@educt/pages/auth/auth';
import NotFoundPage from '@educt/pages/404/404';
import CreateCoursePage from '@educt/pages/courses/create/create';
+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';
import { UserRoleEnum } from './enums';
@@ -43,6 +44,14 @@ export const routes: IRouteOption[] = [
isPrivate: true,
roles: [UserRoleEnum.ADMIN, UserRoleEnum.TEACHER],
},
+ {
+ path: '/courses/edit/:id',
+ title: 'Course editor',
+ component: EditCoursePage,
+ isPrivate: true,
+ exact: true,
+ roles: [UserRoleEnum.ADMIN, UserRoleEnum.TEACHER],
+ },
{
path: '/lessons',
title: 'Lessons',
diff --git a/src/services/CourseService.ts b/src/services/CourseService.ts
index 77af6aa..257c6ee 100644
--- a/src/services/CourseService.ts
+++ b/src/services/CourseService.ts
@@ -1,7 +1,7 @@
import { AxiosInstance } from 'axios';
import * as helpers from '@educt/helpers';
import { IApiRespose, ICourse } from '@educt/interfaces';
-import { CreateCourseParamsType, FetchCoursesParams } from '@educt/types';
+import { CreateCourseParamsType, FetchCoursesParams, UpdateCourseParamsType } from '@educt/types';
import { CourseStatusEnum } from '@educt/enums';
export class CourseService {
@@ -28,6 +28,19 @@ export class CourseService {
return result.data;
}
+ /**
+ * Fetch course by id param
+ *
+ * @param id Course id
+ * @returns Course
+ */
+ public async fetchById(
+ id: string
+ ): Promise>> {
+ const result = await this.api.get(`/v1/courses/${id}`);
+ return result.data;
+ }
+
/**
* Update status of course by id
*
@@ -59,6 +72,15 @@ export class CourseService {
return result.data;
}
+ public async update(
+ id: string,
+ data: UpdateCourseParamsType
+ ): Promise>> {
+ const result = await this.api.patch(`/v1/courses/${id}`, data);
+
+ return result.data;
+ }
+
/**
* Delete course by id
*
diff --git a/src/types/index.d.ts b/src/types/index.d.ts
index 45ab36a..a8b3f04 100644
--- a/src/types/index.d.ts
+++ b/src/types/index.d.ts
@@ -135,6 +135,15 @@ export type CreateCourseParamsType = {
description: string;
teacher_id: string;
category_id: string;
- image: File | undefined;
+ image?: File | undefined;
status: CourseStatusEnum;
};
+
+export type UpdateCourseParamsType = {
+ title?: string | undefined;
+ description?: string | undefined;
+ teacher_id?: string | undefined;
+ category_id?: string | undefined;
+ image?: File | undefined;
+ status?: CourseStatusEnum | undefined;
+};