feat: page store & deleting lesson via dialog

This commit is contained in:
Sergey Yarkov 2021-11-29 22:57:04 +03:00
parent c14f0cb8c7
commit 947c9b92e1
13 changed files with 272 additions and 23 deletions

View File

@ -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<DeleteLessonDialogPropsType> = ({ 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 (
<ConfirmDialog
isOpen={isOpen}
isLoading={isLoading}
onClose={isConfirmed => 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;

View File

@ -0,0 +1,3 @@
import DeleteLessonDialog from './DeleteLessonDialog';
export default DeleteLessonDialog;

View File

@ -0,0 +1,3 @@
import { useDeleteLesson } from './useDeleteLesson';
export { useDeleteLesson };

View File

@ -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<ILesson | undefined, Parameters<typeof deleteLesson>>(deleteLesson);
return { deleteLesson: execute, ...state };
};
export { useDeleteLesson };

View File

@ -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<ICourse, 'students_count' | 'likes_count' | 'l
const useFetchCourseQuery = (id: string) => {
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;
};

View File

@ -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';

View File

@ -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<ICourse, 'students_count' | 'likes_count' | 'lessons_count'>;
@ -38,26 +49,40 @@ const CreateLessonButton: React.FC<{ id: string }> = ({ id }) => {
const LessonList: React.FC<LessonListPropsType> = ({ course }) => {
const history = useHistory();
const { lessonService } = useRootStore();
const [lessons, setLessons] = useState<ILesson[]>(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<LessonListPropsType> = ({ course }) => {
<Button size='md' p='0 60px' mr='1'>
Edit
</Button>
<IconButton aria-label='Delete lesson' variant='ghost' colorScheme='red' icon={<DeleteIcon />} />
<IconButton
onClick={() => handleDeleteLesson(lesson)}
aria-label='Delete lesson'
variant='ghost'
colorScheme='red'
icon={<DeleteIcon />}
/>
</Flex>
<Flex justifyContent='flex-end' mt={{ base: '2' }}>
<Text as='span' color='gray.500' mr='3'>
@ -165,6 +196,7 @@ const LessonList: React.FC<LessonListPropsType> = ({ course }) => {
<Box style={{ overflow: 'auto' }}>
{lessons.length !== 0 ? (
<>
<DeleteLessonDialog isOpen={isOpenDeleteDialog} onClose={onCloseDeleteDialog} />
<Flex mt='2' mb='3' padding='0 20px' alignItems='center' justifyContent='space-between'>
<Text fontWeight='medium'>Total: ({lessons.length})</Text>
<Button onClick={handleCreateLesson}>Create new</Button>
@ -219,4 +251,4 @@ const LessonList: React.FC<LessonListPropsType> = ({ course }) => {
);
};
export default LessonList;
export default observer(LessonList);

View File

@ -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<IPageProps> = () => {
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<IPageProps> = () => {
/**
* Loading
*/
if (isLoading || !course) return <LoadingPage />;
if (!course || isLoading) return <LoadingPage />;
return (
<>
@ -92,4 +97,4 @@ const EditCoursePage: React.FC<IPageProps> = () => {
);
};
export default observer(EditCoursePage);
export default EditCoursePage;

View File

@ -21,7 +21,7 @@ import CreateUserForm from './components/CreateUserForm';
import { useRootStore } from '@educt/hooks/useRootStore';
/**
* Context
* Context Providers
*/
import { UsersPageContextProvider } from '@educt/providers';

View File

@ -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<IApiRespose<ILesson>> {
const result = await this.api.delete(`/v1/lessons/${id}`);
return result.data;
}
/**
* Save order of lessons
*

21
src/stores/PageStore.ts Normal file
View File

@ -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);
}
}

View File

@ -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);

View File

@ -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<ICourse, 'students_count' | 'likes_count' | 'lessons_count'> | 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;
}
}