refactor: refactored course & lesson query hooks

This commit is contained in:
Sergey Yarkov 2021-12-01 23:58:30 +03:00
parent 2fa555b8be
commit dd86228cab
11 changed files with 178 additions and 120 deletions

6
package-lock.json generated
View File

@ -2552,9 +2552,9 @@
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
}, },
"typescript": { "typescript": {
"version": "4.4.4", "version": "4.5.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.2.tgz",
"integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==", "integrity": "sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==",
"dev": true "dev": true
}, },
"use-callback-ref": { "use-callback-ref": {

View File

@ -46,7 +46,7 @@
"@types/validator": "^13.6.3", "@types/validator": "^13.6.3",
"@vitejs/plugin-react": "^1.0.0", "@vitejs/plugin-react": "^1.0.0",
"rollup-plugin-visualizer": "^5.5.2", "rollup-plugin-visualizer": "^5.5.2",
"typescript": "^4.3.2", "typescript": "^4.5.2",
"vite": "^2.6.4" "vite": "^2.6.4"
} }
} }

View File

@ -17,17 +17,9 @@ import CourseForm from './CourseForm';
/** /**
* Hooks * Hooks
*/ */
import { useState } from 'react';
import { useHistory } from 'react-router'; import { useHistory } from 'react-router';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { useToast } from '@chakra-ui/toast'; import { useCreateCourse } from '@educt/hooks/queries';
import useIsMountedRef from '@educt/hooks/useIsMountedRef';
import { useErrorHandler } from 'react-error-boundary';
/**
* Services
*/
import { CourseServiceInstance } from '@educt/services';
/** /**
* Schema * Schema
@ -37,21 +29,16 @@ import CourseFormSchema from './CourseForm.validator';
type CreateFormCoursePropsType = {}; type CreateFormCoursePropsType = {};
const CreateFormCourse: React.FC<CreateFormCoursePropsType> = () => { const CreateFormCourse: React.FC<CreateFormCoursePropsType> = () => {
const [isLoading, setIsLoading] = useState<boolean>(false);
const form = useForm<InputFields>({ resolver: yupResolver(CourseFormSchema) }); const form = useForm<InputFields>({ resolver: yupResolver(CourseFormSchema) });
const isMountedRef = useIsMountedRef(); const { createCourse, isLoading } = useCreateCourse();
const history = useHistory(); const history = useHistory();
const toast = useToast();
const handleError = useErrorHandler();
/** /**
* Submit handler * Submit handler
*/ */
// TODO: move request to useCreateCourse hook
const onSubmit: SubmitHandler<InputFields> = async data => { const onSubmit: SubmitHandler<InputFields> = async data => {
try { try {
setIsLoading(true); const course = await createCourse({
const course = await CourseServiceInstance.create({
title: data.title, title: data.title,
description: data.description, description: data.description,
teacher_id: data.teacher_id, teacher_id: data.teacher_id,
@ -59,22 +46,10 @@ const CreateFormCourse: React.FC<CreateFormCoursePropsType> = () => {
image: data.image, image: data.image,
status: CourseStatusEnum.DRAFT, status: CourseStatusEnum.DRAFT,
}); });
toast({ title: `Course successfully created.`, status: 'success' });
history.push(`/courses/edit/${course.data.id}`); if (course) history.push(`/courses/edit/${course.id}`);
} catch (error: any) { } catch (error) {
if (error.response) { console.error(error);
if (error.response.status === 422) {
toast({ title: `${error.response.data.errors[0].message}`, status: 'error' });
} else {
toast({ title: `${error.message}`, status: 'error' });
}
} else {
handleError(error);
}
} finally {
if (isMountedRef.current) {
setIsLoading(false);
}
} }
}; };

View File

@ -16,21 +16,14 @@ import CourseForm from './CourseForm';
/** /**
* Hooks * Hooks
*/ */
import { useState } from 'react';
import { useParams } from 'react-router'; import { useParams } from 'react-router';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { useErrorHandler } from 'react-error-boundary';
import { useToast } from '@chakra-ui/toast';
/**
* Services
*/
import { CourseServiceInstance } from '@educt/services';
/** /**
* Schema * Schema
*/ */
import CourseFormSchema from './CourseForm.validator'; import CourseFormSchema from './CourseForm.validator';
import { useUpdateCourse } from '@educt/hooks/queries';
type EditFormCoursePropsType = { type EditFormCoursePropsType = {
defaultValues?: defaultValues?:
@ -45,7 +38,6 @@ type EditFormCoursePropsType = {
}; };
const EditFormCourse: React.FC<EditFormCoursePropsType> = ({ defaultValues }) => { const EditFormCourse: React.FC<EditFormCoursePropsType> = ({ defaultValues }) => {
const [isLoading, setIsLoading] = useState<boolean>(false);
const form = useForm<InputFields>({ const form = useForm<InputFields>({
resolver: yupResolver(CourseFormSchema), resolver: yupResolver(CourseFormSchema),
defaultValues: { defaultValues: {
@ -56,44 +48,30 @@ const EditFormCourse: React.FC<EditFormCoursePropsType> = ({ defaultValues }) =>
image: undefined, image: undefined,
}, },
}); });
const toast = useToast(); const { updateCourse, isLoading } = useUpdateCourse();
const handleError = useErrorHandler();
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();
/** /**
* Submit handler * Submit handler
*/ */
// TODO move request to useUpdateCourse hook
const onSubmit: SubmitHandler<InputFields> = async data => { const onSubmit: SubmitHandler<InputFields> = async data => {
try { try {
setIsLoading(true); const updated = await updateCourse(id, data);
const course = await CourseServiceInstance.update(id, {
title: data.title,
description: data.description,
teacher_id: data.teacher_id,
category_id: data.category_id,
image: data.image,
});
toast({ title: `Course updated.`, status: 'info' });
if (updated) {
/** /**
* Update fields with new values * Update fields with new values
*/ */
form.reset({ form.reset({
title: course.data.title, title: updated.title,
description: course.data.description, description: updated.description,
teacher_id: course.data.teacher.id, teacher_id: updated.teacher.id,
category_id: course.data.category.id, category_id: updated.category.id,
image: undefined, image: undefined,
}); });
} catch (error: any) {
if (error.response) {
toast({ title: `${error.message}`, status: 'error' });
} else {
handleError(error);
} }
} finally { } catch (error) {
setIsLoading(false); console.error(error);
} }
}; };

View File

@ -0,0 +1,49 @@
import { useToast } from '@chakra-ui/toast';
import { CourseStatusEnum } from '@educt/enums';
import { ICourse } from '@educt/interfaces';
import { CourseServiceInstance } from '@educt/services';
import { CreateCourseParamsType } from '@educt/types';
import { useErrorHandler } from 'react-error-boundary';
import useAsync from '../../useAsync';
type CreatedCourseDataType =
| Pick<ICourse, 'id' | 'title' | 'description' | 'status' | 'created_at' | 'updated_at'>
| undefined;
const useCreateCourse = () => {
const toast = useToast();
const handleError = useErrorHandler();
const createCourse = async (data: CreateCourseParamsType) => {
try {
const result = await CourseServiceInstance.create({
title: data.title,
description: data.description,
teacher_id: data.teacher_id,
category_id: data.category_id,
image: data.image,
status: CourseStatusEnum.DRAFT,
});
toast({ title: `Course successfully created.`, status: 'success' });
return result.data;
} catch (error: any) {
if (error.response) {
if (error.response.status === 422) {
toast({ title: `${error.response.data.errors[0].message}`, status: 'error' });
} else {
toast({ title: `${error.message}`, status: 'error' });
}
} else {
handleError(error);
}
return Promise.reject(error);
}
};
const { execute, ...state } = useAsync<CreatedCourseDataType, Parameters<typeof createCourse>>(createCourse);
return { createCourse: execute, ...state };
};
export { useCreateCourse };

View File

@ -0,0 +1,35 @@
import { useToast } from '@chakra-ui/toast';
import { ICourse } from '@educt/interfaces';
import { useErrorHandler } from 'react-error-boundary';
import { useRootStore } from '@educt/hooks/useRootStore';
import useAsync from '@educt/hooks/useAsync';
type DeletedCourseDataType = Omit<ICourse, 'teacher' | 'students' | 'lessons'> | undefined;
const useDeleteCourse = () => {
const { courseStore } = useRootStore();
const toast = useToast();
const handleError = useErrorHandler();
const deleteCourse = async (id: string) => {
try {
const result = await courseStore.deleteCourse(id);
toast({ title: 'Course deleted.', status: 'info' });
return result.data;
} catch (error: any) {
if (error.response) {
toast({ title: error.message, status: 'error' });
} else {
handleError(error);
}
return Promise.reject(error);
}
};
const { execute, ...state } = useAsync<DeletedCourseDataType, Parameters<typeof deleteCourse>>(deleteCourse);
return { deleteCourse: execute, ...state };
};
export { useDeleteCourse };

View File

@ -0,0 +1,41 @@
import { useToast } from '@chakra-ui/toast';
import { useErrorHandler } from 'react-error-boundary';
import { CourseServiceInstance } from '@educt/services';
import { UpdateCourseParamsType } from '@educt/types';
import useAsync from '../../useAsync';
import { ICourse } from '@educt/interfaces';
type UpdatedCourseDataType = Omit<ICourse, 'students_count' | 'likes_count' | 'lessons_count'> | undefined;
const useUpdateCourse = () => {
const toast = useToast();
const handleError = useErrorHandler();
const updateCourse = async (id: string, data: UpdateCourseParamsType) => {
try {
const result = await CourseServiceInstance.update(id, {
title: data.title,
description: data.description,
teacher_id: data.teacher_id,
category_id: data.category_id,
image: data.image,
});
toast({ title: `Course updated.`, status: 'info' });
return result.data;
} catch (error: any) {
if (error.response) {
toast({ title: `${error.message}`, status: 'error' });
} else {
handleError(error);
}
return Promise.reject(error);
}
};
const { execute, ...state } = useAsync<UpdatedCourseDataType, Parameters<typeof updateCourse>>(updateCourse);
return { updateCourse: execute, ...state };
};
export { useUpdateCourse };

View File

@ -1,3 +1,6 @@
import { useDeleteLesson } from './useDeleteLesson'; import { useDeleteLesson } from './lesson/useDeleteLesson';
export { useDeleteLesson }; import { useCreateCourse } from './course/useCreateCourse';
import { useUpdateCourse } from './course/useUpdateCourse';
export { useCreateCourse, useUpdateCourse, useDeleteLesson };

View File

@ -1,8 +1,10 @@
import { useToast } from '@chakra-ui/toast'; import { useToast } from '@chakra-ui/toast';
import { ILesson } from '@educt/interfaces'; import { ILesson } from '@educt/interfaces';
import { useErrorHandler } from 'react-error-boundary'; import { useErrorHandler } from 'react-error-boundary';
import useAsync from '../useAsync'; import useAsync from '../../useAsync';
import { useRootStore } from '../useRootStore'; import { useRootStore } from '../../useRootStore';
type CreatedLessonDataType = ILesson | undefined;
const useDeleteLesson = () => { const useDeleteLesson = () => {
const { const {
@ -22,10 +24,12 @@ const useDeleteLesson = () => {
} else { } else {
handleError(error); handleError(error);
} }
return Promise.reject(error);
} }
}; };
const { execute, ...state } = useAsync<ILesson | undefined, Parameters<typeof deleteLesson>>(deleteLesson); const { execute, ...state } = useAsync<CreatedLessonDataType, Parameters<typeof deleteLesson>>(deleteLesson);
return { deleteLesson: execute, ...state }; return { deleteLesson: execute, ...state };
}; };

View File

@ -1,14 +1,9 @@
import React, { useContext, useRef, useState } from 'react'; import React, { useContext } from 'react';
import {
AlertDialog, /**
AlertDialogBody, * Types
AlertDialogFooter, */
AlertDialogHeader, import { ICourse } from '@educt/interfaces';
AlertDialogContent,
AlertDialogOverlay,
Button,
useToast,
} from '@chakra-ui/react';
/** /**
* Components * Components
@ -23,9 +18,7 @@ import { CoursesPageContext } from '@educt/contexts';
/** /**
* Hooks * Hooks
*/ */
import { useErrorHandler } from 'react-error-boundary'; import { useDeleteCourse } from '@educt/hooks/queries/course/useDeleteCourse';
import { useRootStore } from '@educt/hooks/useRootStore';
import { ICourse } from '@educt/interfaces';
type DeleteCourseDialogPropsType = { type DeleteCourseDialogPropsType = {
course: Pick<ICourse, 'id' | 'title'>; course: Pick<ICourse, 'id' | 'title'>;
@ -34,34 +27,14 @@ type DeleteCourseDialogPropsType = {
}; };
const DeleteCourseDialog: React.FC<DeleteCourseDialogPropsType> = ({ onClose, isOpen, course }) => { const DeleteCourseDialog: React.FC<DeleteCourseDialogPropsType> = ({ onClose, isOpen, course }) => {
const { courseStore } = useRootStore();
const { setDeletingCourse } = useContext(CoursesPageContext); const { setDeletingCourse } = useContext(CoursesPageContext);
const [isLoading, setIsLoading] = useState<boolean>(false); const { deleteCourse, isLoading } = useDeleteCourse();
const handleError = useErrorHandler();
const toast = useToast();
/** const handleDelete = async (isConfirmed: boolean, id: string) => {
* Delete course handler
*
* @param isConfirmed Is dialog confirmed
* @param id Course id
*/
const deleteCourseHandler = async (isConfirmed: boolean, id: string) => {
if (isConfirmed) { if (isConfirmed) {
try { await deleteCourse(id)
setIsLoading(true); .catch(error => console.error(error))
await courseStore.deleteCourse(id); .finally(() => setDeletingCourse(undefined));
toast({ title: 'Course deleted.', status: 'info' });
} catch (error: any) {
if (error.response) {
toast({ title: error.message, status: 'error' });
} else {
handleError(error);
}
} finally {
setIsLoading(false);
setDeletingCourse(undefined);
}
} }
/** /**
@ -74,7 +47,7 @@ const DeleteCourseDialog: React.FC<DeleteCourseDialogPropsType> = ({ onClose, is
<ConfirmDialog <ConfirmDialog
isOpen={isOpen} isOpen={isOpen}
isLoading={isLoading} isLoading={isLoading}
onClose={isConfirmed => deleteCourseHandler(isConfirmed, course.id)} onClose={isConfirmed => handleDelete(isConfirmed, course.id)}
title='Confirm deleting' title='Confirm deleting'
message={`The course with title "${course.title}" will be removed from the system. After deleting a course, the lessons in it will also be deleted.`} message={`The course with title "${course.title}" will be removed from the system. After deleting a course, the lessons in it will also be deleted.`}
confirmMessage='Delete' confirmMessage='Delete'

View File

@ -38,7 +38,7 @@ type UpdateUserContactsFormPropsType = {
const UpdateUserContactsForm: React.FC<UpdateUserContactsFormPropsType> = ({ contacts }) => { const UpdateUserContactsForm: React.FC<UpdateUserContactsFormPropsType> = ({ contacts }) => {
const { const {
register, register,
formState: { errors, isDirty }, formState: { errors, dirtyFields, isDirty },
handleSubmit, handleSubmit,
reset, reset,
} = useForm<UpdateUserContactsInputType>({ } = useForm<UpdateUserContactsInputType>({