diff --git a/package-lock.json b/package-lock.json index 79df364..104260e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2101,6 +2101,11 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "react-movable": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/react-movable/-/react-movable-3.0.2.tgz", + "integrity": "sha512-dDDYm3CRnDy8YLXMyyaR2MbcQiTwhPOP+dfl3fZukiI6mN1flVatcjSozT7HXjVk2yHwBC67ZOWGVAmjY6F/dA==" + }, "react-refresh": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.10.0.tgz", diff --git a/package.json b/package.json index 88a5b2e..84df647 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "react-helmet": "^6.1.0", "react-hook-form": "^7.12.2", "react-icons": "^3.11.0", + "react-movable": "^3.0.2", "react-router-dom": "^5.2.0", "react-spinners": "^0.11.0", "validator": "^13.6.0", diff --git a/src/components/SetCourseStatusButton/SetCourseStatusButton.tsx b/src/components/SetCourseStatusButton/SetCourseStatusButton.tsx new file mode 100644 index 0000000..f14f08b --- /dev/null +++ b/src/components/SetCourseStatusButton/SetCourseStatusButton.tsx @@ -0,0 +1,52 @@ +import React, { useState } from 'react'; +import { Button } from '@chakra-ui/react'; +import { AiOutlineEye, AiOutlineEyeInvisible } from 'react-icons/ai'; +import { CourseStatusEnum } from '@educt/enums'; +import useSetCourseStatusQuery from '@educt/hooks/useSetCourseStatusQuery'; + +type SetCourseStatusButtonPropsType = { + courseId: string; + currentStatus: CourseStatusEnum; + loadingText?: string | undefined; +}; + +const SetCourseStatusButton: React.FC = ({ + courseId, + currentStatus, + loadingText = 'Wait a second...', +}) => { + const [status, setStatus] = useState(currentStatus); + const { setCourseStatus, isLoading } = useSetCourseStatusQuery(); + + const handleChangeStatus = async (newStatus: CourseStatusEnum) => { + await setCourseStatus(courseId, newStatus); + setStatus(newStatus); + }; + + const Components: { [k in keyof typeof CourseStatusEnum]: any } = { + PUBLISHED: ( + + ), + DRAFT: ( + + ), + }; + + return Components[status]; +}; + +export default SetCourseStatusButton; diff --git a/src/components/SetCourseStatusButton/index.ts b/src/components/SetCourseStatusButton/index.ts new file mode 100644 index 0000000..bb60614 --- /dev/null +++ b/src/components/SetCourseStatusButton/index.ts @@ -0,0 +1,3 @@ +import SetCourseStatusButton from './SetCourseStatusButton'; + +export default SetCourseStatusButton; diff --git a/src/hooks/useSetCourseStatusQuery.ts b/src/hooks/useSetCourseStatusQuery.ts new file mode 100644 index 0000000..853988c --- /dev/null +++ b/src/hooks/useSetCourseStatusQuery.ts @@ -0,0 +1,49 @@ +/** + * Hooks + */ +import { CourseStatusEnum } from '@educt/enums'; +import { useErrorHandler } from 'react-error-boundary'; +import useAsync from './useAsync'; +import { useRootStore } from './useRootStore'; +import { useToast } from '@chakra-ui/toast'; + +type QueryResponseDataType = {} | undefined; +export type SetCourseStatusFnType = ( + id: string, + newStatus: CourseStatusEnum +) => Promise; + +const useSetCourseStatusQuery = () => { + const { courseStore } = useRootStore(); + const toast = useToast(); + const handleError = useErrorHandler(); + + const setCourseStatus = async (id: string, newStatus: CourseStatusEnum) => { + try { + const result = await courseStore.setCourseStatus(id, newStatus); + switch (newStatus) { + case CourseStatusEnum.PUBLISHED: + toast({ title: 'Course has been published.', status: 'success' }); + break; + case CourseStatusEnum.DRAFT: + toast({ title: 'Course has been marked as draft.', status: 'info' }); + break; + default: + break; + } + return result.data; + } catch (error: any) { + if (error.response) { + toast({ title: error.message, status: 'error' }); + } else { + handleError(error); + } + } + }; + + const { execute, ...state } = useAsync>(setCourseStatus); + + return { setCourseStatus: execute, ...state }; +}; + +export default useSetCourseStatusQuery; diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index 92b277c..9b2f49f 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -50,6 +50,7 @@ export interface ICategory { id: string; title: string; description: string; + color?: ColorType | undefined | null; } /** diff --git a/src/pages/courses/components/CategoryList/CategoryItem.tsx b/src/pages/courses/components/CategoryList/CategoryItem.tsx index 7bd92f5..5c58ed3 100644 --- a/src/pages/courses/components/CategoryList/CategoryItem.tsx +++ b/src/pages/courses/components/CategoryList/CategoryItem.tsx @@ -22,6 +22,7 @@ export type CategoryItemPropsType = { const CategoryItem: React.FC = ({ category }) => { const { selectedCategory, setSelectedCategory } = useContext(CoursesPageContext); + const isSelected = selectedCategory?.id === category.id; /** * Handle click on change category @@ -31,10 +32,12 @@ const CategoryItem: React.FC = ({ category }) => { return ( diff --git a/src/pages/courses/components/CourseList/CourseItem.tsx b/src/pages/courses/components/CourseList/CourseItem.tsx index e09e0cf..9dc05ed 100644 --- a/src/pages/courses/components/CourseList/CourseItem.tsx +++ b/src/pages/courses/components/CourseList/CourseItem.tsx @@ -13,6 +13,7 @@ import { DeleteIcon, EditIcon } from '@chakra-ui/icons'; */ import { ICourse } from '@educt/interfaces'; import { CourseStatusEnum } from '@educt/enums'; +import { SetCourseStatusFnType } from '@educt/hooks/useSetCourseStatusQuery'; /** * Components @@ -29,7 +30,7 @@ import { useRootStore } from '@educt/hooks/useRootStore'; export type CourseItemPropsType = { course: Omit; onDelete: (course: Pick) => void; - onSetStatus: (id: string, status: CourseStatusEnum) => Promise; + onSetStatus: SetCourseStatusFnType; }; const CourseItem: React.FC = ({ course, onDelete, onSetStatus }) => { diff --git a/src/pages/courses/components/CourseList/CourseList.tsx b/src/pages/courses/components/CourseList/CourseList.tsx index 4a52fec..92b6596 100644 --- a/src/pages/courses/components/CourseList/CourseList.tsx +++ b/src/pages/courses/components/CourseList/CourseList.tsx @@ -20,13 +20,12 @@ import LoadingList from '@educt/components/LoadingList'; import { useContext, useEffect } from 'react'; import { useRootStore } from '@educt/hooks/useRootStore'; import { useDisclosure } from '@chakra-ui/hooks'; -import { useToast } from '@chakra-ui/toast'; -import { useErrorHandler } from 'react-error-boundary'; /** * Contexts */ import { CoursesPageContext } from '@educt/contexts'; +import useSetCourseStatusQuery from '@educt/hooks/useSetCourseStatusQuery'; type CourseListPropsType = { render: React.FC; @@ -39,8 +38,7 @@ const CourseList: React.FC = ({ render: Item }) => { } = useRootStore(); const { selectedCategory, courseStatus, deletingCourse, setDeletingCourse } = useContext(CoursesPageContext); const { onOpen: onOpenDeleteDialog, onClose: onCloseDeleteDialog, isOpen: isOpenDeleteDialog } = useDisclosure(); - const handleError = useErrorHandler(); - const toast = useToast(); + const { setCourseStatus } = useSetCourseStatusQuery(); const { courses, isLoading } = courseStore; /** @@ -55,22 +53,6 @@ const CourseList: React.FC = ({ render: Item }) => { } }, [courseStore, selectedCategory, courseStatus]); - /** - * Handle update course status - */ - const onSetStatus = async (id: string, status: CourseStatusEnum) => { - try { - await courseStore.setCourseStatus(id, status); - toast({ title: 'Status updated.', status: 'info' }); - } catch (error: any) { - if (error.response) { - toast({ title: error.message, status: 'error' }); - } else { - handleError(error); - } - } - }; - if (courses === null || isLoading) return ; return ( @@ -88,7 +70,7 @@ const CourseList: React.FC = ({ render: Item }) => { setDeletingCourse(course); onOpenDeleteDialog(); }} - onSetStatus={onSetStatus} + onSetStatus={setCourseStatus} /> ))} diff --git a/src/pages/courses/edit/components/LessonList/LessonItem.tsx b/src/pages/courses/edit/components/LessonList/LessonItem.tsx deleted file mode 100644 index 224f259..0000000 --- a/src/pages/courses/edit/components/LessonList/LessonItem.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import React from 'react'; -import moment from 'moment'; -import { Flex, Box, Text, Icon, Button, IconButton } from '@chakra-ui/react'; -import { DeleteIcon } from '@chakra-ui/icons'; -import { MdTimer, MdAttachment } from 'react-icons/md'; - -/** - * Types - */ -import type { ILesson } from '@educt/interfaces'; - -/** - * Hooks - */ -import { useHistory } from 'react-router'; -import { useColorModeValue } from '@chakra-ui/color-mode'; - -export type LessonItemPropsType = { - lesson: ILesson; -}; - -const LessonItem: React.FC = ({ lesson }) => { - const history = useHistory(); - - const handleEditLesson = (): void => history.push(`/lessons/edit/${lesson.id}`); - const handleDeleteLesson = (): void => undefined; - - return ( - - - - - - - - - - 1. - - {lesson.title} - - - Sergey Yarkov - - - - - - - - } - /> - - - - - - - - ~{moment.duration(lesson.duration, 'minutes').humanize()} - - - - - - - - 4 attachments - - - - - - - - ); -}; - -export default LessonItem; diff --git a/src/pages/courses/edit/components/LessonList/LessonList.tsx b/src/pages/courses/edit/components/LessonList/LessonList.tsx index 28df04c..8ab888a 100644 --- a/src/pages/courses/edit/components/LessonList/LessonList.tsx +++ b/src/pages/courses/edit/components/LessonList/LessonList.tsx @@ -1,19 +1,24 @@ -import React from 'react'; -import { Box, Stack, Text, Button, StackDivider } from '@chakra-ui/react'; +import React, { useState } from 'react'; +import moment from 'moment'; +import { List, arrayMove } from 'react-movable'; +import { Flex, Box, Stack, Text, IconButton, Icon, Button } from '@chakra-ui/react'; /** * Types */ -import type { LessonItemPropsType } from './LessonItem'; -import type { ICourse } from '@educt/interfaces'; +import type { ICourse, ILesson } from '@educt/interfaces'; /** * Hooks */ import { useHistory } from 'react-router'; +import { useColorMode } from '@chakra-ui/react'; +import { DeleteIcon } from '@chakra-ui/icons'; +import { MdTimer, MdAttachment } from 'react-icons/md'; +import { DragHandleIcon } from '@chakra-ui/icons'; + type LessonListPropsType = { - render: React.FC; course: Omit; }; @@ -31,16 +36,103 @@ const CreateLessonButton: React.FC<{ id: string }> = ({ id }) => { ); }; -const LessonList: React.FC = ({ render: Item, course }) => { +const LessonList: React.FC = ({ course }) => { + const history = useHistory(); + const [lessons, setLessons] = useState(course.lessons); + const { colorMode } = useColorMode(); + + const handleEditLesson = (id: string): void => history.push(`/lessons/edit/${id}`); + const handleDeleteLesson = (id: string): void => undefined; + return ( {course.lessons.length !== 0 ? ( - - {course.lessons.map(lesson => ( - - ))} - + setLessons(arrayMove(lessons, oldIndex, newIndex))} + renderList={({ children, props }) => {children}} + renderItem={({ value: lesson, props, isDragged, index }) => ( + + + + + + + + + + + + + {index}. + + {lesson.title} + + + Sergey Yarkov + + + + + + + + handleDeleteLesson(lesson.id)} + aria-label='Delete lesson' + variant='ghost' + colorScheme='red' + icon={} + /> + + + + + + + + ~{moment.duration(lesson.duration, 'minutes').humanize()} + + + + + + + + 4 attachments + + + + + + + + )} + /> ) : ( diff --git a/src/pages/courses/edit/components/LessonList/index.ts b/src/pages/courses/edit/components/LessonList/index.ts index 2166a30..2b934e6 100644 --- a/src/pages/courses/edit/components/LessonList/index.ts +++ b/src/pages/courses/edit/components/LessonList/index.ts @@ -1,4 +1,3 @@ import LessonList from './LessonList'; -import LessonItem from './LessonItem'; -export { LessonList, LessonItem }; +export default LessonList; diff --git a/src/pages/courses/edit/edit.tsx b/src/pages/courses/edit/edit.tsx index 0e0e765..add76a2 100644 --- a/src/pages/courses/edit/edit.tsx +++ b/src/pages/courses/edit/edit.tsx @@ -1,5 +1,6 @@ -import React, { useEffect } from 'react'; +import React from 'react'; import Helmet from 'react-helmet'; +import { observer } from 'mobx-react'; import { Redirect } from 'react-router-dom'; import { Box, Heading, Text, Tab, TabList, Flex, Tabs, TabPanels, TabPanel } from '@chakra-ui/react'; @@ -13,7 +14,8 @@ import type { IPageProps } from '@educt/interfaces'; */ import LoadingPage from '@educt/components/LoadingPage'; import { EditCourseForm } from '@educt/components/Forms/CourseForm'; -import { LessonList, LessonItem } from './components/LessonList'; +import SetCourseStatusButton from '@educt/components/SetCourseStatusButton'; +import LessonList from './components/LessonList'; import { StudentList, StudentItem } from './components/StudentList'; /** @@ -44,12 +46,14 @@ const EditCoursePage: React.FC = () => { Editing {course.title} - - + + Course editor {course.title} - + + + @@ -74,7 +78,7 @@ const EditCoursePage: React.FC = () => { - + @@ -86,4 +90,4 @@ const EditCoursePage: React.FC = () => { ); }; -export default EditCoursePage; +export default observer(EditCoursePage); diff --git a/src/pages/profile/components/CourseList/CourseItem.tsx b/src/pages/profile/components/CourseList/CourseItem.tsx index 1deb371..df05588 100644 --- a/src/pages/profile/components/CourseList/CourseItem.tsx +++ b/src/pages/profile/components/CourseList/CourseItem.tsx @@ -30,7 +30,13 @@ const CourseItem: React.FC = ({ course }) => { - + {course.category.title}