diff --git a/package-lock.json b/package-lock.json index da46de3..f428103 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,7 +31,7 @@ "react-player": "^2.9.0", "react-router-dom": "^5.2.0", "react-spinners": "^0.11.0", - "react-virtuoso": "^2.2.8", + "react-virtuoso": "^2.13.1", "socket.io-client": "^4.4.1", "validator": "^13.6.0", "yup": "^0.32.9" @@ -1988,11 +1988,11 @@ } }, "node_modules/@virtuoso.dev/react-urx": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/@virtuoso.dev/react-urx/-/react-urx-0.2.8.tgz", - "integrity": "sha512-QV9ZJ9Yla88ySgN+DuHLDYoQa0UDRrEq88sLqQESZUWzLughrkpg1BLmQT7FuiU2M78A+rQ9DR3GBlWrJIx+Bw==", + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/@virtuoso.dev/react-urx/-/react-urx-0.2.13.tgz", + "integrity": "sha512-MY0ugBDjFb5Xt8v2HY7MKcRGqw/3gTpMlLXId2EwQvYJoC8sP7nnXjAxcBtTB50KTZhO0SbzsFimaZ7pSdApwA==", "dependencies": { - "@virtuoso.dev/urx": "^0.2.8" + "@virtuoso.dev/urx": "^0.2.13" }, "engines": { "node": ">=10" @@ -2002,9 +2002,9 @@ } }, "node_modules/@virtuoso.dev/urx": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/@virtuoso.dev/urx/-/urx-0.2.8.tgz", - "integrity": "sha512-40uiTn5z1J/7at9Ao/aesyFhE7sS1cpb2MrbP/JSS6dcLMsQJcAZGedCY3bKkyjANH9DX/ciUpkpmT3TXmUBOQ==" + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/@virtuoso.dev/urx/-/urx-0.2.13.tgz", + "integrity": "sha512-iirJNv92A1ZWxoOHHDYW/1KPoi83939o83iUBQHIim0i3tMeSKEh+bxhJdTHQ86Mr4uXx9xGUTq69cp52ZP8Xw==" }, "node_modules/@vitejs/plugin-react": { "version": "1.0.4", @@ -4849,18 +4849,19 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, "node_modules/react-virtuoso": { - "version": "2.2.8", - "resolved": "https://registry.npmjs.org/react-virtuoso/-/react-virtuoso-2.2.8.tgz", - "integrity": "sha512-DArJ29ZrhdygdLKdXs/GB3VNDxaAF7YjdgIjlWPuK6UsV848hNmtw9px3LPdcQBErxaz7XCtHNfc7yInX+/M7g==", + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/react-virtuoso/-/react-virtuoso-2.13.1.tgz", + "integrity": "sha512-Wek6hYPYU7W1r9fGEF1/sMrFOH8VV8PW64fCRxx9OSSlq5q+5Cp0uf+7eP0jFWqpZtC2DrqFoj+seZXjkiyHrA==", "dependencies": { - "@virtuoso.dev/react-urx": "^0.2.8", - "@virtuoso.dev/urx": "^0.2.8" + "@virtuoso.dev/react-urx": "^0.2.12", + "@virtuoso.dev/urx": "^0.2.12" }, "engines": { "node": ">=10" }, "peerDependencies": { - "react": ">=16" + "react": ">=16 || >=17 || >= 18", + "react-dom": ">=16 || >=17 || >= 18" } }, "node_modules/redux": { @@ -7131,17 +7132,17 @@ } }, "@virtuoso.dev/react-urx": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/@virtuoso.dev/react-urx/-/react-urx-0.2.8.tgz", - "integrity": "sha512-QV9ZJ9Yla88ySgN+DuHLDYoQa0UDRrEq88sLqQESZUWzLughrkpg1BLmQT7FuiU2M78A+rQ9DR3GBlWrJIx+Bw==", + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/@virtuoso.dev/react-urx/-/react-urx-0.2.13.tgz", + "integrity": "sha512-MY0ugBDjFb5Xt8v2HY7MKcRGqw/3gTpMlLXId2EwQvYJoC8sP7nnXjAxcBtTB50KTZhO0SbzsFimaZ7pSdApwA==", "requires": { - "@virtuoso.dev/urx": "^0.2.8" + "@virtuoso.dev/urx": "^0.2.13" } }, "@virtuoso.dev/urx": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/@virtuoso.dev/urx/-/urx-0.2.8.tgz", - "integrity": "sha512-40uiTn5z1J/7at9Ao/aesyFhE7sS1cpb2MrbP/JSS6dcLMsQJcAZGedCY3bKkyjANH9DX/ciUpkpmT3TXmUBOQ==" + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/@virtuoso.dev/urx/-/urx-0.2.13.tgz", + "integrity": "sha512-iirJNv92A1ZWxoOHHDYW/1KPoi83939o83iUBQHIim0i3tMeSKEh+bxhJdTHQ86Mr4uXx9xGUTq69cp52ZP8Xw==" }, "@vitejs/plugin-react": { "version": "1.0.4", @@ -9200,12 +9201,12 @@ } }, "react-virtuoso": { - "version": "2.2.8", - "resolved": "https://registry.npmjs.org/react-virtuoso/-/react-virtuoso-2.2.8.tgz", - "integrity": "sha512-DArJ29ZrhdygdLKdXs/GB3VNDxaAF7YjdgIjlWPuK6UsV848hNmtw9px3LPdcQBErxaz7XCtHNfc7yInX+/M7g==", + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/react-virtuoso/-/react-virtuoso-2.13.1.tgz", + "integrity": "sha512-Wek6hYPYU7W1r9fGEF1/sMrFOH8VV8PW64fCRxx9OSSlq5q+5Cp0uf+7eP0jFWqpZtC2DrqFoj+seZXjkiyHrA==", "requires": { - "@virtuoso.dev/react-urx": "^0.2.8", - "@virtuoso.dev/urx": "^0.2.8" + "@virtuoso.dev/react-urx": "^0.2.12", + "@virtuoso.dev/urx": "^0.2.12" } }, "redux": { diff --git a/package.json b/package.json index 71982e9..11c2449 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "react-player": "^2.9.0", "react-router-dom": "^5.2.0", "react-spinners": "^0.11.0", - "react-virtuoso": "^2.2.8", + "react-virtuoso": "^2.13.1", "socket.io-client": "^4.4.1", "validator": "^13.6.0", "yup": "^0.32.9" diff --git a/src/App.tsx b/src/App.tsx index ce78a65..14844db 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -18,6 +18,19 @@ import { SocketContext } from './contexts'; import { HistoryMessageType, UserOnlineListType, UserSeesionType } from './types'; import { useToast } from '@chakra-ui/react'; +/** + * Virtuoso's resize observer can this error, + * which is caught by DnD and aborts dragging. + */ +window.addEventListener('error', e => { + if ( + e.message === 'ResizeObserver loop completed with undelivered notifications.' || + e.message === 'ResizeObserver loop limit exceeded' + ) { + e.stopImmediatePropagation(); + } +}); + const App = () => { const { userStore, diff --git a/src/components/Forms/CourseForm/CourseForm.tsx b/src/components/Forms/CourseForm/CourseForm.tsx index bac67e9..3d56bf2 100644 --- a/src/components/Forms/CourseForm/CourseForm.tsx +++ b/src/components/Forms/CourseForm/CourseForm.tsx @@ -58,9 +58,10 @@ const CourseForm: React.FC = ({ onSubmit, buttonLabel, isLo */ const loadUsersOptions = async (): Promise => { try { - const users = await UserServiceInstance.fetchAll({ limit: 12, role: UserRoleEnum.TEACHER }); + const teachers = await UserServiceInstance.fetchAll({ role: UserRoleEnum.TEACHER }); + const admins = await UserServiceInstance.fetchAll({ role: UserRoleEnum.ADMIN }); - return users.data.map(user => ({ + return teachers.data.concat(admins.data).map(user => ({ label: user.fullname, value: user.id, })); diff --git a/src/components/Forms/LessonForm/CreateLessonForm.tsx b/src/components/Forms/LessonForm/CreateLessonForm.tsx index af5e3f2..d29a494 100644 --- a/src/components/Forms/LessonForm/CreateLessonForm.tsx +++ b/src/components/Forms/LessonForm/CreateLessonForm.tsx @@ -27,6 +27,7 @@ const CreateLessonForm: React.FC = () => { description: data.description, duration: data.duration, video: data.video, + linked_video_url: data.linked_video_url, materials: Array.from(data.materials || []), course_id, }); diff --git a/src/components/Forms/LessonForm/EditLessonForm.tsx b/src/components/Forms/LessonForm/EditLessonForm.tsx index 537d4fe..a7acebc 100644 --- a/src/components/Forms/LessonForm/EditLessonForm.tsx +++ b/src/components/Forms/LessonForm/EditLessonForm.tsx @@ -27,6 +27,7 @@ const EditLessonForm: React.FC = ({ lesson }) => { title: lesson.title, description: lesson.description, duration: lesson.duration, + linked_video_url: lesson.linked_video_url || undefined, video: undefined, materials: undefined, }, @@ -41,6 +42,7 @@ const EditLessonForm: React.FC = ({ lesson }) => { description: data.description, video: data.video, duration: data.duration, + linked_video_url: data.linked_video_url, materials: data.materials?.length !== 0 ? Array.from(data.materials || []) : null, }); diff --git a/src/components/Forms/LessonForm/LessonForm.tsx b/src/components/Forms/LessonForm/LessonForm.tsx index 7b6b3e2..669a687 100644 --- a/src/components/Forms/LessonForm/LessonForm.tsx +++ b/src/components/Forms/LessonForm/LessonForm.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import { FormControl, FormHelperText, @@ -26,6 +26,7 @@ export type InputFields = { description: string; duration: string; video: File; + linked_video_url?: string | undefined; materials?: FileList | undefined; }; @@ -53,6 +54,8 @@ const LessonForm: React.FC = ({ formState: { errors, isDirty }, } = reactHookForm; + const [isLinkedVideoUrl, setIsLinkedVideoUrl] = useState(false); + const watchDescription = watch('description'); return ( @@ -85,29 +88,43 @@ const LessonForm: React.FC = ({ - - Video - ( - onChange(file)} - file={file} - preloadedVideoUrl={preloadedVideo && constants.BACKEND_URL + preloadedVideo.url} - /> - )} - /> - - {errors.video?.message} - - - Supported formats:{' '} - {Object.keys(VideoSupportedFormatsEnum) - .map(ext => `${ext.toLowerCase()}`) - .join(', ')} - - + {isLinkedVideoUrl ? ( + + Video URL + + + {errors.linked_video_url?.message} + + setIsLinkedVideoUrl(false)}> + choose from files + + + ) : ( + + Video + ( + onChange(file)} + file={file} + setIsLinkedVideoUrl={setIsLinkedVideoUrl} + preloadedVideoUrl={preloadedVideo && constants.BACKEND_URL + preloadedVideo.url} + /> + )} + /> + + {errors.video?.message} + + + Supported formats:{' '} + {Object.keys(VideoSupportedFormatsEnum) + .map(ext => `${ext.toLowerCase()}`) + .join(', ')} + + + )} Video duration diff --git a/src/components/Forms/LessonForm/LessonForm.validator.ts b/src/components/Forms/LessonForm/LessonForm.validator.ts index 79f13d8..7ffca8b 100644 --- a/src/components/Forms/LessonForm/LessonForm.validator.ts +++ b/src/components/Forms/LessonForm/LessonForm.validator.ts @@ -18,7 +18,8 @@ const CreateLessonFormSchema = yup .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'), + .optional(), + linked_video_url: yup.string().matches(new RegExp('^(http|https|ftp)://'), 'URL is incorrect'), materials: yup .mixed() .test( diff --git a/src/components/Select/Select.tsx b/src/components/Select/Select.tsx index 13d2ad4..503d006 100644 --- a/src/components/Select/Select.tsx +++ b/src/components/Select/Select.tsx @@ -41,7 +41,7 @@ const Select: React.FC = props => { > {options?.find(option => option.value === value)?.label || props?.placeholder || 'Select Option'} - + {options && options.length !== 0 ? ( options.map(option => ( = props => { onClick={() => { props.onChange(option.value); }} + pb='0' + pt='0' + minH='35px' > {option.label} diff --git a/src/components/VideoUploader/VideoUploader.tsx b/src/components/VideoUploader/VideoUploader.tsx index 35c4023..228d78f 100644 --- a/src/components/VideoUploader/VideoUploader.tsx +++ b/src/components/VideoUploader/VideoUploader.tsx @@ -1,6 +1,6 @@ -import React, { useEffect, useRef, useState } from 'react'; +import React, { Dispatch, SetStateAction, 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 { MdLink, MdUpload } from 'react-icons/md'; import { VideoSupportedFormatsEnum } from '@educt/enums'; import { LessonVideoType } from '@educt/types'; @@ -8,9 +8,15 @@ type VideoUploaderPropsType = { onChange: (file: File) => void; file?: File | LessonVideoType | undefined; preloadedVideoUrl?: string | undefined; + setIsLinkedVideoUrl?: Dispatch>; }; -const VideoUploader: React.FC = ({ onChange, file, preloadedVideoUrl }) => { +const VideoUploader: React.FC = ({ + onChange, + file, + preloadedVideoUrl, + setIsLinkedVideoUrl, +}) => { const fileInputRef = useRef(null); const videoRef = useRef(null); const [videoObjectUrl, setVideoObjectUrl] = useState(undefined); @@ -42,11 +48,23 @@ const VideoUploader: React.FC = ({ onChange, file, prelo }; const ChangeVideoButton = () => ( - ); + const ChangeUrlVideoButton = () => ( + + ); + useEffect(() => { if (videoRef.current) { videoRef.current.load(); @@ -91,9 +109,14 @@ const VideoUploader: React.FC = ({ onChange, file, prelo - + + + + ) : ( @@ -102,7 +125,10 @@ const VideoUploader: React.FC = ({ onChange, file, prelo - + + + + )} = () => { if (me === null) return ; + const videoURL = lesson.video ? constants.BACKEND_URL + lesson.video.url : lesson.linked_video_url; + return ( @@ -153,13 +155,13 @@ const LessonPage: React.FC = () => { - {lesson.video ? ( + {lesson.video || lesson.linked_video_url ? ( ) : ( diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 8ab0790..69881af 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -207,6 +207,7 @@ export type CreateLessonParamsType = { duration: string; course_id: string; video: File; + linked_video_url?: string | undefined; materials?: File[] | undefined; }; @@ -216,6 +217,7 @@ export type UpdateLessonParamsType = { duration?: string | undefined; course_id?: string | undefined; video?: File | undefined; + linked_video_url?: string | undefined; materials?: File[] | undefined | null; };