mirror of
https://github.com/sergeyyarkov/educt-client.git
synced 2026-10-11 18:29:15 +03:00
feat: added video uploader
This commit is contained in:
parent
784c1ed431
commit
359deec3bc
112
src/components/FilesUploader/FilesUploader.tsx
Normal file
112
src/components/FilesUploader/FilesUploader.tsx
Normal 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;
|
||||||
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;
|
||||||
@ -10,6 +10,7 @@ const CreateLessonForm: React.FC = () => {
|
|||||||
|
|
||||||
const onSubmit: SubmitHandler<InputFields> = async data => {
|
const onSubmit: SubmitHandler<InputFields> = async data => {
|
||||||
console.log('creating...');
|
console.log('creating...');
|
||||||
|
console.log(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -11,13 +11,12 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
Textarea,
|
Textarea,
|
||||||
Flex,
|
Flex,
|
||||||
Icon,
|
|
||||||
IconButton,
|
|
||||||
Divider,
|
Divider,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { AddIcon, CloseIcon } from '@chakra-ui/icons';
|
import { Controller, UseFormReturn } from 'react-hook-form';
|
||||||
import { UseFormReturn } from 'react-hook-form';
|
import VideoUploader from '@educt/components/VideoUploader';
|
||||||
import { MdOutlineFilePresent, MdUpload } from 'react-icons/md';
|
import FilesUploader from '@educt/components/FilesUploader';
|
||||||
|
import { FileSupportedFormatsEnum, VideoSupportedFormatsEnum } from '@educt/enums';
|
||||||
|
|
||||||
export type InputFields = {
|
export type InputFields = {
|
||||||
title: string;
|
title: string;
|
||||||
@ -68,68 +67,51 @@ const LessonForm: React.FC<LessonFormPropsType> = ({ onSubmit, buttonLabel, isLo
|
|||||||
|
|
||||||
<FormControl isRequired id='video'>
|
<FormControl isRequired id='video'>
|
||||||
<FormLabel>Video</FormLabel>
|
<FormLabel>Video</FormLabel>
|
||||||
<Flex
|
<Controller
|
||||||
p='40px 0'
|
control={control}
|
||||||
textAlign={'center'}
|
name='video'
|
||||||
borderRadius={'md'}
|
render={({ field: { onChange, value: file } }) => (
|
||||||
flexDir={'column'}
|
<VideoUploader onChange={file => onChange(file)} file={file} />
|
||||||
justifyContent={'center'}
|
)}
|
||||||
alignItems={'center'}
|
/>
|
||||||
borderWidth={'1px'}
|
<Text as='small' color='red.500'>
|
||||||
>
|
{errors.video?.message}
|
||||||
<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.
|
|
||||||
</Text>
|
</Text>
|
||||||
</Text>
|
<FormHelperText>
|
||||||
</Box>
|
Supported formats:{' '}
|
||||||
<Button mt='6'>Select file</Button>
|
{Object.keys(VideoSupportedFormatsEnum)
|
||||||
</Flex>
|
.map(ext => `${ext.toLowerCase()}`)
|
||||||
<FormHelperText>Supported formats: mp4, mov, avi, wmv, webm, flv</FormHelperText>
|
.join(', ')}
|
||||||
|
</FormHelperText>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormControl isRequired id='duration'>
|
<FormControl isRequired id='duration'>
|
||||||
<FormLabel>Video duration</FormLabel>
|
<FormLabel>Video duration</FormLabel>
|
||||||
<Input type='time' defaultValue={'00:30:00'} step='1' {...register('duration')} />
|
<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>
|
<FormHelperText>hours:minutes:seconds</FormHelperText>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<FormLabel>Materials</FormLabel>
|
<FormLabel>Materials</FormLabel>
|
||||||
<Stack spacing={'1'}>
|
<Controller
|
||||||
{[1, 2, 3].map(k => (
|
control={control}
|
||||||
<Flex
|
name='materials'
|
||||||
key={k}
|
render={({ field: { onChange, value: files } }) => (
|
||||||
alignItems={'center'}
|
<FilesUploader onChange={files => onChange(files)} files={files} />
|
||||||
justifyContent={'space-between'}
|
)}
|
||||||
p='3'
|
/>
|
||||||
bg={'gray.50'}
|
<Text as='small' color='red.500'>
|
||||||
borderRadius={'lg'}
|
{errors.materials?.map(m => m.message)}
|
||||||
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
|
|
||||||
</Text>
|
</Text>
|
||||||
</Text>
|
<FormHelperText>
|
||||||
</Flex>
|
Supported formats:{' '}
|
||||||
<IconButton aria-label='remove' variant={'unstyled'} size='xs' icon={<CloseIcon />} />
|
{Object.keys(FileSupportedFormatsEnum)
|
||||||
</Flex>
|
.map(ext => `${ext.toLowerCase()}`)
|
||||||
))}
|
.join(', ')}
|
||||||
</Stack>
|
</FormHelperText>
|
||||||
<Button leftIcon={<AddIcon />} variant={'outline'} size='sm' mt='3'>
|
|
||||||
Upload new
|
|
||||||
</Button>
|
|
||||||
<FormHelperText>Supported formats: pdf, zip, rar, doc, docx</FormHelperText>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|||||||
@ -1,11 +1,37 @@
|
|||||||
|
import { VideoSupportedFormatsEnum } from '@educt/enums';
|
||||||
import yup from '@educt/schema';
|
import yup from '@educt/schema';
|
||||||
|
|
||||||
|
const VIDEO_FILE_SIZE = 5000 * 1024 * 1024;
|
||||||
|
const MATERIAL_FILE_SIZE = 10000 * 1024;
|
||||||
|
|
||||||
const LessonFormSchema = yup.object().shape({
|
const LessonFormSchema = yup.object().shape({
|
||||||
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', (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;
|
export default LessonFormSchema;
|
||||||
|
|||||||
90
src/components/VideoUploader/VideoUploader.tsx
Normal file
90
src/components/VideoUploader/VideoUploader.tsx
Normal 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;
|
||||||
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/zip',
|
||||||
|
RAR = 'application/x-rar-compressed',
|
||||||
|
DOC = 'application/msword',
|
||||||
|
DOCX = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||||
|
}
|
||||||
|
|||||||
@ -22,7 +22,7 @@ const CreateLessonPage: React.FC<IPageProps> = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PageWrapper>
|
<PageWrapper>
|
||||||
<PageHeading heading='Lesson Creation' description='Create new lesson ' />
|
<PageHeading heading='Lesson Creation' description='Create new lesson' />
|
||||||
<PageContent>
|
<PageContent>
|
||||||
<CreateLessonForm />
|
<CreateLessonForm />
|
||||||
</PageContent>
|
</PageContent>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user