mirror of
https://github.com/sergeyyarkov/educt-client.git
synced 2026-10-11 10:19:22 +03:00
feat: downloading materials from lesson
This commit is contained in:
parent
2f8b7d3f71
commit
37c5d8c36e
@ -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);
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
}) => {
|
}) => {
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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
|
||||||
*
|
*
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user