mirror of
https://github.com/sergeyyarkov/educt-client.git
synced 2026-10-11 10:19:22 +03:00
feat: updated lesson page layout & fethcing lessons
This commit is contained in:
parent
44cc8f47ec
commit
e4eb6e84fb
21
src/hooks/queries/course/useFetchCourseLessons.ts
Normal file
21
src/hooks/queries/course/useFetchCourseLessons.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import useAsync from '@educt/hooks/useAsync';
|
||||||
|
import { ILesson } from '@educt/interfaces';
|
||||||
|
import { CourseServiceInstance } from '@educt/services';
|
||||||
|
|
||||||
|
const useFetchCourseLessons = () => {
|
||||||
|
const fetch = async (id: string) => {
|
||||||
|
try {
|
||||||
|
const { data } = await CourseServiceInstance.fetchLessonsById(id);
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const { execute, ...state } = useAsync<Array<ILesson>, Parameters<typeof fetch>>(fetch);
|
||||||
|
|
||||||
|
return { fetchCourseLessons: execute, ...state };
|
||||||
|
};
|
||||||
|
|
||||||
|
export { useFetchCourseLessons };
|
||||||
@ -24,6 +24,7 @@ import LoadingPage from '@educt/components/LoadingPage';
|
|||||||
import { useHistory, useParams } from 'react-router-dom';
|
import { useHistory, useParams } from 'react-router-dom';
|
||||||
import { useFetchCourse } from '@educt/hooks/queries';
|
import { useFetchCourse } from '@educt/hooks/queries';
|
||||||
import { useRootStore } from '@educt/hooks/useRootStore';
|
import { useRootStore } from '@educt/hooks/useRootStore';
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
|
|
||||||
const CoursePage: React.FC<IPageProps> = () => {
|
const CoursePage: React.FC<IPageProps> = () => {
|
||||||
const {
|
const {
|
||||||
@ -47,6 +48,9 @@ const CoursePage: React.FC<IPageProps> = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PageWrapper maxW='1200px'>
|
<PageWrapper maxW='1200px'>
|
||||||
|
<Helmet>
|
||||||
|
<title>{course.title}</title>
|
||||||
|
</Helmet>
|
||||||
<CourseHeader>
|
<CourseHeader>
|
||||||
<CourseHeading title={course.title} teacher={course.teacher} category={course.category} />
|
<CourseHeading title={course.title} teacher={course.teacher} category={course.category} />
|
||||||
<CourseInfo
|
<CourseInfo
|
||||||
|
|||||||
@ -1,11 +1,27 @@
|
|||||||
import React from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { Flex, Box, Button, Divider, Heading, Text, Grid, GridItem, Stack, Icon, IconButton } from '@chakra-ui/react';
|
import { Link as ReactRouterLink, useHistory } from 'react-router-dom';
|
||||||
|
import {
|
||||||
|
Flex,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Divider,
|
||||||
|
Heading,
|
||||||
|
Text,
|
||||||
|
Grid,
|
||||||
|
LinkBox,
|
||||||
|
LinkOverlay,
|
||||||
|
GridItem,
|
||||||
|
Stack,
|
||||||
|
Icon,
|
||||||
|
IconButton,
|
||||||
|
useToast,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Types
|
* Types
|
||||||
*/
|
*/
|
||||||
import { IPageProps } from '@educt/interfaces';
|
import { ILesson, IPageProps } from '@educt/interfaces';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hooks
|
* Hooks
|
||||||
@ -14,7 +30,10 @@ import { Redirect, useParams } from 'react-router-dom';
|
|||||||
import { PageContent, PageHeading, PageWrapper } from '@educt/components/PageElements';
|
import { PageContent, PageHeading, PageWrapper } from '@educt/components/PageElements';
|
||||||
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 { MdCheckCircle, MdOutlineFilePresent, MdSkipNext, MdSkipPrevious } from 'react-icons/md';
|
import { MdCheckCircle, 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';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lesson Page
|
* Lesson Page
|
||||||
@ -22,32 +41,87 @@ import { MdCheckCircle, MdOutlineFilePresent, MdSkipNext, MdSkipPrevious } from
|
|||||||
const LessonPage: React.FC<IPageProps> = () => {
|
const LessonPage: React.FC<IPageProps> = () => {
|
||||||
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 toast = useToast();
|
||||||
|
const history = useHistory();
|
||||||
|
const {
|
||||||
|
userStore: { me },
|
||||||
|
} = useRootStore();
|
||||||
|
const [nextLesson, setNextLesson] = useState<ILesson | null>(null);
|
||||||
|
const [prevLesson, setPrevLesson] = useState<ILesson | null>(null);
|
||||||
|
const [isPageAvailable, setIsPageAvailable] = useState<boolean>(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
/**
|
||||||
|
* Сheck if the user has access to the lesson
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define next and prev lessons
|
||||||
|
*/
|
||||||
|
if (lessons) {
|
||||||
|
const currentLessonIndex = lessons.findIndex(lesson => lesson.id === id);
|
||||||
|
|
||||||
|
setNextLesson(lessons[currentLessonIndex + 1] || null);
|
||||||
|
setPrevLesson(lessons[currentLessonIndex - 1] || null);
|
||||||
|
}
|
||||||
|
}, [lessons]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (lesson !== null) {
|
||||||
|
fetchCourseLessons(lesson.course_id);
|
||||||
|
}
|
||||||
|
}, [lesson]);
|
||||||
|
|
||||||
if (error?.response?.status === 404) {
|
if (error?.response?.status === 404) {
|
||||||
return <Redirect to='/404' />;
|
return <Redirect to='/404' />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!lesson || 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 handlePrevLesson = () => prevLesson && history.push(`/lesson/${prevLesson.id}`);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageWrapper>
|
<PageWrapper>
|
||||||
|
<Helmet>
|
||||||
|
<title>{lesson.title}</title>
|
||||||
|
</Helmet>
|
||||||
<PageHeading heading={lesson.title}>
|
<PageHeading heading={lesson.title}>
|
||||||
<Box mt='3'>
|
<Box mt='3'>
|
||||||
<Text fontSize={'sm'}>~{moment.duration(lesson.duration, 'minutes').humanize()}</Text>
|
<Text fontSize={'sm'}>~{moment.duration(lesson.duration, 'minutes').humanize()}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</PageHeading>
|
</PageHeading>
|
||||||
<PageContent>
|
<PageContent>
|
||||||
<Grid templateColumns={{ base: '1fr', xl: '3fr 1fr' }} gap={'7'}>
|
<Grid templateColumns={{ base: '1fr', xl: '3fr 1fr' }}>
|
||||||
<GridItem>
|
<GridItem>
|
||||||
<Box>
|
<Box>
|
||||||
<Box bg={lesson.color?.hex} minH={'500px'} />
|
<Box bg={lesson.color?.hex} minH={'500px'} />
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Flex justifyContent={'flex-end'} mt='3'>
|
<Flex justifyContent={'flex-end'} mt='3'>
|
||||||
<IconButton aria-label='prev-lesson' icon={<MdSkipPrevious />} size='sm' mr='2' />
|
{prevLesson && (
|
||||||
<Button aria-label='next-lesson' leftIcon={<MdSkipNext />} size='sm'>
|
<IconButton
|
||||||
|
onClick={handlePrevLesson}
|
||||||
|
aria-label='prev-lesson'
|
||||||
|
icon={<MdSkipPrevious />}
|
||||||
|
size='sm'
|
||||||
|
mr='2'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{nextLesson && (
|
||||||
|
<Button onClick={handleNextLesson} aria-label='next-lesson' leftIcon={<MdSkipNext />} size='sm'>
|
||||||
Next lesson
|
Next lesson
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Box mt='7'>
|
<Box mt='7'>
|
||||||
@ -61,27 +135,35 @@ const LessonPage: React.FC<IPageProps> = () => {
|
|||||||
</GridItem>
|
</GridItem>
|
||||||
<GridItem>
|
<GridItem>
|
||||||
<Box>
|
<Box>
|
||||||
<Stack maxH={'445px'} overflowY={'scroll'}>
|
<Stack maxH={'500px'} overflowY={'scroll'} spacing={'0'}>
|
||||||
{Array(50)
|
{lessons.map((lesson, i) => (
|
||||||
.fill(1)
|
<LinkBox
|
||||||
.map((n, i) => (
|
as='div'
|
||||||
<Flex key={i} alignItems={'center'} borderRadius={'xl'}>
|
display={'flex'}
|
||||||
<Icon as={MdCheckCircle} color='green.500' w={7} h={7} mr='2' />
|
key={lesson.id}
|
||||||
|
bg={lesson.id === id ? 'gray.50' : ''}
|
||||||
|
p='4'
|
||||||
|
alignItems={'center'}
|
||||||
|
_hover={{
|
||||||
|
bg: 'gray.50',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon as={MdOutlineCircle} color='gray.500' w={6} h={6} mr='2' />
|
||||||
<Box>
|
<Box>
|
||||||
<Text as='small' color='gray.500'>
|
<Text as='small' color='gray.500'>
|
||||||
Lesson {i + 1}
|
Lesson {i + 1}
|
||||||
</Text>
|
</Text>
|
||||||
|
<LinkOverlay as={ReactRouterLink} to={`/lesson/${lesson.id}`}>
|
||||||
<Text fontSize='sm' fontWeight={'medium'}>
|
<Text fontSize='sm' fontWeight={'medium'}>
|
||||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Vel, eaque.
|
{lesson.title}
|
||||||
</Text>
|
</Text>
|
||||||
|
</LinkOverlay>
|
||||||
</Box>
|
</Box>
|
||||||
</Flex>
|
</LinkBox>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Divider mt='5' mb='5' />
|
<Box p='5'>
|
||||||
|
|
||||||
<Box>
|
|
||||||
<Text fontSize={'2xl'} fontWeight={'medium'}>
|
<Text fontSize={'2xl'} fontWeight={'medium'}>
|
||||||
Materials
|
Materials
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { AxiosInstance } from 'axios';
|
import { AxiosInstance } from 'axios';
|
||||||
import { ApiServiceInstance } from '.';
|
import { ApiServiceInstance } from '.';
|
||||||
import * as helpers from '@educt/helpers';
|
import * as helpers from '@educt/helpers';
|
||||||
import { IApiRespose, ICourse, IUser } from '@educt/interfaces';
|
import { IApiRespose, ICourse, ILesson, IUser } from '@educt/interfaces';
|
||||||
import { CourseStatusEnum } from '@educt/enums';
|
import { CourseStatusEnum } from '@educt/enums';
|
||||||
import { CreateCourseParamsType, FetchCoursesParams, UpdateCourseParamsType } from '@educt/types';
|
import { CreateCourseParamsType, FetchCoursesParams, UpdateCourseParamsType } from '@educt/types';
|
||||||
|
|
||||||
@ -40,6 +40,17 @@ class CourseService {
|
|||||||
return result.data;
|
return result.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch lessons by course id
|
||||||
|
*
|
||||||
|
* @param id Course id
|
||||||
|
* @returns Array of lessons
|
||||||
|
*/
|
||||||
|
public async fetchLessonsById(id: string): Promise<IApiRespose<Array<ILesson>>> {
|
||||||
|
const result = await this.api.get(`/v1/courses/${id}/lessons`);
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update status of course by id
|
* Update status of course by id
|
||||||
*
|
*
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user