From d7f17b59d82f07e9f0f6946b4656c75717c34dda Mon Sep 17 00:00:00 2001 From: akmatoff Date: Wed, 21 Feb 2024 23:46:37 +0600 Subject: [PATCH 1/3] Task queries, interfaces and requests --- src/constants/apiConstants.ts | 2 +- src/constants/queryKeys.ts | 1 + src/constants/resource.ts | 12 +++--- src/interfaces/answer.ts | 11 ++++++ src/interfaces/task.ts | 22 +++++++++++ src/queries/sections.ts | 6 ++- src/queries/tasks.ts | 72 +++++++++++++++++++++++++++++++++++ src/requests/tasks.ts | 26 +++++++++++++ 8 files changed, 143 insertions(+), 9 deletions(-) create mode 100644 src/interfaces/answer.ts create mode 100644 src/interfaces/task.ts create mode 100644 src/queries/tasks.ts create mode 100644 src/requests/tasks.ts diff --git a/src/constants/apiConstants.ts b/src/constants/apiConstants.ts index 4abfd19..d6d9caf 100644 --- a/src/constants/apiConstants.ts +++ b/src/constants/apiConstants.ts @@ -9,4 +9,4 @@ export const API_SUBJECTS = BASE_URL.concat("/subjects/"); export const API_SECTIONS = BASE_URL.concat("/sections/"); export const API_TOPICS = BASE_URL.concat("/topics/"); export const API_LECTURES = BASE_URL.concat("/lectures/"); -export const API_QUIZZES = BASE_URL.concat("/quizzes/"); +export const API_TASKS = BASE_URL.concat("/tasks/"); diff --git a/src/constants/queryKeys.ts b/src/constants/queryKeys.ts index 7dce23a..9903ab9 100644 --- a/src/constants/queryKeys.ts +++ b/src/constants/queryKeys.ts @@ -7,4 +7,5 @@ export const QUERY_KEYS = { SECTIONS: "sections", REG_REQUESTS: "reg-requests", LECTURES: "lectures", + TASKS: "tasks", }; diff --git a/src/constants/resource.ts b/src/constants/resource.ts index c2430d9..c11624c 100644 --- a/src/constants/resource.ts +++ b/src/constants/resource.ts @@ -45,12 +45,12 @@ export const RESOURCES: IResource[] = [ href: "/lectures", roles: [Role.MANAGER], }, - // { - // id: "tasks", - // title: "Задания", - // href: "/tasks", - // roles: [Role.MANAGER], - // }, + { + id: "tasks", + title: "Задания", + href: "/tasks", + roles: [Role.MANAGER], + }, // { // id: "task-results", // title: "Результаты заданий", diff --git a/src/interfaces/answer.ts b/src/interfaces/answer.ts new file mode 100644 index 0000000..5110fb7 --- /dev/null +++ b/src/interfaces/answer.ts @@ -0,0 +1,11 @@ +import { ITask } from "./task"; + +export interface IAnswer { + id: number; + text: string; + isCorrectAnswer: boolean; + taskId: number; + task: ITask; + createdAt: string; + updatedAt: string; +} diff --git a/src/interfaces/task.ts b/src/interfaces/task.ts new file mode 100644 index 0000000..fcab324 --- /dev/null +++ b/src/interfaces/task.ts @@ -0,0 +1,22 @@ +import { IAnswer } from "./answer"; +import { ITopic } from "./topic"; + +export interface ITask { + id: number; + text: string; + image?: string; + tip?: string; + topicId: number; + topic: ITopic; + answers: IAnswer[]; + createdAt: string; + updatedAt: string; +} + +export interface ITaskCreate { + text: string; + image?: string; + tip?: string; + topicId: number; + topic: ITopic; +} diff --git a/src/queries/sections.ts b/src/queries/sections.ts index 00a060c..3a74de6 100644 --- a/src/queries/sections.ts +++ b/src/queries/sections.ts @@ -1,10 +1,11 @@ import { QUERY_KEYS } from "@/constants/queryKeys"; -import { ISection } from "@/interfaces/section"; +import { ISection, ISectionCreate } from "@/interfaces/section"; import { createSection, getSectionDetails, getSections, removeSection, + updateSection, } from "@/requests/sections"; import { useMutation, useQuery } from "@tanstack/react-query"; @@ -52,7 +53,8 @@ interface MutationQuery { export const useSectionMutation = ({ onSuccess, onError }: MutationQuery) => { const { data, mutate, isPending } = useMutation({ - mutationFn: createSection, + mutationFn: (data: ISectionCreate, id?: number) => + id ? updateSection(id, data) : createSection(data), onSuccess, onError, }); diff --git a/src/queries/tasks.ts b/src/queries/tasks.ts new file mode 100644 index 0000000..6cd6c2e --- /dev/null +++ b/src/queries/tasks.ts @@ -0,0 +1,72 @@ +import { QUERY_KEYS } from "@/constants/queryKeys"; +import { ITask, ITaskCreate } from "@/interfaces/task"; +import { + createTask, + getTaskDetails, + getTasks, + removeTask, + updateTask, +} from "@/requests/tasks"; +import { useMutation, useQuery } from "@tanstack/react-query"; + +export const useTasksQuery = () => { + const { data, isPending } = useQuery({ + queryFn: getTasks, + queryKey: [QUERY_KEYS.TASKS], + }); + + return { + data, + isPending, + }; +}; + +interface QueryParams { + enabled?: boolean; +} + +export const useTaskDetails = (id: number, { enabled }: QueryParams) => { + const { data, isPending } = useQuery({ + queryFn: () => getTaskDetails(id), + queryKey: [QUERY_KEYS.TASKS, id], + enabled, + }); + + return { + data, + isPending, + }; +}; + +interface MutationQuery { + onSuccess?: (data: ITask) => void; + onError?: () => void; +} + +export const useTaskMutation = (params?: MutationQuery) => { + const { data, mutate, isPending } = useMutation({ + mutationFn: (data: ITaskCreate, id?: number) => + id ? updateTask(id, data) : createTask(data), + onSuccess: params?.onSuccess, + onError: params?.onError, + }); + + return { + data, + mutate, + isPending, + }; +}; + +export const useTaskDeletion = (params?: MutationQuery) => { + const { mutate, isPending } = useMutation({ + mutationFn: (id: number) => removeTask(id), + onSuccess: params?.onSuccess, + onError: params?.onError, + }); + + return { + mutate, + isPending, + }; +}; diff --git a/src/requests/tasks.ts b/src/requests/tasks.ts new file mode 100644 index 0000000..b34ce0c --- /dev/null +++ b/src/requests/tasks.ts @@ -0,0 +1,26 @@ +import { API_TASKS } from "@/constants/apiConstants"; +import { ITask, ITaskCreate } from "@/interfaces/task"; +import axios from "axios"; + +export async function getTasks(): Promise { + return axios.get(API_TASKS).then(({ data }) => data); +} + +export async function getTaskDetails(id: number): Promise { + return axios.get(`${API_TASKS}${id}`).then(({ data }) => data); +} + +export async function createTask(data: ITaskCreate): Promise { + return axios.post(API_TASKS, data).then(({ data }) => data); +} + +export async function updateTask( + id: number, + data: Partial +): Promise { + return axios.patch(`${API_TASKS}${id}`, data).then(({ data }) => data); +} + +export async function removeTask(id: number) { + return axios.delete(`${API_TASKS}${id}`).then(({ data }) => data); +} From b4ebd4237005ab57f38f544539f5a871732d92dd Mon Sep 17 00:00:00 2001 From: akmatoff Date: Thu, 22 Feb 2024 00:15:02 +0600 Subject: [PATCH 2/3] Tasks list and details CRUD --- src/constants/resource.ts | 2 +- src/constants/routes.ts | 1 + src/interfaces/task.ts | 1 - src/pages/tasks/TaskDetails.tsx | 180 ++++++++++++++++++++++++++++++++ src/pages/tasks/TasksPage.tsx | 37 +++++++ src/queries/tasks.ts | 4 +- src/routes/RootRouter.tsx | 4 + 7 files changed, 225 insertions(+), 4 deletions(-) create mode 100644 src/pages/tasks/TaskDetails.tsx create mode 100644 src/pages/tasks/TasksPage.tsx diff --git a/src/constants/resource.ts b/src/constants/resource.ts index c11624c..3e59c6a 100644 --- a/src/constants/resource.ts +++ b/src/constants/resource.ts @@ -47,7 +47,7 @@ export const RESOURCES: IResource[] = [ }, { id: "tasks", - title: "Задания", + title: "Задачи", href: "/tasks", roles: [Role.MANAGER], }, diff --git a/src/constants/routes.ts b/src/constants/routes.ts index a444476..bdce582 100644 --- a/src/constants/routes.ts +++ b/src/constants/routes.ts @@ -16,5 +16,6 @@ export const ROUTES = { TOPICS: "/office/topics", TOPIC: "/office/topic", TASKS: "/office/tasks", + TASK: "/office/task", TASK_RESULTS: "/office/task-results", }; diff --git a/src/interfaces/task.ts b/src/interfaces/task.ts index fcab324..83dc915 100644 --- a/src/interfaces/task.ts +++ b/src/interfaces/task.ts @@ -18,5 +18,4 @@ export interface ITaskCreate { image?: string; tip?: string; topicId: number; - topic: ITopic; } diff --git a/src/pages/tasks/TaskDetails.tsx b/src/pages/tasks/TaskDetails.tsx new file mode 100644 index 0000000..e9d404a --- /dev/null +++ b/src/pages/tasks/TaskDetails.tsx @@ -0,0 +1,180 @@ +import CustomInput from "@/components/shared/CustomInput/CustomInput"; +import RelationInput from "@/components/shared/RelationInput/RelationInput"; +import Resource from "@/components/shared/Resource/Resource"; +import TextEditor from "@/components/shared/TextEditor/TextEditor"; +import { QUERY_KEYS } from "@/constants/queryKeys"; +import { ROUTES } from "@/constants/routes"; +import { useNotification } from "@/hooks/useNotification"; +import { IOption } from "@/interfaces/common"; +import { ITaskCreate } from "@/interfaces/task"; +import { + useTaskDeletion, + useTaskDetails, + useTaskMutation, +} from "@/queries/tasks"; +import { useTopicsQuery } from "@/queries/topics"; +import { useQueryClient } from "@tanstack/react-query"; +import { useEffect, useMemo, useState } from "react"; +import { Controller, SubmitHandler, useForm } from "react-hook-form"; +import { useNavigate, useParams } from "react-router-dom"; + +const initialValues: ITaskCreate = { + text: "", + tip: "", + topicId: -1, +}; + +function TaskDetails() { + const queryClient = useQueryClient(); + + const { id } = useParams(); + + const navigate = useNavigate(); + + const [search, setSearch] = useState(""); + + const { showSuccessNotification, showErrorNotification } = useNotification(); + + const { data: existingTask, isLoading: isTaskLoading } = useTaskDetails( + +id!, + { enabled: !!id } + ); + + const { + data: topics, + isLoading: isTopicsLoading, + refetch, + } = useTopicsQuery({ params: { search } }); + + const [activeValue, setActiveValue] = useState({ + label: topics?.[0].title, + value: topics?.[0].id.toString(), + }); + + const topicOptions = useMemo( + () => + topics?.map((topic) => ({ + label: topic.title, + value: topic.id.toString(), + })) || [], + [topics] + ); + + const { mutate, isPending } = useTaskMutation({ + onSuccess: () => { + queryClient.invalidateQueries({ + refetchType: "all", + queryKey: [QUERY_KEYS.TASKS], + }); + + navigate(ROUTES.TASKS); + + showSuccessNotification(); + }, + onError: () => { + showErrorNotification(); + }, + }); + + const { mutate: deleteTask, isPending: isDeleting } = useTaskDeletion({ + onSuccess: () => { + queryClient.invalidateQueries({ + refetchType: "all", + queryKey: [QUERY_KEYS.TASKS], + }); + + navigate(ROUTES.TASKS); + + showSuccessNotification(); + }, + onError: () => { + showErrorNotification(); + }, + }); + + const taskForm = useForm({ + defaultValues: initialValues, + mode: "onBlur", + }); + + const isValid = Object.values(taskForm.formState.errors).length === 0; + + const onSubmit: SubmitHandler = (data: ITaskCreate) => { + mutate(data); + }; + + useEffect(() => { + if (existingTask && id) { + taskForm.reset(existingTask); + setActiveValue({ + label: existingTask.topic.title, + value: existingTask.topic.id.toString(), + }); + } + }, [existingTask, taskForm, id]); + + useEffect(() => { + if (!existingTask || !id) { + setActiveValue({ + label: topics?.[0].title, + value: topics?.[0].id.toString(), + }); + } + + if (topics) { + taskForm.setValue("topicId", topics[0].id); + } + }, [topics, taskForm, existingTask, id]); + + return ( + deleteTask(+id!)} + isDeleting={isDeleting} + onSaveClick={() => onSubmit(taskForm.getValues())} + > + ( + + )} + /> + + taskForm.setValue("tip", value)} + /> + + { + taskForm.setValue("topicId", +value.value); + setActiveValue(value); + }} + label="Топик" + placeholder="Выберите топик..." + isLoading={isTopicsLoading} + onSearch={(value) => { + setSearch(value); + refetch(); + }} + /> + + ); +} + +export default TaskDetails; diff --git a/src/pages/tasks/TasksPage.tsx b/src/pages/tasks/TasksPage.tsx new file mode 100644 index 0000000..c874403 --- /dev/null +++ b/src/pages/tasks/TasksPage.tsx @@ -0,0 +1,37 @@ +import ResourceList from "@/components/shared/ResourceList/ResourceList"; +import Table, { TableColumn, TableRow } from "@/components/shared/Table/Table"; +import { ROUTES } from "@/constants/routes"; +import { useTasksQuery } from "@/queries/tasks"; +import { useNavigate } from "react-router-dom"; + +function TasksPage() { + const { data: tasks, isPending } = useTasksQuery(); + + const navigate = useNavigate(); + + return ( + navigate(ROUTES.TASK)} + itemsLength={tasks?.length} + > + + {tasks?.map((task) => ( + navigate(`${ROUTES.TASK}/${task.id}`)} + > + {task.id} + {task.text} + {task.topic?.title} + + ))} +
+
+ ); +} + +export default TasksPage; diff --git a/src/queries/tasks.ts b/src/queries/tasks.ts index 6cd6c2e..5130ca5 100644 --- a/src/queries/tasks.ts +++ b/src/queries/tasks.ts @@ -26,7 +26,7 @@ interface QueryParams { } export const useTaskDetails = (id: number, { enabled }: QueryParams) => { - const { data, isPending } = useQuery({ + const { data, isLoading } = useQuery({ queryFn: () => getTaskDetails(id), queryKey: [QUERY_KEYS.TASKS, id], enabled, @@ -34,7 +34,7 @@ export const useTaskDetails = (id: number, { enabled }: QueryParams) => { return { data, - isPending, + isLoading, }; }; diff --git a/src/routes/RootRouter.tsx b/src/routes/RootRouter.tsx index b23c12d..b543872 100644 --- a/src/routes/RootRouter.tsx +++ b/src/routes/RootRouter.tsx @@ -10,6 +10,8 @@ import SectionDetails from "@/pages/sections/SectionDetails"; import SectionsPage from "@/pages/sections/SectionsPage"; import SubjectDetails from "@/pages/subjects/SubjectDetails"; import SubjectsPage from "@/pages/subjects/SubjectsPage"; +import TaskDetails from "@/pages/tasks/TaskDetails"; +import TasksPage from "@/pages/tasks/TasksPage"; import TopicDetails from "@/pages/topics/TopicDetails"; import TopicsPage from "@/pages/topics/TopicsPage"; import UserDetails from "@/pages/users/UserDetails"; @@ -46,6 +48,8 @@ const router = createBrowserRouter( } /> } /> } /> + } /> + } /> ) From e93d1a2465eac4b4fb98030cb6dc8e49ccc35f23 Mon Sep 17 00:00:00 2001 From: akmatoff Date: Thu, 22 Feb 2024 00:16:59 +0600 Subject: [PATCH 3/3] Add existing task page to router --- src/routes/RootRouter.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/routes/RootRouter.tsx b/src/routes/RootRouter.tsx index b543872..158d3fc 100644 --- a/src/routes/RootRouter.tsx +++ b/src/routes/RootRouter.tsx @@ -50,6 +50,7 @@ const router = createBrowserRouter( } /> } /> } /> + } /> )