feat: added video uploader

This commit is contained in:
Sergey Yarkov 2022-01-04 23:54:04 +03:00
parent 784c1ed431
commit 359deec3bc
9 changed files with 291 additions and 58 deletions

View File

@ -0,0 +1,112 @@
import React, { useState } 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?: Array<File> | undefined;
};
const FilesUploader: React.FC<FilesUploaderPropsType> = ({ onChange, files }) => {
const fileInputRef = useRef<HTMLInputElement>(null);
const [selectedFiles, setSelectedFiles] = useState<Array<File>>([]);
const onSelectFileHandler = () => {
if (fileInputRef.current) {
fileInputRef.current.click();
}
};
/**
* Detach file
*/
const onRemoveSelectedFileHandler = (selectedFile: File) => () =>
setSelectedFiles(selectedFiles => {
const updated = selectedFiles.filter(file => file.name !== selectedFile.name);
onChange(updated);
return updated;
});
/**
* Attach file
*/
const onChangeFileHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
e.preventDefault();
const { files } = e.target;
if (files && files.length !== 0) {
const isAlreadySelected = selectedFiles.find(selectedFile =>
Array.from(files).some(file => file.name === selectedFile.name)
);
if (isAlreadySelected) return;
setSelectedFiles(selectedFiles => {
const updated = [...selectedFiles, ...Array.from(files)];
onChange(updated);
return updated;
});
}
};
return (
<Box>
<Input
type='file'
ref={fileInputRef}
onChange={onChangeFileHandler}
display='none'
multiple
accept={Object.keys(FileSupportedFormatsEnum)
.map(ext => `.${ext.toLowerCase()}`)
.join(', ')}
/>
{files && (
<Stack>
{selectedFiles.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;

View File

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

View File

@ -10,6 +10,7 @@ const CreateLessonForm: React.FC = () => {
const onSubmit: SubmitHandler<InputFields> = async data => {
console.log('creating...');
console.log(data);
};
return (

View File

@ -11,13 +11,12 @@ import {
Button,
Textarea,
Flex,
Icon,
IconButton,
Divider,
} from '@chakra-ui/react';
import { AddIcon, CloseIcon } from '@chakra-ui/icons';
import { UseFormReturn } from 'react-hook-form';
import { MdOutlineFilePresent, MdUpload } from 'react-icons/md';
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 = {
title: string;
@ -68,68 +67,51 @@ const LessonForm: React.FC<LessonFormPropsType> = ({ onSubmit, buttonLabel, isLo
<FormControl isRequired id='video'>
<FormLabel>Video</FormLabel>
<Flex
p='40px 0'
textAlign={'center'}
borderRadius={'md'}
flexDir={'column'}
justifyContent={'center'}
alignItems={'center'}
borderWidth={'1px'}
>
<Box bg='gray.100' 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.
<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>
</Text>
</Box>
<Button mt='6'>Select file</Button>
</Flex>
<FormHelperText>Supported formats: mp4, mov, avi, wmv, webm, flv</FormHelperText>
<FormHelperText>
Supported formats:{' '}
{Object.keys(VideoSupportedFormatsEnum)
.map(ext => `${ext.toLowerCase()}`)
.join(', ')}
</FormHelperText>
</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>
<Stack spacing={'1'}>
{[1, 2, 3].map(k => (
<Flex
key={k}
alignItems={'center'}
justifyContent={'space-between'}
p='3'
bg={'gray.50'}
borderRadius={'lg'}
maxW='500px'
>
<Flex alignItems={'center'}>
<Icon as={MdOutlineFilePresent} mr='2' h={4} w={4} />
<Text overflow={'hidden'} fontSize={'sm'} fontWeight={'medium'} lineHeight={'1rem'}>
file.pdf
<br />
<Text as='small' color='gray.500'>
245kB
<Controller
control={control}
name='materials'
render={({ field: { onChange, value: files } }) => (
<FilesUploader onChange={files => onChange(files)} files={files} />
)}
/>
<Text as='small' color='red.500'>
{errors.materials?.map(m => m.message)}
</Text>
</Text>
</Flex>
<IconButton aria-label='remove' variant={'unstyled'} size='xs' icon={<CloseIcon />} />
</Flex>
))}
</Stack>
<Button leftIcon={<AddIcon />} variant={'outline'} size='sm' mt='3'>
Upload new
</Button>
<FormHelperText>Supported formats: pdf, zip, rar, doc, docx</FormHelperText>
<FormHelperText>
Supported formats:{' '}
{Object.keys(FileSupportedFormatsEnum)
.map(ext => `${ext.toLowerCase()}`)
.join(', ')}
</FormHelperText>
</FormControl>
<Divider />

View File

@ -1,11 +1,37 @@
import { VideoSupportedFormatsEnum } from '@educt/enums';
import yup from '@educt/schema';
const VIDEO_FILE_SIZE = 5000 * 1024 * 1024;
const MATERIAL_FILE_SIZE = 10000 * 1024;
const LessonFormSchema = yup.object().shape({
title: yup.string().required('Title field name is required').max(90, 'Title must be at most 90 characters'),
description: yup
.string()
.required('Description field name is required')
.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', (value: File) => {
if (!value) return true;
return value && value.size <= VIDEO_FILE_SIZE;
})
.test('fileFormat', 'This file format is not supported', (value: File) => {
if (!value) return true;
return value && Object.values(VideoSupportedFormatsEnum).includes(value.type as VideoSupportedFormatsEnum);
})
.required('Video field name is required'),
// materials: yup
// .mixed()
// .test('fileSize', 'File size is too large', (value: File) => {
// if (!value) return true;
// return value && value.size <= VIDEO_FILE_SIZE;
// })
// .test('fileFormat', 'This file format is not supported', (value: File) => {
// if (!value) return true;
// return value && VIDEO_SUPPORTED_FORMATS.includes(value.type);
// }),
});
export default LessonFormSchema;

View File

@ -0,0 +1,90 @@
import React, { useRef } 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 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];
file.type.match('video.*') && onChange(file);
}
};
return (
<Box>
<Input
type='file'
ref={fileInputRef}
onChange={onChangeFileHandler}
display='none'
accept={Object.keys(VideoSupportedFormatsEnum)
.map(ext => `.${ext.toLowerCase()}`)
.join(', ')}
/>
{file ? (
<Box>
<AspectRatio maxH='lg' mt='2'>
<video controls style={{ borderRadius: '5px' }}>
<source src={URL.createObjectURL(file)} />
</video>
</AspectRatio>
<Button
onClick={onSelectFileHandler}
leftIcon={<MdUpload />}
variant={'outline'}
size='sm'
mt='2'
float='right'
>
Change video
</Button>
</Box>
) : (
<Flex
p='40px 0'
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>
)}
</Box>
);
};
export default VideoUploader;

View File

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

View File

@ -8,3 +8,19 @@ export enum CourseStatusEnum {
PUBLISHED = 'PUBLISHED',
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/zip',
RAR = 'application/x-rar-compressed',
DOC = 'application/msword',
DOCX = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
}

View File

@ -22,7 +22,7 @@ const CreateLessonPage: React.FC<IPageProps> = () => {
return (
<PageWrapper>
<PageHeading heading='Lesson Creation' description='Create new lesson ' />
<PageHeading heading='Lesson Creation' description='Create new lesson' />
<PageContent>
<CreateLessonForm />
</PageContent>