feat: create course validation & creating course

This commit is contained in:
Sergey Yarkov 2021-10-27 23:39:33 +03:00
parent 5604f2e85f
commit 4d0fef3fb1
6 changed files with 198 additions and 38 deletions

View File

@ -44,7 +44,7 @@ const Layout: React.FC<LayoutPropsType> = ({ children, roles }) => {
as='main'
flex='1 0'
maxW='85rem'
ml='25rem'
ml='20rem'
mt='7rem'
mb='1rem'
pr='2rem'

View File

@ -85,7 +85,7 @@ const CourseList: React.FC<CourseListPropsType> = ({ courses, isLoading }) => {
{deletingCourse && <DeleteCourseDialog onClose={onCloseDeleteDialog} isOpen={isOpenDeleteDialog} />}
{courses.length !== 0 ? (
<Grid
templateColumns='repeat(3, 1fr)'
templateColumns='repeat(3, minmax(0, 1fr))'
gap='6'
sx={{
'@media (max-width: 1280px)': {

View File

@ -1,22 +1,46 @@
import React from 'react';
import { Box, Grid, useToast } from '@chakra-ui/react';
import { Input } from '@chakra-ui/react';
import { Flex, Button, Textarea } from '@chakra-ui/react';
import { FormControl, FormLabel, InputGroup, InputRightElement } from '@chakra-ui/react';
import React, { useState } from 'react';
import {
Box,
Grid,
useToast,
Text,
Flex,
Button,
Textarea,
FormControl,
FormLabel,
InputGroup,
InputRightElement,
Input,
} from '@chakra-ui/react';
import { MdCloudUpload } from 'react-icons/md';
import { SubmitHandler, useForm, useWatch, Controller } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import AsyncSelect from 'react-select/async';
import * as helpers from '@educt/helpers';
/**
* Types
*/
import { MdCloudUpload } from 'react-icons/md';
import { OptionType } from '@educt/types';
import { CourseStatusEnum, UserRoleEnum } from '@educt/enums';
/**
* Hooks
*/
import { useHistory } from 'react-router';
import { useRootStore } from '@educt/hooks/useRootStore';
import { UserRoleEnum } from '@educt/enums';
import { useErrorHandler } from 'react-error-boundary';
import { SubmitHandler, useForm, Controller } from 'react-hook-form';
import useIsMountedRef from '@educt/hooks/useIsMountedRef';
/**
* Schema
*/
import CreateCourseSchema from './CreateCourseForm.validator';
type CreateCourseInputType = {
title: string;
image: File;
image: FileList;
category_id: string;
description: string;
teacher_id: string;
@ -27,21 +51,45 @@ const CreateCourseForm: React.FC<CreateCourseFormProps> = () => {
const {
userStore: { userService },
categoryStore: { categoryService },
courseStore: { courseService },
} = useRootStore();
const { register, formState, handleSubmit, control, reset } = useForm<CreateCourseInputType>();
const {
register,
handleSubmit,
formState: { errors },
control,
watch,
reset,
} = 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 (inputValue: string) => {
const loadUsersOptions = async (inputValue: string): Promise<OptionType[] | undefined> => {
try {
const users = await userService.fetchAll({ search: inputValue, role: UserRoleEnum.TEACHER });
return users.data.map(user => ({
label: user.fullname,
value: user.id,
}));
// TODO: remove filter later
return users.data
.filter(user => helpers.userContainRoles(user.roles, [UserRoleEnum.TEACHER]))
.map(user => ({
label: user.fullname,
value: user.id,
}));
} catch (error: any) {
if (error.response) {
toast({ title: 'Error' });
@ -54,7 +102,7 @@ const CreateCourseForm: React.FC<CreateCourseFormProps> = () => {
/**
* Load categories into select filed
*/
const loadCategoriesOptions = async (inputValue: string) => {
const loadCategoriesOptions = async (inputValue: string): Promise<OptionType[] | undefined> => {
try {
const categories = await categoryService.fetchAll();
return categories.data.map(category => ({
@ -74,27 +122,68 @@ const CreateCourseForm: React.FC<CreateCourseFormProps> = () => {
* Submit form handler
*/
const onSubmit: SubmitHandler<CreateCourseInputType> = async data => {
console.log(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[0],
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)}>
<Grid templateColumns={{ md: '2.5fr 1fr' }} gap='5'>
<FormControl id='title'>
<FormControl id='title' isInvalid={!!errors.title}>
<FormLabel>Title</FormLabel>
<InputGroup>
<Input size='md' placeholder='Type course title' type='text' {...register('title')} />
<InputRightElement fontSize='sm' mr='2' color='gray.500' children={'0/60'} />
<Input pr='60px' size='md' placeholder='Type course title' type='text' {...register('title')} />
<InputRightElement
mr='2'
children={
<Text as='small' color={!!errors.title || title.length > 60 ? 'red.500' : 'gray.500'}>
{title.length}/60
</Text>
}
/>
</InputGroup>
<Text as='small' color='red.500'>
{errors.title?.message}
</Text>
</FormControl>
<FormControl id='image'>
<FormLabel>Image</FormLabel>
<Input size='md' variant='unstyled' type='file' {...register('image')} />
</FormControl>
<FormControl id='category_id' gridColumn='1'>
<FormLabel>Category</FormLabel>
<FormControl id='image' isInvalid={!!errors.image}>
<FormLabel>Image</FormLabel>
<Input size='md' variant='unstyled' {...register('image')} type='file' accept='image/*' />
<Text as='small' color='red.500'>
{errors.image?.message}
</Text>
</FormControl>
<FormControl id='category_id' gridColumn='1' isInvalid={!!errors.category_id}>
<FormLabel>Category</FormLabel>
<Controller
control={control}
name='category_id'
@ -111,8 +200,11 @@ const CreateCourseForm: React.FC<CreateCourseFormProps> = () => {
/>
)}
/>
<Text as='small' color='red.500'>
{errors.category_id?.message}
</Text>
</FormControl>
<FormControl id='teacher_id' gridColumn={{ md: '2' }} gridRow={{ md: '1' }}>
<FormControl id='teacher_id' gridColumn={{ md: '2' }} gridRow={{ md: '1' }} isInvalid={!!errors.teacher_id}>
<FormLabel>Teacher</FormLabel>
<Controller
control={control}
@ -129,17 +221,41 @@ const CreateCourseForm: React.FC<CreateCourseFormProps> = () => {
/>
)}
/>
<Text as='small' color='red.500'>
{errors.teacher_id?.message}
</Text>
</FormControl>
<FormControl id='description' gridColumn='1'>
<FormControl id='description' gridColumn='1' isInvalid={!!errors.description}>
<FormLabel>Description</FormLabel>
<Textarea resize='none' minH='150px' placeholder='Write some description for course...' />
<Textarea
resize='none'
minH='150px'
placeholder='Write some description for course...'
{...register('description')}
/>
<Text as='small' color='red.500'>
{errors.description?.message}
</Text>
</FormControl>
</Grid>
<Flex mt='6'>
<Button type='submit' mr='2' colorScheme='blue' variant='outline' rightIcon={<MdCloudUpload size='14px' />}>
<Button
loadingText='Saving...'
type='submit'
mr='2'
colorScheme='blue'
variant='outline'
rightIcon={<MdCloudUpload size='14px' />}
onClick={() => setStatus(CourseStatusEnum.DRAFT)}
>
Save as Draft
</Button>
<Button type='submit' colorScheme='green'>
<Button
loadingText='Saving...'
type='submit'
colorScheme='green'
onClick={() => setStatus(CourseStatusEnum.PUBLISHED)}
>
Publish
</Button>
</Flex>

View File

@ -1,15 +1,21 @@
import yup from '@educt/schema';
const MAX_FILE_SIZE = 300 * 1024;
const FILE_SIZE = 160 * 1024;
const SUPPORTED_FORMATS = ['image/jpg', 'image/jpeg', 'image/gif', 'image/png'];
const CreateCourseSchema = yup.object().shape({
title: yup.string().required('Title field name is required'),
title: yup.string().required('Title field name is required').max(60, 'Title must be at most 60 characters'),
description: yup.string().required('Description field name is required'),
image: yup
.mixed()
.test('fileSize', 'File size is too large', (value: File) => value && value.size >= MAX_FILE_SIZE)
.test('fileFormat', 'Usupported file format', (value: File) => value && SUPPORTED_FORMATS.includes(value.type))
.test('fileSize', 'File size is too large', (value: FileList) => {
if (!value.length) return true;
return value[0] && value[0].size <= FILE_SIZE;
})
.test('fileFormat', 'This file format is not supported', (value: FileList) => {
if (!value.length) return true;
return value[0] && SUPPORTED_FORMATS.includes(value[0].type);
})
.optional(),
category_id: yup.string().required('Category field is required'),
teacher_id: yup.string().required('Teacher field is required'),

View File

@ -1,6 +1,6 @@
import { AxiosInstance } from 'axios';
import { IApiRespose, ICourse } from '@educt/interfaces';
import { FetchCoursesParams } from '@educt/types';
import { CreateCourseParamsType, FetchCoursesParams } from '@educt/types';
import { CourseStatusEnum } from '@educt/enums';
export class CourseService {
@ -41,6 +41,30 @@ export class CourseService {
return result.data;
}
/**
*
* @param data Data for creating course
* @returns
*/
public async create(
data: CreateCourseParamsType
): Promise<IApiRespose<Pick<ICourse, 'id' | 'title' | 'description' | 'status' | 'created_at' | 'updated_at'>>> {
const formData = new FormData();
/**
* Append value to form-data format
*/
Object.entries(data).forEach(([key, value]) => value && formData.append(key, value));
const result = await this.api.post('/v1/courses', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
return result.data;
}
/**
* Delete course by id
*

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

@ -28,6 +28,11 @@ export type ConfirmEmailContainerDataType = {
expired_seconds: number;
};
export type OptionType = {
value: string;
label: string;
};
export type ProfilePageStatusType = 'update-password' | 'update-email' | 'confirm-email' | 'default';
export type ProfilePageDataType = {
/**
@ -127,3 +132,12 @@ export type CreateUserParamsType = {
password: string;
};
export type UpdateUserParamsType = { first_name?: string; last_name?: string; email?: string };
export type CreateCourseParamsType = {
title: string;
description: string;
teacher_id: string;
category_id: string;
image: File | undefined;
status: CourseStatusEnum;
};