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",
|
"@craco/craco": "^7.0.0",
|
||||||
"@emotion/react": "^11.10.5",
|
"@emotion/react": "^11.10.5",
|
||||||
"@emotion/styled": "^11.10.5",
|
"@emotion/styled": "^11.10.5",
|
||||||
|
"@frontend/kitui": "^1.0.54",
|
||||||
"@mui/icons-material": "^5.10.14",
|
"@mui/icons-material": "^5.10.14",
|
||||||
"@mui/material": "^5.10.14",
|
"@mui/material": "^5.10.14",
|
||||||
"@mui/x-date-pickers": "^6.16.1",
|
"@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 { Button, Typography } from "@mui/material";
|
||||||
import ComplexNavText from "./ComplexNavText";
|
import { createQuiz } from "@root/quizesV2";
|
||||||
import SectionWrapper from "@ui_kit/SectionWrapper";
|
import SectionWrapper from "@ui_kit/SectionWrapper";
|
||||||
import {quizStore} from "@root/quizes";
|
import ComplexNavText from "./ComplexNavText";
|
||||||
import {Link, useNavigate} from "react-router-dom";
|
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() {
|
export default function FirstQuiz() {
|
||||||
const {listQuizes, updateQuizesList, removeQuiz, createBlank} = quizStore()
|
const navigate = useNavigate();
|
||||||
const navigate = useNavigate()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionWrapper
|
<SectionWrapper
|
||||||
maxWidth="lg"
|
maxWidth="lg"
|
||||||
sx={{
|
sx={{
|
||||||
mt: "25px",
|
mt: "25px",
|
||||||
mb: "70px",
|
mb: "70px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ComplexNavText text1="Кабинет квизов" />
|
<ComplexNavText text1="Кабинет квизов" />
|
||||||
<Typography
|
<Typography
|
||||||
variant="h4"
|
variant="h4"
|
||||||
sx={{
|
sx={{
|
||||||
mt: "20px",
|
mt: "20px",
|
||||||
mb: "30px",
|
mb: "30px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Создайте свой первый квиз
|
Создайте свой первый квиз
|
||||||
</Typography>
|
</Typography>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
data-cy="create-quiz"
|
data-cy="create-quiz"
|
||||||
onClick={() => {
|
onClick={() => createQuiz(navigate)}
|
||||||
navigate(`/setting/${createBlank()}`);
|
>
|
||||||
}}
|
Создать +
|
||||||
>
|
</Button>
|
||||||
Создать +
|
</SectionWrapper>
|
||||||
</Button>
|
);
|
||||||
</SectionWrapper>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ import React from "react";
|
|||||||
import { quizStore } from "@root/quizes";
|
import { quizStore } from "@root/quizes";
|
||||||
import FirstQuiz from "./FirstQuiz";
|
import FirstQuiz from "./FirstQuiz";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { createQuiz } from "@root/quizesV2";
|
||||||
interface Props {
|
interface Props {
|
||||||
outerContainerSx?: SxProps<Theme>;
|
outerContainerSx?: SxProps<Theme>;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
@ -51,9 +52,7 @@ export default function MyQuizzesFull({
|
|||||||
padding: isMobile ? "10px" : "10px 47px",
|
padding: isMobile ? "10px" : "10px 47px",
|
||||||
minWidth: "44px",
|
minWidth: "44px",
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => createQuiz(navigate)}
|
||||||
navigate(`/setting/${createBlank()}`);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{isMobile ? "+" : "Создать +"}
|
{isMobile ? "+" : "Создать +"}
|
||||||
</Button>
|
</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":{
|
"compilerOptions": {
|
||||||
"baseUrl":"./src",
|
"baseUrl": "./src",
|
||||||
"paths":{
|
"paths": {
|
||||||
"@ui_kit/*":[
|
"@ui_kit/*": [
|
||||||
"./ui_kit/*"
|
"./ui_kit/*"
|
||||||
],
|
],
|
||||||
"@icons/*":[
|
"@icons/*": [
|
||||||
"./assets/icons/*"
|
"./assets/icons/*"
|
||||||
],
|
],
|
||||||
"@root/*": [
|
"@root/*": [
|
||||||
"./stores/*"
|
"./stores/*"
|
||||||
]
|
],
|
||||||
|
"@api/*": [
|
||||||
|
"./api/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
Loading…
Reference in New Issue
Block a user