mirror of
https://github.com/sergeyyarkov/educt-client.git
synced 2026-10-11 18:29:15 +03:00
Merge branch 'feature/lesson-page' into develop
This commit is contained in:
commit
e62757ab18
@ -1 +1 @@
|
|||||||
export const BACKEND_URL: string = 'http://localhost:3333';
|
export const BACKEND_URL = 'http://localhost:3333';
|
||||||
|
|||||||
22
package-lock.json
generated
22
package-lock.json
generated
@ -1741,6 +1741,11 @@
|
|||||||
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
|
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"deepmerge": {
|
||||||
|
"version": "4.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
|
||||||
|
"integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg=="
|
||||||
|
},
|
||||||
"define-properties": {
|
"define-properties": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
|
||||||
@ -2849,6 +2854,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz",
|
||||||
"integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA="
|
"integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA="
|
||||||
},
|
},
|
||||||
|
"load-script": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/load-script/-/load-script-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-BJGTngvuVkPuSUp+PaPSuscMbKQ="
|
||||||
|
},
|
||||||
"lodash": {
|
"lodash": {
|
||||||
"version": "4.17.21",
|
"version": "4.17.21",
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
@ -3324,6 +3334,18 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||||
},
|
},
|
||||||
|
"react-player": {
|
||||||
|
"version": "2.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-player/-/react-player-2.9.0.tgz",
|
||||||
|
"integrity": "sha512-jNUkTfMmUhwPPAktAdIqiBcVUKsFKrVGH6Ocutj6535CNfM91yrvWxHg6fvIX8Y/fjYUPoejddwh7qboNV9vGA==",
|
||||||
|
"requires": {
|
||||||
|
"deepmerge": "^4.0.0",
|
||||||
|
"load-script": "^1.0.0",
|
||||||
|
"memoize-one": "^5.1.1",
|
||||||
|
"prop-types": "^15.7.2",
|
||||||
|
"react-fast-compare": "^3.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"react-redux": {
|
"react-redux": {
|
||||||
"version": "7.2.6",
|
"version": "7.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.6.tgz",
|
||||||
|
|||||||
@ -28,6 +28,7 @@
|
|||||||
"react-helmet": "^6.1.0",
|
"react-helmet": "^6.1.0",
|
||||||
"react-hook-form": "^7.12.2",
|
"react-hook-form": "^7.12.2",
|
||||||
"react-icons": "^4.3.1",
|
"react-icons": "^4.3.1",
|
||||||
|
"react-player": "^2.9.0",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-spinners": "^0.11.0",
|
"react-spinners": "^0.11.0",
|
||||||
"react-virtuoso": "^2.2.8",
|
"react-virtuoso": "^2.2.8",
|
||||||
|
|||||||
25
src/components/Forms/LessonForm/CreateLessonForm.tsx
Normal file
25
src/components/Forms/LessonForm/CreateLessonForm.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
|
import React from 'react';
|
||||||
|
import { SubmitHandler, useForm } from 'react-hook-form';
|
||||||
|
import { LessonForm } from '.';
|
||||||
|
import { InputFields } from './LessonForm';
|
||||||
|
import LessonFormSchema from './LessonForm.validator';
|
||||||
|
|
||||||
|
const CreateLessonForm: React.FC = () => {
|
||||||
|
const form = useForm<InputFields>({ resolver: yupResolver(LessonFormSchema) });
|
||||||
|
|
||||||
|
const onSubmit: SubmitHandler<InputFields> = async data => {
|
||||||
|
console.log('creating...');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LessonForm
|
||||||
|
isLoading={false}
|
||||||
|
reactHookForm={{ ...form }}
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
buttonLabel='Create'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CreateLessonForm;
|
||||||
82
src/components/Forms/LessonForm/LessonForm.tsx
Normal file
82
src/components/Forms/LessonForm/LessonForm.tsx
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
FormHelperText,
|
||||||
|
FormLabel,
|
||||||
|
Text,
|
||||||
|
Input,
|
||||||
|
InputGroup,
|
||||||
|
Stack,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Textarea,
|
||||||
|
Flex,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { UseFormReturn } from 'react-hook-form';
|
||||||
|
|
||||||
|
export type InputFields = {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LessonFormPropsType = {
|
||||||
|
onSubmit: () => Promise<void>;
|
||||||
|
buttonLabel: string;
|
||||||
|
isLoading: boolean;
|
||||||
|
reactHookForm: UseFormReturn<InputFields>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const LessonForm: React.FC<LessonFormPropsType> = ({ onSubmit, buttonLabel, isLoading, reactHookForm }) => {
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
register,
|
||||||
|
watch,
|
||||||
|
formState: { errors, isDirty },
|
||||||
|
} = reactHookForm;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={onSubmit}>
|
||||||
|
<Stack spacing={'5'}>
|
||||||
|
<FormControl isRequired id='title' isInvalid={!!errors.title}>
|
||||||
|
<FormLabel>Title</FormLabel>
|
||||||
|
<InputGroup>
|
||||||
|
<Input placeholder='Lesson name' type='text' {...register('title')} />
|
||||||
|
</InputGroup>
|
||||||
|
<FormHelperText>e.g Variables in Javascript</FormHelperText>
|
||||||
|
<Text as='small' color='red.500'>
|
||||||
|
{errors.title?.message}
|
||||||
|
</Text>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl isRequired id='description' isInvalid={!!errors.description}>
|
||||||
|
<FormLabel>Description</FormLabel>
|
||||||
|
<Textarea resize='none' minH='150px' placeholder='Course description...' {...register('description')} />
|
||||||
|
<Flex justifyContent='space-between'>
|
||||||
|
<Text as='small' color='red.500'>
|
||||||
|
{errors.description?.message}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl isRequired id='video'>
|
||||||
|
<FormLabel>Video</FormLabel>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<Box mt='5'>
|
||||||
|
<Button
|
||||||
|
isLoading={isLoading}
|
||||||
|
isDisabled={!isDirty}
|
||||||
|
loadingText='Saving...'
|
||||||
|
type='submit'
|
||||||
|
colorScheme='blue'
|
||||||
|
variant='outline'
|
||||||
|
>
|
||||||
|
{buttonLabel}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LessonForm;
|
||||||
11
src/components/Forms/LessonForm/LessonForm.validator.ts
Normal file
11
src/components/Forms/LessonForm/LessonForm.validator.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import yup from '@educt/schema';
|
||||||
|
|
||||||
|
const LessonFormSchema = yup.object().shape({
|
||||||
|
title: yup.string().required('Title field name is required').max(90, 'Title must be at most 90 characters'),
|
||||||
|
description: yup
|
||||||
|
.string()
|
||||||
|
.required('Description field name is required')
|
||||||
|
.max(250, 'Description must be at most 250 characters'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default LessonFormSchema;
|
||||||
3
src/components/Forms/LessonForm/index.ts
Normal file
3
src/components/Forms/LessonForm/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import LessonForm from './LessonForm';
|
||||||
|
|
||||||
|
export { LessonForm };
|
||||||
@ -1,7 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { BoxProps, Box, Heading, Text, Divider } from '@chakra-ui/react';
|
import { BoxProps, Box, Flex, Heading, Text, Divider } from '@chakra-ui/react';
|
||||||
|
|
||||||
interface IPageHeadingProps extends BoxProps {
|
interface IPageHeadingProps extends BoxProps {
|
||||||
|
headingPrefix?: JSX.Element;
|
||||||
heading: string;
|
heading: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
}
|
}
|
||||||
@ -11,10 +12,13 @@ export const PageWrapper: React.FC<BoxProps> = props => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const PageHeading: React.FC<IPageHeadingProps> = props => {
|
export const PageHeading: React.FC<IPageHeadingProps> = props => {
|
||||||
const { heading, description, ...boxProps } = props;
|
const { headingPrefix, heading, description, ...boxProps } = props;
|
||||||
return (
|
return (
|
||||||
<Box {...boxProps}>
|
<Box {...boxProps}>
|
||||||
|
<Flex alignItems={'center'}>
|
||||||
|
{headingPrefix}
|
||||||
<Heading as='h1'>{heading}</Heading>
|
<Heading as='h1'>{heading}</Heading>
|
||||||
|
</Flex>
|
||||||
{description && <Text mt='2'>{description}</Text>}
|
{description && <Text mt='2'>{description}</Text>}
|
||||||
{props.children}
|
{props.children}
|
||||||
<Divider mt='5' />
|
<Divider mt='5' />
|
||||||
|
|||||||
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 };
|
||||||
@ -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) => {
|
||||||
|
try {
|
||||||
const { data } = await LessonServiceInstance.fetchLessonById(id);
|
const { data } = await LessonServiceInstance.fetchLessonById(id);
|
||||||
return data;
|
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);
|
||||||
|
|||||||
22
src/hooks/queries/lesson/useFetchLessonProgress.ts
Normal file
22
src/hooks/queries/lesson/useFetchLessonProgress.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { LessonServiceInstance } from '@educt/services';
|
||||||
|
import type { LessonProgress } from '@educt/types';
|
||||||
|
import useAsync from '../../useAsync';
|
||||||
|
|
||||||
|
const useFetchLessonProgress = () => {
|
||||||
|
const fetch = async (id: string) => {
|
||||||
|
try {
|
||||||
|
const result = await LessonServiceInstance.fetchProgress(id);
|
||||||
|
return result.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const { execute, ...state } = useAsync<LessonProgress, Parameters<typeof fetch>>(fetch);
|
||||||
|
|
||||||
|
return { fetchLessonProgress: execute, ...state };
|
||||||
|
};
|
||||||
|
|
||||||
|
export { useFetchLessonProgress };
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import type { RouteComponentProps } from 'react-router-dom';
|
import type { RouteComponentProps } from 'react-router-dom';
|
||||||
import { AttachmentFileType, ColorType, LinkType } from '@educt/types';
|
import { AttachmentFileType, ColorType, LessonProgress, LessonVideoType, LinkType } from '@educt/types';
|
||||||
import { CourseStatusEnum, UserRoleEnum } from '../enums';
|
import { CourseStatusEnum, UserRoleEnum } from '../enums';
|
||||||
|
|
||||||
export interface IAppConfig {
|
export interface IAppConfig {
|
||||||
@ -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' | 'progress'>[];
|
||||||
students: IUser[];
|
students: IUser[];
|
||||||
color: ColorType | null;
|
color: ColorType | null;
|
||||||
students_count: string;
|
students_count: string;
|
||||||
@ -45,10 +45,31 @@ 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>;
|
||||||
|
progress: LessonProgress;
|
||||||
|
content: ILessonContent;
|
||||||
|
video?: LessonVideoType | undefined | null;
|
||||||
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;
|
||||||
|
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;
|
||||||
|
|||||||
@ -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,16 +1,20 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Box, Heading } from '@chakra-ui/react';
|
|
||||||
|
|
||||||
import { IPageProps } from '@educt/interfaces';
|
import { IPageProps } from '@educt/interfaces';
|
||||||
|
import { PageContent, PageHeading, PageWrapper } from '@educt/components/PageElements';
|
||||||
|
import CreateLessonForm from '@educt/components/Forms/LessonForm/CreateLessonForm';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create Lesson page
|
* Create Lesson page
|
||||||
*/
|
*/
|
||||||
const CreateLessonPage: React.FC<IPageProps> = () => {
|
const CreateLessonPage: React.FC<IPageProps> = () => {
|
||||||
return (
|
return (
|
||||||
<Box>
|
<PageWrapper>
|
||||||
<Heading as='h1'>Create Lesson page</Heading>
|
<PageHeading heading='Lesson Creation' description='Create new lesson ' />
|
||||||
</Box>
|
<PageContent>
|
||||||
|
<CreateLessonForm />
|
||||||
|
</PageContent>
|
||||||
|
</PageWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -11,24 +11,23 @@ import { IPageProps } from '@educt/interfaces';
|
|||||||
*/
|
*/
|
||||||
import PrevPageButton from '@educt/components/PrevPageButton';
|
import PrevPageButton from '@educt/components/PrevPageButton';
|
||||||
import { CreateCourseForm } from '@educt/components/Forms/CourseForm';
|
import { CreateCourseForm } from '@educt/components/Forms/CourseForm';
|
||||||
|
import { PageContent, PageHeading, PageWrapper } from '@educt/components/PageElements';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create course page
|
* Create course page
|
||||||
*/
|
*/
|
||||||
const CreateCoursePage: React.FC<IPageProps> = () => {
|
const CreateCoursePage: React.FC<IPageProps> = () => {
|
||||||
return (
|
return (
|
||||||
<Box>
|
<PageWrapper>
|
||||||
<Box>
|
<PageHeading
|
||||||
<Flex alignItems='center'>
|
headingPrefix={<PrevPageButton prevPage='/courses' />}
|
||||||
<PrevPageButton prevPage='/courses' />
|
heading='Course Creation'
|
||||||
<Heading as='h1'>Course creation</Heading>
|
description='Fill in the required fields to create a new course'
|
||||||
</Flex>
|
/>
|
||||||
<Text mt='3'>Fill in the required fields to create a course</Text>
|
<PageContent>
|
||||||
</Box>
|
|
||||||
<Box mt='5'>
|
|
||||||
<CreateCourseForm />
|
<CreateCourseForm />
|
||||||
</Box>
|
</PageContent>
|
||||||
</Box>
|
</PageWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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(`/courses/edit/${course.id}/create-lesson`);
|
||||||
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;
|
||||||
}) => {
|
}) => {
|
||||||
|
|||||||
@ -14,6 +14,10 @@ import type { IPageProps } from '@educt/interfaces';
|
|||||||
import LoadingPage from '@educt/components/LoadingPage';
|
import LoadingPage from '@educt/components/LoadingPage';
|
||||||
import { EditCourseForm } from '@educt/components/Forms/CourseForm';
|
import { EditCourseForm } from '@educt/components/Forms/CourseForm';
|
||||||
import DeleteCourseDialog from '@educt/components/Dialogs/DeleteCourseDialog';
|
import DeleteCourseDialog from '@educt/components/Dialogs/DeleteCourseDialog';
|
||||||
|
import PrevPageButton from '@educt/components/PrevPageButton';
|
||||||
|
import { DeleteButton } from '@educt/components/Buttons';
|
||||||
|
import { StatusButton } from './components';
|
||||||
|
import { PageContent, PageHeading, PageWrapper } from '@educt/components/PageElements';
|
||||||
import { StudentTableList, StudentTableRow } from './components/StudentTableList';
|
import { StudentTableList, StudentTableRow } from './components/StudentTableList';
|
||||||
import LessonList from './components/LessonList';
|
import LessonList from './components/LessonList';
|
||||||
|
|
||||||
@ -23,9 +27,6 @@ import LessonList from './components/LessonList';
|
|||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { useFetchCourse } from '@educt/hooks/queries';
|
import { useFetchCourse } from '@educt/hooks/queries';
|
||||||
import { useDisclosure } from '@chakra-ui/hooks';
|
import { useDisclosure } from '@chakra-ui/hooks';
|
||||||
import { PageContent, PageHeading, PageWrapper } from '@educt/components/PageElements';
|
|
||||||
import { DeleteButton } from '@educt/components/Buttons';
|
|
||||||
import { StatusButton } from './components';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Course editor
|
* Course editor
|
||||||
@ -54,7 +55,11 @@ const EditCoursePage: React.FC<IPageProps> = () => {
|
|||||||
</Helmet>
|
</Helmet>
|
||||||
|
|
||||||
<DeleteCourseDialog course={course} onClose={onCloseDeleteDialog} isOpen={isOpenDeleteDialog} />
|
<DeleteCourseDialog course={course} onClose={onCloseDeleteDialog} isOpen={isOpenDeleteDialog} />
|
||||||
<PageHeading heading='Course editor' description={course.title}>
|
<PageHeading
|
||||||
|
headingPrefix={<PrevPageButton prevPage='/courses' />}
|
||||||
|
heading='Course editor'
|
||||||
|
description={course.title}
|
||||||
|
>
|
||||||
<Flex mt='5' justifyContent='flex-end'>
|
<Flex mt='5' justifyContent='flex-end'>
|
||||||
<StatusButton courseId={course.id} currentStatus={course.status} />
|
<StatusButton courseId={course.id} currentStatus={course.status} />
|
||||||
<DeleteButton onClick={onOpenDeleteDialog} ml='2' />
|
<DeleteButton onClick={onOpenDeleteDialog} ml='2' />
|
||||||
|
|||||||
@ -1,53 +1,186 @@
|
|||||||
import React from 'react';
|
import React 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 * as helpres from '@educt/helpers';
|
||||||
|
import { Link as ReactRouterLink, Redirect } from 'react-router-dom';
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
|
import axios from 'axios';
|
||||||
|
import {
|
||||||
|
Flex,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Divider,
|
||||||
|
Heading,
|
||||||
|
Text,
|
||||||
|
Grid,
|
||||||
|
LinkBox,
|
||||||
|
LinkOverlay,
|
||||||
|
GridItem,
|
||||||
|
Stack,
|
||||||
|
Icon,
|
||||||
|
IconButton,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import ReactPlayer from 'react-player';
|
||||||
|
import { MdOutlineCircle, MdCheckCircle, MdOutlineFilePresent, MdSkipNext, MdSkipPrevious } from 'react-icons/md';
|
||||||
|
import LoadingPage from '@educt/components/LoadingPage';
|
||||||
|
import { PageContent, PageHeading, PageWrapper } from '@educt/components/PageElements';
|
||||||
|
import { EditIcon } from '@chakra-ui/icons';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Types
|
* Types
|
||||||
*/
|
*/
|
||||||
import { IPageProps } from '@educt/interfaces';
|
import { ILesson, ILessonMaterial, IPageProps } from '@educt/interfaces';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hooks
|
* Hooks
|
||||||
*/
|
*/
|
||||||
import { Redirect, useParams } from 'react-router-dom';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { PageContent, PageHeading, PageWrapper } from '@educt/components/PageElements';
|
import { useHistory, useParams } from 'react-router-dom';
|
||||||
|
import { useRootStore } from '@educt/hooks/useRootStore';
|
||||||
|
import { useColorModeValue, useToast } from '@chakra-ui/react';
|
||||||
import { useFetchLesson } from '@educt/hooks/queries/lesson/useFetchLesson';
|
import { useFetchLesson } from '@educt/hooks/queries/lesson/useFetchLesson';
|
||||||
import LoadingPage from '@educt/components/LoadingPage';
|
import { useFetchCourseLessons } from '@educt/hooks/queries/course/useFetchCourseLessons';
|
||||||
import { MdCheckCircle, MdOutlineFilePresent, MdSkipNext, MdSkipPrevious } from 'react-icons/md';
|
import { useFetchLessonProgress } from '@educt/hooks/queries/lesson/useFetchLessonProgress';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Services
|
||||||
|
*/
|
||||||
|
import { LessonServiceInstance } from '@educt/services';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lesson Page
|
* Lesson Page
|
||||||
*/
|
*/
|
||||||
const LessonPage: React.FC<IPageProps> = () => {
|
const LessonPage: React.FC<IPageProps> = () => {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
|
const {
|
||||||
|
userStore: { me },
|
||||||
|
} = useRootStore();
|
||||||
const { error, data: lesson, isLoading } = useFetchLesson(id);
|
const { error, data: lesson, isLoading } = useFetchLesson(id);
|
||||||
|
const { fetchCourseLessons, data: lessons } = useFetchCourseLessons();
|
||||||
|
const { fetchLessonProgress } = useFetchLessonProgress();
|
||||||
|
const history = useHistory();
|
||||||
|
const toast = useToast();
|
||||||
|
const [nextLesson, setNextLesson] = useState<ILesson | null>(null);
|
||||||
|
const [prevLesson, setPrevLesson] = useState<ILesson | null>(null);
|
||||||
|
const currentLessonRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const bg = useColorModeValue('gray.50', 'gray.700');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
currentLessonRef.current?.scrollIntoView({ block: 'center' });
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
||||||
|
(async () => {
|
||||||
|
await fetchCourseLessons(lesson.course_id);
|
||||||
|
lesson.video && (await fetchLessonProgress(lesson.id));
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
}, [lesson]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lesson not found
|
||||||
|
*/
|
||||||
if (error?.response?.status === 404) {
|
if (error?.response?.status === 404) {
|
||||||
return <Redirect to='/404' />;
|
return <Redirect to='/404' />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!lesson || isLoading) return <LoadingPage />;
|
/**
|
||||||
|
* Unavailable lesson for authorized user
|
||||||
|
*/
|
||||||
|
if (error?.response?.status === 403) {
|
||||||
|
return <Redirect to={`/courses`} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!lesson || !lessons || isLoading) return <LoadingPage />;
|
||||||
|
|
||||||
|
const handleNextLesson = () => nextLesson && history.push(`/lesson/${nextLesson.id}`);
|
||||||
|
|
||||||
|
const handlePrevLesson = () => prevLesson && history.push(`/lesson/${prevLesson.id}`);
|
||||||
|
|
||||||
|
const handleEditLesson = () => history.push(`/lessons/edit/${id}`);
|
||||||
|
|
||||||
|
const handleDownloadMaterial =
|
||||||
|
({ name, client_name }: ILessonMaterial) =>
|
||||||
|
async () => {
|
||||||
|
try {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
if (error.response) {
|
||||||
|
if (error.response.status === 404) {
|
||||||
|
toast({ title: 'File not found.', status: 'error' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (me === null) return <LoadingPage />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageWrapper>
|
<PageWrapper>
|
||||||
|
<Helmet>
|
||||||
|
<title>{lesson.title}</title>
|
||||||
|
</Helmet>
|
||||||
<PageHeading heading={lesson.title}>
|
<PageHeading heading={lesson.title}>
|
||||||
<Box mt='3'>
|
<Flex mt='3' alignItems={'center'} justifyContent={'space-between'}>
|
||||||
<Text fontSize={'sm'}>~{moment.duration(lesson.duration, 'minutes').humanize()}</Text>
|
<Text fontSize={'sm'}>~{moment.duration(lesson.duration, 'minutes').humanize()}</Text>
|
||||||
</Box>
|
{(me.isAdmin || me.isTeacher) && (
|
||||||
|
<Button onClick={handleEditLesson} leftIcon={<EditIcon />} variant={'outline'} size='sm'>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
</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'} />
|
<ReactPlayer width={'100%'} height={'100%'} controls url={lesson.video?.url} />
|
||||||
|
{!lesson.video && (
|
||||||
|
<Flex h='500px' justifyContent={'center'} alignItems={'center'}>
|
||||||
|
<Text color='gray.500' userSelect={'none'}>
|
||||||
|
Video has not been uploaded yet.
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
</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'>
|
||||||
@ -59,43 +192,83 @@ const LessonPage: React.FC<IPageProps> = () => {
|
|||||||
<Divider mt='4' />
|
<Divider mt='4' />
|
||||||
</Box>
|
</Box>
|
||||||
</GridItem>
|
</GridItem>
|
||||||
<GridItem>
|
<GridItem minWidth={'250px'}>
|
||||||
<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) => (
|
ref={lesson.id === id ? currentLessonRef : undefined}
|
||||||
<Flex key={i} alignItems={'center'} borderRadius={'xl'}>
|
as='div'
|
||||||
<Icon as={MdCheckCircle} color='green.500' w={7} h={7} mr='2' />
|
display={'flex'}
|
||||||
|
minH='100'
|
||||||
|
key={lesson.id}
|
||||||
|
bg={lesson.id === id ? bg : ''}
|
||||||
|
p='4'
|
||||||
|
alignItems={'center'}
|
||||||
|
_hover={{ bg }}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
as={lesson.progress ? MdCheckCircle : MdOutlineCircle}
|
||||||
|
color={lesson.progress ? 'green.500' : '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>
|
||||||
|
{lesson.materials.length !== 0 ? (
|
||||||
<Stack>
|
<Stack>
|
||||||
{[1, 2, 3].map(n => (
|
{lesson.materials.map(material => (
|
||||||
<Flex key={n} alignItems={'center'} mt='5' p='3' bg='gray.50' borderRadius={'lg'}>
|
<Flex
|
||||||
|
key={material.id}
|
||||||
|
alignItems={'center'}
|
||||||
|
mt='5'
|
||||||
|
p='3'
|
||||||
|
bg={bg}
|
||||||
|
borderRadius={'lg'}
|
||||||
|
_hover={{ textDecor: 'underline' }}
|
||||||
|
>
|
||||||
<Icon as={MdOutlineFilePresent} mr='2' h={4} w={4} />
|
<Icon as={MdOutlineFilePresent} mr='2' h={4} w={4} />
|
||||||
<Text fontSize={'sm'} fontWeight={'medium'} lineHeight={'1rem'}>
|
<Text
|
||||||
lesson-1_sourse-code.zip <br />
|
onClick={handleDownloadMaterial(material)}
|
||||||
<Text as='small'>190kB</Text>
|
cursor={'pointer'}
|
||||||
|
overflow={'hidden'}
|
||||||
|
fontSize={'sm'}
|
||||||
|
fontWeight={'medium'}
|
||||||
|
lineHeight={'1rem'}
|
||||||
|
>
|
||||||
|
{material.client_name}
|
||||||
|
<br />
|
||||||
|
<Text as='small' color='gray.500'>
|
||||||
|
{helpres.transformBytes(material.size)}
|
||||||
|
</Text>
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</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,8 +60,10 @@ export const ProfileDescription: React.FC<IProfileDescriptionProps> = props => {
|
|||||||
{email}
|
{email}
|
||||||
</Text>
|
</Text>
|
||||||
</Heading>
|
</Heading>
|
||||||
|
<Box>
|
||||||
<UserBadge mt='2' roles={roles} />
|
<UserBadge mt='2' roles={roles} />
|
||||||
</Box>
|
</Box>
|
||||||
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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
|
||||||
*
|
*
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { AxiosInstance } from 'axios';
|
import { AxiosInstance } from 'axios';
|
||||||
import { IApiRespose, ILesson } from '@educt/interfaces';
|
import { IApiRespose, ILesson } from '@educt/interfaces';
|
||||||
import { ApiServiceInstance } from './ApiService';
|
import { ApiServiceInstance } from './ApiService';
|
||||||
|
import type { LessonProgress } from '@educt/types';
|
||||||
|
|
||||||
class LessonService {
|
class LessonService {
|
||||||
public api: AxiosInstance;
|
public api: AxiosInstance;
|
||||||
@ -20,6 +21,28 @@ 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get progress about lesson
|
||||||
|
*
|
||||||
|
* @param id Lesson id
|
||||||
|
* @returns Progress info
|
||||||
|
*/
|
||||||
|
public async fetchProgress(id: string): Promise<IApiRespose<LessonProgress>> {
|
||||||
|
const result = await this.api.get(`/v1/lessons/${id}/progress`);
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete lesson by id
|
* Delete lesson by id
|
||||||
*
|
*
|
||||||
|
|||||||
17
src/types/index.d.ts
vendored
17
src/types/index.d.ts
vendored
@ -34,6 +34,23 @@ export type ColorType = {
|
|||||||
hex: string;
|
hex: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type LessonProgress = {
|
||||||
|
progress: {
|
||||||
|
is_watched: boolean;
|
||||||
|
} | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LessonVideoType = {
|
||||||
|
id: number;
|
||||||
|
client_name: string;
|
||||||
|
ext: string;
|
||||||
|
name: string;
|
||||||
|
size: number;
|
||||||
|
url: string;
|
||||||
|
created_at: '2022-01-02T21:39:08.481+03:00';
|
||||||
|
updated_at: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type ConfirmEmailContainerDataType = {
|
export type ConfirmEmailContainerDataType = {
|
||||||
newEmail: string;
|
newEmail: string;
|
||||||
expired_seconds: number;
|
expired_seconds: number;
|
||||||
|
|||||||
@ -29,6 +29,10 @@ export default defineConfig({
|
|||||||
target: constants.BACKEND_URL,
|
target: constants.BACKEND_URL,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
|
'/video': {
|
||||||
|
target: constants.BACKEND_URL,
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user