From 37c5d8c36e79cea0dd0568267629cf5b2bc70814 Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Tue, 21 Dec 2021 20:51:08 +0300 Subject: [PATCH] feat: downloading materials from lesson --- src/hooks/queries/lesson/useFetchLesson.ts | 22 ++++- src/interfaces/index.ts | 22 ++++- .../edit/components/LessonList/LessonList.tsx | 6 +- src/pages/lesson/lesson.tsx | 96 ++++++++++++------- .../components/CourseList/CourseItem.tsx | 1 + src/pages/profile/components/index.tsx | 4 +- src/services/LessonService.ts | 11 +++ 7 files changed, 120 insertions(+), 42 deletions(-) diff --git a/src/hooks/queries/lesson/useFetchLesson.ts b/src/hooks/queries/lesson/useFetchLesson.ts index 7c2f27b..ddf6915 100644 --- a/src/hooks/queries/lesson/useFetchLesson.ts +++ b/src/hooks/queries/lesson/useFetchLesson.ts @@ -1,12 +1,28 @@ -import useAsync from '@educt/hooks/useAsync'; import { ILesson } from '@educt/interfaces'; import { LessonServiceInstance } from '@educt/services'; +import useAsync from '@educt/hooks/useAsync'; import { useEffect } from 'react'; +import { useToast } from '@chakra-ui/react'; +import { useErrorHandler } from 'react-error-boundary'; const useFetchLesson = (id: string) => { + const toast = useToast(); + const handleError = useErrorHandler(); + const fetch = async (id: string) => { - const { data } = await LessonServiceInstance.fetchLessonById(id); - return data; + try { + const { data } = await LessonServiceInstance.fetchLessonById(id); + return data; + } catch (error: any) { + if (error.response) { + if (error.response.status === 403) { + toast({ title: 'You are not able to view this lesson.', status: 'warning' }); + } + } else { + handleError(error); + } + return Promise.reject(error); + } }; const { execute: fetchLessonById, ...state } = useAsync>(fetch); diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index 37adf39..53e43fa 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -27,7 +27,7 @@ export interface ICourse { status: CourseStatusEnum; teacher: Pick; category: ICategory; - lessons: ILesson[]; + lessons: Omit[]; students: IUser[]; color: ColorType | null; students_count: string; @@ -45,10 +45,30 @@ export interface ILesson { duration: string; color?: ColorType | undefined | null; materials_count?: string | undefined; + materials: Array; + content: ILessonContent; created_at: string; updated_at: string; } +export interface ILessonMaterial { + id: number; + lesson_id: string; + name: string; + client_name: string; + size: number; + url: string; + ext: string; + created_at: string; + updated_at: string; +} + +export interface ILessonContent { + id: number; + lesson_id: string; + video_url: string; +} + export interface ICategory { id: string; title: string; diff --git a/src/pages/courses/edit/components/LessonList/LessonList.tsx b/src/pages/courses/edit/components/LessonList/LessonList.tsx index a59c65e..e5bff33 100644 --- a/src/pages/courses/edit/components/LessonList/LessonList.tsx +++ b/src/pages/courses/edit/components/LessonList/LessonList.tsx @@ -56,13 +56,13 @@ const CreateLessonButton: React.FC<{ id: string }> = ({ id }) => { const LessonList: React.FC = ({ course }) => { const history = useHistory(); const { onOpen: onOpenDelDialog, onClose: onCloseDelDialog, isOpen: isOpenDelDialog } = useDisclosure(); - const [lessons, setLessons] = useState>(course.lessons); + const [lessons, setLessons] = useState(course.lessons); const [deleting, setDeleting] = useState | null>(null); const handleError = useErrorHandler(); const handleCreateLesson = (): void => history.push('/lessons/create'); const handleEditLesson = (id: string): void => history.push(`/lessons/edit/${id}`); - const handleDeleteLesson = (lesson: ILesson): void => { + const handleDeleteLesson = (lesson: ICourse['lessons'][number]): void => { setDeleting({ id: lesson.id, title: lesson.title }); onOpenDelDialog(); }; @@ -93,7 +93,7 @@ const LessonList: React.FC = ({ course }) => { index, }: { provided: DraggableProvided; - lesson: ILesson; + lesson: ICourse['lessons'][number]; isDragging: boolean; index: number; }) => { diff --git a/src/pages/lesson/lesson.tsx b/src/pages/lesson/lesson.tsx index 86d6707..9c3349c 100644 --- a/src/pages/lesson/lesson.tsx +++ b/src/pages/lesson/lesson.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useRef, useState } from 'react'; import moment from 'moment'; +import * as helpres from '@educt/helpers'; import { Link as ReactRouterLink, useHistory } from 'react-router-dom'; import { Flex, @@ -15,14 +16,13 @@ import { Stack, Icon, IconButton, - useToast, useColorModeValue, } from '@chakra-ui/react'; /** * Types */ -import { ILesson, IPageProps } from '@educt/interfaces'; +import { ILesson, ILessonMaterial, IPageProps } from '@educt/interfaces'; /** * Hooks @@ -32,41 +32,23 @@ import { PageContent, PageHeading, PageWrapper } from '@educt/components/PageEle import { useFetchLesson } from '@educt/hooks/queries/lesson/useFetchLesson'; import LoadingPage from '@educt/components/LoadingPage'; import { MdOutlineCircle, MdOutlineFilePresent, MdSkipNext, MdSkipPrevious } from 'react-icons/md'; -import { useRootStore } from '@educt/hooks/useRootStore'; import { Helmet } from 'react-helmet'; import { useFetchCourseLessons } from '@educt/hooks/queries/course/useFetchCourseLessons'; +import { LessonServiceInstance } from '@educt/services'; /** * Lesson Page */ const LessonPage: React.FC = () => { - const { - userStore: { me }, - } = useRootStore(); const { id } = useParams<{ id: string }>(); const { error, data: lesson, isLoading } = useFetchLesson(id); const { fetchCourseLessons, data: lessons } = useFetchCourseLessons(); - const toast = useToast(); const history = useHistory(); const [nextLesson, setNextLesson] = useState(null); const [prevLesson, setPrevLesson] = useState(null); - const [isPageAvailable, setIsPageAvailable] = useState(true); const currentLessonRef = useRef(null); const bg = useColorModeValue('gray.50', 'gray.700'); - useEffect(() => { - /** - * Сheck if the user has access to the lesson - */ - if (me === null || lesson === null) return; - const isHasAccess = me.courses?.some(course => course.id === lesson.course_id) ?? true; - - if (!isHasAccess) { - toast({ title: 'You are not able to view this lesson.', status: 'warning' }); - setIsPageAvailable(false); - } - }, []); - useEffect(() => { currentLessonRef.current?.scrollIntoView({ block: 'center' }); window.scrollTo(0, 0); @@ -88,17 +70,39 @@ const LessonPage: React.FC = () => { } }, [lesson]); + /** + * Lesson not found + */ if (error?.response?.status === 404) { return ; } + /** + * Unavailable lesson for authorized user + */ + if (error?.response?.status === 403) { + return ; + } + if (!lesson || !lessons || isLoading) return ; - if (!isPageAvailable) return ; - const handleNextLesson = () => nextLesson && history.push(`/lesson/${nextLesson.id}`); + const handlePrevLesson = () => prevLesson && history.push(`/lesson/${prevLesson.id}`); + const handleDownloadMaterial = + ({ name, client_name }: ILessonMaterial) => + async () => { + const blob = await LessonServiceInstance.fetchMaterial(name); + if (blob instanceof Blob) { + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = `${client_name}`; + link.click(); + window.URL.revokeObjectURL(link.href); + } + }; + return ( @@ -177,17 +181,41 @@ const LessonPage: React.FC = () => { Materials - - {[1, 2, 3].map(n => ( - - - - lesson-1_sourse-code.zip
- 190kB -
-
- ))} -
+ {lesson.materials.length !== 0 ? ( + + {lesson.materials.map(material => ( + + + + {material.client_name} +
+ {helpres.transformBytes(material.size)} +
+
+ ))} +
+ ) : ( + + + There are no materials + + + )} diff --git a/src/pages/profile/components/CourseList/CourseItem.tsx b/src/pages/profile/components/CourseList/CourseItem.tsx index 109bcc2..e5f4112 100644 --- a/src/pages/profile/components/CourseList/CourseItem.tsx +++ b/src/pages/profile/components/CourseList/CourseItem.tsx @@ -37,6 +37,7 @@ const CourseItem: React.FC = ({ course }) => { = props => { {email} - + + + diff --git a/src/services/LessonService.ts b/src/services/LessonService.ts index 1bc4388..7073638 100644 --- a/src/services/LessonService.ts +++ b/src/services/LessonService.ts @@ -20,6 +20,17 @@ class LessonService { return result.data; } + /** + * Download material file by file name + * + * @param fileName File name + * @returns Blob + */ + public async fetchMaterial(fileName: string): Promise { + const result = await this.api.get(`/v1/lessons/materials/${fileName}`, { responseType: 'blob' }); + return result.data; + } + /** * Delete lesson by id *