feat: downloading materials from lesson

This commit is contained in:
Sergey Yarkov 2021-12-21 20:51:08 +03:00
parent 2f8b7d3f71
commit 37c5d8c36e
7 changed files with 120 additions and 42 deletions

View File

@ -1,12 +1,28 @@
import useAsync from '@educt/hooks/useAsync';
import { ILesson } from '@educt/interfaces'; import { ILesson } from '@educt/interfaces';
import { LessonServiceInstance } from '@educt/services'; import { LessonServiceInstance } from '@educt/services';
import useAsync from '@educt/hooks/useAsync';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useToast } from '@chakra-ui/react';
import { useErrorHandler } from 'react-error-boundary';
const useFetchLesson = (id: string) => { const useFetchLesson = (id: string) => {
const toast = useToast();
const handleError = useErrorHandler();
const fetch = async (id: string) => { const fetch = async (id: string) => {
const { data } = await LessonServiceInstance.fetchLessonById(id); try {
return data; 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<ILesson, Parameters<typeof fetch>>(fetch); const { execute: fetchLessonById, ...state } = useAsync<ILesson, Parameters<typeof fetch>>(fetch);

View File

@ -27,7 +27,7 @@ export interface ICourse {
status: CourseStatusEnum; status: CourseStatusEnum;
teacher: Pick<IUser, 'id' | 'first_name' | 'fullname' | 'last_name' | 'email' | 'created_at' | 'updated_at'>; teacher: Pick<IUser, 'id' | 'first_name' | 'fullname' | 'last_name' | 'email' | 'created_at' | 'updated_at'>;
category: ICategory; category: ICategory;
lessons: ILesson[]; lessons: Omit<ILesson, 'materials' | 'content'>[];
students: IUser[]; students: IUser[];
color: ColorType | null; color: ColorType | null;
students_count: string; students_count: string;
@ -45,10 +45,30 @@ export interface ILesson {
duration: string; duration: string;
color?: ColorType | undefined | null; color?: ColorType | undefined | null;
materials_count?: string | undefined; materials_count?: string | undefined;
materials: Array<ILessonMaterial>;
content: ILessonContent;
created_at: string; created_at: string;
updated_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 { export interface ICategory {
id: string; id: string;
title: string; title: string;

View File

@ -56,13 +56,13 @@ const CreateLessonButton: React.FC<{ id: string }> = ({ id }) => {
const LessonList: React.FC<LessonListPropsType> = ({ course }) => { const LessonList: React.FC<LessonListPropsType> = ({ course }) => {
const history = useHistory(); const history = useHistory();
const { onOpen: onOpenDelDialog, onClose: onCloseDelDialog, isOpen: isOpenDelDialog } = useDisclosure(); const { onOpen: onOpenDelDialog, onClose: onCloseDelDialog, isOpen: isOpenDelDialog } = useDisclosure();
const [lessons, setLessons] = useState<Array<ILesson>>(course.lessons); const [lessons, setLessons] = useState<ICourse['lessons']>(course.lessons);
const [deleting, setDeleting] = useState<Pick<ILesson, 'id' | 'title'> | null>(null); const [deleting, setDeleting] = useState<Pick<ILesson, 'id' | 'title'> | null>(null);
const handleError = useErrorHandler(); const handleError = useErrorHandler();
const handleCreateLesson = (): void => history.push('/lessons/create'); const handleCreateLesson = (): void => history.push('/lessons/create');
const handleEditLesson = (id: string): void => history.push(`/lessons/edit/${id}`); 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 }); setDeleting({ id: lesson.id, title: lesson.title });
onOpenDelDialog(); onOpenDelDialog();
}; };
@ -93,7 +93,7 @@ const LessonList: React.FC<LessonListPropsType> = ({ course }) => {
index, index,
}: { }: {
provided: DraggableProvided; provided: DraggableProvided;
lesson: ILesson; lesson: ICourse['lessons'][number];
isDragging: boolean; isDragging: boolean;
index: number; index: number;
}) => { }) => {

View File

@ -1,5 +1,6 @@
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import moment from 'moment'; import moment from 'moment';
import * as helpres from '@educt/helpers';
import { Link as ReactRouterLink, useHistory } from 'react-router-dom'; import { Link as ReactRouterLink, useHistory } from 'react-router-dom';
import { import {
Flex, Flex,
@ -15,14 +16,13 @@ import {
Stack, Stack,
Icon, Icon,
IconButton, IconButton,
useToast,
useColorModeValue, useColorModeValue,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
/** /**
* Types * Types
*/ */
import { ILesson, IPageProps } from '@educt/interfaces'; import { ILesson, ILessonMaterial, IPageProps } from '@educt/interfaces';
/** /**
* Hooks * Hooks
@ -32,41 +32,23 @@ import { PageContent, PageHeading, PageWrapper } from '@educt/components/PageEle
import { useFetchLesson } from '@educt/hooks/queries/lesson/useFetchLesson'; import { useFetchLesson } from '@educt/hooks/queries/lesson/useFetchLesson';
import LoadingPage from '@educt/components/LoadingPage'; import LoadingPage from '@educt/components/LoadingPage';
import { MdOutlineCircle, MdOutlineFilePresent, MdSkipNext, MdSkipPrevious } from 'react-icons/md'; import { MdOutlineCircle, MdOutlineFilePresent, MdSkipNext, MdSkipPrevious } from 'react-icons/md';
import { useRootStore } from '@educt/hooks/useRootStore';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { useFetchCourseLessons } from '@educt/hooks/queries/course/useFetchCourseLessons'; import { useFetchCourseLessons } from '@educt/hooks/queries/course/useFetchCourseLessons';
import { LessonServiceInstance } from '@educt/services';
/** /**
* Lesson Page * Lesson Page
*/ */
const LessonPage: React.FC<IPageProps> = () => { const LessonPage: React.FC<IPageProps> = () => {
const {
userStore: { me },
} = useRootStore();
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();
const { error, data: lesson, isLoading } = useFetchLesson(id); const { error, data: lesson, isLoading } = useFetchLesson(id);
const { fetchCourseLessons, data: lessons } = useFetchCourseLessons(); const { fetchCourseLessons, data: lessons } = useFetchCourseLessons();
const toast = useToast();
const history = useHistory(); const history = useHistory();
const [nextLesson, setNextLesson] = useState<ILesson | null>(null); const [nextLesson, setNextLesson] = useState<ILesson | null>(null);
const [prevLesson, setPrevLesson] = useState<ILesson | null>(null); const [prevLesson, setPrevLesson] = useState<ILesson | null>(null);
const [isPageAvailable, setIsPageAvailable] = useState<boolean>(true);
const currentLessonRef = useRef<HTMLDivElement | null>(null); const currentLessonRef = useRef<HTMLDivElement | null>(null);
const bg = useColorModeValue('gray.50', 'gray.700'); 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(() => { useEffect(() => {
currentLessonRef.current?.scrollIntoView({ block: 'center' }); currentLessonRef.current?.scrollIntoView({ block: 'center' });
window.scrollTo(0, 0); window.scrollTo(0, 0);
@ -88,17 +70,39 @@ const LessonPage: React.FC<IPageProps> = () => {
} }
}, [lesson]); }, [lesson]);
/**
* Lesson not found
*/
if (error?.response?.status === 404) { if (error?.response?.status === 404) {
return <Redirect to='/404' />; return <Redirect to='/404' />;
} }
/**
* Unavailable lesson for authorized user
*/
if (error?.response?.status === 403) {
return <Redirect to={`/courses`} />;
}
if (!lesson || !lessons || isLoading) return <LoadingPage />; if (!lesson || !lessons || isLoading) return <LoadingPage />;
if (!isPageAvailable) return <Redirect to={`/course/${lesson.course_id}`} />;
const handleNextLesson = () => nextLesson && history.push(`/lesson/${nextLesson.id}`); const handleNextLesson = () => nextLesson && history.push(`/lesson/${nextLesson.id}`);
const handlePrevLesson = () => prevLesson && history.push(`/lesson/${prevLesson.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 ( return (
<PageWrapper> <PageWrapper>
<Helmet> <Helmet>
@ -177,17 +181,41 @@ const LessonPage: React.FC<IPageProps> = () => {
<Text fontSize={'2xl'} fontWeight={'medium'}> <Text fontSize={'2xl'} fontWeight={'medium'}>
Materials Materials
</Text> </Text>
<Stack> {lesson.materials.length !== 0 ? (
{[1, 2, 3].map(n => ( <Stack>
<Flex key={n} alignItems={'center'} mt='5' p='3' bg={bg} borderRadius={'lg'}> {lesson.materials.map(material => (
<Icon as={MdOutlineFilePresent} mr='2' h={4} w={4} /> <Flex
<Text fontSize={'sm'} fontWeight={'medium'} lineHeight={'1rem'}> key={material.id}
lesson-1_sourse-code.zip <br /> alignItems={'center'}
<Text as='small'>190kB</Text> mt='5'
</Text> p='3'
</Flex> bg={bg}
))} borderRadius={'lg'}
</Stack> _hover={{ textDecor: 'underline' }}
>
<Icon as={MdOutlineFilePresent} mr='2' h={4} w={4} />
<Text
onClick={handleDownloadMaterial(material)}
cursor={'pointer'}
overflow={'hidden'}
fontSize={'sm'}
fontWeight={'medium'}
lineHeight={'1rem'}
>
{material.client_name}
<br />
<Text as='small'>{helpres.transformBytes(material.size)}</Text>
</Text>
</Flex>
))}
</Stack>
) : (
<Box textAlign={'center'} mt='3'>
<Text color='gray.500' fontSize={'sm'}>
There are no materials
</Text>
</Box>
)}
</Box> </Box>
</Box> </Box>
</GridItem> </GridItem>

View File

@ -37,6 +37,7 @@ const CourseItem: React.FC<CourseItemPropsType> = ({ course }) => {
</Text> </Text>
<Box> <Box>
<Tag <Tag
size='sm'
color='white' color='white'
boxShadow={`inset 0 0 0px 1px ${course.category.color?.hex}`} boxShadow={`inset 0 0 0px 1px ${course.category.color?.hex}`}
bg={course.category.color?.hex} bg={course.category.color?.hex}

View File

@ -60,7 +60,9 @@ export const ProfileDescription: React.FC<IProfileDescriptionProps> = props => {
{email} {email}
</Text> </Text>
</Heading> </Heading>
<UserBadge mt='2' roles={roles} /> <Box>
<UserBadge mt='2' roles={roles} />
</Box>
</Box> </Box>
</Flex> </Flex>
</Box> </Box>

View File

@ -20,6 +20,17 @@ class LessonService {
return result.data; return result.data;
} }
/**
* Download material file by file name
*
* @param fileName File name
* @returns Blob
*/
public async fetchMaterial(fileName: string): Promise<Blob> {
const result = await this.api.get(`/v1/lessons/materials/${fileName}`, { responseType: 'blob' });
return result.data;
}
/** /**
* Delete lesson by id * Delete lesson by id
* *