Merge branch 'requests-refactor' into 'dev'
style: requests See merge request frontend/squiz!321
This commit is contained in:
commit
73e8a03a22
@ -24,7 +24,7 @@ import {
|
||||
UserAccount,
|
||||
useUserFetcher,
|
||||
} from "@frontend/kitui";
|
||||
import makeRequest from "@api/makeRequest";
|
||||
import { makeRequest } from "@api/makeRequest";
|
||||
import type { OriginalUserAccount } from "@root/user";
|
||||
import {
|
||||
clearUserData,
|
||||
|
@ -1,4 +1,4 @@
|
||||
import makeRequest from "@api/makeRequest";
|
||||
import { makeRequest } from "@api/makeRequest";
|
||||
|
||||
import type {
|
||||
LoginRequest,
|
||||
@ -8,19 +8,19 @@ import type {
|
||||
} from "@frontend/kitui";
|
||||
import { parseAxiosError } from "../utils/parse-error";
|
||||
|
||||
const apiUrl = process.env.REACT_APP_DOMAIN + "/auth";
|
||||
const API_URL = process.env.REACT_APP_DOMAIN + "/auth";
|
||||
|
||||
export async function register(
|
||||
export const register = async (
|
||||
login: string,
|
||||
password: string,
|
||||
phoneNumber: string,
|
||||
): Promise<[RegisterResponse | null, string?]> {
|
||||
): Promise<[RegisterResponse | null, string?]> => {
|
||||
try {
|
||||
const registerResponse = await makeRequest<
|
||||
RegisterRequest,
|
||||
RegisterResponse
|
||||
>({
|
||||
url: apiUrl + "/register",
|
||||
url: `${API_URL}/register`,
|
||||
body: { login, password, phoneNumber },
|
||||
useToken: false,
|
||||
withCredentials: true,
|
||||
@ -32,15 +32,15 @@ export async function register(
|
||||
|
||||
return [null, `Не удалось зарегестрировать аккаунт. ${error}`];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export async function login(
|
||||
export const login = async (
|
||||
login: string,
|
||||
password: string,
|
||||
): Promise<[LoginResponse | null, string?]> {
|
||||
): Promise<[LoginResponse | null, string?]> => {
|
||||
try {
|
||||
const loginResponse = await makeRequest<LoginRequest, LoginResponse>({
|
||||
url: apiUrl + "/login",
|
||||
url: `${API_URL}/login`,
|
||||
body: { login, password },
|
||||
useToken: false,
|
||||
withCredentials: true,
|
||||
@ -52,13 +52,13 @@ export async function login(
|
||||
|
||||
return [null, `Не удалось войти. ${error}`];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export async function logout(): Promise<[unknown, string?]> {
|
||||
export const logout = async (): Promise<[unknown, string?]> => {
|
||||
try {
|
||||
const logoutResponse = await makeRequest<never, void>({
|
||||
url: apiUrl + "/logout",
|
||||
method: "POST",
|
||||
url: `${API_URL}/logout`,
|
||||
useToken: true,
|
||||
withCredentials: true,
|
||||
});
|
||||
@ -67,13 +67,13 @@ export async function logout(): Promise<[unknown, string?]> {
|
||||
} catch (nativeError) {
|
||||
const [error] = parseAxiosError(nativeError);
|
||||
|
||||
return [null];
|
||||
return [null, error];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export async function recover(
|
||||
export const recover = async (
|
||||
email: string,
|
||||
): Promise<[unknown | null, string?]> {
|
||||
): Promise<[unknown | null, string?]> => {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("email", email);
|
||||
@ -81,16 +81,18 @@ export async function recover(
|
||||
"RedirectionURL",
|
||||
process.env.REACT_APP_DOMAIN + "/changepwd",
|
||||
);
|
||||
|
||||
const recoverResponse = await makeRequest<unknown, unknown>({
|
||||
url: process.env.REACT_APP_DOMAIN + "/codeword/recover",
|
||||
body: formData,
|
||||
useToken: false,
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
return [recoverResponse];
|
||||
} catch (nativeError) {
|
||||
const [error] = parseAxiosError(nativeError);
|
||||
|
||||
return [null, `Не удалось восстановить пароль. ${error}`];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -1,15 +1,15 @@
|
||||
import { UserAccount } from "@frontend/kitui";
|
||||
import makeRequest from "@api/makeRequest";
|
||||
import { makeRequest } from "@api/makeRequest";
|
||||
|
||||
import { parseAxiosError } from "@utils/parse-error";
|
||||
|
||||
const apiUrl = process.env.REACT_APP_DOMAIN + "/customer";
|
||||
const API_URL = process.env.REACT_APP_DOMAIN + "/customer";
|
||||
|
||||
export async function payCart(): Promise<[UserAccount | null, string?]> {
|
||||
export const payCart = async (): Promise<[UserAccount | null, string?]> => {
|
||||
try {
|
||||
const payCartResponse = await makeRequest<never, UserAccount>({
|
||||
url: apiUrl + "/cart/pay",
|
||||
method: "POST",
|
||||
url: `${API_URL}/cart/pay`,
|
||||
useToken: true,
|
||||
});
|
||||
|
||||
@ -19,4 +19,4 @@ export async function payCart(): Promise<[UserAccount | null, string?]> {
|
||||
|
||||
return [null, `Не удалось оплатить товар из корзины. ${error}`];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -1,16 +1,31 @@
|
||||
import axios from "axios";
|
||||
import { makeRequest } from "@api/makeRequest";
|
||||
|
||||
const domen = process.env.REACT_APP_DOMAIN;
|
||||
import { parseAxiosError } from "@utils/parse-error";
|
||||
|
||||
export function sendContactFormRequest(body: {
|
||||
const API_URL = process.env.REACT_APP_DOMAIN + "/feedback";
|
||||
|
||||
type SendContactFormBody = {
|
||||
contact: string;
|
||||
whoami: string;
|
||||
}) {
|
||||
return axios(`${domen}/feedback/callme`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
data: body,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const sendContactFormRequest = async (
|
||||
body: SendContactFormBody,
|
||||
): Promise<[unknown | null, string?, number?]> => {
|
||||
try {
|
||||
const sendContactFormResponse = await makeRequest<
|
||||
SendContactFormBody,
|
||||
unknown
|
||||
>({
|
||||
method: "POST",
|
||||
url: `${API_URL}/callme`,
|
||||
body,
|
||||
});
|
||||
|
||||
return [sendContactFormResponse];
|
||||
} catch (nativeError) {
|
||||
const [error, status] = parseAxiosError(nativeError);
|
||||
|
||||
return [null, `Не удалось отправить контакты. ${error}`, status];
|
||||
}
|
||||
};
|
||||
|
@ -5,12 +5,15 @@ import type { Discount } from "@model/discounts";
|
||||
|
||||
const API_URL = process.env.REACT_APP_DOMAIN + "/price/discount";
|
||||
|
||||
export async function getDiscounts(
|
||||
export const getDiscounts = async (
|
||||
userId: string,
|
||||
): Promise<[Discount[] | null, string?]> {
|
||||
): Promise<[Discount[] | null, string?]> => {
|
||||
try {
|
||||
const { Discounts } = await makeRequest<unknown, { Discounts: Discount[] }>(
|
||||
{ method: "GET", url: `${API_URL}/user/${userId}` },
|
||||
{
|
||||
method: "GET",
|
||||
url: `${API_URL}/user/${userId}`,
|
||||
},
|
||||
);
|
||||
|
||||
return [Discounts];
|
||||
@ -19,4 +22,4 @@ export async function getDiscounts(
|
||||
|
||||
return [null, `Не удалось получить скидки. ${error}`];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -6,6 +6,8 @@ import { clearUserData } from "@root/user";
|
||||
import { clearQuizData } from "@root/quizes/store";
|
||||
import { redirect } from "react-router-dom";
|
||||
|
||||
import type { AxiosResponse } from "axios";
|
||||
|
||||
interface MakeRequest {
|
||||
method?: Method | undefined;
|
||||
url: string;
|
||||
@ -17,18 +19,22 @@ interface MakeRequest {
|
||||
withCredentials?: boolean | undefined;
|
||||
}
|
||||
|
||||
async function makeRequest<TRequest = unknown, TResponse = unknown>(
|
||||
data: MakeRequest,
|
||||
): Promise<TResponse> {
|
||||
try {
|
||||
const response = await KIT.makeRequest<unknown>(data);
|
||||
type ExtendedAxiosResponse = AxiosResponse & { message: string };
|
||||
|
||||
export const makeRequest = async <TRequest = unknown, TResponse = unknown>(
|
||||
data: MakeRequest,
|
||||
): Promise<TResponse> => {
|
||||
try {
|
||||
const response = await KIT.makeRequest<unknown, TResponse>(data);
|
||||
|
||||
return response;
|
||||
} catch (nativeError) {
|
||||
const error = nativeError as AxiosError;
|
||||
|
||||
return response as TResponse;
|
||||
} catch (e) {
|
||||
const error = e as AxiosError;
|
||||
if (
|
||||
error.response?.status === 400 &&
|
||||
error.response?.data?.message === "refreshToken is empty"
|
||||
(error.response?.data as ExtendedAxiosResponse)?.message ===
|
||||
"refreshToken is empty"
|
||||
) {
|
||||
cleanAuthTicketData();
|
||||
clearAuthToken();
|
||||
@ -36,7 +42,7 @@ async function makeRequest<TRequest = unknown, TResponse = unknown>(
|
||||
clearQuizData();
|
||||
redirect("/");
|
||||
}
|
||||
throw e;
|
||||
|
||||
throw nativeError;
|
||||
}
|
||||
}
|
||||
export default makeRequest;
|
||||
};
|
||||
|
@ -1,25 +1,27 @@
|
||||
import makeRequest from "@api/makeRequest";
|
||||
import { makeRequest } from "@api/makeRequest";
|
||||
|
||||
import { parseAxiosError } from "@utils/parse-error";
|
||||
|
||||
const apiUrl = process.env.REACT_APP_DOMAIN + "/codeword/promocode";
|
||||
type ActivatePromocodeRequest = { codeword: string } | { fastLink: string };
|
||||
type ActivatePromocodeResponse = { greetings: string };
|
||||
|
||||
export async function activatePromocode(promocode: string) {
|
||||
const API_URL = process.env.REACT_APP_DOMAIN + "/codeword/promocode";
|
||||
|
||||
export const activatePromocode = async (promocode: string) => {
|
||||
try {
|
||||
const response = await makeRequest<
|
||||
{ codeword: string } | { fastLink: string },
|
||||
{ greetings: string }
|
||||
ActivatePromocodeRequest,
|
||||
ActivatePromocodeResponse
|
||||
>({
|
||||
url: apiUrl + "/activate",
|
||||
method: "POST",
|
||||
contentType: true,
|
||||
url: API_URL + "/activate",
|
||||
body: { codeword: promocode },
|
||||
contentType: true,
|
||||
});
|
||||
|
||||
return response.greetings;
|
||||
} catch (nativeError) {
|
||||
const [error] = parseAxiosError(nativeError);
|
||||
throw new Error(error);
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import makeRequest from "@api/makeRequest";
|
||||
import { makeRequest } from "@api/makeRequest";
|
||||
import { CreateQuestionRequest } from "model/question/create";
|
||||
import { RawQuestion } from "model/question/question";
|
||||
import {
|
||||
@ -18,67 +18,136 @@ import {
|
||||
CopyQuestionResponse,
|
||||
} from "@model/question/copy";
|
||||
import { replaceSpacesToEmptyLines } from "../utils/replaceSpacesToEmptyLines";
|
||||
import { parseAxiosError } from "@utils/parse-error";
|
||||
|
||||
const baseUrl = process.env.REACT_APP_DOMAIN + "/squiz";
|
||||
const API_URL = process.env.REACT_APP_DOMAIN + "/squiz";
|
||||
|
||||
function createQuestion(body: CreateQuestionRequest) {
|
||||
return makeRequest<CreateQuestionRequest, RawQuestion>({
|
||||
url: `${baseUrl}/question/create`,
|
||||
body,
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
export const createQuestion = async (
|
||||
body: CreateQuestionRequest,
|
||||
): Promise<[RawQuestion | null, string?]> => {
|
||||
try {
|
||||
const createdQuestion = await makeRequest<
|
||||
CreateQuestionRequest,
|
||||
RawQuestion
|
||||
>({
|
||||
method: "POST",
|
||||
url: `${API_URL}/question/create`,
|
||||
body,
|
||||
});
|
||||
|
||||
async function getQuestionList(body?: Partial<GetQuestionListRequest>) {
|
||||
if (!body?.quiz_id) return null;
|
||||
return [createdQuestion];
|
||||
} catch (nativeError) {
|
||||
const [error] = parseAxiosError(nativeError);
|
||||
|
||||
const response = await makeRequest<
|
||||
GetQuestionListRequest,
|
||||
GetQuestionListResponse
|
||||
>({
|
||||
url: `${baseUrl}/question/getList`,
|
||||
body: { ...defaultGetQuestionListBody, ...body },
|
||||
method: "POST",
|
||||
});
|
||||
const clearArrayFromEmptySpaceBlaBlaValue = response.items?.map(
|
||||
(question) => {
|
||||
let data = question;
|
||||
for (let key in question) {
|
||||
const k = key as keyof RawQuestion;
|
||||
//@ts-ignore
|
||||
if (question[key] === " ") data[key] = "";
|
||||
}
|
||||
return data;
|
||||
},
|
||||
);
|
||||
return [null, error];
|
||||
}
|
||||
};
|
||||
|
||||
return replaceSpacesToEmptyLines(clearArrayFromEmptySpaceBlaBlaValue);
|
||||
}
|
||||
const getQuestionList = async (
|
||||
body?: Partial<GetQuestionListRequest>,
|
||||
): Promise<[RawQuestion[] | null, string?]> => {
|
||||
try {
|
||||
if (!body?.quiz_id) return [null, "Квиз не найден"];
|
||||
|
||||
function editQuestion(body: EditQuestionRequest, signal?: AbortSignal) {
|
||||
return makeRequest<EditQuestionRequest, EditQuestionResponse>({
|
||||
url: `${baseUrl}/question/edit`,
|
||||
body,
|
||||
method: "PATCH",
|
||||
signal,
|
||||
});
|
||||
}
|
||||
const response = await makeRequest<
|
||||
GetQuestionListRequest,
|
||||
GetQuestionListResponse
|
||||
>({
|
||||
method: "POST",
|
||||
url: `${API_URL}/question/getList`,
|
||||
body: { ...defaultGetQuestionListBody, ...body },
|
||||
});
|
||||
|
||||
function copyQuestion(questionId: number, quizId: number) {
|
||||
return makeRequest<CopyQuestionRequest, CopyQuestionResponse>({
|
||||
url: `${baseUrl}/question/copy`,
|
||||
body: { id: questionId, quiz_id: quizId },
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
const clearArrayFromEmptySpaceBlaBlaValue = response.items?.map(
|
||||
(question) => {
|
||||
let data = question;
|
||||
|
||||
function deleteQuestion(id: number) {
|
||||
return makeRequest<DeleteQuestionRequest, DeleteQuestionResponse>({
|
||||
url: `${baseUrl}/question/delete`,
|
||||
body: { id },
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
for (let key in question) {
|
||||
if (question[key as keyof RawQuestion] === " ") {
|
||||
//@ts-ignore
|
||||
data[key] = "";
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
);
|
||||
|
||||
return [
|
||||
replaceSpacesToEmptyLines(clearArrayFromEmptySpaceBlaBlaValue) ?? null,
|
||||
];
|
||||
} catch (nativeError) {
|
||||
const [error] = parseAxiosError(nativeError);
|
||||
|
||||
return [null, error];
|
||||
}
|
||||
};
|
||||
|
||||
export const editQuestion = async (
|
||||
body: EditQuestionRequest,
|
||||
signal?: AbortSignal,
|
||||
): Promise<[EditQuestionResponse | null, string?]> => {
|
||||
try {
|
||||
const editedQuestion = await makeRequest<
|
||||
EditQuestionRequest,
|
||||
EditQuestionResponse
|
||||
>({
|
||||
method: "PATCH",
|
||||
url: `${API_URL}/question/edit`,
|
||||
body,
|
||||
signal,
|
||||
});
|
||||
|
||||
return [editedQuestion];
|
||||
} catch (nativeError) {
|
||||
const [error] = parseAxiosError(nativeError);
|
||||
|
||||
return [null, error];
|
||||
}
|
||||
};
|
||||
|
||||
export const copyQuestion = async (
|
||||
questionId: number,
|
||||
quizId: number,
|
||||
): Promise<[CopyQuestionResponse | null, string?]> => {
|
||||
try {
|
||||
const copiedQuestion = await makeRequest<
|
||||
CopyQuestionRequest,
|
||||
CopyQuestionResponse
|
||||
>({
|
||||
method: "POST",
|
||||
url: `${API_URL}/question/copy`,
|
||||
body: { id: questionId, quiz_id: quizId },
|
||||
});
|
||||
|
||||
return [copiedQuestion];
|
||||
} catch (nativeError) {
|
||||
const [error] = parseAxiosError(nativeError);
|
||||
|
||||
return [null, error];
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteQuestion = async (
|
||||
id: number,
|
||||
): Promise<[DeleteQuestionResponse | null, string?]> => {
|
||||
try {
|
||||
const deletedQuestion = await makeRequest<
|
||||
DeleteQuestionRequest,
|
||||
DeleteQuestionResponse
|
||||
>({
|
||||
url: `${API_URL}/question/delete`,
|
||||
body: { id },
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
return [deletedQuestion];
|
||||
} catch (nativeError) {
|
||||
const [error] = parseAxiosError(nativeError);
|
||||
|
||||
return [null, error];
|
||||
}
|
||||
};
|
||||
|
||||
export const questionApi = {
|
||||
create: createQuestion,
|
||||
|
198
src/api/quiz.ts
198
src/api/quiz.ts
@ -1,4 +1,4 @@
|
||||
import makeRequest from "@api/makeRequest";
|
||||
import { makeRequest } from "@api/makeRequest";
|
||||
import { defaultQuizConfig } from "@model/quizSettings";
|
||||
import { CopyQuizRequest, CopyQuizResponse } from "model/quiz/copy";
|
||||
import { CreateQuizRequest } from "model/quiz/create";
|
||||
@ -7,73 +7,157 @@ import { EditQuizRequest, EditQuizResponse } from "model/quiz/edit";
|
||||
import { GetQuizRequest, GetQuizResponse } from "model/quiz/get";
|
||||
import { GetQuizListRequest, GetQuizListResponse } from "model/quiz/getList";
|
||||
import { RawQuiz } from "model/quiz/quiz";
|
||||
import { parseAxiosError } from "@utils/parse-error";
|
||||
|
||||
const baseUrl = process.env.REACT_APP_DOMAIN + "/squiz";
|
||||
const imagesUrl = process.env.REACT_APP_DOMAIN + "/squizstorer";
|
||||
type AddedQuizImagesResponse = {
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
function createQuiz(body?: Partial<CreateQuizRequest>) {
|
||||
return makeRequest<CreateQuizRequest, RawQuiz>({
|
||||
url: `${baseUrl}/quiz/create`,
|
||||
body: { ...defaultCreateQuizBody, ...body },
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
const API_URL = process.env.REACT_APP_DOMAIN + "/squiz";
|
||||
const IMAGES_URL = process.env.REACT_APP_DOMAIN + "/squizstorer";
|
||||
|
||||
async function getQuizList(body?: Partial<GetQuizListRequest>) {
|
||||
const response = await makeRequest<GetQuizListRequest, GetQuizListResponse>({
|
||||
url: `${baseUrl}/quiz/getList`,
|
||||
body: { ...defaultGetQuizListBody, ...body },
|
||||
method: "POST",
|
||||
});
|
||||
export const createQuiz = async (
|
||||
body?: Partial<CreateQuizRequest>,
|
||||
): Promise<[RawQuiz | null, string?]> => {
|
||||
try {
|
||||
const createdQuiz = await makeRequest<CreateQuizRequest, RawQuiz>({
|
||||
method: "POST",
|
||||
url: `${API_URL}/quiz/create`,
|
||||
body: { ...defaultCreateQuizBody, ...body },
|
||||
});
|
||||
|
||||
return response.items;
|
||||
}
|
||||
return [createdQuiz];
|
||||
} catch (nativeError) {
|
||||
const [error] = parseAxiosError(nativeError);
|
||||
|
||||
function getQuiz(body?: Partial<GetQuizRequest>) {
|
||||
return makeRequest<GetQuizRequest, GetQuizResponse>({
|
||||
url: `${baseUrl}/quiz/get`,
|
||||
body: { ...defaultGetQuizBody, ...body },
|
||||
method: "GET",
|
||||
});
|
||||
}
|
||||
return [null, error];
|
||||
}
|
||||
};
|
||||
|
||||
async function editQuiz(body: EditQuizRequest, signal?: AbortSignal) {
|
||||
return makeRequest<EditQuizRequest, EditQuizResponse>({
|
||||
url: `${baseUrl}/quiz/edit`,
|
||||
body,
|
||||
method: "PATCH",
|
||||
signal,
|
||||
});
|
||||
}
|
||||
export const getQuizList = async (
|
||||
body?: Partial<CreateQuizRequest>,
|
||||
): Promise<[RawQuiz[] | null, string?]> => {
|
||||
try {
|
||||
const { items } = await makeRequest<
|
||||
GetQuizListRequest,
|
||||
GetQuizListResponse
|
||||
>({
|
||||
method: "POST",
|
||||
url: `${API_URL}/quiz/getList`,
|
||||
body: { ...defaultGetQuizListBody, ...body },
|
||||
});
|
||||
|
||||
function copyQuiz(id: number) {
|
||||
return makeRequest<CopyQuizRequest, CopyQuizResponse>({
|
||||
url: `${baseUrl}/quiz/copy`,
|
||||
body: { id },
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
return [items];
|
||||
} catch (nativeError) {
|
||||
const [error] = parseAxiosError(nativeError);
|
||||
|
||||
function deleteQuiz(id: number) {
|
||||
return makeRequest<DeleteQuizRequest, DeleteQuizResponse>({
|
||||
url: `${baseUrl}/quiz/delete`,
|
||||
body: { id },
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
return [null, error];
|
||||
}
|
||||
};
|
||||
|
||||
function addQuizImages(quizId: number, image: Blob) {
|
||||
const formData = new FormData();
|
||||
export const getQuiz = async (
|
||||
body?: Partial<GetQuizRequest>,
|
||||
): Promise<[GetQuizResponse | null, string?]> => {
|
||||
try {
|
||||
const quiz = await makeRequest<GetQuizRequest, GetQuizResponse>({
|
||||
method: "GET",
|
||||
url: `${API_URL}/quiz/get`,
|
||||
body: { ...defaultGetQuizBody, ...body },
|
||||
});
|
||||
|
||||
formData.append("quiz", quizId.toString());
|
||||
formData.append("image", image);
|
||||
return [quiz];
|
||||
} catch (nativeError) {
|
||||
const [error] = parseAxiosError(nativeError);
|
||||
|
||||
return makeRequest<FormData, { [key: string]: string }>({
|
||||
url: `${imagesUrl}/quiz/putImages`,
|
||||
body: formData,
|
||||
method: "PUT",
|
||||
});
|
||||
}
|
||||
return [null, error];
|
||||
}
|
||||
};
|
||||
|
||||
export const editQuiz = async (
|
||||
body: EditQuizRequest,
|
||||
signal?: AbortSignal,
|
||||
): Promise<[EditQuizResponse | null, string?]> => {
|
||||
try {
|
||||
const editedQuiz = await makeRequest<EditQuizRequest, EditQuizResponse>({
|
||||
method: "PATCH",
|
||||
url: `${API_URL}/quiz/edit`,
|
||||
body,
|
||||
signal,
|
||||
});
|
||||
|
||||
return [editedQuiz];
|
||||
} catch (nativeError) {
|
||||
const [error] = parseAxiosError(nativeError);
|
||||
|
||||
return [null, error];
|
||||
}
|
||||
};
|
||||
|
||||
export const copyQuiz = async (
|
||||
id: number,
|
||||
): Promise<[EditQuizResponse | null, string?]> => {
|
||||
try {
|
||||
const copiedQuiz = await makeRequest<CopyQuizRequest, CopyQuizResponse>({
|
||||
method: "POST",
|
||||
url: `${API_URL}/quiz/copy`,
|
||||
body: { id },
|
||||
});
|
||||
|
||||
return [copiedQuiz];
|
||||
} catch (nativeError) {
|
||||
const [error] = parseAxiosError(nativeError);
|
||||
|
||||
return [null, error];
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteQuiz = async (
|
||||
id: number,
|
||||
): Promise<[DeleteQuizResponse | null, string?]> => {
|
||||
try {
|
||||
const deletedQuiz = await makeRequest<
|
||||
DeleteQuizRequest,
|
||||
DeleteQuizResponse
|
||||
>({
|
||||
method: "DELETE",
|
||||
url: `${API_URL}/quiz/delete`,
|
||||
body: { id },
|
||||
});
|
||||
|
||||
return [deletedQuiz];
|
||||
} catch (nativeError) {
|
||||
const [error] = parseAxiosError(nativeError);
|
||||
|
||||
return [null, error];
|
||||
}
|
||||
};
|
||||
|
||||
export const addQuizImages = async (
|
||||
quizId: number,
|
||||
image: Blob,
|
||||
): Promise<[AddedQuizImagesResponse | null, string?]> => {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append("quiz", quizId.toString());
|
||||
formData.append("image", image);
|
||||
|
||||
const addedQuizImages = await makeRequest<
|
||||
FormData,
|
||||
AddedQuizImagesResponse
|
||||
>({
|
||||
url: `${IMAGES_URL}/quiz/putImages`,
|
||||
body: formData,
|
||||
method: "PUT",
|
||||
});
|
||||
|
||||
return [addedQuizImages];
|
||||
} catch (nativeError) {
|
||||
const [error] = parseAxiosError(nativeError);
|
||||
|
||||
return [null, error];
|
||||
}
|
||||
};
|
||||
|
||||
export const quizApi = {
|
||||
create: createQuiz,
|
||||
|
@ -1,4 +1,4 @@
|
||||
import makeRequest from "@api/makeRequest";
|
||||
import { makeRequest } from "@api/makeRequest";
|
||||
import { RawResult } from "@model/result/result";
|
||||
|
||||
interface IResultListBody {
|
||||
@ -29,47 +29,47 @@ export interface IAnswerResult {
|
||||
question_id: number;
|
||||
}
|
||||
|
||||
async function getResultList(quizId: number, page: number, body: any) {
|
||||
const API_URL = process.env.REACT_APP_DOMAIN + `/squiz`;
|
||||
|
||||
const getResultList = async (quizId: number, page: number, body: any) => {
|
||||
return makeRequest<IResultListBody, RawResult>({
|
||||
url: process.env.REACT_APP_DOMAIN + `/squiz/results/getResults/${quizId}`,
|
||||
method: "POST",
|
||||
url: `${API_URL}/results/getResults/${quizId}`,
|
||||
body: { page: page, limit: 10, ...body },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function deleteResult(resultId: number) {
|
||||
const deleteResult = async (resultId: number) => {
|
||||
return makeRequest<unknown, unknown>({
|
||||
url: process.env.REACT_APP_DOMAIN + `/squiz/results/delete/${resultId}`,
|
||||
body: {},
|
||||
method: "DELETE",
|
||||
url: `${API_URL}/results/delete/${resultId}`,
|
||||
body: {},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function obsolescenceResult(idResultArray: number[]) {
|
||||
const obsolescenceResult = (idResultArray: number[]) => {
|
||||
return makeRequest<unknown, unknown>({
|
||||
url: process.env.REACT_APP_DOMAIN + `/squiz/result/seen`,
|
||||
body: {
|
||||
answers: idResultArray,
|
||||
},
|
||||
method: "PATCH",
|
||||
url: `${API_URL}/result/seen`,
|
||||
body: { answers: idResultArray },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function getAnswerResultList(resultId: number) {
|
||||
const getAnswerResultList = (resultId: number) => {
|
||||
return makeRequest<unknown, IAnswerResult[]>({
|
||||
url: process.env.REACT_APP_DOMAIN + `/squiz/result/${resultId}`,
|
||||
method: "GET",
|
||||
url: `${API_URL}/result/${resultId}`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function AnswerResultListEx(quizId: number, body: any) {
|
||||
const AnswerResultListEx = (quizId: number, body: any) => {
|
||||
return makeRequest<unknown, unknown>({
|
||||
responseType: "blob",
|
||||
url: process.env.REACT_APP_DOMAIN + `/squiz/results/${quizId}/export`,
|
||||
method: "POST",
|
||||
url: `${API_URL}/results/${quizId}/export`,
|
||||
body: body,
|
||||
responseType: "blob",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const resultApi = {
|
||||
getList: getResultList,
|
||||
|
@ -1,9 +1,7 @@
|
||||
import makeRequest from "@api/makeRequest";
|
||||
import { makeRequest } from "@api/makeRequest";
|
||||
|
||||
import { parseAxiosError } from "@utils/parse-error";
|
||||
|
||||
const apiUrl = process.env.REACT_APP_DOMAIN + "/squiz/statistic";
|
||||
|
||||
export type DevicesResponse = {
|
||||
Device: Record<string, number>;
|
||||
OS: Record<string, number>;
|
||||
@ -29,6 +27,8 @@ type TRequest = {
|
||||
from: number;
|
||||
};
|
||||
|
||||
const API_URL = process.env.REACT_APP_DOMAIN + "/squiz/statistic";
|
||||
|
||||
export const getDevices = async (
|
||||
quizId: string,
|
||||
to: number,
|
||||
@ -37,7 +37,7 @@ export const getDevices = async (
|
||||
try {
|
||||
const devicesResponse = await makeRequest<TRequest, DevicesResponse>({
|
||||
method: "POST",
|
||||
url: `${apiUrl}/${quizId}/devices`,
|
||||
url: `${API_URL}/${quizId}/devices`,
|
||||
withCredentials: true,
|
||||
body: { to, from },
|
||||
});
|
||||
@ -58,7 +58,7 @@ export const getGeneral = async (
|
||||
try {
|
||||
const generalResponse = await makeRequest<TRequest, GeneralResponse>({
|
||||
method: "POST",
|
||||
url: `${apiUrl}/${quizId}/general`,
|
||||
url: `${API_URL}/${quizId}/general`,
|
||||
withCredentials: true,
|
||||
body: { to, from },
|
||||
});
|
||||
@ -79,7 +79,7 @@ export const getQuestions = async (
|
||||
try {
|
||||
const questionsResponse = await makeRequest<TRequest, QuestionsResponse>({
|
||||
method: "POST",
|
||||
url: `${apiUrl}/${quizId}/questions`,
|
||||
url: `${API_URL}/${quizId}/questions`,
|
||||
withCredentials: true,
|
||||
body: { to, from },
|
||||
});
|
||||
|
@ -1,20 +1,20 @@
|
||||
import makeRequest from "@api/makeRequest";
|
||||
import { makeRequest } from "@api/makeRequest";
|
||||
import { parseAxiosError } from "../utils/parse-error";
|
||||
|
||||
import { SendTicketMessageRequest } from "@frontend/kitui";
|
||||
|
||||
const apiUrl = process.env.REACT_APP_DOMAIN + "/heruvym";
|
||||
const API_URL = process.env.REACT_APP_DOMAIN + "/heruvym";
|
||||
|
||||
export async function sendTicketMessage(
|
||||
export const sendTicketMessage = async (
|
||||
ticketId: string,
|
||||
message: string,
|
||||
): Promise<[null, string?]> {
|
||||
): Promise<[null, string?]> => {
|
||||
try {
|
||||
const sendTicketMessageResponse = await makeRequest<
|
||||
SendTicketMessageRequest,
|
||||
null
|
||||
>({
|
||||
url: `${apiUrl}/send`,
|
||||
url: `${API_URL}/send`,
|
||||
method: "POST",
|
||||
useToken: true,
|
||||
body: { ticket: ticketId, message: message, lang: "ru", files: [] },
|
||||
@ -26,12 +26,12 @@ export async function sendTicketMessage(
|
||||
|
||||
return [null, `Не удалось отправить сообщение. ${error}`];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export async function shownMessage(id: string): Promise<[null, string?]> {
|
||||
export const shownMessage = async (id: string): Promise<[null, string?]> => {
|
||||
try {
|
||||
const shownMessageResponse = await makeRequest<{ id: string }, null>({
|
||||
url: apiUrl + "/shown",
|
||||
url: `${API_URL}/shown`,
|
||||
method: "POST",
|
||||
useToken: true,
|
||||
body: { id },
|
||||
@ -43,4 +43,4 @@ export async function shownMessage(id: string): Promise<[null, string?]> {
|
||||
|
||||
return [null, `Не удалось прочесть сообщение. ${error}`];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -4,10 +4,15 @@ interface Props {
|
||||
color?: string;
|
||||
bgcolor?: string;
|
||||
marL?: string;
|
||||
width?: string
|
||||
width?: string;
|
||||
}
|
||||
|
||||
export default function CopyIcon({ color, bgcolor, marL, width = "36px" }: Props) {
|
||||
export default function CopyIcon({
|
||||
color,
|
||||
bgcolor,
|
||||
marL,
|
||||
width = "36px",
|
||||
}: Props) {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
|
@ -68,13 +68,16 @@ export default function Analytics() {
|
||||
|
||||
useEffect(() => {
|
||||
const getData = async (): Promise<void> => {
|
||||
try {
|
||||
if (editQuizId !== null) {
|
||||
const gottenQuizes = await quizApi.getList();
|
||||
setQuizes(gottenQuizes);
|
||||
if (editQuizId !== null) {
|
||||
const [gottenQuizes, gottenQuizesError] = await quizApi.getList();
|
||||
|
||||
if (gottenQuizesError) {
|
||||
console.error("Не удалось получить квизы", gottenQuizesError);
|
||||
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Не удалось получить квизы", error);
|
||||
|
||||
setQuizes(gottenQuizes);
|
||||
}
|
||||
};
|
||||
|
||||
@ -179,7 +182,9 @@ export default function Analytics() {
|
||||
},
|
||||
}}
|
||||
value={from}
|
||||
onChange={(date) => {setFrom(date ? date.startOf("day") : moment())}}
|
||||
onChange={(date) => {
|
||||
setFrom(date ? date.startOf("day") : moment());
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
@ -222,7 +227,9 @@ export default function Analytics() {
|
||||
},
|
||||
}}
|
||||
value={to}
|
||||
onChange={(date) => {setTo(date ? date.endOf("day") : moment())}}
|
||||
onChange={(date) => {
|
||||
setTo(date ? date.endOf("day") : moment());
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
@ -122,7 +122,7 @@ export const DesignFilling = ({
|
||||
width: "100%",
|
||||
borderRadius: "12px",
|
||||
height: "calc(100vh - 300px)",
|
||||
mb: "76px"
|
||||
mb: "76px",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
|
@ -158,11 +158,14 @@ export default function QuizInstallationCard() {
|
||||
<IconButton
|
||||
edge="end"
|
||||
sx={{ marginTop: "22px" }}
|
||||
onClick={() => navigator.clipboard.writeText( // TODO
|
||||
onClick={() =>
|
||||
navigator.clipboard.writeText(
|
||||
// TODO
|
||||
document.getElementById(
|
||||
"outlined-multiline-static"
|
||||
).value
|
||||
)}
|
||||
"outlined-multiline-static",
|
||||
).value,
|
||||
)
|
||||
}
|
||||
>
|
||||
<CopyIcon
|
||||
color={"#ffffff"}
|
||||
|
@ -136,8 +136,7 @@ export const VKPixelInstruction = () => {
|
||||
<ListItem>
|
||||
<Typography>
|
||||
• Посетитель отправил заявку с заполненным полем Х:
|
||||
<b>penaquiz-formfield-X</b>, где X — одно из полей.
|
||||
Например,
|
||||
<b>penaquiz-formfield-X</b>, где X — одно из полей. Например,
|
||||
</Typography>
|
||||
</ListItem>
|
||||
<List>
|
||||
@ -163,7 +162,8 @@ export const VKPixelInstruction = () => {
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Typography>
|
||||
<b>penaquiz</b>-formfield-text (это будет кастомное поле, которое вы настроили сами)
|
||||
<b>penaquiz</b>-formfield-text (это будет кастомное поле,
|
||||
которое вы настроили сами)
|
||||
</Typography>
|
||||
</ListItem>
|
||||
</List>
|
||||
|
@ -132,8 +132,7 @@ export const YandexInstruction = () => {
|
||||
<ListItem>
|
||||
<Typography>
|
||||
• Посетитель отправил заявку с заполненным полем Х:
|
||||
<b>penaquiz-formfield-X</b>, где X — одно из полей.
|
||||
Например,
|
||||
<b>penaquiz-formfield-X</b>, где X — одно из полей. Например,
|
||||
</Typography>
|
||||
</ListItem>
|
||||
<List>
|
||||
@ -159,7 +158,8 @@ export const YandexInstruction = () => {
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Typography>
|
||||
<b>penaquiz</b>-formfield-text (это будет кастомное поле, которое вы настроили сами)
|
||||
<b>penaquiz</b>-formfield-text (это будет кастомное поле,
|
||||
которое вы настроили сами)
|
||||
</Typography>
|
||||
</ListItem>
|
||||
</List>
|
||||
|
@ -168,10 +168,7 @@ export default function HowItWorks() {
|
||||
}}
|
||||
>
|
||||
<Icon21 />
|
||||
<Typography fontSize="18px">
|
||||
{" "}
|
||||
на сайте
|
||||
</Typography>
|
||||
<Typography fontSize="18px"> на сайте</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
|
@ -99,283 +99,290 @@ const QuestionPageCardTitle = memo<Props>(function ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
padding: isMobile ? "10px" : "20px 10px 20px 20px",
|
||||
flexDirection: "row",
|
||||
flexWrap: isMobile && isExpanded ? "wrap" : "nowrap",
|
||||
}}
|
||||
>
|
||||
<FormControl
|
||||
variant="standard"
|
||||
sx={{
|
||||
p: 0,
|
||||
maxWidth: isTablet ? "549px" : "640px",
|
||||
width: "100%",
|
||||
marginRight: isMobile ? "0px" : "16.1px",
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
flexBasis: isMobile && isExpanded ? "calc(100% - 30px)" : null,
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
id="questionTitle"
|
||||
value={title}
|
||||
placeholder={"Заголовок вопроса"}
|
||||
onChange={({ target }) => setTitle(target.value || " ")}
|
||||
onFocus={handleInputFocus}
|
||||
onBlur={handleInputBlur}
|
||||
inputProps={{
|
||||
maxLength: maxLengthTextField,
|
||||
}}
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<Box>
|
||||
<InputAdornment
|
||||
ref={anchorRef}
|
||||
position="start"
|
||||
sx={{ cursor: "pointer" }}
|
||||
onClick={() => setOpen((isOpened) => !isOpened)}
|
||||
>
|
||||
{IconAndrom(isExpanded, questionType)}
|
||||
</InputAdornment>
|
||||
<ChooseAnswerModal
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
anchorRef={anchorRef}
|
||||
questionId={questionId}
|
||||
questionContentId={questionContentId}
|
||||
questionType={questionType}
|
||||
/>
|
||||
</Box>
|
||||
),
|
||||
endAdornment: isTextFieldtActive &&
|
||||
title.length >= maxLengthTextField - 7 && (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
marginTop: "5px",
|
||||
marginLeft: "auto",
|
||||
position: "absolute",
|
||||
bottom: "-28px",
|
||||
right: "0",
|
||||
}}
|
||||
>
|
||||
<Typography fontSize="14px">{title.length}</Typography>
|
||||
<span>/</span>
|
||||
<Typography fontSize="14px">
|
||||
{maxLengthTextField}
|
||||
</Typography>
|
||||
</Box>
|
||||
),
|
||||
}}
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
margin: isMobile ? "10px 0" : 0,
|
||||
"& .MuiInputBase-root": {
|
||||
color: "#000000",
|
||||
backgroundColor: isExpanded
|
||||
? theme.palette.background.default
|
||||
: "transparent",
|
||||
height: "48px",
|
||||
borderRadius: "10px",
|
||||
".MuiOutlinedInput-notchedOutline": {
|
||||
borderWidth: "1px !important",
|
||||
border: !isExpanded ? "none" : null,
|
||||
},
|
||||
"& .MuiInputBase-input::placeholder": {
|
||||
color: "#4D4D4D",
|
||||
opacity: 0.8,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<IconButton
|
||||
disableRipple
|
||||
sx={{
|
||||
order: isMobile && isExpanded ? "0" : "1",
|
||||
padding: isMobile ? "0" : "0 5px",
|
||||
right: isMobile ? "0" : null,
|
||||
bottom: isMobile ? "0" : null,
|
||||
marginLeft: !isMobile && isExpanded ? "10px" : null,
|
||||
}}
|
||||
{...draggableProps}
|
||||
onMouseDown={collapseAllQuestions}
|
||||
onTouchStart={collapseAllQuestions}
|
||||
>
|
||||
<PointsIcon style={{ color: "#4D4D4D", fontSize: "30px" }} />
|
||||
</IconButton>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-end",
|
||||
width: isMobile ? "100%" : "auto",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
sx={{ padding: "0", margin: "5px" }}
|
||||
disableRipple
|
||||
data-cy="expand-question"
|
||||
onClick={() => toggleExpandQuestion(questionId)}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ArrowDownIcon
|
||||
style={{
|
||||
width: "18px",
|
||||
color: "#4D4D4D",
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<ExpandLessIcon
|
||||
sx={{
|
||||
boxSizing: "border-box",
|
||||
fill: theme.palette.brightPurple.main,
|
||||
background: "#FFF",
|
||||
borderRadius: "6px",
|
||||
height: "30px",
|
||||
width: "30px",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</IconButton>
|
||||
{isExpanded ? (
|
||||
<></>
|
||||
) : (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
height: "30px",
|
||||
borderRight: "solid 1px #4D4D4D",
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
sx={{ padding: "0" }}
|
||||
onClick={() => copyQuestion(questionId, quizId)}
|
||||
>
|
||||
<CopyIcon style={{ color: theme.palette.brightPurple.main }} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
borderRadius: "6px",
|
||||
padding: "0",
|
||||
margin: "0 5px 0 10px",
|
||||
}}
|
||||
onClick={() => {
|
||||
if (questionType === null) {
|
||||
deleteQuestion(questionId);
|
||||
return;
|
||||
}
|
||||
if (questionHasParent) {
|
||||
setOpenDelete(true);
|
||||
} else {
|
||||
deleteQuestionWithTimeout(questionId, () =>
|
||||
DeleteFunction(questionId),
|
||||
);
|
||||
}
|
||||
}}
|
||||
data-cy="delete-question"
|
||||
>
|
||||
<DeleteIcon
|
||||
style={{ color: theme.palette.brightPurple.main }}
|
||||
/>
|
||||
</IconButton>
|
||||
<Modal open={openDelete} onClose={() => setOpenDelete(false)}>
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
padding: "30px",
|
||||
borderRadius: "10px",
|
||||
background: "#FFFFFF",
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" sx={{ textAlign: "center" }}>
|
||||
Вы удаляете вопрос, участвующий в ветвлении. Все его потомки
|
||||
потеряют данные ветвления. Вы уверены, что хотите удалить
|
||||
вопрос?
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
marginTop: "30px",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
gap: "15px",
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
sx={{ minWidth: "150px" }}
|
||||
onClick={() => setOpenDelete(false)}
|
||||
>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
sx={{ minWidth: "150px" }}
|
||||
onClick={() => {
|
||||
deleteQuestionWithTimeout(questionId, () =>
|
||||
DeleteFunction(questionId),
|
||||
);
|
||||
}}
|
||||
>
|
||||
Подтвердить
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
</Box>
|
||||
)}
|
||||
{page !== null && (
|
||||
<Box
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
padding: isMobile ? "10px" : "20px 10px 20px 20px",
|
||||
flexDirection: "row",
|
||||
flexWrap: isMobile && isExpanded ? "wrap" : "nowrap",
|
||||
}}
|
||||
>
|
||||
<FormControl
|
||||
variant="standard"
|
||||
sx={{
|
||||
p: 0,
|
||||
maxWidth: isTablet ? "549px" : "640px",
|
||||
width: "100%",
|
||||
marginRight: isMobile ? "0px" : "16.1px",
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
flexBasis: isMobile && isExpanded ? "calc(100% - 30px)" : null,
|
||||
}}
|
||||
justifyContent: "center",
|
||||
height: "30px",
|
||||
width: "30px",
|
||||
marginLeft: "3px",
|
||||
borderRadius: "50%",
|
||||
fontSize: "16px",
|
||||
color: isExpanded ? theme.palette.brightPurple.main : "#FFF",
|
||||
background: isExpanded
|
||||
? "#EEE4FC"
|
||||
: theme.palette.brightPurple.main,
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
id="questionTitle"
|
||||
value={title}
|
||||
placeholder={"Заголовок вопроса"}
|
||||
onChange={({ target }) => setTitle(target.value || " ")}
|
||||
onFocus={handleInputFocus}
|
||||
onBlur={handleInputBlur}
|
||||
inputProps={{
|
||||
maxLength: maxLengthTextField,
|
||||
}}
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<Box>
|
||||
<InputAdornment
|
||||
ref={anchorRef}
|
||||
position="start"
|
||||
sx={{ cursor: "pointer" }}
|
||||
onClick={() => setOpen((isOpened) => !isOpened)}
|
||||
>
|
||||
{IconAndrom(isExpanded, questionType)}
|
||||
</InputAdornment>
|
||||
<ChooseAnswerModal
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
anchorRef={anchorRef}
|
||||
questionId={questionId}
|
||||
questionContentId={questionContentId}
|
||||
questionType={questionType}
|
||||
/>
|
||||
</Box>
|
||||
),
|
||||
endAdornment: isTextFieldtActive &&
|
||||
title.length >= maxLengthTextField - 7 && (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
marginTop: "5px",
|
||||
marginLeft: "auto",
|
||||
position: "absolute",
|
||||
bottom: "-28px",
|
||||
right: "0",
|
||||
}}
|
||||
>
|
||||
<Typography fontSize="14px">{title.length}</Typography>
|
||||
<span>/</span>
|
||||
<Typography fontSize="14px">{maxLengthTextField}</Typography>
|
||||
</Box>
|
||||
),
|
||||
}}
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
margin: isMobile ? "10px 0" : 0,
|
||||
"& .MuiInputBase-root": {
|
||||
color: "#000000",
|
||||
backgroundColor: isExpanded
|
||||
? theme.palette.background.default
|
||||
: "transparent",
|
||||
height: "48px",
|
||||
borderRadius: "10px",
|
||||
".MuiOutlinedInput-notchedOutline": {
|
||||
borderWidth: "1px !important",
|
||||
border: !isExpanded ? "none" : null,
|
||||
},
|
||||
"& .MuiInputBase-input::placeholder": {
|
||||
color: "#4D4D4D",
|
||||
opacity: 0.8,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<IconButton
|
||||
disableRipple
|
||||
sx={{
|
||||
order: isMobile && isExpanded ? "0" : "1",
|
||||
padding: isMobile ? "0" : "0 5px",
|
||||
right: isMobile ? "0" : null,
|
||||
bottom: isMobile ? "0" : null,
|
||||
marginLeft: !isMobile && isExpanded ? "10px" : null,
|
||||
}}
|
||||
{...draggableProps}
|
||||
onMouseDown={collapseAllQuestions}
|
||||
onTouchStart={collapseAllQuestions}
|
||||
>
|
||||
<PointsIcon style={{ color: "#4D4D4D", fontSize: "30px" }} />
|
||||
</IconButton>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-end",
|
||||
width: isMobile ? "100%" : "auto",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
sx={{ padding: "0", margin: "5px" }}
|
||||
disableRipple
|
||||
data-cy="expand-question"
|
||||
onClick={() => toggleExpandQuestion(questionId)}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ArrowDownIcon
|
||||
style={{
|
||||
width: "18px",
|
||||
color: "#4D4D4D",
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<ExpandLessIcon
|
||||
sx={{
|
||||
boxSizing: "border-box",
|
||||
fill: theme.palette.brightPurple.main,
|
||||
background: "#FFF",
|
||||
borderRadius: "6px",
|
||||
height: "30px",
|
||||
width: "30px",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</IconButton>
|
||||
{isExpanded ? (
|
||||
<></>
|
||||
) : (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
height: "30px",
|
||||
borderRight: "solid 1px #4D4D4D",
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
sx={{ padding: "0" }}
|
||||
onClick={() => copyQuestion(questionId, quizId)}
|
||||
>
|
||||
<CopyIcon style={{ color: theme.palette.brightPurple.main }} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
borderRadius: "6px",
|
||||
padding: "0",
|
||||
margin: "0 5px 0 10px",
|
||||
}}
|
||||
onClick={() => {
|
||||
if (questionType === null) {
|
||||
deleteQuestion(questionId);
|
||||
return;
|
||||
}
|
||||
if (questionHasParent) {
|
||||
setOpenDelete(true);
|
||||
} else {
|
||||
deleteQuestionWithTimeout(questionId, () =>
|
||||
DeleteFunction(questionId),
|
||||
);
|
||||
}
|
||||
}}
|
||||
data-cy="delete-question"
|
||||
>
|
||||
<DeleteIcon style={{ color: theme.palette.brightPurple.main }} />
|
||||
</IconButton>
|
||||
<Modal open={openDelete} onClose={() => setOpenDelete(false)}>
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
padding: "30px",
|
||||
borderRadius: "10px",
|
||||
background: "#FFFFFF",
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" sx={{ textAlign: "center" }}>
|
||||
Вы удаляете вопрос, участвующий в ветвлении. Все его потомки
|
||||
потеряют данные ветвления. Вы уверены, что хотите удалить
|
||||
вопрос?
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
marginTop: "30px",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
gap: "15px",
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
sx={{ minWidth: "150px" }}
|
||||
onClick={() => setOpenDelete(false)}
|
||||
>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
sx={{ minWidth: "150px" }}
|
||||
onClick={() => {
|
||||
deleteQuestionWithTimeout(questionId, () =>
|
||||
DeleteFunction(questionId),
|
||||
);
|
||||
}}
|
||||
>
|
||||
Подтвердить
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
</Box>
|
||||
)}
|
||||
{page !== null && (
|
||||
<Box
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: "30px",
|
||||
width: "30px",
|
||||
marginLeft: "3px",
|
||||
borderRadius: "50%",
|
||||
fontSize: "16px",
|
||||
color: isExpanded ? theme.palette.brightPurple.main : "#FFF",
|
||||
background: isExpanded
|
||||
? "#EEE4FC"
|
||||
: theme.palette.brightPurple.main,
|
||||
}}
|
||||
>
|
||||
{page + 1}
|
||||
</Box>
|
||||
)}
|
||||
{page + 1}
|
||||
</Box>
|
||||
|
||||
)}
|
||||
</Box>
|
||||
{questionType !== null &&
|
||||
<Box sx={{
|
||||
</Box>
|
||||
{questionType !== null && (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
flexWrap: isMobile ? "wrap" : undefined,
|
||||
gap: "10px",
|
||||
padding: "0 20px 20px 20px"
|
||||
}}>
|
||||
<Typography>ID Вопроса</Typography>
|
||||
<Typography
|
||||
id={"id-copy"}
|
||||
>{questionBackendId}</Typography>
|
||||
<IconButton
|
||||
edge="end"
|
||||
onClick={() => navigator.clipboard.writeText(document.querySelector("#id-copy").innerText)
|
||||
}
|
||||
>
|
||||
<CopyIconPurple
|
||||
color={"#ffffff"}
|
||||
width={"30px"}
|
||||
bgcolor={theme.palette.brightPurple.main}
|
||||
/>
|
||||
</IconButton>
|
||||
</Box>}
|
||||
padding: "0 20px 20px 20px",
|
||||
}}
|
||||
>
|
||||
<Typography>ID Вопроса</Typography>
|
||||
<Typography id={"id-copy"}>{questionBackendId}</Typography>
|
||||
<IconButton
|
||||
edge="end"
|
||||
onClick={() =>
|
||||
navigator.clipboard.writeText(
|
||||
document.querySelector("#id-copy").innerText,
|
||||
)
|
||||
}
|
||||
>
|
||||
<CopyIconPurple
|
||||
color={"#ffffff"}
|
||||
width={"30px"}
|
||||
bgcolor={theme.palette.brightPurple.main}
|
||||
/>
|
||||
</IconButton>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
@ -13,26 +13,39 @@ export const useGetData = (filterNew: string, filterDate: string): void => {
|
||||
|
||||
useEffect(() => {
|
||||
const getData = async (): Promise<void> => {
|
||||
try {
|
||||
if (editQuizId !== null) {
|
||||
const quizes = await quizApi.getList();
|
||||
setQuizes(quizes);
|
||||
if (editQuizId !== null) {
|
||||
const [quizes, quizesError] = await quizApi.getList();
|
||||
|
||||
const questions = await questionApi.getList({ quiz_id: editQuizId });
|
||||
setQuestions(questions);
|
||||
|
||||
const result = await resultApi.getList(
|
||||
editQuizId,
|
||||
0,
|
||||
parseFilters(filterNew, filterDate),
|
||||
if (quizesError) {
|
||||
console.error(
|
||||
"An error occurred while receiving data: ",
|
||||
quizesError,
|
||||
);
|
||||
if (result.total_count === 0) {
|
||||
console.error("No results found");
|
||||
}
|
||||
setResults(result);
|
||||
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("An error occurred while receiving data: ", error);
|
||||
|
||||
setQuizes(quizes);
|
||||
|
||||
const [questions, questionsError] = await questionApi.getList({
|
||||
quiz_id: editQuizId,
|
||||
});
|
||||
|
||||
if (questionsError) {
|
||||
return console.error(questionsError);
|
||||
}
|
||||
|
||||
setQuestions(questions);
|
||||
|
||||
const result = await resultApi.getList(
|
||||
editQuizId,
|
||||
0,
|
||||
parseFilters(filterNew, filterDate),
|
||||
);
|
||||
if (result.total_count === 0) {
|
||||
console.error("No results found");
|
||||
}
|
||||
setResults(result);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -9,16 +9,17 @@ import { useCurrentQuiz } from "@root/quizes/hooks";
|
||||
import CustomTextField from "@ui_kit/CustomTextField";
|
||||
|
||||
import {
|
||||
Box,
|
||||
IconButton,
|
||||
Paper,
|
||||
Button,
|
||||
Typography,
|
||||
TextField,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
FormControl,
|
||||
Popover, InputAdornment,
|
||||
Box,
|
||||
IconButton,
|
||||
Paper,
|
||||
Button,
|
||||
Typography,
|
||||
TextField,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
FormControl,
|
||||
Popover,
|
||||
InputAdornment,
|
||||
} from "@mui/material";
|
||||
import MiniButtonSetting from "@ui_kit/MiniButtonSetting";
|
||||
|
||||
@ -260,28 +261,31 @@ export const ResultCard = ({ resultContract, resultData }: Props) => {
|
||||
}}
|
||||
>
|
||||
<Box sx={{ p: "0 20px", pt: "10px" }}>
|
||||
<Box sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "10px",
|
||||
mb: "19px",
|
||||
}}>
|
||||
<Typography>ID результата</Typography>
|
||||
<Typography
|
||||
id={"id-copy"}
|
||||
>{resultData.backendId}</Typography>
|
||||
<IconButton
|
||||
edge="end"
|
||||
onClick={() => navigator.clipboard.writeText(document.querySelector("#id-copy").innerText)
|
||||
}
|
||||
>
|
||||
<CopyIcon
|
||||
color={"#ffffff"}
|
||||
width={"30px"}
|
||||
bgcolor={theme.palette.brightPurple.main}
|
||||
/>
|
||||
</IconButton>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "10px",
|
||||
mb: "19px",
|
||||
}}
|
||||
>
|
||||
<Typography>ID результата</Typography>
|
||||
<Typography id={"id-copy"}>{resultData.backendId}</Typography>
|
||||
<IconButton
|
||||
edge="end"
|
||||
onClick={() =>
|
||||
navigator.clipboard.writeText(
|
||||
document.querySelector("#id-copy").innerText,
|
||||
)
|
||||
}
|
||||
>
|
||||
<CopyIcon
|
||||
color={"#ffffff"}
|
||||
width={"30px"}
|
||||
bgcolor={theme.palette.brightPurple.main}
|
||||
/>
|
||||
</IconButton>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
|
@ -2,7 +2,7 @@ import { logout } from "@api/auth";
|
||||
import { activatePromocode } from "@api/promocode";
|
||||
import type { Tariff } from "@frontend/kitui";
|
||||
import { useToken } from "@frontend/kitui";
|
||||
import makeRequest from "@api/makeRequest";
|
||||
import { makeRequest } from "@api/makeRequest";
|
||||
import ArrowLeft from "@icons/questionsPage/arrowLeft";
|
||||
import type { GetTariffsResponse } from "@model/tariff";
|
||||
import {
|
||||
@ -200,7 +200,10 @@ function TariffPage() {
|
||||
setDiscounts(discounts);
|
||||
}
|
||||
})
|
||||
.catch((error) => {if (error.message !== "" && typeof(error.message)==="string") enqueueSnackbar(error.message)})
|
||||
.catch((error) => {
|
||||
if (error.message !== "" && typeof error.message === "string")
|
||||
enqueueSnackbar(error.message);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -63,7 +63,9 @@ export const createTariffElements = (
|
||||
lineHeight: "21px",
|
||||
}}
|
||||
>
|
||||
{currencyFormatter.format(Math.trunc(priceBeforeDiscounts) / 100)}
|
||||
{currencyFormatter.format(
|
||||
Math.trunc(priceBeforeDiscounts) / 100,
|
||||
)}
|
||||
</Typography>
|
||||
)}
|
||||
<Typography
|
||||
@ -74,7 +76,9 @@ export const createTariffElements = (
|
||||
color: "#4D4D4D",
|
||||
}}
|
||||
>
|
||||
{currencyFormatter.format(Math.trunc(priceAfterDiscounts) / 100)}
|
||||
{currencyFormatter.format(
|
||||
Math.trunc(priceAfterDiscounts) / 100,
|
||||
)}
|
||||
</Typography>
|
||||
</>
|
||||
}
|
||||
|
@ -57,9 +57,9 @@ export default function ViewPublicationPage() {
|
||||
if (questionsIsLoading) return null;
|
||||
|
||||
if (!quiz) throw new Error("Quiz not found");
|
||||
if (!rawQuestions) throw new Error("Questions not found");
|
||||
if (!rawQuestions?.[0]) throw new Error("Questions not found");
|
||||
|
||||
const questions = rawQuestions.map(rawQuestionToQuestion);
|
||||
const questions = rawQuestions[0].map(rawQuestionToQuestion);
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
@ -68,7 +68,7 @@ export default function ViewPublicationPage() {
|
||||
>
|
||||
<QuizAnswerer
|
||||
quizSettings={{
|
||||
cnt: rawQuestions?.length,
|
||||
cnt: rawQuestions[0]?.length,
|
||||
questions,
|
||||
recentlyCompleted: false,
|
||||
settings: {
|
||||
|
@ -18,7 +18,7 @@ import { object, string } from "yup";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useUserStore } from "@root/user";
|
||||
|
||||
import makeRequest from "@api/makeRequest";
|
||||
import { makeRequest } from "@api/makeRequest";
|
||||
import { setAuthToken } from "@frontend/kitui";
|
||||
import { parseAxiosError } from "@utils/parse-error";
|
||||
interface Values {
|
||||
|
@ -15,7 +15,7 @@ import {
|
||||
import { deleteQuiz, setEditQuizId } from "@root/quizes/actions";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { inCart } from "../../pages/Tariffs/Tariffs";
|
||||
import makeRequest from "@api/makeRequest";
|
||||
import { makeRequest } from "@api/makeRequest";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { useDomainDefine } from "@utils/hooks/useDomainDefine";
|
||||
import CopyIcon from "@icons/CopyIcon";
|
||||
|
@ -38,11 +38,11 @@ export default function Main({ sidebar, header, footer, Page }: Props) {
|
||||
const { isTestServer } = useDomainDefine();
|
||||
useEffect(() => {
|
||||
const getData = async () => {
|
||||
const quizes = await quizApi.getList();
|
||||
const [quizes] = await quizApi.getList();
|
||||
setQuizes(quizes);
|
||||
|
||||
if (editQuizId) {
|
||||
const questions = await questionApi.getList({ quiz_id: editQuizId });
|
||||
const [questions] = await questionApi.getList({ quiz_id: editQuizId });
|
||||
|
||||
setQuestions(questions);
|
||||
//Всегда должен существовать хоть 1 резулт - "line"
|
||||
|
@ -58,12 +58,14 @@ export const sendContactForm = async (): Promise<string | null> => {
|
||||
|
||||
try {
|
||||
useContactFormStore.setState({ isSubmitDisabled: true });
|
||||
const response = await sendContactFormRequest({
|
||||
const [_, error, status] = await sendContactFormRequest({
|
||||
contact,
|
||||
whoami: mail,
|
||||
});
|
||||
|
||||
if (response.status !== 200) throw new Error(response.statusText);
|
||||
if (status !== 200) {
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
useContactFormStore.setState({ ...initialState });
|
||||
|
||||
|
@ -296,35 +296,36 @@ export const updateQuestion = async <T = AnyTypedQuizQuestion>(
|
||||
if (!q) return;
|
||||
if (q.type === null)
|
||||
throw new Error("Cannot send update request for untyped question");
|
||||
try {
|
||||
const response = await questionApi.edit(
|
||||
questionToEditQuestionRequest(replaceEmptyLinesToSpace(q)),
|
||||
);
|
||||
rollbackQuestions = useQuestionsStore.getState();
|
||||
|
||||
//Если мы делаем листочек веточкой - удаляем созданный к нему результ
|
||||
const questionResult = useQuestionsStore
|
||||
.getState()
|
||||
.questions.find(
|
||||
(questionResult) =>
|
||||
questionResult.type === "result" &&
|
||||
questionResult.content.rule.parentId === q.content.id,
|
||||
);
|
||||
if (questionResult && q.content.rule.default.length !== 0)
|
||||
deleteQuestion(questionResult.quizId);
|
||||
|
||||
if (q.backendId !== response.updated) {
|
||||
console.warn(
|
||||
`Question backend id has changed from ${q.backendId} to ${response.updated}`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (isAxiosCanceledError(error)) return;
|
||||
const [response, editError] = await questionApi.edit(
|
||||
questionToEditQuestionRequest(replaceEmptyLinesToSpace(q)),
|
||||
);
|
||||
|
||||
if (editError) {
|
||||
useQuestionsStore.setState(rollbackQuestions);
|
||||
|
||||
devlog("Error editing question", { error, questionId });
|
||||
enqueueSnackbar("Не удалось сохранить вопрос");
|
||||
devlog("Error editing question", { editError, questionId });
|
||||
enqueueSnackbar(editError);
|
||||
}
|
||||
|
||||
rollbackQuestions = useQuestionsStore.getState();
|
||||
|
||||
//Если мы делаем листочек веточкой - удаляем созданный к нему результ
|
||||
const questionResult = useQuestionsStore
|
||||
.getState()
|
||||
.questions.find(
|
||||
(questionResult) =>
|
||||
questionResult.type === "result" &&
|
||||
questionResult.content.rule.parentId === q.content.id,
|
||||
);
|
||||
|
||||
if (questionResult && q.content.rule.default.length !== 0) {
|
||||
deleteQuestion(String(questionResult.quizId));
|
||||
}
|
||||
|
||||
if (q.backendId !== response?.updated) {
|
||||
console.warn(
|
||||
`Question backend id has changed from ${q.backendId} to ${response?.updated}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@ -430,28 +431,32 @@ export const uploadQuestionImage = async (
|
||||
.questions.find((q) => q.id === questionId);
|
||||
if (!question || !quizQid) return;
|
||||
|
||||
try {
|
||||
const response = await quizApi.addImages(question.quizId, blob);
|
||||
|
||||
const values = Object.values(response);
|
||||
if (values.length !== 1) {
|
||||
console.warn("Error uploading image");
|
||||
return;
|
||||
}
|
||||
|
||||
const imageId = values[0];
|
||||
const imageUrl = `https://storage.yandexcloud.net/squizimages/${quizQid}/${imageId}`;
|
||||
|
||||
updateQuestion(questionId, (question) => {
|
||||
updateFn(question, imageUrl);
|
||||
});
|
||||
|
||||
return imageUrl;
|
||||
} catch (error) {
|
||||
devlog("Error uploading question image", error);
|
||||
const [images, addImagesError] = await quizApi.addImages(
|
||||
question.quizId,
|
||||
blob,
|
||||
);
|
||||
|
||||
if (addImagesError || !images) {
|
||||
devlog("Error uploading question image", addImagesError);
|
||||
enqueueSnackbar("Не удалось загрузить изображение");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const values = Object.values(images);
|
||||
if (values.length !== 1) {
|
||||
console.warn("Error uploading image");
|
||||
return;
|
||||
}
|
||||
|
||||
const imageId = values[0];
|
||||
const imageUrl = `https://storage.yandexcloud.net/squizimages/${quizQid}/${imageId}`;
|
||||
|
||||
updateQuestion(questionId, (question) => {
|
||||
updateFn(question, imageUrl);
|
||||
});
|
||||
|
||||
return imageUrl;
|
||||
};
|
||||
|
||||
export const setQuestionInnerName = (questionId: string, name: string) => {
|
||||
@ -489,40 +494,42 @@ export const createTypedQuestion = async (
|
||||
(q) => q.type === "result" || q.type === null,
|
||||
).length;
|
||||
|
||||
try {
|
||||
const createdQuestion = await questionApi.create({
|
||||
quiz_id: question.quizId,
|
||||
type,
|
||||
title: question.title,
|
||||
description: question.description,
|
||||
page: questions.length - untypedOrResultQuestionsLength,
|
||||
required: false,
|
||||
content: JSON.stringify(defaultQuestionByType[type].content),
|
||||
});
|
||||
const [createdQuestion, createdQuestionError] = await questionApi.create({
|
||||
quiz_id: question.quizId,
|
||||
type,
|
||||
title: question.title,
|
||||
description: question.description,
|
||||
page: questions.length - untypedOrResultQuestionsLength,
|
||||
required: false,
|
||||
content: JSON.stringify(defaultQuestionByType[type].content),
|
||||
});
|
||||
|
||||
setProducedState(
|
||||
(state) => {
|
||||
const questionIndex = state.questions.findIndex(
|
||||
(q) => q.id === questionId,
|
||||
);
|
||||
if (questionIndex !== -1)
|
||||
state.questions.splice(
|
||||
questionIndex,
|
||||
1,
|
||||
rawQuestionToQuestion(createdQuestion),
|
||||
);
|
||||
},
|
||||
{
|
||||
type: "createTypedQuestion",
|
||||
question,
|
||||
},
|
||||
);
|
||||
if (createdQuestionError || !createdQuestion) {
|
||||
devlog("Error creating question", createdQuestionError);
|
||||
enqueueSnackbar(createdQuestionError);
|
||||
|
||||
updateQuestionOrders();
|
||||
} catch (error) {
|
||||
devlog("Error creating question", error);
|
||||
enqueueSnackbar("Не удалось создать вопрос");
|
||||
return;
|
||||
}
|
||||
|
||||
setProducedState(
|
||||
(state) => {
|
||||
const questionIndex = state.questions.findIndex(
|
||||
(q) => q.id === questionId,
|
||||
);
|
||||
if (questionIndex !== -1)
|
||||
state.questions.splice(
|
||||
questionIndex,
|
||||
1,
|
||||
rawQuestionToQuestion(createdQuestion),
|
||||
);
|
||||
},
|
||||
{
|
||||
type: "createTypedQuestion",
|
||||
question,
|
||||
},
|
||||
);
|
||||
|
||||
updateQuestionOrders();
|
||||
});
|
||||
|
||||
export const deleteQuestion = async (questionId: string) =>
|
||||
@ -537,16 +544,17 @@ export const deleteQuestion = async (questionId: string) =>
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await questionApi.delete(question.backendId);
|
||||
const [_, deleteError] = await questionApi.delete(question.backendId);
|
||||
|
||||
removeQuestion(questionId);
|
||||
|
||||
updateQuestionOrders();
|
||||
} catch (error) {
|
||||
devlog("Error deleting question", error);
|
||||
if (deleteError) {
|
||||
devlog("Error deleting question", deleteError);
|
||||
enqueueSnackbar("Не удалось удалить вопрос");
|
||||
return;
|
||||
}
|
||||
|
||||
removeQuestion(questionId);
|
||||
|
||||
updateQuestionOrders();
|
||||
});
|
||||
|
||||
export const copyQuestion = async (questionId: string, quizId: number) => {
|
||||
@ -587,39 +595,39 @@ export const copyQuestion = async (questionId: string, quizId: number) => {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { updated: newQuestionId } = await questionApi.copy(
|
||||
question.backendId,
|
||||
quizId,
|
||||
);
|
||||
const [copiedQuestionResult, copiedQuestionError] =
|
||||
await questionApi.copy(question.backendId, quizId);
|
||||
|
||||
const copiedQuestion = structuredClone(question);
|
||||
copiedQuestion.backendId = newQuestionId;
|
||||
copiedQuestion.id = frontId;
|
||||
copiedQuestion.content.id = frontId;
|
||||
copiedQuestion.content.rule = {
|
||||
main: [],
|
||||
parentId: "",
|
||||
default: "",
|
||||
children: [],
|
||||
};
|
||||
if (copiedQuestionError || !copiedQuestionResult) {
|
||||
devlog("Error copying question", copiedQuestionError);
|
||||
enqueueSnackbar(copiedQuestionError);
|
||||
|
||||
setProducedState(
|
||||
(state) => {
|
||||
state.questions.push(copiedQuestion);
|
||||
},
|
||||
{
|
||||
type: "copyQuestion",
|
||||
questionId,
|
||||
quizId,
|
||||
},
|
||||
);
|
||||
|
||||
updateQuestionOrders();
|
||||
} catch (error) {
|
||||
devlog("Error copying question", error);
|
||||
enqueueSnackbar("Не удалось скопировать вопрос");
|
||||
return;
|
||||
}
|
||||
|
||||
const copiedQuestion = structuredClone(question);
|
||||
copiedQuestion.backendId = copiedQuestionResult.updated;
|
||||
copiedQuestion.id = frontId;
|
||||
copiedQuestion.content.id = frontId;
|
||||
copiedQuestion.content.rule = {
|
||||
main: [],
|
||||
parentId: "",
|
||||
default: "",
|
||||
children: [],
|
||||
};
|
||||
|
||||
setProducedState(
|
||||
(state) => {
|
||||
state.questions.push(copiedQuestion);
|
||||
},
|
||||
{
|
||||
type: "copyQuestion",
|
||||
questionId,
|
||||
quizId,
|
||||
},
|
||||
);
|
||||
|
||||
updateQuestionOrders();
|
||||
},
|
||||
);
|
||||
};
|
||||
@ -704,30 +712,30 @@ export const createResult = async (
|
||||
);
|
||||
content.rule.parentId = parentContentId;
|
||||
|
||||
try {
|
||||
const createdQuestion: RawQuestion = await questionApi.create({
|
||||
quiz_id: quizId,
|
||||
type: "result",
|
||||
title: "",
|
||||
description: "",
|
||||
page: 101,
|
||||
required: true,
|
||||
content: JSON.stringify(content),
|
||||
});
|
||||
const [createdQuestion, createdQuestionError] = await questionApi.create({
|
||||
quiz_id: quizId,
|
||||
type: "result",
|
||||
title: "",
|
||||
description: "",
|
||||
page: 101,
|
||||
required: true,
|
||||
content: JSON.stringify(content),
|
||||
});
|
||||
|
||||
setProducedState(
|
||||
(state) => {
|
||||
state.questions.push(rawQuestionToQuestion(createdQuestion));
|
||||
},
|
||||
{
|
||||
type: "createBackResult",
|
||||
createdQuestion,
|
||||
},
|
||||
);
|
||||
return createdQuestion;
|
||||
} catch (error) {
|
||||
devlog("Error creating question", error);
|
||||
enqueueSnackbar("Не удалось создать вопрос");
|
||||
if (createdQuestionError || !createdQuestion) {
|
||||
throw new Error(createdQuestionError);
|
||||
|
||||
devlog("Error creating question", createdQuestionError);
|
||||
enqueueSnackbar(createdQuestionError);
|
||||
}
|
||||
|
||||
setProducedState(
|
||||
(state) => {
|
||||
state.questions.push(rawQuestionToQuestion(createdQuestion));
|
||||
},
|
||||
{ type: "createBackResult", createdQuestion },
|
||||
);
|
||||
|
||||
return createdQuestion;
|
||||
}
|
||||
});
|
||||
|
@ -14,7 +14,7 @@ export function useQuestions() {
|
||||
["questions", quiz?.backendId],
|
||||
([, id]) => questionApi.getList({ quiz_id: id }),
|
||||
{
|
||||
onSuccess: setQuestions,
|
||||
onSuccess: ([questions]) => setQuestions(questions),
|
||||
onError: (error) => {
|
||||
const message = isAxiosError<string>(error)
|
||||
? error.response?.data ?? ""
|
||||
|
@ -155,43 +155,44 @@ export const updateQuiz = (
|
||||
|
||||
clearTimeout(requestTimeoutId);
|
||||
requestTimeoutId = setTimeout(async () => {
|
||||
requestQueue
|
||||
.enqueue(`updateQuiz-${quizId}`, async () => {
|
||||
const quiz = useQuizStore
|
||||
.getState()
|
||||
.quizes.find((q) => q.id === quizId);
|
||||
if (!quiz) return;
|
||||
requestQueue.enqueue(`updateQuiz-${quizId}`, async () => {
|
||||
const quiz = useQuizStore.getState().quizes.find((q) => q.id === quizId);
|
||||
if (!quiz) return;
|
||||
|
||||
const response = await quizApi.edit(quizToEditQuizRequest(quiz));
|
||||
const [editedQuiz, editedQuizError] = await quizApi.edit(
|
||||
quizToEditQuizRequest(quiz),
|
||||
);
|
||||
|
||||
setQuizBackendId(quizId, response.updated);
|
||||
setEditQuizId(response.updated);
|
||||
})
|
||||
.catch((error) => {
|
||||
if (isAxiosCanceledError(error)) return;
|
||||
if (editedQuizError || !editedQuiz) {
|
||||
devlog("Error editing quiz", editedQuizError, quizId);
|
||||
enqueueSnackbar(editedQuizError);
|
||||
|
||||
devlog("Error editing quiz", error, quizId);
|
||||
enqueueSnackbar("Не удалось сохранить настройки quiz");
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setQuizBackendId(quizId, editedQuiz.updated);
|
||||
setEditQuizId(editedQuiz.updated);
|
||||
});
|
||||
}, REQUEST_DEBOUNCE);
|
||||
};
|
||||
|
||||
export const createQuiz = async (navigate: NavigateFunction) =>
|
||||
requestQueue.enqueue("createQuiz", async () => {
|
||||
try {
|
||||
const rawQuiz = await quizApi.create();
|
||||
const quiz = rawQuizToQuiz(rawQuiz);
|
||||
const [rawQuiz, createQuizError] = await quizApi.create();
|
||||
|
||||
addQuiz(quiz);
|
||||
setEditQuizId(quiz.backendId);
|
||||
navigate("/edit");
|
||||
createUntypedQuestion(rawQuiz.id);
|
||||
} catch (error) {
|
||||
devlog("Error creating quiz", error);
|
||||
if (createQuizError || !rawQuiz) {
|
||||
devlog("Error creating quiz", createQuizError);
|
||||
enqueueSnackbar(createQuizError);
|
||||
|
||||
const message = getMessageFromFetchError(error) ?? "";
|
||||
enqueueSnackbar(`Не удалось создать quiz. ${message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const quiz = rawQuizToQuiz(rawQuiz);
|
||||
|
||||
addQuiz(quiz);
|
||||
setEditQuizId(quiz.backendId);
|
||||
navigate("/edit");
|
||||
createUntypedQuestion(rawQuiz.id);
|
||||
});
|
||||
|
||||
export const deleteQuiz = async (quizId: string) =>
|
||||
@ -199,16 +200,17 @@ export const deleteQuiz = async (quizId: string) =>
|
||||
const quiz = useQuizStore.getState().quizes.find((q) => q.id === quizId);
|
||||
if (!quiz) return;
|
||||
|
||||
try {
|
||||
await quizApi.delete(quiz.backendId);
|
||||
const [_, deleteQuizError] = await quizApi.delete(quiz.backendId);
|
||||
|
||||
removeQuiz(quizId);
|
||||
} catch (error) {
|
||||
devlog("Error deleting quiz", error);
|
||||
if (deleteQuizError) {
|
||||
devlog("Error deleting quiz", deleteQuizError);
|
||||
|
||||
const message = getMessageFromFetchError(error) ?? "";
|
||||
enqueueSnackbar(`Не удалось удалить quiz. ${message}`);
|
||||
enqueueSnackbar(deleteQuizError);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
removeQuiz(quizId);
|
||||
});
|
||||
export const updateRootContentId = (quizId: string, id: string) => {
|
||||
if (id.length === 0) {
|
||||
@ -248,30 +250,28 @@ export const copyQuiz = async (quizId: string) =>
|
||||
const quiz = useQuizStore.getState().quizes.find((q) => q.id === quizId);
|
||||
if (!quiz) return;
|
||||
|
||||
try {
|
||||
const { updated } = await quizApi.copy(quiz.backendId);
|
||||
let newQuiz: Quiz = {
|
||||
...quiz,
|
||||
id: String(updated),
|
||||
session_count: 0,
|
||||
passed_count: 0,
|
||||
};
|
||||
const [copiedQuiz, copyError] = await quizApi.copy(quiz.backendId);
|
||||
|
||||
setProducedState(
|
||||
(state) => {
|
||||
state.quizes.unshift(newQuiz);
|
||||
},
|
||||
{
|
||||
type: "addQuiz",
|
||||
quiz,
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
devlog("Error copying quiz", error);
|
||||
if (copyError || !copiedQuiz) {
|
||||
devlog("Error copying quiz", copyError);
|
||||
enqueueSnackbar(copyError);
|
||||
|
||||
const message = getMessageFromFetchError(error) ?? "";
|
||||
enqueueSnackbar(`Не удалось скопировать quiz. ${message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
let newQuiz: Quiz = {
|
||||
...quiz,
|
||||
id: String(copiedQuiz.updated),
|
||||
session_count: 0,
|
||||
passed_count: 0,
|
||||
};
|
||||
|
||||
setProducedState(
|
||||
(state) => {
|
||||
state.quizes.unshift(newQuiz);
|
||||
},
|
||||
{ type: "addQuiz", quiz },
|
||||
);
|
||||
});
|
||||
|
||||
export const uploadQuizImage = async (
|
||||
@ -282,28 +282,32 @@ export const uploadQuizImage = async (
|
||||
const quiz = useQuizStore.getState().quizes.find((q) => q.id === quizId);
|
||||
if (!quiz) return;
|
||||
|
||||
try {
|
||||
const response = await quizApi.addImages(quiz.backendId, blob);
|
||||
const [addedImages, addImagesError] = await quizApi.addImages(
|
||||
quiz.backendId,
|
||||
blob,
|
||||
);
|
||||
|
||||
const values = Object.values(response);
|
||||
if (values.length !== 1) {
|
||||
console.warn("Error uploading image");
|
||||
return;
|
||||
}
|
||||
if (addImagesError || !addedImages) {
|
||||
devlog("Error uploading quiz image", addImagesError);
|
||||
enqueueSnackbar(addImagesError);
|
||||
|
||||
const imageId = values[0];
|
||||
|
||||
updateQuiz(quizId, (quiz) => {
|
||||
updateFn(
|
||||
quiz,
|
||||
`https://storage.yandexcloud.net/squizimages/${quiz.qid}/${imageId}`,
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
devlog("Error uploading quiz image", error);
|
||||
|
||||
enqueueSnackbar("Не удалось загрузить изображение");
|
||||
return;
|
||||
}
|
||||
|
||||
const values = Object.values(addedImages);
|
||||
if (values.length !== 1) {
|
||||
console.warn("Error uploading image");
|
||||
return;
|
||||
}
|
||||
|
||||
const imageId = values[0];
|
||||
|
||||
updateQuiz(quizId, (quiz) => {
|
||||
updateFn(
|
||||
quiz,
|
||||
`https://storage.yandexcloud.net/squizimages/${quiz.qid}/${imageId}`,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
function setProducedState<A extends string | { type: unknown }>(
|
||||
|
@ -11,7 +11,7 @@ export function useQuizes() {
|
||||
"quizes",
|
||||
() => quizApi.getList(),
|
||||
{
|
||||
onSuccess: setQuizes,
|
||||
onSuccess: ([quizes]) => setQuizes(quizes),
|
||||
onError: (error: unknown) => {
|
||||
const message = isAxiosError<string>(error)
|
||||
? error.response?.data ?? ""
|
||||
|
@ -96,13 +96,15 @@ export const ExportResults = async (
|
||||
parseFilters(filterNew, filterDate),
|
||||
);
|
||||
|
||||
console.log(typeof data)
|
||||
console.log(typeof data);
|
||||
|
||||
const blob = new Blob([data as BlobPart], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8" });
|
||||
const blob = new Blob([data as BlobPart], {
|
||||
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8",
|
||||
});
|
||||
|
||||
const link = document.createElement("a");
|
||||
link.href = window.URL.createObjectURL(data as Blob);
|
||||
console.log(link)
|
||||
console.log(link);
|
||||
link.download = `report_${new Date().getTime()}.xlsx`;
|
||||
link.click();
|
||||
} catch (nativeError) {
|
||||
|
@ -1,18 +1,28 @@
|
||||
import {useEffect, useLayoutEffect, useRef, useState} from "react";
|
||||
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||
import { Box, Button, Modal, Typography } from "@mui/material";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { mutate } from "swr";
|
||||
|
||||
import makeRequest from "@api/makeRequest";
|
||||
import { makeRequest } from "@api/makeRequest";
|
||||
import { getDiscounts } from "@api/discounts";
|
||||
|
||||
import {clearUserData, OriginalUserAccount, setUserAccount, useUserStore} from "@root/user";
|
||||
import {
|
||||
clearUserData,
|
||||
OriginalUserAccount,
|
||||
setUserAccount,
|
||||
useUserStore,
|
||||
} from "@root/user";
|
||||
import { parseAxiosError } from "@utils/parse-error";
|
||||
import { useUserAccountFetcher } from "../App";
|
||||
import type { Discount } from "@model/discounts";
|
||||
import {clearAuthToken, createUserAccount, devlog, getMessageFromFetchError} from "@frontend/kitui";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
import {isAxiosError} from "axios";
|
||||
import {
|
||||
clearAuthToken,
|
||||
createUserAccount,
|
||||
devlog,
|
||||
getMessageFromFetchError,
|
||||
} from "@frontend/kitui";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { isAxiosError } from "axios";
|
||||
|
||||
export function CheckFastlink() {
|
||||
const user = useUserStore();
|
||||
@ -21,9 +31,7 @@ export function CheckFastlink() {
|
||||
const [discounts, setDiscounts] = useState<Discount[]>([]);
|
||||
const [askToChange, setAskToChange] = useState(false);
|
||||
const [promocode, setPromocode] = useState("");
|
||||
console.log(
|
||||
user.userAccount,
|
||||
user.customerAccount)
|
||||
console.log(user.userAccount, user.customerAccount);
|
||||
useEffect(() => {
|
||||
const get = async () => {
|
||||
if (!user.userId) {
|
||||
@ -53,7 +61,11 @@ export function CheckFastlink() {
|
||||
contentType: true,
|
||||
body: { fastLink: promocode },
|
||||
});
|
||||
enqueueSnackbar(response.greetings !== "" ? response.greetings : "Промокод успешно активирован");
|
||||
enqueueSnackbar(
|
||||
response.greetings !== ""
|
||||
? response.greetings
|
||||
: "Промокод успешно активирован",
|
||||
);
|
||||
localStorage.setItem("fl", "");
|
||||
const controller = new AbortController();
|
||||
const responseAccount = await makeRequest<never, any>({
|
||||
@ -63,8 +75,8 @@ export function CheckFastlink() {
|
||||
useToken: true,
|
||||
withCredentials: false,
|
||||
signal: controller.signal,
|
||||
})
|
||||
setUserAccount(responseAccount)
|
||||
});
|
||||
setUserAccount(responseAccount);
|
||||
mutate("discounts");
|
||||
return response.greetings;
|
||||
} catch (nativeError) {
|
||||
@ -104,8 +116,12 @@ export function CheckFastlink() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [user.userId, discounts, user.customerAccount?.createdAt, user.userAccount?.created_at]);
|
||||
|
||||
}, [
|
||||
user.userId,
|
||||
discounts,
|
||||
user.customerAccount?.createdAt,
|
||||
user.userAccount?.created_at,
|
||||
]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
|
@ -5,7 +5,7 @@ import {
|
||||
useTicketMessages,
|
||||
useTicketsFetcher,
|
||||
} from "@frontend/kitui";
|
||||
import makeRequest from "@api/makeRequest";
|
||||
import { makeRequest } from "@api/makeRequest";
|
||||
import FloatingSupportChat from "./FloatingSupportChat";
|
||||
import { useUserStore } from "@root/user";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
@ -123,7 +123,7 @@ export default () => {
|
||||
const message = getMessageFromFetchError(error);
|
||||
if (message) enqueueSnackbar(message);
|
||||
},
|
||||
onFetchStateChange: () => { },
|
||||
onFetchStateChange: () => {},
|
||||
enabled: Boolean(user),
|
||||
});
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user