refactor: editing base fields of course

This commit is contained in:
Sergey Yarkov 2021-11-13 23:01:45 +03:00
parent cd2af7eae1
commit aa351d1c15
16 changed files with 607 additions and 277 deletions

View File

@ -1,15 +1,22 @@
import React from 'react'; import React from 'react';
import Cookies from 'js-cookie'; 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'; import { MdError } from 'react-icons/md';
/**
* Hooks
*/
import { useEffect } from 'react';
import { useToast } from '@chakra-ui/toast';
import { useRootStore } from '@educt/hooks/useRootStore'; import { useRootStore } from '@educt/hooks/useRootStore';
import { Redirect } from 'react-router';
const ErrorFallback: React.FC<{ error: any }> = props => { const ErrorFallback: React.FC<{ error: any }> = props => {
const { userStore, authStore } = useRootStore(); const { userStore, authStore } = useRootStore();
const { error } = props; const { error } = props;
const toast = useToast(); const toast = useToast();
React.useEffect(() => { useEffect(() => {
toast({ title: `${error.message}`, duration: 10000, isClosable: true, status: 'error' }); toast({ title: `${error.message}`, duration: 10000, isClosable: true, status: 'error' });
}); });
@ -18,7 +25,7 @@ const ErrorFallback: React.FC<{ error: any }> = props => {
userStore.me = null; userStore.me = null;
authStore.setIsLoggedIn(false); authStore.setIsLoggedIn(false);
Cookies.remove('logged_in'); Cookies.remove('logged_in');
} } else if (error.response.status === 404) return <Redirect to='/404' />;
} }
return ( return (

View File

@ -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<void>;
buttonLabel: string;
isLoading: boolean;
reactHookForm: UseFormReturn<InputFields>;
};
const CourseForm: React.FC<CourseFormPropsType> = ({ 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<OptionType[] | undefined> => {
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<OptionType[] | undefined> => {
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 (
<form onSubmit={onSubmit}>
<Stack spacing='5'>
<FormControl isRequired id='title' isInvalid={!!errors.title}>
<FormLabel>Name</FormLabel>
<InputGroup>
<Input pr='60px' size='md' placeholder='Course name' type='text' {...register('title')} />
<InputRightElement
mr='2'
children={
<Text as='small' color={!!errors.title || watchTitle?.length > 90 ? 'red.500' : 'gray.500'}>
{watchTitle?.length || 0}/90
</Text>
}
/>
</InputGroup>
<FormHelperText>e.g Web Development in Javascript</FormHelperText>
<Text as='small' color='red.500'>
{errors.title?.message}
</Text>
</FormControl>
<FormControl isRequired id='description' isInvalid={!!errors.description}>
<FormLabel>Description</FormLabel>
<Textarea resize='none' minH='150px' placeholder='Course description...' {...register('description')} />
<Text as='small' color='red.500'>
{errors.description?.message}
</Text>
</FormControl>
<Flex justifyContent='space-between' flexDirection={{ base: 'column', lg: 'row' }}>
<Box flexGrow={1} flexBasis='0' mr={{ base: '0', lg: '7' }}>
<Stack spacing='5'>
<FormControl isRequired id='category_id' isInvalid={!!errors.category_id}>
<FormLabel>Category</FormLabel>
<Controller
control={control}
name='category_id'
render={({ field: { onChange, value } }) => (
<AsyncSelect
loadOptions={loadCategoriesOptions}
onChange={selected => onChange(selected)}
value={value}
placeholder='Select Category'
noOptionsMessage='There are no categories.'
loadingText='Wait...'
/>
)}
/>
<Text as='small' color='red.500'>
{errors.category_id?.message}
</Text>
</FormControl>
<FormControl isRequired id='teacher_id' isInvalid={!!errors.teacher_id}>
<FormLabel>Author</FormLabel>
<Controller
control={control}
name='teacher_id'
render={({ field: { onChange, value } }) => (
<AsyncSelect
loadOptions={loadUsersOptions}
onChange={selected => onChange(selected)}
value={value}
placeholder='Select Author'
noOptionsMessage='There are no teachers.'
loadingText='Wait...'
/>
)}
/>
<Text as='small' color='red.500'>
{errors.teacher_id?.message}
</Text>
</FormControl>
</Stack>
</Box>
<Box flexGrow={0} flexBasis='50%' mt={{ base: '5', lg: '0' }}>
<FormControl id='image' isInvalid={!!errors.image}>
<FormLabel>Image</FormLabel>
<Controller
control={control}
name='image'
render={({ field: { onChange } }) => (
<FileSelect onChange={file => onChange(file)} supportedFormats={['JPG', 'JPEG', 'PNG', 'WEBP']} />
)}
/>
<Text as='small' color='red.500'>
{errors.image?.message}
</Text>
</FormControl>
</Box>
</Flex>
</Stack>
<Divider mt='5' />
<Box mt='5'>
<Button
isLoading={isLoading}
isDisabled={!isDirty}
loadingText='Saving...'
type='submit'
colorScheme='blue'
variant='outline'
>
{buttonLabel}
</Button>
</Box>
</form>
);
};
export default CourseForm;

View File

@ -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<CreateFormCoursePropsType> = () => {
const [isLoading, setIsLoading] = useState<boolean>(false);
const {
courseStore: { courseService },
} = useRootStore();
const form = useForm<InputFields>({ resolver: yupResolver(CourseFormSchema) });
const isMountedRef = useIsMountedRef();
const history = useHistory();
const toast = useToast();
const handleError = useErrorHandler();
/**
* Submit handler
*/
const onSubmit: SubmitHandler<InputFields> = 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 (
<Box>
<CourseForm
reactHookForm={{ ...form }}
onSubmit={form.handleSubmit(onSubmit)}
isLoading={isLoading}
buttonLabel='Create'
/>
</Box>
);
};
export default CreateFormCourse;

View File

@ -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<EditFormCoursePropsType> = ({ defaultValues }) => {
const [isLoading, setIsLoading] = useState<boolean>(false);
const {
courseStore: { courseService },
} = useRootStore();
const form = useForm<InputFields>({
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<InputFields> = 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 (
<Box>
<CourseForm
reactHookForm={{ ...form }}
onSubmit={form.handleSubmit(onSubmit)}
isLoading={isLoading}
buttonLabel='Save'
/>
</Box>
);
};
export default EditFormCourse;

View File

@ -0,0 +1,4 @@
import CreateCourseForm from './CreateCourseForm';
import EditCourseForm from './EditCourseForm';
export { CreateCourseForm, EditCourseForm };

View File

@ -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<ICourse, 'students_count' | 'likes_count' | 'lessons_count'> | null;
error: any;
loading: boolean;
fetched: boolean;
};
const useFetchCourseQuery = (id: string) => {
const {
courseStore: { courseService },
} = useRootStore();
const [state, setState] = useState<CourseStateType>({ 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;

View File

@ -21,6 +21,7 @@ import CourseStatusBadge from '@educt/components/CourseStatusBadge';
/** /**
* Hooks * Hooks
*/ */
import { useHistory } from 'react-router';
import { useColorMode } from '@chakra-ui/color-mode'; import { useColorMode } from '@chakra-ui/color-mode';
import { useRootStore } from '@educt/hooks/useRootStore'; import { useRootStore } from '@educt/hooks/useRootStore';
@ -35,6 +36,7 @@ const CourseItem: React.FC<CourseItemPropsType> = ({ course, onDelete, onSetStat
userStore: { me }, userStore: { me },
} = useRootStore(); } = useRootStore();
const { colorMode } = useColorMode(); const { colorMode } = useColorMode();
const history = useHistory();
if (me === null) return null; if (me === null) return null;
@ -54,7 +56,9 @@ const CourseItem: React.FC<CourseItemPropsType> = ({ course, onDelete, onSetStat
_active={{ backgroundColor: colorMode === 'dark' ? 'gray.600' : 'gray.200' }} _active={{ backgroundColor: colorMode === 'dark' ? 'gray.600' : 'gray.200' }}
/> />
<MenuList> <MenuList>
<MenuItem icon={<EditIcon />}>Edit course</MenuItem> <MenuItem onClick={() => history.push(`/courses/edit/${course.id}`)} icon={<EditIcon />}>
Edit
</MenuItem>
{(() => { {(() => {
switch (course.status) { switch (course.status) {
case CourseStatusEnum.DRAFT: case CourseStatusEnum.DRAFT:

View File

@ -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<CreateCourseFormPropsType> = () => {
const {
userStore: { userService },
categoryStore: { categoryService },
courseStore: { courseService },
} = useRootStore();
const {
register,
handleSubmit,
formState: { errors },
control,
} = useForm<CreateCourseInputType>({
resolver: yupResolver(CreateCourseSchema),
});
const handleError = useErrorHandler();
const toast = useToast();
const history = useHistory();
const isMountedRef = useIsMountedRef();
const [isLoading, setIsLoading] = useState<boolean>(false);
const [status, setStatus] = useState<CourseStatusEnum | undefined>(undefined);
const title = useWatch({
control,
name: 'title',
defaultValue: '',
});
/**
* Load users into select field
*/
const loadUsersOptions = async (): Promise<OptionType[] | undefined> => {
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<OptionType[] | undefined> => {
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<CreateCourseInputType> = 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 (
<Box>
<form onSubmit={handleSubmit(onSubmit)}>
<Stack spacing='5'>
<FormControl isRequired id='title' isInvalid={!!errors.title}>
<FormLabel>Name</FormLabel>
<InputGroup>
<Input pr='60px' size='md' placeholder='Course name' type='text' {...register('title')} />
<InputRightElement
mr='2'
children={
<Text as='small' color={!!errors.title || title.length > 90 ? 'red.500' : 'gray.500'}>
{title.length}/90
</Text>
}
/>
</InputGroup>
<FormHelperText>e.g Web Development in Javascript</FormHelperText>
<Text as='small' color='red.500'>
{errors.title?.message}
</Text>
</FormControl>
<FormControl isRequired id='description' isInvalid={!!errors.description}>
<FormLabel>Description</FormLabel>
<Textarea resize='none' minH='150px' placeholder='Course description...' {...register('description')} />
<Text as='small' color='red.500'>
{errors.description?.message}
</Text>
</FormControl>
<Flex justifyContent='space-between' flexDirection={{ base: 'column', lg: 'row' }}>
<Box flexGrow={1} flexBasis='0' mr={{ base: '0', lg: '7' }}>
<Stack spacing='5'>
<FormControl isRequired id='category_id' isInvalid={!!errors.category_id}>
<FormLabel>Category</FormLabel>
<Controller
control={control}
name='category_id'
render={({ field: { onChange, value } }) => (
<AsyncSelect
loadOptions={loadCategoriesOptions}
onChange={selected => onChange(selected)}
value={value}
placeholder='Select Category'
noOptionsMessage='There are no categories.'
loadingText='Wait...'
/>
)}
/>
<Text as='small' color='red.500'>
{errors.category_id?.message}
</Text>
</FormControl>
<FormControl isRequired id='teacher_id' isInvalid={!!errors.teacher_id}>
<FormLabel>Author</FormLabel>
<Controller
control={control}
name='teacher_id'
render={({ field: { onChange, value } }) => (
<AsyncSelect
loadOptions={loadUsersOptions}
onChange={selected => onChange(selected)}
value={value}
placeholder='Select Author'
noOptionsMessage='There are no teachers.'
loadingText='Wait...'
/>
)}
/>
<Text as='small' color='red.500'>
{errors.teacher_id?.message}
</Text>
</FormControl>
</Stack>
</Box>
<Box flexGrow={0} flexBasis='50%' mt={{ base: '5', lg: '0' }}>
<FormControl id='image' isInvalid={!!errors.image}>
<FormLabel>Image</FormLabel>
<Controller
control={control}
name='image'
render={({ field: { onChange } }) => (
<FileSelect onChange={file => onChange(file)} supportedFormats={['JPG', 'JPEG', 'PNG', 'WEBP']} />
)}
/>
<Text as='small' color='red.500'>
{errors.image?.message}
</Text>
</FormControl>
</Box>
</Flex>
</Stack>
<Divider mt='5' />
<Box mt='5'>
<Button
isLoading={isLoading}
loadingText='Saving...'
type='submit'
colorScheme='blue'
onClick={() => setStatus(CourseStatusEnum.PUBLISHED)}
>
Create
</Button>
</Box>
</form>
</Box>
);
};
export default CreateCourseForm;

View File

@ -1,3 +0,0 @@
import CreateCourseForm from './CreateCourseForm';
export default CreateCourseForm;

View File

@ -10,7 +10,7 @@ import { IPageProps } from '@educt/interfaces';
* Components * Components
*/ */
import PrevPageButton from '@educt/components/PrevPageButton'; import PrevPageButton from '@educt/components/PrevPageButton';
import CreateCourseForm from './components'; import { CreateCourseForm } from '@educt/components/Forms/CourseForm';
/** /**
* Create course page * Create course page

View File

@ -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<IPageProps> = () => {
const params = useParams<{ id: string }>();
const { data: course, loading, error } = useFetchCourseQuery(params.id);
/**
* Not Found
*/
if (error?.response?.status === 404) return <Redirect to='/404' />;
/**
* Loading
*/
if (loading || course === null) return <LoadingPage />;
return (
<>
<Helmet>
<title>Editing {course.title}</title>
</Helmet>
<Box>
<Heading as='h1'>Course editor</Heading>
<Text mt='2'>{course.title}</Text>
</Box>
<Box>
<Tabs isLazy mt='8'>
<TabList>
<Tab>Info</Tab>
<Tab>Lessons</Tab>
<Tab>Students</Tab>
</TabList>
<TabPanels>
<TabPanel>
<EditCourseForm
defaultValues={{
title: course.title,
description: course.description,
category_id: course.category.id,
teacher_id: course.teacher.id,
image: undefined,
}}
/>
</TabPanel>
<TabPanel>
{course.lessons.length === 0 ? (
<Box textAlign='center' mt='10'>
<Text>No lessons have been added to this course yet</Text>
<Button mt='3' size='sm' colorScheme='blue' variant='outline'>
Create new lesson
</Button>
</Box>
) : null}
</TabPanel>
<TabPanel></TabPanel>
</TabPanels>
</Tabs>
</Box>
</>
);
};
export default EditCoursePage;

View File

@ -69,7 +69,6 @@ const UpdateUserContactsForm: React.FC<UpdateUserContactsFormPropsType> = ({ con
telegram_id: data.telegram_id ?? '', telegram_id: data.telegram_id ?? '',
vk_id: data.vk_id ?? '', vk_id: data.vk_id ?? '',
}); });
setIsLoading(false);
} catch (error: any) { } catch (error: any) {
if (error.response) { if (error.response) {
switch (error.response.status) { switch (error.response.status) {

View File

@ -7,6 +7,7 @@ import { IPrivateRouteProps } from './components/PrivateRoute/PrivateRoute';
import AuthPage from '@educt/pages/auth/auth'; import AuthPage from '@educt/pages/auth/auth';
import NotFoundPage from '@educt/pages/404/404'; import NotFoundPage from '@educt/pages/404/404';
import CreateCoursePage from '@educt/pages/courses/create/create'; 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 ChangePasswordPage from '@educt/pages/profile/change-password/change-password';
import ChangeEmailPage from '@educt/pages/profile/change-email/change-email'; import ChangeEmailPage from '@educt/pages/profile/change-email/change-email';
import { UserRoleEnum } from './enums'; import { UserRoleEnum } from './enums';
@ -43,6 +44,14 @@ export const routes: IRouteOption[] = [
isPrivate: true, isPrivate: true,
roles: [UserRoleEnum.ADMIN, UserRoleEnum.TEACHER], 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', path: '/lessons',
title: 'Lessons', title: 'Lessons',

View File

@ -1,7 +1,7 @@
import { AxiosInstance } from 'axios'; import { AxiosInstance } from 'axios';
import * as helpers from '@educt/helpers'; import * as helpers from '@educt/helpers';
import { IApiRespose, ICourse } from '@educt/interfaces'; import { IApiRespose, ICourse } from '@educt/interfaces';
import { CreateCourseParamsType, FetchCoursesParams } from '@educt/types'; import { CreateCourseParamsType, FetchCoursesParams, UpdateCourseParamsType } from '@educt/types';
import { CourseStatusEnum } from '@educt/enums'; import { CourseStatusEnum } from '@educt/enums';
export class CourseService { export class CourseService {
@ -28,6 +28,19 @@ export class CourseService {
return result.data; return result.data;
} }
/**
* Fetch course by id param
*
* @param id Course id
* @returns Course
*/
public async fetchById(
id: string
): Promise<IApiRespose<Omit<ICourse, 'students_count' | 'likes_count' | 'lessons_count'>>> {
const result = await this.api.get(`/v1/courses/${id}`);
return result.data;
}
/** /**
* Update status of course by id * Update status of course by id
* *
@ -59,6 +72,15 @@ export class CourseService {
return result.data; return result.data;
} }
public async update(
id: string,
data: UpdateCourseParamsType
): Promise<IApiRespose<Omit<ICourse, 'students_count' | 'likes_count' | 'lessons_count'>>> {
const result = await this.api.patch(`/v1/courses/${id}`, data);
return result.data;
}
/** /**
* Delete course by id * Delete course by id
* *

11
src/types/index.d.ts vendored
View File

@ -135,6 +135,15 @@ export type CreateCourseParamsType = {
description: string; description: string;
teacher_id: string; teacher_id: string;
category_id: string; category_id: string;
image: File | undefined; image?: File | undefined;
status: CourseStatusEnum; 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;
};