This commit is contained in:
IlyaDoronin 2024-05-15 14:44:10 +03:00
parent 73e8a03a22
commit 17a697893d
27 changed files with 590 additions and 354 deletions

@ -44,6 +44,7 @@ import RecoverPassword from "./pages/auth/RecoverPassword";
import { InfoPrivilege } from "./pages/InfoPrivilege"; import { InfoPrivilege } from "./pages/InfoPrivilege";
import OutdatedLink from "./pages/auth/OutdatedLink"; import OutdatedLink from "./pages/auth/OutdatedLink";
import { useAfterpay } from "@utils/hooks/useAfterpay"; import { useAfterpay } from "@utils/hooks/useAfterpay";
import { useUserAccountFetcher } from "@utils/hooks/useUserAccountFetcher";
const MyQuizzesFull = lazy(() => import("./pages/createQuize/MyQuizzesFull")); const MyQuizzesFull = lazy(() => import("./pages/createQuize/MyQuizzesFull"));
@ -92,65 +93,13 @@ const LazyLoading = ({ children, fallback }: SuspenseProps) => (
<Suspense fallback={fallback ?? <></>}>{children}</Suspense> <Suspense fallback={fallback ?? <></>}>{children}</Suspense>
); );
export function useUserAccountFetcher<T = UserAccount>({
onError,
onNewUserAccount,
url,
userId,
}: {
url: string;
userId: string | null;
onNewUserAccount: (response: T) => void;
onError?: (error: any) => void;
}) {
const onNewUserAccountRef = useRef(onNewUserAccount);
const onErrorRef = useRef(onError);
useLayoutEffect(() => {
onNewUserAccountRef.current = onNewUserAccount;
onErrorRef.current = onError;
}, [onError, onNewUserAccount]);
useEffect(() => {
if (!userId) return;
const controller = new AbortController();
makeRequest<never, T>({
url,
contentType: true,
method: "GET",
useToken: true,
withCredentials: false,
signal: controller.signal,
})
.then((result) => {
devlog("User account", result);
onNewUserAccountRef.current(result);
})
.catch((error) => {
devlog("Error fetching user account", error);
if (isAxiosError(error) && error.response?.status === 404) {
createUserAccount(controller.signal, url.replace("get", "create"))
.then((result) => {
devlog("Created user account", result);
onNewUserAccountRef.current(result as T);
})
.catch((error) => {
devlog("Error creating user account", error);
onErrorRef.current?.(error);
});
} else {
onErrorRef.current?.(error);
}
});
return () => controller.abort();
}, [url, userId]);
}
export default function App() { export default function App() {
const userId = useUserStore((state) => state.userId); const userId = useUserStore((state) => state.userId);
const location = useLocation(); const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
useUserFetcher({ useUserFetcher({
url: process.env.REACT_APP_DOMAIN + `/user/${userId}`, url: `${process.env.REACT_APP_DOMAIN}/user/${userId}`,
userId, userId,
onNewUser: setUser, onNewUser: setUser,
onError: (error) => { onError: (error) => {
@ -164,7 +113,7 @@ export default function App() {
}); });
useUserAccountFetcher<UserAccount>({ useUserAccountFetcher<UserAccount>({
url: process.env.REACT_APP_DOMAIN + "/customer/account", url: `${process.env.REACT_APP_DOMAIN}/customer/account`,
userId, userId,
onNewUserAccount: setCustomerAccount, onNewUserAccount: setCustomerAccount,
onError: (error) => { onError: (error) => {
@ -179,7 +128,7 @@ export default function App() {
}); });
useUserAccountFetcher<OriginalUserAccount>({ useUserAccountFetcher<OriginalUserAccount>({
url: process.env.REACT_APP_DOMAIN + "/squiz/account/get", url: `${process.env.REACT_APP_DOMAIN}/squiz/account/get`,
userId, userId,
onNewUserAccount: setUserAccount, onNewUserAccount: setUserAccount,
onError: (error) => { onError: (error) => {

@ -8,7 +8,7 @@ import type {
} from "@frontend/kitui"; } from "@frontend/kitui";
import { parseAxiosError } from "../utils/parse-error"; import { parseAxiosError } from "../utils/parse-error";
const API_URL = process.env.REACT_APP_DOMAIN + "/auth"; const API_URL = `${process.env.REACT_APP_DOMAIN}/auth`;
export const register = async ( export const register = async (
login: string, login: string,
@ -79,11 +79,11 @@ export const recover = async (
formData.append("email", email); formData.append("email", email);
formData.append( formData.append(
"RedirectionURL", "RedirectionURL",
process.env.REACT_APP_DOMAIN + "/changepwd", `${process.env.REACT_APP_DOMAIN}/changepwd`,
); );
const recoverResponse = await makeRequest<unknown, unknown>({ const recoverResponse = await makeRequest<unknown, unknown>({
url: process.env.REACT_APP_DOMAIN + "/codeword/recover", url: `${process.env.REACT_APP_DOMAIN}/codeword/recover`,
body: formData, body: formData,
useToken: false, useToken: false,
withCredentials: true, withCredentials: true,

@ -3,13 +3,13 @@ import { makeRequest } from "@api/makeRequest";
import { parseAxiosError } from "@utils/parse-error"; import { parseAxiosError } from "@utils/parse-error";
const API_URL = process.env.REACT_APP_DOMAIN + "/customer"; const API_URL = `${process.env.REACT_APP_DOMAIN}/customer/cart/`;
export const payCart = async (): Promise<[UserAccount | null, string?]> => { const payCart = async (): Promise<[UserAccount | null, string?]> => {
try { try {
const payCartResponse = await makeRequest<never, UserAccount>({ const payCartResponse = await makeRequest<never, UserAccount>({
method: "POST", method: "POST",
url: `${API_URL}/cart/pay`, url: `${API_URL}/pay`,
useToken: true, useToken: true,
}); });
@ -20,3 +20,41 @@ export const payCart = async (): Promise<[UserAccount | null, string?]> => {
return [null, `Не удалось оплатить товар из корзины. ${error}`]; return [null, `Не удалось оплатить товар из корзины. ${error}`];
} }
}; };
const addCartItem = async (id: string): Promise<[unknown | null, string?]> => {
try {
const addedItem = await makeRequest<never, unknown>({
method: "PATCH",
url: `${API_URL}?id=${id}`,
});
return [addedItem];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось добавить товар в корзину. ${error}`];
}
};
const deleteCartItem = async (
id: string,
): Promise<[unknown | null, string?]> => {
try {
const deletedItem = await makeRequest<never, unknown>({
method: "DELETE",
url: `${API_URL}?id=${id}`,
});
return [deletedItem];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось удалить товар из корзины. ${error}`];
}
};
export const cartApi = {
pay: payCart,
add: addCartItem,
delete: deleteCartItem,
};

@ -2,7 +2,7 @@ import { makeRequest } from "@api/makeRequest";
import { parseAxiosError } from "@utils/parse-error"; import { parseAxiosError } from "@utils/parse-error";
const API_URL = process.env.REACT_APP_DOMAIN + "/feedback"; const API_URL = `${process.env.REACT_APP_DOMAIN}/feedback`;
type SendContactFormBody = { type SendContactFormBody = {
contact: string; contact: string;

@ -3,7 +3,7 @@ import { parseAxiosError } from "@utils/parse-error";
import type { Discount } from "@model/discounts"; import type { Discount } from "@model/discounts";
const API_URL = process.env.REACT_APP_DOMAIN + "/price/discount"; const API_URL = `${process.env.REACT_APP_DOMAIN}/price/discount`;
export const getDiscounts = async ( export const getDiscounts = async (
userId: string, userId: string,

@ -5,23 +5,26 @@ import { parseAxiosError } from "@utils/parse-error";
type ActivatePromocodeRequest = { codeword: string } | { fastLink: string }; type ActivatePromocodeRequest = { codeword: string } | { fastLink: string };
type ActivatePromocodeResponse = { greetings: string }; type ActivatePromocodeResponse = { greetings: string };
const API_URL = process.env.REACT_APP_DOMAIN + "/codeword/promocode"; const API_URL = `${process.env.REACT_APP_DOMAIN}/codeword/promocode`;
export const activatePromocode = async (promocode: string) => { export const activatePromocode = async (
promocode: string,
): Promise<[string | null, string?]> => {
try { try {
const response = await makeRequest< const response = await makeRequest<
ActivatePromocodeRequest, ActivatePromocodeRequest,
ActivatePromocodeResponse ActivatePromocodeResponse
>({ >({
method: "POST", method: "POST",
url: API_URL + "/activate", url: `${API_URL}/activate`,
body: { codeword: promocode }, body: { codeword: promocode },
contentType: true, contentType: true,
}); });
return response.greetings; return [response.greetings];
} catch (nativeError) { } catch (nativeError) {
const [error] = parseAxiosError(nativeError); const [error] = parseAxiosError(nativeError);
throw new Error(error);
return [null, `Ошибка при активации промокода. ${error}`];
} }
}; };

@ -20,7 +20,7 @@ import {
import { replaceSpacesToEmptyLines } from "../utils/replaceSpacesToEmptyLines"; import { replaceSpacesToEmptyLines } from "../utils/replaceSpacesToEmptyLines";
import { parseAxiosError } from "@utils/parse-error"; import { parseAxiosError } from "@utils/parse-error";
const API_URL = process.env.REACT_APP_DOMAIN + "/squiz"; const API_URL = "/squiz";
export const createQuestion = async ( export const createQuestion = async (
body: CreateQuestionRequest, body: CreateQuestionRequest,

@ -13,8 +13,8 @@ type AddedQuizImagesResponse = {
[key: string]: string; [key: string]: string;
}; };
const API_URL = process.env.REACT_APP_DOMAIN + "/squiz"; const API_URL = `${process.env.REACT_APP_DOMAIN}/squiz`;
const IMAGES_URL = process.env.REACT_APP_DOMAIN + "/squizstorer"; const IMAGES_URL = `${process.env.REACT_APP_DOMAIN}/squizstorer`;
export const createQuiz = async ( export const createQuiz = async (
body?: Partial<CreateQuizRequest>, body?: Partial<CreateQuizRequest>,

@ -1,6 +1,8 @@
import { makeRequest } from "@api/makeRequest"; import { makeRequest } from "@api/makeRequest";
import { RawResult } from "@model/result/result"; import { RawResult } from "@model/result/result";
import { parseAxiosError } from "@utils/parse-error";
interface IResultListBody { interface IResultListBody {
to: number; to: number;
from: string; from: string;
@ -29,46 +31,102 @@ export interface IAnswerResult {
question_id: number; question_id: number;
} }
const API_URL = process.env.REACT_APP_DOMAIN + `/squiz`; const API_URL = `${process.env.REACT_APP_DOMAIN}/squiz`;
const getResultList = async (quizId: number, page: number, body: any) => { const getResultList = async (
return makeRequest<IResultListBody, RawResult>({ quizId: number,
method: "POST", page: number,
url: `${API_URL}/results/getResults/${quizId}`, body: any,
body: { page: page, limit: 10, ...body }, ): Promise<[RawResult | null, string?]> => {
}); try {
const resultList = await makeRequest<IResultListBody, RawResult>({
method: "POST",
url: `${API_URL}/results/getResults/${quizId}`,
body: { page: page, limit: 10, ...body },
});
return [resultList];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось получить результат. ${error}`];
}
}; };
const deleteResult = async (resultId: number) => { const deleteResult = async (
return makeRequest<unknown, unknown>({ resultId: number,
method: "DELETE", ): Promise<[unknown | null, string?]> => {
url: `${API_URL}/results/delete/${resultId}`, try {
body: {}, const deletedResult = await makeRequest<unknown, unknown>({
}); method: "DELETE",
url: `${API_URL}/results/delete/${resultId}`,
body: {},
});
return [deletedResult];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось удалить результат. ${error}`];
}
}; };
const obsolescenceResult = (idResultArray: number[]) => { const obsolescenceResult = async (
return makeRequest<unknown, unknown>({ idResultArray: number[],
method: "PATCH", ): Promise<[unknown | null, string?]> => {
url: `${API_URL}/result/seen`, try {
body: { answers: idResultArray }, const obsolescencedResult = await makeRequest<unknown, unknown>({
}); method: "PATCH",
url: `${API_URL}/result/seen`,
body: { answers: idResultArray },
});
return [obsolescencedResult];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось изменить результат. ${error}`];
}
}; };
const getAnswerResultList = (resultId: number) => { const getAnswerResultList = async (
return makeRequest<unknown, IAnswerResult[]>({ resultId: number,
method: "GET", ): Promise<[IAnswerResult[] | null, string?]> => {
url: `${API_URL}/result/${resultId}`, try {
}); const answerResultList = await makeRequest<unknown, IAnswerResult[]>({
method: "GET",
url: `${API_URL}/result/${resultId}`,
});
return [answerResultList];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось получить список результатов. ${error}`];
}
}; };
const AnswerResultListEx = (quizId: number, body: any) => { const AnswerResultListEx = async (
return makeRequest<unknown, unknown>({ quizId: number,
method: "POST", body: any,
url: `${API_URL}/results/${quizId}/export`, ): Promise<[unknown | null, string?]> => {
body: body, try {
responseType: "blob", const answerResultListEx = await makeRequest<unknown, unknown>({
}); method: "POST",
url: `${API_URL}/results/${quizId}/export`,
body: body,
responseType: "blob",
});
return [answerResultListEx];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [
null,
`Не удалось получить список устаревших результатов. ${error}`,
];
}
}; };
export const resultApi = { export const resultApi = {

@ -27,7 +27,7 @@ type TRequest = {
from: number; from: number;
}; };
const API_URL = process.env.REACT_APP_DOMAIN + "/squiz/statistic"; const API_URL = `${process.env.REACT_APP_DOMAIN}/squiz/statistic`;
export const getDevices = async ( export const getDevices = async (
quizId: string, quizId: string,

23
src/api/tariff.ts Normal file

@ -0,0 +1,23 @@
import { makeRequest } from "@api/makeRequest";
import { parseAxiosError } from "@utils/parse-error";
import type { GetTariffsResponse } from "@frontend/kitui";
const API_URL = `${process.env.REACT_APP_DOMAIN}/strator/tariff`;
export const getTariffs = async (
page: number,
): Promise<[GetTariffsResponse | null, string?]> => {
try {
const tariffs = await makeRequest<never, GetTariffsResponse>({
method: "GET",
url: `${API_URL}?page=${page}&limit=100`,
});
return [tariffs];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Ошибка при получении списка тарифов. ${error}`];
}
};

@ -1,9 +1,12 @@
import { makeRequest } from "@api/makeRequest"; import { makeRequest } from "@api/makeRequest";
import { createTicket as createTicketRequest } from "@frontend/kitui";
import { parseAxiosError } from "../utils/parse-error"; import { parseAxiosError } from "../utils/parse-error";
import { SendTicketMessageRequest } from "@frontend/kitui"; import { SendTicketMessageRequest } from "@frontend/kitui";
const API_URL = process.env.REACT_APP_DOMAIN + "/heruvym"; import type { CreateTicketResponse } from "@frontend/kitui";
const API_URL = `${process.env.REACT_APP_DOMAIN}/heruvym`;
export const sendTicketMessage = async ( export const sendTicketMessage = async (
ticketId: string, ticketId: string,
@ -44,3 +47,46 @@ export const shownMessage = async (id: string): Promise<[null, string?]> => {
return [null, `Не удалось прочесть сообщение. ${error}`]; return [null, `Не удалось прочесть сообщение. ${error}`];
} }
}; };
export const sendFile = async (
ticketId: string,
file: File,
): Promise<[unknown | null, string?]> => {
try {
const body = new FormData();
body.append(file.name, file);
body.append("ticket", ticketId);
const sendResponse = await makeRequest<FormData, unknown>({
method: "POST",
url: `${process.env.REACT_APP_DOMAIN}/heruvym/sendFiles`,
body,
});
return [sendResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось отправить файл. ${error}`];
}
};
export const createTicket = async (
message: string,
useToken: boolean,
): Promise<[CreateTicketResponse | null, string?]> => {
try {
const createdTicket = await createTicketRequest({
url: `${process.env.REACT_APP_DOMAIN}/heruvym/create`,
body: { Title: "Unauth title", Message: message },
useToken,
});
return [createdTicket];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось создать тикет. ${error}`];
}
};

62
src/api/user.ts Normal file

@ -0,0 +1,62 @@
import { makeRequest } from "@api/makeRequest";
import { parseAxiosError } from "@utils/parse-error";
import type { UserAccount } from "@frontend/kitui";
import type { OriginalUserAccount } from "@root/user";
export const getUser = async (): Promise<[UserAccount | null, string?]> => {
try {
const user = await makeRequest<never, UserAccount>({
method: "GET",
url: `${process.env.REACT_APP_DOMAIN}/customer/account`,
});
return [user];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось получить пользователя. ${error}`];
}
};
export const getAccount = async (): Promise<
[OriginalUserAccount | null, string?]
> => {
try {
const controller = new AbortController();
const account = await makeRequest<never, OriginalUserAccount>({
url: `${process.env.REACT_APP_DOMAIN}/squiz/account/get`,
contentType: true,
method: "GET",
useToken: true,
withCredentials: false,
signal: controller.signal,
});
return [account];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось получить данные аккаунта. ${error}`];
}
};
export const recoverUser = async (
password: string,
): Promise<[unknown | null, string?]> => {
try {
const recoverResponse = await makeRequest<{ password: string }, unknown>({
url: `${process.env.REACT_APP_DOMAIN}/user`,
method: "PATCH",
body: { password },
});
return [recoverResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось восстановить пароль. ${error}`];
}
};

@ -19,9 +19,10 @@ import { useQuizStore } from "@root/quizes/store";
import { useQuestionsStore } from "@root/questions/store"; import { useQuestionsStore } from "@root/questions/store";
import AddressIcon from "@icons/ContactFormIcon/AddressIcon"; import AddressIcon from "@icons/ContactFormIcon/AddressIcon";
import type { AxiosError } from "axios";
import { DeleteModal } from "./DeleteModal"; import { DeleteModal } from "./DeleteModal";
import type { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
interface CardAnswerProps { interface CardAnswerProps {
isNew: boolean; isNew: boolean;
idResult: number; idResult: number;
@ -50,29 +51,31 @@ export const CardAnswer: FC<CardAnswerProps> = ({
const theme = useTheme(); const theme = useTheme();
const isTablet = useMediaQuery(theme.breakpoints.down(1000)); const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const [resultsAnswer, setResultsAnswer] = useState<IAnswerResult[]>([]); const [resultsAnswer, setResultsAnswer] = useState<IAnswerResult[]>([]);
const [questionsResultState, setQuestionsResultState] = useState([]); const [questionsResultState, setQuestionsResultState] = useState<
AnyTypedQuizQuestion[]
>([]);
const { editQuizId } = useQuizStore(); const { editQuizId } = useQuizStore();
const { questions } = useQuestionsStore(); const { questions } = useQuestionsStore();
const openResults = async () => { const openResults = async () => {
setIsOpen(!isOpen); setIsOpen(!isOpen);
if (!isOpen) { if (!isOpen) {
try { const [resAnswer, answerError] = await resultApi.getAnswerList(idResult);
const resAnswer = await resultApi.getAnswerList(idResult);
const resAnswerOnly = resAnswer.filter((res) => res.Result !== true);
const resQuiz = resAnswer.filter((res) => res.Result === true);
setResultsAnswer(resAnswerOnly);
const idResults = resQuiz[0].question_id;
const questionsResult = questions.filter(
(q) => q.backendId === idResults,
);
setQuestionsResultState(questionsResult);
} catch (nativeError) {
const error = nativeError as AxiosError;
if (error.response?.statusText === "Payment Required") { if (answerError || !resAnswer) {
openPrePaymentModal(); openPrePaymentModal();
}
return;
} }
const resAnswerOnly = resAnswer.filter((res) => res.Result !== true);
const resQuiz = resAnswer.filter((res) => res.Result === true);
setResultsAnswer(resAnswerOnly);
const idResults = resQuiz[0].question_id;
const questionsResult = questions.filter(
(q) => q.type && q.backendId === idResults,
) as AnyTypedQuizQuestion[];
setQuestionsResultState(questionsResult);
} }
}; };

@ -78,17 +78,20 @@ export const QuizAnswersPage: FC = () => {
const PaginationHC = async (e: ChangeEvent<unknown>, value: number) => { const PaginationHC = async (e: ChangeEvent<unknown>, value: number) => {
setPage(value); setPage(value);
try { if (editQuizId !== null) {
if (editQuizId !== null) { const [result, resultError] = await resultApi.getList(
const result = await resultApi.getList( editQuizId,
editQuizId, value - 1,
value - 1, parseFilters(filterNew, filterDate),
parseFilters(filterNew, filterDate), );
);
setResults(result); if (resultError || !result) {
console.error("An error occurred while receiving data: ", resultError);
return;
} }
} catch (error) {
console.error("An error occurred while receiving data: ", error); setResults(result);
} }
}; };

@ -98,11 +98,18 @@ export const QuizSettingsMenu: FC<QuizSettingsMenuProps> = ({
}} }}
onclickUpdate={async () => { onclickUpdate={async () => {
if (editQuizId !== null) { if (editQuizId !== null) {
const result = await resultApi.getList( const [result, resultError] = await resultApi.getList(
editQuizId, editQuizId,
page - 1, page - 1,
parseFilters(filterNew, filterDate), parseFilters(filterNew, filterDate),
); );
if (resultError || !result) {
console.error(resultError);
return;
}
setResults(result); setResults(result);
} else { } else {
console.error("editQuizId is null"); console.error("editQuizId is null");

@ -37,11 +37,18 @@ export const useGetData = (filterNew: string, filterDate: string): void => {
setQuestions(questions); setQuestions(questions);
const result = await resultApi.getList( const [result, resultError] = await resultApi.getList(
editQuizId, editQuizId,
0, 0,
parseFilters(filterNew, filterDate), parseFilters(filterNew, filterDate),
); );
if (resultError || !result) {
console.error(resultError);
return;
}
if (result.total_count === 0) { if (result.total_count === 0) {
console.error("No results found"); console.error("No results found");
} }

@ -33,6 +33,9 @@ import { currencyFormatter } from "./tariffsUtils/currencyFormatter";
import { useWallet, setCash } from "@root/cash"; import { useWallet, setCash } from "@root/cash";
import { handleLogoutClick } from "@utils/HandleLogoutClick"; import { handleLogoutClick } from "@utils/HandleLogoutClick";
import { getDiscounts } from "@api/discounts"; import { getDiscounts } from "@api/discounts";
import { cartApi } from "@api/cart";
import { getUser } from "@api/user";
import { getTariffs } from "@api/tariff";
import type { Discount } from "@model/discounts"; import type { Discount } from "@model/discounts";
@ -62,25 +65,20 @@ function TariffPage() {
const getTariffsList = async (): Promise<Tariff[]> => { const getTariffsList = async (): Promise<Tariff[]> => {
const tariffsList: Tariff[] = []; const tariffsList: Tariff[] = [];
const { tariffs, totalPages } = await makeRequest< const [tariffsResponse, tariffsResponseError] = await getTariffs(1);
never,
GetTariffsResponse
>({
method: "GET",
url: process.env.REACT_APP_DOMAIN + "/strator/tariff?page=1&limit=100",
});
tariffsList.push(...tariffs); if (tariffsResponseError || !tariffsResponse) {
return tariffsList;
}
for (let page = 2; page <= totalPages; page += 1) { tariffsList.push(...tariffsResponse.tariffs);
const tariffsResult = await makeRequest<never, GetTariffsResponse>({
method: "GET",
url:
process.env.REACT_APP_DOMAIN +
`/strator/tariff?page=${page}&limit=100`,
});
tariffsList.push(...tariffsResult.tariffs); for (let page = 2; page <= tariffsResponse.totalPages; page += 1) {
const [tariffsResult] = await getTariffs(1);
if (tariffsResult) {
tariffsList.push(...tariffsResult.tariffs);
}
} }
return tariffsList; return tariffsList;
@ -88,10 +86,12 @@ function TariffPage() {
useEffect(() => { useEffect(() => {
const get = async () => { const get = async () => {
const user = await makeRequest({ const [user, userError] = await getUser();
method: "GET",
url: process.env.REACT_APP_DOMAIN + "/customer/account", if (userError) {
}); return;
}
const tariffsList = await getTariffsList(); const tariffsList = await getTariffsList();
if (userId) { if (userId) {
@ -123,27 +123,22 @@ function TariffPage() {
outCart(user.cart); outCart(user.cart);
} }
//Добавляем желаемый тариф в корзину //Добавляем желаемый тариф в корзину
await makeRequest({ const [_, addError] = await cartApi.add(id);
method: "PATCH",
url: process.env.REACT_APP_DOMAIN + `/customer/cart?id=${id}`,
});
//Если нам хватает денежек - покупаем тариф
try { if (addError) {
const data = await makeRequest({ //Развращаем товары в корзину
method: "POST", inCart();
url: process.env.REACT_APP_DOMAIN + "/customer/cart/pay",
}); return;
setCash( }
currencyFormatter.format(Number(data.wallet.cash) / 100),
Number(data.wallet.cash), //Если нам хватает денежек - покупаем тариф
Number(data.wallet.cash) / 100, const [data, payError] = await cartApi.pay();
);
enqueueSnackbar("Тариф успешно приобретён"); if (payError || !data) {
} catch (e) {
//если денег не хватило //если денег не хватило
if (e.response.data.message.includes("insufficient funds")) { if (addError) {
let cashDif = Number(e.response.data.message.split(":")[1]); let cashDif = Number(addError.split(":")[1]);
var link = document.createElement("a"); var link = document.createElement("a");
link.href = `https://${isTestServer ? "s" : ""}hub.pena.digital/quizpayment?action=squizpay&dif=${cashDif}&data=${token}&userid=${userId}`; link.href = `https://${isTestServer ? "s" : ""}hub.pena.digital/quizpayment?action=squizpay&dif=${cashDif}&data=${token}&userid=${userId}`;
document.body.appendChild(link); document.body.appendChild(link);
@ -152,7 +147,18 @@ function TariffPage() {
} }
//другая ошибка //другая ошибка
enqueueSnackbar("Произошла ошибка. Попробуйте позже"); enqueueSnackbar("Произошла ошибка. Попробуйте позже");
return;
} }
setCash(
currencyFormatter.format(Number(data.wallet.cash) / 100),
Number(data.wallet.cash),
Number(data.wallet.cash) / 100,
);
enqueueSnackbar("Тариф успешно приобретён");
//Развращаем товары в корзину //Развращаем товары в корзину
inCart(); inCart();
}; };
@ -179,31 +185,32 @@ function TariffPage() {
return tariff.privileges[0].privilegeId !== "squizHideBadge"; return tariff.privileges[0].privilegeId !== "squizHideBadge";
}); });
function handleApplyPromocode() { async function handleApplyPromocode() {
if (!promocodeField) return; if (!promocodeField) return;
activatePromocode(promocodeField) const [greetings, error] = await activatePromocode(promocodeField);
.then(async (greetings) => {
enqueueSnackbar(greetings);
if (!userId) { if (error) {
return; enqueueSnackbar(error);
}
const [discounts, discountsError] = await getDiscounts(userId); return;
}
if (discountsError) { enqueueSnackbar(greetings);
throw new Error(discountsError);
}
if (discounts?.length) { if (!userId) {
setDiscounts(discounts); return;
} }
})
.catch((error) => { const [discounts, discountsError] = await getDiscounts(userId);
if (error.message !== "" && typeof error.message === "string")
enqueueSnackbar(error.message); if (discountsError) {
}); throw new Error(discountsError);
}
if (discounts?.length) {
setDiscounts(discounts);
}
} }
return ( return (
@ -396,19 +403,18 @@ export const inCart = () => {
let saveCart = JSON.parse(localStorage.getItem("saveCart") || "[]"); let saveCart = JSON.parse(localStorage.getItem("saveCart") || "[]");
if (Array.isArray(saveCart)) { if (Array.isArray(saveCart)) {
saveCart.forEach(async (id: string) => { saveCart.forEach(async (id: string) => {
try { const [_, addError] = await cartApi.add(id);
await makeRequest({
method: "PATCH",
url: process.env.REACT_APP_DOMAIN + `/customer/cart?id=${id}`,
});
if (addError) {
console.error(addError);
} else {
let index = saveCart.indexOf("green"); let index = saveCart.indexOf("green");
if (index !== -1) { if (index !== -1) {
saveCart.splice(index, 1); saveCart.splice(index, 1);
} }
localStorage.setItem("saveCart", JSON.stringify(saveCart)); localStorage.setItem("saveCart", JSON.stringify(saveCart));
} catch (e) {
console.error("Я не смог добавить тариф в корзину :( " + id);
} }
}); });
} else { } else {
@ -418,16 +424,16 @@ export const inCart = () => {
const outCart = (cart: string[]) => { const outCart = (cart: string[]) => {
//Сделаем муторно и подольше, зато при прерывании сессии данные потеряются минимально //Сделаем муторно и подольше, зато при прерывании сессии данные потеряются минимально
cart.forEach(async (id: string) => { cart.forEach(async (id: string) => {
try { const [_, deleteError] = await cartApi.delete(id);
await makeRequest({
method: "DELETE", if (deleteError) {
url: process.env.REACT_APP_DOMAIN + `/customer/cart?id=${id}`, console.error(deleteError);
});
let saveCart = JSON.parse(localStorage.getItem("saveCart") || "[]"); return;
saveCart = saveCart.push(id);
localStorage.setItem("saveCart", JSON.stringify(saveCart));
} catch (e) {
console.error("Я не смог удалить из корзины тариф :(");
} }
let saveCart = JSON.parse(localStorage.getItem("saveCart") || "[]");
saveCart = saveCart.push(id);
localStorage.setItem("saveCart", JSON.stringify(saveCart));
}); });
}; };

@ -21,6 +21,7 @@ import { useUserStore } from "@root/user";
import { makeRequest } from "@api/makeRequest"; import { makeRequest } from "@api/makeRequest";
import { setAuthToken } from "@frontend/kitui"; import { setAuthToken } from "@frontend/kitui";
import { parseAxiosError } from "@utils/parse-error"; import { parseAxiosError } from "@utils/parse-error";
import { recoverUser } from "@api/user";
interface Values { interface Values {
password: string; password: string;
} }
@ -55,21 +56,17 @@ export default function RecoverPassword() {
onSubmit: async (values, formikHelpers) => { onSubmit: async (values, formikHelpers) => {
if (tokenUser) { if (tokenUser) {
setAuthToken(tokenUser || ""); setAuthToken(tokenUser || "");
try { const [_, recoverError] = await recoverUser(values.password);
const response = await makeRequest<unknown, unknown>({
url: process.env.REACT_APP_DOMAIN + "/user/", if (recoverError) {
method: "PATCH", setAuthToken("");
body: { password: values.password }, enqueueSnackbar(
}); `Извините, произошла ошибка, попробуйте повторить позже. ${recoverError}`,
);
} else {
setIsDialogOpen(false); setIsDialogOpen(false);
navigate("/"); navigate("/");
enqueueSnackbar("Пароль успешно сменён"); enqueueSnackbar("Пароль успешно сменён");
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
setAuthToken("");
enqueueSnackbar(
`Извините, произошла ошибка, попробуйте повторить позже. ${error}`,
);
} }
} else { } else {
enqueueSnackbar("Неверный url-адрес"); enqueueSnackbar("Неверный url-адрес");

@ -7,7 +7,7 @@ import {
} from "@root/user"; } from "@root/user";
import { clearAuthToken, getMessageFromFetchError } from "@frontend/kitui"; import { clearAuthToken, getMessageFromFetchError } from "@frontend/kitui";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { useUserAccountFetcher } from "../../App"; import { useUserAccountFetcher } from "@utils/hooks/useUserAccountFetcher";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import moment from "moment"; import moment from "moment";
@ -17,7 +17,7 @@ export default function AvailablePrivilege() {
const userId = useUserStore((state) => state.userId); const userId = useUserStore((state) => state.userId);
const navigate = useNavigate(); const navigate = useNavigate();
useUserAccountFetcher<OriginalUserAccount>({ useUserAccountFetcher<OriginalUserAccount>({
url: process.env.REACT_APP_DOMAIN + "/squiz/account/get", url: `${process.env.REACT_APP_DOMAIN}/squiz/account/get`,
userId, userId,
onNewUserAccount: setUserAccount, onNewUserAccount: setUserAccount,
onError: (error) => { onError: (error) => {

@ -20,6 +20,7 @@ import { enqueueSnackbar } from "notistack";
import { useDomainDefine } from "@utils/hooks/useDomainDefine"; import { useDomainDefine } from "@utils/hooks/useDomainDefine";
import CopyIcon from "@icons/CopyIcon"; import CopyIcon from "@icons/CopyIcon";
import ChartIcon from "@icons/ChartIcon"; import ChartIcon from "@icons/ChartIcon";
import { cartApi } from "@api/cart";
interface Props { interface Props {
quiz: Quiz; quiz: Quiz;
@ -55,17 +56,17 @@ export default function QuizCard({
useLayoutEffect(() => { useLayoutEffect(() => {
const pay = async () => { const pay = async () => {
try { const [_, payError] = await cartApi.pay();
await makeRequest({
method: "POST", if (payError) {
url: process.env.REACT_APP_DOMAIN + "/customer/cart/pay",
});
inCart();
} catch (e) {
enqueueSnackbar( enqueueSnackbar(
"Попробуйте снова купить тариф после зачисления средств", "Попробуйте снова купить тариф после зачисления средств",
); );
return;
} }
inCart();
}; };
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const fromSquiz = params.get("action"); const fromSquiz = params.get("action");

@ -129,7 +129,7 @@ export const quizStore = create<QuizStore>()(
config: { config: {
noStartPage: false, noStartPage: false,
type: "", // quiz или form type: "", // quiz или form
logo: process.env.REACT_APP_DOMAIN + "/img/logo", logo: `${process.env.REACT_APP_DOMAIN}/img/logo`,
startpage: { startpage: {
description: "", // приветственный текст опроса description: "", // приветственный текст опроса
button: "", // текст на кнопке начала опроса button: "", // текст на кнопке начала опроса

@ -39,15 +39,16 @@ export const deleteResult = async (resultId: number) =>
.results.find((r) => r.id === resultId); .results.find((r) => r.id === resultId);
if (!result) return; if (!result) return;
try { const [_, deleteError] = await resultApi.delete(Number(result.id));
await resultApi.delete(Number(result.id));
removeResult(resultId);
} catch (error) {
devlog("Error delete result", error);
const message = getMessageFromFetchError(error) ?? ""; if (deleteError) {
enqueueSnackbar(`Не удалось удалить результат. ${message}`); devlog("Error delete result", deleteError);
enqueueSnackbar(deleteError);
return;
} }
removeResult(resultId);
}); });
export const obsolescenceResult = async ( export const obsolescenceResult = async (
@ -68,17 +69,24 @@ export const obsolescenceResult = async (
if (typeof lossDebouncer === "number") clearTimeout(lossDebouncer); if (typeof lossDebouncer === "number") clearTimeout(lossDebouncer);
lossDebouncer = setTimeout(async () => { lossDebouncer = setTimeout(async () => {
//стреляем на лишение новизны //стреляем на лишение новизны
try { const [_, obsolescenceError] = await resultApi.obsolescence(lossId);
await resultApi.obsolescence(lossId);
lossId = [];
} catch (error) {
devlog("Error", error);
const message = getMessageFromFetchError(error) ?? ""; if (obsolescenceError) {
enqueueSnackbar(`Ошибка. ${message}`); devlog("Error", obsolescenceError);
enqueueSnackbar(obsolescenceError);
return;
} }
lossId = [];
}, 3000); }, 3000);
const resultList = await resultApi.getList(editQuizId); const [resultList, resultError] = await resultApi.getList(editQuizId);
if (resultError || !resultList) {
return;
}
setResults(resultList); setResults(resultList);
}, },
); );
@ -90,30 +98,28 @@ export const ExportResults = async (
openPrePaymentModal: () => void, openPrePaymentModal: () => void,
editQuizId: number, editQuizId: number,
) => { ) => {
try { const [data, resultError] = await resultApi.export(
const data = await resultApi.export( editQuizId,
editQuizId, parseFilters(filterNew, filterDate),
parseFilters(filterNew, filterDate), );
);
console.log(typeof data); if (resultError) {
openPrePaymentModal();
const blob = new Blob([data as BlobPart], { return;
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);
link.download = `report_${new Date().getTime()}.xlsx`;
link.click();
} catch (nativeError) {
const error = nativeError as AxiosError;
if (error.response?.statusText === "Payment Required") {
openPrePaymentModal();
}
} }
console.log(typeof data);
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);
link.download = `report_${new Date().getTime()}.xlsx`;
link.click();
}; };
function setProducedState<A extends string | { type: string }>( function setProducedState<A extends string | { type: string }>(

@ -13,7 +13,7 @@ import {
useUserStore, useUserStore,
} from "@root/user"; } from "@root/user";
import { parseAxiosError } from "@utils/parse-error"; import { parseAxiosError } from "@utils/parse-error";
import { useUserAccountFetcher } from "../App"; import { useUserAccountFetcher } from "@utils/hooks/useUserAccountFetcher";
import type { Discount } from "@model/discounts"; import type { Discount } from "@model/discounts";
import { import {
clearAuthToken, clearAuthToken,
@ -23,6 +23,8 @@ import {
} from "@frontend/kitui"; } from "@frontend/kitui";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { isAxiosError } from "axios"; import { isAxiosError } from "axios";
import { activatePromocode } from "@api/promocode";
import { getAccount } from "@api/user";
export function CheckFastlink() { export function CheckFastlink() {
const user = useUserStore(); const user = useUserStore();
@ -50,40 +52,28 @@ export function CheckFastlink() {
const fetchPromocode = async () => { const fetchPromocode = async () => {
if (promocode.length > 0) { if (promocode.length > 0) {
try { const [greetings, activationError] = await activatePromocode(promocode);
const response = await makeRequest<
{ codeword: string } | { fastLink: string }, if (activationError || !greetings) {
{ greetings: string } enqueueSnackbar(activationError);
>({
url:
process.env.REACT_APP_DOMAIN + "/codeword/promocode" + "/activate",
method: "POST",
contentType: true,
body: { fastLink: promocode },
});
enqueueSnackbar(
response.greetings !== ""
? response.greetings
: "Промокод успешно активирован",
);
localStorage.setItem("fl", "");
const controller = new AbortController();
const responseAccount = await makeRequest<never, any>({
url: process.env.REACT_APP_DOMAIN + "/squiz/account/get",
contentType: true,
method: "GET",
useToken: true,
withCredentials: false,
signal: controller.signal,
});
setUserAccount(responseAccount);
mutate("discounts");
return response.greetings;
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
enqueueSnackbar(error);
localStorage.setItem("fl", ""); localStorage.setItem("fl", "");
return;
} }
enqueueSnackbar(
greetings !== "" ? greetings : "Промокод успешно активирован",
);
localStorage.setItem("fl", "");
const [responseAccount, accountError] = await getAccount();
if (accountError || !responseAccount) {
return;
}
setUserAccount(responseAccount);
mutate("discounts");
return greetings;
} }
}; };

@ -1,5 +1,4 @@
import { import {
createTicket,
TicketMessage, TicketMessage,
useSSESubscription, useSSESubscription,
useTicketMessages, useTicketMessages,
@ -23,6 +22,7 @@ import {
} from "@root/ticket"; } from "@root/ticket";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { getMessageFromFetchError } from "@utils/backendMessageHandler"; import { getMessageFromFetchError } from "@utils/backendMessageHandler";
import { createTicket, sendFile as sendFileRequest } from "@api/ticket";
type ModalWarningType = type ModalWarningType =
| "errorType" | "errorType"
@ -100,7 +100,7 @@ export default () => {
}, [isChatOpened]); }, [isChatOpened]);
useTicketsFetcher({ useTicketsFetcher({
url: process.env.REACT_APP_DOMAIN + "/heruvym/getTickets", url: `${process.env.REACT_APP_DOMAIN}/heruvym/getTickets`,
ticketsPerPage: 10, ticketsPerPage: 10,
ticketApiPage: 0, ticketApiPage: 0,
onSuccess: (result) => { onSuccess: (result) => {
@ -128,7 +128,7 @@ export default () => {
}); });
useTicketMessages({ useTicketMessages({
url: process.env.REACT_APP_DOMAIN + "/heruvym/getMessages", url: `${process.env.REACT_APP_DOMAIN}/heruvym/getMessages`,
isUnauth: true, isUnauth: true,
ticketId: ticket.sessionData?.ticketId, ticketId: ticket.sessionData?.ticketId,
messagesPerPage: ticket.messagesPerPage, messagesPerPage: ticket.messagesPerPage,
@ -146,9 +146,7 @@ export default () => {
useSSESubscription<TicketMessage>({ useSSESubscription<TicketMessage>({
enabled: enabled:
sseEnabled && isActiveSSETab && Boolean(ticket.sessionData?.sessionId), sseEnabled && isActiveSSETab && Boolean(ticket.sessionData?.sessionId),
url: url: `${process.env.REACT_APP_DOMAIN}/heruvym/ticket?ticket=${ticket.sessionData?.ticketId}&s=${ticket.sessionData?.sessionId}`,
process.env.REACT_APP_DOMAIN +
`/heruvym/ticket?ticket=${ticket.sessionData?.ticketId}&s=${ticket.sessionData?.sessionId}`,
onNewData: (ticketMessages) => { onNewData: (ticketMessages) => {
const isTicketClosed = ticketMessages.some( const isTicketClosed = ticketMessages.some(
(message) => message.session_id === "close", (message) => message.session_id === "close",
@ -195,25 +193,21 @@ export default () => {
let successful = false; let successful = false;
setIsMessageSending(true); setIsMessageSending(true);
if (!ticket.sessionData?.ticketId) { if (!ticket.sessionData?.ticketId) {
try { const [data, createError] = await createTicket(
const data = await createTicket({ messageField,
url: process.env.REACT_APP_DOMAIN + "/heruvym/create", Boolean(user),
body: { );
Title: "Unauth title",
Message: messageField, if (createError || !data) {
},
useToken: Boolean(user),
});
successful = true;
setTicketData({
ticketId: data.Ticket,
sessionId: data.sess,
});
} catch (error: any) {
successful = false; successful = false;
const errorMessage = getMessageFromFetchError(error);
if (errorMessage) enqueueSnackbar(errorMessage); enqueueSnackbar(createError);
} else {
successful = true;
setTicketData({ ticketId: data.Ticket, sessionId: data.sess });
} }
setIsMessageSending(false); setIsMessageSending(false);
} else { } else {
const [_, sendTicketMessageError] = await sendTicketMessage( const [_, sendTicketMessageError] = await sendTicketMessage(
@ -234,45 +228,29 @@ export default () => {
const sendFile = async (file: File) => { const sendFile = async (file: File) => {
if (file === undefined) return true; if (file === undefined) return true;
let data; let ticketId = ticket.sessionData?.ticketId;
if (!ticket.sessionData?.ticketId) { if (!ticket.sessionData?.ticketId) {
try { const [data, createError] = await createTicket("", Boolean(user));
data = await createTicket({ ticketId = data?.Ticket;
url: process.env.REACT_APP_DOMAIN + "/heruvym/create",
body: { if (createError || !data) {
Title: "Unauth title", enqueueSnackbar(createError);
Message: "", } else {
}, setTicketData({ ticketId: data.Ticket, sessionId: data.sess });
useToken: Boolean(user),
});
setTicketData({
ticketId: data.Ticket,
sessionId: data.sess,
});
} catch (error: any) {
const errorMessage = getMessageFromFetchError(error);
if (errorMessage) enqueueSnackbar(errorMessage);
} }
setIsMessageSending(false); setIsMessageSending(false);
} }
const ticketId = ticket.sessionData?.ticketId || data?.Ticket;
if (ticketId !== undefined) { if (ticketId !== undefined) {
if (file.size > MAX_FILE_SIZE) return setModalWarningType("errorSize"); if (file.size > MAX_FILE_SIZE) return setModalWarningType("errorSize");
try {
const body = new FormData();
body.append(file.name, file); const [_, sendFileError] = await sendFileRequest(ticketId, file);
body.append("ticket", ticketId);
await makeRequest({ if (sendFileError) {
url: process.env.REACT_APP_DOMAIN + "/heruvym/sendFiles", enqueueSnackbar(sendFileError);
body: body,
method: "POST",
});
} catch (error: any) {
const errorMessage = getMessageFromFetchError(error);
if (errorMessage) enqueueSnackbar(errorMessage);
} }
return true; return true;
} }
}; };

@ -2,7 +2,7 @@ import { useNavigate } from "react-router-dom";
import { setCash } from "@root/cash"; import { setCash } from "@root/cash";
import { useUserStore } from "@root/user"; import { useUserStore } from "@root/user";
import { payCart } from "@api/cart"; import { cartApi } from "@api/cart";
import { currencyFormatter } from "../../pages/Tariffs/tariffsUtils/currencyFormatter"; import { currencyFormatter } from "../../pages/Tariffs/tariffsUtils/currencyFormatter";
const MINUTE = 1000 * 60; const MINUTE = 1000 * 60;
@ -36,7 +36,7 @@ export const useAfterpay = () => {
async function tryPayCart() { async function tryPayCart() {
tryCount += 1; tryCount += 1;
const [data, payCartError] = await payCart(); const [data, payCartError] = await cartApi.pay();
if (data !== null) if (data !== null)
setCash( setCash(
currencyFormatter.format(Number(data.wallet.cash) / 100), currencyFormatter.format(Number(data.wallet.cash) / 100),

@ -0,0 +1,59 @@
import { useEffect, useLayoutEffect, useRef } from "react";
import { createUserAccount, devlog } from "@frontend/kitui";
import { isAxiosError } from "axios";
import { makeRequest } from "@api/makeRequest";
import type { UserAccount } from "@frontend/kitui";
export const useUserAccountFetcher = <T = UserAccount>({
onError,
onNewUserAccount,
url,
userId,
}: {
url: string;
userId: string | null;
onNewUserAccount: (response: T) => void;
onError?: (error: any) => void;
}) => {
const onNewUserAccountRef = useRef(onNewUserAccount);
const onErrorRef = useRef(onError);
useLayoutEffect(() => {
onNewUserAccountRef.current = onNewUserAccount;
onErrorRef.current = onError;
}, [onError, onNewUserAccount]);
useEffect(() => {
if (!userId) return;
const controller = new AbortController();
makeRequest<never, T>({
url,
contentType: true,
method: "GET",
useToken: true,
withCredentials: false,
signal: controller.signal,
})
.then((result) => {
devlog("User account", result);
onNewUserAccountRef.current(result);
})
.catch((error) => {
devlog("Error fetching user account", error);
if (isAxiosError(error) && error.response?.status === 404) {
createUserAccount(controller.signal, url.replace("get", "create"))
.then((result) => {
devlog("Created user account", result);
onNewUserAccountRef.current(result as T);
})
.catch((error) => {
devlog("Error creating user account", error);
onErrorRef.current?.(error);
});
} else {
onErrorRef.current?.(error);
}
});
return () => controller.abort();
}, [url, userId]);
};