mirror of
https://github.com/sergeyyarkov/educt-client.git
synced 2026-10-11 10:19:22 +03:00
Merge branch 'feature/create-lesson' into develop
This commit is contained in:
commit
76f72dc9c4
12
package-lock.json
generated
12
package-lock.json
generated
@ -1072,9 +1072,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@hookform/resolvers": {
|
"@hookform/resolvers": {
|
||||||
"version": "2.8.2",
|
"version": "2.8.5",
|
||||||
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-2.8.2.tgz",
|
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-2.8.5.tgz",
|
||||||
"integrity": "sha512-oDTGrm7yHxT4VIv4kJcfjvWvozCbdcL0/jrpbAkeo39FYn78XnKOrrGTNmrXdfu+EQVkNIdZ47IYRFManZ4jUA=="
|
"integrity": "sha512-n7eFm4snsejpfZVH8q2NU7YJbsGhF61IHB0TxgK11nmg08RymEr3KscnbTAZaPd9RaXa3vUPoULgBRN2nVDtMg=="
|
||||||
},
|
},
|
||||||
"@humanwhocodes/config-array": {
|
"@humanwhocodes/config-array": {
|
||||||
"version": "0.9.2",
|
"version": "0.9.2",
|
||||||
@ -3320,9 +3320,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"react-hook-form": {
|
"react-hook-form": {
|
||||||
"version": "7.17.4",
|
"version": "7.22.5",
|
||||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.17.4.tgz",
|
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.22.5.tgz",
|
||||||
"integrity": "sha512-7XhbCr7d9fDC1TgcK/BUbt7D3q0VJMu7jPErfsa0JrxVjv/nni41xWdJcy0Zb7R+Np8OsCkQ2lMyloAtE3DLiQ=="
|
"integrity": "sha512-Q2zaeQFXdVQ8l3hcywhltH+Nzj4vo50wMVujHDVN/1Xy9IOaSZJwYBXA2CYTpK6rq41fnXviw3jTLb04c7Gu9Q=="
|
||||||
},
|
},
|
||||||
"react-icons": {
|
"react-icons": {
|
||||||
"version": "4.3.1",
|
"version": "4.3.1",
|
||||||
|
|||||||
@ -13,7 +13,7 @@
|
|||||||
"@chakra-ui/react": "^1.6.10",
|
"@chakra-ui/react": "^1.6.10",
|
||||||
"@emotion/react": "^11.5.0",
|
"@emotion/react": "^11.5.0",
|
||||||
"@emotion/styled": "^11.3.0",
|
"@emotion/styled": "^11.3.0",
|
||||||
"@hookform/resolvers": "^2.8.0",
|
"@hookform/resolvers": "^2.8.5",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"framer-motion": "^4.0.0",
|
"framer-motion": "^4.0.0",
|
||||||
"js-cookie": "^3.0.0",
|
"js-cookie": "^3.0.0",
|
||||||
@ -26,7 +26,7 @@
|
|||||||
"react-dom": "^17.0.0",
|
"react-dom": "^17.0.0",
|
||||||
"react-error-boundary": "^3.1.3",
|
"react-error-boundary": "^3.1.3",
|
||||||
"react-helmet": "^6.1.0",
|
"react-helmet": "^6.1.0",
|
||||||
"react-hook-form": "^7.12.2",
|
"react-hook-form": "^7.22.5",
|
||||||
"react-icons": "^4.3.1",
|
"react-icons": "^4.3.1",
|
||||||
"react-player": "^2.9.0",
|
"react-player": "^2.9.0",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
|
|||||||
101
src/components/FilesUploader/FilesUploader.tsx
Normal file
101
src/components/FilesUploader/FilesUploader.tsx
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import * as helpres from '@educt/helpers';
|
||||||
|
import { Stack, Flex, Box, Icon, Text, Input, IconButton, Button } from '@chakra-ui/react';
|
||||||
|
import { MdOutlineFilePresent } from 'react-icons/md';
|
||||||
|
import { AddIcon, CloseIcon } from '@chakra-ui/icons';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hooks
|
||||||
|
*/
|
||||||
|
import { useRef } from 'react';
|
||||||
|
import { FileSupportedFormatsEnum } from '@educt/enums';
|
||||||
|
|
||||||
|
type FilesUploaderPropsType = {
|
||||||
|
onChange: (files: File[]) => void;
|
||||||
|
files?: File[] | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FilesUploader: React.FC<FilesUploaderPropsType> = ({ onChange, files }) => {
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const onSelectFileHandler = () => {
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.click();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detach file
|
||||||
|
*/
|
||||||
|
const onRemoveSelectedFileHandler = (selectedFile: File) => () => {
|
||||||
|
onChange([...Array.from(files?.filter(file => file.name !== selectedFile.name) || [])]);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attach file
|
||||||
|
*/
|
||||||
|
const onChangeFileHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (e.target.files && e.target.files.length !== 0) {
|
||||||
|
const isAlreadySelected = (files || []).find(selectedFile =>
|
||||||
|
Array.from(e.target.files || []).some(file => file.name === selectedFile.name)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isAlreadySelected) return;
|
||||||
|
onChange([...Array.from(files || []), ...e.target.files]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Input
|
||||||
|
type='file'
|
||||||
|
ref={fileInputRef}
|
||||||
|
onChange={onChangeFileHandler}
|
||||||
|
display='none'
|
||||||
|
multiple
|
||||||
|
accept={Object.keys(FileSupportedFormatsEnum)
|
||||||
|
.map(ext => `.${ext.toLowerCase()}`)
|
||||||
|
.join(', ')}
|
||||||
|
/>
|
||||||
|
{files && (
|
||||||
|
<Stack>
|
||||||
|
{files.map((file, index) => (
|
||||||
|
<Flex
|
||||||
|
key={index}
|
||||||
|
alignItems={'center'}
|
||||||
|
justifyContent={'space-between'}
|
||||||
|
p='3'
|
||||||
|
bg={'gray.50'}
|
||||||
|
borderRadius={'lg'}
|
||||||
|
>
|
||||||
|
<Flex alignItems={'center'}>
|
||||||
|
<Icon as={MdOutlineFilePresent} mr='2' h={4} w={4} />
|
||||||
|
<Text overflow={'hidden'} fontSize={'sm'} fontWeight={'medium'} lineHeight={'1rem'}>
|
||||||
|
{file.name}
|
||||||
|
<br />
|
||||||
|
<Text as='small' color='gray.500'>
|
||||||
|
{helpres.transformBytes(file.size)}
|
||||||
|
</Text>
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
<IconButton
|
||||||
|
onClick={onRemoveSelectedFileHandler(file)}
|
||||||
|
aria-label='remove'
|
||||||
|
variant={'unstyled'}
|
||||||
|
size='xs'
|
||||||
|
icon={<CloseIcon />}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
<Button onClick={onSelectFileHandler} float={'right'} leftIcon={<AddIcon />} variant={'outline'} size='sm' mt='3'>
|
||||||
|
Upload new
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FilesUploader;
|
||||||
3
src/components/FilesUploader/index.ts
Normal file
3
src/components/FilesUploader/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import FilesUploader from './FilesUploader';
|
||||||
|
|
||||||
|
export default FilesUploader;
|
||||||
@ -1,20 +1,41 @@
|
|||||||
import { yupResolver } from '@hookform/resolvers/yup';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import { SubmitHandler, useForm } from 'react-hook-form';
|
import { SubmitHandler, useForm } from 'react-hook-form';
|
||||||
|
import type { InputFields } from './LessonForm';
|
||||||
import { LessonForm } from '.';
|
import { LessonForm } from '.';
|
||||||
import { InputFields } from './LessonForm';
|
|
||||||
|
/**
|
||||||
|
* Validator
|
||||||
|
*/
|
||||||
import LessonFormSchema from './LessonForm.validator';
|
import LessonFormSchema from './LessonForm.validator';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hooks
|
||||||
|
*/
|
||||||
|
import { useCreateLesson } from '@educt/hooks/queries';
|
||||||
|
import { useHistory, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
const CreateLessonForm: React.FC = () => {
|
const CreateLessonForm: React.FC = () => {
|
||||||
const form = useForm<InputFields>({ resolver: yupResolver(LessonFormSchema) });
|
const form = useForm<InputFields>({ resolver: yupResolver(LessonFormSchema) });
|
||||||
|
const { createLesson, isLoading } = useCreateLesson();
|
||||||
|
const { id: course_id } = useParams<{ id: string }>();
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
const onSubmit: SubmitHandler<InputFields> = async data => {
|
const onSubmit: SubmitHandler<InputFields> = async data => {
|
||||||
console.log('creating...');
|
await createLesson({
|
||||||
|
title: data.title,
|
||||||
|
description: data.description,
|
||||||
|
duration: data.duration,
|
||||||
|
video: data.video,
|
||||||
|
materials: Array.from(data.materials || []),
|
||||||
|
course_id,
|
||||||
|
});
|
||||||
|
history.push(`/courses/edit/${course_id}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LessonForm
|
<LessonForm
|
||||||
isLoading={false}
|
isLoading={isLoading}
|
||||||
reactHookForm={{ ...form }}
|
reactHookForm={{ ...form }}
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
buttonLabel='Create'
|
buttonLabel='Create'
|
||||||
|
|||||||
@ -11,12 +11,19 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
Textarea,
|
Textarea,
|
||||||
Flex,
|
Flex,
|
||||||
|
Divider,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { UseFormReturn } from 'react-hook-form';
|
import { Controller, UseFormReturn } from 'react-hook-form';
|
||||||
|
import VideoUploader from '@educt/components/VideoUploader';
|
||||||
|
import FilesUploader from '@educt/components/FilesUploader';
|
||||||
|
import { FileSupportedFormatsEnum, VideoSupportedFormatsEnum } from '@educt/enums';
|
||||||
|
|
||||||
export type InputFields = {
|
export type InputFields = {
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
duration: string;
|
||||||
|
video: File;
|
||||||
|
materials?: FileList | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LessonFormPropsType = {
|
export type LessonFormPropsType = {
|
||||||
@ -30,7 +37,6 @@ const LessonForm: React.FC<LessonFormPropsType> = ({ onSubmit, buttonLabel, isLo
|
|||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
register,
|
register,
|
||||||
watch,
|
|
||||||
formState: { errors, isDirty },
|
formState: { errors, isDirty },
|
||||||
} = reactHookForm;
|
} = reactHookForm;
|
||||||
|
|
||||||
@ -42,15 +48,15 @@ const LessonForm: React.FC<LessonFormPropsType> = ({ onSubmit, buttonLabel, isLo
|
|||||||
<InputGroup>
|
<InputGroup>
|
||||||
<Input placeholder='Lesson name' type='text' {...register('title')} />
|
<Input placeholder='Lesson name' type='text' {...register('title')} />
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
<FormHelperText>e.g Variables in Javascript</FormHelperText>
|
|
||||||
<Text as='small' color='red.500'>
|
<Text as='small' color='red.500'>
|
||||||
{errors.title?.message}
|
{errors.title?.message}
|
||||||
</Text>
|
</Text>
|
||||||
|
<FormHelperText>e.g Introduction</FormHelperText>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormControl isRequired id='description' isInvalid={!!errors.description}>
|
<FormControl isRequired id='description' isInvalid={!!errors.description}>
|
||||||
<FormLabel>Description</FormLabel>
|
<FormLabel>Description</FormLabel>
|
||||||
<Textarea resize='none' minH='150px' placeholder='Course description...' {...register('description')} />
|
<Textarea resize='none' minH='150px' placeholder='Lesson description...' {...register('description')} />
|
||||||
<Flex justifyContent='space-between'>
|
<Flex justifyContent='space-between'>
|
||||||
<Text as='small' color='red.500'>
|
<Text as='small' color='red.500'>
|
||||||
{errors.description?.message}
|
{errors.description?.message}
|
||||||
@ -60,8 +66,55 @@ const LessonForm: React.FC<LessonFormPropsType> = ({ onSubmit, buttonLabel, isLo
|
|||||||
|
|
||||||
<FormControl isRequired id='video'>
|
<FormControl isRequired id='video'>
|
||||||
<FormLabel>Video</FormLabel>
|
<FormLabel>Video</FormLabel>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name='video'
|
||||||
|
render={({ field: { onChange, value: file } }) => (
|
||||||
|
<VideoUploader onChange={file => onChange(file)} file={file} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Text as='small' color='red.500'>
|
||||||
|
{errors.video?.message}
|
||||||
|
</Text>
|
||||||
|
<FormHelperText>
|
||||||
|
Supported formats:{' '}
|
||||||
|
{Object.keys(VideoSupportedFormatsEnum)
|
||||||
|
.map(ext => `${ext.toLowerCase()}`)
|
||||||
|
.join(', ')}
|
||||||
|
</FormHelperText>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl isRequired id='duration'>
|
||||||
|
<FormLabel>Video duration</FormLabel>
|
||||||
|
<Input type='time' defaultValue={'00:30:00'} step='1' {...register('duration')} />
|
||||||
|
<Text as='small' color='red.500'>
|
||||||
|
{errors.duration?.message}
|
||||||
|
</Text>
|
||||||
|
<FormHelperText>hours:minutes:seconds</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Materials</FormLabel>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name='materials'
|
||||||
|
render={({ field: { onChange, value: files } }) => (
|
||||||
|
<FilesUploader onChange={files => onChange(files)} files={Array.from(files || [])} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Text as='small' color='red.500'>
|
||||||
|
{errors.materials?.message}
|
||||||
|
</Text>
|
||||||
|
<FormHelperText>
|
||||||
|
Supported formats:{' '}
|
||||||
|
{Object.keys(FileSupportedFormatsEnum)
|
||||||
|
.map(ext => `${ext.toLowerCase()}`)
|
||||||
|
.join(', ')}
|
||||||
|
</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
<Box mt='5'>
|
<Box mt='5'>
|
||||||
<Button
|
<Button
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
|
|||||||
@ -1,11 +1,40 @@
|
|||||||
|
import * as helpers from '@educt/helpers';
|
||||||
import yup from '@educt/schema';
|
import yup from '@educt/schema';
|
||||||
|
|
||||||
const LessonFormSchema = yup.object().shape({
|
const VIDEO_FILE_SIZE = 5000 * 1024 * 1024;
|
||||||
|
const MATERIAL_FILE_SIZE = 100000 * 1024;
|
||||||
|
|
||||||
|
const LessonFormSchema = yup
|
||||||
|
.object({
|
||||||
title: yup.string().required('Title field name is required').max(90, 'Title must be at most 90 characters'),
|
title: yup.string().required('Title field name is required').max(90, 'Title must be at most 90 characters'),
|
||||||
description: yup
|
description: yup
|
||||||
.string()
|
.string()
|
||||||
.required('Description field name is required')
|
.required('Description field name is required')
|
||||||
.max(250, 'Description must be at most 250 characters'),
|
.max(250, 'Description must be at most 250 characters'),
|
||||||
});
|
duration: yup.string().required('Duration field name is required'),
|
||||||
|
video: yup
|
||||||
|
.mixed()
|
||||||
|
.test(
|
||||||
|
'fileSize',
|
||||||
|
`File size is too large, maximum size is ${helpers.transformBytes(VIDEO_FILE_SIZE)}`,
|
||||||
|
(value: File) => {
|
||||||
|
if (!value) return true;
|
||||||
|
return value && value.size <= VIDEO_FILE_SIZE;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.required('Video field name is required'),
|
||||||
|
materials: yup
|
||||||
|
.mixed()
|
||||||
|
.test(
|
||||||
|
'fileSize',
|
||||||
|
`File size is too large, maximum size is ${helpers.transformBytes(MATERIAL_FILE_SIZE)}`,
|
||||||
|
(values: File[]) => {
|
||||||
|
if (!values) return true;
|
||||||
|
return values && values.every(value => value.size <= MATERIAL_FILE_SIZE);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
})
|
||||||
|
.required();
|
||||||
|
|
||||||
export default LessonFormSchema;
|
export default LessonFormSchema;
|
||||||
|
|||||||
111
src/components/VideoUploader/VideoUploader.tsx
Normal file
111
src/components/VideoUploader/VideoUploader.tsx
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import { Flex, Box, Icon, Text, Button, Input, AspectRatio, useColorModeValue } from '@chakra-ui/react';
|
||||||
|
import { MdUpload } from 'react-icons/md';
|
||||||
|
import { VideoSupportedFormatsEnum } from '@educt/enums';
|
||||||
|
|
||||||
|
type VideoUploaderPropsType = {
|
||||||
|
onChange: (file: File) => void;
|
||||||
|
file?: File | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const VideoUploader: React.FC<VideoUploaderPropsType> = ({ onChange, file }) => {
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const videoRef = useRef<HTMLVideoElement>(null);
|
||||||
|
const [videoObjectUrl, setVideoObjectUrl] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
|
const onSelectFileHandler = () => {
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.click();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onChangeFileHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const { files } = e.target;
|
||||||
|
|
||||||
|
if (files && files.length !== 0) {
|
||||||
|
const file = files[0];
|
||||||
|
|
||||||
|
if (file.type.match('video.*')) {
|
||||||
|
onChange(file);
|
||||||
|
setVideoObjectUrl(prevUrl => {
|
||||||
|
prevUrl && URL.revokeObjectURL(prevUrl);
|
||||||
|
const newUrl = URL.createObjectURL(file);
|
||||||
|
return newUrl;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (videoRef.current) {
|
||||||
|
videoRef.current.load();
|
||||||
|
}
|
||||||
|
}, [videoObjectUrl]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
{file && videoObjectUrl ? (
|
||||||
|
<Box>
|
||||||
|
<AspectRatio maxH='lg' mt='2'>
|
||||||
|
<video ref={videoRef} controls style={{ borderRadius: '5px' }}>
|
||||||
|
<source src={videoObjectUrl} />
|
||||||
|
</video>
|
||||||
|
</AspectRatio>
|
||||||
|
<Button
|
||||||
|
onClick={onSelectFileHandler}
|
||||||
|
leftIcon={<MdUpload />}
|
||||||
|
variant={'outline'}
|
||||||
|
size='sm'
|
||||||
|
mt='2'
|
||||||
|
float='right'
|
||||||
|
>
|
||||||
|
Change video
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Flex
|
||||||
|
p='40px 10px'
|
||||||
|
textAlign={'center'}
|
||||||
|
borderRadius={'md'}
|
||||||
|
flexDir={'column'}
|
||||||
|
justifyContent={'center'}
|
||||||
|
alignItems={'center'}
|
||||||
|
borderWidth={'1px'}
|
||||||
|
>
|
||||||
|
<Box bg={useColorModeValue('gray.100', 'gray.700')} p='6' borderRadius={'full'}>
|
||||||
|
<Icon as={MdUpload} h={16} w={16} color='gray.400' />
|
||||||
|
</Box>
|
||||||
|
<Box mt='4'>
|
||||||
|
<Text fontSize={'md'}>
|
||||||
|
Upload a video file <br />
|
||||||
|
<Text as='span' fontSize='sm' color='gray.500'>
|
||||||
|
The video will be uploaded to cloud or local storage depending on the server configuration.
|
||||||
|
</Text>
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Button onClick={onSelectFileHandler} mt='6'>
|
||||||
|
Select file
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
<Input
|
||||||
|
type='file'
|
||||||
|
ref={fileInputRef}
|
||||||
|
onChange={onChangeFileHandler}
|
||||||
|
maxW={'0'}
|
||||||
|
h='0'
|
||||||
|
display={'block'}
|
||||||
|
opacity={'0'}
|
||||||
|
ml='auto'
|
||||||
|
mr='auto'
|
||||||
|
accept={Object.keys(VideoSupportedFormatsEnum)
|
||||||
|
.map(ext => `.${ext.toLowerCase()}`)
|
||||||
|
.join(', ')}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VideoUploader;
|
||||||
3
src/components/VideoUploader/index.ts
Normal file
3
src/components/VideoUploader/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import VideoUploader from './VideoUploader';
|
||||||
|
|
||||||
|
export default VideoUploader;
|
||||||
@ -8,3 +8,19 @@ export enum CourseStatusEnum {
|
|||||||
PUBLISHED = 'PUBLISHED',
|
PUBLISHED = 'PUBLISHED',
|
||||||
DRAFT = 'DRAFT',
|
DRAFT = 'DRAFT',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum VideoSupportedFormatsEnum {
|
||||||
|
MP4 = 'video/mp4',
|
||||||
|
MOV = 'video/quicktime',
|
||||||
|
AVI = 'video/x-msvideo',
|
||||||
|
WMV = 'video/x-ms-wmv',
|
||||||
|
FLV = 'video/x-flv',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum FileSupportedFormatsEnum {
|
||||||
|
PDF = 'application/pdf',
|
||||||
|
ZIP = 'application/x-zip-compressed',
|
||||||
|
RAR = 'application/x-rar-compressed',
|
||||||
|
DOC = 'application/msword',
|
||||||
|
DOCX = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||||
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { useLogout } from './auth/useLogout';
|
|||||||
/**
|
/**
|
||||||
* Lesson
|
* Lesson
|
||||||
*/
|
*/
|
||||||
|
import { useCreateLesson } from './lesson/useCreateLesson';
|
||||||
import { useDeleteLesson } from './lesson/useDeleteLesson';
|
import { useDeleteLesson } from './lesson/useDeleteLesson';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -36,6 +37,7 @@ export {
|
|||||||
useSetCourseStatus,
|
useSetCourseStatus,
|
||||||
useAttachStudents,
|
useAttachStudents,
|
||||||
useDetachStudents,
|
useDetachStudents,
|
||||||
|
useCreateLesson,
|
||||||
useDeleteLesson,
|
useDeleteLesson,
|
||||||
useCreateUser,
|
useCreateUser,
|
||||||
useDeleteUser,
|
useDeleteUser,
|
||||||
|
|||||||
45
src/hooks/queries/lesson/useCreateLesson.ts
Normal file
45
src/hooks/queries/lesson/useCreateLesson.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import { ILesson } from '@educt/interfaces';
|
||||||
|
import { LessonServiceInstance } from '@educt/services';
|
||||||
|
import type { CreateLessonParamsType } from '@educt/types';
|
||||||
|
import useAsync from '@educt/hooks/useAsync';
|
||||||
|
import { useErrorHandler } from 'react-error-boundary';
|
||||||
|
import { useToast } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
const useCreateLesson = () => {
|
||||||
|
const toast = useToast();
|
||||||
|
const handleError = useErrorHandler();
|
||||||
|
|
||||||
|
const createLesson = async (data: CreateLessonParamsType) => {
|
||||||
|
try {
|
||||||
|
const result = await LessonServiceInstance.create(data);
|
||||||
|
toast({ title: `Lesson successfully created.`, status: 'success' });
|
||||||
|
return result.data;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
if (error.response) {
|
||||||
|
switch (error.response.status) {
|
||||||
|
case 422:
|
||||||
|
toast({ title: `${error.response.data.errors[0].message}`, status: 'error' });
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
toast({ title: `${error.message}`, status: 'error' });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
handleError(error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
handleError(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const { execute, ...state } = useAsync<ILesson, Parameters<typeof createLesson>>(createLesson);
|
||||||
|
|
||||||
|
return { createLesson: execute, ...state };
|
||||||
|
};
|
||||||
|
|
||||||
|
export { useCreateLesson };
|
||||||
@ -67,7 +67,7 @@ export interface ILessonMaterial {
|
|||||||
export interface ILessonContent {
|
export interface ILessonContent {
|
||||||
id: number;
|
id: number;
|
||||||
lesson_id: string;
|
lesson_id: string;
|
||||||
video_url: string;
|
body: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICategory {
|
export interface ICategory {
|
||||||
|
|||||||
@ -3,11 +3,23 @@ import React from 'react';
|
|||||||
import { IPageProps } from '@educt/interfaces';
|
import { IPageProps } from '@educt/interfaces';
|
||||||
import { PageContent, PageHeading, PageWrapper } from '@educt/components/PageElements';
|
import { PageContent, PageHeading, PageWrapper } from '@educt/components/PageElements';
|
||||||
import CreateLessonForm from '@educt/components/Forms/LessonForm/CreateLessonForm';
|
import CreateLessonForm from '@educt/components/Forms/LessonForm/CreateLessonForm';
|
||||||
|
import { Redirect, useParams } from 'react-router-dom';
|
||||||
|
import { useFetchCourse } from '@educt/hooks/queries';
|
||||||
|
import LoadingPage from '@educt/components/LoadingPage';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create Lesson page
|
* Create Lesson page
|
||||||
*/
|
*/
|
||||||
const CreateLessonPage: React.FC<IPageProps> = () => {
|
const CreateLessonPage: React.FC<IPageProps> = () => {
|
||||||
|
const params = useParams<{ id: string }>();
|
||||||
|
const { error, data: course, isLoading } = useFetchCourse(params.id);
|
||||||
|
|
||||||
|
if (error?.response?.status === 404) {
|
||||||
|
return <Redirect to='/404' />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!course || isLoading) return <LoadingPage />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageWrapper>
|
<PageWrapper>
|
||||||
<PageHeading heading='Lesson Creation' description='Create new lesson' />
|
<PageHeading heading='Lesson Creation' description='Create new lesson' />
|
||||||
|
|||||||
@ -36,16 +36,10 @@ const EditCoursePage: React.FC<IPageProps> = () => {
|
|||||||
const { error, data: course, isLoading } = useFetchCourse(params.id);
|
const { error, data: course, isLoading } = useFetchCourse(params.id);
|
||||||
const { onOpen: onOpenDeleteDialog, onClose: onCloseDeleteDialog, isOpen: isOpenDeleteDialog } = useDisclosure();
|
const { onOpen: onOpenDeleteDialog, onClose: onCloseDeleteDialog, isOpen: isOpenDeleteDialog } = useDisclosure();
|
||||||
|
|
||||||
/**
|
|
||||||
* Not Found
|
|
||||||
*/
|
|
||||||
if (error?.response?.status === 404) {
|
if (error?.response?.status === 404) {
|
||||||
return <Redirect to='/404' />;
|
return <Redirect to='/404' />;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Loading
|
|
||||||
*/
|
|
||||||
if (!course || isLoading) return <LoadingPage />;
|
if (!course || isLoading) return <LoadingPage />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { AxiosInstance } from 'axios';
|
import { AxiosInstance } from 'axios';
|
||||||
|
import * as helpers from '@educt/helpers';
|
||||||
import { IApiRespose, ILesson } from '@educt/interfaces';
|
import { IApiRespose, ILesson } from '@educt/interfaces';
|
||||||
import { ApiServiceInstance } from './ApiService';
|
import { ApiServiceInstance } from './ApiService';
|
||||||
import type { LessonProgress } from '@educt/types';
|
import type { CreateLessonParamsType, LessonProgress } from '@educt/types';
|
||||||
|
|
||||||
class LessonService {
|
class LessonService {
|
||||||
public api: AxiosInstance;
|
public api: AxiosInstance;
|
||||||
@ -43,6 +44,16 @@ class LessonService {
|
|||||||
return result.data;
|
return result.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create new lesson
|
||||||
|
*
|
||||||
|
* @param data Lesson data for creating
|
||||||
|
*/
|
||||||
|
public async create(data: CreateLessonParamsType): Promise<IApiRespose<ILesson>> {
|
||||||
|
const result = await this.api.post(`/v1/lessons`, helpers.transformToFormData(data));
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete lesson by id
|
* Delete lesson by id
|
||||||
*
|
*
|
||||||
@ -60,7 +71,7 @@ class LessonService {
|
|||||||
* @param ids Ordered ids of lessons
|
* @param ids Ordered ids of lessons
|
||||||
* @returns Empty data
|
* @returns Empty data
|
||||||
*/
|
*/
|
||||||
public async saveOrder(ids: string[]): Promise<IApiRespose<any>> {
|
public async saveOrder(ids: string[]): Promise<IApiRespose<Record<string, unknown>>> {
|
||||||
const result = await this.api.post('/v1/lessons/save-order', {
|
const result = await this.api.post('/v1/lessons/save-order', {
|
||||||
ids,
|
ids,
|
||||||
});
|
});
|
||||||
|
|||||||
9
src/types/index.d.ts
vendored
9
src/types/index.d.ts
vendored
@ -160,3 +160,12 @@ export type UpdateCourseParamsType = {
|
|||||||
image?: File | undefined;
|
image?: File | undefined;
|
||||||
status?: CourseStatusEnum | undefined;
|
status?: CourseStatusEnum | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CreateLessonParamsType = {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
duration: string;
|
||||||
|
course_id: string;
|
||||||
|
video: File;
|
||||||
|
materials?: File[] | undefined;
|
||||||
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user