Compare commits

..

No commits in common. "develop" and "v1.0.0" have entirely different histories.

31 changed files with 154 additions and 5998 deletions

View File

@ -1,15 +0,0 @@
node_modules
dist
dist-ssr
coverage
stats.html
Dockerfile*
docker-compose*
.dockerignore
*.md
*.local
.git
.gitignore
.vscode
.DS_STORE
.env

1
.gitignore vendored
View File

@ -4,4 +4,3 @@ stats.html
dist
dist-ssr
*.local
.env

View File

@ -1,23 +0,0 @@
ARG NODE_IMAGE=node:14.17.3-alpine3.11
ARG NGINX_IMAGE=nginx:1.16.0-alpine
FROM ${NODE_IMAGE} as builder
WORKDIR /home/app
COPY package*.json ./
RUN npm install
COPY . .
# Get build arguments and set env variables
ARG API_URL
ARG WS_URL
ENV VITE_API_URL=${API_URL}
ENV VITE_WS_URL=${WS_URL}
RUN npm run build
FROM ${NGINX_IMAGE} as production
WORKDIR /usr/share/nginx/html
RUN rm /etc/nginx/conf.d/default.conf
COPY --from=builder /home/app/dist /usr/share/nginx/html
COPY ./nginx/default.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

View File

@ -1,3 +1,3 @@
# educt-client
## Edcut client
_This service is part of [Educt](https://github.com/sergeyyarkov/educt) which contains frontend application._
...

2
constants.ts Normal file
View File

@ -0,0 +1,2 @@
export const BACKEND_URL = 'http://localhost:3333';
export const WS_URL = 'ws://localhost:3333';

View File

@ -1,4 +0,0 @@
[[redirects]]
from = "/*"
to = "/index.html"
status = 200

View File

@ -1,8 +0,0 @@
server {
listen 5000;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html =404;
}
}

5811
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,9 @@
{
"name": "educt-client",
"version": "1.1.2",
"version": "1.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"build": "tsc && vite build",
"serve": "vite preview",
"format": "prettier --write \"src/**/*.{js,jsx,ts,tsx}\"",
"lint": "prettier --check \"src/**/*.{js,jsx,ts,tsx}\""
@ -32,7 +32,7 @@
"react-player": "^2.9.0",
"react-router-dom": "^5.2.0",
"react-spinners": "^0.11.0",
"react-virtuoso": "^2.13.1",
"react-virtuoso": "^2.2.8",
"socket.io-client": "^4.4.1",
"validator": "^13.6.0",
"yup": "^0.32.9"

View File

@ -18,19 +18,6 @@ import { SocketContext } from './contexts';
import { HistoryMessageType, UserOnlineListType, UserSeesionType } from './types';
import { useToast } from '@chakra-ui/react';
/**
* Virtuoso's resize observer can this error,
* which is caught by DnD and aborts dragging.
*/
window.addEventListener('error', e => {
if (
e.message === 'ResizeObserver loop completed with undelivered notifications.' ||
e.message === 'ResizeObserver loop limit exceeded'
) {
e.stopImmediatePropagation();
}
});
const App = () => {
const {
userStore,

View File

@ -1,8 +1,8 @@
import { AxiosRequestConfig } from 'axios';
import * as constants from '@educt/constants';
import * as constants from '../constants';
const apiConfig: AxiosRequestConfig = {
baseURL: `${constants.BACKEND_URL}/`,
baseURL: `${constants.BACKEND_URL}/api/`,
responseType: 'json',
withCredentials: true,
};

View File

@ -25,7 +25,6 @@ import { useErrorHandler } from 'react-error-boundary';
* Services
*/
import { CategoryServiceInstance, UserServiceInstance } from '@educt/services';
import { DESCRIPTION_MAX_LENGTH, EDUCATION_DESCRIPTION_MAX_LENGTH } from './CourseForm.validator';
export type InputFields = {
title: string;
@ -58,10 +57,9 @@ const CourseForm: React.FC<CourseFormPropsType> = ({ onSubmit, buttonLabel, isLo
*/
const loadUsersOptions = async (): Promise<OptionType[] | undefined> => {
try {
const teachers = await UserServiceInstance.fetchAll({ role: UserRoleEnum.TEACHER });
const admins = await UserServiceInstance.fetchAll({ role: UserRoleEnum.ADMIN });
const users = await UserServiceInstance.fetchAll({ limit: 12, role: UserRoleEnum.TEACHER });
return teachers.data.concat(admins.data).map(user => ({
return users.data.map(user => ({
label: user.fullname,
value: user.id,
}));
@ -115,11 +113,8 @@ const CourseForm: React.FC<CourseFormPropsType> = ({ onSubmit, buttonLabel, isLo
<Text as='small' color='red.500'>
{errors.description?.message}
</Text>
<Text
as='small'
color={!!errors.description || watchDescription?.length > DESCRIPTION_MAX_LENGTH ? 'red.500' : 'gray.500'}
>
{watchDescription?.length || 0}/{DESCRIPTION_MAX_LENGTH}
<Text as='small' color={!!errors.description || watchDescription?.length > 250 ? 'red.500' : 'gray.500'}>
{watchDescription?.length || 0}/250
</Text>
</Flex>
</FormControl>
@ -135,12 +130,10 @@ const CourseForm: React.FC<CourseFormPropsType> = ({ onSubmit, buttonLabel, isLo
<Text
as='small'
color={
!!errors.education_description || watchEducationDescription.length > EDUCATION_DESCRIPTION_MAX_LENGTH
? 'red.500'
: 'gray.500'
!!errors.education_description || watchEducationDescription.length > 250 ? 'red.500' : 'gray.500'
}
>
{watchEducationDescription.length || 0}/{EDUCATION_DESCRIPTION_MAX_LENGTH}
{watchEducationDescription.length || 0}/250
</Text>
)}
</Flex>

View File

@ -1,20 +1,15 @@
import yup from '@educt/schema';
export const FILE_SIZE = 1000 * 1024;
export const SUPPORTED_FORMATS = ['image/jpg', 'image/jpeg', 'image/png', 'image/webp'];
export const DESCRIPTION_MAX_LENGTH = 250;
export const EDUCATION_DESCRIPTION_MAX_LENGTH = 250;
const FILE_SIZE = 1000 * 1024;
const SUPPORTED_FORMATS = ['image/jpg', 'image/jpeg', 'image/png', 'image/webp'];
const CreateCourseSchema = 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(DESCRIPTION_MAX_LENGTH, 'Description must be at most 250 characters'),
education_description: yup
.string()
.max(EDUCATION_DESCRIPTION_MAX_LENGTH, 'This section must be at most 250 characters')
.nullable(),
.max(250, 'Description must be at most 250 characters'),
education_description: yup.string().max(250, 'This section must be at most 250 characters').nullable(),
image: yup
.mixed()
.test('fileSize', 'File size is too large', (value: File) => {

View File

@ -27,7 +27,6 @@ const CreateLessonForm: React.FC = () => {
description: data.description,
duration: data.duration,
video: data.video,
linked_video_url: data.linked_video_url,
materials: Array.from(data.materials || []),
course_id,
});

View File

@ -27,7 +27,6 @@ const EditLessonForm: React.FC<EditLessonFormPropsType> = ({ lesson }) => {
title: lesson.title,
description: lesson.description,
duration: lesson.duration,
linked_video_url: lesson.linked_video_url || undefined,
video: undefined,
materials: undefined,
},
@ -42,7 +41,6 @@ const EditLessonForm: React.FC<EditLessonFormPropsType> = ({ lesson }) => {
description: data.description,
video: data.video,
duration: data.duration,
linked_video_url: data.linked_video_url,
materials: data.materials?.length !== 0 ? Array.from(data.materials || []) : null,
});

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React from 'react';
import {
FormControl,
FormHelperText,
@ -13,20 +13,17 @@ import {
Flex,
Divider,
} from '@chakra-ui/react';
import * as constants from '@educt/constants';
import { Controller, UseFormReturn } from 'react-hook-form';
import VideoUploader from '@educt/components/VideoUploader';
import FilesUploader from '@educt/components/FilesUploader';
import { FileSupportedFormatsEnum, VideoSupportedFormatsEnum } from '@educt/enums';
import type { AttachmentFileType, LessonVideoType } from '@educt/types';
import { DESCRIPTION_MAX_LENGTH } from './LessonForm.validator';
export type InputFields = {
title: string;
description: string;
duration: string;
video: File;
linked_video_url?: string | undefined;
materials?: FileList | undefined;
};
@ -50,21 +47,9 @@ const LessonForm: React.FC<LessonFormPropsType> = ({
const {
control,
register,
watch,
formState: { errors, isDirty },
unregister,
} = reactHookForm;
const [isLinkedVideoUrl, setIsLinkedVideoUrl] = useState<boolean>(false);
const watchDescription = watch('description');
useEffect(() => {
if (!isLinkedVideoUrl) {
unregister('linked_video_url');
}
}, [isLinkedVideoUrl]);
return (
<form onSubmit={onSubmit}>
<Stack spacing={'5'}>
@ -86,39 +71,16 @@ const LessonForm: React.FC<LessonFormPropsType> = ({
<Text as='small' color='red.500'>
{errors.description?.message}
</Text>
<Text
as='small'
color={!!errors.description || watchDescription?.length > DESCRIPTION_MAX_LENGTH ? 'red.500' : 'gray.500'}
>
{watchDescription?.length || 0}/{DESCRIPTION_MAX_LENGTH}
</Text>
</Flex>
</FormControl>
{isLinkedVideoUrl ? (
<FormControl id='linked_video_url'>
<FormLabel>Video URL</FormLabel>
<Input placeholder='Link to video source. (YouTube, Vimeo, .mp4)' {...register('linked_video_url')} />
<Text as='small' color='red.500'>
{errors.linked_video_url?.message}
</Text>
<FormHelperText textDecoration={'underline'} cursor='pointer' onClick={() => setIsLinkedVideoUrl(false)}>
choose from files
</FormHelperText>
</FormControl>
) : (
<FormControl id='video'>
<FormControl isRequired id='video'>
<FormLabel>Video</FormLabel>
<Controller
control={control}
name='video'
render={({ field: { onChange, value: file } }) => (
<VideoUploader
onChange={file => onChange(file)}
file={file}
setIsLinkedVideoUrl={setIsLinkedVideoUrl}
preloadedVideoUrl={preloadedVideo && constants.BACKEND_URL + preloadedVideo.url}
/>
<VideoUploader onChange={file => onChange(file)} file={file} preloadedVideoUrl={preloadedVideo?.url} />
)}
/>
<Text as='small' color='red.500'>
@ -131,7 +93,6 @@ const LessonForm: React.FC<LessonFormPropsType> = ({
.join(', ')}
</FormHelperText>
</FormControl>
)}
<FormControl isRequired id='duration'>
<FormLabel>Video duration</FormLabel>

View File

@ -1,9 +1,8 @@
import * as helpers from '@educt/helpers';
import yup from '@educt/schema';
export const VIDEO_FILE_SIZE = 5000 * 1024 * 1024;
export const MATERIAL_FILE_SIZE = 100000 * 1024;
export const DESCRIPTION_MAX_LENGTH = 250;
const VIDEO_FILE_SIZE = 5000 * 1024 * 1024;
const MATERIAL_FILE_SIZE = 100000 * 1024;
const CreateLessonFormSchema = yup
.object({
@ -11,15 +10,14 @@ const CreateLessonFormSchema = yup
description: yup
.string()
.required('Description field name is required')
.max(DESCRIPTION_MAX_LENGTH, 'Description must be at most 250 characters'),
.max(250, 'Description must be at most 250 characters'),
duration: yup.string().required('Duration field name is required'),
video: yup
.mixed<File>()
.test('fileSize', `File size is too large, maximum size is ${helpers.transformBytes(VIDEO_FILE_SIZE)}`, value =>
!value ? true : value.size <= VIDEO_FILE_SIZE
)
.optional(),
linked_video_url: yup.string().matches(new RegExp('^(http|https|ftp)://'), 'URL is incorrect'),
.required('Video field name is required'),
materials: yup
.mixed<File[]>()
.test(

View File

@ -41,7 +41,7 @@ const Select: React.FC<ISelectPropsType> = props => {
>
{options?.find(option => option.value === value)?.label || props?.placeholder || 'Select Option'}
</MenuButton>
<MenuList maxH='175px' m='0' p='0' overflowY={'scroll'}>
<MenuList>
{options && options.length !== 0 ? (
options.map(option => (
<MenuItem
@ -49,9 +49,6 @@ const Select: React.FC<ISelectPropsType> = props => {
onClick={() => {
props.onChange(option.value);
}}
pb='0'
pt='0'
minH='35px'
>
{option.label}
</MenuItem>

View File

@ -3,7 +3,7 @@ import { ChatButton } from './ChatButton';
export interface IAvatarProps {
name: string;
isOnline?: boolean | null;
isOnline?: boolean | undefined;
}
export interface IChatButtonProps {

View File

@ -1,6 +1,6 @@
import React, { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { Flex, Box, Icon, Text, Button, Input, AspectRatio, useColorModeValue } from '@chakra-ui/react';
import { MdLink, MdUpload } from 'react-icons/md';
import { MdUpload } from 'react-icons/md';
import { VideoSupportedFormatsEnum } from '@educt/enums';
import { LessonVideoType } from '@educt/types';
@ -8,15 +8,9 @@ type VideoUploaderPropsType = {
onChange: (file: File) => void;
file?: File | LessonVideoType | undefined;
preloadedVideoUrl?: string | undefined;
setIsLinkedVideoUrl?: Dispatch<SetStateAction<boolean>>;
};
const VideoUploader: React.FC<VideoUploaderPropsType> = ({
onChange,
file,
preloadedVideoUrl,
setIsLinkedVideoUrl,
}) => {
const VideoUploader: React.FC<VideoUploaderPropsType> = ({ onChange, file, preloadedVideoUrl }) => {
const fileInputRef = useRef<HTMLInputElement>(null);
const videoRef = useRef<HTMLVideoElement>(null);
const [videoObjectUrl, setVideoObjectUrl] = useState<string | undefined>(undefined);
@ -48,23 +42,11 @@ const VideoUploader: React.FC<VideoUploaderPropsType> = ({
};
const ChangeVideoButton = () => (
<Button mr='1' onClick={onSelectFileHandler} leftIcon={<MdUpload />} variant={'outline'} size='sm' mt='2'>
<Button onClick={onSelectFileHandler} leftIcon={<MdUpload />} variant={'outline'} size='sm' mt='2' float='right'>
Change video
</Button>
);
const ChangeUrlVideoButton = () => (
<Button
onClick={() => setIsLinkedVideoUrl && setIsLinkedVideoUrl(true)}
leftIcon={<MdLink />}
variant={'outline'}
size='sm'
mt='2'
>
From URL
</Button>
);
useEffect(() => {
if (videoRef.current) {
videoRef.current.load();
@ -109,14 +91,9 @@ const VideoUploader: React.FC<VideoUploaderPropsType> = ({
</Text>
</Text>
</Box>
<Flex mt='4'>
<Button size='sm' onClick={onSelectFileHandler} mr='2'>
<Button onClick={onSelectFileHandler} mt='6'>
Select file
</Button>
<Button size='sm' onClick={() => setIsLinkedVideoUrl && setIsLinkedVideoUrl(true)}>
From URL
</Button>
</Flex>
</Flex>
) : (
<Box>
@ -125,10 +102,7 @@ const VideoUploader: React.FC<VideoUploaderPropsType> = ({
<source src={preloadedVideoUrl} />
</video>
</AspectRatio>
<Flex justifyContent={'flex-end'}>
<ChangeVideoButton />
<ChangeUrlVideoButton />
</Flex>
</Box>
)}
<Input

View File

@ -1,2 +0,0 @@
export const BACKEND_URL = import.meta.env.VITE_API_URL || 'http://localhost:3333';
export const WS_URL = import.meta.env.VITE_WS_URL || 'ws://localhost:3333';

10
src/env.d.ts vendored
View File

@ -1,10 +0,0 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_URL?: string | undefined;
readonly VITE_WS_URL?: string | undefined;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}

View File

@ -57,7 +57,6 @@ export interface ILesson {
progress: LessonProgress;
content: ILessonContent;
video?: LessonVideoType | undefined | null;
linked_video_url?: string | null;
created_at: string;
updated_at: string;
}

View File

@ -1,7 +1,6 @@
import React from 'react';
import { observer } from 'mobx-react';
import * as helpres from '@educt/helpers';
import * as constants from '@educt/constants';
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';
@ -46,7 +45,7 @@ const CoursePage: React.FC<IPageProps> = () => {
const isEmptyLessons = course.lessons.length === 0;
const isEmptyStudents = course.students.length === 0;
const handleWatchCourse = () => course.lessons[0] && history.push(`/lesson/${course.lessons[0].id}`);
const handleWatchCourse = () => history.push(`/lesson/${course.lessons[0].id}`);
const handleEditCourse = () => history.push(`/courses/edit/${course.id}`);
@ -74,7 +73,7 @@ const CoursePage: React.FC<IPageProps> = () => {
<Box>
{(me.isAdmin || me.isTeacher) && <EditButton onClick={handleEditCourse} mr='2' />}
<Button
isDisabled={!isCourseAvailable || course.lessons[0] === undefined}
isDisabled={!isCourseAvailable}
onClick={handleWatchCourse}
leftIcon={<MdPlayCircleOutline />}
size='sm'
@ -84,10 +83,7 @@ const CoursePage: React.FC<IPageProps> = () => {
</Button>
</Box>
</Flex>
<CourseBackgroundImage
bg={course.color?.hex}
src={course.image ? `${constants.BACKEND_URL + course.image.url}` : undefined}
/>
<CourseBackgroundImage bg={course.color?.hex} src={course.image?.url} />
</Box>
<Box mt='4'>
<Tabs>
@ -133,7 +129,7 @@ const CoursePage: React.FC<IPageProps> = () => {
<Text fontSize={'2xl'} fontWeight={'medium'}>
What you&apos;ll learn.
</Text>
<Text fontSize={'sm'} mt='2' textAlign={'justify'}>
<Text fontSize={'sm'} mt='2'>
{course.education_description}
</Text>
</Box>

View File

@ -7,7 +7,6 @@ import { Menu, MenuButton, MenuItem, MenuList } from '@chakra-ui/menu';
import { MdMoreHoriz, MdGroup, MdThumbUp, MdVideoLibrary } from 'react-icons/md';
import { AiOutlineEye, AiOutlineEyeInvisible } from 'react-icons/ai';
import { DeleteIcon, EditIcon } from '@chakra-ui/icons';
import * as constants from '@educt/constants';
/**
* Types
@ -103,9 +102,8 @@ const CourseItem: React.FC<CourseItemPropsType> = ({ course, onDelete, onSetStat
<Link
to={`/course/${course.id}`}
as={ReactRouterLink}
display='flex'
flexDir={'column'}
h='100%'
display='block'
h='full'
textDecoration='none'
_visited={{ textDecoration: 'none' }}
_hover={{ textDecoration: 'none' }}
@ -130,11 +128,11 @@ const CourseItem: React.FC<CourseItemPropsType> = ({ course, onDelete, onSetStat
w='100%'
borderTopLeftRadius='lg'
borderTopRightRadius='lg'
src={course.image ? `${constants.BACKEND_URL + course.image.url}` : undefined}
src={course.image.url}
/>
)}
</Box>
<Flex flexDir={'column'} p='0 10px' mt='10px' pb='20px' h='100%'>
<Box p='0 10px' mt='10px' pb='20px'>
<Flex justifyContent='space-between'>
<Badge colorScheme='blue'>{course.category.title}</Badge>
{(me.isAdmin || me.isTeacher) && <CourseStatusBadge status={course.status} />}
@ -142,10 +140,8 @@ const CourseItem: React.FC<CourseItemPropsType> = ({ course, onDelete, onSetStat
<Heading as='h2' fontSize='xl' mt='5px'>
{course.title}
</Heading>
<Text mt='2' mb='4'>
{course.description}
</Text>
<Flex mt='auto' flexWrap='wrap'>
<Text mt='2'>{course.description}</Text>
<Flex mt='20px' flexWrap='wrap'>
<Flex alignItems='center' mr='1'>
<Icon as={MdVideoLibrary} mr='2' />
<Text mr='4'>{course.lessons_count} lessons</Text>
@ -159,7 +155,7 @@ const CourseItem: React.FC<CourseItemPropsType> = ({ course, onDelete, onSetStat
<Text>{course.likes_count} likes</Text>
</Flex>
</Flex>
</Flex>
</Box>
</Link>
</Box>
);

View File

@ -69,7 +69,6 @@ const LessonList: React.FC<LessonListPropsType> = ({ course }) => {
const handleChangeOrder = async (ids: string[]) => {
try {
if (ids.length <= 0) return;
const data = await LessonServiceInstance.saveOrder(ids);
return data;
} catch (error) {

View File

@ -1,6 +1,5 @@
import React from 'react';
import moment from 'moment';
import * as constants from '@educt/constants';
import * as helpres from '@educt/helpers';
import { Link as ReactRouterLink, Redirect } from 'react-router-dom';
import { Helmet } from 'react-helmet';
@ -84,7 +83,7 @@ const LessonPage: React.FC<IPageProps> = () => {
if (lesson !== null) {
(async () => {
await fetchCourseLessons(lesson.course_id);
(lesson.video || lesson.linked_video_url) && (await fetchLessonProgress(lesson.id));
lesson.video && (await fetchLessonProgress(lesson.id));
})();
}
}, [lesson]);
@ -138,8 +137,6 @@ const LessonPage: React.FC<IPageProps> = () => {
if (me === null) return <LoadingPage />;
const videoURL = lesson.video ? constants.BACKEND_URL + lesson.video.url : lesson.linked_video_url;
return (
<Page>
<Helmet>
@ -154,17 +151,11 @@ const LessonPage: React.FC<IPageProps> = () => {
<Page.Content>
<Grid templateColumns={{ base: '1fr', xl: '3fr 1fr' }}>
<GridItem>
<Box overflowY='hidden'>
{lesson.video || lesson.linked_video_url ? (
<ReactPlayer
style={{ backgroundColor: '#000' }}
width={'100%'}
height={'547px'}
controls
url={videoURL || ''}
/>
<Box>
{lesson.video ? (
<ReactPlayer width={'100%'} height={'100%'} controls url={lesson.video?.url} />
) : (
<Flex h='547px' justifyContent={'center'} alignItems={'center'}>
<Flex h='500px' justifyContent={'center'} alignItems={'center'}>
<Text color='gray.500' userSelect={'none'}>
Video has not been uploaded yet.
</Text>
@ -200,7 +191,7 @@ const LessonPage: React.FC<IPageProps> = () => {
</GridItem>
<GridItem minWidth={'250px'}>
<Box>
<Stack maxH={'547px'} minH={'547px'} overflowY={'scroll'} spacing={'0'}>
<Stack maxH={'500px'} overflowY={'scroll'} spacing={'0'}>
{lessons.map((lesson, i) => (
<LinkBox
ref={lesson.id === id ? currentLessonRef : undefined}

View File

@ -26,7 +26,6 @@ import { observer } from 'mobx-react';
* Hooks
*/
import { useFetchCourses, useFetchStat } from '@educt/hooks/queries';
import { CourseStatusEnum } from '@educt/enums';
/**
* Main page
@ -36,7 +35,7 @@ const MainPage: React.FC<IPageProps> = () => {
userStore: { me },
onlineStore,
} = useRootStore();
const { data: courses } = useFetchCourses({ limit: 3, status: CourseStatusEnum.PUBLISHED });
const { data: courses } = useFetchCourses({ limit: 3 });
const { data: stat } = useFetchStat();
if (me === null || courses === null || stat === null) return <LoadingPage />;

View File

@ -1,5 +1,5 @@
import React, { useState, ReactNode } from 'react';
import * as constants from '@educt/constants';
import * as constants from '../../constants';
import {
ChangeEmailPageContext,
CoursesPageContext,

View File

@ -207,7 +207,6 @@ export type CreateLessonParamsType = {
duration: string;
course_id: string;
video: File;
linked_video_url?: string | undefined;
materials?: File[] | undefined;
};
@ -217,7 +216,6 @@ export type UpdateLessonParamsType = {
duration?: string | undefined;
course_id?: string | undefined;
video?: File | undefined;
linked_video_url?: string | undefined;
materials?: File[] | undefined | null;
};

View File

@ -1,4 +1,5 @@
import { defineConfig } from 'vite';
import * as constants from './constants';
import path from 'path';
import react from '@vitejs/plugin-react';
import { visualizer } from 'rollup-plugin-visualizer';
@ -19,7 +20,20 @@ export default defineConfig({
publicDir: 'public',
plugins: [react()],
server: {
port: 5000,
proxy: {
'/api': {
target: constants.BACKEND_URL,
changeOrigin: true,
},
'/uploads': {
target: constants.BACKEND_URL,
changeOrigin: true,
},
'/video': {
target: constants.BACKEND_URL,
changeOrigin: true,
},
},
},
resolve: {
alias: [{ find: '@educt', replacement: path.resolve(__dirname, 'src') }],