Merge branch 'requests-refactor' into 'dev'

style: requests

See merge request frontend/squiz!321
This commit is contained in:
Nastya 2024-05-15 00:04:50 +00:00
commit 73e8a03a22
36 changed files with 1047 additions and 791 deletions

@ -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,

@ -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),
});