mirror of
https://github.com/sergeyyarkov/educt-client.git
synced 2026-10-11 18:29:15 +03:00
feat: rendering course page
This commit is contained in:
parent
63082e50c2
commit
9d67c14fee
@ -1,5 +1,5 @@
|
||||
import { UserRoleEnum } from '@educt/enums';
|
||||
import { IUserRole } from '@educt/interfaces';
|
||||
import { ICourse, IUserRole, UserCourseType } from '@educt/interfaces';
|
||||
|
||||
export function userHasRoles(userRoles: IUserRole[], roles: UserRoleEnum[]): boolean {
|
||||
for (let i = 0; i < roles.length; i += 1) {
|
||||
@ -97,3 +97,8 @@ export function arrayMove<T>(array: T[], from: number, to: number) {
|
||||
export function getDirtyFields<T>(dirtyFields: { [key: string]: boolean | undefined }, data: T) {
|
||||
return Object.fromEntries(Object.keys(dirtyFields).map(k => [k, data[k as keyof T]]));
|
||||
}
|
||||
|
||||
export function userHasCourse(findingCourse: ICourse, userCourses?: Array<UserCourseType> | undefined) {
|
||||
if (!userCourses) return true;
|
||||
return userCourses.some(course => course.id === findingCourse.id);
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
import { Link as ReactRouterLink } from 'react-router-dom';
|
||||
import { Flex, Heading, Text, Link, Icon } from '@chakra-ui/react';
|
||||
import { Heading, Text, Link, Icon } from '@chakra-ui/react';
|
||||
import { MdError } from 'react-icons/md';
|
||||
|
||||
/**
|
||||
|
||||
@ -2,7 +2,7 @@ import React from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Box, Heading } from '@chakra-ui/react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
|
||||
/**
|
||||
* Types
|
||||
|
||||
@ -1,26 +1,9 @@
|
||||
import React from 'react';
|
||||
import { Redirect, useHistory } from 'react-router-dom';
|
||||
import {
|
||||
Flex,
|
||||
Box,
|
||||
Text,
|
||||
Tag,
|
||||
Heading,
|
||||
Icon,
|
||||
Button,
|
||||
Grid,
|
||||
GridItem,
|
||||
Image,
|
||||
Divider,
|
||||
AvatarGroup,
|
||||
LinkBox,
|
||||
LinkOverlay,
|
||||
Avatar,
|
||||
useColorModeValue,
|
||||
} from '@chakra-ui/react';
|
||||
import { MdThumbUpOffAlt, MdPlayCircleOutline, MdOutlineGroup, MdOutlineVideoLibrary } from 'react-icons/md';
|
||||
import { Tabs, TabList, TabPanels, Tab, TabPanel, Stack } from '@chakra-ui/react';
|
||||
import { Link as ReactRouterLink } from 'react-router-dom';
|
||||
import * as helpres from '@educt/helpers';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
import { Flex, Box, Text, Heading, Button, Grid, GridItem, Divider, AvatarGroup, Avatar } from '@chakra-ui/react';
|
||||
import { MdPlayCircleOutline } from 'react-icons/md';
|
||||
import { Tabs, TabList, TabPanels, Tab, TabPanel } from '@chakra-ui/react';
|
||||
|
||||
/**
|
||||
* Types
|
||||
@ -30,78 +13,67 @@ import { IPageProps } from '@educt/interfaces';
|
||||
/**
|
||||
* Components
|
||||
*/
|
||||
import { CourseBackgroundImage, CourseHeader, CourseHeading, CourseInfo, LessonItem, LessonList } from '.';
|
||||
import { PageWrapper, PageContent } from '@educt/components/PageElements';
|
||||
import LoadingPage from '@educt/components/LoadingPage';
|
||||
|
||||
/**
|
||||
* Hooks
|
||||
*/
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useHistory, useParams } from 'react-router-dom';
|
||||
import { useFetchCourse } from '@educt/hooks/queries';
|
||||
import LoadingPage from '@educt/components/LoadingPage';
|
||||
import moment from 'moment';
|
||||
import { useRootStore } from '@educt/hooks/useRootStore';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
const CoursePage: React.FC<IPageProps> = () => {
|
||||
const {
|
||||
userStore: { me },
|
||||
} = useRootStore();
|
||||
const params = useParams<{ id: string }>();
|
||||
const { error, data: course, isLoading } = useFetchCourse(params.id);
|
||||
const history = useHistory();
|
||||
const color = useColorModeValue('gray.50', 'gray.700');
|
||||
|
||||
if (error?.response?.status === 404) {
|
||||
return <Redirect to='/404' />;
|
||||
}
|
||||
|
||||
if (!course || isLoading) return <LoadingPage />;
|
||||
if (!me || !course || isLoading) return <LoadingPage />;
|
||||
|
||||
const onWatchCourse = () => history.push(`/lesson/${course.lessons[0].id}`);
|
||||
const isCourseAvailable = helpres.userHasCourse(course, me.courses);
|
||||
const isEmptyLessons = course.lessons.length === 0;
|
||||
const isEmptyStudents = course.students.length === 0;
|
||||
|
||||
const handleWatchCourse = () => history.push(`/lesson/${course.lessons[0].id}`);
|
||||
|
||||
return (
|
||||
<PageWrapper maxW='1200px'>
|
||||
<Flex justifyContent='space-between' flexDir={{ base: 'column', xl: 'row' }}>
|
||||
<Box>
|
||||
<Heading as='h1' fontSize='3xl' mr='4'>
|
||||
{course.title}
|
||||
</Heading>
|
||||
<Flex mt='3' alignItems={'center'}>
|
||||
<Text fontSize='sm'>{course.teacher.fullname}</Text> •
|
||||
<Tag size='sm' variant='solid' bg={course.category.color?.hex}>
|
||||
{course.category.title}
|
||||
</Tag>
|
||||
</Flex>
|
||||
</Box>
|
||||
<Box mt={{ base: '2', xl: '0' }}>
|
||||
<Flex alignItems={'center'}>
|
||||
<Icon as={MdOutlineVideoLibrary} />
|
||||
<Text as='span' fontSize={'sm'} ml='1' mr='4'>
|
||||
{course.lessons_count}
|
||||
</Text>
|
||||
<Icon as={MdOutlineGroup} />
|
||||
<Text as='span' fontSize={'sm'} ml='1' mr='2'>
|
||||
{course.students_count}
|
||||
</Text>
|
||||
<Button leftIcon={<MdThumbUpOffAlt />} variant='ghost'>
|
||||
{course.likes_count}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Box>
|
||||
</Flex>
|
||||
<CourseHeader>
|
||||
<CourseHeading title={course.title} teacher={course.teacher} category={course.category} />
|
||||
<CourseInfo
|
||||
id={course.id}
|
||||
lessonsCount={course.lessons_count}
|
||||
studentsCount={course.students_count}
|
||||
likesCount={course.likes_count}
|
||||
/>
|
||||
</CourseHeader>
|
||||
|
||||
<PageContent mt={{ base: '5', sm: '2' }}>
|
||||
<Grid templateColumns={{ base: '1fr', xl: '3fr 1fr' }} gap={'7'}>
|
||||
<GridItem>
|
||||
<Box>
|
||||
<Flex justifyContent={'space-between'} alignItems={'center'} mb='2'>
|
||||
<Text fontSize={'sm'}>Last update: {new Date(course.updated_at).toLocaleDateString()}</Text>
|
||||
<Button onClick={onWatchCourse} leftIcon={<MdPlayCircleOutline />} size='sm' variant={'outline'}>
|
||||
<Button
|
||||
isDisabled={!isCourseAvailable}
|
||||
onClick={handleWatchCourse}
|
||||
leftIcon={<MdPlayCircleOutline />}
|
||||
size='sm'
|
||||
variant={'outline'}
|
||||
>
|
||||
Watch
|
||||
</Button>
|
||||
</Flex>
|
||||
<Image
|
||||
bg={course.color?.hex}
|
||||
borderRadius={'lg'}
|
||||
objectFit='cover'
|
||||
w='full'
|
||||
h={{ base: '300px', sm: '300px', md: '300px', lg: '400px' }}
|
||||
src={course.image?.url}
|
||||
/>
|
||||
<CourseBackgroundImage bg={course.color?.hex} src={course.image?.url} />
|
||||
</Box>
|
||||
<Box mt='4'>
|
||||
<Tabs>
|
||||
@ -117,7 +89,7 @@ const CoursePage: React.FC<IPageProps> = () => {
|
||||
</Heading>
|
||||
<Text mt='2'>{course.description}</Text>
|
||||
</Box>
|
||||
{course.students.length !== 0 ? (
|
||||
{!isEmptyStudents ? (
|
||||
<Flex justifyContent={'flex-end'} alignItems={'center'} mt='5'>
|
||||
<Text fontSize={'sm'} mr='2'>
|
||||
Students:
|
||||
@ -152,7 +124,9 @@ const CoursePage: React.FC<IPageProps> = () => {
|
||||
sunt corporis deserunt impedit natus consequuntur?
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Divider mt='5' />
|
||||
|
||||
<Box mt='4'>
|
||||
<Flex alignItems={'center'} justifyContent={'space-between'}>
|
||||
<Text fontSize={'2xl'} fontWeight={'medium'}>
|
||||
@ -162,46 +136,8 @@ const CoursePage: React.FC<IPageProps> = () => {
|
||||
{course.lessons_count} lessons
|
||||
</Text>
|
||||
</Flex>
|
||||
{course.lessons.length !== 0 ? (
|
||||
<Stack spacing={'1'} mt='2' overflowY={'auto'} maxH={'445px'} pr='5px'>
|
||||
{course.lessons.map(lesson => {
|
||||
return (
|
||||
<LinkBox
|
||||
as='div'
|
||||
display={'flex'}
|
||||
key={lesson.id}
|
||||
fontSize={'sm'}
|
||||
justifyContent={'space-between'}
|
||||
pl='5'
|
||||
pr='5'
|
||||
pb='5'
|
||||
pt='5'
|
||||
minH='85px'
|
||||
bg={color}
|
||||
borderRadius={'xl'}
|
||||
transition={'ease-in-out .1s'}
|
||||
_hover={{
|
||||
bg: 'blue.500',
|
||||
color: 'white',
|
||||
}}
|
||||
>
|
||||
<Flex alignItems={'center'}>
|
||||
<Icon as={MdPlayCircleOutline} w={5} h={5} mr='2' />
|
||||
<LinkOverlay
|
||||
as={ReactRouterLink}
|
||||
fontWeight='medium'
|
||||
to={`/lesson/${lesson.id}`}
|
||||
overflowY={'hidden'}
|
||||
maxH='60px'
|
||||
>
|
||||
{lesson.title}
|
||||
</LinkOverlay>
|
||||
</Flex>
|
||||
<Text as='span'>{moment.duration(lesson.duration).asMinutes()}min</Text>
|
||||
</LinkBox>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
{!isEmptyLessons ? (
|
||||
<LessonList course={course} render={LessonItem} />
|
||||
) : (
|
||||
<Box textAlign={'center'} p='15px 0'>
|
||||
<Text fontSize={'sm'} userSelect={'none'} color='gray.500'>
|
||||
@ -218,4 +154,4 @@ const CoursePage: React.FC<IPageProps> = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default CoursePage;
|
||||
export default observer(CoursePage);
|
||||
|
||||
195
src/pages/course/index.tsx
Normal file
195
src/pages/course/index.tsx
Normal file
@ -0,0 +1,195 @@
|
||||
import React from 'react';
|
||||
import moment from 'moment';
|
||||
import * as helpers from '@educt/helpers';
|
||||
import { Link as ReactRouterLink } from 'react-router-dom';
|
||||
import {
|
||||
Box,
|
||||
BoxProps,
|
||||
Flex,
|
||||
FlexProps,
|
||||
Button,
|
||||
Icon,
|
||||
Image,
|
||||
Text,
|
||||
Heading,
|
||||
Tag,
|
||||
Stack,
|
||||
ImageProps,
|
||||
LinkBox,
|
||||
LinkOverlay,
|
||||
LinkBoxProps,
|
||||
StackProps,
|
||||
} from '@chakra-ui/react';
|
||||
import {
|
||||
MdThumbUpOffAlt,
|
||||
MdPlayCircleOutline,
|
||||
MdOutlineGroup,
|
||||
MdOutlineVideoLibrary,
|
||||
MdOutlineLock,
|
||||
} from 'react-icons/md';
|
||||
|
||||
/**
|
||||
* Types
|
||||
*/
|
||||
import type { ICourse, ILesson } from '@educt/interfaces';
|
||||
|
||||
/**
|
||||
* Hooks
|
||||
*/
|
||||
import { useColorModeValue } from '@chakra-ui/react';
|
||||
import { useRootStore } from '@educt/hooks/useRootStore';
|
||||
|
||||
export const CourseHeader: React.FC<FlexProps> = props => (
|
||||
<Flex justifyContent='space-between' flexDir={{ base: 'column', xl: 'row' }} {...props}>
|
||||
{props.children}
|
||||
</Flex>
|
||||
);
|
||||
|
||||
interface ICourseHeadingProps extends BoxProps {
|
||||
title: string;
|
||||
teacher: ICourse['teacher'];
|
||||
category: ICourse['category'];
|
||||
}
|
||||
export const CourseHeading: React.FC<ICourseHeadingProps> = props => {
|
||||
const { title, teacher, category, ...boxProps } = props;
|
||||
|
||||
return (
|
||||
<Box {...boxProps}>
|
||||
<Heading as='h1' fontSize='3xl' mr='4'>
|
||||
{title}
|
||||
</Heading>
|
||||
<Flex mt='3' alignItems={'center'}>
|
||||
<Text fontSize='sm'>{teacher.fullname}</Text> •
|
||||
<Tag size='sm' variant='solid' bg={category.color?.hex}>
|
||||
{category.title}
|
||||
</Tag>
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
interface ICourseInfoProps extends BoxProps {
|
||||
id: string;
|
||||
lessonsCount: string;
|
||||
studentsCount: string;
|
||||
likesCount: string;
|
||||
}
|
||||
export const CourseInfo: React.FC<ICourseInfoProps> = props => {
|
||||
const { id, lessonsCount, studentsCount, likesCount, ...boxProps } = props;
|
||||
|
||||
const handleLikeCourse = (id: string) => () => {
|
||||
// TODO set like request
|
||||
console.log(`like course: ${id}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box mt={{ base: '2', xl: '0' }} {...boxProps}>
|
||||
<Flex alignItems={'center'}>
|
||||
<Icon as={MdOutlineVideoLibrary} />
|
||||
<Text as='span' fontSize={'sm'} ml='1' mr='4'>
|
||||
{lessonsCount}
|
||||
</Text>
|
||||
|
||||
<Icon as={MdOutlineGroup} />
|
||||
<Text as='span' fontSize={'sm'} ml='1' mr='2'>
|
||||
{studentsCount}
|
||||
</Text>
|
||||
|
||||
<Button onClick={handleLikeCourse(id)} leftIcon={<MdThumbUpOffAlt />} variant='ghost'>
|
||||
{likesCount}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export const CourseBackgroundImage: React.FC<ImageProps> = props => {
|
||||
return (
|
||||
<Image
|
||||
borderRadius={'lg'}
|
||||
objectFit='cover'
|
||||
w='full'
|
||||
h={{ base: '300px', sm: '300px', md: '300px', lg: '400px' }}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface ILessonListProps extends StackProps {
|
||||
course: ICourse;
|
||||
render: React.FC<ILessonItemProps>;
|
||||
}
|
||||
export const LessonList: React.FC<ILessonListProps> = props => {
|
||||
const { course, render: Item, ...stackProps } = props;
|
||||
const {
|
||||
userStore: { me },
|
||||
} = useRootStore();
|
||||
const bg = useColorModeValue('gray.50', 'gray.700');
|
||||
|
||||
if (!me) return null;
|
||||
|
||||
const isCourseAvailable = helpers.userHasCourse(course, me.courses);
|
||||
|
||||
return (
|
||||
<Stack spacing={'1'} mt='2' overflowY={'auto'} maxH={'445px'} pr='5px' {...stackProps}>
|
||||
{course.lessons.map(lesson => (
|
||||
<Item
|
||||
key={lesson.id}
|
||||
id={lesson.id}
|
||||
title={lesson.title}
|
||||
duration={lesson.duration}
|
||||
isDisabled={!isCourseAvailable}
|
||||
bg={bg}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
interface ILessonItemProps extends LinkBoxProps {
|
||||
id: string;
|
||||
title: string;
|
||||
duration: string;
|
||||
isDisabled?: boolean | undefined;
|
||||
}
|
||||
export const LessonItem: React.FC<ILessonItemProps> = props => {
|
||||
const { id, title, duration, isDisabled, ...linkBoxProps } = props;
|
||||
return (
|
||||
<LinkBox
|
||||
as='div'
|
||||
display={'flex'}
|
||||
fontSize={'sm'}
|
||||
justifyContent={'space-between'}
|
||||
pl='5'
|
||||
pr='5'
|
||||
pb='5'
|
||||
pt='5'
|
||||
minH='85px'
|
||||
borderRadius={'xl'}
|
||||
transition={'ease-in-out .1s'}
|
||||
_hover={
|
||||
!isDisabled
|
||||
? {
|
||||
bg: 'blue.500',
|
||||
color: 'white',
|
||||
}
|
||||
: {}
|
||||
}
|
||||
{...linkBoxProps}
|
||||
>
|
||||
<Flex alignItems={'center'} fontWeight='medium' userSelect={'none'}>
|
||||
<Icon as={isDisabled ? MdOutlineLock : MdPlayCircleOutline} w={5} h={5} mr='2' />
|
||||
{!isDisabled ? (
|
||||
<LinkOverlay as={ReactRouterLink} to={`/lesson/${id}`} overflowY={'hidden'} maxH='60px'>
|
||||
{title}
|
||||
</LinkOverlay>
|
||||
) : (
|
||||
<Text overflowY={'hidden'} maxH='60px'>
|
||||
{title}
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
<Text as='span'>{moment.duration(duration).asMinutes()}min</Text>
|
||||
</LinkBox>
|
||||
);
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user