diff --git a/src/components/Forms/CourseForm/CreateCourseForm.tsx b/src/components/Forms/CourseForm/CreateCourseForm.tsx index dbf4fd6..b6ecaad 100644 --- a/src/components/Forms/CourseForm/CreateCourseForm.tsx +++ b/src/components/Forms/CourseForm/CreateCourseForm.tsx @@ -47,7 +47,7 @@ const CreateFormCourse: React.FC = () => { status: CourseStatusEnum.DRAFT, }); - if (course) history.push(`/courses/edit/${course.id}`); + history.push(`/courses/edit/${course.id}`); } catch (error) { console.error(error); } diff --git a/src/components/Forms/CourseForm/EditCourseForm.tsx b/src/components/Forms/CourseForm/EditCourseForm.tsx index bff3227..4730cbf 100644 --- a/src/components/Forms/CourseForm/EditCourseForm.tsx +++ b/src/components/Forms/CourseForm/EditCourseForm.tsx @@ -58,18 +58,16 @@ const EditFormCourse: React.FC = ({ defaultValues }) => try { const updated = await updateCourse(id, data); - if (updated) { - /** - * Update fields with new values - */ - form.reset({ - title: updated.title, - description: updated.description, - teacher_id: updated.teacher.id, - category_id: updated.category.id, - image: undefined, - }); - } + /** + * Update fields with new values + */ + form.reset({ + title: updated.title, + description: updated.description, + teacher_id: updated.teacher.id, + category_id: updated.category.id, + image: undefined, + }); } catch (error) { console.error(error); } diff --git a/src/helpers/index.ts b/src/helpers/index.ts index 71a7dbf..f505833 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -84,3 +84,7 @@ export function arrayMove(array: T[], from: number, to: number) { result.splice(to, 0, removed); return result; } + +export function getDirtyFields(dirtyFields: { [key: string]: boolean | undefined }, data: T) { + return Object.fromEntries(Object.keys(dirtyFields).map(k => [k, data[k as keyof T]])); +} diff --git a/src/hooks/queries/auth/useLogin.ts b/src/hooks/queries/auth/useLogin.ts index 8ef22c2..f9f311f 100644 --- a/src/hooks/queries/auth/useLogin.ts +++ b/src/hooks/queries/auth/useLogin.ts @@ -4,7 +4,7 @@ import { useErrorHandler } from 'react-error-boundary'; import useAsync from '../../useAsync'; import { useRootStore } from '../../useRootStore'; -type LoginResultDataType = IToken | undefined; +type LoginResultDataType = IToken; const useLogin = () => { const { authStore } = useRootStore(); diff --git a/src/hooks/queries/course/useCreateCourse.ts b/src/hooks/queries/course/useCreateCourse.ts index 690dafc..800ceac 100644 --- a/src/hooks/queries/course/useCreateCourse.ts +++ b/src/hooks/queries/course/useCreateCourse.ts @@ -6,9 +6,7 @@ import { CreateCourseParamsType } from '@educt/types'; import { useErrorHandler } from 'react-error-boundary'; import useAsync from '../../useAsync'; -type CreatedCourseDataType = - | Pick - | undefined; +type CreatedCourseDataType = Pick; const useCreateCourse = () => { const toast = useToast(); diff --git a/src/hooks/queries/course/useDeleteCourse.ts b/src/hooks/queries/course/useDeleteCourse.ts index 6f5553f..9f67b8b 100644 --- a/src/hooks/queries/course/useDeleteCourse.ts +++ b/src/hooks/queries/course/useDeleteCourse.ts @@ -4,7 +4,7 @@ import { useErrorHandler } from 'react-error-boundary'; import { useRootStore } from '@educt/hooks/useRootStore'; import useAsync from '@educt/hooks/useAsync'; -type DeletedCourseDataType = Omit | undefined; +type DeletedCourseDataType = Omit; const useDeleteCourse = () => { const { courseStore } = useRootStore(); diff --git a/src/hooks/queries/course/useFetchCourse.ts b/src/hooks/queries/course/useFetchCourse.ts index 0e40234..73d4b8d 100644 --- a/src/hooks/queries/course/useFetchCourse.ts +++ b/src/hooks/queries/course/useFetchCourse.ts @@ -10,7 +10,7 @@ import { useEffect } from 'react'; import useAsync from '../../useAsync'; import { useRootStore } from '../../useRootStore'; -type CourseResultDataType = Omit | undefined; +type CourseResultDataType = Omit; const useFetchCourse = (id: string) => { const { diff --git a/src/hooks/queries/course/useSetCourseStatus.ts b/src/hooks/queries/course/useSetCourseStatus.ts index b644c13..0797056 100644 --- a/src/hooks/queries/course/useSetCourseStatus.ts +++ b/src/hooks/queries/course/useSetCourseStatus.ts @@ -7,11 +7,8 @@ import useAsync from '../../useAsync'; import { useRootStore } from '../../useRootStore'; import { useToast } from '@chakra-ui/toast'; -type SetCourseStatusResultDataType = {} | undefined; -export type SetCourseStatusFnType = ( - id: string, - newStatus: CourseStatusEnum -) => Promise; +type SetCourseStatusResultDataType = {}; +export type SetCourseStatusFnType = (id: string, newStatus: CourseStatusEnum) => Promise; const useSetCourseStatus = () => { const { courseStore } = useRootStore(); diff --git a/src/hooks/queries/course/useUpdateCourse.ts b/src/hooks/queries/course/useUpdateCourse.ts index 98e623d..fd5d872 100644 --- a/src/hooks/queries/course/useUpdateCourse.ts +++ b/src/hooks/queries/course/useUpdateCourse.ts @@ -5,7 +5,7 @@ import { UpdateCourseParamsType } from '@educt/types'; import useAsync from '../../useAsync'; import { ICourse } from '@educt/interfaces'; -type UpdatedCourseDataType = Omit | undefined; +type UpdatedCourseDataType = Omit; const useUpdateCourse = () => { const toast = useToast(); diff --git a/src/hooks/queries/index.ts b/src/hooks/queries/index.ts index 8c80970..937aeae 100644 --- a/src/hooks/queries/index.ts +++ b/src/hooks/queries/index.ts @@ -20,6 +20,9 @@ import { useSetCourseStatus } from './course/useSetCourseStatus'; /** * User */ +import { useCreateUser } from './user/useCreateUser'; +import { useDeleteUser } from './user/useDeleteUser'; +import { useUpdateUser } from './user/useUpdateUser'; import { useUpdateUserEmail } from './user/useUpdateUserEmail'; export { @@ -30,5 +33,8 @@ export { useFetchCourse, useSetCourseStatus, useDeleteLesson, + useCreateUser, + useDeleteUser, + useUpdateUser, useUpdateUserEmail, }; diff --git a/src/hooks/queries/lesson/useDeleteLesson.ts b/src/hooks/queries/lesson/useDeleteLesson.ts index 74fc45c..b747115 100644 --- a/src/hooks/queries/lesson/useDeleteLesson.ts +++ b/src/hooks/queries/lesson/useDeleteLesson.ts @@ -4,7 +4,7 @@ import { useErrorHandler } from 'react-error-boundary'; import useAsync from '../../useAsync'; import { useRootStore } from '../../useRootStore'; -type CreatedLessonDataType = ILesson | undefined; +type CreatedLessonDataType = ILesson; const useDeleteLesson = () => { const { diff --git a/src/hooks/queries/user/useCreateUser.ts b/src/hooks/queries/user/useCreateUser.ts new file mode 100644 index 0000000..9753f2c --- /dev/null +++ b/src/hooks/queries/user/useCreateUser.ts @@ -0,0 +1,43 @@ +import { useErrorHandler } from 'react-error-boundary'; +import { CreateUserParamsType, FetchUsersParamsType } from '@educt/types'; +import { useRootStore } from '@educt/hooks/useRootStore'; +import { useToast } from '@chakra-ui/toast'; +import { IUser } from '@educt/interfaces'; +import useAsync from '@educt/hooks/useAsync'; + +type CreatedUserDataType = IUser; + +const useCreateUser = () => { + const { userStore } = useRootStore(); + const toast = useToast(); + const handleError = useErrorHandler(); + + const createUser = async (data: CreateUserParamsType, paramsContext?: FetchUsersParamsType | undefined) => { + try { + const result = await userStore.createUser(data, paramsContext); + toast({ title: 'User created.', status: 'info' }); + return result.data; + } catch (error: any) { + if (error.response) { + switch (error.response.status) { + case 422: + toast({ title: `${error.response.data.errors[0].message}`, status: 'error' }); + break; + default: + toast({ title: `${error.message}`, status: 'error' }); + break; + } + } else { + handleError(error); + } + + return Promise.reject(error); + } + }; + + const { execute, ...state } = useAsync>(createUser); + + return { createUser: execute, ...state }; +}; + +export { useCreateUser }; diff --git a/src/hooks/queries/user/useDeleteUser.ts b/src/hooks/queries/user/useDeleteUser.ts new file mode 100644 index 0000000..6f19f81 --- /dev/null +++ b/src/hooks/queries/user/useDeleteUser.ts @@ -0,0 +1,36 @@ +import { useToast } from '@chakra-ui/toast'; +import useAsync from '@educt/hooks/useAsync'; +import { useRootStore } from '@educt/hooks/useRootStore'; +import { IUser } from '@educt/interfaces'; +import { FetchUsersParamsType } from '@educt/types'; +import { useErrorHandler } from 'react-error-boundary'; + +type DeletedUserDataType = IUser; + +const useDeleteUser = () => { + const { userStore } = useRootStore(); + const toast = useToast(); + const handleError = useErrorHandler(); + + const deleteUser = async (id: string, paramsContext?: FetchUsersParamsType | undefined) => { + try { + const result = await userStore.deleteUser(id, paramsContext); + toast({ title: 'User 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>(deleteUser); + + return { deleteUser: execute, ...state }; +}; + +export { useDeleteUser }; diff --git a/src/hooks/queries/user/useUpdateUser.ts b/src/hooks/queries/user/useUpdateUser.ts new file mode 100644 index 0000000..b4c575e --- /dev/null +++ b/src/hooks/queries/user/useUpdateUser.ts @@ -0,0 +1,43 @@ +import { useErrorHandler } from 'react-error-boundary'; +import { useToast } from '@chakra-ui/toast'; +import { useRootStore } from '@educt/hooks/useRootStore'; +import { UpdateUserParamsType } from '@educt/types'; +import { IUser } from '@educt/interfaces'; +import useAsync from '@educt/hooks/useAsync'; + +type UpdatedUserDataType = IUser; + +const useUpdateUser = () => { + const { userStore } = useRootStore(); + const toast = useToast(); + const handleError = useErrorHandler(); + + const updateUser = async (id: string, params: UpdateUserParamsType) => { + try { + const result = await userStore.updateUser(id, params); + toast({ title: 'User updated.', status: 'info' }); + return result.data; + } catch (error: any) { + if (error.response) { + switch (error.response.status) { + case 422: + toast({ title: `${error.response.data.errors[0].message}`, status: 'error' }); + break; + default: + toast({ title: `${error.message}`, status: 'error' }); + break; + } + } else { + handleError(error); + } + + return Promise.reject(error); + } + }; + + const { execute, ...state } = useAsync>(updateUser); + + return { updateUser: execute, ...state }; +}; + +export { useUpdateUser }; diff --git a/src/hooks/queries/user/useUpdateUserEmail.ts b/src/hooks/queries/user/useUpdateUserEmail.ts index 3b85d3a..d512946 100644 --- a/src/hooks/queries/user/useUpdateUserEmail.ts +++ b/src/hooks/queries/user/useUpdateUserEmail.ts @@ -3,7 +3,7 @@ import { useErrorHandler } from 'react-error-boundary'; import { useRootStore } from '@educt/hooks/useRootStore'; import useAsync from '@educt/hooks/useAsync'; -type ConfirmationResultDataType = { expired_seconds: number } | null; +type ConfirmationResultDataType = { expired_seconds: number }; const useUpdateUserEmail = () => { const { userStore } = useRootStore(); diff --git a/src/pages/courses/components/DeleteCourseDialog/DeleteCourseDialog.tsx b/src/pages/courses/components/DeleteCourseDialog/DeleteCourseDialog.tsx index 1f152b7..667bc5f 100644 --- a/src/pages/courses/components/DeleteCourseDialog/DeleteCourseDialog.tsx +++ b/src/pages/courses/components/DeleteCourseDialog/DeleteCourseDialog.tsx @@ -32,9 +32,13 @@ const DeleteCourseDialog: React.FC = ({ onClose, is const handleDelete = async (isConfirmed: boolean, id: string) => { if (isConfirmed) { - await deleteCourse(id) - .catch(error => console.error(error)) - .finally(() => setDeletingCourse(undefined)); + try { + await deleteCourse(id); + } catch (error: any) { + console.error(error); + } finally { + setDeletingCourse(undefined); + } } /** diff --git a/src/pages/profile/change-email/containers/ConfirmEmailContainer/ConfirmEmailContainer.tsx b/src/pages/profile/change-email/containers/ConfirmEmailContainer/ConfirmEmailContainer.tsx index 0d21847..5347493 100644 --- a/src/pages/profile/change-email/containers/ConfirmEmailContainer/ConfirmEmailContainer.tsx +++ b/src/pages/profile/change-email/containers/ConfirmEmailContainer/ConfirmEmailContainer.tsx @@ -93,13 +93,12 @@ const ConfirmEmailContainer: React.FC = ({ data const resendCodeHandler = async () => { const result = await sendConfirmationCode(data.newEmail); - if (result !== null) { - /** - * Reset timer state - */ - const { expired_seconds } = result; - setExpired(v => (v = expired_seconds)); - } + + /** + * Reset timer state + */ + const { expired_seconds } = result; + setExpired(v => (v = expired_seconds)); }; return ( diff --git a/src/pages/users/components/CreateUserForm/CreateUserForm.tsx b/src/pages/users/components/CreateUserForm/CreateUserForm.tsx index be7c710..ecc4e0e 100644 --- a/src/pages/users/components/CreateUserForm/CreateUserForm.tsx +++ b/src/pages/users/components/CreateUserForm/CreateUserForm.tsx @@ -31,13 +31,15 @@ import { SubmitHandler } from 'react-hook-form'; /** * Hooks */ -import { useContext, useState } from 'react'; -import useIsMountedRef from '@educt/hooks/useIsMountedRef'; +import { useContext } from 'react'; import { useDisclosure } from '@chakra-ui/hooks'; import { useForm } from 'react-hook-form'; import { useRootStore } from '@educt/hooks/useRootStore'; -import { useErrorHandler } from 'react-error-boundary'; -import { useToast } from '@chakra-ui/react'; + +/** + * Hooks + */ +import { useCreateUser } from '@educt/hooks/queries'; /** * Contexts @@ -63,6 +65,7 @@ type CreateUserInputType = { const CreateUserModal: React.FC = () => { const { userStore } = useRootStore(); const { searchingRole, search } = useContext(UsersPageContext); + const { createUser, isLoading } = useCreateUser(); const { isOpen, onOpen, onClose } = useDisclosure(); const { register, @@ -72,10 +75,6 @@ const CreateUserModal: React.FC = () => { } = useForm({ resolver: yupResolver(CreateUserSchema), }); - const [loading, setLoading] = useState(false); - const isMountedRef = useIsMountedRef(); - const toast = useToast(); - const handleError = useErrorHandler(); const { me } = userStore; @@ -84,17 +83,14 @@ const CreateUserModal: React.FC = () => { /** * Submit handler */ - // TODO: move to hook const onSubmit: SubmitHandler = async data => { try { - setLoading(true); - await userStore.createUser(data, { + await createUser(data, { page: userStore.pagination?.current_page, limit: userStore.pagination?.per_page, role: searchingRole, search, }); - toast({ title: 'User created.', status: 'info' }); /** * Clear form state @@ -106,19 +102,7 @@ const CreateUserModal: React.FC = () => { */ onClose(); } 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); - } - } finally { - if (isMountedRef.current) { - setLoading(false); - } + console.error(error); } }; @@ -195,7 +179,7 @@ const CreateUserModal: React.FC = () => { variant='outline' mr={3} colorScheme='blue' - isLoading={loading} + isLoading={isLoading} loadingText='Creating...' leftIcon={} > diff --git a/src/pages/users/components/DeleteUserDialog/DeleteUserDialog.tsx b/src/pages/users/components/DeleteUserDialog/DeleteUserDialog.tsx index df77ec0..54aee08 100644 --- a/src/pages/users/components/DeleteUserDialog/DeleteUserDialog.tsx +++ b/src/pages/users/components/DeleteUserDialog/DeleteUserDialog.tsx @@ -18,10 +18,9 @@ import { UsersPageContext } from '@educt/contexts'; /** * Hooks */ -import { useContext, useState } from 'react'; +import { useContext } from 'react'; import { useRootStore } from '@educt/hooks/useRootStore'; -import { useToast } from '@chakra-ui/react'; -import { useErrorHandler } from 'react-error-boundary'; +import { useDeleteUser } from '@educt/hooks/queries'; type DeleteUserDialogPropsType = { user: IUser; @@ -32,9 +31,7 @@ type DeleteUserDialogPropsType = { const DeleteUserDialog: React.FC = ({ isOpen, onClose, user }) => { const { userStore } = useRootStore(); const { setDeletingUser, searchingRole, searchingPage, search } = useContext(UsersPageContext); - const [isLoading, setIsLoading] = useState(false); - const handleError = useErrorHandler(); - const toast = useToast(); + const { deleteUser, isLoading } = useDeleteUser(); const { pagination } = userStore; @@ -44,26 +41,18 @@ const DeleteUserDialog: React.FC = ({ isOpen, onClose * @param isConfirmed Is dialog confirmed * @param id User id */ - // TODO: move to hook const deleteUserHandler = async (isConfirmed: boolean, id: string) => { if (isConfirmed) { try { - setIsLoading(true); - await userStore.deleteUser(id, { + await deleteUser(id, { page: searchingPage, limit: pagination?.per_page, role: searchingRole, search, }); - toast({ title: 'User deleted.', status: 'info' }); } catch (error: any) { - if (error.response) { - toast({ title: error.message, status: 'error' }); - } else { - handleError(error); - } + console.error(error); } finally { - setIsLoading(false); setDeletingUser(undefined); } } diff --git a/src/pages/users/components/EditUserForm/EditUserForm.tsx b/src/pages/users/components/EditUserForm/EditUserForm.tsx index bdf7265..198b4b7 100644 --- a/src/pages/users/components/EditUserForm/EditUserForm.tsx +++ b/src/pages/users/components/EditUserForm/EditUserForm.tsx @@ -1,4 +1,5 @@ -import React, { useContext, useState } from 'react'; +import React, { useContext } from 'react'; +import * as helpers from '@educt/helpers'; import { Modal, ModalOverlay, @@ -25,7 +26,7 @@ import { yupResolver } from '@hookform/resolvers/yup'; /** * Types */ -import { IMe, IUser } from '@educt/interfaces'; +import { IUser } from '@educt/interfaces'; import { UserRoleEnum } from '@educt/enums'; import { SubmitHandler } from 'react-hook-form'; @@ -34,9 +35,6 @@ import { SubmitHandler } from 'react-hook-form'; */ import { useForm } from 'react-hook-form'; import { useRootStore } from '@educt/hooks/useRootStore'; -import { useErrorHandler } from 'react-error-boundary'; -import { useToast } from '@chakra-ui/react'; -import useIsMountedRef from '@educt/hooks/useIsMountedRef'; /** * Contexts @@ -47,6 +45,7 @@ import { UsersPageContext } from '@educt/contexts'; * Schema */ import UpdateUserSchema from './EditUserForm.validator'; +import { useUpdateUser } from '@educt/hooks/queries'; type UpdateUserFormPropsType = { user: IUser; @@ -66,7 +65,7 @@ type UpdateUserInputType = { const UpdateUserForm: React.FC = ({ user, onClose, isOpen }) => { const { userStore } = useRootStore(); const { setEditingUser } = useContext(UsersPageContext); - const [isLoading, setIsLoading] = useState(false); + const { updateUser, isLoading } = useUpdateUser(); const { register, handleSubmit, @@ -83,9 +82,6 @@ const UpdateUserForm: React.FC = ({ user, onClose, isOp password: '', }, }); - const isMountedRef = useIsMountedRef(); - const toast = useToast(); - const handleError = useErrorHandler(); const { me } = userStore; @@ -94,28 +90,13 @@ const UpdateUserForm: React.FC = ({ user, onClose, isOp /** * Submit handler */ - // TODO: move to hook const onSubmit: SubmitHandler = async data => { try { - setIsLoading(true); - const params = Object.fromEntries(Object.keys(dirtyFields).map(k => [k, data[k as keyof UpdateUserInputType]])); - await userStore.updateUser(user.id, params); - toast({ title: 'User updated.', status: 'info' }); + const params = helpers.getDirtyFields(dirtyFields, data); + await updateUser(user.id, params); onCloseModal(); } 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); - } - } finally { - if (isMountedRef.current) { - setIsLoading(false); - } + console.error(error); } }; diff --git a/src/stores/pages/EditCourseStore.ts b/src/stores/pages/EditCourseStore.ts index 7c65340..37c0869 100644 --- a/src/stores/pages/EditCourseStore.ts +++ b/src/stores/pages/EditCourseStore.ts @@ -8,7 +8,7 @@ import { CourseServiceInstance, LessonServiceInstance } from '@educt/services'; export default class EditCourseStore { public pageStore: PageStore; - public course: Omit | undefined; + public course: Omit | null = null; public deletingLesson: ILesson | undefined;