mirror of
https://github.com/sergeyyarkov/educt-client.git
synced 2026-10-11 18:29:15 +03:00
feat: added layout
This commit is contained in:
parent
f8ae2107eb
commit
63082e50c2
@ -10,15 +10,13 @@ import { CourseServiceInstance } from '@educt/services';
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import useAsync from '../../useAsync';
|
import useAsync from '../../useAsync';
|
||||||
|
|
||||||
type CourseResultDataType = Omit<ICourse, 'students_count' | 'likes_count' | 'lessons_count'>;
|
|
||||||
|
|
||||||
const useFetchCourse = (id: string) => {
|
const useFetchCourse = (id: string) => {
|
||||||
const fetch = async (id: string) => {
|
const fetch = async (id: string) => {
|
||||||
const { data } = await CourseServiceInstance.fetchById(id);
|
const { data } = await CourseServiceInstance.fetchById(id);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
const { execute: fetchCourseById, ...state } = useAsync<CourseResultDataType, Parameters<typeof fetch>>(fetch);
|
const { execute: fetchCourseById, ...state } = useAsync<ICourse, Parameters<typeof fetch>>(fetch);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchCourseById(id);
|
fetchCourseById(id);
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { ChakraProvider, ColorModeScript, theme } from '@chakra-ui/react';
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
|
import { ChakraProvider, ColorModeScript } from '@chakra-ui/react';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
|
import { theme } from './theme';
|
||||||
import { RootStoreProvider } from './providers';
|
import { RootStoreProvider } from './providers';
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
|
|||||||
@ -21,7 +21,7 @@ const NotFoundPage: React.FC<IPageProps> = ({ title }) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
toast({ title: `Page not found`, duration: 10000, isClosable: true, status: 'warning' });
|
toast({ title: `Page not found`, duration: 10000, isClosable: true, status: 'warning' });
|
||||||
});
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@ -41,11 +41,8 @@ const AuthPage: React.FC<IPageProps> = ({ title }) => {
|
|||||||
<Helmet>
|
<Helmet>
|
||||||
<title>{title}</title>
|
<title>{title}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<Box maxW='md' mx='auto'>
|
<Box maxW='md' mx='auto' mt='20'>
|
||||||
<Logo mx='auto' h='14' mb={{ base: '10', md: '20' }} />
|
<Logo mx='auto' h='14' mb={{ base: '10', md: '10' }} />
|
||||||
<Heading mb='8' textAlign='center' size='xl' fontWeight='extrabold'>
|
|
||||||
Login to your account
|
|
||||||
</Heading>
|
|
||||||
<Card>
|
<Card>
|
||||||
<AuthForm />
|
<AuthForm />
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@ -84,7 +84,7 @@ const AuthForm: React.FC = () => {
|
|||||||
<Button
|
<Button
|
||||||
type='submit'
|
type='submit'
|
||||||
colorScheme='blue'
|
colorScheme='blue'
|
||||||
size='lg'
|
size='md'
|
||||||
fontSize='md'
|
fontSize='md'
|
||||||
loadingText='Logging in...'
|
loadingText='Logging in...'
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
|
|||||||
221
src/pages/course/course.tsx
Normal file
221
src/pages/course/course.tsx
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Types
|
||||||
|
*/
|
||||||
|
import { IPageProps } from '@educt/interfaces';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Components
|
||||||
|
*/
|
||||||
|
import { PageWrapper, PageContent } from '@educt/components/PageElements';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hooks
|
||||||
|
*/
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
import { useFetchCourse } from '@educt/hooks/queries';
|
||||||
|
import LoadingPage from '@educt/components/LoadingPage';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
const CoursePage: React.FC<IPageProps> = () => {
|
||||||
|
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 />;
|
||||||
|
|
||||||
|
const onWatchCourse = () => 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>
|
||||||
|
<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'}>
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box mt='4'>
|
||||||
|
<Tabs>
|
||||||
|
<TabList>
|
||||||
|
<Tab>About</Tab>
|
||||||
|
</TabList>
|
||||||
|
|
||||||
|
<TabPanels>
|
||||||
|
<TabPanel>
|
||||||
|
<Box mt='6'>
|
||||||
|
<Heading as='h2' fontSize='2xl' fontWeight={'medium'}>
|
||||||
|
About this course
|
||||||
|
</Heading>
|
||||||
|
<Text mt='2'>{course.description}</Text>
|
||||||
|
</Box>
|
||||||
|
{course.students.length !== 0 ? (
|
||||||
|
<Flex justifyContent={'flex-end'} alignItems={'center'} mt='5'>
|
||||||
|
<Text fontSize={'sm'} mr='2'>
|
||||||
|
Students:
|
||||||
|
</Text>
|
||||||
|
<AvatarGroup size='sm' max={2}>
|
||||||
|
{course.students.map(student => (
|
||||||
|
<Avatar key={student.id} name={student.fullname} />
|
||||||
|
))}
|
||||||
|
</AvatarGroup>
|
||||||
|
</Flex>
|
||||||
|
) : (
|
||||||
|
<Box textAlign={'right'}>
|
||||||
|
<Text fontSize={'sm'} userSelect={'none'} color='gray.500'>
|
||||||
|
0 students
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</TabPanel>
|
||||||
|
</TabPanels>
|
||||||
|
</Tabs>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Divider mt='5' />
|
||||||
|
</GridItem>
|
||||||
|
<GridItem borderRadius={'lg'} minWidth={'290px'} h='550px'>
|
||||||
|
<Box>
|
||||||
|
<Text fontSize={'2xl'} fontWeight={'medium'}>
|
||||||
|
What you'll learn.
|
||||||
|
</Text>
|
||||||
|
<Text fontSize={'sm'} mt='2'>
|
||||||
|
Lorem, ipsum dolor sit amet consectetur adipisicing elit. Iste temporibus harum eos praesentium hic,
|
||||||
|
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'}>
|
||||||
|
Content
|
||||||
|
</Text>
|
||||||
|
<Text as='span' color='gray.500' fontSize={'sm'}>
|
||||||
|
{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>
|
||||||
|
) : (
|
||||||
|
<Box textAlign={'center'} p='15px 0'>
|
||||||
|
<Text fontSize={'sm'} userSelect={'none'} color='gray.500'>
|
||||||
|
There are no lessons here yet
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
<Divider mt='5' />
|
||||||
|
</Box>
|
||||||
|
</GridItem>
|
||||||
|
</Grid>
|
||||||
|
</PageContent>
|
||||||
|
</PageWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CoursePage;
|
||||||
@ -8,6 +8,7 @@ import AuthPage from '@educt/pages/auth/auth';
|
|||||||
import NotFoundPage from '@educt/pages/404/404';
|
import NotFoundPage from '@educt/pages/404/404';
|
||||||
import CreateCoursePage from '@educt/pages/courses/create/create';
|
import CreateCoursePage from '@educt/pages/courses/create/create';
|
||||||
import CreateLessonPage from '@educt/pages/courses/create-lesson/create-lesson';
|
import CreateLessonPage from '@educt/pages/courses/create-lesson/create-lesson';
|
||||||
|
import CoursePage from '@educt/pages/course/course';
|
||||||
import EditCoursePage from '@educt/pages/courses/edit/edit';
|
import EditCoursePage from '@educt/pages/courses/edit/edit';
|
||||||
import ChangePasswordPage from '@educt/pages/profile/change-password/change-password';
|
import ChangePasswordPage from '@educt/pages/profile/change-password/change-password';
|
||||||
import ChangeEmailPage from '@educt/pages/profile/change-email/change-email';
|
import ChangeEmailPage from '@educt/pages/profile/change-email/change-email';
|
||||||
@ -32,6 +33,13 @@ export const routes: IRouteOption[] = [
|
|||||||
exact: true,
|
exact: true,
|
||||||
isPrivate: true,
|
isPrivate: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/course/:id',
|
||||||
|
title: 'Course',
|
||||||
|
component: CoursePage,
|
||||||
|
isPrivate: true,
|
||||||
|
exact: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/courses',
|
path: '/courses',
|
||||||
title: 'Courses',
|
title: 'Courses',
|
||||||
|
|||||||
@ -35,9 +35,7 @@ class CourseService {
|
|||||||
* @param id Course id
|
* @param id Course id
|
||||||
* @returns Course
|
* @returns Course
|
||||||
*/
|
*/
|
||||||
public async fetchById(
|
public async fetchById(id: string): Promise<IApiRespose<ICourse>> {
|
||||||
id: string
|
|
||||||
): Promise<IApiRespose<Omit<ICourse, 'students_count' | 'likes_count' | 'lessons_count'>>> {
|
|
||||||
const result = await this.api.get(`/v1/courses/${id}`);
|
const result = await this.api.get(`/v1/courses/${id}`);
|
||||||
return result.data;
|
return result.data;
|
||||||
}
|
}
|
||||||
|
|||||||
24
src/theme.ts
Normal file
24
src/theme.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { extendTheme } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
export const theme = extendTheme({
|
||||||
|
styles: {
|
||||||
|
global: {
|
||||||
|
body: {
|
||||||
|
scrollbarWidth: 'thin',
|
||||||
|
scrollbarColor: '#000',
|
||||||
|
},
|
||||||
|
'::-webkit-scrollbar': {
|
||||||
|
width: '5px',
|
||||||
|
height: '5px',
|
||||||
|
},
|
||||||
|
'::-webkit-scrollbar-track': {
|
||||||
|
backgroundColor: 'gray.100',
|
||||||
|
borderRadius: '100px',
|
||||||
|
},
|
||||||
|
'::-webkit-scrollbar-thumb': {
|
||||||
|
background: 'blue.600',
|
||||||
|
borderRadius: '100px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user