diff --git a/src/components/Dialogs/DeleteLessonDialog/DeleteLessonDialog.tsx b/src/components/Dialogs/DeleteLessonDialog/DeleteLessonDialog.tsx new file mode 100644 index 0000000..51a12a8 --- /dev/null +++ b/src/components/Dialogs/DeleteLessonDialog/DeleteLessonDialog.tsx @@ -0,0 +1,55 @@ +import React from 'react'; + +/** + * Types + */ +import { ILesson } from '@educt/interfaces'; + +/** + * Components + */ +import ConfirmDialog from '@educt/components/ConfirmDialog/ConfirmDialog'; + +/** + * Hooks + */ +import { useDeleteLesson } from '@educt/hooks/queries'; +import { useRootStore } from '@educt/hooks/useRootStore'; + +type DeleteLessonDialogPropsType = { + isOpen: boolean; + onClose: () => void; +}; + +const DeleteLessonDialog: React.FC = ({ isOpen, onClose }) => { + const { + pageStore: { + editCourseStore: { deletingLesson }, + }, + } = useRootStore(); + const { deleteLesson, isLoading } = useDeleteLesson(); + + const handleDelete = async (isConfirmed: boolean) => { + if (isConfirmed && deletingLesson) { + await deleteLesson(deletingLesson.id); + } + + /** + * Close dialog + */ + onClose(); + }; + + return ( + handleDelete(isConfirmed)} + title='Confirm deleting' + message={`Lesson with title will be removed from the course. Attached materials will also be removed.`} + confirmMessage='Delete' + /> + ); +}; + +export default DeleteLessonDialog; diff --git a/src/components/Dialogs/DeleteLessonDialog/index.ts b/src/components/Dialogs/DeleteLessonDialog/index.ts new file mode 100644 index 0000000..0336573 --- /dev/null +++ b/src/components/Dialogs/DeleteLessonDialog/index.ts @@ -0,0 +1,3 @@ +import DeleteLessonDialog from './DeleteLessonDialog'; + +export default DeleteLessonDialog; diff --git a/src/hooks/queries/index.ts b/src/hooks/queries/index.ts new file mode 100644 index 0000000..2d0b14d --- /dev/null +++ b/src/hooks/queries/index.ts @@ -0,0 +1,3 @@ +import { useDeleteLesson } from './useDeleteLesson'; + +export { useDeleteLesson }; diff --git a/src/hooks/queries/useDeleteLesson.ts b/src/hooks/queries/useDeleteLesson.ts new file mode 100644 index 0000000..d06837a --- /dev/null +++ b/src/hooks/queries/useDeleteLesson.ts @@ -0,0 +1,33 @@ +import { useToast } from '@chakra-ui/toast'; +import { ILesson } from '@educt/interfaces'; +import { useErrorHandler } from 'react-error-boundary'; +import useAsync from '../useAsync'; +import { useRootStore } from '../useRootStore'; + +const useDeleteLesson = () => { + const { + pageStore: { editCourseStore }, + } = useRootStore(); + const toast = useToast(); + const handleError = useErrorHandler(); + + const deleteLesson = async (id: string) => { + try { + const result = await editCourseStore.deleteLessonById(id); + toast({ title: 'Lesson deleted', status: 'info' }); + return result.data; + } catch (error: any) { + if (error.response) { + console.error('Error: ', error); + } else { + handleError(error); + } + } + }; + + const { execute, ...state } = useAsync>(deleteLesson); + + return { deleteLesson: execute, ...state }; +}; + +export { useDeleteLesson }; diff --git a/src/hooks/useFetchCourseQuery.ts b/src/hooks/useFetchCourseQuery.ts index 8a3de78..171618c 100644 --- a/src/hooks/useFetchCourseQuery.ts +++ b/src/hooks/useFetchCourseQuery.ts @@ -2,11 +2,11 @@ * Types */ import { ICourse } from '@educt/interfaces'; -import { useEffect } from 'react'; /** * Hooks */ +import { useEffect } from 'react'; import useAsync from './useAsync'; import { useRootStore } from './useRootStore'; @@ -14,11 +14,11 @@ type QueryResponseDataType = Omit { const { - courseStore: { courseService }, + pageStore: { editCourseStore }, } = useRootStore(); const fetch = async (id: string) => { - const result = await courseService.fetchById(id); + const result = await editCourseStore.loadCourseById(id); return result.data; }; diff --git a/src/hooks/useLoginQuery.ts b/src/hooks/useLoginQuery.ts index f1bf2e1..372fbbb 100644 --- a/src/hooks/useLoginQuery.ts +++ b/src/hooks/useLoginQuery.ts @@ -1,5 +1,5 @@ import { useToast } from '@chakra-ui/react'; -import { IApiRespose, IToken } from '@educt/interfaces'; +import { IToken } from '@educt/interfaces'; import { useErrorHandler } from 'react-error-boundary'; import useAsync from './useAsync'; import { useRootStore } from './useRootStore'; diff --git a/src/pages/courses/edit/components/LessonList/LessonList.tsx b/src/pages/courses/edit/components/LessonList/LessonList.tsx index 32f1911..a5cb94b 100644 --- a/src/pages/courses/edit/components/LessonList/LessonList.tsx +++ b/src/pages/courses/edit/components/LessonList/LessonList.tsx @@ -1,16 +1,21 @@ -import React, { useState } from 'react'; +import React from 'react'; +import { observer } from 'mobx-react'; import { ItemProps, Virtuoso } from 'react-virtuoso'; import { DragDropContext, Draggable, DraggableProvided, Droppable, DropResult } from 'react-beautiful-dnd'; -import moment from 'moment'; -import * as helpers from '@educt/helpers'; import { Flex, Box, Text, IconButton, Icon, Button } from '@chakra-ui/react'; import { DeleteIcon, DragHandleIcon } from '@chakra-ui/icons'; +import moment from 'moment'; /** * Types */ import type { ICourse, ILesson } from '@educt/interfaces'; +/** + * Components + */ +import DeleteLessonDialog from '@educt/components/Dialogs/DeleteLessonDialog'; + /** * Hooks */ @@ -19,6 +24,12 @@ import { useColorMode } from '@chakra-ui/react'; import { MdTimer, MdAttachment } from 'react-icons/md'; import { useRootStore } from '@educt/hooks/useRootStore'; import useDidMountEffect from '@educt/hooks/useDidMountEffect'; +import { useDisclosure } from '@chakra-ui/hooks'; + +/** + * Contexts + */ +import { useErrorHandler } from 'react-error-boundary'; type LessonListPropsType = { course: Omit; @@ -38,26 +49,40 @@ const CreateLessonButton: React.FC<{ id: string }> = ({ id }) => { const LessonList: React.FC = ({ course }) => { const history = useHistory(); - const { lessonService } = useRootStore(); - const [lessons, setLessons] = useState(course.lessons); + const { + lessonService, + pageStore: { editCourseStore }, + } = useRootStore(); + const { onOpen: onOpenDeleteDialog, onClose: onCloseDeleteDialog, isOpen: isOpenDeleteDialog } = useDisclosure(); + const handleError = useErrorHandler(); + const lessons = course.lessons; const handleCreateLesson = (): void => history.push('/lessons/create'); const handleEditLesson = (id: string): void => history.push(`/lessons/edit/${id}`); - const handleDeleteLesson = (id: string): void => undefined; + const handleDeleteLesson = (lesson: ILesson): void => { + editCourseStore.setDeletingLesson(lesson); + onOpenDeleteDialog(); + }; + const handleChangeOrder = async (ids: string[]) => { try { const data = await lessonService.saveOrder(ids); return data; - } catch (error) { - console.error(error); + } catch (error: any) { + if (error.response) { + console.error(error); + } else { + handleError(error); + } } }; + const handleDragEnd = (result: DropResult) => { if (!result.destination) { return; } - setLessons(helpers.arrayMove(lessons, result.source.index, result.destination.index)); + editCourseStore.reorderLessons(result.source.index, result.destination.index); }; const LessonItem = React.useMemo(() => { @@ -117,7 +142,13 @@ const LessonList: React.FC = ({ course }) => { - } /> + handleDeleteLesson(lesson)} + aria-label='Delete lesson' + variant='ghost' + colorScheme='red' + icon={} + /> @@ -165,6 +196,7 @@ const LessonList: React.FC = ({ course }) => { {lessons.length !== 0 ? ( <> + Total: ({lessons.length}) @@ -219,4 +251,4 @@ const LessonList: React.FC = ({ course }) => { ); }; -export default LessonList; +export default observer(LessonList); diff --git a/src/pages/courses/edit/edit.tsx b/src/pages/courses/edit/edit.tsx index 93b89d0..8c3dfb1 100644 --- a/src/pages/courses/edit/edit.tsx +++ b/src/pages/courses/edit/edit.tsx @@ -1,6 +1,5 @@ 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'; @@ -22,6 +21,7 @@ import { StudentList, StudentItem } from './components/StudentList'; * Hooks */ import { useParams } from 'react-router-dom'; +import { useRootStore } from '@educt/hooks/useRootStore'; import useFetchCourseQuery from '@educt/hooks/useFetchCourseQuery'; /** @@ -29,7 +29,12 @@ import useFetchCourseQuery from '@educt/hooks/useFetchCourseQuery'; */ const EditCoursePage: React.FC = () => { const params = useParams<{ id: string }>(); - const { data: course, error, isLoading } = useFetchCourseQuery(params.id); + const { + pageStore: { + editCourseStore: { course }, + }, + } = useRootStore(); + const { error, isLoading } = useFetchCourseQuery(params.id); /** * Not Found @@ -41,7 +46,7 @@ const EditCoursePage: React.FC = () => { /** * Loading */ - if (isLoading || !course) return ; + if (!course || isLoading) return ; return ( <> @@ -92,4 +97,4 @@ const EditCoursePage: React.FC = () => { ); }; -export default observer(EditCoursePage); +export default EditCoursePage; diff --git a/src/pages/users/users.tsx b/src/pages/users/users.tsx index c0469d8..c7f7b19 100644 --- a/src/pages/users/users.tsx +++ b/src/pages/users/users.tsx @@ -21,7 +21,7 @@ import CreateUserForm from './components/CreateUserForm'; import { useRootStore } from '@educt/hooks/useRootStore'; /** - * Context + * Context Providers */ import { UsersPageContextProvider } from '@educt/providers'; diff --git a/src/services/LessonService.ts b/src/services/LessonService.ts index 9e42cc9..2c19740 100644 --- a/src/services/LessonService.ts +++ b/src/services/LessonService.ts @@ -1,4 +1,4 @@ -import { IApiRespose } from '@educt/interfaces'; +import { IApiRespose, ILesson } from '@educt/interfaces'; import { AxiosInstance } from 'axios'; export default class LessonService { @@ -8,6 +8,17 @@ export default class LessonService { this.api = api; } + /** + * Delete lesson by id + * + * @param id Lesson id + * @returns Deleted lesson + */ + public async deleteLesson(id: string): Promise> { + const result = await this.api.delete(`/v1/lessons/${id}`); + return result.data; + } + /** * Save order of lessons * diff --git a/src/stores/PageStore.ts b/src/stores/PageStore.ts new file mode 100644 index 0000000..8c11bc2 --- /dev/null +++ b/src/stores/PageStore.ts @@ -0,0 +1,21 @@ +import { AxiosInstance } from 'axios'; +import { makeAutoObservable } from 'mobx'; +import EditCourseStore from './pages/EditCourseStore'; +import RootStore from './RootStore'; + +export default class PageStore { + public root: RootStore; + + public editCourseStore: EditCourseStore; + + constructor(root: RootStore, api: AxiosInstance) { + this.root = root; + + /** + * Pages + */ + this.editCourseStore = new EditCourseStore(this, api); + + makeAutoObservable(this); + } +} diff --git a/src/stores/RootStore.ts b/src/stores/RootStore.ts index d23eb76..f240d4b 100644 --- a/src/stores/RootStore.ts +++ b/src/stores/RootStore.ts @@ -15,8 +15,11 @@ import UIStore from './UIStore'; import CategoryStore from './CategoryStore'; import CourseStore from './CourseStore'; import LessonService from '@educt/services/LessonService'; +import PageStore from './PageStore'; export default class RootStore { + public pageStore: PageStore; + public courseStore: CourseStore; public categoryStore: CategoryStore; @@ -33,15 +36,16 @@ export default class RootStore { constructor() { /** - * Api service + * Services */ + // TODO add other services this.apiService = new ApiService(this, apiConfig); - this.lessonService = new LessonService(this.apiService.api); /** * Stores */ + this.pageStore = new PageStore(this, this.apiService.api); this.courseStore = new CourseStore(this, this.apiService.api); this.categoryStore = new CategoryStore(this, this.apiService.api); this.uiStore = new UIStore(this); diff --git a/src/stores/pages/EditCourseStore.ts b/src/stores/pages/EditCourseStore.ts new file mode 100644 index 0000000..d920054 --- /dev/null +++ b/src/stores/pages/EditCourseStore.ts @@ -0,0 +1,82 @@ +import * as helpers from '@educt/helpers'; +import { ICourse, ILesson } from '@educt/interfaces'; +import { CourseService } from '@educt/services/CourseService'; +import LessonService from '@educt/services/LessonService'; +import { AxiosInstance } from 'axios'; +import { makeAutoObservable, runInAction } from 'mobx'; +import PageStore from '../PageStore'; + +export default class EditCourseStore { + public pageStore: PageStore; + + public course: Omit | undefined; + + public deletingLesson: ILesson | undefined; + + private courseService: CourseService; + + constructor(pageStore: PageStore, api: AxiosInstance) { + this.pageStore = pageStore; + this.courseService = new CourseService(api); + + makeAutoObservable(this); + } + + /** + * Load course by id into store + * + * @param id Course id + * @returns Course response + */ + public async loadCourseById(id: string) { + try { + const result = await this.courseService.fetchById(id); + + runInAction(() => { + this.course = result.data; + }); + + return result; + } catch (error: any) { + throw error; + } + } + + /** + * Remove lesson from store + * + * @param id Lesson id + * @returns Deleted lesson response + */ + public async deleteLessonById(id: string) { + try { + const result = await this.pageStore.root.lessonService.deleteLesson(id); + + runInAction(() => { + if (this.course) { + this.course.lessons = this.course.lessons.filter(lesson => lesson.id !== result.data.id); + } + }); + + return result; + } catch (error) { + throw error; + } + } + + /** + * Reorder lessons in store list on drag + * + * @param oldIndex + * @param newIndex + */ + public reorderLessons(oldIndex: number, newIndex: number): void { + if (this.course) { + this.course.lessons = helpers.arrayMove(this.course.lessons, oldIndex, newIndex); + } + } + + setDeletingLesson(lesson: ILesson) { + this.deletingLesson = lesson; + } +}