From 947c9b92e1045d5d25e2156248d6f6ef1d53b7cc Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Mon, 29 Nov 2021 22:57:04 +0300 Subject: [PATCH 01/87] feat: page store & deleting lesson via dialog --- .../DeleteLessonDialog/DeleteLessonDialog.tsx | 55 +++++++++++++ .../Dialogs/DeleteLessonDialog/index.ts | 3 + src/hooks/queries/index.ts | 3 + src/hooks/queries/useDeleteLesson.ts | 33 ++++++++ src/hooks/useFetchCourseQuery.ts | 6 +- src/hooks/useLoginQuery.ts | 2 +- .../edit/components/LessonList/LessonList.tsx | 54 +++++++++--- src/pages/courses/edit/edit.tsx | 13 ++- src/pages/users/users.tsx | 2 +- src/services/LessonService.ts | 13 ++- src/stores/PageStore.ts | 21 +++++ src/stores/RootStore.ts | 8 +- src/stores/pages/EditCourseStore.ts | 82 +++++++++++++++++++ 13 files changed, 272 insertions(+), 23 deletions(-) create mode 100644 src/components/Dialogs/DeleteLessonDialog/DeleteLessonDialog.tsx create mode 100644 src/components/Dialogs/DeleteLessonDialog/index.ts create mode 100644 src/hooks/queries/index.ts create mode 100644 src/hooks/queries/useDeleteLesson.ts create mode 100644 src/stores/PageStore.ts create mode 100644 src/stores/pages/EditCourseStore.ts 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; + } +} From b5a99802dc8b03ddc3e9a20058eada3a796d6ba4 Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Mon, 29 Nov 2021 22:59:54 +0300 Subject: [PATCH 02/87] fixed: dialog msg --- .../Dialogs/DeleteLessonDialog/DeleteLessonDialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Dialogs/DeleteLessonDialog/DeleteLessonDialog.tsx b/src/components/Dialogs/DeleteLessonDialog/DeleteLessonDialog.tsx index 51a12a8..3b5b6de 100644 --- a/src/components/Dialogs/DeleteLessonDialog/DeleteLessonDialog.tsx +++ b/src/components/Dialogs/DeleteLessonDialog/DeleteLessonDialog.tsx @@ -46,7 +46,7 @@ const DeleteLessonDialog: React.FC = ({ isOpen, onC 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.`} + message={`Lesson with will be removed from the course. Attached materials will also be removed.`} confirmMessage='Delete' /> ); From 2fa555b8befc8558f6467df81a16a602d29ed307 Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Tue, 30 Nov 2021 23:53:00 +0300 Subject: [PATCH 03/87] refactor: services into singleton --- .../Forms/CourseForm/CourseForm.tsx | 14 +++++----- .../Forms/CourseForm/CreateCourseForm.tsx | 12 ++++---- .../Forms/CourseForm/EditCourseForm.tsx | 14 ++++++---- .../edit/components/LessonList/LessonList.tsx | 8 ++++-- src/services/ApiService.ts | 21 ++++---------- src/services/AuthService.ts | 25 +++++++++++++---- src/services/CategoryService.ts | 5 +++- src/services/CourseService.ts | 7 +++-- src/services/LessonService.ts | 7 +++-- src/services/UserService.ts | 7 +++-- src/services/index.ts | 15 ++++++++++ src/stores/AuthStore.ts | 12 +++----- src/stores/CategoryStore.ts | 10 ++----- src/stores/CourseStore.ts | 16 ++++------- src/stores/PageStore.ts | 5 ++-- src/stores/RootStore.ts | 28 ++++--------------- src/stores/UserStore.ts | 26 ++++++++--------- src/stores/pages/EditCourseStore.ts | 16 ++++------- 18 files changed, 124 insertions(+), 124 deletions(-) create mode 100644 src/services/index.ts diff --git a/src/components/Forms/CourseForm/CourseForm.tsx b/src/components/Forms/CourseForm/CourseForm.tsx index 55e077b..eaf445a 100644 --- a/src/components/Forms/CourseForm/CourseForm.tsx +++ b/src/components/Forms/CourseForm/CourseForm.tsx @@ -20,9 +20,13 @@ import FileSelect from '@educt/components/FileSelect'; * Hooks */ import { useToast } from '@chakra-ui/toast'; -import { useRootStore } from '@educt/hooks/useRootStore'; import { useErrorHandler } from 'react-error-boundary'; +/** + * Services + */ +import { CategoryServiceInstance, UserServiceInstance } from '@educt/services'; + export type InputFields = { title: string; image?: File | undefined; @@ -46,10 +50,6 @@ const CourseForm: React.FC = ({ onSubmit, buttonLabel, isLo formState: { errors, isDirty }, } = reactHookForm; - const { - userStore: { userService }, - categoryStore: { categoryService }, - } = useRootStore(); const handleError = useErrorHandler(); const toast = useToast(); @@ -58,7 +58,7 @@ const CourseForm: React.FC = ({ onSubmit, buttonLabel, isLo */ const loadUsersOptions = async (): Promise => { try { - const users = await userService.fetchAll({ limit: 12, role: UserRoleEnum.TEACHER }); + const users = await UserServiceInstance.fetchAll({ limit: 12, role: UserRoleEnum.TEACHER }); return users.data.map(user => ({ label: user.fullname, @@ -78,7 +78,7 @@ const CourseForm: React.FC = ({ onSubmit, buttonLabel, isLo */ const loadCategoriesOptions = async (): Promise => { try { - const categories = await categoryService.fetchAll(); + const categories = await CategoryServiceInstance.fetchAll(); return categories.data.map(category => ({ label: category.title, value: category.id, diff --git a/src/components/Forms/CourseForm/CreateCourseForm.tsx b/src/components/Forms/CourseForm/CreateCourseForm.tsx index e98400c..b2350d8 100644 --- a/src/components/Forms/CourseForm/CreateCourseForm.tsx +++ b/src/components/Forms/CourseForm/CreateCourseForm.tsx @@ -21,10 +21,14 @@ import { useState } from 'react'; import { useHistory } from 'react-router'; import { useForm } from 'react-hook-form'; import { useToast } from '@chakra-ui/toast'; -import { useRootStore } from '@educt/hooks/useRootStore'; import useIsMountedRef from '@educt/hooks/useIsMountedRef'; import { useErrorHandler } from 'react-error-boundary'; +/** + * Services + */ +import { CourseServiceInstance } from '@educt/services'; + /** * Schema */ @@ -34,9 +38,6 @@ type CreateFormCoursePropsType = {}; const CreateFormCourse: React.FC = () => { const [isLoading, setIsLoading] = useState(false); - const { - courseStore: { courseService }, - } = useRootStore(); const form = useForm({ resolver: yupResolver(CourseFormSchema) }); const isMountedRef = useIsMountedRef(); const history = useHistory(); @@ -46,10 +47,11 @@ const CreateFormCourse: React.FC = () => { /** * Submit handler */ + // TODO: move request to useCreateCourse hook const onSubmit: SubmitHandler = async data => { try { setIsLoading(true); - const course = await courseService.create({ + const course = await CourseServiceInstance.create({ title: data.title, description: data.description, teacher_id: data.teacher_id, diff --git a/src/components/Forms/CourseForm/EditCourseForm.tsx b/src/components/Forms/CourseForm/EditCourseForm.tsx index 5dfb45e..063872c 100644 --- a/src/components/Forms/CourseForm/EditCourseForm.tsx +++ b/src/components/Forms/CourseForm/EditCourseForm.tsx @@ -1,12 +1,12 @@ import React from 'react'; import { Box } from '@chakra-ui/layout'; import { yupResolver } from '@hookform/resolvers/yup'; -import { SubmitHandler } from 'react-hook-form'; /** * Types */ import { InputFields } from './CourseForm'; +import { SubmitHandler } from 'react-hook-form'; /** * Components @@ -19,10 +19,14 @@ import CourseForm from './CourseForm'; import { useState } from 'react'; import { useParams } from 'react-router'; import { useForm } from 'react-hook-form'; -import { useRootStore } from '@educt/hooks/useRootStore'; import { useErrorHandler } from 'react-error-boundary'; import { useToast } from '@chakra-ui/toast'; +/** + * Services + */ +import { CourseServiceInstance } from '@educt/services'; + /** * Schema */ @@ -42,9 +46,6 @@ type EditFormCoursePropsType = { const EditFormCourse: React.FC = ({ defaultValues }) => { const [isLoading, setIsLoading] = useState(false); - const { - courseStore: { courseService }, - } = useRootStore(); const form = useForm({ resolver: yupResolver(CourseFormSchema), defaultValues: { @@ -62,10 +63,11 @@ const EditFormCourse: React.FC = ({ defaultValues }) => /** * Submit handler */ + // TODO move request to useUpdateCourse hook const onSubmit: SubmitHandler = async data => { try { setIsLoading(true); - const course = await courseService.update(id, { + const course = await CourseServiceInstance.update(id, { title: data.title, description: data.description, teacher_id: data.teacher_id, diff --git a/src/pages/courses/edit/components/LessonList/LessonList.tsx b/src/pages/courses/edit/components/LessonList/LessonList.tsx index a5cb94b..b8cb377 100644 --- a/src/pages/courses/edit/components/LessonList/LessonList.tsx +++ b/src/pages/courses/edit/components/LessonList/LessonList.tsx @@ -31,6 +31,11 @@ import { useDisclosure } from '@chakra-ui/hooks'; */ import { useErrorHandler } from 'react-error-boundary'; +/** + * Services + */ +import { LessonServiceInstance } from '@educt/services'; + type LessonListPropsType = { course: Omit; }; @@ -50,7 +55,6 @@ const CreateLessonButton: React.FC<{ id: string }> = ({ id }) => { const LessonList: React.FC = ({ course }) => { const history = useHistory(); const { - lessonService, pageStore: { editCourseStore }, } = useRootStore(); const { onOpen: onOpenDeleteDialog, onClose: onCloseDeleteDialog, isOpen: isOpenDeleteDialog } = useDisclosure(); @@ -66,7 +70,7 @@ const LessonList: React.FC = ({ course }) => { const handleChangeOrder = async (ids: string[]) => { try { - const data = await lessonService.saveOrder(ids); + const data = await LessonServiceInstance.saveOrder(ids); return data; } catch (error: any) { if (error.response) { diff --git a/src/services/ApiService.ts b/src/services/ApiService.ts index 26a2938..8d41722 100644 --- a/src/services/ApiService.ts +++ b/src/services/ApiService.ts @@ -1,15 +1,12 @@ import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'; -import RootStore from '@educt/stores/RootStore'; - -export default class ApiService { - public root: RootStore; +import apiConfig from '@educt/api.config'; +class ApiService { public api: AxiosInstance; public config: AxiosRequestConfig | undefined; - constructor(root: RootStore, config?: AxiosRequestConfig | undefined) { - this.root = root; + constructor(config?: AxiosRequestConfig | undefined) { this.config = config; this.api = axios.create(this.config); this.setupInterceptors(); @@ -20,17 +17,9 @@ export default class ApiService { * Response */ this.api.interceptors.response.use(undefined, error => { - if (error.response) { - /** - * Unauthorized Error 401 - */ - if (error.response.status === 401) { - // this.root.userStore.me = null; - // this.root.authStore.setIsLoggedIn(false); - // Cookies.remove('logged_in'); - } - } return Promise.reject(error); }); } } + +export const ApiServiceInstance = new ApiService(apiConfig); diff --git a/src/services/AuthService.ts b/src/services/AuthService.ts index f91f329..a05988c 100644 --- a/src/services/AuthService.ts +++ b/src/services/AuthService.ts @@ -1,7 +1,20 @@ import { IApiRespose, IToken } from '@educt/interfaces'; import { AxiosInstance } from 'axios'; +import { ApiServiceInstance } from '.'; -export default class AuthService { +export interface AuthServiceImpl { + /** + * Login user with login and passwors + */ + requestLogin(login: string, password: string): Promise>; + + /** + * Logout user + */ + requestLogout(): Promise>; +} + +class AuthService implements AuthServiceImpl { public api: AxiosInstance; constructor(api: AxiosInstance) { @@ -13,9 +26,9 @@ export default class AuthService { * * @param login User login * @param password User password - * @returns Login result data + * @returns Token result data */ - public async requestLogin(login: string, password: string): Promise> { + public async requestLogin(login: string, password: string) { const result = await this.api.post('v1/auth/login', { login, password, @@ -26,10 +39,12 @@ export default class AuthService { /** * Revoke token * - * @returns Any + * @returns Empty obj */ - public async requestLogout(): Promise> { + public async requestLogout() { const result = await this.api.post('v1/auth/logout'); return result.data; } } + +export const AuthServiceInstance = new AuthService(ApiServiceInstance.api); diff --git a/src/services/CategoryService.ts b/src/services/CategoryService.ts index de6a789..2092e8f 100644 --- a/src/services/CategoryService.ts +++ b/src/services/CategoryService.ts @@ -1,7 +1,8 @@ import { AxiosInstance } from 'axios'; import { IApiRespose, ICategory } from '@educt/interfaces'; +import { ApiServiceInstance } from '.'; -export default class CategoryService { +class CategoryService { public api: AxiosInstance; constructor(api: AxiosInstance) { @@ -18,3 +19,5 @@ export default class CategoryService { return result.data; } } + +export const CategoryServiceInstance = new CategoryService(ApiServiceInstance.api); diff --git a/src/services/CourseService.ts b/src/services/CourseService.ts index dd080d1..54aeb5d 100644 --- a/src/services/CourseService.ts +++ b/src/services/CourseService.ts @@ -1,10 +1,11 @@ import { AxiosInstance } from 'axios'; +import { ApiServiceInstance } from '.'; import * as helpers from '@educt/helpers'; import { IApiRespose, ICourse } from '@educt/interfaces'; -import { CreateCourseParamsType, FetchCoursesParams, UpdateCourseParamsType } from '@educt/types'; import { CourseStatusEnum } from '@educt/enums'; +import { CreateCourseParamsType, FetchCoursesParams, UpdateCourseParamsType } from '@educt/types'; -export class CourseService { +class CourseService { public api: AxiosInstance; constructor(api: AxiosInstance) { @@ -96,3 +97,5 @@ export class CourseService { return result.data; } } + +export const CourseServiceInstance = new CourseService(ApiServiceInstance.api); diff --git a/src/services/LessonService.ts b/src/services/LessonService.ts index 2c19740..9c84e83 100644 --- a/src/services/LessonService.ts +++ b/src/services/LessonService.ts @@ -1,7 +1,8 @@ -import { IApiRespose, ILesson } from '@educt/interfaces'; import { AxiosInstance } from 'axios'; +import { IApiRespose, ILesson } from '@educt/interfaces'; +import { ApiServiceInstance } from './ApiService'; -export default class LessonService { +class LessonService { public api: AxiosInstance; constructor(api: AxiosInstance) { @@ -32,3 +33,5 @@ export default class LessonService { return result.data; } } + +export const LessonServiceInstance = new LessonService(ApiServiceInstance.api); diff --git a/src/services/UserService.ts b/src/services/UserService.ts index 97a30fd..16ab024 100644 --- a/src/services/UserService.ts +++ b/src/services/UserService.ts @@ -1,9 +1,10 @@ import { AxiosInstance } from 'axios'; -import { IApiRespose, IMe, IUser, IUserContacts } from '@educt/interfaces'; +import { ApiServiceInstance } from '.'; import * as helpers from '@educt/helpers'; +import { IApiRespose, IMe, IUser, IUserContacts } from '@educt/interfaces'; import { CreateUserParamsType, FetchUsersParamsType, UpdateUserParamsType } from '@educt/types'; -export default class UserService { +class UserService { public api: AxiosInstance; constructor(api: AxiosInstance) { @@ -135,3 +136,5 @@ export default class UserService { return result.data; } } + +export const UserServiceInstance = new UserService(ApiServiceInstance.api); diff --git a/src/services/index.ts b/src/services/index.ts new file mode 100644 index 0000000..83b2aeb --- /dev/null +++ b/src/services/index.ts @@ -0,0 +1,15 @@ +import { ApiServiceInstance } from './ApiService'; +import { AuthServiceInstance } from './AuthService'; +import { CategoryServiceInstance } from './CategoryService'; +import { CourseServiceInstance } from './CourseService'; +import { LessonServiceInstance } from './LessonService'; +import { UserServiceInstance } from './UserService'; + +export { + ApiServiceInstance, + AuthServiceInstance, + CategoryServiceInstance, + CourseServiceInstance, + LessonServiceInstance, + UserServiceInstance, +}; diff --git a/src/stores/AuthStore.ts b/src/stores/AuthStore.ts index 2bbfbe5..c0baa7b 100644 --- a/src/stores/AuthStore.ts +++ b/src/stores/AuthStore.ts @@ -1,11 +1,10 @@ import { makeAutoObservable } from 'mobx'; -import { AxiosInstance } from 'axios'; import Cookies from 'js-cookie'; /** * Services */ -import AuthService from '@educt/services/AuthService'; +import { AuthServiceInstance } from '@educt/services'; /** * Stores @@ -15,13 +14,10 @@ import RootStore from './RootStore'; export default class AuthStore { public root: RootStore; - public authService: AuthService; - public isLoggedIn: boolean = !!Cookies.get('logged_in'); - constructor(root: RootStore, api: AxiosInstance) { + constructor(root: RootStore) { this.root = root; - this.authService = new AuthService(api); makeAutoObservable(this); } @@ -31,7 +27,7 @@ export default class AuthStore { public async login(login: string, password: string) { try { - const result = await this.authService.requestLogin(login, password); + const result = await AuthServiceInstance.requestLogin(login, password); this.setIsLoggedIn(true); Cookies.set('logged_in', 'true'); @@ -44,7 +40,7 @@ export default class AuthStore { public async logout() { try { - const result = await this.authService.requestLogout(); + const result = await AuthServiceInstance.requestLogout(); this.setIsLoggedIn(false); this.root.userStore.reset(); diff --git a/src/stores/CategoryStore.ts b/src/stores/CategoryStore.ts index 7016b0e..e3b3ca4 100644 --- a/src/stores/CategoryStore.ts +++ b/src/stores/CategoryStore.ts @@ -1,4 +1,3 @@ -import { AxiosInstance } from 'axios'; import { makeAutoObservable, runInAction } from 'mobx'; /** @@ -9,7 +8,7 @@ import { ICategory } from '@educt/interfaces'; /** * Services */ -import CategoryService from '@educt/services/CategoryService'; +import { CategoryServiceInstance } from '@educt/services'; /** * Stores @@ -19,15 +18,12 @@ import RootStore from './RootStore'; export default class CategoryStore { public root: RootStore; - public categoryService: CategoryService; - public categories: ICategory[] | null = null; public isLoading: boolean = false; - constructor(root: RootStore, api: AxiosInstance) { + constructor(root: RootStore) { this.root = root; - this.categoryService = new CategoryService(api); makeAutoObservable(this); } @@ -38,7 +34,7 @@ export default class CategoryStore { public async loadCategories() { try { this.setLoading(true); - const result = await this.categoryService.fetchAll(); + const result = await CategoryServiceInstance.fetchAll(); runInAction(() => { this.categories = result.data; diff --git a/src/stores/CourseStore.ts b/src/stores/CourseStore.ts index bd0ca02..8929afd 100644 --- a/src/stores/CourseStore.ts +++ b/src/stores/CourseStore.ts @@ -1,4 +1,3 @@ -import { AxiosInstance } from 'axios'; import { makeAutoObservable, runInAction } from 'mobx'; /** @@ -6,30 +5,27 @@ import { makeAutoObservable, runInAction } from 'mobx'; */ import { FetchCoursesParams } from '@educt/types'; import { ICourse } from '@educt/interfaces'; +import { CourseStatusEnum } from '@educt/enums'; /** * Services */ -import { CourseService } from '@educt/services/CourseService'; +import { CourseServiceInstance } from '@educt/services'; /** * Stores */ import RootStore from './RootStore'; -import { CourseStatusEnum } from '@educt/enums'; export default class CourseStore { public root: RootStore; - public courseService: CourseService; - public courses: Omit[] | null = null; public isLoading: boolean = false; - constructor(root: RootStore, api: AxiosInstance) { + constructor(root: RootStore) { this.root = root; - this.courseService = new CourseService(api); makeAutoObservable(this); } @@ -40,7 +36,7 @@ export default class CourseStore { public async loadCourses(params?: FetchCoursesParams) { try { this.setLoading(true); - const result = await this.courseService.fetchAll(params); + const result = await CourseServiceInstance.fetchAll(params); /** * Load courses in store @@ -59,7 +55,7 @@ export default class CourseStore { public async deleteCourse(id: string) { try { - const result = await this.courseService.delete(id); + const result = await CourseServiceInstance.delete(id); /** * Remove deleted course form store @@ -97,7 +93,7 @@ export default class CourseStore { /** * Make request on update course status */ - const result = await this.courseService.setStatus(id, status); + const result = await CourseServiceInstance.setStatus(id, status); return result; } catch (error: any) { /** diff --git a/src/stores/PageStore.ts b/src/stores/PageStore.ts index 8c11bc2..4450700 100644 --- a/src/stores/PageStore.ts +++ b/src/stores/PageStore.ts @@ -1,4 +1,3 @@ -import { AxiosInstance } from 'axios'; import { makeAutoObservable } from 'mobx'; import EditCourseStore from './pages/EditCourseStore'; import RootStore from './RootStore'; @@ -8,13 +7,13 @@ export default class PageStore { public editCourseStore: EditCourseStore; - constructor(root: RootStore, api: AxiosInstance) { + constructor(root: RootStore) { this.root = root; /** * Pages */ - this.editCourseStore = new EditCourseStore(this, api); + this.editCourseStore = new EditCourseStore(this); makeAutoObservable(this); } diff --git a/src/stores/RootStore.ts b/src/stores/RootStore.ts index f240d4b..eb8d077 100644 --- a/src/stores/RootStore.ts +++ b/src/stores/RootStore.ts @@ -1,11 +1,5 @@ -import apiConfig from '@educt/api.config'; import { makeAutoObservable } from 'mobx'; -/** - * Services - */ -import ApiService from '@educt/services/ApiService'; - /** * Stores */ @@ -14,7 +8,6 @@ import UserStore from './UserStore'; 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 { @@ -30,27 +23,16 @@ export default class RootStore { public userStore: UserStore; - public apiService: ApiService; - - public lessonService: LessonService; - constructor() { - /** - * 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.authStore = new AuthStore(this); + this.categoryStore = new CategoryStore(this); + this.courseStore = new CourseStore(this); + this.pageStore = new PageStore(this); this.uiStore = new UIStore(this); - this.authStore = new AuthStore(this, this.apiService.api); - this.userStore = new UserStore(this, this.apiService.api); + this.userStore = new UserStore(this); makeAutoObservable(this); } diff --git a/src/stores/UserStore.ts b/src/stores/UserStore.ts index 5121b3f..6f4713c 100644 --- a/src/stores/UserStore.ts +++ b/src/stores/UserStore.ts @@ -1,4 +1,3 @@ -import { AxiosInstance } from 'axios'; import { makeAutoObservable, runInAction } from 'mobx'; import * as helpers from '@educt/helpers'; @@ -12,7 +11,7 @@ import { UserRoleEnum } from '@educt/enums'; /** * Services */ -import UserService from '@educt/services/UserService'; +import { UserServiceInstance } from '@educt/services'; /** * Stores @@ -22,8 +21,6 @@ import RootStore from './RootStore'; export default class UserStore { public root: RootStore; - public userService: UserService; - public me: IMe | null = null; public users: IUser[] | null = null; @@ -32,9 +29,8 @@ export default class UserStore { public isLoading: boolean = false; - constructor(root: RootStore, api: AxiosInstance) { + constructor(root: RootStore) { this.root = root; - this.userService = new UserService(api); makeAutoObservable(this); } @@ -45,7 +41,7 @@ export default class UserStore { public async loadUsersData(params?: FetchUsersParamsType) { try { this.setLoading(true); - const result = await this.userService.fetchAll(params); + const result = await UserServiceInstance.fetchAll(params); runInAction(() => { this.users = result.data; @@ -62,7 +58,7 @@ export default class UserStore { public async loadCurrentUserData() { try { - const result = await this.userService.fetchMe(); + const result = await UserServiceInstance.fetchMe(); const { data: { id, first_name, last_name, fullname, email, roles, contacts, courses }, } = result; @@ -91,7 +87,7 @@ export default class UserStore { public async createUser(data: CreateUserParamsType, paramsContext?: FetchUsersParamsType) { try { - const result = await this.userService.create(data); + const result = await UserServiceInstance.create(data); if (this.users !== null) { /** @@ -108,7 +104,7 @@ export default class UserStore { public async updateUser(id: string, params: UpdateUserParamsType) { try { - const result = await this.userService.update(id, params); + const result = await UserServiceInstance.update(id, params); runInAction(() => { if (this.users !== null) { @@ -134,7 +130,7 @@ export default class UserStore { */ public async deleteUser(id: string, paramsContext?: FetchUsersParamsType) { try { - const result = await this.userService.delete(id); + const result = await UserServiceInstance.delete(id); if (this.users !== null) { /** @@ -157,7 +153,7 @@ export default class UserStore { */ public async updateCurrentUserContacts(data: IUserContacts) { try { - const result = await this.userService.updateContacts(data); + const result = await UserServiceInstance.updateContacts(data); runInAction(() => { if (this.me) { @@ -180,7 +176,7 @@ export default class UserStore { */ public async updateCurrentUserPassword(oldPassword: string, newPassword: string) { try { - const result = await this.userService.updatePassword(oldPassword, newPassword); + const result = await UserServiceInstance.updatePassword(oldPassword, newPassword); return result; } catch (error: any) { throw error; @@ -195,7 +191,7 @@ export default class UserStore { */ public async updateCurrentUserEmail(email: string) { try { - const result = await this.userService.updateEmail(email); + const result = await UserServiceInstance.updateEmail(email); return result; } catch (error: any) { throw error; @@ -211,7 +207,7 @@ export default class UserStore { */ public async updateCurrentUserEmailConfirm(email: string, confirmationCode: string) { try { - const result = await this.userService.updateEmailConfirm(email, confirmationCode); + const result = await UserServiceInstance.updateEmailConfirm(email, confirmationCode); runInAction(() => { if (this.me) { this.me.email = result.data.email; diff --git a/src/stores/pages/EditCourseStore.ts b/src/stores/pages/EditCourseStore.ts index d920054..7c65340 100644 --- a/src/stores/pages/EditCourseStore.ts +++ b/src/stores/pages/EditCourseStore.ts @@ -1,11 +1,10 @@ +import { makeAutoObservable, runInAction } from 'mobx'; 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'; +import { CourseServiceInstance, LessonServiceInstance } from '@educt/services'; + export default class EditCourseStore { public pageStore: PageStore; @@ -13,11 +12,8 @@ export default class EditCourseStore { public deletingLesson: ILesson | undefined; - private courseService: CourseService; - - constructor(pageStore: PageStore, api: AxiosInstance) { + constructor(pageStore: PageStore) { this.pageStore = pageStore; - this.courseService = new CourseService(api); makeAutoObservable(this); } @@ -30,7 +26,7 @@ export default class EditCourseStore { */ public async loadCourseById(id: string) { try { - const result = await this.courseService.fetchById(id); + const result = await CourseServiceInstance.fetchById(id); runInAction(() => { this.course = result.data; @@ -50,7 +46,7 @@ export default class EditCourseStore { */ public async deleteLessonById(id: string) { try { - const result = await this.pageStore.root.lessonService.deleteLesson(id); + const result = await LessonServiceInstance.deleteLesson(id); runInAction(() => { if (this.course) { From dd86228cabf90da4534aa414274aa72b3c401c54 Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Wed, 1 Dec 2021 23:58:30 +0300 Subject: [PATCH 04/87] refactor: refactored course & lesson query hooks --- package-lock.json | 6 +-- package.json | 2 +- .../Forms/CourseForm/CreateCourseForm.tsx | 39 +++----------- .../Forms/CourseForm/EditCourseForm.tsx | 54 ++++++------------- src/hooks/queries/course/useCreateCourse.ts | 49 +++++++++++++++++ src/hooks/queries/course/useDeleteCourse.ts | 35 ++++++++++++ src/hooks/queries/course/useUpdateCourse.ts | 41 ++++++++++++++ src/hooks/queries/index.ts | 7 ++- .../queries/{ => lesson}/useDeleteLesson.ts | 10 ++-- .../DeleteCourseDialog/DeleteCourseDialog.tsx | 53 +++++------------- .../UpdateUserContactsForm.tsx | 2 +- 11 files changed, 178 insertions(+), 120 deletions(-) create mode 100644 src/hooks/queries/course/useCreateCourse.ts create mode 100644 src/hooks/queries/course/useDeleteCourse.ts create mode 100644 src/hooks/queries/course/useUpdateCourse.ts rename src/hooks/queries/{ => lesson}/useDeleteLesson.ts (72%) diff --git a/package-lock.json b/package-lock.json index 41e8227..5038596 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2552,9 +2552,9 @@ "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" }, "typescript": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz", - "integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==", + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.2.tgz", + "integrity": "sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==", "dev": true }, "use-callback-ref": { diff --git a/package.json b/package.json index a7d06b4..95776c8 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "@types/validator": "^13.6.3", "@vitejs/plugin-react": "^1.0.0", "rollup-plugin-visualizer": "^5.5.2", - "typescript": "^4.3.2", + "typescript": "^4.5.2", "vite": "^2.6.4" } } diff --git a/src/components/Forms/CourseForm/CreateCourseForm.tsx b/src/components/Forms/CourseForm/CreateCourseForm.tsx index b2350d8..dbf4fd6 100644 --- a/src/components/Forms/CourseForm/CreateCourseForm.tsx +++ b/src/components/Forms/CourseForm/CreateCourseForm.tsx @@ -17,17 +17,9 @@ import CourseForm from './CourseForm'; /** * Hooks */ -import { useState } from 'react'; import { useHistory } from 'react-router'; import { useForm } from 'react-hook-form'; -import { useToast } from '@chakra-ui/toast'; -import useIsMountedRef from '@educt/hooks/useIsMountedRef'; -import { useErrorHandler } from 'react-error-boundary'; - -/** - * Services - */ -import { CourseServiceInstance } from '@educt/services'; +import { useCreateCourse } from '@educt/hooks/queries'; /** * Schema @@ -37,21 +29,16 @@ import CourseFormSchema from './CourseForm.validator'; type CreateFormCoursePropsType = {}; const CreateFormCourse: React.FC = () => { - const [isLoading, setIsLoading] = useState(false); const form = useForm({ resolver: yupResolver(CourseFormSchema) }); - const isMountedRef = useIsMountedRef(); + const { createCourse, isLoading } = useCreateCourse(); const history = useHistory(); - const toast = useToast(); - const handleError = useErrorHandler(); /** * Submit handler */ - // TODO: move request to useCreateCourse hook const onSubmit: SubmitHandler = async data => { try { - setIsLoading(true); - const course = await CourseServiceInstance.create({ + const course = await createCourse({ title: data.title, description: data.description, teacher_id: data.teacher_id, @@ -59,22 +46,10 @@ const CreateFormCourse: React.FC = () => { image: data.image, status: CourseStatusEnum.DRAFT, }); - toast({ title: `Course successfully created.`, status: 'success' }); - history.push(`/courses/edit/${course.data.id}`); - } 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); - } + + if (course) 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 063872c..bff3227 100644 --- a/src/components/Forms/CourseForm/EditCourseForm.tsx +++ b/src/components/Forms/CourseForm/EditCourseForm.tsx @@ -16,21 +16,14 @@ import CourseForm from './CourseForm'; /** * Hooks */ -import { useState } from 'react'; import { useParams } from 'react-router'; 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 */ import CourseFormSchema from './CourseForm.validator'; +import { useUpdateCourse } from '@educt/hooks/queries'; type EditFormCoursePropsType = { defaultValues?: @@ -45,7 +38,6 @@ type EditFormCoursePropsType = { }; const EditFormCourse: React.FC = ({ defaultValues }) => { - const [isLoading, setIsLoading] = useState(false); const form = useForm({ resolver: yupResolver(CourseFormSchema), defaultValues: { @@ -56,44 +48,30 @@ const EditFormCourse: React.FC = ({ defaultValues }) => image: undefined, }, }); - const toast = useToast(); - const handleError = useErrorHandler(); + const { updateCourse, isLoading } = useUpdateCourse(); const { id } = useParams<{ id: string }>(); /** * Submit handler */ - // TODO move request to useUpdateCourse hook const onSubmit: SubmitHandler = async data => { try { - setIsLoading(true); - 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' }); + const updated = await updateCourse(id, data); - /** - * Update fields with new values - */ - form.reset({ - title: course.data.title, - description: course.data.description, - teacher_id: course.data.teacher.id, - category_id: course.data.category.id, - image: undefined, - }); - } catch (error: any) { - if (error.response) { - toast({ title: `${error.message}`, status: 'error' }); - } else { - handleError(error); + 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, + }); } - } finally { - setIsLoading(false); + } catch (error) { + console.error(error); } }; diff --git a/src/hooks/queries/course/useCreateCourse.ts b/src/hooks/queries/course/useCreateCourse.ts new file mode 100644 index 0000000..ab81821 --- /dev/null +++ b/src/hooks/queries/course/useCreateCourse.ts @@ -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 + | 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>(createCourse); + + return { createCourse: execute, ...state }; +}; + +export { useCreateCourse }; diff --git a/src/hooks/queries/course/useDeleteCourse.ts b/src/hooks/queries/course/useDeleteCourse.ts new file mode 100644 index 0000000..6f5553f --- /dev/null +++ b/src/hooks/queries/course/useDeleteCourse.ts @@ -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 | 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>(deleteCourse); + + return { deleteCourse: execute, ...state }; +}; + +export { useDeleteCourse }; diff --git a/src/hooks/queries/course/useUpdateCourse.ts b/src/hooks/queries/course/useUpdateCourse.ts new file mode 100644 index 0000000..98e623d --- /dev/null +++ b/src/hooks/queries/course/useUpdateCourse.ts @@ -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 | 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>(updateCourse); + + return { updateCourse: execute, ...state }; +}; + +export { useUpdateCourse }; diff --git a/src/hooks/queries/index.ts b/src/hooks/queries/index.ts index 2d0b14d..6ee26aa 100644 --- a/src/hooks/queries/index.ts +++ b/src/hooks/queries/index.ts @@ -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 }; diff --git a/src/hooks/queries/useDeleteLesson.ts b/src/hooks/queries/lesson/useDeleteLesson.ts similarity index 72% rename from src/hooks/queries/useDeleteLesson.ts rename to src/hooks/queries/lesson/useDeleteLesson.ts index d06837a..74fc45c 100644 --- a/src/hooks/queries/useDeleteLesson.ts +++ b/src/hooks/queries/lesson/useDeleteLesson.ts @@ -1,8 +1,10 @@ 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'; +import useAsync from '../../useAsync'; +import { useRootStore } from '../../useRootStore'; + +type CreatedLessonDataType = ILesson | undefined; const useDeleteLesson = () => { const { @@ -22,10 +24,12 @@ const useDeleteLesson = () => { } else { handleError(error); } + + return Promise.reject(error); } }; - const { execute, ...state } = useAsync>(deleteLesson); + const { execute, ...state } = useAsync>(deleteLesson); return { deleteLesson: execute, ...state }; }; diff --git a/src/pages/courses/components/DeleteCourseDialog/DeleteCourseDialog.tsx b/src/pages/courses/components/DeleteCourseDialog/DeleteCourseDialog.tsx index d59ff3a..1f152b7 100644 --- a/src/pages/courses/components/DeleteCourseDialog/DeleteCourseDialog.tsx +++ b/src/pages/courses/components/DeleteCourseDialog/DeleteCourseDialog.tsx @@ -1,14 +1,9 @@ -import React, { useContext, useRef, useState } from 'react'; -import { - AlertDialog, - AlertDialogBody, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogContent, - AlertDialogOverlay, - Button, - useToast, -} from '@chakra-ui/react'; +import React, { useContext } from 'react'; + +/** + * Types + */ +import { ICourse } from '@educt/interfaces'; /** * Components @@ -23,9 +18,7 @@ import { CoursesPageContext } from '@educt/contexts'; /** * Hooks */ -import { useErrorHandler } from 'react-error-boundary'; -import { useRootStore } from '@educt/hooks/useRootStore'; -import { ICourse } from '@educt/interfaces'; +import { useDeleteCourse } from '@educt/hooks/queries/course/useDeleteCourse'; type DeleteCourseDialogPropsType = { course: Pick; @@ -34,34 +27,14 @@ type DeleteCourseDialogPropsType = { }; const DeleteCourseDialog: React.FC = ({ onClose, isOpen, course }) => { - const { courseStore } = useRootStore(); const { setDeletingCourse } = useContext(CoursesPageContext); - const [isLoading, setIsLoading] = useState(false); - const handleError = useErrorHandler(); - const toast = useToast(); + const { deleteCourse, isLoading } = useDeleteCourse(); - /** - * Delete course handler - * - * @param isConfirmed Is dialog confirmed - * @param id Course id - */ - const deleteCourseHandler = async (isConfirmed: boolean, id: string) => { + const handleDelete = async (isConfirmed: boolean, id: string) => { if (isConfirmed) { - try { - setIsLoading(true); - await courseStore.deleteCourse(id); - 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); - } + await deleteCourse(id) + .catch(error => console.error(error)) + .finally(() => setDeletingCourse(undefined)); } /** @@ -74,7 +47,7 @@ const DeleteCourseDialog: React.FC = ({ onClose, is deleteCourseHandler(isConfirmed, course.id)} + onClose={isConfirmed => handleDelete(isConfirmed, course.id)} 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.`} confirmMessage='Delete' diff --git a/src/pages/profile/components/UpdateUserContactsForm/UpdateUserContactsForm.tsx b/src/pages/profile/components/UpdateUserContactsForm/UpdateUserContactsForm.tsx index ff9c601..933df5d 100644 --- a/src/pages/profile/components/UpdateUserContactsForm/UpdateUserContactsForm.tsx +++ b/src/pages/profile/components/UpdateUserContactsForm/UpdateUserContactsForm.tsx @@ -38,7 +38,7 @@ type UpdateUserContactsFormPropsType = { const UpdateUserContactsForm: React.FC = ({ contacts }) => { const { register, - formState: { errors, isDirty }, + formState: { errors, dirtyFields, isDirty }, handleSubmit, reset, } = useForm({ From e32932682119ea8bed8b74cc8a946c0b769f31ee Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Thu, 2 Dec 2021 21:53:25 +0300 Subject: [PATCH 05/87] refactor: refactored other hooks query --- src/components/Header/UserInfo.tsx | 4 +- src/components/Nav/Mobile/index.tsx | 4 +- .../SetCourseStatusButton.tsx | 4 +- .../auth/useLogin.ts} | 21 +++--- .../auth/useLogout.ts} | 18 +++-- src/hooks/queries/course/useCreateCourse.ts | 11 +-- .../course/useFetchCourse.ts} | 12 ++-- .../course/useSetCourseStatus.ts} | 18 +++-- src/hooks/queries/index.ts | 30 +++++++- src/hooks/queries/user/useUpdateUserEmail.ts | 49 +++++++++++++ src/hooks/useFetchLessonQuery.ts | 56 --------------- src/hooks/useUpdateUserEmailQuery.ts.ts | 72 ------------------- .../auth/components/AuthForm/AuthForm.tsx | 4 +- .../components/CourseList/CourseItem.tsx | 2 +- .../components/CourseList/CourseList.tsx | 4 +- src/pages/courses/edit/edit.tsx | 4 +- .../UpdateEmailForm/UpdateEmailForm.tsx | 12 ++-- .../ConfirmEmailContainer.tsx | 10 +-- src/pages/profile/profile.tsx | 7 +- .../CreateUserForm/CreateUserForm.tsx | 1 + .../DeleteUserDialog/DeleteUserDialog.tsx | 1 + .../components/EditUserForm/EditUserForm.tsx | 1 + src/services/AuthService.ts | 4 +- 23 files changed, 156 insertions(+), 193 deletions(-) rename src/hooks/{useLoginQuery.ts => queries/auth/useLogin.ts} (75%) rename src/hooks/{useLogoutQuery.ts => queries/auth/useLogout.ts} (56%) rename src/hooks/{useFetchCourseQuery.ts => queries/course/useFetchCourse.ts} (51%) rename src/hooks/{useSetCourseStatusQuery.ts => queries/course/useSetCourseStatus.ts} (71%) create mode 100644 src/hooks/queries/user/useUpdateUserEmail.ts delete mode 100644 src/hooks/useFetchLessonQuery.ts delete mode 100644 src/hooks/useUpdateUserEmailQuery.ts.ts diff --git a/src/components/Header/UserInfo.tsx b/src/components/Header/UserInfo.tsx index c166c63..80213a8 100644 --- a/src/components/Header/UserInfo.tsx +++ b/src/components/Header/UserInfo.tsx @@ -22,11 +22,11 @@ import UserBadge from '@educt/components/UserBadge'; /** * Hooks */ -import useLogoutQuery from '@educt/hooks/useLogoutQuery'; +import { useLogout } from '@educt/hooks/queries'; const UserInfo: React.FC = () => { const { userStore } = useRootStore(); - const { logout } = useLogoutQuery(); + const { logout } = useLogout(); if (userStore.me === null) { return ; diff --git a/src/components/Nav/Mobile/index.tsx b/src/components/Nav/Mobile/index.tsx index 95b518d..31dcca0 100644 --- a/src/components/Nav/Mobile/index.tsx +++ b/src/components/Nav/Mobile/index.tsx @@ -17,7 +17,7 @@ import NavList from '../NavList'; /** * Hooks */ -import useLogoutQuery from '@educt/hooks/useLogoutQuery'; +import { useLogout } from '@educt/hooks/queries'; import { useRootStore } from '@educt/hooks/useRootStore'; import { useColorMode, useDisclosure } from '@chakra-ui/react'; @@ -27,7 +27,7 @@ const NavMobile: React.FC = () => { } = useRootStore(); const { colorMode } = useColorMode(); const { isOpen, onOpen, onClose } = useDisclosure(); - const { logout } = useLogoutQuery(); + const { logout } = useLogout(); return ( <> diff --git a/src/components/SetCourseStatusButton/SetCourseStatusButton.tsx b/src/components/SetCourseStatusButton/SetCourseStatusButton.tsx index f14f08b..55e6092 100644 --- a/src/components/SetCourseStatusButton/SetCourseStatusButton.tsx +++ b/src/components/SetCourseStatusButton/SetCourseStatusButton.tsx @@ -2,7 +2,7 @@ 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'; +import { useSetCourseStatus } from '@educt/hooks/queries'; type SetCourseStatusButtonPropsType = { courseId: string; @@ -16,7 +16,7 @@ const SetCourseStatusButton: React.FC = ({ loadingText = 'Wait a second...', }) => { const [status, setStatus] = useState(currentStatus); - const { setCourseStatus, isLoading } = useSetCourseStatusQuery(); + const { setCourseStatus, isLoading } = useSetCourseStatus(); const handleChangeStatus = async (newStatus: CourseStatusEnum) => { await setCourseStatus(courseId, newStatus); diff --git a/src/hooks/useLoginQuery.ts b/src/hooks/queries/auth/useLogin.ts similarity index 75% rename from src/hooks/useLoginQuery.ts rename to src/hooks/queries/auth/useLogin.ts index 372fbbb..8ef22c2 100644 --- a/src/hooks/useLoginQuery.ts +++ b/src/hooks/queries/auth/useLogin.ts @@ -1,10 +1,12 @@ import { useToast } from '@chakra-ui/react'; import { IToken } from '@educt/interfaces'; import { useErrorHandler } from 'react-error-boundary'; -import useAsync from './useAsync'; -import { useRootStore } from './useRootStore'; +import useAsync from '../../useAsync'; +import { useRootStore } from '../../useRootStore'; -const useLoginQuery = () => { +type LoginResultDataType = IToken | undefined; + +const useLogin = () => { const { authStore } = useRootStore(); const toast = useToast(); const handleError = useErrorHandler(); @@ -21,16 +23,13 @@ const useLoginQuery = () => { return result.data; } catch (error: any) { if (error.response) { - /** - * Handle response error - */ switch (error.response.status) { case 404: toast({ title: 'User not found in a system.', status: 'error', duration: 2000 }); - throw error; + break; case 401: toast({ title: 'Invalid password.', status: 'error', duration: 2000 }); - throw error; + break; default: handleError(error); break; @@ -38,12 +37,14 @@ const useLoginQuery = () => { } else { handleError(error); } + + return Promise.reject(error); } }; - const { execute, ...state } = useAsync>(login); + const { execute, ...state } = useAsync>(login); return { login: execute, ...state }; }; -export default useLoginQuery; +export { useLogin }; diff --git a/src/hooks/useLogoutQuery.ts b/src/hooks/queries/auth/useLogout.ts similarity index 56% rename from src/hooks/useLogoutQuery.ts rename to src/hooks/queries/auth/useLogout.ts index 67eed7f..f1facd8 100644 --- a/src/hooks/useLogoutQuery.ts +++ b/src/hooks/queries/auth/useLogout.ts @@ -1,10 +1,12 @@ import { useToast } from '@chakra-ui/react'; import { useHistory } from 'react-router-dom'; -import { useRootStore } from './useRootStore'; +import { useRootStore } from '../../useRootStore'; import { useErrorHandler } from 'react-error-boundary'; -import useAsync from './useAsync'; +import useAsync from '../../useAsync'; -const useLogoutQuery = () => { +type LogoutResultDataType = {}; + +const useLogout = () => { const { authStore } = useRootStore(); const history = useHistory(); const toast = useToast(); @@ -12,17 +14,19 @@ const useLogoutQuery = () => { const logout = async () => { try { - await authStore.logout(); + const result = await authStore.logout(); history.push('/auth'); toast({ title: 'You are logged out.', isClosable: true, status: 'info' }); - } catch (error) { + return result.data; + } catch (error: any) { handleError(error); + return Promise.reject(error); } }; - const { execute, ...state } = useAsync>(logout); + const { execute, ...state } = useAsync>(logout); return { logout: execute, ...state }; }; -export default useLogoutQuery; +export { useLogout }; diff --git a/src/hooks/queries/course/useCreateCourse.ts b/src/hooks/queries/course/useCreateCourse.ts index ab81821..690dafc 100644 --- a/src/hooks/queries/course/useCreateCourse.ts +++ b/src/hooks/queries/course/useCreateCourse.ts @@ -28,10 +28,13 @@ const useCreateCourse = () => { 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' }); + 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); diff --git a/src/hooks/useFetchCourseQuery.ts b/src/hooks/queries/course/useFetchCourse.ts similarity index 51% rename from src/hooks/useFetchCourseQuery.ts rename to src/hooks/queries/course/useFetchCourse.ts index 171618c..0e40234 100644 --- a/src/hooks/useFetchCourseQuery.ts +++ b/src/hooks/queries/course/useFetchCourse.ts @@ -7,12 +7,12 @@ import { ICourse } from '@educt/interfaces'; * Hooks */ import { useEffect } from 'react'; -import useAsync from './useAsync'; -import { useRootStore } from './useRootStore'; +import useAsync from '../../useAsync'; +import { useRootStore } from '../../useRootStore'; -type QueryResponseDataType = Omit | undefined; +type CourseResultDataType = Omit | undefined; -const useFetchCourseQuery = (id: string) => { +const useFetchCourse = (id: string) => { const { pageStore: { editCourseStore }, } = useRootStore(); @@ -22,7 +22,7 @@ const useFetchCourseQuery = (id: string) => { return result.data; }; - const { execute: fetchCourseById, ...state } = useAsync>(fetch); + const { execute: fetchCourseById, ...state } = useAsync>(fetch); useEffect(() => { fetchCourseById(id); @@ -31,4 +31,4 @@ const useFetchCourseQuery = (id: string) => { return { ...state }; }; -export default useFetchCourseQuery; +export { useFetchCourse }; diff --git a/src/hooks/useSetCourseStatusQuery.ts b/src/hooks/queries/course/useSetCourseStatus.ts similarity index 71% rename from src/hooks/useSetCourseStatusQuery.ts rename to src/hooks/queries/course/useSetCourseStatus.ts index 853988c..b644c13 100644 --- a/src/hooks/useSetCourseStatusQuery.ts +++ b/src/hooks/queries/course/useSetCourseStatus.ts @@ -3,17 +3,17 @@ */ import { CourseStatusEnum } from '@educt/enums'; import { useErrorHandler } from 'react-error-boundary'; -import useAsync from './useAsync'; -import { useRootStore } from './useRootStore'; +import useAsync from '../../useAsync'; +import { useRootStore } from '../../useRootStore'; import { useToast } from '@chakra-ui/toast'; -type QueryResponseDataType = {} | undefined; +type SetCourseStatusResultDataType = {} | undefined; export type SetCourseStatusFnType = ( id: string, newStatus: CourseStatusEnum -) => Promise; +) => Promise; -const useSetCourseStatusQuery = () => { +const useSetCourseStatus = () => { const { courseStore } = useRootStore(); const toast = useToast(); const handleError = useErrorHandler(); @@ -38,12 +38,16 @@ const useSetCourseStatusQuery = () => { } else { handleError(error); } + + return Promise.reject(error); } }; - const { execute, ...state } = useAsync>(setCourseStatus); + const { execute, ...state } = useAsync>( + setCourseStatus + ); return { setCourseStatus: execute, ...state }; }; -export default useSetCourseStatusQuery; +export { useSetCourseStatus }; diff --git a/src/hooks/queries/index.ts b/src/hooks/queries/index.ts index 6ee26aa..8c80970 100644 --- a/src/hooks/queries/index.ts +++ b/src/hooks/queries/index.ts @@ -1,6 +1,34 @@ +/** + * Auth + */ +import { useLogin } from './auth/useLogin'; +import { useLogout } from './auth/useLogout'; + +/** + * Lesson + */ import { useDeleteLesson } from './lesson/useDeleteLesson'; +/** + * Course + */ import { useCreateCourse } from './course/useCreateCourse'; import { useUpdateCourse } from './course/useUpdateCourse'; +import { useFetchCourse } from './course/useFetchCourse'; +import { useSetCourseStatus } from './course/useSetCourseStatus'; -export { useCreateCourse, useUpdateCourse, useDeleteLesson }; +/** + * User + */ +import { useUpdateUserEmail } from './user/useUpdateUserEmail'; + +export { + useLogin, + useLogout, + useCreateCourse, + useUpdateCourse, + useFetchCourse, + useSetCourseStatus, + useDeleteLesson, + useUpdateUserEmail, +}; diff --git a/src/hooks/queries/user/useUpdateUserEmail.ts b/src/hooks/queries/user/useUpdateUserEmail.ts new file mode 100644 index 0000000..3b85d3a --- /dev/null +++ b/src/hooks/queries/user/useUpdateUserEmail.ts @@ -0,0 +1,49 @@ +import { useToast } from '@chakra-ui/toast'; +import { useErrorHandler } from 'react-error-boundary'; +import { useRootStore } from '@educt/hooks/useRootStore'; +import useAsync from '@educt/hooks/useAsync'; + +type ConfirmationResultDataType = { expired_seconds: number } | null; + +const useUpdateUserEmail = () => { + const { userStore } = useRootStore(); + const toast = useToast(); + const handleError = useErrorHandler(); + + const sendConfirmationCode = async (email: string) => { + try { + const result = await userStore.updateCurrentUserEmail(email); + toast({ title: 'Confirmation code has been sent.', status: 'info' }); + return result.data; + } catch (error: any) { + if (error.response) { + if (error.response.status === 503) { + switch (error.response.data.code) { + case 'E_CONFIRM_CODE_EXIST': + toast({ title: 'Please, try send code later.', status: 'info' }); + break; + default: + toast({ title: `Unable to send confirmation code, try again later.`, status: 'error' }); + break; + } + } else if (error.response.status === 422) { + toast({ title: error.response.data.errors[0].message, status: 'error' }); + } else { + handleError(error); + } + } else { + handleError(error); + } + + return Promise.reject(error); + } + }; + + const { execute, ...state } = useAsync>( + sendConfirmationCode + ); + + return { sendConfirmationCode: execute, ...state }; +}; + +export { useUpdateUserEmail }; diff --git a/src/hooks/useFetchLessonQuery.ts b/src/hooks/useFetchLessonQuery.ts deleted file mode 100644 index d280966..0000000 --- a/src/hooks/useFetchLessonQuery.ts +++ /dev/null @@ -1,56 +0,0 @@ -// /** -// * Types -// */ -// import { ILesson } from '@educt/interfaces'; - -// /** -// * Hooks -// */ -// import { useEffect, useState } from 'react'; -// import { useErrorHandler } from 'react-error-boundary'; -// import useIsMountedRef from './useIsMountedRef'; - -// type LessonStateType = { -// data: ILesson | null; -// error: any; -// loading: boolean; -// fetched: boolean; -// }; - -// const useFetchLessonQuery = (id: string) => { -// const [state, setState] = useState({ data: null, error: null, loading: false, fetched: false }); -// const isMountedRef = useIsMountedRef(); -// const handleError = useErrorHandler(); - -// /** -// * Fetch course handler -// */ -// useEffect(() => { -// setState(s => ({ ...s, loading: true })); -// lessonService -// .fetchById(id) -// .then(data => { -// if (isMountedRef.current) { -// setState(s => ({ ...s, data: data.data })); -// } -// }) -// .catch(error => { -// if (error.response) { -// if (isMountedRef.current) { -// setState(s => ({ ...s, error })); -// } -// } else { -// handleError(error); -// } -// }) -// .finally(() => { -// if (isMountedRef.current) { -// setState(s => ({ ...s, loading: false, fetched: true })); -// } -// }); -// }, [id]); - -// return { ...state }; -// }; - -// export default useFetchLessonQuery; diff --git a/src/hooks/useUpdateUserEmailQuery.ts.ts b/src/hooks/useUpdateUserEmailQuery.ts.ts deleted file mode 100644 index 6c52bc0..0000000 --- a/src/hooks/useUpdateUserEmailQuery.ts.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { useToast } from '@chakra-ui/toast'; -import { IApiRespose } from '@educt/interfaces'; -import { useState } from 'react'; -import { useErrorHandler } from 'react-error-boundary'; -import useIsMountedRef from './useIsMountedRef'; -import { useRootStore } from './useRootStore'; - -type UpdateUserEmailStateType = { - result: IApiRespose<{ expired_seconds: number }> | null; - error: any; - loading: boolean; - fetched: boolean; -}; - -const useUpdateUserEmailQuery = () => { - const isMountedRef = useIsMountedRef(); - const { userStore } = useRootStore(); - const [state, setState] = useState({ - result: null, - error: null, - loading: false, - fetched: false, - }); - const toast = useToast(); - const handleError = useErrorHandler(); - - const sendConfirmationCode = async (email: string) => { - try { - setState(s => ({ ...s, loading: true })); - const result = await userStore.updateCurrentUserEmail(email); - toast({ title: 'Confirmation code has been sent.', status: 'info' }); - - if (isMountedRef.current) { - setState(s => ({ ...s, result })); - } - - return result; - } catch (error: any) { - if (isMountedRef.current) { - setState(s => ({ ...s, error })); - } - - if (error.response) { - const status = error.response.status; - if (status === 503) { - switch (error.response.data.code) { - case 'E_CONFIRM_CODE_EXIST': - toast({ title: 'Please, try send code later.', status: 'info' }); - break; - default: - toast({ title: `Unable to send confirmation code, try again later.`, status: 'error' }); - break; - } - } else if (status === 422) { - toast({ title: error.response.data.errors[0].message, status: 'error' }); - } else { - handleError(error); - } - } else { - handleError(error); - } - } finally { - if (isMountedRef.current) { - setState(s => ({ ...s, loading: false, fetched: true })); - } - } - }; - - return { ...state, sendConfirmationCode }; -}; - -export default useUpdateUserEmailQuery; diff --git a/src/pages/auth/components/AuthForm/AuthForm.tsx b/src/pages/auth/components/AuthForm/AuthForm.tsx index b20c5c2..c49d82d 100644 --- a/src/pages/auth/components/AuthForm/AuthForm.tsx +++ b/src/pages/auth/components/AuthForm/AuthForm.tsx @@ -23,7 +23,7 @@ import type { SubmitHandler } from 'react-hook-form'; * Hooks */ import { useForm } from 'react-hook-form'; -import useLoginQuery from '@educt/hooks/useLoginQuery'; +import { useLogin } from '@educt/hooks/queries'; /** * Schema @@ -44,7 +44,7 @@ const AuthForm: React.FC = () => { } = useForm({ resolver: yupResolver(LoginSchema), }); - const { login, isLoading } = useLoginQuery(); + const { login, isLoading } = useLogin(); /** * Login handler diff --git a/src/pages/courses/components/CourseList/CourseItem.tsx b/src/pages/courses/components/CourseList/CourseItem.tsx index 9dc05ed..ec7cddd 100644 --- a/src/pages/courses/components/CourseList/CourseItem.tsx +++ b/src/pages/courses/components/CourseList/CourseItem.tsx @@ -12,8 +12,8 @@ import { DeleteIcon, EditIcon } from '@chakra-ui/icons'; * Types */ import { ICourse } from '@educt/interfaces'; +import type { SetCourseStatusFnType } from '@educt/hooks/queries/course/useSetCourseStatus'; import { CourseStatusEnum } from '@educt/enums'; -import { SetCourseStatusFnType } from '@educt/hooks/useSetCourseStatusQuery'; /** * Components diff --git a/src/pages/courses/components/CourseList/CourseList.tsx b/src/pages/courses/components/CourseList/CourseList.tsx index 92b6596..c5f35f6 100644 --- a/src/pages/courses/components/CourseList/CourseList.tsx +++ b/src/pages/courses/components/CourseList/CourseList.tsx @@ -18,6 +18,7 @@ import LoadingList from '@educt/components/LoadingList'; * Hooks */ import { useContext, useEffect } from 'react'; +import { useSetCourseStatus } from '@educt/hooks/queries'; import { useRootStore } from '@educt/hooks/useRootStore'; import { useDisclosure } from '@chakra-ui/hooks'; @@ -25,7 +26,6 @@ import { useDisclosure } from '@chakra-ui/hooks'; * Contexts */ import { CoursesPageContext } from '@educt/contexts'; -import useSetCourseStatusQuery from '@educt/hooks/useSetCourseStatusQuery'; type CourseListPropsType = { render: React.FC; @@ -38,7 +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 { setCourseStatus } = useSetCourseStatusQuery(); + const { setCourseStatus } = useSetCourseStatus(); const { courses, isLoading } = courseStore; /** diff --git a/src/pages/courses/edit/edit.tsx b/src/pages/courses/edit/edit.tsx index 8c3dfb1..4ecad80 100644 --- a/src/pages/courses/edit/edit.tsx +++ b/src/pages/courses/edit/edit.tsx @@ -22,7 +22,7 @@ import { StudentList, StudentItem } from './components/StudentList'; */ import { useParams } from 'react-router-dom'; import { useRootStore } from '@educt/hooks/useRootStore'; -import useFetchCourseQuery from '@educt/hooks/useFetchCourseQuery'; +import { useFetchCourse } from '@educt/hooks/queries'; /** * Course editor @@ -34,7 +34,7 @@ const EditCoursePage: React.FC = () => { editCourseStore: { course }, }, } = useRootStore(); - const { error, isLoading } = useFetchCourseQuery(params.id); + const { error, isLoading } = useFetchCourse(params.id); /** * Not Found diff --git a/src/pages/profile/change-email/components/UpdateEmailForm/UpdateEmailForm.tsx b/src/pages/profile/change-email/components/UpdateEmailForm/UpdateEmailForm.tsx index 6cc11ad..4f8134c 100644 --- a/src/pages/profile/change-email/components/UpdateEmailForm/UpdateEmailForm.tsx +++ b/src/pages/profile/change-email/components/UpdateEmailForm/UpdateEmailForm.tsx @@ -15,7 +15,7 @@ import type { SubmitHandler } from 'react-hook-form'; */ import { useContext, useEffect, useState } from 'react'; import { useForm } from 'react-hook-form'; -import useUpdateUserEmailQuery from '@educt/hooks/useUpdateUserEmailQuery.ts'; +import { useUpdateUserEmail } from '@educt/hooks/queries'; /** * Contexts @@ -41,7 +41,7 @@ type UpdateEmailFormPropsType = { const UpdateEmailForm: React.FC = ({ currentEmail }) => { const [newEmail, setNewEmail] = useState(null); const { setIsCodeSent, setConfirmEmailData } = useContext(ChangeEmailPageContext); - const { sendConfirmationCode, loading, result } = useUpdateUserEmailQuery(); + const { sendConfirmationCode, isLoading, data } = useUpdateUserEmail(); const { handleSubmit, register, @@ -54,11 +54,11 @@ const UpdateEmailForm: React.FC = ({ currentEmail }) = * Change page view in confirmation code has been sent */ useEffect(() => { - if (newEmail !== null && result !== null) { + if (newEmail !== null && data !== null) { setIsCodeSent(true); - setConfirmEmailData({ newEmail, expired_seconds: result.data.expired_seconds }); + setConfirmEmailData({ newEmail, expired_seconds: data.expired_seconds }); } - }, [result]); + }, [data]); /** * Submit form handler @@ -93,7 +93,7 @@ const UpdateEmailForm: React.FC = ({ currentEmail }) = type='submit' size='md' variant='outline' - isLoading={loading} + isLoading={isLoading} loadingText='Sending...' > Send code diff --git a/src/pages/profile/change-email/containers/ConfirmEmailContainer/ConfirmEmailContainer.tsx b/src/pages/profile/change-email/containers/ConfirmEmailContainer/ConfirmEmailContainer.tsx index 693c418..0d21847 100644 --- a/src/pages/profile/change-email/containers/ConfirmEmailContainer/ConfirmEmailContainer.tsx +++ b/src/pages/profile/change-email/containers/ConfirmEmailContainer/ConfirmEmailContainer.tsx @@ -12,7 +12,7 @@ import { ConfirmEmailDataType } from '@educt/types'; * Hooks */ import { useEffect, useState } from 'react'; -import useUpdateUserEmailQuery from '@educt/hooks/useUpdateUserEmailQuery.ts'; +import { useUpdateUserEmail } from '@educt/hooks/queries'; import useIsMountedRef from '@educt/hooks/useIsMountedRef'; import { useRootStore } from '@educt/hooks/useRootStore'; import { useToast } from '@chakra-ui/toast'; @@ -25,7 +25,7 @@ type ConfirmEmailContainerPropsType = { const ConfirmEmailContainer: React.FC = ({ data }) => { const { userStore } = useRootStore(); - const { sendConfirmationCode, loading } = useUpdateUserEmailQuery(); + const { sendConfirmationCode, isLoading } = useUpdateUserEmail(); const [isVerifying, setIsVerifying] = useState(false); const [code, setCode] = useState(''); const [expired, setExpired] = useState(data.expired_seconds); @@ -93,11 +93,11 @@ const ConfirmEmailContainer: React.FC = ({ data const resendCodeHandler = async () => { const result = await sendConfirmationCode(data.newEmail); - if (result !== undefined) { + if (result !== null) { /** * Reset timer state */ - const { expired_seconds } = result.data; + const { expired_seconds } = result; setExpired(v => (v = expired_seconds)); } }; @@ -158,7 +158,7 @@ const ConfirmEmailContainer: React.FC = ({ data mt='3' textDecoration='underline' loadingText='Wait a second.' - isLoading={loading} + isLoading={isLoading} > Resend code diff --git a/src/pages/profile/profile.tsx b/src/pages/profile/profile.tsx index e1851cc..50da352 100644 --- a/src/pages/profile/profile.tsx +++ b/src/pages/profile/profile.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import { observer } from 'mobx-react'; import { Avatar, Tabs, @@ -35,7 +34,7 @@ import CourseList from '@educt/pages/profile/components/CourseList'; import { useHistory } from 'react-router-dom'; import { useColorMode } from '@chakra-ui/color-mode'; import { useRootStore } from '@educt/hooks/useRootStore'; -import useLogoutQuery from '@educt/hooks/useLogoutQuery'; +import { useLogout } from '@educt/hooks/queries'; /** * Profile page @@ -45,7 +44,7 @@ const ProfilePage: React.FC = () => { userStore: { me }, } = useRootStore(); const history = useHistory(); - const { logout } = useLogoutQuery(); + const { logout } = useLogout(); const { colorMode } = useColorMode(); if (me === null) return ; @@ -139,7 +138,7 @@ const ProfilePage: React.FC = () => { - diff --git a/src/pages/users/components/CreateUserForm/CreateUserForm.tsx b/src/pages/users/components/CreateUserForm/CreateUserForm.tsx index 1b4e9fd..be7c710 100644 --- a/src/pages/users/components/CreateUserForm/CreateUserForm.tsx +++ b/src/pages/users/components/CreateUserForm/CreateUserForm.tsx @@ -84,6 +84,7 @@ const CreateUserModal: React.FC = () => { /** * Submit handler */ + // TODO: move to hook const onSubmit: SubmitHandler = async data => { try { setLoading(true); diff --git a/src/pages/users/components/DeleteUserDialog/DeleteUserDialog.tsx b/src/pages/users/components/DeleteUserDialog/DeleteUserDialog.tsx index d8b760e..df77ec0 100644 --- a/src/pages/users/components/DeleteUserDialog/DeleteUserDialog.tsx +++ b/src/pages/users/components/DeleteUserDialog/DeleteUserDialog.tsx @@ -44,6 +44,7 @@ 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 { diff --git a/src/pages/users/components/EditUserForm/EditUserForm.tsx b/src/pages/users/components/EditUserForm/EditUserForm.tsx index c98348d..bdf7265 100644 --- a/src/pages/users/components/EditUserForm/EditUserForm.tsx +++ b/src/pages/users/components/EditUserForm/EditUserForm.tsx @@ -94,6 +94,7 @@ const UpdateUserForm: React.FC = ({ user, onClose, isOp /** * Submit handler */ + // TODO: move to hook const onSubmit: SubmitHandler = async data => { try { setIsLoading(true); diff --git a/src/services/AuthService.ts b/src/services/AuthService.ts index a05988c..3d81255 100644 --- a/src/services/AuthService.ts +++ b/src/services/AuthService.ts @@ -28,7 +28,7 @@ class AuthService implements AuthServiceImpl { * @param password User password * @returns Token result data */ - public async requestLogin(login: string, password: string) { + public async requestLogin(login: string, password: string): Promise> { const result = await this.api.post('v1/auth/login', { login, password, @@ -41,7 +41,7 @@ class AuthService implements AuthServiceImpl { * * @returns Empty obj */ - public async requestLogout() { + public async requestLogout(): Promise> { const result = await this.api.post('v1/auth/logout'); return result.data; } From 321149a1f277bcdd59463d64d67b53bdb0161092 Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Thu, 2 Dec 2021 22:50:06 +0300 Subject: [PATCH 06/87] refactor: user query hooks --- .../Forms/CourseForm/CreateCourseForm.tsx | 2 +- .../Forms/CourseForm/EditCourseForm.tsx | 22 +++++----- src/helpers/index.ts | 4 ++ src/hooks/queries/auth/useLogin.ts | 2 +- src/hooks/queries/course/useCreateCourse.ts | 4 +- src/hooks/queries/course/useDeleteCourse.ts | 2 +- src/hooks/queries/course/useFetchCourse.ts | 2 +- .../queries/course/useSetCourseStatus.ts | 7 +-- src/hooks/queries/course/useUpdateCourse.ts | 2 +- src/hooks/queries/index.ts | 6 +++ src/hooks/queries/lesson/useDeleteLesson.ts | 2 +- src/hooks/queries/user/useCreateUser.ts | 43 +++++++++++++++++++ src/hooks/queries/user/useDeleteUser.ts | 36 ++++++++++++++++ src/hooks/queries/user/useUpdateUser.ts | 43 +++++++++++++++++++ src/hooks/queries/user/useUpdateUserEmail.ts | 2 +- .../DeleteCourseDialog/DeleteCourseDialog.tsx | 10 +++-- .../ConfirmEmailContainer.tsx | 13 +++--- .../CreateUserForm/CreateUserForm.tsx | 36 +++++----------- .../DeleteUserDialog/DeleteUserDialog.tsx | 21 +++------ .../components/EditUserForm/EditUserForm.tsx | 35 ++++----------- src/stores/pages/EditCourseStore.ts | 2 +- 21 files changed, 189 insertions(+), 107 deletions(-) create mode 100644 src/hooks/queries/user/useCreateUser.ts create mode 100644 src/hooks/queries/user/useDeleteUser.ts create mode 100644 src/hooks/queries/user/useUpdateUser.ts 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; From 3db6174b8b088c04070144f90b9d6c6e486c7369 Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Fri, 3 Dec 2021 00:19:04 +0300 Subject: [PATCH 07/87] feat: delete course from edit page --- .../DeleteCourseDialog/DeleteCourseDialog.tsx | 28 ++++++++++--------- .../Dialogs}/DeleteCourseDialog/index.ts | 0 .../DeleteLessonDialog/DeleteLessonDialog.tsx | 7 ++++- src/contexts/index.ts | 2 ++ .../components/CourseList/CourseList.tsx | 22 ++++++++------- src/pages/courses/edit/edit.tsx | 10 ++++++- src/stores/pages/EditCourseStore.ts | 10 ++++++- 7 files changed, 53 insertions(+), 26 deletions(-) rename src/{pages/courses/components => components/Dialogs}/DeleteCourseDialog/DeleteCourseDialog.tsx (71%) rename src/{pages/courses/components => components/Dialogs}/DeleteCourseDialog/index.ts (100%) diff --git a/src/pages/courses/components/DeleteCourseDialog/DeleteCourseDialog.tsx b/src/components/Dialogs/DeleteCourseDialog/DeleteCourseDialog.tsx similarity index 71% rename from src/pages/courses/components/DeleteCourseDialog/DeleteCourseDialog.tsx rename to src/components/Dialogs/DeleteCourseDialog/DeleteCourseDialog.tsx index 667bc5f..e424eb5 100644 --- a/src/pages/courses/components/DeleteCourseDialog/DeleteCourseDialog.tsx +++ b/src/components/Dialogs/DeleteCourseDialog/DeleteCourseDialog.tsx @@ -1,4 +1,4 @@ -import React, { useContext } from 'react'; +import React from 'react'; /** * Types @@ -10,15 +10,12 @@ import { ICourse } from '@educt/interfaces'; */ import ConfirmDialog from '@educt/components/ConfirmDialog/ConfirmDialog'; -/** - * Contexts - */ -import { CoursesPageContext } from '@educt/contexts'; - /** * Hooks */ import { useDeleteCourse } from '@educt/hooks/queries/course/useDeleteCourse'; +import { useHistory, useLocation } from 'react-router'; +import useIsMountedRef from '@educt/hooks/useIsMountedRef'; type DeleteCourseDialogPropsType = { course: Pick; @@ -27,24 +24,29 @@ type DeleteCourseDialogPropsType = { }; const DeleteCourseDialog: React.FC = ({ onClose, isOpen, course }) => { - const { setDeletingCourse } = useContext(CoursesPageContext); const { deleteCourse, isLoading } = useDeleteCourse(); + const isMountedRef = useIsMountedRef(); + const history = useHistory(); + const location = useLocation(); const handleDelete = async (isConfirmed: boolean, id: string) => { if (isConfirmed) { try { await deleteCourse(id); + if (isMountedRef.current && location.pathname !== '/courses') { + history.push('/courses'); + } } catch (error: any) { console.error(error); - } finally { - setDeletingCourse(undefined); } } - /** - * Close dialog - */ - onClose(); + if (isMountedRef.current) { + /** + * Close dialog + */ + onClose(); + } }; return ( diff --git a/src/pages/courses/components/DeleteCourseDialog/index.ts b/src/components/Dialogs/DeleteCourseDialog/index.ts similarity index 100% rename from src/pages/courses/components/DeleteCourseDialog/index.ts rename to src/components/Dialogs/DeleteCourseDialog/index.ts diff --git a/src/components/Dialogs/DeleteLessonDialog/DeleteLessonDialog.tsx b/src/components/Dialogs/DeleteLessonDialog/DeleteLessonDialog.tsx index 3b5b6de..dfaf8c4 100644 --- a/src/components/Dialogs/DeleteLessonDialog/DeleteLessonDialog.tsx +++ b/src/components/Dialogs/DeleteLessonDialog/DeleteLessonDialog.tsx @@ -21,6 +21,7 @@ type DeleteLessonDialogPropsType = { onClose: () => void; }; +//TODO pass lesson by prop const DeleteLessonDialog: React.FC = ({ isOpen, onClose }) => { const { pageStore: { @@ -31,7 +32,11 @@ const DeleteLessonDialog: React.FC = ({ isOpen, onC const handleDelete = async (isConfirmed: boolean) => { if (isConfirmed && deletingLesson) { - await deleteLesson(deletingLesson.id); + try { + await deleteLesson(deletingLesson.id); + } catch (error: any) { + console.error(error); + } } /** diff --git a/src/contexts/index.ts b/src/contexts/index.ts index 6174192..2502637 100644 --- a/src/contexts/index.ts +++ b/src/contexts/index.ts @@ -29,6 +29,8 @@ export const CoursesPageContext = createContext({ setSelectedCategory: () => {}, courseStatus: undefined, setCourseStatus: () => {}, + + //TODO: remove deletingCourse: undefined, setDeletingCourse: () => {}, }); diff --git a/src/pages/courses/components/CourseList/CourseList.tsx b/src/pages/courses/components/CourseList/CourseList.tsx index c5f35f6..2ccb741 100644 --- a/src/pages/courses/components/CourseList/CourseList.tsx +++ b/src/pages/courses/components/CourseList/CourseList.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import { observer } from 'mobx-react'; import { Box, Grid, Text } from '@chakra-ui/react'; @@ -11,7 +11,7 @@ import { CourseItemPropsType } from './CourseItem'; /** * Components */ -import DeleteCourseDialog from '../DeleteCourseDialog'; +import DeleteCourseDialog from '@educt/components/Dialogs/DeleteCourseDialog'; import LoadingList from '@educt/components/LoadingList'; /** @@ -26,6 +26,7 @@ import { useDisclosure } from '@chakra-ui/hooks'; * Contexts */ import { CoursesPageContext } from '@educt/contexts'; +import { ICourse } from '@educt/interfaces'; type CourseListPropsType = { render: React.FC; @@ -36,11 +37,17 @@ const CourseList: React.FC = ({ render: Item }) => { userStore: { me }, courseStore, } = useRootStore(); - const { selectedCategory, courseStatus, deletingCourse, setDeletingCourse } = useContext(CoursesPageContext); + const [deleting, setDeleting] = useState | null>(null); + const { selectedCategory, courseStatus } = useContext(CoursesPageContext); const { onOpen: onOpenDeleteDialog, onClose: onCloseDeleteDialog, isOpen: isOpenDeleteDialog } = useDisclosure(); const { setCourseStatus } = useSetCourseStatus(); const { courses, isLoading } = courseStore; + const deleteHandler = (course: Pick) => { + setDeleting(course); + onOpenDeleteDialog(); + }; + /** * Fetch courses handler */ @@ -57,19 +64,14 @@ const CourseList: React.FC = ({ render: Item }) => { return ( - {deletingCourse && ( - - )} + {deleting && } {courses.length !== 0 ? ( {courses.map(course => ( { - setDeletingCourse(course); - onOpenDeleteDialog(); - }} + onDelete={() => deleteHandler({ id: course.id, title: course.title })} onSetStatus={setCourseStatus} /> ))} diff --git a/src/pages/courses/edit/edit.tsx b/src/pages/courses/edit/edit.tsx index 4ecad80..878f833 100644 --- a/src/pages/courses/edit/edit.tsx +++ b/src/pages/courses/edit/edit.tsx @@ -1,7 +1,8 @@ import React from 'react'; import Helmet from 'react-helmet'; import { Redirect } from 'react-router-dom'; -import { Box, Heading, Text, Tab, TabList, Flex, Tabs, TabPanels, TabPanel } from '@chakra-ui/react'; +import { Box, Heading, Text, Tab, TabList, Flex, Tabs, TabPanels, TabPanel, Button } from '@chakra-ui/react'; +import { DeleteIcon } from '@chakra-ui/icons'; /** * Types @@ -16,6 +17,7 @@ import { EditCourseForm } from '@educt/components/Forms/CourseForm'; import SetCourseStatusButton from '@educt/components/SetCourseStatusButton'; import LessonList from './components/LessonList'; import { StudentList, StudentItem } from './components/StudentList'; +import DeleteCourseDialog from '@educt/components/Dialogs/DeleteCourseDialog'; /** * Hooks @@ -23,6 +25,7 @@ import { StudentList, StudentItem } from './components/StudentList'; import { useParams } from 'react-router-dom'; import { useRootStore } from '@educt/hooks/useRootStore'; import { useFetchCourse } from '@educt/hooks/queries'; +import { useDisclosure } from '@chakra-ui/hooks'; /** * Course editor @@ -35,6 +38,7 @@ const EditCoursePage: React.FC = () => { }, } = useRootStore(); const { error, isLoading } = useFetchCourse(params.id); + const { onOpen: onOpenDeleteDialog, onClose: onCloseDeleteDialog, isOpen: isOpenDeleteDialog } = useDisclosure(); /** * Not Found @@ -53,6 +57,7 @@ const EditCoursePage: React.FC = () => { Editing {course.title} + Course editor @@ -60,6 +65,9 @@ const EditCoursePage: React.FC = () => { + diff --git a/src/stores/pages/EditCourseStore.ts b/src/stores/pages/EditCourseStore.ts index 37c0869..4850a6d 100644 --- a/src/stores/pages/EditCourseStore.ts +++ b/src/stores/pages/EditCourseStore.ts @@ -10,6 +10,10 @@ export default class EditCourseStore { public course: Omit | null = null; + public isDeleting: boolean = false; + + public deletingCourse: Pick | undefined; + public deletingLesson: ILesson | undefined; constructor(pageStore: PageStore) { @@ -72,7 +76,11 @@ export default class EditCourseStore { } } - setDeletingLesson(lesson: ILesson) { + public setDeletingLesson(lesson: ILesson) { this.deletingLesson = lesson; } + + public setDeletingCourse(course: Pick | undefined) { + this.deletingCourse = course; + } } From ba514954e6a4d15ef228934cc26fe21e109806b6 Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Fri, 3 Dec 2021 22:10:41 +0300 Subject: [PATCH 08/87] refactor: refactored dialog --- .../DeleteLessonDialog/DeleteLessonDialog.tsx | 14 ++---- .../DeleteUserDialog/DeleteUserDialog.tsx | 6 +-- .../Dialogs}/DeleteUserDialog/index.ts | 0 src/contexts/index.ts | 8 ---- .../components/CourseList/CourseList.tsx | 4 +- .../edit/components/LessonList/LessonList.tsx | 13 +++--- .../components/EditUserForm/EditUserForm.tsx | 45 +++++++------------ .../users/components/UserList/UserList.tsx | 43 ++++++++---------- src/providers/index.tsx | 11 +---- src/stores/pages/EditCourseStore.ts | 16 +------ src/types/index.d.ts | 18 -------- 11 files changed, 54 insertions(+), 124 deletions(-) rename src/{pages/users/components => components/Dialogs}/DeleteUserDialog/DeleteUserDialog.tsx (90%) rename src/{pages/users/components => components/Dialogs}/DeleteUserDialog/index.ts (100%) diff --git a/src/components/Dialogs/DeleteLessonDialog/DeleteLessonDialog.tsx b/src/components/Dialogs/DeleteLessonDialog/DeleteLessonDialog.tsx index dfaf8c4..7a6084c 100644 --- a/src/components/Dialogs/DeleteLessonDialog/DeleteLessonDialog.tsx +++ b/src/components/Dialogs/DeleteLessonDialog/DeleteLessonDialog.tsx @@ -14,26 +14,20 @@ import ConfirmDialog from '@educt/components/ConfirmDialog/ConfirmDialog'; * Hooks */ import { useDeleteLesson } from '@educt/hooks/queries'; -import { useRootStore } from '@educt/hooks/useRootStore'; type DeleteLessonDialogPropsType = { + lesson: Pick; isOpen: boolean; onClose: () => void; }; -//TODO pass lesson by prop -const DeleteLessonDialog: React.FC = ({ isOpen, onClose }) => { - const { - pageStore: { - editCourseStore: { deletingLesson }, - }, - } = useRootStore(); +const DeleteLessonDialog: React.FC = ({ lesson, isOpen, onClose }) => { const { deleteLesson, isLoading } = useDeleteLesson(); const handleDelete = async (isConfirmed: boolean) => { - if (isConfirmed && deletingLesson) { + if (isConfirmed) { try { - await deleteLesson(deletingLesson.id); + await deleteLesson(lesson.id); } catch (error: any) { console.error(error); } diff --git a/src/pages/users/components/DeleteUserDialog/DeleteUserDialog.tsx b/src/components/Dialogs/DeleteUserDialog/DeleteUserDialog.tsx similarity index 90% rename from src/pages/users/components/DeleteUserDialog/DeleteUserDialog.tsx rename to src/components/Dialogs/DeleteUserDialog/DeleteUserDialog.tsx index 54aee08..9e41895 100644 --- a/src/pages/users/components/DeleteUserDialog/DeleteUserDialog.tsx +++ b/src/components/Dialogs/DeleteUserDialog/DeleteUserDialog.tsx @@ -23,14 +23,14 @@ import { useRootStore } from '@educt/hooks/useRootStore'; import { useDeleteUser } from '@educt/hooks/queries'; type DeleteUserDialogPropsType = { - user: IUser; + user: Pick; isOpen: boolean; onClose: () => void; }; const DeleteUserDialog: React.FC = ({ isOpen, onClose, user }) => { const { userStore } = useRootStore(); - const { setDeletingUser, searchingRole, searchingPage, search } = useContext(UsersPageContext); + const { searchingRole, searchingPage, search } = useContext(UsersPageContext); const { deleteUser, isLoading } = useDeleteUser(); const { pagination } = userStore; @@ -52,8 +52,6 @@ const DeleteUserDialog: React.FC = ({ isOpen, onClose }); } catch (error: any) { console.error(error); - } finally { - setDeletingUser(undefined); } } diff --git a/src/pages/users/components/DeleteUserDialog/index.ts b/src/components/Dialogs/DeleteUserDialog/index.ts similarity index 100% rename from src/pages/users/components/DeleteUserDialog/index.ts rename to src/components/Dialogs/DeleteUserDialog/index.ts diff --git a/src/contexts/index.ts b/src/contexts/index.ts index 2502637..066f0f8 100644 --- a/src/contexts/index.ts +++ b/src/contexts/index.ts @@ -18,10 +18,6 @@ export const UsersPageContext = createContext({ setSearchingPage: () => {}, search: undefined, setSearch: () => {}, - editingUser: undefined, - setEditingUser: () => {}, - deletingUser: undefined, - setDeletingUser: () => {}, }); export const CoursesPageContext = createContext({ @@ -29,8 +25,4 @@ export const CoursesPageContext = createContext({ setSelectedCategory: () => {}, courseStatus: undefined, setCourseStatus: () => {}, - - //TODO: remove - deletingCourse: undefined, - setDeletingCourse: () => {}, }); diff --git a/src/pages/courses/components/CourseList/CourseList.tsx b/src/pages/courses/components/CourseList/CourseList.tsx index 2ccb741..a3b4b79 100644 --- a/src/pages/courses/components/CourseList/CourseList.tsx +++ b/src/pages/courses/components/CourseList/CourseList.tsx @@ -43,7 +43,7 @@ const CourseList: React.FC = ({ render: Item }) => { const { setCourseStatus } = useSetCourseStatus(); const { courses, isLoading } = courseStore; - const deleteHandler = (course: Pick) => { + const handleDeleteCourse = (course: Pick) => { setDeleting(course); onOpenDeleteDialog(); }; @@ -71,7 +71,7 @@ const CourseList: React.FC = ({ render: Item }) => { deleteHandler({ id: course.id, title: course.title })} + onDelete={() => handleDeleteCourse({ id: course.id, title: course.title })} onSetStatus={setCourseStatus} /> ))} diff --git a/src/pages/courses/edit/components/LessonList/LessonList.tsx b/src/pages/courses/edit/components/LessonList/LessonList.tsx index b8cb377..cd6694e 100644 --- a/src/pages/courses/edit/components/LessonList/LessonList.tsx +++ b/src/pages/courses/edit/components/LessonList/LessonList.tsx @@ -1,10 +1,10 @@ -import React from 'react'; +import React, { useState } 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 { Flex, Box, Text, IconButton, Icon, Button } from '@chakra-ui/react'; import { DeleteIcon, DragHandleIcon } from '@chakra-ui/icons'; -import moment from 'moment'; /** * Types @@ -58,13 +58,14 @@ const LessonList: React.FC = ({ course }) => { pageStore: { editCourseStore }, } = useRootStore(); const { onOpen: onOpenDeleteDialog, onClose: onCloseDeleteDialog, isOpen: isOpenDeleteDialog } = useDisclosure(); + const [deleting, setDeleting] = useState | null>(null); 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 = (lesson: ILesson): void => { - editCourseStore.setDeletingLesson(lesson); + setDeleting({ id: lesson.id, title: lesson.title }); onOpenDeleteDialog(); }; @@ -143,7 +144,7 @@ const LessonList: React.FC = ({ course }) => { - = ({ course }) => { {lessons.length !== 0 ? ( <> - + {deleting && ( + + )} Total: ({lessons.length}) diff --git a/src/pages/users/components/EditUserForm/EditUserForm.tsx b/src/pages/users/components/EditUserForm/EditUserForm.tsx index 198b4b7..7027781 100644 --- a/src/pages/users/components/EditUserForm/EditUserForm.tsx +++ b/src/pages/users/components/EditUserForm/EditUserForm.tsx @@ -1,4 +1,4 @@ -import React, { useContext } from 'react'; +import React from 'react'; import * as helpers from '@educt/helpers'; import { Modal, @@ -33,19 +33,15 @@ import { SubmitHandler } from 'react-hook-form'; /** * Hooks */ +import { useEffect, useMemo } from 'react'; import { useForm } from 'react-hook-form'; import { useRootStore } from '@educt/hooks/useRootStore'; - -/** - * Contexts - */ -import { UsersPageContext } from '@educt/contexts'; +import { useUpdateUser } from '@educt/hooks/queries'; /** * Schema */ import UpdateUserSchema from './EditUserForm.validator'; -import { useUpdateUser } from '@educt/hooks/queries'; type UpdateUserFormPropsType = { user: IUser; @@ -62,9 +58,11 @@ type UpdateUserInputType = { password: string | null; }; -const UpdateUserForm: React.FC = ({ user, onClose, isOpen }) => { - const { userStore } = useRootStore(); - const { setEditingUser } = useContext(UsersPageContext); +const UpdateUserForm: React.FC = props => { + const { user, onClose, isOpen } = props; + const { + userStore: { me }, + } = useRootStore(); const { updateUser, isLoading } = useUpdateUser(); const { register, @@ -73,18 +71,11 @@ const UpdateUserForm: React.FC = ({ user, onClose, isOp formState: { errors, isDirty, dirtyFields }, } = useForm({ resolver: yupResolver(UpdateUserSchema), - defaultValues: { - first_name: user.first_name, - last_name: user.last_name, - email: user.email, - role: user.roles[0].slug, - login: '', - password: '', - }, + defaultValues: useMemo(() => { + return props.user; + }, [props]), }); - const { me } = userStore; - if (me === null) return null; /** @@ -94,20 +85,18 @@ const UpdateUserForm: React.FC = ({ user, onClose, isOp try { const params = helpers.getDirtyFields(dirtyFields, data); await updateUser(user.id, params); - onCloseModal(); + onClose(); } catch (error: any) { console.error(error); } }; - const onCloseModal = () => { - reset({}); - setEditingUser(undefined); - onClose(); - }; + useEffect(() => { + reset(props.user); + }, [props.user]); return ( - +
@@ -187,7 +176,7 @@ const UpdateUserForm: React.FC = ({ user, onClose, isOp > Save - +
diff --git a/src/pages/users/components/UserList/UserList.tsx b/src/pages/users/components/UserList/UserList.tsx index dc85974..e5d9040 100644 --- a/src/pages/users/components/UserList/UserList.tsx +++ b/src/pages/users/components/UserList/UserList.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import { observer } from 'mobx-react'; import { ChevronLeftIcon, ChevronRightIcon } from '@chakra-ui/icons'; import { Flex, Box, Stack, Text } from '@chakra-ui/layout'; @@ -9,7 +9,7 @@ import { Button } from '@chakra-ui/button'; */ import LoadingList from '@educt/components/LoadingList'; import EditUserForm from '../EditUserForm'; -import DeleteUserDialog from '../DeleteUserDialog'; +import DeleteUserDialog from '@educt/components/Dialogs/DeleteUserDialog'; /** * Types @@ -29,26 +29,29 @@ import { useEffect, useContext } from 'react'; import { useErrorHandler } from 'react-error-boundary'; import { useRootStore } from '@educt/hooks/useRootStore'; import { useDisclosure } from '@chakra-ui/hooks'; +import { IUser } from '@educt/interfaces'; type UserListPropsType = { render: React.FC; limit?: number }; const UserList: React.FC = ({ render: Item, limit }) => { const { userStore } = useRootStore(); - const { - searchingRole, - search, - searchingPage, - setSearchingPage, - editingUser, - setEditingUser, - deletingUser, - setDeletingUser, - } = useContext(UsersPageContext); + const { searchingRole, search, searchingPage, setSearchingPage } = useContext(UsersPageContext); const handleError = useErrorHandler(); + const [deleting, setDeleting] = useState | null>(null); + const [editing, setEditing] = useState(null); const { onOpen: onOpenEditModal, onClose: onCloseEditModal, isOpen: isOpenEditModal } = useDisclosure(); const { onOpen: onOpenDeleteDialog, onClose: onCloseDeleteDialog, isOpen: isOpenDeleteDialog } = useDisclosure(); const { users, pagination, me, isLoading } = userStore; + const handleEditUser = (user: IUser) => { + setEditing(user); + onOpenEditModal(); + }; + const handleDeleteUser = (user: Pick) => { + setDeleting(user); + onOpenDeleteDialog(); + }; + /** * Fetch users handler */ @@ -69,10 +72,8 @@ const UserList: React.FC = ({ render: Item, limit }) => { return ( - {editingUser && } - {deletingUser && ( - - )} + {editing && } + {deleting && } {users.length !== 0 ? ( <> @@ -86,14 +87,8 @@ const UserList: React.FC = ({ render: Item, limit }) => { { - setEditingUser(user); - onOpenEditModal(); - }} - onDelete={user => { - setDeletingUser(user); - onOpenDeleteDialog(); - }} + onEdit={user => handleEditUser(user)} + onDelete={user => handleDeleteUser({ id: user.id, fullname: user.fullname })} /> ))} diff --git a/src/providers/index.tsx b/src/providers/index.tsx index 65b21cb..a9daa1b 100644 --- a/src/providers/index.tsx +++ b/src/providers/index.tsx @@ -35,8 +35,6 @@ export const UsersPageContextProvider: React.FC = ({ children }) => { const [searchingRole, setSearchingRole] = useState(undefined); const [searchingPage, setSearchingPage] = useState(1); const [search, setSearch] = useState(undefined); - const [editingUser, setEditingUser] = useState(undefined); - const [deletingUser, setDeletingUser] = useState(undefined); return ( { setSearchingPage, search, setSearch, - editingUser, - setEditingUser, - deletingUser, - setDeletingUser, }} > {children} @@ -64,7 +58,6 @@ export const UsersPageContextProvider: React.FC = ({ children }) => { export const CoursesPageContextProvider: React.FC = ({ children }) => { const [selectedCategory, setSelectedCategory] = useState(undefined); const [courseStatus, setCourseStatus] = useState(undefined); - const [deletingCourse, setDeletingCourse] = useState | undefined>(undefined); const context = React.useMemo( () => ({ @@ -72,10 +65,8 @@ export const CoursesPageContextProvider: React.FC = ({ children }) => { setCourseStatus, selectedCategory, setSelectedCategory, - deletingCourse, - setDeletingCourse, }), - [courseStatus, setCourseStatus, selectedCategory, setSelectedCategory, deletingCourse, setDeletingCourse] + [courseStatus, setCourseStatus, selectedCategory, setSelectedCategory] ); return {children}; diff --git a/src/stores/pages/EditCourseStore.ts b/src/stores/pages/EditCourseStore.ts index 4850a6d..3a3d586 100644 --- a/src/stores/pages/EditCourseStore.ts +++ b/src/stores/pages/EditCourseStore.ts @@ -1,6 +1,6 @@ import { makeAutoObservable, runInAction } from 'mobx'; import * as helpers from '@educt/helpers'; -import { ICourse, ILesson } from '@educt/interfaces'; +import { ICourse } from '@educt/interfaces'; import PageStore from '../PageStore'; import { CourseServiceInstance, LessonServiceInstance } from '@educt/services'; @@ -10,12 +10,6 @@ export default class EditCourseStore { public course: Omit | null = null; - public isDeleting: boolean = false; - - public deletingCourse: Pick | undefined; - - public deletingLesson: ILesson | undefined; - constructor(pageStore: PageStore) { this.pageStore = pageStore; @@ -75,12 +69,4 @@ export default class EditCourseStore { this.course.lessons = helpers.arrayMove(this.course.lessons, oldIndex, newIndex); } } - - public setDeletingLesson(lesson: ILesson) { - this.deletingLesson = lesson; - } - - public setDeletingCourse(course: Pick | undefined) { - this.deletingCourse = course; - } } diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 26c52fa..6dc0175 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -71,23 +71,11 @@ export type UsersPageContextType = { search: string | undefined; setSearch: React.Dispatch>; - /** - * Set editing user state when press on edit button - */ - editingUser: IUser | undefined; - setEditingUser: React.Dispatch>; - /** * Search users by page */ searchingPage: number; setSearchingPage: React.Dispatch>; - - /** - * Set deleting user state when press on delete button - */ - deletingUser: IUser | undefined; - setDeletingUser: React.Dispatch>; }; export type CoursesPageContextType = { @@ -102,12 +90,6 @@ export type CoursesPageContextType = { */ courseStatus: CourseStatusEnum | undefined; setCourseStatus: React.Dispatch>; - - /** - * Set deleting course state when press on delete button - */ - deletingCourse: Pick | undefined; - setDeletingCourse: React.Dispatch | undefined>>; }; export type FetchCoursesParams = { /** From 093247899afeac59cf8243c7a6a84b5ee6f9fb4e Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Sun, 5 Dec 2021 23:11:27 +0300 Subject: [PATCH 09/87] feat: added students table list --- .../AddStudentsModal/AddSudentsModal.tsx | 51 ++++++++ .../Modals/AddStudentsModal/index.ts | 3 + src/interfaces/index.ts | 6 +- .../components/StudentList/StudentItem.tsx | 16 --- .../components/StudentList/StudentList.tsx | 36 ------ .../edit/components/StudentList/index.ts | 4 - .../StudentTableList/BulkActionsMenu.tsx | 45 +++++++ .../StudentTableList/Cells/index.tsx | 68 ++++++++++ .../StudentTableList/StudentTableHead.tsx | 44 +++++++ .../StudentTableList/StudentTableList.tsx | 119 ++++++++++++++++++ .../StudentTableList/StudentTableRow.tsx | 27 ++++ .../edit/components/StudentTableList/index.ts | 4 + src/pages/courses/edit/edit.tsx | 6 +- src/stores/UIStore.ts | 2 +- 14 files changed, 369 insertions(+), 62 deletions(-) create mode 100644 src/components/Modals/AddStudentsModal/AddSudentsModal.tsx create mode 100644 src/components/Modals/AddStudentsModal/index.ts delete mode 100644 src/pages/courses/edit/components/StudentList/StudentItem.tsx delete mode 100644 src/pages/courses/edit/components/StudentList/StudentList.tsx delete mode 100644 src/pages/courses/edit/components/StudentList/index.ts create mode 100644 src/pages/courses/edit/components/StudentTableList/BulkActionsMenu.tsx create mode 100644 src/pages/courses/edit/components/StudentTableList/Cells/index.tsx create mode 100644 src/pages/courses/edit/components/StudentTableList/StudentTableHead.tsx create mode 100644 src/pages/courses/edit/components/StudentTableList/StudentTableList.tsx create mode 100644 src/pages/courses/edit/components/StudentTableList/StudentTableRow.tsx create mode 100644 src/pages/courses/edit/components/StudentTableList/index.ts diff --git a/src/components/Modals/AddStudentsModal/AddSudentsModal.tsx b/src/components/Modals/AddStudentsModal/AddSudentsModal.tsx new file mode 100644 index 0000000..5887933 --- /dev/null +++ b/src/components/Modals/AddStudentsModal/AddSudentsModal.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { Button } from '@chakra-ui/button'; +import { Text, Flex } from '@chakra-ui/react'; +import { + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalFooter, + ModalBody, + ModalCloseButton, +} from '@chakra-ui/react'; +import { useDisclosure, Divider } from '@chakra-ui/react'; +import { MdGroup } from 'react-icons/md'; + +type AddStudentsModalPropsType = {}; + +const AddStudentsModal: React.FC = () => { + const { isOpen, onOpen, onClose } = useDisclosure(); + + return ( + <> + + + + + + + + Add students + + + + + + + + + + + + + + ); +}; + +export default AddStudentsModal; diff --git a/src/components/Modals/AddStudentsModal/index.ts b/src/components/Modals/AddStudentsModal/index.ts new file mode 100644 index 0000000..2dc8b48 --- /dev/null +++ b/src/components/Modals/AddStudentsModal/index.ts @@ -0,0 +1,3 @@ +import AddStudentsModal from './AddSudentsModal'; + +export default AddStudentsModal; diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index 9b2f49f..8dd813c 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -23,10 +23,10 @@ export interface ICourse { title: string; description: string; status: CourseStatusEnum; - teacher: Pick; + teacher: Pick; category: ICategory; lessons: ILesson[]; - students: Array>; + students: Array>; color: ColorType | null; students_count: string; likes_count: string; @@ -92,6 +92,8 @@ export interface IUser { email: string; roles: IUserRole[]; contacts: IUserContacts | null; + created_at: string; + updated_at: string; } /** diff --git a/src/pages/courses/edit/components/StudentList/StudentItem.tsx b/src/pages/courses/edit/components/StudentList/StudentItem.tsx deleted file mode 100644 index 96083f1..0000000 --- a/src/pages/courses/edit/components/StudentList/StudentItem.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from 'react'; - -/** - * Types - */ -import { IUser } from '@educt/interfaces'; - -export type StudentItemPropsType = { - student: Pick; -}; - -const StudentItem: React.FC = ({ student }) => { - return
{student.fullname}
; -}; - -export default StudentItem; diff --git a/src/pages/courses/edit/components/StudentList/StudentList.tsx b/src/pages/courses/edit/components/StudentList/StudentList.tsx deleted file mode 100644 index 916dc57..0000000 --- a/src/pages/courses/edit/components/StudentList/StudentList.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react'; -import { Stack, Box, Text, Button } from '@chakra-ui/react'; - -/** - * Types - */ -import { StudentItemPropsType } from './StudentItem'; -import { ICourse } from '@educt/interfaces'; - -type StudentListPropsType = { - render: React.FC; - students: ICourse['students']; -}; - -const StudentList: React.FC = ({ render: Item, students }) => { - return ( - - {students.length !== 0 ? ( - - {students.map(student => ( - - ))} - - ) : ( - - There are no students in the course yet - - - )} - - ); -}; - -export default StudentList; diff --git a/src/pages/courses/edit/components/StudentList/index.ts b/src/pages/courses/edit/components/StudentList/index.ts deleted file mode 100644 index afb24b8..0000000 --- a/src/pages/courses/edit/components/StudentList/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import StudentList from './StudentList'; -import StudentItem from './StudentItem'; - -export { StudentList, StudentItem }; diff --git a/src/pages/courses/edit/components/StudentTableList/BulkActionsMenu.tsx b/src/pages/courses/edit/components/StudentTableList/BulkActionsMenu.tsx new file mode 100644 index 0000000..4542cf5 --- /dev/null +++ b/src/pages/courses/edit/components/StudentTableList/BulkActionsMenu.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { Button, Menu, MenuButton, MenuList, MenuItem } from '@chakra-ui/react'; +import { ChevronDownIcon, DeleteIcon } from '@chakra-ui/icons'; +import { MdRemove, MdImportExport } from 'react-icons/md'; + +import type { ICourse } from '@educt/interfaces'; + +type BulkActionsMenuPropsType = { + selected: ICourse['students']; + rows: ICourse['students']; + setRows: React.Dispatch>; +}; + +const BulkActionsMenu: React.FC = ({ selected, rows, setRows }) => { + const handleDetach = async () => { + console.log( + `Detaching from course: `, + selected.map(s => s.id) + ); + setRows(rows.filter(r => selected.every(s => s.id !== r.id))); + }; + const handleDelete = async () => {}; + const handleExport = async () => {}; + + return ( + + }> + Bulk Actions + + + }> + Remove from course + + } isDisabled> + Export + + } color='red' isDisabled> + Delete + + + + ); +}; + +export default BulkActionsMenu; diff --git a/src/pages/courses/edit/components/StudentTableList/Cells/index.tsx b/src/pages/courses/edit/components/StudentTableList/Cells/index.tsx new file mode 100644 index 0000000..fbc067d --- /dev/null +++ b/src/pages/courses/edit/components/StudentTableList/Cells/index.tsx @@ -0,0 +1,68 @@ +import React, { memo } from 'react'; +import { Link as ReactRouterLink } from 'react-router-dom'; +import { Flex, Box, Avatar, Td, Checkbox, Link, Text, IconButton } from '@chakra-ui/react'; +import { Menu, MenuList, MenuButton, MenuItem } from '@chakra-ui/react'; +import { MdEdit, MdMoreHoriz, MdRemove } from 'react-icons/md'; +import { DeleteIcon } from '@chakra-ui/icons'; + +/** + * Types + */ +import type { StudentTableRowPropsType } from '../StudentTableRow'; + +/** + * Hooks + */ +import { useColorModeValue } from '@chakra-ui/react'; +import { IUser } from '@educt/interfaces'; + +type CheckBoxCellPropsType = Pick; +export const CheckBoxCell: React.FC = ({ isSelected, onSelect, student }) => ( + + onSelect(e, student)} /> + +); + +type InfoCellPropsType = Pick; +export const InfoCell: React.FC = memo(({ id, fullname, email }) => ( + + + + + + {fullname} + + + {email} + + + + +)); + +type RegisteredCellPropsType = { registered: string }; +export const RegisteredCell: React.FC = memo(({ registered }) => ( + {new Date(registered).toLocaleDateString()} +)); + +type ActionsCellPropsType = Pick; +export const ActionsCell: React.FC = memo(({ id }) => ( + + + } variant='ghost' /> + + }>Edit + }>Remove from course + } color='red.400'> + Delete + + + + +)); diff --git a/src/pages/courses/edit/components/StudentTableList/StudentTableHead.tsx b/src/pages/courses/edit/components/StudentTableList/StudentTableHead.tsx new file mode 100644 index 0000000..0483830 --- /dev/null +++ b/src/pages/courses/edit/components/StudentTableList/StudentTableHead.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { Icon, Flex, Checkbox, Text } from '@chakra-ui/react'; +import { Thead, Tr, Th } from '@chakra-ui/react'; +import { MdSettings, MdSupervisorAccount } from 'react-icons/md'; + +/** + * Hooks + */ +import { useColorModeValue } from '@chakra-ui/react'; + +type StudentTableHeadPropsType = { + onSelectAll?: (e: React.ChangeEvent) => void; +}; + +const StudentTableHead: React.FC = ({ onSelectAll }) => { + return ( + + + + + + + + + + Name + + + + Registered + + + + + Actions + + + + + + ); +}; + +export default StudentTableHead; diff --git a/src/pages/courses/edit/components/StudentTableList/StudentTableList.tsx b/src/pages/courses/edit/components/StudentTableList/StudentTableList.tsx new file mode 100644 index 0000000..5289beb --- /dev/null +++ b/src/pages/courses/edit/components/StudentTableList/StudentTableList.tsx @@ -0,0 +1,119 @@ +import React from 'react'; +import { Table, Tbody, Button, Menu, MenuButton, MenuList, MenuItem } from '@chakra-ui/react'; +import { Input, InputGroup, InputLeftElement } from '@chakra-ui/react'; +import { Flex, Box, Text } from '@chakra-ui/react'; +import { DeleteIcon, ChevronDownIcon } from '@chakra-ui/icons'; +import { MdSearch, MdImportExport, MdRemove } from 'react-icons/md'; + +import AddStudentsModal from '@educt/components/Modals/AddStudentsModal'; +import StudentTableHead from './StudentTableHead'; + +/** + * Types + */ +import type { StudentTableRowPropsType } from './StudentTableRow'; +import type { ICourse } from '@educt/interfaces'; + +/** + * Hooks + */ +import { useCallback, useState } from 'react'; +import BulkActionsMenu from './BulkActionsMenu'; + +type StudentTableListPropsType = { + render: React.FC; + students: ICourse['students']; +}; + +const StudentTableList: React.FC = ({ render: Row, students }) => { + const [rows, setRows] = useState(students); + const [selected, setSelected] = useState([]); + const [search, setSearch] = useState(''); + + /** + * Search for a student by input + */ + const handleSearch = (e: React.ChangeEvent) => { + setSearch(e.target.value); + setRows([ + ...students.filter( + s => s.fullname.toLowerCase().includes(e.target.value) || s.email.toLowerCase().includes(e.target.value) + ), + ]); + }; + + /** + * Select all students in a table + */ + const handleSelectAll = (e: React.ChangeEvent) => { + if (e.target.checked) { + setSelected(rows); + return; + } + + setSelected([]); + }; + + /** + * Select a student with checkbox + */ + const handleSelect = useCallback((e: React.ChangeEvent, student: ICourse['students'][number]) => { + if (e.target.checked) { + setSelected(prev => [...prev, student]); + return; + } + + setSelected(prev => prev.filter(s => s.id !== student.id)); + }, []); + + if (rows.length === 0) { + return ( + + There are no students in the course yet + + + ); + } + + return ( + + + + + } /> + + + + + + {selected.length !== 0 && ( + <> + + You have selected {selected.length} student(s). + + + + )} + + + + + + + {rows.map(student => ( + s.id === student.id)} + onSelect={handleSelect} + /> + ))} + +
+
+ ); +}; + +export default StudentTableList; diff --git a/src/pages/courses/edit/components/StudentTableList/StudentTableRow.tsx b/src/pages/courses/edit/components/StudentTableList/StudentTableRow.tsx new file mode 100644 index 0000000..9468705 --- /dev/null +++ b/src/pages/courses/edit/components/StudentTableList/StudentTableRow.tsx @@ -0,0 +1,27 @@ +import React, { memo } from 'react'; +import { Tr } from '@chakra-ui/react'; + +/** + * Cells + */ +import { CheckBoxCell, InfoCell, RegisteredCell, ActionsCell } from './Cells'; +import { ICourse } from '@educt/interfaces'; + +export type StudentTableRowPropsType = { + isSelected: boolean; + student: ICourse['students'][number]; + onSelect: (e: React.ChangeEvent, student: ICourse['students'][number]) => void; +}; + +const StudentTableItem: React.FC = ({ student, isSelected, onSelect }) => { + return ( + + + + + + + ); +}; + +export default memo(StudentTableItem); diff --git a/src/pages/courses/edit/components/StudentTableList/index.ts b/src/pages/courses/edit/components/StudentTableList/index.ts new file mode 100644 index 0000000..deba552 --- /dev/null +++ b/src/pages/courses/edit/components/StudentTableList/index.ts @@ -0,0 +1,4 @@ +import StudentTableList from './StudentTableList'; +import StudentTableRow from './StudentTableRow'; + +export { StudentTableList, StudentTableRow }; diff --git a/src/pages/courses/edit/edit.tsx b/src/pages/courses/edit/edit.tsx index 878f833..d6e02b6 100644 --- a/src/pages/courses/edit/edit.tsx +++ b/src/pages/courses/edit/edit.tsx @@ -15,9 +15,9 @@ import type { IPageProps } from '@educt/interfaces'; import LoadingPage from '@educt/components/LoadingPage'; import { EditCourseForm } from '@educt/components/Forms/CourseForm'; import SetCourseStatusButton from '@educt/components/SetCourseStatusButton'; -import LessonList from './components/LessonList'; -import { StudentList, StudentItem } from './components/StudentList'; import DeleteCourseDialog from '@educt/components/Dialogs/DeleteCourseDialog'; +import { StudentTableList, StudentTableRow } from './components/StudentTableList'; +import LessonList from './components/LessonList'; /** * Hooks @@ -96,7 +96,7 @@ const EditCoursePage: React.FC = () => { - + diff --git a/src/stores/UIStore.ts b/src/stores/UIStore.ts index 9605cdf..d3022fc 100644 --- a/src/stores/UIStore.ts +++ b/src/stores/UIStore.ts @@ -35,7 +35,7 @@ export default class UIStore { } public setLocation(location: string): void { - console.log(`[history]: location changed to "${location}"`); this.location = location; + console.info(`[history]: location changed to "${location}"`); } } From 4368498dcda93627c49b9aecc8fe5ae08fb054ab Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Mon, 6 Dec 2021 00:03:22 +0300 Subject: [PATCH 10/87] refactor: add students modal --- .../AddStudentsModal/AddSudentsModal.tsx | 12 +++---- .../edit/components/LessonList/LessonList.tsx | 10 +++--- .../StudentTableList/StudentTableList.tsx | 33 ++++++++++++------- 3 files changed, 31 insertions(+), 24 deletions(-) diff --git a/src/components/Modals/AddStudentsModal/AddSudentsModal.tsx b/src/components/Modals/AddStudentsModal/AddSudentsModal.tsx index 5887933..5a6ec6c 100644 --- a/src/components/Modals/AddStudentsModal/AddSudentsModal.tsx +++ b/src/components/Modals/AddStudentsModal/AddSudentsModal.tsx @@ -13,16 +13,14 @@ import { import { useDisclosure, Divider } from '@chakra-ui/react'; import { MdGroup } from 'react-icons/md'; -type AddStudentsModalPropsType = {}; - -const AddStudentsModal: React.FC = () => { - const { isOpen, onOpen, onClose } = useDisclosure(); +type AddStudentsModalPropsType = { + isOpen: boolean; + onClose: () => void; +}; +const AddStudentsModal: React.FC = ({ isOpen, onClose }) => { return ( <> - diff --git a/src/pages/courses/edit/components/LessonList/LessonList.tsx b/src/pages/courses/edit/components/LessonList/LessonList.tsx index cd6694e..8ef8946 100644 --- a/src/pages/courses/edit/components/LessonList/LessonList.tsx +++ b/src/pages/courses/edit/components/LessonList/LessonList.tsx @@ -46,7 +46,7 @@ const CreateLessonButton: React.FC<{ id: string }> = ({ id }) => { const handleCreateLesson = (): void => history.push(`${id}/create-lesson`); return ( - ); @@ -206,7 +206,9 @@ const LessonList: React.FC = ({ course }) => { )} Total: ({lessons.length}) - + = ({ course }) => { ) : ( No lessons have been added to this course yet - - - + )}
diff --git a/src/pages/courses/edit/components/StudentTableList/StudentTableList.tsx b/src/pages/courses/edit/components/StudentTableList/StudentTableList.tsx index 5289beb..8ab4406 100644 --- a/src/pages/courses/edit/components/StudentTableList/StudentTableList.tsx +++ b/src/pages/courses/edit/components/StudentTableList/StudentTableList.tsx @@ -1,11 +1,9 @@ import React from 'react'; -import { Table, Tbody, Button, Menu, MenuButton, MenuList, MenuItem } from '@chakra-ui/react'; -import { Input, InputGroup, InputLeftElement } from '@chakra-ui/react'; -import { Flex, Box, Text } from '@chakra-ui/react'; -import { DeleteIcon, ChevronDownIcon } from '@chakra-ui/icons'; -import { MdSearch, MdImportExport, MdRemove } from 'react-icons/md'; +import { Table, Tbody, Button, Input, InputGroup, InputLeftElement, Flex, Box, Text } from '@chakra-ui/react'; +import { MdSearch } from 'react-icons/md'; import AddStudentsModal from '@educt/components/Modals/AddStudentsModal'; +import BulkActionsMenu from './BulkActionsMenu'; import StudentTableHead from './StudentTableHead'; /** @@ -18,7 +16,7 @@ import type { ICourse } from '@educt/interfaces'; * Hooks */ import { useCallback, useState } from 'react'; -import BulkActionsMenu from './BulkActionsMenu'; +import { useDisclosure } from '@chakra-ui/react'; type StudentTableListPropsType = { render: React.FC; @@ -29,6 +27,11 @@ const StudentTableList: React.FC = ({ render: Row, st const [rows, setRows] = useState(students); const [selected, setSelected] = useState([]); const [search, setSearch] = useState(''); + const { + isOpen: isOpenAddStudentModal, + onOpen: onOpenAddStudentModal, + onClose: onCloseAddStudentModal, + } = useDisclosure(); /** * Search for a student by input @@ -68,15 +71,21 @@ const StudentTableList: React.FC = ({ render: Row, st if (rows.length === 0) { return ( - - There are no students in the course yet - - + <> + + + There are no students in the course yet + + + ); } return ( + @@ -94,12 +103,12 @@ const StudentTableList: React.FC = ({ render: Row, st )} - - +
{rows.map(student => ( From 2c0bf3a8c5de475ae0183f423325c754e672e278 Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Mon, 6 Dec 2021 21:07:26 +0300 Subject: [PATCH 11/87] refactor: auth page layout --- src/components/Card/Card.tsx | 15 ++++ src/components/Card/index.ts | 3 + src/components/Fields/PasswordField.tsx | 76 +++++++++++++++++ src/components/Logo/Logo.tsx | 28 +++++++ src/components/Logo/index.ts | 3 + src/pages/auth/auth.tsx | 21 +++-- .../auth/components/AuthForm/AuthForm.tsx | 81 ++++++++----------- 7 files changed, 175 insertions(+), 52 deletions(-) create mode 100644 src/components/Card/Card.tsx create mode 100644 src/components/Card/index.ts create mode 100644 src/components/Fields/PasswordField.tsx create mode 100644 src/components/Logo/Logo.tsx create mode 100644 src/components/Logo/index.ts diff --git a/src/components/Card/Card.tsx b/src/components/Card/Card.tsx new file mode 100644 index 0000000..4978adb --- /dev/null +++ b/src/components/Card/Card.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { Box, BoxProps, useColorModeValue } from '@chakra-ui/react'; + +const Card: React.FC = (props: BoxProps) => ( + +); + +export default Card; diff --git a/src/components/Card/index.ts b/src/components/Card/index.ts new file mode 100644 index 0000000..8c371a2 --- /dev/null +++ b/src/components/Card/index.ts @@ -0,0 +1,3 @@ +import Card from './Card'; + +export { Card }; diff --git a/src/components/Fields/PasswordField.tsx b/src/components/Fields/PasswordField.tsx new file mode 100644 index 0000000..e14006a --- /dev/null +++ b/src/components/Fields/PasswordField.tsx @@ -0,0 +1,76 @@ +import React, { useRef } from 'react'; +import { + Box, + Flex, + FormControl, + FormLabel, + Text, + IconButton, + Input, + InputGroup, + InputProps, + InputRightElement, + InputLeftElement, + Icon, + useDisclosure, + useMergeRefs, + useColorModeValue as mode, +} from '@chakra-ui/react'; +import { HiEye, HiEyeOff } from 'react-icons/hi'; +import { MdVpnKey } from 'react-icons/md'; + +interface PasswordFieldPropsType extends InputProps { + errorMessage?: string | undefined; +} + +export const PasswordField = React.forwardRef((props, ref) => { + const { errorMessage, ...other } = props; + const { isOpen, onToggle } = useDisclosure(); + const inputRef = useRef(null); + const mergeRef = useMergeRefs(inputRef, ref); + + const onClickReveal = () => { + onToggle(); + const input = inputRef.current; + if (input) { + input.focus({ preventScroll: true }); + const length = input.value.length * 2; + requestAnimationFrame(() => { + input.setSelectionRange(length, length); + }); + } + }; + + return ( + + + Password + + + } /> + + : } + onClick={onClickReveal} + /> + + + + {errorMessage && ( + + {errorMessage} + + )} + + ); +}); diff --git a/src/components/Logo/Logo.tsx b/src/components/Logo/Logo.tsx new file mode 100644 index 0000000..bc90f5f --- /dev/null +++ b/src/components/Logo/Logo.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { chakra, HTMLChakraProps, useColorModeValue, useToken } from '@chakra-ui/react'; + +const Logo: React.FC> = props => { + const [white, black] = useToken('colors', ['white', 'gray.800']); + return ( + + + + + ); +}; + +export default Logo; diff --git a/src/components/Logo/index.ts b/src/components/Logo/index.ts new file mode 100644 index 0000000..72acb33 --- /dev/null +++ b/src/components/Logo/index.ts @@ -0,0 +1,3 @@ +import Logo from './Logo'; + +export default Logo; diff --git a/src/pages/auth/auth.tsx b/src/pages/auth/auth.tsx index da894e9..94d7bdc 100644 --- a/src/pages/auth/auth.tsx +++ b/src/pages/auth/auth.tsx @@ -2,7 +2,7 @@ import React from 'react'; import Helmet from 'react-helmet'; import { observer } from 'mobx-react'; import { Redirect } from 'react-router-dom'; -import { Flex } from '@chakra-ui/react'; +import { Box, Heading, Text, Link } from '@chakra-ui/react'; /** * Types @@ -12,12 +12,15 @@ import { IPageProps } from '@educt/interfaces'; /** * Components */ +import Logo from '@educt/components/Logo'; import AuthForm from './components/AuthForm'; +import { Card } from '@educt/components/Card'; /** * Hooks */ import { useRootStore } from '@educt/hooks/useRootStore'; +import { useColorModeValue } from '@chakra-ui/react'; /** * Auth page @@ -34,14 +37,20 @@ const AuthPage: React.FC = ({ title }) => { } return ( - <> + {title} - - - - + + + + Login to your account + + + + + + ); }; diff --git a/src/pages/auth/components/AuthForm/AuthForm.tsx b/src/pages/auth/components/AuthForm/AuthForm.tsx index c49d82d..4b81387 100644 --- a/src/pages/auth/components/AuthForm/AuthForm.tsx +++ b/src/pages/auth/components/AuthForm/AuthForm.tsx @@ -8,10 +8,10 @@ import { Button, Icon, Text, - Flex, + Stack, Box, } from '@chakra-ui/react'; -import { MdAccountCircle, MdSchool, MdVpnKey } from 'react-icons/md'; +import { MdAccountCircle, MdVpnKey } from 'react-icons/md'; import { yupResolver } from '@hookform/resolvers/yup'; /** @@ -29,6 +29,7 @@ import { useLogin } from '@educt/hooks/queries'; * Schema */ import LoginSchema from './AuthForm.validator'; +import { PasswordField } from '@educt/components/Fields/PasswordField'; type AuthInputType = { login: string; @@ -52,52 +53,40 @@ const AuthForm: React.FC = () => { const onSubmit: SubmitHandler = data => login(data.login, data.password).finally(() => reset()); return ( - -
- - - - - - - - - Login - - } /> - - - - {errors.login?.message} - - + + + + Login + + } /> + + + + {errors.login?.message} + + - - Password - - } /> - - - - {errors.password?.message} - - + - - - - -
+ + + ); }; From 677e4d9ef5520d9905a9b81b179dd0147f5a6019 Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Mon, 6 Dec 2021 22:25:01 +0300 Subject: [PATCH 12/87] refactor: nav layout --- package-lock.json | 14 +---- package.json | 2 +- src/components/Header/Header.tsx | 37 ++++------- ...otifications.tsx => NotificationsMenu.tsx} | 4 +- .../Header/{UserInfo.tsx => ProfileMenu.tsx} | 4 +- src/components/Nav/Desktop/index.tsx | 17 ++---- src/components/Nav/Mobile/index.tsx | 6 +- src/components/Nav/NavLink.tsx | 61 ++++++++++--------- src/config.ts | 22 ++++--- src/interfaces/index.ts | 1 + 10 files changed, 79 insertions(+), 89 deletions(-) rename src/components/Header/{UserNotifications.tsx => NotificationsMenu.tsx} (91%) rename src/components/Header/{UserInfo.tsx => ProfileMenu.tsx} (95%) diff --git a/package-lock.json b/package-lock.json index 5038596..84556ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1337,11 +1337,6 @@ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" }, - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" - }, "caniuse-lite": { "version": "1.0.30001267", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001267.tgz", @@ -2155,12 +2150,9 @@ "integrity": "sha512-7XhbCr7d9fDC1TgcK/BUbt7D3q0VJMu7jPErfsa0JrxVjv/nni41xWdJcy0Zb7R+Np8OsCkQ2lMyloAtE3DLiQ==" }, "react-icons": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-3.11.0.tgz", - "integrity": "sha512-JRgiI/vdF6uyBgyZhVyYJUZAop95Sy4XDe/jmT3R/bKliFWpO/uZBwvSjWEdxwzec7SYbEPNPck0Kff2tUGM2Q==", - "requires": { - "camelcase": "^5.0.0" - } + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.3.1.tgz", + "integrity": "sha512-cB10MXLTs3gVuXimblAdI71jrJx8njrJZmNMEMC+sQu5B/BIOmlsAjskdqpn81y8UBVEGuHODd7/ci5DvoSzTQ==" }, "react-is": { "version": "16.13.1", diff --git a/package.json b/package.json index 95776c8..db21b00 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "react-error-boundary": "^3.1.3", "react-helmet": "^6.1.0", "react-hook-form": "^7.12.2", - "react-icons": "^3.11.0", + "react-icons": "^4.3.1", "react-router-dom": "^5.2.0", "react-spinners": "^0.11.0", "react-virtuoso": "^2.2.8", diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index 9081cee..9cae6d4 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -1,25 +1,24 @@ import React from 'react'; import { observer } from 'mobx-react'; -import { Flex, Box, Heading, useColorMode } from '@chakra-ui/react'; -import { MdSchool } from 'react-icons/md'; -import config from '@educt/config'; +import { Flex, Box } from '@chakra-ui/react'; /** * Components */ +import Logo from '@educt/components/Logo'; import NavMobile from '@educt/components/Nav/Mobile'; import ColorModeSwitcher from '@educt/components/ColorModeSwitcher'; -import UserInfo from './UserInfo'; -import UserNotifications from './UserNotifications'; +import ProfileMenu from './ProfileMenu'; +import NotificationsMenu from './NotificationsMenu'; /** * Hooks */ import { useMediaQuery } from '@chakra-ui/media-query'; +import { useColorModeValue } from '@chakra-ui/color-mode'; const Header: React.FC = () => { const [isDesktop] = useMediaQuery('(min-width: 992px)'); - const { colorMode } = useColorMode(); return ( { top={0} left={0} right={0} - // borderBottomWidth={1} - // h='43px' - boxShadow='md' - backgroundColor={colorMode === 'dark' ? 'gray.800' : 'white'} + borderBottomWidth='1px' + backgroundColor={useColorModeValue('white', 'gray.800')} zIndex={4} > { alignItems='center' padding='5px 1.5rem 5px 1.5rem' > - - - - - - - {config.metaData.appName} - - - - {isDesktop ? ( + + {isDesktop && ( - - + + - ) : ( - )} + {!isDesktop && } ); diff --git a/src/components/Header/UserNotifications.tsx b/src/components/Header/NotificationsMenu.tsx similarity index 91% rename from src/components/Header/UserNotifications.tsx rename to src/components/Header/NotificationsMenu.tsx index c21e39c..a35d7cc 100644 --- a/src/components/Header/UserNotifications.tsx +++ b/src/components/Header/NotificationsMenu.tsx @@ -6,7 +6,7 @@ import { MdNotifications } from 'react-icons/md'; import { useRootStore } from '@educt/hooks/useRootStore'; import { observer } from 'mobx-react'; -const UserNotifications: React.FC = () => { +const NotificationsMenu: React.FC = () => { const { userStore } = useRootStore(); if (userStore.me === null) { @@ -39,4 +39,4 @@ const UserNotifications: React.FC = () => { ); }; -export default observer(UserNotifications); +export default observer(NotificationsMenu); diff --git a/src/components/Header/UserInfo.tsx b/src/components/Header/ProfileMenu.tsx similarity index 95% rename from src/components/Header/UserInfo.tsx rename to src/components/Header/ProfileMenu.tsx index 80213a8..cd44d81 100644 --- a/src/components/Header/UserInfo.tsx +++ b/src/components/Header/ProfileMenu.tsx @@ -24,7 +24,7 @@ import UserBadge from '@educt/components/UserBadge'; */ import { useLogout } from '@educt/hooks/queries'; -const UserInfo: React.FC = () => { +const ProfileMenu: React.FC = () => { const { userStore } = useRootStore(); const { logout } = useLogout(); @@ -64,4 +64,4 @@ const UserInfo: React.FC = () => { ); }; -export default observer(UserInfo); +export default observer(ProfileMenu); diff --git a/src/components/Nav/Desktop/index.tsx b/src/components/Nav/Desktop/index.tsx index ec33bb5..445bd3b 100644 --- a/src/components/Nav/Desktop/index.tsx +++ b/src/components/Nav/Desktop/index.tsx @@ -37,17 +37,10 @@ const Nav: React.FC = () => { zIndex='1' backgroundColor={colorMode === 'dark' ? 'gray.800' : 'white'} > - - + + + + @@ -55,7 +48,7 @@ const Nav: React.FC = () => { - + diff --git a/src/components/Nav/Mobile/index.tsx b/src/components/Nav/Mobile/index.tsx index 31dcca0..e55b2fc 100644 --- a/src/components/Nav/Mobile/index.tsx +++ b/src/components/Nav/Mobile/index.tsx @@ -13,6 +13,7 @@ import config from '@educt/config'; * Components */ import NavList from '../NavList'; +import Logo from '@educt/components/Logo'; /** * Hooks @@ -37,7 +38,10 @@ const NavMobile: React.FC = () => { - + + + + {me !== null ? ( diff --git a/src/components/Nav/NavLink.tsx b/src/components/Nav/NavLink.tsx index 1b1957f..560449c 100644 --- a/src/components/Nav/NavLink.tsx +++ b/src/components/Nav/NavLink.tsx @@ -1,62 +1,67 @@ import React from 'react'; import { observer } from 'mobx-react'; -import { userContainRoles } from '@educt/helpers'; -import { Box, useColorMode } from '@chakra-ui/react'; +import * as helpers from '@educt/helpers'; +import { Flex, Box, Icon, Text } from '@chakra-ui/react'; + +/** + * Types + */ import type { LinkType } from '@educt/types'; /** * Hooks */ import { useRootStore } from '@educt/hooks/useRootStore'; +import { useColorMode } from '@chakra-ui/react'; type NavLinkPropsType = { link: LinkType; onClickLink: (link: LinkType) => void; }; -/** - * Returns the link component for navigation. - */ const NavLink: React.FC = ({ link, onClickLink }) => { const { uiStore, userStore } = useRootStore(); const { colorMode } = useColorMode(); + const isActive = uiStore.location === link.location; const onClick = () => onClickLink(link); if (!link.public && link.roles !== undefined) { const { me } = userStore; if (me === null) return null; - if (!userContainRoles(me.roles, link.roles)) return null; + if (!helpers.userContainRoles(me.roles, link.roles)) return null; } return ( - - - {link.title} - + + + + {link.title} + + ); }; diff --git a/src/config.ts b/src/config.ts index 06102fe..99c03cf 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,5 +1,12 @@ import { UserRoleEnum } from '@educt/enums'; -import { MdAccountCircle, MdCollectionsBookmark, MdGroup, MdHome, MdMessage, MdList } from 'react-icons/md'; +import { + MdOutlineHome, + MdOutlineMessage, + MdOutlineCollectionsBookmark, + MdOutlineVideoLibrary, + MdOutlineGroup, + MdOutlineAccountCircle, +} from 'react-icons/md'; import { IAppConfig } from './interfaces'; import packageJson from '../package.json'; @@ -11,28 +18,29 @@ const config: IAppConfig = { appName: 'Educt', appDescription: 'Learning management system', appVersion: packageJson.version, + appGithubLink: 'https://github.com/sergeyyarkov/educt-client', }, /** * Links that are rendered in the navigation */ links: [ - { location: '/', title: 'Main', public: true, icon: MdHome }, - { location: '/courses', title: 'Courses', public: true, icon: MdCollectionsBookmark }, + { location: '/', title: 'Main', public: true, icon: MdOutlineHome }, + { location: '/courses', title: 'Courses', public: true, icon: MdOutlineCollectionsBookmark }, { location: '/lessons', title: 'Lessons', public: false, - icon: MdList, + icon: MdOutlineVideoLibrary, roles: [UserRoleEnum.ADMIN, UserRoleEnum.TEACHER], }, - { location: '/messages', title: 'Messages', public: true, icon: MdMessage }, - { location: '/profile', title: 'Profile', public: true, icon: MdAccountCircle }, + { location: '/messages', title: 'Messages', public: true, icon: MdOutlineMessage }, + { location: '/profile', title: 'Profile', public: true, icon: MdOutlineAccountCircle }, { location: '/users', title: 'Users', public: false, roles: [UserRoleEnum.ADMIN, UserRoleEnum.TEACHER], - icon: MdGroup, + icon: MdOutlineGroup, }, ], }; diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index 8dd813c..2e5ce23 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -7,6 +7,7 @@ export interface IAppConfig { appName: string; appDescription: string; appVersion: string; + appGithubLink: string; }; links: LinkType[]; } From 546a92bc8cb2ed554068e9c2e86ce6fd715db2e7 Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Tue, 7 Dec 2021 00:32:31 +0300 Subject: [PATCH 13/87] feat: add button component --- src/components/Buttons/index.tsx | 9 +++++++++ src/pages/courses/courses.tsx | 12 ++---------- .../components/StudentTableList/BulkActionsMenu.tsx | 2 +- .../components/StudentTableList/StudentTableList.tsx | 8 ++++++-- .../components/CreateUserForm/CreateUserForm.tsx | 5 ++--- src/pages/users/components/UserList/UserList.tsx | 6 ++++-- src/pages/users/components/UserSearch/UserSearch.tsx | 4 ++-- 7 files changed, 26 insertions(+), 20 deletions(-) create mode 100644 src/components/Buttons/index.tsx diff --git a/src/components/Buttons/index.tsx b/src/components/Buttons/index.tsx new file mode 100644 index 0000000..d67b216 --- /dev/null +++ b/src/components/Buttons/index.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import { ButtonProps, Button } from '@chakra-ui/button'; +import { AddIcon } from '@chakra-ui/icons'; + +export const CreateButton: React.FC = props => ( + +); diff --git a/src/pages/courses/courses.tsx b/src/pages/courses/courses.tsx index 33485dc..05fb8a0 100644 --- a/src/pages/courses/courses.tsx +++ b/src/pages/courses/courses.tsx @@ -27,6 +27,7 @@ import { useRootStore } from '@educt/hooks/useRootStore'; import { useHistory } from 'react-router'; import CategoryItem from './components/CategoryList/CategoryItem'; import CourseItem from './components/CourseList/CourseItem'; +import { CreateButton } from '@educt/components/Buttons'; /** * Courses page @@ -47,16 +48,7 @@ const CoursesPage: React.FC = () => { Courses List of all courses. - {(me.isAdmin || me.isTeacher) && ( - - )} + {(me.isAdmin || me.isTeacher) && history.push('/courses/create')} />} {(me.isAdmin || me.isTeacher) && } diff --git a/src/pages/courses/edit/components/StudentTableList/BulkActionsMenu.tsx b/src/pages/courses/edit/components/StudentTableList/BulkActionsMenu.tsx index 4542cf5..ad7aa40 100644 --- a/src/pages/courses/edit/components/StudentTableList/BulkActionsMenu.tsx +++ b/src/pages/courses/edit/components/StudentTableList/BulkActionsMenu.tsx @@ -24,7 +24,7 @@ const BulkActionsMenu: React.FC = ({ selected, rows, s return ( - }> + }> Bulk Actions diff --git a/src/pages/courses/edit/components/StudentTableList/StudentTableList.tsx b/src/pages/courses/edit/components/StudentTableList/StudentTableList.tsx index 8ab4406..cf69bf7 100644 --- a/src/pages/courses/edit/components/StudentTableList/StudentTableList.tsx +++ b/src/pages/courses/edit/components/StudentTableList/StudentTableList.tsx @@ -17,6 +17,7 @@ import type { ICourse } from '@educt/interfaces'; */ import { useCallback, useState } from 'react'; import { useDisclosure } from '@chakra-ui/react'; +import { AddIcon } from '@chakra-ui/icons'; type StudentTableListPropsType = { render: React.FC; @@ -90,7 +91,7 @@ const StudentTableList: React.FC = ({ render: Row, st } /> - + @@ -103,7 +104,7 @@ const StudentTableList: React.FC = ({ render: Row, st )} - @@ -121,6 +122,9 @@ const StudentTableList: React.FC = ({ render: Row, st ))}
+ + {students.length} students +
); }; diff --git a/src/pages/users/components/CreateUserForm/CreateUserForm.tsx b/src/pages/users/components/CreateUserForm/CreateUserForm.tsx index ecc4e0e..9d9abce 100644 --- a/src/pages/users/components/CreateUserForm/CreateUserForm.tsx +++ b/src/pages/users/components/CreateUserForm/CreateUserForm.tsx @@ -50,6 +50,7 @@ import { UsersPageContext } from '@educt/contexts'; * Schema */ import CreateUserSchema from './CreateUserForm.validator'; +import { CreateButton } from '@educt/components/Buttons'; type CreateUserFormPropsType = {}; @@ -108,9 +109,7 @@ const CreateUserModal: React.FC = () => { return ( <> - + diff --git a/src/pages/users/components/UserList/UserList.tsx b/src/pages/users/components/UserList/UserList.tsx index e5d9040..c932e0c 100644 --- a/src/pages/users/components/UserList/UserList.tsx +++ b/src/pages/users/components/UserList/UserList.tsx @@ -77,7 +77,7 @@ const UserList: React.FC = ({ render: Item, limit }) => { {users.length !== 0 ? ( <> - + Total: ({pagination.total}) Actions @@ -98,7 +98,9 @@ const UserList: React.FC = ({ render: Item, limit }) => { - Page {pagination.current_page} of {pagesCount} + + Page {pagination.current_page} of {pagesCount} + ); + +export const EditButton: React.FC = props => ( + +); + +export const SaveButton: React.FC = props => ( + +); diff --git a/src/components/ErrorFallback.tsx b/src/components/ErrorFallback.tsx index eb76168..9ec6ab9 100644 --- a/src/components/ErrorFallback.tsx +++ b/src/components/ErrorFallback.tsx @@ -1,6 +1,7 @@ import React from 'react'; import Cookies from 'js-cookie'; -import { Flex, Box, Heading, Text } from '@chakra-ui/react'; +import { Redirect } from 'react-router'; +import { Icon, Box, Heading, Text } from '@chakra-ui/react'; import { MdError } from 'react-icons/md'; /** @@ -9,7 +10,6 @@ import { MdError } from 'react-icons/md'; import { useEffect } from 'react'; import { useToast } from '@chakra-ui/toast'; import { useRootStore } from '@educt/hooks/useRootStore'; -import { Redirect } from 'react-router'; const ErrorFallback: React.FC<{ error: any }> = props => { const { userStore, authStore } = useRootStore(); @@ -30,9 +30,7 @@ const ErrorFallback: React.FC<{ error: any }> = props => { return ( - - - + Something went wrong diff --git a/src/components/PageElements/index.tsx b/src/components/PageElements/index.tsx new file mode 100644 index 0000000..b5cf7c3 --- /dev/null +++ b/src/components/PageElements/index.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { BoxProps, Box, Heading, Text, Divider } from '@chakra-ui/react'; + +interface IPageHeadingProps extends BoxProps { + heading: string; + description?: string; +} + +export const PageWrapper: React.FC = props => { + return {props.children}; +}; + +export const PageHeading: React.FC = props => { + const { heading, description, ...boxProps } = props; + return ( + + {heading} + {description && {description}} + {props.children} + + + ); +}; + +export const PageContent: React.FC = props => {props.children}; + +export const PageFooter: React.FC = props => ( + + + {props.children} + +); diff --git a/src/components/UserBadge/UserBadge.tsx b/src/components/UserBadge/UserBadge.tsx index ee037d0..eeebf09 100644 --- a/src/components/UserBadge/UserBadge.tsx +++ b/src/components/UserBadge/UserBadge.tsx @@ -1,38 +1,30 @@ -import { Badge } from '@chakra-ui/react'; +import { BadgeProps, Badge, Box } from '@chakra-ui/react'; import React from 'react'; import { UserRoleEnum } from '@educt/enums'; import { IUserRole } from '@educt/interfaces'; import { userHasRoles } from '@educt/helpers'; -/** - * Returns the badge for user based on roles - */ -const UserBadge: React.FC<{ roles: IUserRole[] }> = ({ roles }) => { +export interface IUserBadgeProps extends BadgeProps { + roles: IUserRole[]; +} + +const UserBadge: React.FC = props => { + const { roles } = props; const getCurrentRole = () => { if (userHasRoles(roles, [UserRoleEnum.ADMIN])) return { name: 'Administrator', color: 'purple' }; if (userHasRoles(roles, [UserRoleEnum.TEACHER])) return { name: 'Teacher', color: 'orange' }; if (userHasRoles(roles, [UserRoleEnum.STUDENT])) return { name: 'Student', color: 'blue' }; - /** - * The user has no roles - */ return { name: 'Guest', color: 'gray' }; }; const role = getCurrentRole(); - if (role !== undefined) { - return ( - - {role.name} - - ); - } - - /** - * Cannot find any role for user - */ - return null; + return ( + + {role.name} + + ); }; export default UserBadge; diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index 2e5ce23..031a917 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -43,6 +43,7 @@ export interface ILesson { description: string; duration: string; color?: ColorType | undefined | null; + materials_count?: string | undefined; created_at: string; updated_at: string; } diff --git a/src/pages/404/404.tsx b/src/pages/404/404.tsx index 32b1a4c..53d58a9 100644 --- a/src/pages/404/404.tsx +++ b/src/pages/404/404.tsx @@ -1,18 +1,25 @@ import React from 'react'; import Helmet from 'react-helmet'; import { Link as ReactRouterLink } from 'react-router-dom'; -import { Flex, Box, Heading, Text, useToast, Link } from '@chakra-ui/react'; +import { Flex, Heading, Text, Link, Icon } from '@chakra-ui/react'; import { MdError } from 'react-icons/md'; /** * Types */ import { IPageProps } from '@educt/interfaces'; +import { PageContent, PageWrapper } from '@educt/components/PageElements'; + +/** + * Hooks + */ +import { useEffect } from 'react'; +import { useToast } from '@chakra-ui/react'; const NotFoundPage: React.FC = ({ title }) => { const toast = useToast(); - React.useEffect(() => { + useEffect(() => { toast({ title: `Page not found`, duration: 10000, isClosable: true, status: 'warning' }); }); @@ -21,18 +28,18 @@ const NotFoundPage: React.FC = ({ title }) => { {title} - - - - - - Page not found - - Sorry the page you are looking for could not be found. - - Return to main page - - + + + + + Page not found + + Sorry the page you are looking for could not be found. + + Return to main page + + + ); }; diff --git a/src/pages/auth/auth.tsx b/src/pages/auth/auth.tsx index 94d7bdc..e7f4288 100644 --- a/src/pages/auth/auth.tsx +++ b/src/pages/auth/auth.tsx @@ -1,8 +1,8 @@ import React from 'react'; import Helmet from 'react-helmet'; -import { observer } from 'mobx-react'; import { Redirect } from 'react-router-dom'; -import { Box, Heading, Text, Link } from '@chakra-ui/react'; +import { observer } from 'mobx-react'; +import { Box, Heading } from '@chakra-ui/react'; /** * Types diff --git a/src/pages/courses/components/CourseStatusTabs/CourseStatusTabs.tsx b/src/pages/courses/components/CourseStatusTabs/CourseStatusTabs.tsx index ac89c72..834f6b6 100644 --- a/src/pages/courses/components/CourseStatusTabs/CourseStatusTabs.tsx +++ b/src/pages/courses/components/CourseStatusTabs/CourseStatusTabs.tsx @@ -23,7 +23,7 @@ const CourseStatusTabs: React.FC = () => { const handleChangeStatus = (status: CourseStatusEnum | undefined) => setCourseStatus(status); return ( - + handleChangeStatus(undefined)}>All handleChangeStatus(CourseStatusEnum.PUBLISHED)}>Published diff --git a/src/pages/courses/courses.tsx b/src/pages/courses/courses.tsx index 05fb8a0..26cf8e1 100644 --- a/src/pages/courses/courses.tsx +++ b/src/pages/courses/courses.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import { Flex, Box, Heading, Text, Button } from '@chakra-ui/react'; -import { AddIcon } from '@chakra-ui/icons'; +import { Box } from '@chakra-ui/react'; /** * Types @@ -10,10 +9,13 @@ import { IPageProps } from '@educt/interfaces'; /** * Components */ +import { PageWrapper, PageHeading, PageContent } from '@educt/components/PageElements'; +import LoadingPage from '@educt/components/LoadingPage'; import CourseList from './components/CourseList'; import CategoryList from './components/CategoryList'; import CourseStatusTabs from './components/CourseStatusTabs'; -import LoadingPage from '@educt/components/LoadingPage'; +import CategoryItem from './components/CategoryList/CategoryItem'; +import CourseItem from './components/CourseList/CourseItem'; /** * Providers @@ -24,10 +26,6 @@ import { CoursesPageContextProvider } from '@educt/providers'; * Hooks */ import { useRootStore } from '@educt/hooks/useRootStore'; -import { useHistory } from 'react-router'; -import CategoryItem from './components/CategoryList/CategoryItem'; -import CourseItem from './components/CourseList/CourseItem'; -import { CreateButton } from '@educt/components/Buttons'; /** * Courses page @@ -36,28 +34,21 @@ const CoursesPage: React.FC = () => { const { userStore: { me }, } = useRootStore(); - const history = useHistory(); if (me === null) return ; return ( - - - - Courses - List of all courses. - - {(me.isAdmin || me.isTeacher) && history.push('/courses/create')} />} - - + + + {(me.isAdmin || me.isTeacher) && } - - + + ); }; diff --git a/src/pages/courses/edit/components/LessonList/LessonList.tsx b/src/pages/courses/edit/components/LessonList/LessonList.tsx index 8ef8946..bfb63f3 100644 --- a/src/pages/courses/edit/components/LessonList/LessonList.tsx +++ b/src/pages/courses/edit/components/LessonList/LessonList.tsx @@ -35,6 +35,7 @@ import { useErrorHandler } from 'react-error-boundary'; * Services */ import { LessonServiceInstance } from '@educt/services'; +import { CreateButton } from '@educt/components/Buttons'; type LessonListPropsType = { course: Omit; @@ -169,7 +170,7 @@ const LessonList: React.FC = ({ course }) => {
- 4 attachments + {lesson.materials_count} attachments
@@ -205,10 +206,10 @@ const LessonList: React.FC = ({ course }) => { )} - Total: ({lessons.length}) - + + {lessons.length} lessons + + ; @@ -89,7 +90,7 @@ const StudentTableList: React.FC = ({ render: Row, st - + } /> @@ -104,9 +105,7 @@ const StudentTableList: React.FC = ({ render: Row, st )} - + diff --git a/src/pages/profile/components/CourseList/CourseItem.tsx b/src/pages/profile/components/CourseList/CourseItem.tsx index df05588..109bcc2 100644 --- a/src/pages/profile/components/CourseList/CourseItem.tsx +++ b/src/pages/profile/components/CourseList/CourseItem.tsx @@ -1,7 +1,7 @@ import React from 'react'; import moment from 'moment'; import { Link as ReactRouterLink } from 'react-router-dom'; -import { Tag, TagLabel, LinkBox, Text, LinkOverlay, Icon, Link } from '@chakra-ui/react'; +import { Tag, TagLabel, LinkBox, Text, LinkOverlay, Icon } from '@chakra-ui/react'; import { Box, Flex } from '@chakra-ui/layout'; /** @@ -14,6 +14,7 @@ import { TimeIcon } from '@chakra-ui/icons'; * Hooks */ import { useColorModeValue } from '@chakra-ui/color-mode'; +import { MdOutlineVideoLibrary } from 'react-icons/md'; type CourseItemPropsType = { course: UserCourseType; @@ -22,30 +23,11 @@ type CourseItemPropsType = { const CourseItem: React.FC = ({ course }) => { return ( - - - - {course.lessons_count} lessons - - - - - - {course.category.title} - - - = ({ course }) => { {course.title} + + + {course.category.title} + + - + + + {course.lessons_count} lessons { - return ( - - - - - - - - - - - - - - - - ); -}; - -export default LoadingPage; diff --git a/src/pages/profile/components/LoadingPage/index.ts b/src/pages/profile/components/LoadingPage/index.ts deleted file mode 100644 index f661969..0000000 --- a/src/pages/profile/components/LoadingPage/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import LoadingPage from './LoadingPage'; - -export default LoadingPage; diff --git a/src/pages/profile/components/UpdateUserContactsForm/UpdateUserContactsForm.tsx b/src/pages/profile/components/UpdateUserContactsForm/UpdateUserContactsForm.tsx index 933df5d..704293b 100644 --- a/src/pages/profile/components/UpdateUserContactsForm/UpdateUserContactsForm.tsx +++ b/src/pages/profile/components/UpdateUserContactsForm/UpdateUserContactsForm.tsx @@ -23,6 +23,7 @@ import { useErrorHandler } from 'react-error-boundary'; * Schema */ import UpdateUserContactsSchema from './UpdateUserContactsForm.validator'; +import { SaveButton } from '@educt/components/Buttons'; type UpdateUserContactsInputType = { phone_number: string | null; @@ -152,19 +153,7 @@ const UpdateUserContactsForm: React.FC = ({ con - + } isDisabled={!isDirty} /> ); diff --git a/src/pages/profile/components/index.tsx b/src/pages/profile/components/index.tsx new file mode 100644 index 0000000..d531838 --- /dev/null +++ b/src/pages/profile/components/index.tsx @@ -0,0 +1,98 @@ +import React from 'react'; +import { FlexProps, Flex, BoxProps, Box, Heading, Text } from '@chakra-ui/layout'; +import { Avatar } from '@chakra-ui/avatar'; +import { Button, ButtonProps } from '@chakra-ui/button'; +import { Skeleton, SkeletonCircle, Spinner } from '@chakra-ui/react'; + +/** + * Types + */ +import { IUserRole } from '@educt/interfaces'; + +/** + * Components + */ +import UserBadge from '@educt/components/UserBadge'; + +/** + * Hooks + */ +import { useColorMode } from '@chakra-ui/react'; +import { useLogout } from '@educt/hooks/queries'; + +export const ProfileBaseInfo: React.FC = props => { + return ( + + {props.children} + + ); +}; + +interface IProfileAvatarProps extends BoxProps { + name: string; +} +export const ProfileAvatar: React.FC = props => { + const { colorMode } = useColorMode(); + const isDark = colorMode === 'dark'; + + return ( + + + + ); +}; + +interface IProfileDescriptionProps extends BoxProps { + fullname: string; + email: string; + roles: IUserRole[]; +} +export const ProfileDescription: React.FC = props => { + const { fullname, email, roles, ...boxProps } = props; + return ( + + + + + {fullname} +
+ + {email} + +
+ +
+
+
+ ); +}; + +export const ProfileSignOutButton: React.FC = props => { + const { logout } = useLogout(); + + return ( + + ); +}; + +export const ProfileLoading: React.FC = props => { + return ( + + + + + + + + + + + + + + + + ); +}; diff --git a/src/pages/profile/profile.tsx b/src/pages/profile/profile.tsx index 50da352..da7b680 100644 --- a/src/pages/profile/profile.tsx +++ b/src/pages/profile/profile.tsx @@ -13,7 +13,7 @@ import { InputGroup, InputRightElement, } from '@chakra-ui/react'; -import { Box, Flex, Heading, Text, Stack } from '@chakra-ui/layout'; +import { Box, Stack } from '@chakra-ui/layout'; /** * Types @@ -23,18 +23,16 @@ import { IPageProps } from '@educt/interfaces'; /** * Components */ -import LoadingPage from './components/LoadingPage'; -import UserBadge from '@educt/components/UserBadge'; +import { PageHeading, PageContent, PageWrapper, PageFooter } from '@educt/components/PageElements'; +import { ProfileAvatar, ProfileBaseInfo, ProfileDescription, ProfileLoading, ProfileSignOutButton } from './components'; +import { CourseList } from './components/CourseList'; import UpdateUserContactsForm from '@educt/pages/profile/components/UpdateUserContactsForm'; -import CourseList from '@educt/pages/profile/components/CourseList'; /** * Hooks */ import { useHistory } from 'react-router-dom'; -import { useColorMode } from '@chakra-ui/color-mode'; import { useRootStore } from '@educt/hooks/useRootStore'; -import { useLogout } from '@educt/hooks/queries'; /** * Profile page @@ -44,35 +42,17 @@ const ProfilePage: React.FC = () => { userStore: { me }, } = useRootStore(); const history = useHistory(); - const { logout } = useLogout(); - const { colorMode } = useColorMode(); - if (me === null) return ; + if (me === null) return ; return ( - - My account - Update you contacts information or password here. - - - - - - {me.fullname} - - - {me.email} - - - - - - + + + + + + + Account information @@ -82,7 +62,7 @@ const ProfilePage: React.FC = () => {
- + First name @@ -136,13 +116,11 @@ const ProfilePage: React.FC = () => { )}
-
- - - -
+ + + + + ); }; diff --git a/src/pages/users/components/UserList/UserList.tsx b/src/pages/users/components/UserList/UserList.tsx index c932e0c..aac103b 100644 --- a/src/pages/users/components/UserList/UserList.tsx +++ b/src/pages/users/components/UserList/UserList.tsx @@ -30,6 +30,8 @@ import { useErrorHandler } from 'react-error-boundary'; import { useRootStore } from '@educt/hooks/useRootStore'; import { useDisclosure } from '@chakra-ui/hooks'; import { IUser } from '@educt/interfaces'; +import UserSearch from '../UserSearch'; +import CreateUserForm from '../CreateUserForm'; type UserListPropsType = { render: React.FC; limit?: number }; @@ -76,6 +78,10 @@ const UserList: React.FC = ({ render: Item, limit }) => { {deleting && } {users.length !== 0 ? ( <> + + + + Total: ({pagination.total}) diff --git a/src/pages/users/components/UserList/index.ts b/src/pages/users/components/UserList/index.ts index a3c8e07..8ba9cff 100644 --- a/src/pages/users/components/UserList/index.ts +++ b/src/pages/users/components/UserList/index.ts @@ -1,3 +1,4 @@ import UserList from './UserList'; +import UserItem from './UserItem'; -export default UserList; +export { UserList, UserItem }; diff --git a/src/pages/users/users.tsx b/src/pages/users/users.tsx index c7f7b19..727bf5e 100644 --- a/src/pages/users/users.tsx +++ b/src/pages/users/users.tsx @@ -1,19 +1,16 @@ import React from 'react'; -import { Box, Flex, Heading, Text } from '@chakra-ui/react'; /** * Types */ -import { IPageProps } from '@educt/interfaces'; +import type { IPageProps } from '@educt/interfaces'; /** * Components */ +import { PageContent, PageHeading, PageWrapper } from '@educt/components/PageElements'; +import { UserList, UserItem } from './components/UserList'; import LoadingPage from '@educt/components/LoadingPage'; -import UserSearch from './components/UserSearch'; -import UserList from './components/UserList'; -import UserItem from './components/UserList/UserItem'; -import CreateUserForm from './components/CreateUserForm'; /** * Hooks @@ -36,18 +33,14 @@ const UsersPage: React.FC = () => { if (me === null) return ; return ( + // TODO: delete page context for better performance - - User management - You can add or delete users on this page. - - - - - + + + - - + + ); }; diff --git a/src/stores/UserStore.ts b/src/stores/UserStore.ts index 6f4713c..1df1df0 100644 --- a/src/stores/UserStore.ts +++ b/src/stores/UserStore.ts @@ -60,7 +60,7 @@ export default class UserStore { try { const result = await UserServiceInstance.fetchMe(); const { - data: { id, first_name, last_name, fullname, email, roles, contacts, courses }, + data: { id, first_name, last_name, fullname, email, roles, contacts, courses, created_at, updated_at }, } = result; runInAction(() => { @@ -76,6 +76,8 @@ export default class UserStore { isAdmin: helpers.userContainRoles(roles, [UserRoleEnum.ADMIN]), isTeacher: helpers.userContainRoles(roles, [UserRoleEnum.TEACHER]), isStudent: helpers.userContainRoles(roles, [UserRoleEnum.STUDENT]), + created_at, + updated_at, }; }); From d1d345ca88849ebbe4ef4e106d9a027c729a4b8d Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Wed, 8 Dec 2021 20:47:06 +0300 Subject: [PATCH 15/87] feat: added courses page store --- src/components/Buttons/index.tsx | 8 +- src/components/SetCourseStatusButton/index.ts | 3 - .../components/CategoryList/CategoryItem.tsx | 10 ++- .../components/CategoryList/CategoryList.tsx | 10 ++- .../components/CourseList/CourseList.tsx | 14 ++-- .../CourseStatusTabs/CourseStatusTabs.tsx | 36 -------- .../components/CourseStatusTabs/index.ts | 3 - src/pages/courses/components/index.tsx | 31 +++++++ src/pages/courses/courses.tsx | 32 +++----- .../courses/edit/components/index.tsx} | 35 ++++---- src/pages/courses/edit/edit.tsx | 82 ++++++++++--------- src/stores/PageStore.ts | 4 + src/stores/pages/CoursesStore.ts | 30 +++++++ 13 files changed, 168 insertions(+), 130 deletions(-) delete mode 100644 src/components/SetCourseStatusButton/index.ts delete mode 100644 src/pages/courses/components/CourseStatusTabs/CourseStatusTabs.tsx delete mode 100644 src/pages/courses/components/CourseStatusTabs/index.ts create mode 100644 src/pages/courses/components/index.tsx rename src/{components/SetCourseStatusButton/SetCourseStatusButton.tsx => pages/courses/edit/components/index.tsx} (66%) create mode 100644 src/stores/pages/CoursesStore.ts diff --git a/src/components/Buttons/index.tsx b/src/components/Buttons/index.tsx index ff1f715..6b3b170 100644 --- a/src/components/Buttons/index.tsx +++ b/src/components/Buttons/index.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { ButtonProps, Button } from '@chakra-ui/button'; -import { AddIcon, EditIcon } from '@chakra-ui/icons'; +import { AddIcon, DeleteIcon, EditIcon } from '@chakra-ui/icons'; import { MdSave } from 'react-icons/md'; export const CreateButton: React.FC = props => ( @@ -9,6 +9,12 @@ export const CreateButton: React.FC = props => ( ); +export const DeleteButton: React.FC = props => ( + +); + export const EditButton: React.FC = props => ( @@ -38,8 +42,11 @@ const SetCourseStatusButton: React.FC = ({ @@ -48,5 +55,3 @@ const SetCourseStatusButton: React.FC = ({ return Components[status]; }; - -export default SetCourseStatusButton; diff --git a/src/pages/courses/edit/edit.tsx b/src/pages/courses/edit/edit.tsx index d6e02b6..d644009 100644 --- a/src/pages/courses/edit/edit.tsx +++ b/src/pages/courses/edit/edit.tsx @@ -1,8 +1,7 @@ import React from 'react'; import Helmet from 'react-helmet'; import { Redirect } from 'react-router-dom'; -import { Box, Heading, Text, Tab, TabList, Flex, Tabs, TabPanels, TabPanel, Button } from '@chakra-ui/react'; -import { DeleteIcon } from '@chakra-ui/icons'; +import { Box, Heading, Text, Tab, TabList, Flex, Tabs, TabPanels, TabPanel } from '@chakra-ui/react'; /** * Types @@ -14,7 +13,6 @@ import type { IPageProps } from '@educt/interfaces'; */ import LoadingPage from '@educt/components/LoadingPage'; import { EditCourseForm } from '@educt/components/Forms/CourseForm'; -import SetCourseStatusButton from '@educt/components/SetCourseStatusButton'; import DeleteCourseDialog from '@educt/components/Dialogs/DeleteCourseDialog'; import { StudentTableList, StudentTableRow } from './components/StudentTableList'; import LessonList from './components/LessonList'; @@ -26,6 +24,9 @@ import { useParams } from 'react-router-dom'; import { useRootStore } from '@educt/hooks/useRootStore'; import { useFetchCourse } from '@educt/hooks/queries'; import { useDisclosure } from '@chakra-ui/hooks'; +import { PageContent, PageWrapper } from '@educt/components/PageElements'; +import { DeleteButton } from '@educt/components/Buttons'; +import { StatusButton } from './components'; /** * Course editor @@ -53,55 +54,58 @@ const EditCoursePage: React.FC = () => { if (!course || isLoading) return ; return ( - <> + Editing {course.title} + + Course editor {course.title} - - + + - - - - Info - Lessons - Students - - - - - - - - - - - - - - - - - + + + + + Info + Lessons + Students + + + + + + + + + + + + + + + + + + + ); }; diff --git a/src/stores/PageStore.ts b/src/stores/PageStore.ts index 4450700..da2c98f 100644 --- a/src/stores/PageStore.ts +++ b/src/stores/PageStore.ts @@ -1,10 +1,13 @@ import { makeAutoObservable } from 'mobx'; +import CoursesStore from './pages/CoursesStore'; import EditCourseStore from './pages/EditCourseStore'; import RootStore from './RootStore'; export default class PageStore { public root: RootStore; + public coursesStore: CoursesStore; + public editCourseStore: EditCourseStore; constructor(root: RootStore) { @@ -13,6 +16,7 @@ export default class PageStore { /** * Pages */ + this.coursesStore = new CoursesStore(this); this.editCourseStore = new EditCourseStore(this); makeAutoObservable(this); diff --git a/src/stores/pages/CoursesStore.ts b/src/stores/pages/CoursesStore.ts new file mode 100644 index 0000000..101ee8d --- /dev/null +++ b/src/stores/pages/CoursesStore.ts @@ -0,0 +1,30 @@ +import { makeAutoObservable } from 'mobx'; +import { CourseStatusEnum } from '@educt/enums'; +import PageStore from '../PageStore'; + +/** + * Types + */ +import type { ICategory } from '@educt/interfaces'; + +export default class CoursesStore { + public pageStore: PageStore; + + public selectedCategory: ICategory | null = null; + + public showedStatus: CourseStatusEnum | undefined; + + constructor(pageStore: PageStore) { + this.pageStore = pageStore; + + makeAutoObservable(this); + } + + public setSelectedCategory(category: ICategory | null): void { + this.selectedCategory = category; + } + + public setShowedStatus(newStatus: CourseStatusEnum | undefined): void { + this.showedStatus = newStatus; + } +} From 7303dd5b43f4bef65b6a987edbf7f0515426b5f3 Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Thu, 9 Dec 2021 23:22:29 +0300 Subject: [PATCH 16/87] feat: attach students modal layout --- src/components/Buttons/index.tsx | 10 +- src/components/Modal/BaseModal.tsx | 66 +++++++ src/components/Modal/index.ts | 3 + .../AddStudentsModal/AddSudentsModal.tsx | 167 ++++++++++++++---- src/pages/courses/courses.tsx | 10 +- .../StudentTableList/StudentTableList.tsx | 5 +- src/pages/courses/edit/edit.tsx | 16 +- src/services/UserService.ts | 2 +- 8 files changed, 226 insertions(+), 53 deletions(-) create mode 100644 src/components/Modal/BaseModal.tsx create mode 100644 src/components/Modal/index.ts diff --git a/src/components/Buttons/index.tsx b/src/components/Buttons/index.tsx index 6b3b170..212f487 100644 --- a/src/components/Buttons/index.tsx +++ b/src/components/Buttons/index.tsx @@ -9,14 +9,20 @@ export const CreateButton: React.FC = props => ( ); +export const AddButton: React.FC = props => ( + +); + export const DeleteButton: React.FC = props => ( - ); export const EditButton: React.FC = props => ( - ); diff --git a/src/components/Modal/BaseModal.tsx b/src/components/Modal/BaseModal.tsx new file mode 100644 index 0000000..8dd2701 --- /dev/null +++ b/src/components/Modal/BaseModal.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import { Button } from '@chakra-ui/button'; +import { Text, Flex, Box, Divider } from '@chakra-ui/react'; +import { + Icon, + ModalProps, + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalFooter, + ModalBody, + ModalCloseButton, +} from '@chakra-ui/react'; +import { IconType } from 'react-icons'; + +export interface IBaseModalProps extends ModalProps { + isOpen: boolean; + onClose: () => void; + heading: string; + icon?: IconType; + description?: string; + confirmText?: string; +} + +const BaseModal: React.FC = props => { + const { heading, description, isOpen, onClose, confirmText, icon, ...modalProps } = props; + + return ( + + + + + + {icon && } + {heading} + + {description && ( + + + {description} + + + )} + + + + + + + {props.children} + + + + + + + + ); +}; + +export default BaseModal; diff --git a/src/components/Modal/index.ts b/src/components/Modal/index.ts new file mode 100644 index 0000000..76e3dba --- /dev/null +++ b/src/components/Modal/index.ts @@ -0,0 +1,3 @@ +import BaseModal from './BaseModal'; + +export default BaseModal; diff --git a/src/components/Modals/AddStudentsModal/AddSudentsModal.tsx b/src/components/Modals/AddStudentsModal/AddSudentsModal.tsx index 5a6ec6c..e357a24 100644 --- a/src/components/Modals/AddStudentsModal/AddSudentsModal.tsx +++ b/src/components/Modals/AddStudentsModal/AddSudentsModal.tsx @@ -1,48 +1,147 @@ -import React from 'react'; -import { Button } from '@chakra-ui/button'; -import { Text, Flex } from '@chakra-ui/react'; +import React, { useCallback, useEffect, useState } from 'react'; + import { - Modal, - ModalOverlay, - ModalContent, - ModalHeader, - ModalFooter, - ModalBody, - ModalCloseButton, + Tag, + TagLabel, + TagCloseButton, + Input, + Box, + Flex, + IconButton, + BoxProps, + InputGroup, + Text, + Stack, + InputRightElement, + Avatar, } from '@chakra-ui/react'; -import { useDisclosure, Divider } from '@chakra-ui/react'; +import BaseModal from '@educt/components/Modal'; import { MdGroup } from 'react-icons/md'; +import { AddIcon, SearchIcon } from '@chakra-ui/icons'; +import { UserServiceInstance } from '@educt/services'; +import { IUser } from '@educt/interfaces'; +import { useErrorHandler } from 'react-error-boundary'; +import { BeatLoader } from 'react-spinners'; +import { Virtuoso } from 'react-virtuoso'; +import { FetchUsersParamsType } from '@educt/types'; type AddStudentsModalPropsType = { isOpen: boolean; onClose: () => void; }; +const SelectUsersInput: React.FC = props => { + const [isLoading, setIsLoading] = useState(false); + const [search, setSearch] = useState(''); + const [selected, setSelected] = useState>([]); + const [users, setUsers] = useState>([]); + const [limit, setLimit] = useState(5); + const filtered = users.filter(u => selected.every(s => s.id !== u.id)); + const handleError = useErrorHandler(); + const isEmpty = filtered.length === 0; + + const fetch = async (params?: FetchUsersParamsType | undefined) => { + try { + setIsLoading(true); + const { data } = await UserServiceInstance.fetchAll({ search, limit }); + setUsers(data); + } catch (error: any) { + handleError(error); + } finally { + setIsLoading(false); + } + }; + + const handleSearch = (e: React.ChangeEvent) => setSearch(e.target.value); + const handleSelect = (user: IUser) => () => setSelected(prev => [...prev, user]); + const handleRemoveSelected = (id: IUser['id']) => () => setSelected(prev => prev.filter(u => u.id !== id)); + + useEffect(() => { + fetch(); + }, [search]); + + return ( + + {selected.length !== 0 && ( + + {selected.map(selected => ( + + + {selected.fullname} + + + ))} + + )} + + fetch()} + onChange={handleSearch} + type='text' + placeholder='Search for users...' + /> + } /> + + + {isLoading && ( + + Loading...   + + + )} + + {!isLoading && isEmpty && ( + + Cannot find any user + + )} + + {!isLoading && !isEmpty && ( + + ( + + + + + {user.fullname} + + + {user.email} + + + } + ml='auto' + borderRadius='full' + size='sm' + /> + + )} + /> + + )} + + ); +}; + const AddStudentsModal: React.FC = ({ isOpen, onClose }) => { return ( - <> - - - - - - - Add students - - - - - - - - - - - - - + + + ); }; diff --git a/src/pages/courses/courses.tsx b/src/pages/courses/courses.tsx index 5a36232..c678e1d 100644 --- a/src/pages/courses/courses.tsx +++ b/src/pages/courses/courses.tsx @@ -10,6 +10,7 @@ import { IPageProps } from '@educt/interfaces'; */ import { PageWrapper, PageHeading, PageContent } from '@educt/components/PageElements'; import { CreateButton } from '@educt/components/Buttons'; +import { Box } from '@chakra-ui/layout'; import LoadingPage from '@educt/components/LoadingPage'; import CourseList from './components/CourseList'; import CategoryList from './components/CategoryList'; @@ -32,12 +33,17 @@ const CoursesPage: React.FC = () => { } = useRootStore(); const history = useHistory(); + const handleCreateCourse = () => history.push('/courses/create'); + if (me === null) return ; return ( - - {(me.isAdmin || me.isTeacher) && history.push('/courses/create')} />} + + + {(me.isAdmin || me.isTeacher) && } + + diff --git a/src/pages/courses/edit/components/StudentTableList/StudentTableList.tsx b/src/pages/courses/edit/components/StudentTableList/StudentTableList.tsx index 4034568..562b08f 100644 --- a/src/pages/courses/edit/components/StudentTableList/StudentTableList.tsx +++ b/src/pages/courses/edit/components/StudentTableList/StudentTableList.tsx @@ -3,6 +3,7 @@ import { Table, Tbody, Button, Input, InputGroup, InputLeftElement, Flex, Box, T import { MdSearch } from 'react-icons/md'; import AddStudentsModal from '@educt/components/Modals/AddStudentsModal'; +import { AddButton } from '@educt/components/Buttons'; import BulkActionsMenu from './BulkActionsMenu'; import StudentTableHead from './StudentTableHead'; @@ -17,8 +18,6 @@ import type { ICourse } from '@educt/interfaces'; */ import { useCallback, useState } from 'react'; import { useDisclosure } from '@chakra-ui/react'; -import { AddIcon } from '@chakra-ui/icons'; -import { CreateButton } from '@educt/components/Buttons'; type StudentTableListPropsType = { render: React.FC; @@ -105,7 +104,7 @@ const StudentTableList: React.FC = ({ render: Row, st )} - +
diff --git a/src/pages/courses/edit/edit.tsx b/src/pages/courses/edit/edit.tsx index d644009..5bf060a 100644 --- a/src/pages/courses/edit/edit.tsx +++ b/src/pages/courses/edit/edit.tsx @@ -24,7 +24,7 @@ import { useParams } from 'react-router-dom'; import { useRootStore } from '@educt/hooks/useRootStore'; import { useFetchCourse } from '@educt/hooks/queries'; import { useDisclosure } from '@chakra-ui/hooks'; -import { PageContent, PageWrapper } from '@educt/components/PageElements'; +import { PageContent, PageHeading, PageWrapper } from '@educt/components/PageElements'; import { DeleteButton } from '@educt/components/Buttons'; import { StatusButton } from './components'; @@ -60,18 +60,12 @@ const EditCoursePage: React.FC = () => { - - - - Course editor - {course.title} - - + + - - - + + diff --git a/src/services/UserService.ts b/src/services/UserService.ts index 16ab024..8737e65 100644 --- a/src/services/UserService.ts +++ b/src/services/UserService.ts @@ -28,7 +28,7 @@ class UserService { * @returns Array of users */ public async fetchAll(params?: FetchUsersParamsType): Promise> { - const { page = 1, limit = 6, role, search } = params || {}; + const { page, limit, role, search } = params || {}; const result = await this.api.get('/v1/users', { params: { page, From 5c0599fe0ac936bbbb341bfb73275840b47f25e9 Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Fri, 10 Dec 2021 23:04:58 +0300 Subject: [PATCH 17/87] feat: attach student list query --- src/components/Modal/BaseModal.tsx | 15 +- .../AddStudentsModal/AddSudentsModal.tsx | 153 ++++-------------- .../SelectUsersInput/SelectUsersInput.tsx | 143 ++++++++++++++++ src/components/SelectUsersInput/index.ts | 3 + src/helpers/index.ts | 9 +- .../StudentTableList/StudentTableList.tsx | 13 +- src/pages/courses/edit/edit.tsx | 4 +- src/services/CourseService.ts | 8 + 8 files changed, 219 insertions(+), 129 deletions(-) create mode 100644 src/components/SelectUsersInput/SelectUsersInput.tsx create mode 100644 src/components/SelectUsersInput/index.ts diff --git a/src/components/Modal/BaseModal.tsx b/src/components/Modal/BaseModal.tsx index 8dd2701..e56764b 100644 --- a/src/components/Modal/BaseModal.tsx +++ b/src/components/Modal/BaseModal.tsx @@ -17,14 +17,18 @@ import { IconType } from 'react-icons'; export interface IBaseModalProps extends ModalProps { isOpen: boolean; onClose: () => void; + onProceed: (e: React.MouseEvent) => void; heading: string; icon?: IconType; description?: string; confirmText?: string; + isLoading?: boolean | undefined; + loadingText?: string | undefined; } const BaseModal: React.FC = props => { - const { heading, description, isOpen, onClose, confirmText, icon, ...modalProps } = props; + const { heading, description, isOpen, onClose, onProceed, confirmText, icon, isLoading, loadingText, ...modalProps } = + props; return ( @@ -54,7 +58,14 @@ const BaseModal: React.FC = props => { - diff --git a/src/components/Modals/AddStudentsModal/AddSudentsModal.tsx b/src/components/Modals/AddStudentsModal/AddSudentsModal.tsx index e357a24..770f14e 100644 --- a/src/components/Modals/AddStudentsModal/AddSudentsModal.tsx +++ b/src/components/Modals/AddStudentsModal/AddSudentsModal.tsx @@ -1,136 +1,44 @@ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useState } from 'react'; -import { - Tag, - TagLabel, - TagCloseButton, - Input, - Box, - Flex, - IconButton, - BoxProps, - InputGroup, - Text, - Stack, - InputRightElement, - Avatar, -} from '@chakra-ui/react'; import BaseModal from '@educt/components/Modal'; import { MdGroup } from 'react-icons/md'; -import { AddIcon, SearchIcon } from '@chakra-ui/icons'; -import { UserServiceInstance } from '@educt/services'; +import { UserRoleEnum } from '@educt/enums'; +import SelectUsersInput from '@educt/components/SelectUsersInput'; import { IUser } from '@educt/interfaces'; -import { useErrorHandler } from 'react-error-boundary'; -import { BeatLoader } from 'react-spinners'; -import { Virtuoso } from 'react-virtuoso'; -import { FetchUsersParamsType } from '@educt/types'; +import { CourseServiceInstance } from '@educt/services'; +import { useToast } from '@chakra-ui/react'; +import useIsMountedRef from '@educt/hooks/useIsMountedRef'; type AddStudentsModalPropsType = { isOpen: boolean; onClose: () => void; + courseId: string; }; -const SelectUsersInput: React.FC = props => { +const AddStudentsModal: React.FC = ({ isOpen, onClose, courseId }) => { const [isLoading, setIsLoading] = useState(false); - const [search, setSearch] = useState(''); - const [selected, setSelected] = useState>([]); - const [users, setUsers] = useState>([]); - const [limit, setLimit] = useState(5); - const filtered = users.filter(u => selected.every(s => s.id !== u.id)); - const handleError = useErrorHandler(); - const isEmpty = filtered.length === 0; + const isMountedRef = useIsMountedRef(); + const [students, setStudents] = useState>([]); + const toast = useToast(); - const fetch = async (params?: FetchUsersParamsType | undefined) => { - try { - setIsLoading(true); - const { data } = await UserServiceInstance.fetchAll({ search, limit }); - setUsers(data); - } catch (error: any) { - handleError(error); - } finally { - setIsLoading(false); + const handleSelectStudents = (students: Array) => setStudents(students); + const handleAttachStudents = async () => { + if (students.length !== 0) { + try { + setIsLoading(true); + const ids = students.map(s => s.id); + await CourseServiceInstance.attachStudentsList(courseId, ids); + + toast({ title: 'Students successfully attached.', status: 'success' }); + onClose(); + } catch (error: any) { + throw error; + } finally { + if (isMountedRef.current) setIsLoading(false); + } } }; - const handleSearch = (e: React.ChangeEvent) => setSearch(e.target.value); - const handleSelect = (user: IUser) => () => setSelected(prev => [...prev, user]); - const handleRemoveSelected = (id: IUser['id']) => () => setSelected(prev => prev.filter(u => u.id !== id)); - - useEffect(() => { - fetch(); - }, [search]); - - return ( - - {selected.length !== 0 && ( - - {selected.map(selected => ( - - - {selected.fullname} - - - ))} - - )} - - fetch()} - onChange={handleSearch} - type='text' - placeholder='Search for users...' - /> - } /> - - - {isLoading && ( - - Loading...   - - - )} - - {!isLoading && isEmpty && ( - - Cannot find any user - - )} - - {!isLoading && !isEmpty && ( - - ( - - - - - {user.fullname} - - - {user.email} - - - } - ml='auto' - borderRadius='full' - size='sm' - /> - - )} - /> - - )} - - ); -}; - -const AddStudentsModal: React.FC = ({ isOpen, onClose }) => { return ( = ({ isOpen, onClose confirmText='Add students' isOpen={isOpen} onClose={onClose} + onProceed={handleAttachStudents} + isLoading={isLoading} + loadingText='Saving...' > - + handleSelectStudents(selected)} + placeholder='Search for students...' + searchParams={{ role: UserRoleEnum.STUDENT }} + /> ); }; diff --git a/src/components/SelectUsersInput/SelectUsersInput.tsx b/src/components/SelectUsersInput/SelectUsersInput.tsx new file mode 100644 index 0000000..12f66d2 --- /dev/null +++ b/src/components/SelectUsersInput/SelectUsersInput.tsx @@ -0,0 +1,143 @@ +import React, { useEffect, useState } from 'react'; + +import { + Tag, + TagLabel, + TagCloseButton, + Input, + Box, + Flex, + IconButton, + InputGroup, + Text, + Stack, + InputRightElement, + Avatar, +} from '@chakra-ui/react'; +import { AddIcon, SearchIcon } from '@chakra-ui/icons'; +import { UserServiceInstance } from '@educt/services'; +import { IUser } from '@educt/interfaces'; +import { useErrorHandler } from 'react-error-boundary'; +import { BeatLoader } from 'react-spinners'; +import { Virtuoso } from 'react-virtuoso'; +import { FetchUsersParamsType } from '@educt/types'; + +type SelectUsersInputPropsType = { + placeholder?: string | undefined; + searchParams?: FetchUsersParamsType | undefined; + onSelect: (selected: Array) => void; +}; + +const SelectUsersInput: React.FC = ({ placeholder, searchParams: params, onSelect }) => { + const [isLoading, setIsLoading] = useState(false); + const [search, setSearch] = useState(''); + const [selected, setSelected] = useState>([]); + const [users, setUsers] = useState>([]); + const filtered = users.filter(u => selected.every(s => s.id !== u.id)); + const handleError = useErrorHandler(); + const isEmpty = filtered.length === 0; + + const fetch = async () => { + try { + setIsLoading(true); + const { data } = await UserServiceInstance.fetchAll({ search, ...params }); + setUsers(data); + } catch (error: any) { + handleError(error); + } finally { + setIsLoading(false); + } + }; + + const handleSearch = (e: React.ChangeEvent) => setSearch(e.target.value); + + const handleSelect = (user: IUser) => () => { + setSelected(prev => { + const updated = [...prev, user]; + onSelect(updated); + return updated; + }); + }; + const handleRemoveSelected = (id: IUser['id']) => () => { + setSelected(prev => { + const updated = prev.filter(u => u.id !== id); + onSelect(updated); + return updated; + }); + }; + + useEffect(() => { + fetch(); + }, [search]); + + return ( + + {selected.length !== 0 && ( + + {selected.map(selected => ( + + + {selected.fullname} + + + ))} + + )} + + + } /> + + + {isLoading && ( + + Loading...   + + + )} + + {!isLoading && isEmpty && ( + + Cannot find any user + + )} + + {!isLoading && !isEmpty && ( + + ( + + + + + {user.fullname} + + + {user.email} + + + } + ml='auto' + borderRadius='full' + size='sm' + /> + + )} + /> + + )} + + ); +}; + +export default SelectUsersInput; diff --git a/src/components/SelectUsersInput/index.ts b/src/components/SelectUsersInput/index.ts new file mode 100644 index 0000000..26a35e0 --- /dev/null +++ b/src/components/SelectUsersInput/index.ts @@ -0,0 +1,3 @@ +import SelectUsersInput from './SelectUsersInput'; + +export default SelectUsersInput; diff --git a/src/helpers/index.ts b/src/helpers/index.ts index f505833..641f21f 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -46,7 +46,14 @@ export function transformToFormData(data: object): FormData { /** * Append value to form-data format */ - Object.entries(data).forEach(([key, value]) => value && formData.append(key, value)); + Object.entries(data).forEach(([key, value]) => { + if (Array.isArray(value)) { + value.forEach((v, i) => formData.append(`${key}[${i}]`, v)); + return; + } + + formData.append(key, value); + }); return formData; } diff --git a/src/pages/courses/edit/components/StudentTableList/StudentTableList.tsx b/src/pages/courses/edit/components/StudentTableList/StudentTableList.tsx index 562b08f..b6faf54 100644 --- a/src/pages/courses/edit/components/StudentTableList/StudentTableList.tsx +++ b/src/pages/courses/edit/components/StudentTableList/StudentTableList.tsx @@ -21,10 +21,13 @@ import { useDisclosure } from '@chakra-ui/react'; type StudentTableListPropsType = { render: React.FC; - students: ICourse['students']; + course: Omit; }; -const StudentTableList: React.FC = ({ render: Row, students }) => { +const StudentTableList: React.FC = props => { + const { render: Row, course } = props; + const { students } = course; + const [rows, setRows] = useState(students); const [selected, setSelected] = useState([]); const [search, setSearch] = useState(''); @@ -73,7 +76,7 @@ const StudentTableList: React.FC = ({ render: Row, st if (rows.length === 0) { return ( <> - + There are no students in the course yet
- {students.length} students + {students.length} student(s) ); diff --git a/src/pages/courses/edit/edit.tsx b/src/pages/courses/edit/edit.tsx index 5bf060a..a8847d9 100644 --- a/src/pages/courses/edit/edit.tsx +++ b/src/pages/courses/edit/edit.tsx @@ -1,7 +1,7 @@ import React from 'react'; import Helmet from 'react-helmet'; import { Redirect } from 'react-router-dom'; -import { Box, Heading, Text, Tab, TabList, Flex, Tabs, TabPanels, TabPanel } from '@chakra-ui/react'; +import { Box, Tab, TabList, Flex, Tabs, TabPanels, TabPanel } from '@chakra-ui/react'; /** * Types @@ -93,7 +93,7 @@ const EditCoursePage: React.FC = () => { - + diff --git a/src/services/CourseService.ts b/src/services/CourseService.ts index 54aeb5d..bc10fd4 100644 --- a/src/services/CourseService.ts +++ b/src/services/CourseService.ts @@ -96,6 +96,14 @@ class CourseService { const result = await this.api.delete(`/v1/courses/${id}`); return result.data; } + + public async attachStudentsList(courseId: string, ids: Array): Promise> { + const result = await this.api.post( + `/v1/courses/${courseId}/attach-student-list`, + helpers.transformToFormData({ students: ids }) + ); + return result.data; + } } export const CourseServiceInstance = new CourseService(ApiServiceInstance.api); From ef07c9c7beaf16a95c3022e8d3929145ce0bbf1c Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Sat, 11 Dec 2021 19:31:36 +0300 Subject: [PATCH 18/87] feat: added eslint & fixed errors --- .eslintrc.json | 20 + package-lock.json | 1382 +++++++++++++++++ package.json | 4 + .../ConfirmDialog/ConfirmDialog.tsx | 2 +- .../DeleteCourseDialog/DeleteCourseDialog.tsx | 2 +- .../DeleteLessonDialog/DeleteLessonDialog.tsx | 2 +- .../DeleteUserDialog/DeleteUserDialog.tsx | 2 +- src/components/ErrorFallback.tsx | 17 +- src/components/Fields/PasswordField.tsx | 6 +- .../Forms/CourseForm/CourseForm.tsx | 31 +- .../Forms/CourseForm/CreateCourseForm.tsx | 4 +- .../AddStudentsModal/AddSudentsModal.tsx | 6 +- .../SelectUsersInput/SelectUsersInput.tsx | 7 +- src/components/UserBadge/UserBadge.tsx | 2 +- src/contexts/index.ts | 14 +- src/hooks/queries/auth/useLogout.ts | 4 +- .../queries/course/useSetCourseStatus.ts | 2 +- .../auth/components/AuthForm/AuthForm.tsx | 7 +- .../edit/components/LessonList/LessonList.tsx | 8 +- .../StudentTableList/BulkActionsMenu.tsx | 4 +- .../StudentTableList/StudentTableList.tsx | 4 +- .../CreateUserForm/CreateUserForm.tsx | 4 +- .../components/UserSearch/UserSearch.tsx | 4 +- src/services/AuthService.ts | 4 +- src/services/CourseService.ts | 4 +- src/services/UserService.ts | 2 +- src/stores/AuthStore.ts | 28 +- src/stores/CategoryStore.ts | 4 +- src/stores/CourseStore.ts | 30 +- src/stores/UserStore.ts | 166 +- src/stores/pages/EditCourseStore.ts | 32 +- src/types/index.d.ts | 2 +- 32 files changed, 1576 insertions(+), 234 deletions(-) create mode 100644 .eslintrc.json diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..d4c88ef --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,20 @@ +{ + "env": { + "browser": true, + "es2021": true + }, + "extends": ["eslint:recommended", "plugin:react/recommended", "plugin:@typescript-eslint/recommended"], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaFeatures": { + "jsx": true + }, + "ecmaVersion": "latest", + "sourceType": "module" + }, + "plugins": ["react", "@typescript-eslint"], + "rules": { + "react/display-name": "off", + "react/prop-types": "off" + } +} diff --git a/package-lock.json b/package-lock.json index 84556ed..ff14158 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1043,11 +1043,82 @@ "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz", "integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==" }, + "@eslint/eslintrc": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.0.5.tgz", + "integrity": "sha512-BLxsnmK3KyPunz5wmCCpqy0YelEoxxGmH73Is+Z74oOTMtExcjkr3dDR6quwrjh1YspA8DH9gnX1o069KiS9AQ==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.2.0", + "globals": "^13.9.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "globals": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz", + "integrity": "sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + } + } + }, "@hookform/resolvers": { "version": "2.8.2", "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-2.8.2.tgz", "integrity": "sha512-oDTGrm7yHxT4VIv4kJcfjvWvozCbdcL0/jrpbAkeo39FYn78XnKOrrGTNmrXdfu+EQVkNIdZ47IYRFManZ4jUA==" }, + "@humanwhocodes/config-array": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.2.tgz", + "integrity": "sha512-UXOuFCGcwciWckOpmfKDq/GyhlTf9pN/BzG//x8p8zTOFEcGuA68ANXheFS0AGvy3qgZqLBUkMs7hqzqCKOVwA==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + } + }, + "@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, "@popperjs/core": { "version": "2.4.4", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.4.4.tgz", @@ -1114,6 +1185,12 @@ "integrity": "sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==", "dev": true }, + "@types/json-schema": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", + "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", + "dev": true + }, "@types/lodash": { "version": "4.14.175", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.175.tgz", @@ -1234,6 +1311,135 @@ "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.0.tgz", "integrity": "sha1-DSUBJorY+ZYrdA04fEZU9fjiPlI=" }, + "@typescript-eslint/eslint-plugin": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.6.0.tgz", + "integrity": "sha512-MIbeMy5qfLqtgs1hWd088k1hOuRsN9JrHUPwVVKCD99EOUqScd7SrwoZl4Gso05EAP9w1kvLWUVGJOVpRPkDPA==", + "dev": true, + "requires": { + "@typescript-eslint/experimental-utils": "5.6.0", + "@typescript-eslint/scope-manager": "5.6.0", + "debug": "^4.3.2", + "functional-red-black-tree": "^1.0.1", + "ignore": "^5.1.8", + "regexpp": "^3.2.0", + "semver": "^7.3.5", + "tsutils": "^3.21.0" + }, + "dependencies": { + "ignore": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.9.tgz", + "integrity": "sha512-2zeMQpbKz5dhZ9IwL0gbxSW5w0NK/MSAMtNuhgIHEPmaU3vPdKPL0UdvUCXs5SS4JAwsBxysK5sFMW8ocFiVjQ==", + "dev": true + }, + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "@typescript-eslint/experimental-utils": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.6.0.tgz", + "integrity": "sha512-VDoRf3Qj7+W3sS/ZBXZh3LBzp0snDLEgvp6qj0vOAIiAPM07bd5ojQ3CTzF/QFl5AKh7Bh1ycgj6lFBJHUt/DA==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.9", + "@typescript-eslint/scope-manager": "5.6.0", + "@typescript-eslint/types": "5.6.0", + "@typescript-eslint/typescript-estree": "5.6.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0" + }, + "dependencies": { + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + } + } + }, + "@typescript-eslint/parser": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.6.0.tgz", + "integrity": "sha512-YVK49NgdUPQ8SpCZaOpiq1kLkYRPMv9U5gcMrywzI8brtwZjr/tG3sZpuHyODt76W/A0SufNjYt9ZOgrC4tLIQ==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "5.6.0", + "@typescript-eslint/types": "5.6.0", + "@typescript-eslint/typescript-estree": "5.6.0", + "debug": "^4.3.2" + } + }, + "@typescript-eslint/scope-manager": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.6.0.tgz", + "integrity": "sha512-1U1G77Hw2jsGWVsO2w6eVCbOg0HZ5WxL/cozVSTfqnL/eB9muhb8THsP0G3w+BB5xAHv9KptwdfYFAUfzcIh4A==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.6.0", + "@typescript-eslint/visitor-keys": "5.6.0" + } + }, + "@typescript-eslint/types": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.6.0.tgz", + "integrity": "sha512-OIZffked7mXv4mXzWU5MgAEbCf9ecNJBKi+Si6/I9PpTaj+cf2x58h2oHW5/P/yTnPkKaayfjhLvx+crnl5ubA==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.6.0.tgz", + "integrity": "sha512-92vK5tQaE81rK7fOmuWMrSQtK1IMonESR+RJR2Tlc7w4o0MeEdjgidY/uO2Gobh7z4Q1hhS94Cr7r021fMVEeA==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.6.0", + "@typescript-eslint/visitor-keys": "5.6.0", + "debug": "^4.3.2", + "globby": "^11.0.4", + "is-glob": "^4.0.3", + "semver": "^7.3.5", + "tsutils": "^3.21.0" + }, + "dependencies": { + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "@typescript-eslint/visitor-keys": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.6.0.tgz", + "integrity": "sha512-1p7hDp5cpRFUyE3+lvA74egs+RWSgumrBpzBCDzfTFv0aQ7lIeay80yU0hIxgAhwQ6PcasW35kaOCyDOv6O/Ng==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.6.0", + "eslint-visitor-keys": "^3.0.0" + } + }, "@virtuoso.dev/react-urx": { "version": "0.2.8", "resolved": "https://registry.npmjs.org/@virtuoso.dev/react-urx/-/react-urx-0.2.8.tgz", @@ -1263,6 +1469,36 @@ "resolve": "^1.20.0" } }, + "acorn": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.6.0.tgz", + "integrity": "sha512-U1riIR+lBSNi3IbxtaHOIKdH8sLFv3NYfNv8sg7ZsNhcfl4HF2++BfqqrNAxoCLQW1iiylOj76ecnaUxz+z9yw==", + "dev": true + }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true + }, "ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -1277,6 +1513,12 @@ "color-convert": "^1.9.0" } }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, "aria-hidden": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.1.3.tgz", @@ -1292,6 +1534,36 @@ } } }, + "array-includes": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.4.tgz", + "integrity": "sha512-ZTNSQkmWumEbiHO2GF4GmWxYVTiQyJy2XOTa15sdQSrvKn7l+180egQMqlrMOUMCyLMD7pmyQe4mMDUT6Behrw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1", + "get-intrinsic": "^1.1.1", + "is-string": "^1.0.7" + } + }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, + "array.prototype.flatmap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.2.5.tgz", + "integrity": "sha512-08u6rVyi1Lj7oqWbS9nUxliETrtIROT4XGTA4D/LWGten6E3ocm7cy9SIrmNHOL5XVbVuckUp3X6Xyg8/zpvHA==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0" + } + }, "axios": { "version": "0.21.4", "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", @@ -1310,6 +1582,31 @@ "resolve": "^1.12.0" } }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, "browserslist": { "version": "4.17.4", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.17.4.tgz", @@ -1371,6 +1668,12 @@ "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.14.tgz", "integrity": "sha512-mKDjINe3tc6hGelUMNDzuhorIUZ7kS7BwyY0r2wQd2HOH2tRuJykiC06iSEX8y1TuhNzvz4GcJnK16mM2J1NMQ==" }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, "convert-source-map": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", @@ -1399,6 +1702,17 @@ "yaml": "^1.7.2" } }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, "css-box-model": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", @@ -1421,11 +1735,44 @@ "ms": "2.1.2" } }, + "deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "requires": { + "object-keys": "^1.0.12" + } + }, "detect-node-es": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + } + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, "electron-to-chromium": { "version": "1.3.870", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.870.tgz", @@ -1438,6 +1785,15 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dev": true, + "requires": { + "ansi-colors": "^4.1.1" + } + }, "error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -1446,6 +1802,45 @@ "is-arrayish": "^0.2.1" } }, + "es-abstract": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.1.tgz", + "integrity": "sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "get-intrinsic": "^1.1.1", + "get-symbol-description": "^1.0.0", + "has": "^1.0.3", + "has-symbols": "^1.0.2", + "internal-slot": "^1.0.3", + "is-callable": "^1.2.4", + "is-negative-zero": "^2.0.1", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.1", + "is-string": "^1.0.7", + "is-weakref": "^1.0.1", + "object-inspect": "^1.11.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "string.prototype.trimend": "^1.0.4", + "string.prototype.trimstart": "^1.0.4", + "unbox-primitive": "^1.0.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, "esbuild": { "version": "0.13.6", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.13.6.tgz", @@ -1601,17 +1996,340 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, + "eslint": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.4.1.tgz", + "integrity": "sha512-TxU/p7LB1KxQ6+7aztTnO7K0i+h0tDi81YRY9VzB6Id71kNz+fFYnf5HD5UOQmxkzcoa0TlVZf9dpMtUv0GpWg==", + "dev": true, + "requires": { + "@eslint/eslintrc": "^1.0.5", + "@humanwhocodes/config-array": "^0.9.2", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "enquirer": "^2.3.5", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.1.0", + "eslint-utils": "^3.0.0", + "eslint-visitor-keys": "^3.1.0", + "espree": "^9.2.0", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^6.0.1", + "globals": "^13.6.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "progress": "^2.0.0", + "regexpp": "^3.2.0", + "semver": "^7.2.1", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "globals": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz", + "integrity": "sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "eslint-plugin-react": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.27.1.tgz", + "integrity": "sha512-meyunDjMMYeWr/4EBLTV1op3iSG3mjT/pz5gti38UzfM4OPpNc2m0t2xvKCOMU5D6FSdd34BIMFOvQbW+i8GAA==", + "dev": true, + "requires": { + "array-includes": "^3.1.4", + "array.prototype.flatmap": "^1.2.5", + "doctrine": "^2.1.0", + "estraverse": "^5.3.0", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.0.4", + "object.entries": "^1.1.5", + "object.fromentries": "^2.0.5", + "object.hasown": "^1.1.0", + "object.values": "^1.1.5", + "prop-types": "^15.7.2", + "resolve": "^2.0.0-next.3", + "semver": "^6.3.0", + "string.prototype.matchall": "^4.0.6" + }, + "dependencies": { + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "resolve": { + "version": "2.0.0-next.3", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.3.tgz", + "integrity": "sha512-W8LucSynKUIDu9ylraa7ueVZ7hc0uAgJBxVsQSKOXOyle8a93qXhcz+XAXZ8bIq2d6i4Ehddn6Evt+0/UwKk6Q==", + "dev": true, + "requires": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + } + } + } + }, + "eslint-scope": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.0.tgz", + "integrity": "sha512-aWwkhnS0qAXqNOgKOK0dJ2nvzEbhEvpy8OlJ9kZ0FeZnA6zpjv1/Vei+puGFFX7zkPCkHHXb7IDX3A+7yPrRWg==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^2.0.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true + } + } + }, + "eslint-visitor-keys": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.1.0.tgz", + "integrity": "sha512-yWJFpu4DtjsWKkt5GeNBBuZMlNcYVs6vRCLoCVEJrTjaSB6LC98gFipNK/erM2Heg/E8mIK+hXG/pJMLK+eRZA==", + "dev": true + }, + "espree": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.2.0.tgz", + "integrity": "sha512-oP3utRkynpZWF/F2x/HZJ+AGtnIclaR7z1pYPxy7NYM2fSO6LgK/Rkny8anRSPK/VwEA1eqm2squui0T7ZMOBg==", + "dev": true, + "requires": { + "acorn": "^8.6.0", + "acorn-jsx": "^5.3.1", + "eslint-visitor-keys": "^3.1.0" + } + }, + "esquery": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, "estree-walker": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "dev": true }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-glob": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz", + "integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, "find-root": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" }, + "flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "requires": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + } + }, + "flatted": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.4.tgz", + "integrity": "sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw==", + "dev": true + }, "focus-lock": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/focus-lock/-/focus-lock-0.8.1.tgz", @@ -1670,6 +2388,12 @@ "tslib": "^2.1.0" } }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, "fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", @@ -1682,6 +2406,12 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, "gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -1709,12 +2439,67 @@ "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==" }, + "get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + } + }, + "glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, "globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true }, + "globby": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz", + "integrity": "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.1.1", + "ignore": "^5.1.4", + "merge2": "^1.3.0", + "slash": "^3.0.0" + }, + "dependencies": { + "ignore": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.9.tgz", + "integrity": "sha512-2zeMQpbKz5dhZ9IwL0gbxSW5w0NK/MSAMtNuhgIHEPmaU3vPdKPL0UdvUCXs5SS4JAwsBxysK5sFMW8ocFiVjQ==", + "dev": true + } + } + }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -1723,6 +2508,12 @@ "function-bind": "^1.1.1" } }, + "has-bigints": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", + "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==", + "dev": true + }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -1733,6 +2524,15 @@ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==" }, + "has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, "hey-listen": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/hey-listen/-/hey-listen-1.0.8.tgz", @@ -1759,6 +2559,12 @@ "react-is": "^16.7.0" } }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, "import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -1768,6 +2574,39 @@ "resolve-from": "^4.0.0" } }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "internal-slot": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", + "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + } + }, "invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -1781,6 +2620,31 @@ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" }, + "is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "requires": { + "has-bigints": "^1.0.1" + } + }, + "is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-callable": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", + "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", + "dev": true + }, "is-core-module": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz", @@ -1789,18 +2653,106 @@ "has": "^1.0.3" } }, + "is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, "is-docker": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", "dev": true }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, "is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-number-object": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.6.tgz", + "integrity": "sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-shared-array-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz", + "integrity": "sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==", + "dev": true + }, + "is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, "is-wsl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", @@ -1815,6 +2767,12 @@ "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, "js-cookie": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.1.tgz", @@ -1825,6 +2783,15 @@ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -1836,6 +2803,18 @@ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, "json5": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", @@ -1845,6 +2824,26 @@ "minimist": "^1.2.5" } }, + "jsx-ast-utils": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.2.1.tgz", + "integrity": "sha512-uP5vu8xfy2F9A6LGC22KO7e2/vGTS1MhP+18f++ZNlf0Ohaxbc9nIEwHAsejlJKyzfZzU5UIhe5ItYkitcZnZA==", + "dev": true, + "requires": { + "array-includes": "^3.1.3", + "object.assign": "^4.1.2" + } + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, "lines-and-columns": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", @@ -1860,6 +2859,12 @@ "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, "lodash.mergewith": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", @@ -1873,11 +2878,36 @@ "js-tokens": "^3.0.0 || ^4.0.0" } }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, "memoize-one": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "micromatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.2.3" + } + }, "mini-create-react-context": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz", @@ -1887,6 +2917,15 @@ "tiny-warning": "^1.0.3" } }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, "minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", @@ -1933,6 +2972,12 @@ "integrity": "sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ==", "dev": true }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, "node-releases": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.0.tgz", @@ -1949,6 +2994,76 @@ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz", "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==" }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + }, + "object.entries": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.5.tgz", + "integrity": "sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + } + }, + "object.fromentries": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.5.tgz", + "integrity": "sha512-CAyG5mWQRRiBU57Re4FKoTBjXfDoNwdFVH2Y1tS9PqCsfUTymAohOkEMSG3aRNKmv4lV3O7p1et7c187q6bynw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + } + }, + "object.hasown": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.0.tgz", + "integrity": "sha512-MhjYRfj3GBlhSkDHo6QmvgjRLXQ2zndabdf3nX0yTyZK9rPfxb6uRpAac8HXNLy1GpqWtZ81Qh4v3uOls2sRAg==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + } + }, + "object.values": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz", + "integrity": "sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, "open": { "version": "7.4.2", "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", @@ -1959,6 +3074,20 @@ "is-wsl": "^2.1.1" } }, + "optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + } + }, "parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -1978,6 +3107,18 @@ "lines-and-columns": "^1.1.6" } }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, "path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -2038,6 +3179,18 @@ } } }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, "prop-types": { "version": "15.7.2", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", @@ -2053,6 +3206,12 @@ "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.4.tgz", "integrity": "sha512-sFPkHQjVKheDNnPvotjQmm3KD3uk1fWKUN7CrpdbwmUx3CrG3QiM8QpTSimvig5vTXmTvjz7+TDvXOI9+4rkcg==" }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, "qs": { "version": "6.10.1", "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz", @@ -2061,6 +3220,12 @@ "side-channel": "^1.0.4" } }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, "raf-schd": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", @@ -2303,6 +3468,22 @@ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" }, + "regexp.prototype.flags": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz", + "integrity": "sha512-JiBdRBq91WlY7uRJ0ds7R+dU02i6LKi8r3BuQhNXn+kmeLN+EfHhfjqMRis1zJxnlu88hq/4dx0P2OP3APRTOA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true + }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -2328,6 +3509,21 @@ "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz", "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==" }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, "rollup": { "version": "2.58.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.58.0.tgz", @@ -2430,6 +3626,15 @@ } } }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -2450,6 +3655,21 @@ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", "dev": true }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, "side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", @@ -2460,6 +3680,12 @@ "object-inspect": "^1.9.0" } }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -2482,6 +3708,42 @@ "strip-ansi": "^6.0.1" } }, + "string.prototype.matchall": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.6.tgz", + "integrity": "sha512-6WgDX8HmQqvEd7J+G6VtAahhsQIssiZ8zl7zKh1VDMFyL3hRTJP4FTNA3RbIp2TOQ9AYNDcc7e3fH0Qbup+DBg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1", + "get-intrinsic": "^1.1.1", + "has-symbols": "^1.0.2", + "internal-slot": "^1.0.3", + "regexp.prototype.flags": "^1.3.1", + "side-channel": "^1.0.4" + } + }, + "string.prototype.trimend": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", + "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "string.prototype.trimstart": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", + "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, "strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -2491,6 +3753,12 @@ "ansi-regex": "^5.0.1" } }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, "style-value-types": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/style-value-types/-/style-value-types-4.1.4.tgz", @@ -2513,6 +3781,12 @@ "has-flag": "^3.0.0" } }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, "tiny-invariant": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz", @@ -2528,6 +3802,15 @@ "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, "toggle-selection": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", @@ -2543,12 +3826,65 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" }, + "tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } + } + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + }, "typescript": { "version": "4.5.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.2.tgz", "integrity": "sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==", "dev": true }, + "unbox-primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", + "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "has-bigints": "^1.0.1", + "has-symbols": "^1.0.2", + "which-boxed-primitive": "^1.0.2" + } + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, "use-callback-ref": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.2.5.tgz", @@ -2575,6 +3911,12 @@ } } }, + "v8-compile-cache": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", + "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", + "dev": true + }, "validator": { "version": "13.6.0", "resolved": "https://registry.npmjs.org/validator/-/validator-13.6.0.tgz", @@ -2606,6 +3948,46 @@ "loose-envify": "^1.0.0" } }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "requires": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + } + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "yaml": { "version": "1.10.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", diff --git a/package.json b/package.json index db21b00..f735c49 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,11 @@ "@types/react-helmet": "^6.1.2", "@types/react-router-dom": "^5.1.8", "@types/validator": "^13.6.3", + "@typescript-eslint/eslint-plugin": "^5.6.0", + "@typescript-eslint/parser": "^5.6.0", "@vitejs/plugin-react": "^1.0.0", + "eslint": "^8.4.1", + "eslint-plugin-react": "^7.27.1", "rollup-plugin-visualizer": "^5.5.2", "typescript": "^4.5.2", "vite": "^2.6.4" diff --git a/src/components/ConfirmDialog/ConfirmDialog.tsx b/src/components/ConfirmDialog/ConfirmDialog.tsx index 156b5e1..2621c37 100644 --- a/src/components/ConfirmDialog/ConfirmDialog.tsx +++ b/src/components/ConfirmDialog/ConfirmDialog.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React from 'react'; import { AlertDialog, AlertDialogBody, diff --git a/src/components/Dialogs/DeleteCourseDialog/DeleteCourseDialog.tsx b/src/components/Dialogs/DeleteCourseDialog/DeleteCourseDialog.tsx index e424eb5..b49c904 100644 --- a/src/components/Dialogs/DeleteCourseDialog/DeleteCourseDialog.tsx +++ b/src/components/Dialogs/DeleteCourseDialog/DeleteCourseDialog.tsx @@ -36,7 +36,7 @@ const DeleteCourseDialog: React.FC = ({ onClose, is if (isMountedRef.current && location.pathname !== '/courses') { history.push('/courses'); } - } catch (error: any) { + } catch (error) { console.error(error); } } diff --git a/src/components/Dialogs/DeleteLessonDialog/DeleteLessonDialog.tsx b/src/components/Dialogs/DeleteLessonDialog/DeleteLessonDialog.tsx index 7a6084c..788c009 100644 --- a/src/components/Dialogs/DeleteLessonDialog/DeleteLessonDialog.tsx +++ b/src/components/Dialogs/DeleteLessonDialog/DeleteLessonDialog.tsx @@ -28,7 +28,7 @@ const DeleteLessonDialog: React.FC = ({ lesson, isO if (isConfirmed) { try { await deleteLesson(lesson.id); - } catch (error: any) { + } catch (error) { console.error(error); } } diff --git a/src/components/Dialogs/DeleteUserDialog/DeleteUserDialog.tsx b/src/components/Dialogs/DeleteUserDialog/DeleteUserDialog.tsx index 9e41895..dfbc1c1 100644 --- a/src/components/Dialogs/DeleteUserDialog/DeleteUserDialog.tsx +++ b/src/components/Dialogs/DeleteUserDialog/DeleteUserDialog.tsx @@ -50,7 +50,7 @@ const DeleteUserDialog: React.FC = ({ isOpen, onClose role: searchingRole, search, }); - } catch (error: any) { + } catch (error) { console.error(error); } } diff --git a/src/components/ErrorFallback.tsx b/src/components/ErrorFallback.tsx index 9ec6ab9..5d7f0b5 100644 --- a/src/components/ErrorFallback.tsx +++ b/src/components/ErrorFallback.tsx @@ -1,5 +1,6 @@ import React from 'react'; import Cookies from 'js-cookie'; +import axios, { AxiosError } from 'axios'; import { Redirect } from 'react-router'; import { Icon, Box, Heading, Text } from '@chakra-ui/react'; import { MdError } from 'react-icons/md'; @@ -11,7 +12,7 @@ import { useEffect } from 'react'; import { useToast } from '@chakra-ui/toast'; import { useRootStore } from '@educt/hooks/useRootStore'; -const ErrorFallback: React.FC<{ error: any }> = props => { +const ErrorFallback: React.FC<{ error: Error | AxiosError }> = props => { const { userStore, authStore } = useRootStore(); const { error } = props; const toast = useToast(); @@ -20,12 +21,14 @@ const ErrorFallback: React.FC<{ error: any }> = props => { toast({ title: `${error.message}`, duration: 10000, isClosable: true, status: 'error' }); }); - if (error.response) { - if (error.response.status === 401) { - userStore.me = null; - authStore.setIsLoggedIn(false); - Cookies.remove('logged_in'); - } else if (error.response.status === 404) return ; + if (axios.isAxiosError(error)) { + if (error.response) { + if (error.response.status === 401) { + userStore.me = null; + authStore.setIsLoggedIn(false); + Cookies.remove('logged_in'); + } else if (error.response.status === 404) return ; + } } return ( diff --git a/src/components/Fields/PasswordField.tsx b/src/components/Fields/PasswordField.tsx index e14006a..38d25f9 100644 --- a/src/components/Fields/PasswordField.tsx +++ b/src/components/Fields/PasswordField.tsx @@ -47,7 +47,9 @@ export const PasswordField = React.forwardRefPassword - } /> + + + ); }); + +PasswordField.displayName = 'PasswordField'; diff --git a/src/components/Forms/CourseForm/CourseForm.tsx b/src/components/Forms/CourseForm/CourseForm.tsx index eaf445a..bc39571 100644 --- a/src/components/Forms/CourseForm/CourseForm.tsx +++ b/src/components/Forms/CourseForm/CourseForm.tsx @@ -19,7 +19,6 @@ import FileSelect from '@educt/components/FileSelect'; /** * Hooks */ -import { useToast } from '@chakra-ui/toast'; import { useErrorHandler } from 'react-error-boundary'; /** @@ -51,7 +50,6 @@ const CourseForm: React.FC = ({ onSubmit, buttonLabel, isLo } = reactHookForm; const handleError = useErrorHandler(); - const toast = useToast(); /** * Load users into select field @@ -64,12 +62,8 @@ const CourseForm: React.FC = ({ onSubmit, buttonLabel, isLo label: user.fullname, value: user.id, })); - } catch (error: any) { - if (error.response) { - toast({ title: 'Error' }); - } else { - handleError(error); - } + } catch (error) { + handleError(error); } }; @@ -83,12 +77,8 @@ const CourseForm: React.FC = ({ onSubmit, buttonLabel, isLo label: category.title, value: category.id, })); - } catch (error: any) { - if (error.response) { - toast({ title: 'Error' }); - } else { - handleError(error); - } + } catch (error) { + handleError(error); } }; @@ -102,14 +92,11 @@ const CourseForm: React.FC = ({ onSubmit, buttonLabel, isLo Name - 90 ? 'red.500' : 'gray.500'}> - {watchTitle?.length || 0}/90 - - } - /> + + 90 ? 'red.500' : 'gray.500'}> + {watchTitle?.length || 0}/90 + + e.g Web Development in Javascript diff --git a/src/components/Forms/CourseForm/CreateCourseForm.tsx b/src/components/Forms/CourseForm/CreateCourseForm.tsx index b6ecaad..defbb91 100644 --- a/src/components/Forms/CourseForm/CreateCourseForm.tsx +++ b/src/components/Forms/CourseForm/CreateCourseForm.tsx @@ -26,9 +26,7 @@ import { useCreateCourse } from '@educt/hooks/queries'; */ import CourseFormSchema from './CourseForm.validator'; -type CreateFormCoursePropsType = {}; - -const CreateFormCourse: React.FC = () => { +const CreateFormCourse: React.FC = () => { const form = useForm({ resolver: yupResolver(CourseFormSchema) }); const { createCourse, isLoading } = useCreateCourse(); const history = useHistory(); diff --git a/src/components/Modals/AddStudentsModal/AddSudentsModal.tsx b/src/components/Modals/AddStudentsModal/AddSudentsModal.tsx index 770f14e..bff81d3 100644 --- a/src/components/Modals/AddStudentsModal/AddSudentsModal.tsx +++ b/src/components/Modals/AddStudentsModal/AddSudentsModal.tsx @@ -29,10 +29,10 @@ const AddStudentsModal: React.FC = ({ isOpen, onClose const ids = students.map(s => s.id); await CourseServiceInstance.attachStudentsList(courseId, ids); - toast({ title: 'Students successfully attached.', status: 'success' }); + toast({ title: 'Students successfully added.', status: 'success' }); onClose(); - } catch (error: any) { - throw error; + } catch (error) { + console.error(error); } finally { if (isMountedRef.current) setIsLoading(false); } diff --git a/src/components/SelectUsersInput/SelectUsersInput.tsx b/src/components/SelectUsersInput/SelectUsersInput.tsx index 12f66d2..17ca8fd 100644 --- a/src/components/SelectUsersInput/SelectUsersInput.tsx +++ b/src/components/SelectUsersInput/SelectUsersInput.tsx @@ -42,7 +42,7 @@ const SelectUsersInput: React.FC = ({ placeholder, se setIsLoading(true); const { data } = await UserServiceInstance.fetchAll({ search, ...params }); setUsers(data); - } catch (error: any) { + } catch (error) { handleError(error); } finally { setIsLoading(false); @@ -58,6 +58,7 @@ const SelectUsersInput: React.FC = ({ placeholder, se return updated; }); }; + const handleRemoveSelected = (id: IUser['id']) => () => { setSelected(prev => { const updated = prev.filter(u => u.id !== id); @@ -91,7 +92,9 @@ const SelectUsersInput: React.FC = ({ placeholder, se type='text' placeholder={placeholder || 'Search for users...'} /> - } /> + + + {isLoading && ( diff --git a/src/components/UserBadge/UserBadge.tsx b/src/components/UserBadge/UserBadge.tsx index eeebf09..6a37661 100644 --- a/src/components/UserBadge/UserBadge.tsx +++ b/src/components/UserBadge/UserBadge.tsx @@ -1,4 +1,4 @@ -import { BadgeProps, Badge, Box } from '@chakra-ui/react'; +import { BadgeProps, Badge } from '@chakra-ui/react'; import React from 'react'; import { UserRoleEnum } from '@educt/enums'; import { IUserRole } from '@educt/interfaces'; diff --git a/src/contexts/index.ts b/src/contexts/index.ts index 066f0f8..1bc8b83 100644 --- a/src/contexts/index.ts +++ b/src/contexts/index.ts @@ -6,23 +6,23 @@ export const StoreContext = createContext(undefined); export const ChangeEmailPageContext = createContext({ isCodeSent: false, - setIsCodeSent: () => {}, + setIsCodeSent: () => null, confirmEmailData: undefined, - setConfirmEmailData: () => {}, + setConfirmEmailData: () => null, }); export const UsersPageContext = createContext({ searchingRole: undefined, - setSearchingRole: () => {}, + setSearchingRole: () => null, searchingPage: 1, - setSearchingPage: () => {}, + setSearchingPage: () => null, search: undefined, - setSearch: () => {}, + setSearch: () => null, }); export const CoursesPageContext = createContext({ selectedCategory: undefined, - setSelectedCategory: () => {}, + setSelectedCategory: () => null, courseStatus: undefined, - setCourseStatus: () => {}, + setCourseStatus: () => null, }); diff --git a/src/hooks/queries/auth/useLogout.ts b/src/hooks/queries/auth/useLogout.ts index f1facd8..040fedf 100644 --- a/src/hooks/queries/auth/useLogout.ts +++ b/src/hooks/queries/auth/useLogout.ts @@ -4,7 +4,7 @@ import { useRootStore } from '../../useRootStore'; import { useErrorHandler } from 'react-error-boundary'; import useAsync from '../../useAsync'; -type LogoutResultDataType = {}; +type LogoutResultDataType = Record; const useLogout = () => { const { authStore } = useRootStore(); @@ -18,7 +18,7 @@ const useLogout = () => { history.push('/auth'); toast({ title: 'You are logged out.', isClosable: true, status: 'info' }); return result.data; - } catch (error: any) { + } catch (error) { handleError(error); return Promise.reject(error); } diff --git a/src/hooks/queries/course/useSetCourseStatus.ts b/src/hooks/queries/course/useSetCourseStatus.ts index 0797056..db1cd58 100644 --- a/src/hooks/queries/course/useSetCourseStatus.ts +++ b/src/hooks/queries/course/useSetCourseStatus.ts @@ -7,7 +7,7 @@ import useAsync from '../../useAsync'; import { useRootStore } from '../../useRootStore'; import { useToast } from '@chakra-ui/toast'; -type SetCourseStatusResultDataType = {}; +type SetCourseStatusResultDataType = Record; export type SetCourseStatusFnType = (id: string, newStatus: CourseStatusEnum) => Promise; const useSetCourseStatus = () => { diff --git a/src/pages/auth/components/AuthForm/AuthForm.tsx b/src/pages/auth/components/AuthForm/AuthForm.tsx index 4b81387..412773f 100644 --- a/src/pages/auth/components/AuthForm/AuthForm.tsx +++ b/src/pages/auth/components/AuthForm/AuthForm.tsx @@ -9,9 +9,8 @@ import { Icon, Text, Stack, - Box, } from '@chakra-ui/react'; -import { MdAccountCircle, MdVpnKey } from 'react-icons/md'; +import { MdAccountCircle } from 'react-icons/md'; import { yupResolver } from '@hookform/resolvers/yup'; /** @@ -58,7 +57,9 @@ const AuthForm: React.FC = () => { Login - } /> + + + diff --git a/src/pages/courses/edit/components/LessonList/LessonList.tsx b/src/pages/courses/edit/components/LessonList/LessonList.tsx index bfb63f3..9eb8949 100644 --- a/src/pages/courses/edit/components/LessonList/LessonList.tsx +++ b/src/pages/courses/edit/components/LessonList/LessonList.tsx @@ -74,12 +74,8 @@ const LessonList: React.FC = ({ course }) => { try { const data = await LessonServiceInstance.saveOrder(ids); return data; - } catch (error: any) { - if (error.response) { - console.error(error); - } else { - handleError(error); - } + } catch (error) { + handleError(error); } }; diff --git a/src/pages/courses/edit/components/StudentTableList/BulkActionsMenu.tsx b/src/pages/courses/edit/components/StudentTableList/BulkActionsMenu.tsx index ad7aa40..ba47e76 100644 --- a/src/pages/courses/edit/components/StudentTableList/BulkActionsMenu.tsx +++ b/src/pages/courses/edit/components/StudentTableList/BulkActionsMenu.tsx @@ -19,8 +19,8 @@ const BulkActionsMenu: React.FC = ({ selected, rows, s ); setRows(rows.filter(r => selected.every(s => s.id !== r.id))); }; - const handleDelete = async () => {}; - const handleExport = async () => {}; + const handleDelete = async () => undefined; + const handleExport = async () => undefined; return ( diff --git a/src/pages/courses/edit/components/StudentTableList/StudentTableList.tsx b/src/pages/courses/edit/components/StudentTableList/StudentTableList.tsx index b6faf54..3a2a6f4 100644 --- a/src/pages/courses/edit/components/StudentTableList/StudentTableList.tsx +++ b/src/pages/courses/edit/components/StudentTableList/StudentTableList.tsx @@ -93,7 +93,9 @@ const StudentTableList: React.FC = props => { - } /> + + + diff --git a/src/pages/users/components/CreateUserForm/CreateUserForm.tsx b/src/pages/users/components/CreateUserForm/CreateUserForm.tsx index 9d9abce..9013f58 100644 --- a/src/pages/users/components/CreateUserForm/CreateUserForm.tsx +++ b/src/pages/users/components/CreateUserForm/CreateUserForm.tsx @@ -52,8 +52,6 @@ import { UsersPageContext } from '@educt/contexts'; import CreateUserSchema from './CreateUserForm.validator'; import { CreateButton } from '@educt/components/Buttons'; -type CreateUserFormPropsType = {}; - type CreateUserInputType = { first_name: string; last_name: string; @@ -63,7 +61,7 @@ type CreateUserInputType = { password: string; }; -const CreateUserModal: React.FC = () => { +const CreateUserModal: React.FC = () => { const { userStore } = useRootStore(); const { searchingRole, search } = useContext(UsersPageContext); const { createUser, isLoading } = useCreateUser(); diff --git a/src/pages/users/components/UserSearch/UserSearch.tsx b/src/pages/users/components/UserSearch/UserSearch.tsx index fe3f7d0..c5f7937 100644 --- a/src/pages/users/components/UserSearch/UserSearch.tsx +++ b/src/pages/users/components/UserSearch/UserSearch.tsx @@ -50,7 +50,9 @@ const UserSearch: React.FC = () => { - } /> + + + diff --git a/src/services/AuthService.ts b/src/services/AuthService.ts index 3d81255..f23ca6b 100644 --- a/src/services/AuthService.ts +++ b/src/services/AuthService.ts @@ -11,7 +11,7 @@ export interface AuthServiceImpl { /** * Logout user */ - requestLogout(): Promise>; + requestLogout(): Promise>>; } class AuthService implements AuthServiceImpl { @@ -41,7 +41,7 @@ class AuthService implements AuthServiceImpl { * * @returns Empty obj */ - public async requestLogout(): Promise> { + public async requestLogout(): Promise>> { const result = await this.api.post('v1/auth/logout'); return result.data; } diff --git a/src/services/CourseService.ts b/src/services/CourseService.ts index bc10fd4..2919e74 100644 --- a/src/services/CourseService.ts +++ b/src/services/CourseService.ts @@ -49,7 +49,7 @@ class CourseService { * @param status Course status * @returns Empty data */ - public async setStatus(id: string, status: CourseStatusEnum): Promise> { + public async setStatus(id: string, status: CourseStatusEnum): Promise>> { const result = await this.api.post(`/v1/courses/${id}/set-status`, { status, }); @@ -97,7 +97,7 @@ class CourseService { return result.data; } - public async attachStudentsList(courseId: string, ids: Array): Promise> { + public async attachStudentsList(courseId: string, ids: Array): Promise>> { const result = await this.api.post( `/v1/courses/${courseId}/attach-student-list`, helpers.transformToFormData({ students: ids }) diff --git a/src/services/UserService.ts b/src/services/UserService.ts index 8737e65..4b357b6 100644 --- a/src/services/UserService.ts +++ b/src/services/UserService.ts @@ -99,7 +99,7 @@ class UserService { * @param newPassword New password * @returns Empty object */ - public async updatePassword(oldPassword: string, newPassword: string): Promise> { + public async updatePassword(oldPassword: string, newPassword: string): Promise>> { const result = await this.api.patch('v1/me/password', { oldPassword, newPassword, diff --git a/src/stores/AuthStore.ts b/src/stores/AuthStore.ts index c0baa7b..48bc225 100644 --- a/src/stores/AuthStore.ts +++ b/src/stores/AuthStore.ts @@ -14,7 +14,7 @@ import RootStore from './RootStore'; export default class AuthStore { public root: RootStore; - public isLoggedIn: boolean = !!Cookies.get('logged_in'); + public isLoggedIn = !!Cookies.get('logged_in'); constructor(root: RootStore) { this.root = root; @@ -26,28 +26,20 @@ export default class AuthStore { } public async login(login: string, password: string) { - try { - const result = await AuthServiceInstance.requestLogin(login, password); + const result = await AuthServiceInstance.requestLogin(login, password); - this.setIsLoggedIn(true); - Cookies.set('logged_in', 'true'); + this.setIsLoggedIn(true); + Cookies.set('logged_in', 'true'); - return result; - } catch (error) { - throw error; - } + return result; } public async logout() { - try { - const result = await AuthServiceInstance.requestLogout(); + const result = await AuthServiceInstance.requestLogout(); - this.setIsLoggedIn(false); - this.root.userStore.reset(); - Cookies.remove('logged_in'); - return result; - } catch (error) { - throw error; - } + this.setIsLoggedIn(false); + this.root.userStore.reset(); + Cookies.remove('logged_in'); + return result; } } diff --git a/src/stores/CategoryStore.ts b/src/stores/CategoryStore.ts index e3b3ca4..795e7f1 100644 --- a/src/stores/CategoryStore.ts +++ b/src/stores/CategoryStore.ts @@ -20,7 +20,7 @@ export default class CategoryStore { public categories: ICategory[] | null = null; - public isLoading: boolean = false; + public isLoading = false; constructor(root: RootStore) { this.root = root; @@ -41,8 +41,6 @@ export default class CategoryStore { }); return result; - } catch (error: any) { - throw error; } finally { this.setLoading(false); } diff --git a/src/stores/CourseStore.ts b/src/stores/CourseStore.ts index 8929afd..efe1d65 100644 --- a/src/stores/CourseStore.ts +++ b/src/stores/CourseStore.ts @@ -22,7 +22,7 @@ export default class CourseStore { public courses: Omit[] | null = null; - public isLoading: boolean = false; + public isLoading = false; constructor(root: RootStore) { this.root = root; @@ -46,30 +46,24 @@ export default class CourseStore { }); return result; - } catch (error: any) { - throw error; } finally { this.setLoading(false); } } public async deleteCourse(id: string) { - try { - const result = await CourseServiceInstance.delete(id); + const result = await CourseServiceInstance.delete(id); - /** - * Remove deleted course form store - */ - runInAction(() => { - if (this.courses !== null) { - this.courses = this.courses.filter(course => course.id !== id); - } - }); + /** + * Remove deleted course form store + */ + runInAction(() => { + if (this.courses !== null) { + this.courses = this.courses.filter(course => course.id !== id); + } + }); - return result; - } catch (error: any) { - throw error; - } + return result; } public async setCourseStatus(id: string, status: CourseStatusEnum) { @@ -95,7 +89,7 @@ export default class CourseStore { */ const result = await CourseServiceInstance.setStatus(id, status); return result; - } catch (error: any) { + } catch (error) { /** * Resume status if there was an error on request */ diff --git a/src/stores/UserStore.ts b/src/stores/UserStore.ts index 1df1df0..1e74717 100644 --- a/src/stores/UserStore.ts +++ b/src/stores/UserStore.ts @@ -27,7 +27,7 @@ export default class UserStore { public pagination: IPaginationMeta | undefined; - public isLoading: boolean = false; + public isLoading = false; constructor(root: RootStore) { this.root = root; @@ -49,79 +49,65 @@ export default class UserStore { }); return result; - } catch (error: any) { - throw error; } finally { this.setLoading(false); } } public async loadCurrentUserData() { - try { - const result = await UserServiceInstance.fetchMe(); - const { - data: { id, first_name, last_name, fullname, email, roles, contacts, courses, created_at, updated_at }, - } = result; + const result = await UserServiceInstance.fetchMe(); + const { + data: { id, first_name, last_name, fullname, email, roles, contacts, courses, created_at, updated_at }, + } = result; - runInAction(() => { - this.me = { - id, - first_name, - last_name, - email, - roles, - fullname, - contacts, - courses, - isAdmin: helpers.userContainRoles(roles, [UserRoleEnum.ADMIN]), - isTeacher: helpers.userContainRoles(roles, [UserRoleEnum.TEACHER]), - isStudent: helpers.userContainRoles(roles, [UserRoleEnum.STUDENT]), - created_at, - updated_at, - }; - }); + runInAction(() => { + this.me = { + id, + first_name, + last_name, + email, + roles, + fullname, + contacts, + courses, + isAdmin: helpers.userContainRoles(roles, [UserRoleEnum.ADMIN]), + isTeacher: helpers.userContainRoles(roles, [UserRoleEnum.TEACHER]), + isStudent: helpers.userContainRoles(roles, [UserRoleEnum.STUDENT]), + created_at, + updated_at, + }; + }); - return result; - } catch (error: any) { - throw error; - } + return result; } public async createUser(data: CreateUserParamsType, paramsContext?: FetchUsersParamsType) { - try { - const result = await UserServiceInstance.create(data); + const result = await UserServiceInstance.create(data); - if (this.users !== null) { - /** - * Fetch updated users data with new pagination data - */ - await this.loadUsersData(paramsContext); - } - - return result; - } catch (error: any) { - throw error; + if (this.users !== null) { + /** + * Fetch updated users data with new pagination data + */ + await this.loadUsersData(paramsContext); } + + return result; } public async updateUser(id: string, params: UpdateUserParamsType) { - try { - const result = await UserServiceInstance.update(id, params); + const result = await UserServiceInstance.update(id, params); - runInAction(() => { - if (this.users !== null) { - const userIndex = this.users.findIndex(user => user.id === id); + runInAction(() => { + if (this.users !== null) { + const userIndex = this.users.findIndex(user => user.id === id); - if (userIndex !== -1) { - this.users[userIndex] = result.data; - } + if (userIndex !== -1) { + this.users[userIndex] = result.data; } - }); + } + }); - return result; - } catch (error: any) { - throw error; - } + return result; } /** @@ -131,20 +117,16 @@ export default class UserStore { * @returns */ public async deleteUser(id: string, paramsContext?: FetchUsersParamsType) { - try { - const result = await UserServiceInstance.delete(id); + const result = await UserServiceInstance.delete(id); - if (this.users !== null) { - /** - * Fetch updated users data with new pagination data - */ - await this.loadUsersData(paramsContext); - } - - return result; - } catch (error: any) { - throw error; + if (this.users !== null) { + /** + * Fetch updated users data with new pagination data + */ + await this.loadUsersData(paramsContext); } + + return result; } /** @@ -154,19 +136,15 @@ export default class UserStore { * @returns Updated user contacts */ public async updateCurrentUserContacts(data: IUserContacts) { - try { - const result = await UserServiceInstance.updateContacts(data); + const result = await UserServiceInstance.updateContacts(data); - runInAction(() => { - if (this.me) { - this.me.contacts = result.data; - } - }); + runInAction(() => { + if (this.me) { + this.me.contacts = result.data; + } + }); - return result; - } catch (error: any) { - throw error; - } + return result; } /** @@ -177,12 +155,8 @@ export default class UserStore { * @returns Data result status */ public async updateCurrentUserPassword(oldPassword: string, newPassword: string) { - try { - const result = await UserServiceInstance.updatePassword(oldPassword, newPassword); - return result; - } catch (error: any) { - throw error; - } + const result = await UserServiceInstance.updatePassword(oldPassword, newPassword); + return result; } /** @@ -192,12 +166,8 @@ export default class UserStore { * @returns Expires at code in seconds */ public async updateCurrentUserEmail(email: string) { - try { - const result = await UserServiceInstance.updateEmail(email); - return result; - } catch (error: any) { - throw error; - } + const result = await UserServiceInstance.updateEmail(email); + return result; } /** @@ -208,17 +178,13 @@ export default class UserStore { * @returns Updated user email */ public async updateCurrentUserEmailConfirm(email: string, confirmationCode: string) { - try { - const result = await UserServiceInstance.updateEmailConfirm(email, confirmationCode); - runInAction(() => { - if (this.me) { - this.me.email = result.data.email; - } - }); - return result; - } catch (error: any) { - throw error; - } + const result = await UserServiceInstance.updateEmailConfirm(email, confirmationCode); + runInAction(() => { + if (this.me) { + this.me.email = result.data.email; + } + }); + return result; } public reset(): void { diff --git a/src/stores/pages/EditCourseStore.ts b/src/stores/pages/EditCourseStore.ts index 3a3d586..4845188 100644 --- a/src/stores/pages/EditCourseStore.ts +++ b/src/stores/pages/EditCourseStore.ts @@ -23,17 +23,13 @@ export default class EditCourseStore { * @returns Course response */ public async loadCourseById(id: string) { - try { - const result = await CourseServiceInstance.fetchById(id); + const result = await CourseServiceInstance.fetchById(id); - runInAction(() => { - this.course = result.data; - }); + runInAction(() => { + this.course = result.data; + }); - return result; - } catch (error: any) { - throw error; - } + return result; } /** @@ -43,19 +39,15 @@ export default class EditCourseStore { * @returns Deleted lesson response */ public async deleteLessonById(id: string) { - try { - const result = await LessonServiceInstance.deleteLesson(id); + const result = await LessonServiceInstance.deleteLesson(id); - runInAction(() => { - if (this.course) { - this.course.lessons = this.course.lessons.filter(lesson => lesson.id !== result.data.id); - } - }); + runInAction(() => { + if (this.course) { + this.course.lessons = this.course.lessons.filter(lesson => lesson.id !== result.data.id); + } + }); - return result; - } catch (error) { - throw error; - } + return result; } /** diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 6dc0175..b3492b7 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -1,6 +1,6 @@ import React from 'react'; import { CourseStatusEnum, UserRoleEnum } from '@educt/enums'; -import { ICategory, ICourse, IUser } from '@educt/interfaces'; +import { ICategory } from '@educt/interfaces'; import { IconType } from 'react-icons/lib'; export type LinkType = { From 08d2e0ccde364f1618453380c4dd8c6b79581fe8 Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Sat, 11 Dec 2021 23:30:26 +0300 Subject: [PATCH 19/87] feat: added delete actions on users in table --- .../AddStudentsModal/AddSudentsModal.tsx | 24 +++++-- .../SelectStudentsInput.tsx} | 14 +++- src/components/SelectStudentsInput/index.ts | 3 + src/components/SelectUsersInput/index.ts | 3 - .../StudentTableList/Cells/index.tsx | 18 +++-- .../StudentTableList/StudentTableList.tsx | 72 +++++++++++++++++-- .../StudentTableList/StudentTableRow.tsx | 13 +++- 7 files changed, 123 insertions(+), 24 deletions(-) rename src/components/{SelectUsersInput/SelectUsersInput.tsx => SelectStudentsInput/SelectStudentsInput.tsx} (93%) create mode 100644 src/components/SelectStudentsInput/index.ts delete mode 100644 src/components/SelectUsersInput/index.ts diff --git a/src/components/Modals/AddStudentsModal/AddSudentsModal.tsx b/src/components/Modals/AddStudentsModal/AddSudentsModal.tsx index bff81d3..f7e56ed 100644 --- a/src/components/Modals/AddStudentsModal/AddSudentsModal.tsx +++ b/src/components/Modals/AddStudentsModal/AddSudentsModal.tsx @@ -3,19 +3,27 @@ import React, { useState } from 'react'; import BaseModal from '@educt/components/Modal'; import { MdGroup } from 'react-icons/md'; import { UserRoleEnum } from '@educt/enums'; -import SelectUsersInput from '@educt/components/SelectUsersInput'; -import { IUser } from '@educt/interfaces'; +import SelectStudentsInput from '@educt/components/SelectStudentsInput'; +import { ICourse, IUser } from '@educt/interfaces'; import { CourseServiceInstance } from '@educt/services'; import { useToast } from '@chakra-ui/react'; import useIsMountedRef from '@educt/hooks/useIsMountedRef'; type AddStudentsModalPropsType = { + course: Omit; + currentStudents: ICourse['students']; isOpen: boolean; onClose: () => void; - courseId: string; + onAdded: (users: Array) => void; }; -const AddStudentsModal: React.FC = ({ isOpen, onClose, courseId }) => { +const AddStudentsModal: React.FC = ({ + isOpen, + onClose, + onAdded, + currentStudents, + course, +}) => { const [isLoading, setIsLoading] = useState(false); const isMountedRef = useIsMountedRef(); const [students, setStudents] = useState>([]); @@ -27,9 +35,10 @@ const AddStudentsModal: React.FC = ({ isOpen, onClose try { setIsLoading(true); const ids = students.map(s => s.id); - await CourseServiceInstance.attachStudentsList(courseId, ids); + await CourseServiceInstance.attachStudentsList(course.id, ids); - toast({ title: 'Students successfully added.', status: 'success' }); + toast({ title: 'Students added.', status: 'info' }); + onAdded(students); onClose(); } catch (error) { console.error(error); @@ -51,7 +60,8 @@ const AddStudentsModal: React.FC = ({ isOpen, onClose isLoading={isLoading} loadingText='Saving...' > - handleSelectStudents(selected)} placeholder='Search for students...' searchParams={{ role: UserRoleEnum.STUDENT }} diff --git a/src/components/SelectUsersInput/SelectUsersInput.tsx b/src/components/SelectStudentsInput/SelectStudentsInput.tsx similarity index 93% rename from src/components/SelectUsersInput/SelectUsersInput.tsx rename to src/components/SelectStudentsInput/SelectStudentsInput.tsx index 17ca8fd..74d8904 100644 --- a/src/components/SelectUsersInput/SelectUsersInput.tsx +++ b/src/components/SelectStudentsInput/SelectStudentsInput.tsx @@ -16,7 +16,7 @@ import { } from '@chakra-ui/react'; import { AddIcon, SearchIcon } from '@chakra-ui/icons'; import { UserServiceInstance } from '@educt/services'; -import { IUser } from '@educt/interfaces'; +import { ICourse, IUser } from '@educt/interfaces'; import { useErrorHandler } from 'react-error-boundary'; import { BeatLoader } from 'react-spinners'; import { Virtuoso } from 'react-virtuoso'; @@ -26,14 +26,22 @@ type SelectUsersInputPropsType = { placeholder?: string | undefined; searchParams?: FetchUsersParamsType | undefined; onSelect: (selected: Array) => void; + currentStudents: ICourse['students']; }; -const SelectUsersInput: React.FC = ({ placeholder, searchParams: params, onSelect }) => { +const SelectUsersInput: React.FC = ({ + placeholder, + searchParams: params, + onSelect, + currentStudents, +}) => { const [isLoading, setIsLoading] = useState(false); const [search, setSearch] = useState(''); const [selected, setSelected] = useState>([]); const [users, setUsers] = useState>([]); - const filtered = users.filter(u => selected.every(s => s.id !== u.id)); + const filtered = users + .filter(u => selected.every(s => s.id !== u.id)) + .filter(u => currentStudents.every(s => s.id !== u.id)); const handleError = useErrorHandler(); const isEmpty = filtered.length === 0; diff --git a/src/components/SelectStudentsInput/index.ts b/src/components/SelectStudentsInput/index.ts new file mode 100644 index 0000000..7c1da77 --- /dev/null +++ b/src/components/SelectStudentsInput/index.ts @@ -0,0 +1,3 @@ +import SelectStudentsInput from './SelectStudentsInput'; + +export default SelectStudentsInput; diff --git a/src/components/SelectUsersInput/index.ts b/src/components/SelectUsersInput/index.ts deleted file mode 100644 index 26a35e0..0000000 --- a/src/components/SelectUsersInput/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import SelectUsersInput from './SelectUsersInput'; - -export default SelectUsersInput; diff --git a/src/pages/courses/edit/components/StudentTableList/Cells/index.tsx b/src/pages/courses/edit/components/StudentTableList/Cells/index.tsx index fbc067d..1e520ea 100644 --- a/src/pages/courses/edit/components/StudentTableList/Cells/index.tsx +++ b/src/pages/courses/edit/components/StudentTableList/Cells/index.tsx @@ -51,15 +51,23 @@ export const RegisteredCell: React.FC = memo(({ registe {new Date(registered).toLocaleDateString()} )); -type ActionsCellPropsType = Pick; -export const ActionsCell: React.FC = memo(({ id }) => ( +type ActionsCellPropsType = { + onEdit: () => void; + onRemove: () => void; + onDelete: () => void; +}; +export const ActionsCell: React.FC = memo(({ onEdit, onRemove, onDelete }) => ( } variant='ghost' /> - }>Edit - }>Remove from course - } color='red.400'> + }> + Edit + + }> + Remove from course + + } color='red.400'> Delete diff --git a/src/pages/courses/edit/components/StudentTableList/StudentTableList.tsx b/src/pages/courses/edit/components/StudentTableList/StudentTableList.tsx index 3a2a6f4..5ca7191 100644 --- a/src/pages/courses/edit/components/StudentTableList/StudentTableList.tsx +++ b/src/pages/courses/edit/components/StudentTableList/StudentTableList.tsx @@ -1,8 +1,8 @@ import React from 'react'; -import { Table, Tbody, Button, Input, InputGroup, InputLeftElement, Flex, Box, Text } from '@chakra-ui/react'; +import { Table, Tbody, Button, Input, InputGroup, InputLeftElement, Flex, Box, Text, useToast } from '@chakra-ui/react'; import { MdSearch } from 'react-icons/md'; - import AddStudentsModal from '@educt/components/Modals/AddStudentsModal'; +import { BeatLoader } from 'react-spinners'; import { AddButton } from '@educt/components/Buttons'; import BulkActionsMenu from './BulkActionsMenu'; import StudentTableHead from './StudentTableHead'; @@ -18,6 +18,7 @@ import type { ICourse } from '@educt/interfaces'; */ import { useCallback, useState } from 'react'; import { useDisclosure } from '@chakra-ui/react'; +import { UserServiceInstance } from '@educt/services'; type StudentTableListPropsType = { render: React.FC; @@ -28,6 +29,7 @@ const StudentTableList: React.FC = props => { const { render: Row, course } = props; const { students } = course; + const [isLoading, setIsLoading] = useState(false); const [rows, setRows] = useState(students); const [selected, setSelected] = useState([]); const [search, setSearch] = useState(''); @@ -36,6 +38,7 @@ const StudentTableList: React.FC = props => { onOpen: onOpenAddStudentModal, onClose: onCloseAddStudentModal, } = useDisclosure(); + const toast = useToast(); /** * Search for a student by input @@ -73,10 +76,52 @@ const StudentTableList: React.FC = props => { setSelected(prev => prev.filter(s => s.id !== student.id)); }, []); + /** + * Delete student handler + */ + const handleDelete = (id: string) => { + return async () => { + try { + setIsLoading(true); + const { data } = await UserServiceInstance.delete(id); + toast({ title: 'Student deleted', status: 'info' }); + setRows(prev => prev.filter(s => s.id !== data.id)); + } catch (error) { + console.error(error); + } finally { + setIsLoading(false); + } + }; + }; + + /** + * Edit student handler + */ + const handleEdit = (id: string) => { + return () => { + console.log(`editing id: ${id}`); + }; + }; + + /** + * Remove from course student handler + */ + const handleRemove = (id: string) => { + return () => { + console.log(`removing id: ${id}`); + }; + }; + if (rows.length === 0) { return ( <> - + setRows(prev => [...prev, ...users])} + /> There are no students in the course yet - - - - - ) : ( - + + + + + + + + Total: ({pagination.total}) + Actions + + {!isLoading ? ( + + {users.map(user => ( + handleEditUser(user)} + onDelete={user => handleDeleteUser({ id: user.id, fullname: user.fullname })} + /> + ))} + + ) : ( + + )} + + {users.length === 0 && ( + Cannot find any users. )} + + + + Page {pagination.current_page} of {pagesCount} + + + + + + + ); }; diff --git a/src/stores/PageStore.ts b/src/stores/PageStore.ts index da2c98f..1fb7a09 100644 --- a/src/stores/PageStore.ts +++ b/src/stores/PageStore.ts @@ -1,6 +1,5 @@ import { makeAutoObservable } from 'mobx'; import CoursesStore from './pages/CoursesStore'; -import EditCourseStore from './pages/EditCourseStore'; import RootStore from './RootStore'; export default class PageStore { @@ -8,8 +7,6 @@ export default class PageStore { public coursesStore: CoursesStore; - public editCourseStore: EditCourseStore; - constructor(root: RootStore) { this.root = root; @@ -17,7 +14,6 @@ export default class PageStore { * Pages */ this.coursesStore = new CoursesStore(this); - this.editCourseStore = new EditCourseStore(this); makeAutoObservable(this); } diff --git a/src/stores/pages/EditCourseStore.ts b/src/stores/pages/EditCourseStore.ts deleted file mode 100644 index 8aa2bde..0000000 --- a/src/stores/pages/EditCourseStore.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { makeAutoObservable, runInAction } from 'mobx'; -import * as helpers from '@educt/helpers'; -import { ICourse } from '@educt/interfaces'; -import PageStore from '../PageStore'; - -import { CourseServiceInstance, LessonServiceInstance } from '@educt/services'; - -export default class EditCourseStore { - public pageStore: PageStore; - - public course: Omit | null = null; - - constructor(pageStore: PageStore) { - this.pageStore = pageStore; - - makeAutoObservable(this); - } - - /** - * Load course by id into store - * - * @param id Course id - * @returns Course response - */ - public async loadCourseById(id: string) { - const result = await CourseServiceInstance.fetchById(id); - - runInAction(() => { - this.course = result.data; - }); - - return result; - } - - // TODO update stor on detaching, attaching, deleting students - - /** - * Remove lesson from store - * - * @param id Lesson id - * @returns Deleted lesson response - */ - public async deleteLessonById(id: string) { - const result = await LessonServiceInstance.deleteLesson(id); - - runInAction(() => { - if (this.course) { - this.course.lessons = this.course.lessons.filter(lesson => lesson.id !== result.data.id); - } - }); - - return result; - } - - /** - * 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); - } - } -} From 1dff6be5e44b87c1d2f9a81bcdfcd4f8f40cdc16 Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Thu, 16 Dec 2021 00:01:04 +0300 Subject: [PATCH 24/87] feat: editing students from table --- src/components/Modal/BaseModal.tsx | 19 +- .../Modals/EditUserModal/EditUserModal.tsx | 152 ++++++++++++++ .../EditUserModal/EditUserModal.validator.ts} | 0 src/components/Modals/EditUserModal/index.ts | 3 + src/interfaces/index.ts | 2 +- .../StudentTableList/StudentTableList.tsx | 29 ++- .../StudentTableList/StudentTableRow.tsx | 4 +- .../components/EditUserForm/EditUserForm.tsx | 187 ------------------ .../users/components/EditUserForm/index.ts | 3 - .../users/components/UserList/UserList.tsx | 4 +- 10 files changed, 202 insertions(+), 201 deletions(-) create mode 100644 src/components/Modals/EditUserModal/EditUserModal.tsx rename src/{pages/users/components/EditUserForm/EditUserForm.validator.ts => components/Modals/EditUserModal/EditUserModal.validator.ts} (100%) create mode 100644 src/components/Modals/EditUserModal/index.ts delete mode 100644 src/pages/users/components/EditUserForm/EditUserForm.tsx delete mode 100644 src/pages/users/components/EditUserForm/index.ts diff --git a/src/components/Modal/BaseModal.tsx b/src/components/Modal/BaseModal.tsx index e56764b..f9f1eb1 100644 --- a/src/components/Modal/BaseModal.tsx +++ b/src/components/Modal/BaseModal.tsx @@ -16,8 +16,9 @@ import { IconType } from 'react-icons'; export interface IBaseModalProps extends ModalProps { isOpen: boolean; + isDisabled: boolean; onClose: () => void; - onProceed: (e: React.MouseEvent) => void; + onProceed?: (e: React.MouseEvent) => void; heading: string; icon?: IconType; description?: string; @@ -27,8 +28,19 @@ export interface IBaseModalProps extends ModalProps { } const BaseModal: React.FC = props => { - const { heading, description, isOpen, onClose, onProceed, confirmText, icon, isLoading, loadingText, ...modalProps } = - props; + const { + heading, + description, + isOpen, + onClose, + onProceed, + confirmText, + icon, + isLoading, + isDisabled, + loadingText, + ...modalProps + } = props; return ( @@ -61,6 +73,7 @@ const BaseModal: React.FC = props => { - - - - - - ); -}; - -export default UpdateUserForm; diff --git a/src/pages/users/components/EditUserForm/index.ts b/src/pages/users/components/EditUserForm/index.ts deleted file mode 100644 index 4b02fdc..0000000 --- a/src/pages/users/components/EditUserForm/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import EditUserForm from './EditUserForm'; - -export default EditUserForm; diff --git a/src/pages/users/components/UserList/UserList.tsx b/src/pages/users/components/UserList/UserList.tsx index 6039f34..d525133 100644 --- a/src/pages/users/components/UserList/UserList.tsx +++ b/src/pages/users/components/UserList/UserList.tsx @@ -8,7 +8,7 @@ import { Button } from '@chakra-ui/button'; * Components */ import LoadingList from '@educt/components/LoadingList'; -import EditUserForm from '../EditUserForm'; +import EditUserModal from '@educt/components/Modals/EditUserModal'; import DeleteUserDialog from '@educt/components/Dialogs/DeleteUserDialog'; /** @@ -74,7 +74,7 @@ const UserList: React.FC = ({ render: Item, limit }) => { return ( - {editing && } + {editing && } {deleting && } From d133b699828b12bf91288d5facc713e0bb707c24 Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Thu, 16 Dec 2021 20:52:14 +0300 Subject: [PATCH 25/87] fix: fixed search in students table --- src/components/Modal/BaseModal.tsx | 2 +- .../AddStudentsModal/AddSudentsModal.tsx | 1 + .../auth/components/AuthForm/AuthForm.tsx | 9 +- .../StudentTableList/StudentTableList.tsx | 100 +++++++++--------- src/services/CourseService.ts | 7 +- 5 files changed, 64 insertions(+), 55 deletions(-) diff --git a/src/components/Modal/BaseModal.tsx b/src/components/Modal/BaseModal.tsx index f9f1eb1..3afbf01 100644 --- a/src/components/Modal/BaseModal.tsx +++ b/src/components/Modal/BaseModal.tsx @@ -16,7 +16,7 @@ import { IconType } from 'react-icons'; export interface IBaseModalProps extends ModalProps { isOpen: boolean; - isDisabled: boolean; + isDisabled?: boolean | undefined; onClose: () => void; onProceed?: (e: React.MouseEvent) => void; heading: string; diff --git a/src/components/Modals/AddStudentsModal/AddSudentsModal.tsx b/src/components/Modals/AddStudentsModal/AddSudentsModal.tsx index 1a24ceb..cd2164e 100644 --- a/src/components/Modals/AddStudentsModal/AddSudentsModal.tsx +++ b/src/components/Modals/AddStudentsModal/AddSudentsModal.tsx @@ -34,6 +34,7 @@ const AddStudentsModal: React.FC = ({ const [selected, setSelected] = useState>([]); const handleSelectStudents = (students: Array) => setSelected(students); + const handleAttachStudents = async () => { if (selected.length !== 0) { try { diff --git a/src/pages/auth/components/AuthForm/AuthForm.tsx b/src/pages/auth/components/AuthForm/AuthForm.tsx index 412773f..74186b2 100644 --- a/src/pages/auth/components/AuthForm/AuthForm.tsx +++ b/src/pages/auth/components/AuthForm/AuthForm.tsx @@ -60,7 +60,13 @@ const AuthForm: React.FC = () => { - + {errors.login?.message} @@ -72,6 +78,7 @@ const AuthForm: React.FC = () => { placeholder='******' isInvalid={!!errors.password} errorMessage={errors.password?.message} + autoComplete='current-password' />