From 0dc80fd0e809f98cbbccf11d66c28452a4c8cdd5 Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Sun, 16 Jan 2022 20:17:27 +0300 Subject: [PATCH] feat: edit lesson page --- .../FilesUploader/FilesUploader.tsx | 89 +++++++++++++------ .../Forms/LessonForm/CreateLessonForm.tsx | 4 +- .../Forms/LessonForm/EditLessonForm.tsx | 62 +++++++++++++ .../Forms/LessonForm/LessonForm.tsx | 20 ++++- .../Forms/LessonForm/LessonForm.validator.ts | 31 +++---- .../VideoUploader/VideoUploader.tsx | 42 ++++++--- src/helpers/index.ts | 5 ++ src/pages/courses/components/index.tsx | 5 ++ src/pages/lesson/lesson.tsx | 5 +- src/pages/lessons/edit-lesson/edit-lesson.tsx | 32 +++++++ src/routes.config.ts | 9 ++ src/services/CourseService.ts | 8 ++ 12 files changed, 251 insertions(+), 61 deletions(-) create mode 100644 src/components/Forms/LessonForm/EditLessonForm.tsx create mode 100644 src/pages/lessons/edit-lesson/edit-lesson.tsx diff --git a/src/components/FilesUploader/FilesUploader.tsx b/src/components/FilesUploader/FilesUploader.tsx index 5069c1c..1907b95 100644 --- a/src/components/FilesUploader/FilesUploader.tsx +++ b/src/components/FilesUploader/FilesUploader.tsx @@ -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 = ({ onChange, files }) => { +const FileContent: React.FC<{ name: string; size: number }> = ({ name, size }) => ( + + + + {name} +
+ + {helpers.transformBytes(size)} + +
+
+); + +const FileWrapper: React.FC = ({ children }) => ( + + {children} + +); + +const FilesUploader: React.FC = ({ onChange, files, preloadedFiles }) => { + const [preloaded, setPreloaded] = useState(preloadedFiles || []); const fileInputRef = useRef(null); const onSelectFileHandler = () => { @@ -59,27 +81,30 @@ const FilesUploader: React.FC = ({ onChange, files }) => .map(ext => `.${ext.toLowerCase()}`) .join(', ')} /> + {preloadedFiles && files?.length === 0 && ( + + {preloaded.map((file, index) => ( + + + { + setPreloaded(prev => prev.filter(file => file.size !== file.size)); + onChange([]); + }} + aria-label='remove' + variant={'unstyled'} + size='xs' + icon={} + /> + + ))} + + )} {files && ( {files.map((file, index) => ( - - - - - {file.name} -
- - {helpres.transformBytes(file.size)} - -
-
+ + = ({ onChange, files }) => size='xs' icon={} /> -
+ ))}
)} - ); diff --git a/src/components/Forms/LessonForm/CreateLessonForm.tsx b/src/components/Forms/LessonForm/CreateLessonForm.tsx index b00c241..af5e3f2 100644 --- a/src/components/Forms/LessonForm/CreateLessonForm.tsx +++ b/src/components/Forms/LessonForm/CreateLessonForm.tsx @@ -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({ resolver: yupResolver(LessonFormSchema) }); + const form = useForm({ resolver: yupResolver(CreateLessonFormSchema) }); const { createLesson, isLoading } = useCreateLesson(); const { id: course_id } = useParams<{ id: string }>(); const history = useHistory(); diff --git a/src/components/Forms/LessonForm/EditLessonForm.tsx b/src/components/Forms/LessonForm/EditLessonForm.tsx new file mode 100644 index 0000000..72e74bc --- /dev/null +++ b/src/components/Forms/LessonForm/EditLessonForm.tsx @@ -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 = ({ lesson }) => { + const form = useForm({ + 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 = async data => { + const updatedFields = helpers.getDirtyFields(form.formState.dirtyFields, data); + console.log(updatedFields); + }; + + return ( + { + return { + name: material.client_name, + extname: material.ext, + size: material.size, + url: `materials/${material.name}`, + mimeType: '', + }; + })} + /> + ); +}; + +export default EditLessonForm; diff --git a/src/components/Forms/LessonForm/LessonForm.tsx b/src/components/Forms/LessonForm/LessonForm.tsx index dea0ce4..45e9048 100644 --- a/src/components/Forms/LessonForm/LessonForm.tsx +++ b/src/components/Forms/LessonForm/LessonForm.tsx @@ -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; + preloadedVideo?: LessonVideoType | undefined; + preloadedMaterials?: AttachmentFileType[] | undefined; }; -const LessonForm: React.FC = ({ onSubmit, buttonLabel, isLoading, reactHookForm }) => { +const LessonForm: React.FC = ({ + onSubmit, + buttonLabel, + isLoading, + reactHookForm, + preloadedVideo, + preloadedMaterials, +}) => { const { control, register, @@ -70,7 +80,7 @@ const LessonForm: React.FC = ({ onSubmit, buttonLabel, isLo control={control} name='video' render={({ field: { onChange, value: file } }) => ( - onChange(file)} file={file} /> + onChange(file)} file={file} preloadedVideoUrl={preloadedVideo?.url} /> )} /> @@ -99,7 +109,11 @@ const LessonForm: React.FC = ({ onSubmit, buttonLabel, isLo control={control} name='materials' render={({ field: { onChange, value: files } }) => ( - onChange(files)} files={Array.from(files || [])} /> + onChange(files)} + files={Array.from(files || [])} + preloadedFiles={preloadedMaterials} + /> )} /> diff --git a/src/components/Forms/LessonForm/LessonForm.validator.ts b/src/components/Forms/LessonForm/LessonForm.validator.ts index 04fc076..873d2f8 100644 --- a/src/components/Forms/LessonForm/LessonForm.validator.ts +++ b/src/components/Forms/LessonForm/LessonForm.validator.ts @@ -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() + .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() .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() + .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 }; diff --git a/src/components/VideoUploader/VideoUploader.tsx b/src/components/VideoUploader/VideoUploader.tsx index 7dbb354..35c4023 100644 --- a/src/components/VideoUploader/VideoUploader.tsx +++ b/src/components/VideoUploader/VideoUploader.tsx @@ -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 = ({ onChange, file }) => { +const VideoUploader: React.FC = ({ onChange, file, preloadedVideoUrl }) => { const fileInputRef = useRef(null); const videoRef = useRef(null); const [videoObjectUrl, setVideoObjectUrl] = useState(undefined); + const isPreloadedVideo = preloadedVideoUrl !== undefined; const onSelectFileHandler = () => { if (fileInputRef.current) { @@ -38,12 +41,24 @@ const VideoUploader: React.FC = ({ onChange, file }) => } }; + const ChangeVideoButton = () => ( + + ); + useEffect(() => { if (videoRef.current) { videoRef.current.load(); } }, [videoObjectUrl]); + useEffect(() => { + if (file && file instanceof File) { + setVideoObjectUrl(URL.createObjectURL(file)); + } + }, []); + return ( {file && videoObjectUrl ? ( @@ -53,18 +68,9 @@ const VideoUploader: React.FC = ({ onChange, file }) => - + - ) : ( + ) : !isPreloadedVideo ? ( = ({ onChange, file }) => Select file + ) : ( + + + + + + )} = ({ onChange, file }) => maxW={'0'} h='0' display={'block'} + required={false} opacity={'0'} ml='auto' mr='auto' diff --git a/src/helpers/index.ts b/src/helpers/index.ts index c245fd9..e8672d2 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -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 diff --git a/src/pages/courses/components/index.tsx b/src/pages/courses/components/index.tsx index c527d17..3591567 100644 --- a/src/pages/courses/components/index.tsx +++ b/src/pages/courses/components/index.tsx @@ -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 = props => { @@ -14,6 +15,10 @@ export const CoursesTabs: React.FC = props => { pageStore: { coursesStore }, } = useRootStore(); + useEffect(() => { + coursesStore.setShowedStatus(undefined); + }, []); + return ( me && (me.isAdmin || me.isTeacher ? ( diff --git a/src/pages/lesson/lesson.tsx b/src/pages/lesson/lesson.tsx index 235c751..adec301 100644 --- a/src/pages/lesson/lesson.tsx +++ b/src/pages/lesson/lesson.tsx @@ -156,8 +156,9 @@ const LessonPage: React.FC = () => { - - {!lesson.video && ( + {lesson.video ? ( + + ) : ( Video has not been uploaded yet. diff --git a/src/pages/lessons/edit-lesson/edit-lesson.tsx b/src/pages/lessons/edit-lesson/edit-lesson.tsx new file mode 100644 index 0000000..e04896f --- /dev/null +++ b/src/pages/lessons/edit-lesson/edit-lesson.tsx @@ -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 = () => { + const params = useParams<{ id: string }>(); + const { error, data: lesson, isLoading } = useFetchLesson(params.id); + + if (error?.response?.status === 404) { + return ; + } + + if (!lesson || isLoading) return ; + + return ( + + + + + + + ); +}; + +export default EditLessonPage; diff --git a/src/routes.config.ts b/src/routes.config.ts index 75b90ba..c668776 100644 --- a/src/routes.config.ts +++ b/src/routes.config.ts @@ -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', diff --git a/src/services/CourseService.ts b/src/services/CourseService.ts index beb7fd4..a6470d6 100644 --- a/src/services/CourseService.ts +++ b/src/services/CourseService.ts @@ -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