feat: create course page layout

This commit is contained in:
Sergey Yarkov 2021-10-26 23:56:26 +03:00
parent 3799f150a8
commit 5604f2e85f
4 changed files with 174 additions and 19 deletions

47
package-lock.json generated
View File

@ -1187,6 +1187,14 @@
"@types/react-router": "*" "@types/react-router": "*"
} }
}, },
"@types/react-transition-group": {
"version": "4.4.4",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.4.tgz",
"integrity": "sha512-7gAPz7anVK5xzbeQW9wFBDg7G++aPLAFY0QaSMOou9rJZpbuI58WAuJrgu+qR92l61grlnCUe7AFX8KGahAgug==",
"requires": {
"@types/react": "*"
}
},
"@types/scheduler": { "@types/scheduler": {
"version": "0.16.2", "version": "0.16.2",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
@ -1387,6 +1395,15 @@
"resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="
}, },
"dom-helpers": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
"requires": {
"@babel/runtime": "^7.8.7",
"csstype": "^3.0.2"
}
},
"electron-to-chromium": { "electron-to-chromium": {
"version": "1.3.870", "version": "1.3.870",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.870.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.870.tgz",
@ -1834,6 +1851,11 @@
"js-tokens": "^3.0.0 || ^4.0.0" "js-tokens": "^3.0.0 || ^4.0.0"
} }
}, },
"memoize-one": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="
},
"mini-create-react-context": { "mini-create-react-context": {
"version": "0.4.1", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz", "resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz",
@ -2166,6 +2188,20 @@
"tiny-warning": "^1.0.0" "tiny-warning": "^1.0.0"
} }
}, },
"react-select": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/react-select/-/react-select-5.1.0.tgz",
"integrity": "sha512-SkEBD1AYsSXrIdNj5HBt7+Ehe+jxdiB448J0atJqR6lE3l/GcFlRf4JYB3NlHe/02jrW4AnIQLo1t0IqWrxXOw==",
"requires": {
"@babel/runtime": "^7.12.0",
"@emotion/cache": "^11.4.0",
"@emotion/react": "^11.1.1",
"@types/react-transition-group": "^4.4.0",
"memoize-one": "^5.0.0",
"prop-types": "^15.6.0",
"react-transition-group": "^4.3.0"
}
},
"react-side-effect": { "react-side-effect": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.1.tgz", "resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.1.tgz",
@ -2188,6 +2224,17 @@
} }
} }
}, },
"react-transition-group": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.2.tgz",
"integrity": "sha512-/RNYfRAMlZwDSr6z4zNKV6xu53/e2BuaBbGhbyYIXTrmgu/bGHzmqOs7mJSJBHy9Ud+ApHx3QjrkKSp1pxvlFg==",
"requires": {
"@babel/runtime": "^7.5.5",
"dom-helpers": "^5.0.1",
"loose-envify": "^1.4.0",
"prop-types": "^15.6.2"
}
},
"regenerator-runtime": { "regenerator-runtime": {
"version": "0.13.9", "version": "0.13.9",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",

View File

@ -27,6 +27,7 @@
"react-hook-form": "^7.12.2", "react-hook-form": "^7.12.2",
"react-icons": "^3.11.0", "react-icons": "^3.11.0",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
"react-select": "^5.1.0",
"validator": "^13.6.0", "validator": "^13.6.0",
"yup": "^0.32.9" "yup": "^0.32.9"
}, },

View File

@ -1,58 +1,147 @@
import React from 'react'; import React from 'react';
import { Box, Grid } from '@chakra-ui/react'; import { Box, Grid, useToast } from '@chakra-ui/react';
import { Input } from '@chakra-ui/react'; import { Input } from '@chakra-ui/react';
import { Select, Flex, Button, Textarea } from '@chakra-ui/react'; import { Flex, Button, Textarea } from '@chakra-ui/react';
import { FormControl, FormLabel, InputGroup, InputRightElement } from '@chakra-ui/react'; import { FormControl, FormLabel, InputGroup, InputRightElement } from '@chakra-ui/react';
import AsyncSelect from 'react-select/async';
/** /**
* Types * Types
*/ */
import { MdCloudUpload } from 'react-icons/md'; import { MdCloudUpload } from 'react-icons/md';
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';
type CreateCourseInputType = {
title: string;
image: File;
category_id: string;
description: string;
teacher_id: string;
};
type CreateCourseFormProps = {}; type CreateCourseFormProps = {};
const CreateCourseForm: React.FC<CreateCourseFormProps> = () => { const CreateCourseForm: React.FC<CreateCourseFormProps> = () => {
const {
userStore: { userService },
categoryStore: { categoryService },
} = useRootStore();
const { register, formState, handleSubmit, control, reset } = useForm<CreateCourseInputType>();
const handleError = useErrorHandler();
const toast = useToast();
/**
* Load users into select field
*/
const loadUsersOptions = async (inputValue: string) => {
try {
const users = await userService.fetchAll({ search: inputValue, 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 (inputValue: string) => {
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 => {
console.log(data);
};
return ( return (
<Box> <Box>
<form> <form onSubmit={handleSubmit(onSubmit)}>
<Grid templateColumns={{ md: '2.5fr 1fr' }} gap='5'> <Grid templateColumns={{ md: '2.5fr 1fr' }} gap='5'>
<FormControl id='title'> <FormControl id='title'>
<FormLabel>Title</FormLabel> <FormLabel>Title</FormLabel>
<InputGroup> <InputGroup>
<Input size='md' placeholder='Type course title' type='text' /> <Input size='md' placeholder='Type course title' type='text' {...register('title')} />
<InputRightElement fontSize='sm' mr='2' color='gray.500' children={'0/60'} /> <InputRightElement fontSize='sm' mr='2' color='gray.500' children={'0/60'} />
</InputGroup> </InputGroup>
</FormControl> </FormControl>
<FormControl id='image'> <FormControl id='image'>
<FormLabel>Image</FormLabel> <FormLabel>Image</FormLabel>
<Input size='md' variant='unstyled' type='file' /> <Input size='md' variant='unstyled' type='file' {...register('image')} />
</FormControl> </FormControl>
<FormControl id='category_id' gridColumn='1'>
<FormControl id='category' gridColumn='1'>
<FormLabel>Category</FormLabel> <FormLabel>Category</FormLabel>
<Select size='md' placeholder='Select category'>
<option value='option1'>Option 1</option>
<option value='option2'>Option 2</option>
<option value='option3'>Option 3</option>
</Select>
</FormControl>
<FormControl id='title' gridColumn={{ md: '2' }} gridRow={{ md: '1' }}> <Controller
control={control}
name='category_id'
render={({ field: { onChange } }) => (
<AsyncSelect
defaultOptions
cacheOptions
loadOptions={loadCategoriesOptions}
isSearchable={false}
onChange={selected => onChange(selected?.value)}
placeholder='Select Category'
loadingMessage={() => 'Loading...'}
noOptionsMessage={() => 'There are no categories.'}
/>
)}
/>
</FormControl>
<FormControl id='teacher_id' gridColumn={{ md: '2' }} gridRow={{ md: '1' }}>
<FormLabel>Teacher</FormLabel> <FormLabel>Teacher</FormLabel>
<Button w='full'>Select Teacher</Button> <Controller
control={control}
name='teacher_id'
render={({ field: { onChange } }) => (
<AsyncSelect
defaultOptions
cacheOptions
loadOptions={loadUsersOptions}
onChange={selected => onChange(selected?.value)}
placeholder='Select Teacher'
loadingMessage={() => 'Loading...'}
noOptionsMessage={() => 'Cannot find any teacher.'}
/>
)}
/>
</FormControl> </FormControl>
<FormControl id='description' gridColumn='1'> <FormControl id='description' gridColumn='1'>
<FormLabel>Description</FormLabel> <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...' />
</FormControl> </FormControl>
</Grid> </Grid>
<Flex mt='6'> <Flex mt='6'>
<Button mr='2' colorScheme='blue' variant='outline' rightIcon={<MdCloudUpload size='14px' />}> <Button type='submit' mr='2' colorScheme='blue' variant='outline' rightIcon={<MdCloudUpload size='14px' />}>
Save as Draft Save as Draft
</Button> </Button>
<Button colorScheme='green'>Publish</Button> <Button type='submit' colorScheme='green'>
Publish
</Button>
</Flex> </Flex>
</form> </form>
</Box> </Box>

View File

@ -0,0 +1,18 @@
import yup from '@educt/schema';
const MAX_FILE_SIZE = 300 * 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'),
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))
.optional(),
category_id: yup.string().required('Category field is required'),
teacher_id: yup.string().required('Teacher field is required'),
});
export default CreateCourseSchema;