feat: edit lesson page

This commit is contained in:
Sergey Yarkov 2022-01-16 20:17:27 +03:00
parent 76f72dc9c4
commit 0dc80fd0e8
12 changed files with 251 additions and 61 deletions

View File

@ -1,21 +1,43 @@
import React from 'react';
import * as helpres from '@educt/helpers';
import React, { useState } from 'react';
import * as helpers 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';
import { MdAttachFile, MdOutlineFilePresent } from 'react-icons/md';
import { CloseIcon } from '@chakra-ui/icons';
/**
* Hooks
*/
import { useRef } from 'react';
import { FileSupportedFormatsEnum } from '@educt/enums';
import { AttachmentFileType } from '@educt/types';
type FilesUploaderPropsType = {
onChange: (files: File[]) => void;
files?: File[] | undefined;
preloadedFiles?: AttachmentFileType[] | undefined;
};
const FilesUploader: React.FC<FilesUploaderPropsType> = ({ onChange, files }) => {
const FileContent: React.FC<{ name: string; size: number }> = ({ name, size }) => (
<Flex alignItems={'center'}>
<Icon as={MdOutlineFilePresent} mr='2' h={4} w={4} />
<Text overflow={'hidden'} fontSize={'sm'} fontWeight={'medium'} lineHeight={'1rem'}>
{name}
<br />
<Text as='small' color='gray.500'>
{helpers.transformBytes(size)}
</Text>
</Text>
</Flex>
);
const FileWrapper: React.FC = ({ children }) => (
<Flex alignItems={'center'} justifyContent={'space-between'} p='3' bg={'gray.50'} borderRadius={'lg'}>
{children}
</Flex>
);
const FilesUploader: React.FC<FilesUploaderPropsType> = ({ onChange, files, preloadedFiles }) => {
const [preloaded, setPreloaded] = useState<AttachmentFileType[]>(preloadedFiles || []);
const fileInputRef = useRef<HTMLInputElement>(null);
const onSelectFileHandler = () => {
@ -59,27 +81,30 @@ const FilesUploader: React.FC<FilesUploaderPropsType> = ({ onChange, files }) =>
.map(ext => `.${ext.toLowerCase()}`)
.join(', ')}
/>
{preloadedFiles && files?.length === 0 && (
<Stack>
{preloaded.map((file, index) => (
<FileWrapper key={index}>
<FileContent name={file.name} size={file.size} />
<IconButton
onClick={() => {
setPreloaded(prev => prev.filter(file => file.size !== file.size));
onChange([]);
}}
aria-label='remove'
variant={'unstyled'}
size='xs'
icon={<CloseIcon />}
/>
</FileWrapper>
))}
</Stack>
)}
{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>
<FileWrapper key={index}>
<FileContent name={file.name} size={file.size} />
<IconButton
onClick={onRemoveSelectedFileHandler(file)}
aria-label='remove'
@ -87,12 +112,24 @@ const FilesUploader: React.FC<FilesUploaderPropsType> = ({ onChange, files }) =>
size='xs'
icon={<CloseIcon />}
/>
</Flex>
</FileWrapper>
))}
</Stack>
)}
<Button onClick={onSelectFileHandler} float={'right'} leftIcon={<AddIcon />} variant={'outline'} size='sm' mt='3'>
Upload new
{files?.length === 0 && preloaded?.length === 0 && (
<Text color={'gray.500'} userSelect={'none'} fontSize={'sm'} textAlign={'center'}>
No file selected
</Text>
)}
<Button
onClick={onSelectFileHandler}
float={'right'}
leftIcon={<MdAttachFile />}
variant={'outline'}
size='sm'
mt='3'
>
Select files
</Button>
</Box>
);

View File

@ -7,7 +7,7 @@ import { LessonForm } from '.';
/**
* Validator
*/
import LessonFormSchema from './LessonForm.validator';
import { CreateLessonFormSchema } from './LessonForm.validator';
/**
* Hooks
@ -16,7 +16,7 @@ import { useCreateLesson } from '@educt/hooks/queries';
import { useHistory, useParams } from 'react-router-dom';
const CreateLessonForm: React.FC = () => {
const form = useForm<InputFields>({ resolver: yupResolver(LessonFormSchema) });
const form = useForm<InputFields>({ resolver: yupResolver(CreateLessonFormSchema) });
const { createLesson, isLoading } = useCreateLesson();
const { id: course_id } = useParams<{ id: string }>();
const history = useHistory();

View File

@ -0,0 +1,62 @@
import React from 'react';
import * as helpers from '@educt/helpers';
import { yupResolver } from '@hookform/resolvers/yup';
import { SubmitHandler, useForm } from 'react-hook-form';
import type { InputFields } from './LessonForm';
import { LessonForm } from '.';
/**
* Validator
*/
import { EditLessonFormSchema } from './LessonForm.validator';
/**
* Hooks
*/
import { useHistory, useParams } from 'react-router-dom';
import { ILesson } from '@educt/interfaces';
type EditLessonFormPropsType = {
lesson: ILesson;
};
const EditLessonForm: React.FC<EditLessonFormPropsType> = ({ lesson }) => {
const form = useForm<InputFields>({
resolver: yupResolver(EditLessonFormSchema),
defaultValues: {
title: lesson.title,
description: lesson.description,
duration: lesson.duration,
video: undefined,
materials: undefined,
},
});
const { id: lesson_id } = useParams<{ id: string }>();
const history = useHistory();
const onSubmit: SubmitHandler<InputFields> = async data => {
const updatedFields = helpers.getDirtyFields(form.formState.dirtyFields, data);
console.log(updatedFields);
};
return (
<LessonForm
isLoading={false}
reactHookForm={{ ...form }}
onSubmit={form.handleSubmit(onSubmit)}
buttonLabel='Save'
preloadedVideo={lesson.video || undefined}
preloadedMaterials={lesson.materials.map(material => {
return {
name: material.client_name,
extname: material.ext,
size: material.size,
url: `materials/${material.name}`,
mimeType: '',
};
})}
/>
);
};
export default EditLessonForm;

View File

@ -17,6 +17,7 @@ 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';
import type { AttachmentFileType, LessonVideoType } from '@educt/types';
export type InputFields = {
title: string;
@ -31,9 +32,18 @@ export type LessonFormPropsType = {
buttonLabel: string;
isLoading: boolean;
reactHookForm: UseFormReturn<InputFields>;
preloadedVideo?: LessonVideoType | undefined;
preloadedMaterials?: AttachmentFileType[] | undefined;
};
const LessonForm: React.FC<LessonFormPropsType> = ({ onSubmit, buttonLabel, isLoading, reactHookForm }) => {
const LessonForm: React.FC<LessonFormPropsType> = ({
onSubmit,
buttonLabel,
isLoading,
reactHookForm,
preloadedVideo,
preloadedMaterials,
}) => {
const {
control,
register,
@ -70,7 +80,7 @@ const LessonForm: React.FC<LessonFormPropsType> = ({ onSubmit, buttonLabel, isLo
control={control}
name='video'
render={({ field: { onChange, value: file } }) => (
<VideoUploader onChange={file => onChange(file)} file={file} />
<VideoUploader onChange={file => onChange(file)} file={file} preloadedVideoUrl={preloadedVideo?.url} />
)}
/>
<Text as='small' color='red.500'>
@ -99,7 +109,11 @@ const LessonForm: React.FC<LessonFormPropsType> = ({ onSubmit, buttonLabel, isLo
control={control}
name='materials'
render={({ field: { onChange, value: files } }) => (
<FilesUploader onChange={files => onChange(files)} files={Array.from(files || [])} />
<FilesUploader
onChange={files => onChange(files)}
files={Array.from(files || [])}
preloadedFiles={preloadedMaterials}
/>
)}
/>
<Text as='small' color='red.500'>

View File

@ -4,7 +4,7 @@ import yup from '@educt/schema';
const VIDEO_FILE_SIZE = 5000 * 1024 * 1024;
const MATERIAL_FILE_SIZE = 100000 * 1024;
const LessonFormSchema = yup
const CreateLessonFormSchema = yup
.object({
title: yup.string().required('Title field name is required').max(90, 'Title must be at most 90 characters'),
description: yup
@ -13,28 +13,29 @@ const LessonFormSchema = yup
.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;
}
.mixed<File>()
.test('fileSize', `File size is too large, maximum size is ${helpers.transformBytes(VIDEO_FILE_SIZE)}`, value =>
!value ? true : value.size <= VIDEO_FILE_SIZE
)
.required('Video field name is required'),
materials: yup
.mixed()
.mixed<File[]>()
.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);
}
values => (!values ? true : values.every(value => value.size <= MATERIAL_FILE_SIZE))
)
.optional(),
})
.required();
export default LessonFormSchema;
const EditLessonFormSchema = CreateLessonFormSchema.shape({
video: yup
.mixed<File>()
.test('fileSize', `File size is too large, maximum size is ${helpers.transformBytes(VIDEO_FILE_SIZE)}`, value =>
!value ? true : value.size <= VIDEO_FILE_SIZE
)
.optional(),
});
export { CreateLessonFormSchema, EditLessonFormSchema };

View File

@ -2,16 +2,19 @@ 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';
import { LessonVideoType } from '@educt/types';
type VideoUploaderPropsType = {
onChange: (file: File) => void;
file?: File | undefined;
file?: File | LessonVideoType | undefined;
preloadedVideoUrl?: string | undefined;
};
const VideoUploader: React.FC<VideoUploaderPropsType> = ({ onChange, file }) => {
const VideoUploader: React.FC<VideoUploaderPropsType> = ({ onChange, file, preloadedVideoUrl }) => {
const fileInputRef = useRef<HTMLInputElement>(null);
const videoRef = useRef<HTMLVideoElement>(null);
const [videoObjectUrl, setVideoObjectUrl] = useState<string | undefined>(undefined);
const isPreloadedVideo = preloadedVideoUrl !== undefined;
const onSelectFileHandler = () => {
if (fileInputRef.current) {
@ -38,12 +41,24 @@ const VideoUploader: React.FC<VideoUploaderPropsType> = ({ onChange, file }) =>
}
};
const ChangeVideoButton = () => (
<Button onClick={onSelectFileHandler} leftIcon={<MdUpload />} variant={'outline'} size='sm' mt='2' float='right'>
Change video
</Button>
);
useEffect(() => {
if (videoRef.current) {
videoRef.current.load();
}
}, [videoObjectUrl]);
useEffect(() => {
if (file && file instanceof File) {
setVideoObjectUrl(URL.createObjectURL(file));
}
}, []);
return (
<Box>
{file && videoObjectUrl ? (
@ -53,18 +68,9 @@ const VideoUploader: React.FC<VideoUploaderPropsType> = ({ onChange, file }) =>
<source src={videoObjectUrl} />
</video>
</AspectRatio>
<Button
onClick={onSelectFileHandler}
leftIcon={<MdUpload />}
variant={'outline'}
size='sm'
mt='2'
float='right'
>
Change video
</Button>
<ChangeVideoButton />
</Box>
) : (
) : !isPreloadedVideo ? (
<Flex
p='40px 10px'
textAlign={'center'}
@ -89,6 +95,15 @@ const VideoUploader: React.FC<VideoUploaderPropsType> = ({ onChange, file }) =>
Select file
</Button>
</Flex>
) : (
<Box>
<AspectRatio maxH='lg' mt='2'>
<video controls style={{ borderRadius: '5px' }}>
<source src={preloadedVideoUrl} />
</video>
</AspectRatio>
<ChangeVideoButton />
</Box>
)}
<Input
type='file'
@ -97,6 +112,7 @@ const VideoUploader: React.FC<VideoUploaderPropsType> = ({ onChange, file }) =>
maxW={'0'}
h='0'
display={'block'}
required={false}
opacity={'0'}
ml='auto'
mr='auto'

View File

@ -80,6 +80,11 @@ export function transformBytes(bytes: number, decimals = 2): string {
return `${parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`;
}
export function transformBlobToFile(blob: Blob, fileName: string) {
const file = new File([blob], fileName, { type: blob.type });
return file;
}
/**
*
* @param array Array

View File

@ -6,6 +6,7 @@ import { CourseStatusEnum } from '@educt/enums';
/**
* Hooks
*/
import { useEffect } from 'react';
import { useRootStore } from '@educt/hooks/useRootStore';
export const CoursesTabs: React.FC<BoxProps> = props => {
@ -14,6 +15,10 @@ export const CoursesTabs: React.FC<BoxProps> = props => {
pageStore: { coursesStore },
} = useRootStore();
useEffect(() => {
coursesStore.setShowedStatus(undefined);
}, []);
return (
me &&
(me.isAdmin || me.isTeacher ? (

View File

@ -156,8 +156,9 @@ const LessonPage: React.FC<IPageProps> = () => {
<Grid templateColumns={{ base: '1fr', xl: '3fr 1fr' }}>
<GridItem>
<Box>
{lesson.video ? (
<ReactPlayer width={'100%'} height={'100%'} controls url={lesson.video?.url} />
{!lesson.video && (
) : (
<Flex h='500px' justifyContent={'center'} alignItems={'center'}>
<Text color='gray.500' userSelect={'none'}>
Video has not been uploaded yet.

View File

@ -0,0 +1,32 @@
import React from 'react';
import { IPageProps } from '@educt/interfaces';
import { PageContent, PageHeading, PageWrapper } from '@educt/components/PageElements';
import { Redirect, useParams } from 'react-router-dom';
import LoadingPage from '@educt/components/LoadingPage';
import { useFetchLesson } from '@educt/hooks/queries/lesson/useFetchLesson';
import EditLessonForm from '@educt/components/Forms/LessonForm/EditLessonForm';
/**
* Create Lesson page
*/
const EditLessonPage: React.FC<IPageProps> = () => {
const params = useParams<{ id: string }>();
const { error, data: lesson, isLoading } = useFetchLesson(params.id);
if (error?.response?.status === 404) {
return <Redirect to='/404' />;
}
if (!lesson || isLoading) return <LoadingPage />;
return (
<PageWrapper>
<PageHeading heading='Lesson Editor' description={lesson.description} />
<PageContent>
<EditLessonForm lesson={lesson} />
</PageContent>
</PageWrapper>
);
};
export default EditLessonPage;

View File

@ -8,6 +8,7 @@ import AuthPage from '@educt/pages/auth/auth';
import NotFoundPage from '@educt/pages/404/404';
import CreateCoursePage from '@educt/pages/courses/create/create';
import CreateLessonPage from '@educt/pages/courses/create-lesson/create-lesson';
import EditLessonPage from './pages/lessons/edit-lesson/edit-lesson';
import CoursePage from '@educt/pages/course/course';
import EditCoursePage from '@educt/pages/courses/edit/edit';
import ChangePasswordPage from '@educt/pages/profile/change-password/change-password';
@ -78,6 +79,14 @@ export const routes: IRouteOption[] = [
exact: true,
roles: [UserRoleEnum.ADMIN, UserRoleEnum.TEACHER],
},
{
path: '/lessons/edit/:id',
title: 'Edit Lesson',
component: EditLessonPage,
isPrivate: true,
exact: true,
roles: [UserRoleEnum.ADMIN, UserRoleEnum.TEACHER],
},
{
path: '/lesson/:id',
title: 'Lesson',

View File

@ -88,6 +88,7 @@ class CourseService {
}
/**
* Create new course
*
* @param data Data for creating course
* @returns
@ -104,6 +105,13 @@ class CourseService {
return result.data;
}
/**
* Update course
*
* @param id Course id
* @param data Data to update
* @returns Updated course
*/
public async update(
id: string,
data: UpdateCourseParamsType