Skip to content

Commit

Permalink
Merge pull request #1 from codiumkg/feature/tasks
Browse files Browse the repository at this point in the history
[FEATURE] Tasks CRUD
  • Loading branch information
akmatoff authored Feb 21, 2024
2 parents b4e731c + e93d1a2 commit 3b2e34a
Show file tree
Hide file tree
Showing 12 changed files with 365 additions and 9 deletions.
2 changes: 1 addition & 1 deletion src/constants/apiConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/");
1 change: 1 addition & 0 deletions src/constants/queryKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export const QUERY_KEYS = {
SECTIONS: "sections",
REG_REQUESTS: "reg-requests",
LECTURES: "lectures",
TASKS: "tasks",
};
12 changes: 6 additions & 6 deletions src/constants/resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: "Результаты заданий",
Expand Down
1 change: 1 addition & 0 deletions src/constants/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ export const ROUTES = {
TOPICS: "/office/topics",
TOPIC: "/office/topic",
TASKS: "/office/tasks",
TASK: "/office/task",
TASK_RESULTS: "/office/task-results",
};
11 changes: 11 additions & 0 deletions src/interfaces/answer.ts
Original file line number Diff line number Diff line change
@@ -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;
}
21 changes: 21 additions & 0 deletions src/interfaces/task.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
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;
}
180 changes: 180 additions & 0 deletions src/pages/tasks/TaskDetails.tsx
Original file line number Diff line number Diff line change
@@ -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<IOption>({
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<ITaskCreate>({
defaultValues: initialValues,
mode: "onBlur",
});

const isValid = Object.values(taskForm.formState.errors).length === 0;

const onSubmit: SubmitHandler<ITaskCreate> = (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 (
<Resource
title="Задача"
isExisting={!!id}
isLoading={isTaskLoading}
isSaveDisabled={!isValid || !taskForm.formState.isDirty}
isSaveButtonLoading={isPending}
onDeleteClick={() => deleteTask(+id!)}
isDeleting={isDeleting}
onSaveClick={() => onSubmit(taskForm.getValues())}
>
<Controller
name="text"
control={taskForm.control}
render={({ field }) => (
<TextEditor
label="Содержимое"
{...field}
placeholder="Введите содержимое задачи..."
/>
)}
/>

<CustomInput
{...taskForm.register("tip")}
label="Подсказка"
placeholder="Введите подсказку..."
errorMessage={taskForm.formState.errors.tip?.message}
onChangeCallback={(value) => taskForm.setValue("tip", value)}
/>

<RelationInput
name="topic"
options={topicOptions}
activeValue={activeValue}
setActiveValue={(value) => {
taskForm.setValue("topicId", +value.value);
setActiveValue(value);
}}
label="Топик"
placeholder="Выберите топик..."
isLoading={isTopicsLoading}
onSearch={(value) => {
setSearch(value);
refetch();
}}
/>
</Resource>
);
}

export default TaskDetails;
37 changes: 37 additions & 0 deletions src/pages/tasks/TasksPage.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<ResourceList
title="Задачи"
isLoading={isPending}
onCreateClick={() => navigate(ROUTES.TASK)}
itemsLength={tasks?.length}
>
<Table
headers={[{ title: "ID" }, { title: "Содержимое" }, { title: "Топик" }]}
>
{tasks?.map((task) => (
<TableRow
key={task.id}
onClick={() => navigate(`${ROUTES.TASK}/${task.id}`)}
>
<TableColumn>{task.id}</TableColumn>
<TableColumn>{task.text}</TableColumn>
<TableColumn>{task.topic?.title}</TableColumn>
</TableRow>
))}
</Table>
</ResourceList>
);
}

export default TasksPage;
6 changes: 4 additions & 2 deletions src/queries/sections.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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,
});
Expand Down
72 changes: 72 additions & 0 deletions src/queries/tasks.ts
Original file line number Diff line number Diff line change
@@ -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, isLoading } = useQuery({
queryFn: () => getTaskDetails(id),
queryKey: [QUERY_KEYS.TASKS, id],
enabled,
});

return {
data,
isLoading,
};
};

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,
};
};
Loading

0 comments on commit 3b2e34a

Please sign in to comment.