add api request & new stores
This commit is contained in:
parent
c9f9f3a4b0
commit
31c13b990d
1
.yarnrc
Normal file
1
.yarnrc
Normal file
@ -0,0 +1 @@
|
||||
"@frontend:registry" "https://penahub.gitlab.yandexcloud.net/api/v4/packages/npm/"
|
@ -6,6 +6,7 @@
|
||||
"@craco/craco": "^7.0.0",
|
||||
"@emotion/react": "^11.10.5",
|
||||
"@emotion/styled": "^11.10.5",
|
||||
"@frontend/kitui": "^1.0.54",
|
||||
"@mui/icons-material": "^5.10.14",
|
||||
"@mui/material": "^5.10.14",
|
||||
"@mui/x-date-pickers": "^6.16.1",
|
||||
|
100
src/api/question.ts
Normal file
100
src/api/question.ts
Normal file
@ -0,0 +1,100 @@
|
||||
import { makeRequest } from "@frontend/kitui";
|
||||
import { CreateQuestionRequest } from "model/question/create";
|
||||
import { Question } from "model/question/question";
|
||||
import { GetQuestionListRequest, GetQuestionListResponse } from "model/question/getList";
|
||||
import { EditQuestionRequest, EditQuestionResponse } from "model/question/edit";
|
||||
import { DeleteQuestionRequest, DeleteQuestionResponse } from "model/question/delete";
|
||||
import { CopyQuestionRequest, CopyQuestionResponse } from "model/question/copy";
|
||||
|
||||
|
||||
const baseUrl = process.env.NODE_ENV === "production" ? "/squiz" : "https://squiz.pena.digital/squiz";
|
||||
|
||||
export function createQuestion(body: CreateQuestionRequest = defaultCreateQuestionBody) {
|
||||
return makeRequest<CreateQuestionRequest, Question>({
|
||||
url: `${baseUrl}/question/create`,
|
||||
body,
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
|
||||
export function getQuestionList(body: GetQuestionListRequest = defaultGetQuestionListBody) {
|
||||
return makeRequest<GetQuestionListRequest, GetQuestionListResponse>({
|
||||
url: `${baseUrl}/question/getList`,
|
||||
body,
|
||||
method: "GET",
|
||||
});
|
||||
}
|
||||
|
||||
export function editQuestion(updatedQuestion: Question, signal?: AbortSignal) {
|
||||
const body: EditQuestionRequest = {
|
||||
id: updatedQuestion.id,
|
||||
title: updatedQuestion.title,
|
||||
desc: updatedQuestion.description,
|
||||
type: updatedQuestion.type,
|
||||
required: updatedQuestion.required,
|
||||
page: updatedQuestion.page,
|
||||
};
|
||||
|
||||
return makeRequest<EditQuestionRequest, EditQuestionResponse>({
|
||||
url: `${baseUrl}/question/edit`,
|
||||
body,
|
||||
method: "PATCH",
|
||||
signal,
|
||||
});
|
||||
}
|
||||
|
||||
export function copyQuestion(copyQuestionBody: CopyQuestionRequest) {
|
||||
return makeRequest<CopyQuestionRequest, CopyQuestionResponse>({
|
||||
url: `${baseUrl}/question/copy`,
|
||||
body: copyQuestionBody,
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteQuestion(id: number) {
|
||||
return makeRequest<DeleteQuestionRequest, DeleteQuestionResponse>({
|
||||
url: `${baseUrl}/question/delete`,
|
||||
body: { id },
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
|
||||
export const questionApi = {
|
||||
create: createQuestion,
|
||||
getList: getQuestionList,
|
||||
edit: editQuestion,
|
||||
copy: copyQuestion,
|
||||
delete: deleteQuestion,
|
||||
};
|
||||
|
||||
|
||||
const defaultCreateQuestionBody: CreateQuestionRequest = {
|
||||
"quiz_id": 0,
|
||||
"title": "string",
|
||||
"description": "string",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"page": 0,
|
||||
"content": "string",
|
||||
};
|
||||
|
||||
const defaultGetQuestionListBody: GetQuestionListRequest = {
|
||||
"limit": 0,
|
||||
"offset": 0,
|
||||
"from": 0,
|
||||
"to": 0,
|
||||
"search": "string",
|
||||
"type": "string",
|
||||
"deleted": true,
|
||||
"required": true,
|
||||
"quiz_id": 0
|
||||
};
|
||||
|
||||
const defaultEditQuestionBody: EditQuestionRequest = {
|
||||
"id": 0,
|
||||
"title": "string",
|
||||
"desc": "string",
|
||||
"type": "",
|
||||
"required": true,
|
||||
"page": 0
|
||||
};
|
142
src/api/quiz.ts
Normal file
142
src/api/quiz.ts
Normal file
@ -0,0 +1,142 @@
|
||||
import { makeRequest } from "@frontend/kitui";
|
||||
import { CopyQuizRequest, CopyQuizResponse } from "model/quiz/copy";
|
||||
import { CreateQuizRequest } from "model/quiz/create";
|
||||
import { DeleteQuizRequest, DeleteQuizResponse } from "model/quiz/delete";
|
||||
import { EditQuizRequest, EditQuizResponse } from "model/quiz/edit";
|
||||
import { GetQuizRequest, GetQuizResponse } from "model/quiz/get";
|
||||
import { GetQuizListRequest, GetQuizListResponse } from "model/quiz/getList";
|
||||
import { BackendQuiz } from "model/quiz/quiz";
|
||||
|
||||
|
||||
const baseUrl = process.env.NODE_ENV === "production" ? "/squiz" : "https://squiz.pena.digital/squiz";
|
||||
|
||||
function createQuiz(body: CreateQuizRequest = defaultCreateQuizBody) {
|
||||
return makeRequest<CreateQuizRequest, BackendQuiz>({
|
||||
url: `${baseUrl}/quiz/create`,
|
||||
body,
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
|
||||
function getQuizList(body: GetQuizListRequest = defaultGetQuizListBody) {
|
||||
return makeRequest<GetQuizListRequest, GetQuizListResponse>({
|
||||
url: `${baseUrl}/quiz/getList`,
|
||||
body,
|
||||
method: "GET",
|
||||
});
|
||||
}
|
||||
|
||||
function getQuiz(body: GetQuizRequest = defaultGetQuizBody) {
|
||||
return makeRequest<GetQuizRequest, GetQuizResponse>({
|
||||
url: `${baseUrl}/quiz/get`,
|
||||
body,
|
||||
method: "GET",
|
||||
});
|
||||
}
|
||||
|
||||
function editQuiz(body: EditQuizRequest = defaultEditQuizBody) {
|
||||
return makeRequest<EditQuizRequest, EditQuizResponse>({
|
||||
url: `${baseUrl}/quiz/edit`,
|
||||
body,
|
||||
method: "PATCH",
|
||||
});
|
||||
}
|
||||
|
||||
function copyQuiz(id: number) {
|
||||
return makeRequest<CopyQuizRequest, CopyQuizResponse>({
|
||||
url: `${baseUrl}/quiz/copy`,
|
||||
body: { id },
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
|
||||
function deleteQuiz(id: number) {
|
||||
return makeRequest<DeleteQuizRequest, DeleteQuizResponse>({
|
||||
url: `${baseUrl}/quiz/delete`,
|
||||
body: { id },
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
|
||||
function addQuizImages(quizId: number, image: Blob) {
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append("quiz", quizId.toString());
|
||||
formData.append("image", image);
|
||||
|
||||
return makeRequest<FormData, never>({
|
||||
url: `${baseUrl}/quiz/putImages`,
|
||||
body: formData,
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
|
||||
export const quizApi = {
|
||||
create: createQuiz,
|
||||
getList: getQuizList,
|
||||
get: getQuiz,
|
||||
edit: editQuiz,
|
||||
copy: copyQuiz,
|
||||
delete: deleteQuiz,
|
||||
addImages: addQuizImages,
|
||||
};
|
||||
|
||||
|
||||
const defaultCreateQuizBody: CreateQuizRequest = {
|
||||
"fingerprinting": true,
|
||||
"repeatable": true,
|
||||
"note_prevented": true,
|
||||
"mail_notifications": true,
|
||||
"unique_answers": true,
|
||||
"name": "string",
|
||||
"description": "string",
|
||||
"config": "string",
|
||||
"status": "string",
|
||||
"limit": 0,
|
||||
"due_to": 0,
|
||||
"time_of_passing": 0,
|
||||
"pausable": true,
|
||||
"question_cnt": 0,
|
||||
"super": true,
|
||||
"group_id": 0,
|
||||
};
|
||||
|
||||
const defaultEditQuizBody: EditQuizRequest = {
|
||||
"id": 0,
|
||||
"fp": true,
|
||||
"rep": true,
|
||||
"note_prevented": true,
|
||||
"mailing": true,
|
||||
"uniq": true,
|
||||
"name": "string",
|
||||
"desc": "string",
|
||||
"conf": "string",
|
||||
"status": "string",
|
||||
"limit": 0,
|
||||
"due_to": 0,
|
||||
"time_of_passing": 0,
|
||||
"pausable": true,
|
||||
"question_cnt": 0,
|
||||
"super": true,
|
||||
"group_id": 0,
|
||||
};
|
||||
|
||||
const defaultGetQuizBody: GetQuizRequest = {
|
||||
"quiz_id": "string",
|
||||
"limit": 0,
|
||||
"page": 0,
|
||||
"need_config": true,
|
||||
};
|
||||
|
||||
const defaultGetQuizListBody: GetQuizListRequest = {
|
||||
"limit": 0,
|
||||
"offset": 0,
|
||||
"from": 0,
|
||||
"to": 0,
|
||||
"search": "string",
|
||||
"status": "string",
|
||||
"deleted": true,
|
||||
"archived": true,
|
||||
"super": true,
|
||||
"group_id": 0,
|
||||
};
|
8
src/model/question/copy.ts
Normal file
8
src/model/question/copy.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export interface CopyQuestionRequest {
|
||||
id: number;
|
||||
quiz_id: number;
|
||||
}
|
||||
|
||||
export interface CopyQuestionResponse {
|
||||
updated: number;
|
||||
}
|
9
src/model/question/create.ts
Normal file
9
src/model/question/create.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export interface CreateQuestionRequest {
|
||||
quiz_id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
type: string;
|
||||
required: boolean;
|
||||
page: number;
|
||||
content: string;
|
||||
}
|
7
src/model/question/delete.ts
Normal file
7
src/model/question/delete.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export interface DeleteQuestionRequest {
|
||||
id: number;
|
||||
}
|
||||
|
||||
export interface DeleteQuestionResponse {
|
||||
deactivated: number;
|
||||
}
|
12
src/model/question/edit.ts
Normal file
12
src/model/question/edit.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export interface EditQuestionRequest {
|
||||
id: number;
|
||||
title: string;
|
||||
desc: string;
|
||||
type: "test" | "button" | "file" | "checkbox" | "select" | "none" | "";
|
||||
required: boolean;
|
||||
page: number;
|
||||
}
|
||||
|
||||
export interface EditQuestionResponse {
|
||||
updated: number;
|
||||
}
|
16
src/model/question/getList.ts
Normal file
16
src/model/question/getList.ts
Normal file
@ -0,0 +1,16 @@
|
||||
export interface GetQuestionListRequest {
|
||||
limit: number;
|
||||
offset: number;
|
||||
from: number;
|
||||
to: number;
|
||||
search: string;
|
||||
type: string;
|
||||
deleted: boolean;
|
||||
required: boolean;
|
||||
quiz_id: number;
|
||||
}
|
||||
|
||||
export interface GetQuestionListResponse {
|
||||
count: number;
|
||||
items: unknown[]; // TODO
|
||||
}
|
15
src/model/question/question.ts
Normal file
15
src/model/question/question.ts
Normal file
@ -0,0 +1,15 @@
|
||||
export interface Question {
|
||||
id: number;
|
||||
quiz_id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
type: "test" | "button" | "file" | "checkbox" | "select" | "none" | "";
|
||||
required: boolean;
|
||||
deleted: boolean;
|
||||
page: number;
|
||||
content: string;
|
||||
version: number;
|
||||
parent_ids: number[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
7
src/model/quiz/copy.ts
Normal file
7
src/model/quiz/copy.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export interface CopyQuizRequest {
|
||||
id: number;
|
||||
}
|
||||
|
||||
export interface CopyQuizResponse {
|
||||
updated: number;
|
||||
}
|
18
src/model/quiz/create.ts
Normal file
18
src/model/quiz/create.ts
Normal file
@ -0,0 +1,18 @@
|
||||
export interface CreateQuizRequest {
|
||||
fingerprinting: boolean;
|
||||
repeatable: boolean;
|
||||
note_prevented: boolean;
|
||||
mail_notifications: boolean;
|
||||
unique_answers: boolean;
|
||||
name: string;
|
||||
description: string;
|
||||
config: string;
|
||||
status: string;
|
||||
limit: number;
|
||||
due_to: number;
|
||||
time_of_passing: number;
|
||||
pausable: boolean;
|
||||
question_cnt: number;
|
||||
super: boolean;
|
||||
group_id: number;
|
||||
}
|
7
src/model/quiz/delete.ts
Normal file
7
src/model/quiz/delete.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export interface DeleteQuizRequest {
|
||||
id: number;
|
||||
}
|
||||
|
||||
export interface DeleteQuizResponse {
|
||||
deactivated: number;
|
||||
}
|
23
src/model/quiz/edit.ts
Normal file
23
src/model/quiz/edit.ts
Normal file
@ -0,0 +1,23 @@
|
||||
export interface EditQuizRequest {
|
||||
id: number;
|
||||
fp: boolean;
|
||||
rep: boolean;
|
||||
note_prevented: boolean;
|
||||
mailing: boolean;
|
||||
uniq: boolean;
|
||||
name: string;
|
||||
desc: string;
|
||||
conf: string;
|
||||
status: string;
|
||||
limit: number;
|
||||
due_to: number;
|
||||
time_of_passing: number;
|
||||
pausable: boolean;
|
||||
question_cnt: number;
|
||||
super: boolean;
|
||||
group_id: number;
|
||||
}
|
||||
|
||||
export interface EditQuizResponse {
|
||||
updated: number;
|
||||
}
|
29
src/model/quiz/get.ts
Normal file
29
src/model/quiz/get.ts
Normal file
@ -0,0 +1,29 @@
|
||||
export interface GetQuizRequest {
|
||||
quiz_id: string;
|
||||
limit: number;
|
||||
page: number;
|
||||
need_config: boolean;
|
||||
}
|
||||
|
||||
export interface GetQuizResponse {
|
||||
cnt: number;
|
||||
settings: {
|
||||
fp: boolean;
|
||||
rep: boolean;
|
||||
name: string;
|
||||
cfg: string;
|
||||
lim: number;
|
||||
due: number;
|
||||
delay: number;
|
||||
pausable: boolean;
|
||||
};
|
||||
items: {
|
||||
id: number;
|
||||
title: string;
|
||||
desc: string;
|
||||
typ: string;
|
||||
req: boolean;
|
||||
p: number;
|
||||
c: string;
|
||||
}[];
|
||||
}
|
17
src/model/quiz/getList.ts
Normal file
17
src/model/quiz/getList.ts
Normal file
@ -0,0 +1,17 @@
|
||||
export interface GetQuizListRequest {
|
||||
limit: number;
|
||||
offset: number;
|
||||
from: number;
|
||||
to: number;
|
||||
search: string;
|
||||
status: string;
|
||||
deleted: boolean;
|
||||
archived: boolean;
|
||||
super: boolean;
|
||||
group_id: number;
|
||||
}
|
||||
|
||||
export interface GetQuizListResponse {
|
||||
count: number;
|
||||
items: unknown[]; // TODO
|
||||
}
|
29
src/model/quiz/quiz.ts
Normal file
29
src/model/quiz/quiz.ts
Normal file
@ -0,0 +1,29 @@
|
||||
export interface BackendQuiz {
|
||||
id: number;
|
||||
qid: string;
|
||||
deleted: boolean;
|
||||
archived: boolean;
|
||||
fingerprinting: boolean;
|
||||
repeatable: boolean;
|
||||
note_prevented: boolean;
|
||||
mail_notifications: boolean;
|
||||
unique_answers: boolean;
|
||||
name: string;
|
||||
description: string;
|
||||
config: string;
|
||||
status: string;
|
||||
limit: number;
|
||||
due_to: number;
|
||||
time_of_passing: number;
|
||||
pausable: boolean;
|
||||
version: number;
|
||||
version_comment: string;
|
||||
parent_ids: number[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
question_cnt: number;
|
||||
passed_count: number;
|
||||
average_time: number;
|
||||
super: boolean;
|
||||
group_id: number;
|
||||
}
|
@ -1,46 +1,38 @@
|
||||
import {Button, Typography, useTheme} from "@mui/material";
|
||||
import ComplexNavText from "./ComplexNavText";
|
||||
import { Button, Typography } from "@mui/material";
|
||||
import { createQuiz } from "@root/quizesV2";
|
||||
import SectionWrapper from "@ui_kit/SectionWrapper";
|
||||
import {quizStore} from "@root/quizes";
|
||||
import {Link, useNavigate} from "react-router-dom";
|
||||
import ComplexNavText from "./ComplexNavText";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
function getRandom(min: number, max:number) {
|
||||
min = Math.ceil(min);
|
||||
max = Math.floor(max);
|
||||
return Math.floor(Math.random() * (max - min)) + min;
|
||||
}
|
||||
|
||||
export default function FirstQuiz() {
|
||||
const {listQuizes, updateQuizesList, removeQuiz, createBlank} = quizStore()
|
||||
const navigate = useNavigate()
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<SectionWrapper
|
||||
maxWidth="lg"
|
||||
sx={{
|
||||
mt: "25px",
|
||||
mb: "70px",
|
||||
}}
|
||||
>
|
||||
<ComplexNavText text1="Кабинет квизов" />
|
||||
<Typography
|
||||
variant="h4"
|
||||
sx={{
|
||||
mt: "20px",
|
||||
mb: "30px",
|
||||
}}
|
||||
>
|
||||
Создайте свой первый квиз
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
data-cy="create-quiz"
|
||||
onClick={() => {
|
||||
navigate(`/setting/${createBlank()}`);
|
||||
}}
|
||||
>
|
||||
Создать +
|
||||
</Button>
|
||||
</SectionWrapper>
|
||||
);
|
||||
return (
|
||||
<SectionWrapper
|
||||
maxWidth="lg"
|
||||
sx={{
|
||||
mt: "25px",
|
||||
mb: "70px",
|
||||
}}
|
||||
>
|
||||
<ComplexNavText text1="Кабинет квизов" />
|
||||
<Typography
|
||||
variant="h4"
|
||||
sx={{
|
||||
mt: "20px",
|
||||
mb: "30px",
|
||||
}}
|
||||
>
|
||||
Создайте свой первый квиз
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
data-cy="create-quiz"
|
||||
onClick={() => createQuiz(navigate)}
|
||||
>
|
||||
Создать +
|
||||
</Button>
|
||||
</SectionWrapper>
|
||||
);
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ import React from "react";
|
||||
import { quizStore } from "@root/quizes";
|
||||
import FirstQuiz from "./FirstQuiz";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { createQuiz } from "@root/quizesV2";
|
||||
interface Props {
|
||||
outerContainerSx?: SxProps<Theme>;
|
||||
children?: React.ReactNode;
|
||||
@ -51,9 +52,7 @@ export default function MyQuizzesFull({
|
||||
padding: isMobile ? "10px" : "10px 47px",
|
||||
minWidth: "44px",
|
||||
}}
|
||||
onClick={() => {
|
||||
navigate(`/setting/${createBlank()}`);
|
||||
}}
|
||||
onClick={() => createQuiz(navigate)}
|
||||
>
|
||||
{isMobile ? "+" : "Создать +"}
|
||||
</Button>
|
||||
|
144
src/stores/questionsV2.ts
Normal file
144
src/stores/questionsV2.ts
Normal file
@ -0,0 +1,144 @@
|
||||
import { questionApi } from "@api/question";
|
||||
import { devlog } from "@frontend/kitui";
|
||||
import { produce } from "immer";
|
||||
import { Question } from "model/question/question";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { isAxiosCanceledError } from "utils/isAxiosCanceledError";
|
||||
import { create } from "zustand";
|
||||
import { devtools } from "zustand/middleware";
|
||||
|
||||
|
||||
type QuestionsStore = {
|
||||
questionsById: Record<number, Question | undefined>;
|
||||
};
|
||||
|
||||
const initialState: QuestionsStore = {
|
||||
questionsById: {},
|
||||
};
|
||||
|
||||
export const useQuestionsStore = create<QuestionsStore>()(
|
||||
devtools(
|
||||
() => initialState,
|
||||
{
|
||||
name: "QuestionsStore",
|
||||
enabled: process.env.NODE_ENV === "development",
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export const setQuestions = (questions: QuestionsStore["questionsById"]) => useQuestionsStore.setState({ questionsById: questions });
|
||||
|
||||
export const setQuestion = (question: Question) => setProducedState(state => {
|
||||
state.questionsById[question.id] = question;
|
||||
}, {
|
||||
type: "setQuestion",
|
||||
question,
|
||||
});
|
||||
|
||||
export const setQuestionField = <T extends keyof Question>(
|
||||
questionId: number,
|
||||
field: T,
|
||||
value: Question[T],
|
||||
) => setProducedState(state => {
|
||||
const question = state.questionsById[questionId];
|
||||
if (!question) return;
|
||||
|
||||
question[field] = value;
|
||||
}, {
|
||||
type: "setQuestionField",
|
||||
questionId,
|
||||
field,
|
||||
value,
|
||||
});
|
||||
|
||||
let savedOriginalQuestion: Question | null = null;
|
||||
let controller: AbortController | null = null;
|
||||
|
||||
export const setQuestionFieldOptimistic = async <T extends keyof Question>(
|
||||
questionId: number,
|
||||
field: T,
|
||||
value: Question[T],
|
||||
) => {
|
||||
const question = useQuestionsStore.getState().questionsById[questionId] ?? null;
|
||||
if (!question) return;
|
||||
|
||||
const currentUpdatedQuestion = produce(question, draft => {
|
||||
draft[field] = value;
|
||||
});
|
||||
controller?.abort();
|
||||
controller = new AbortController();
|
||||
savedOriginalQuestion ??= question;
|
||||
|
||||
setQuestion(currentUpdatedQuestion);
|
||||
try {
|
||||
const { updated } = await questionApi.edit(currentUpdatedQuestion, controller.signal);
|
||||
// await new Promise((resolve, reject) => setTimeout(reject, 2000, new Error("Api rejected")));
|
||||
|
||||
setQuestionField(question.id, "version", updated);
|
||||
controller = null;
|
||||
savedOriginalQuestion = null;
|
||||
} catch (error) {
|
||||
if (isAxiosCanceledError(error)) return;
|
||||
|
||||
devlog("Error editing question", { error, question: question, currentUpdatedQuestion });
|
||||
enqueueSnackbar("Не удалось сохранить вопрос");
|
||||
if (!savedOriginalQuestion) {
|
||||
devlog("Cannot rollback question");
|
||||
throw new Error("Cannot rollback question");
|
||||
}
|
||||
|
||||
setQuestion(savedOriginalQuestion);
|
||||
controller = null;
|
||||
savedOriginalQuestion = null;
|
||||
}
|
||||
};
|
||||
|
||||
export const updateQuestionWithFn = (
|
||||
questionId: number,
|
||||
updateFn: (question: Question) => void,
|
||||
) => setProducedState(state => {
|
||||
const question = state.questionsById[questionId];
|
||||
if (!question) return;
|
||||
|
||||
updateFn(question);
|
||||
}, {
|
||||
type: "updateQuestion",
|
||||
questionId,
|
||||
updateFn: updateFn.toString(),
|
||||
});
|
||||
|
||||
export const createQuestion = async () => {
|
||||
try {
|
||||
const question = await questionApi.create();
|
||||
|
||||
setQuestion(question);
|
||||
} catch (error) {
|
||||
devlog("Error creating question", error);
|
||||
enqueueSnackbar("Не удалось создать вопрос");
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteQuestion = async (questionId: number) => {
|
||||
try {
|
||||
await questionApi.delete(questionId);
|
||||
|
||||
removeQuestion(questionId);
|
||||
} catch (error) {
|
||||
devlog("Error deleting question", error);
|
||||
enqueueSnackbar("Не удалось удалить вопрос");
|
||||
}
|
||||
};
|
||||
|
||||
export const removeQuestion = (questionId: number) => setProducedState(state => {
|
||||
delete state.questionsById[questionId];
|
||||
}, {
|
||||
type: "removeQuestion",
|
||||
questionId,
|
||||
});
|
||||
|
||||
function setProducedState<A extends string | { type: unknown; }>(
|
||||
recipe: (state: QuestionsStore) => void,
|
||||
action?: A,
|
||||
) {
|
||||
useQuestionsStore.setState(state => produce(state, recipe), false, action);
|
||||
}
|
103
src/stores/quizesV2.ts
Normal file
103
src/stores/quizesV2.ts
Normal file
@ -0,0 +1,103 @@
|
||||
import { quizApi } from "@api/quiz";
|
||||
import { devlog } from "@frontend/kitui";
|
||||
import { produce } from "immer";
|
||||
import { BackendQuiz } from "model/quiz/quiz";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { NavigateFunction } from "react-router";
|
||||
import { create } from "zustand";
|
||||
import { devtools } from "zustand/middleware";
|
||||
|
||||
|
||||
type QuizStore = {
|
||||
quizes: Record<number, BackendQuiz | undefined>;
|
||||
};
|
||||
|
||||
const initialState: QuizStore = {
|
||||
quizes: {},
|
||||
};
|
||||
|
||||
export const useQuizStore = create<QuizStore>()(
|
||||
devtools(
|
||||
() => initialState,
|
||||
{
|
||||
name: "QuizStore",
|
||||
enabled: process.env.NODE_ENV === "development",
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export const setQuizes = (quizes: QuizStore["quizes"]) => useQuizStore.setState({ quizes });
|
||||
|
||||
export const setQuiz = (quiz: BackendQuiz) => setProducedState(state => {
|
||||
state.quizes[quiz.id] = quiz;
|
||||
}, {
|
||||
type: "setQuiz",
|
||||
quiz,
|
||||
});
|
||||
|
||||
export const removeQuiz = (quizId: number) => setProducedState(state => {
|
||||
delete state.quizes[quizId];
|
||||
}, {
|
||||
type: "removeQuiz",
|
||||
quizId,
|
||||
});
|
||||
|
||||
export const setQuizField = <T extends keyof BackendQuiz>(
|
||||
quizId: number,
|
||||
field: T,
|
||||
value: BackendQuiz[T],
|
||||
) => setProducedState(state => {
|
||||
const quiz = state.quizes[quizId];
|
||||
if (!quiz) return;
|
||||
|
||||
quiz[field] = value;
|
||||
}, {
|
||||
type: "setQuizField",
|
||||
quizId,
|
||||
field,
|
||||
value,
|
||||
});
|
||||
|
||||
export const updateQuiz = (
|
||||
quizId: number,
|
||||
updateFn: (quiz: BackendQuiz) => void,
|
||||
) => setProducedState(state => {
|
||||
const quiz = state.quizes[quizId];
|
||||
if (!quiz) return;
|
||||
|
||||
updateFn(quiz);
|
||||
}, {
|
||||
type: "updateQuiz",
|
||||
quizId,
|
||||
updateFn: updateFn.toString(),
|
||||
});
|
||||
|
||||
export const createQuiz = async (navigate: NavigateFunction) => {
|
||||
try {
|
||||
const quiz = await quizApi.create();
|
||||
|
||||
setQuiz(quiz);
|
||||
navigate(`/settings/${quiz.id}`);
|
||||
} catch (error) {
|
||||
devlog("Error creating quiz", error);
|
||||
enqueueSnackbar("Не удалось создать квиз");
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteQuiz = async (quizId: number) => {
|
||||
try {
|
||||
await quizApi.delete(quizId);
|
||||
|
||||
removeQuiz(quizId);
|
||||
} catch (error) {
|
||||
devlog("Error deleting quiz", error);
|
||||
enqueueSnackbar("Не удалось удалить квиз");
|
||||
}
|
||||
};
|
||||
|
||||
function setProducedState<A extends string | { type: unknown; }>(
|
||||
recipe: (state: QuizStore) => void,
|
||||
action?: A,
|
||||
) {
|
||||
useQuizStore.setState(state => produce(state, recipe), false, action);
|
||||
}
|
6
src/utils/isAxiosCanceledError.ts
Normal file
6
src/utils/isAxiosCanceledError.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { isAxiosError } from "axios";
|
||||
|
||||
|
||||
export function isAxiosCanceledError(error: unknown) {
|
||||
return isAxiosError(error) && error.code === "ERR_CANCELED";
|
||||
}
|
@ -1,16 +1,19 @@
|
||||
{
|
||||
"compilerOptions":{
|
||||
"baseUrl":"./src",
|
||||
"paths":{
|
||||
"@ui_kit/*":[
|
||||
"./ui_kit/*"
|
||||
],
|
||||
"@icons/*":[
|
||||
"./assets/icons/*"
|
||||
],
|
||||
"@root/*": [
|
||||
"./stores/*"
|
||||
]
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./src",
|
||||
"paths": {
|
||||
"@ui_kit/*": [
|
||||
"./ui_kit/*"
|
||||
],
|
||||
"@icons/*": [
|
||||
"./assets/icons/*"
|
||||
],
|
||||
"@root/*": [
|
||||
"./stores/*"
|
||||
],
|
||||
"@api/*": [
|
||||
"./api/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user