From 17a697893d19f10b5ba3aa05bfd57135de0b8ea0 Mon Sep 17 00:00:00 2001 From: IlyaDoronin Date: Wed, 15 May 2024 14:44:10 +0300 Subject: [PATCH] fix --- src/App.tsx | 59 +------ src/api/auth.ts | 6 +- src/api/cart.ts | 44 ++++- src/api/contactForm.ts | 2 +- src/api/discounts.ts | 2 +- src/api/promocode.ts | 13 +- src/api/question.ts | 2 +- src/api/quiz.ts | 4 +- src/api/result.ts | 120 ++++++++++---- src/api/statistic.ts | 2 +- src/api/tariff.ts | 23 +++ src/api/ticket.ts | 48 +++++- src/api/user.ts | 62 +++++++ src/pages/QuizAnswersPage/CardAnswer.tsx | 37 +++-- src/pages/QuizAnswersPage/QuizAnswersPage.tsx | 23 +-- .../QuizAnswersPage/QuizSettingsMenu.tsx | 9 +- src/pages/QuizAnswersPage/useGetData.ts | 9 +- src/pages/Tariffs/Tariffs.tsx | 156 +++++++++--------- src/pages/auth/RecoverPassword.tsx | 21 +-- src/pages/createQuize/AvailablePrivilege.tsx | 4 +- src/pages/createQuize/QuizCard.tsx | 15 +- src/stores/quizes.ts | 2 +- src/stores/results/actions.ts | 78 +++++---- src/ui_kit/CheckFastlink.tsx | 56 +++---- src/ui_kit/FloatingSupportChat/index.tsx | 84 ++++------ src/utils/hooks/useAfterpay.ts | 4 +- src/utils/hooks/useUserAccountFetcher.ts | 59 +++++++ 27 files changed, 590 insertions(+), 354 deletions(-) create mode 100644 src/api/tariff.ts create mode 100644 src/api/user.ts create mode 100644 src/utils/hooks/useUserAccountFetcher.ts diff --git a/src/App.tsx b/src/App.tsx index 72daabc1..6ae36c59 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -44,6 +44,7 @@ import RecoverPassword from "./pages/auth/RecoverPassword"; import { InfoPrivilege } from "./pages/InfoPrivilege"; import OutdatedLink from "./pages/auth/OutdatedLink"; import { useAfterpay } from "@utils/hooks/useAfterpay"; +import { useUserAccountFetcher } from "@utils/hooks/useUserAccountFetcher"; const MyQuizzesFull = lazy(() => import("./pages/createQuize/MyQuizzesFull")); @@ -92,65 +93,13 @@ const LazyLoading = ({ children, fallback }: SuspenseProps) => ( }>{children} ); -export function useUserAccountFetcher({ - 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({ - 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() { const userId = useUserStore((state) => state.userId); const location = useLocation(); const navigate = useNavigate(); useUserFetcher({ - url: process.env.REACT_APP_DOMAIN + `/user/${userId}`, + url: `${process.env.REACT_APP_DOMAIN}/user/${userId}`, userId, onNewUser: setUser, onError: (error) => { @@ -164,7 +113,7 @@ export default function App() { }); useUserAccountFetcher({ - url: process.env.REACT_APP_DOMAIN + "/customer/account", + url: `${process.env.REACT_APP_DOMAIN}/customer/account`, userId, onNewUserAccount: setCustomerAccount, onError: (error) => { @@ -179,7 +128,7 @@ export default function App() { }); useUserAccountFetcher({ - url: process.env.REACT_APP_DOMAIN + "/squiz/account/get", + url: `${process.env.REACT_APP_DOMAIN}/squiz/account/get`, userId, onNewUserAccount: setUserAccount, onError: (error) => { diff --git a/src/api/auth.ts b/src/api/auth.ts index 67b19907..194ece1b 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -8,7 +8,7 @@ import type { } from "@frontend/kitui"; 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 ( login: string, @@ -79,11 +79,11 @@ export const recover = async ( formData.append("email", email); formData.append( "RedirectionURL", - process.env.REACT_APP_DOMAIN + "/changepwd", + `${process.env.REACT_APP_DOMAIN}/changepwd`, ); const recoverResponse = await makeRequest({ - url: process.env.REACT_APP_DOMAIN + "/codeword/recover", + url: `${process.env.REACT_APP_DOMAIN}/codeword/recover`, body: formData, useToken: false, withCredentials: true, diff --git a/src/api/cart.ts b/src/api/cart.ts index 80d46f27..257a5035 100644 --- a/src/api/cart.ts +++ b/src/api/cart.ts @@ -3,13 +3,13 @@ import { makeRequest } from "@api/makeRequest"; 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 { const payCartResponse = await makeRequest({ method: "POST", - url: `${API_URL}/cart/pay`, + url: `${API_URL}/pay`, useToken: true, }); @@ -20,3 +20,41 @@ export const payCart = async (): Promise<[UserAccount | null, string?]> => { return [null, `Не удалось оплатить товар из корзины. ${error}`]; } }; + +const addCartItem = async (id: string): Promise<[unknown | null, string?]> => { + try { + const addedItem = await makeRequest({ + 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({ + 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, +}; diff --git a/src/api/contactForm.ts b/src/api/contactForm.ts index 00ac2b77..6ed0f71d 100644 --- a/src/api/contactForm.ts +++ b/src/api/contactForm.ts @@ -2,7 +2,7 @@ import { makeRequest } from "@api/makeRequest"; 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 = { contact: string; diff --git a/src/api/discounts.ts b/src/api/discounts.ts index 2cd1fdf1..c025ec7f 100644 --- a/src/api/discounts.ts +++ b/src/api/discounts.ts @@ -3,7 +3,7 @@ import { parseAxiosError } from "@utils/parse-error"; 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 ( userId: string, diff --git a/src/api/promocode.ts b/src/api/promocode.ts index 2da8fd5b..87e90a01 100644 --- a/src/api/promocode.ts +++ b/src/api/promocode.ts @@ -5,23 +5,26 @@ import { parseAxiosError } from "@utils/parse-error"; type ActivatePromocodeRequest = { codeword: string } | { fastLink: 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 { const response = await makeRequest< ActivatePromocodeRequest, ActivatePromocodeResponse >({ method: "POST", - url: API_URL + "/activate", + url: `${API_URL}/activate`, body: { codeword: promocode }, contentType: true, }); - return response.greetings; + return [response.greetings]; } catch (nativeError) { const [error] = parseAxiosError(nativeError); - throw new Error(error); + + return [null, `Ошибка при активации промокода. ${error}`]; } }; diff --git a/src/api/question.ts b/src/api/question.ts index fb5fecf1..7a62485f 100644 --- a/src/api/question.ts +++ b/src/api/question.ts @@ -20,7 +20,7 @@ import { import { replaceSpacesToEmptyLines } from "../utils/replaceSpacesToEmptyLines"; import { parseAxiosError } from "@utils/parse-error"; -const API_URL = process.env.REACT_APP_DOMAIN + "/squiz"; +const API_URL = "/squiz"; export const createQuestion = async ( body: CreateQuestionRequest, diff --git a/src/api/quiz.ts b/src/api/quiz.ts index f4dc5637..476563ff 100644 --- a/src/api/quiz.ts +++ b/src/api/quiz.ts @@ -13,8 +13,8 @@ type AddedQuizImagesResponse = { [key: string]: string; }; -const API_URL = process.env.REACT_APP_DOMAIN + "/squiz"; -const IMAGES_URL = process.env.REACT_APP_DOMAIN + "/squizstorer"; +const API_URL = `${process.env.REACT_APP_DOMAIN}/squiz`; +const IMAGES_URL = `${process.env.REACT_APP_DOMAIN}/squizstorer`; export const createQuiz = async ( body?: Partial, diff --git a/src/api/result.ts b/src/api/result.ts index edada028..bb176219 100644 --- a/src/api/result.ts +++ b/src/api/result.ts @@ -1,6 +1,8 @@ import { makeRequest } from "@api/makeRequest"; import { RawResult } from "@model/result/result"; +import { parseAxiosError } from "@utils/parse-error"; + interface IResultListBody { to: number; from: string; @@ -29,46 +31,102 @@ export interface IAnswerResult { 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) => { - return makeRequest({ - method: "POST", - url: `${API_URL}/results/getResults/${quizId}`, - body: { page: page, limit: 10, ...body }, - }); +const getResultList = async ( + quizId: number, + page: number, + body: any, +): Promise<[RawResult | null, string?]> => { + try { + const resultList = await makeRequest({ + 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) => { - return makeRequest({ - method: "DELETE", - url: `${API_URL}/results/delete/${resultId}`, - body: {}, - }); +const deleteResult = async ( + resultId: number, +): Promise<[unknown | null, string?]> => { + try { + const deletedResult = await makeRequest({ + method: "DELETE", + url: `${API_URL}/results/delete/${resultId}`, + body: {}, + }); + + return [deletedResult]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + + return [null, `Не удалось удалить результат. ${error}`]; + } }; -const obsolescenceResult = (idResultArray: number[]) => { - return makeRequest({ - method: "PATCH", - url: `${API_URL}/result/seen`, - body: { answers: idResultArray }, - }); +const obsolescenceResult = async ( + idResultArray: number[], +): Promise<[unknown | null, string?]> => { + try { + const obsolescencedResult = await makeRequest({ + 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) => { - return makeRequest({ - method: "GET", - url: `${API_URL}/result/${resultId}`, - }); +const getAnswerResultList = async ( + resultId: number, +): Promise<[IAnswerResult[] | null, string?]> => { + try { + const answerResultList = await makeRequest({ + method: "GET", + url: `${API_URL}/result/${resultId}`, + }); + + return [answerResultList]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + + return [null, `Не удалось получить список результатов. ${error}`]; + } }; -const AnswerResultListEx = (quizId: number, body: any) => { - return makeRequest({ - method: "POST", - url: `${API_URL}/results/${quizId}/export`, - body: body, - responseType: "blob", - }); +const AnswerResultListEx = async ( + quizId: number, + body: any, +): Promise<[unknown | null, string?]> => { + try { + const answerResultListEx = await makeRequest({ + 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 = { diff --git a/src/api/statistic.ts b/src/api/statistic.ts index 110d637f..b085b64b 100644 --- a/src/api/statistic.ts +++ b/src/api/statistic.ts @@ -27,7 +27,7 @@ type TRequest = { 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 ( quizId: string, diff --git a/src/api/tariff.ts b/src/api/tariff.ts new file mode 100644 index 00000000..0ddaceee --- /dev/null +++ b/src/api/tariff.ts @@ -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({ + method: "GET", + url: `${API_URL}?page=${page}&limit=100`, + }); + return [tariffs]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + + return [null, `Ошибка при получении списка тарифов. ${error}`]; + } +}; diff --git a/src/api/ticket.ts b/src/api/ticket.ts index bdbe2f83..eb681604 100644 --- a/src/api/ticket.ts +++ b/src/api/ticket.ts @@ -1,9 +1,12 @@ import { makeRequest } from "@api/makeRequest"; +import { createTicket as createTicketRequest } from "@frontend/kitui"; import { parseAxiosError } from "../utils/parse-error"; 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 ( ticketId: string, @@ -44,3 +47,46 @@ export const shownMessage = async (id: string): Promise<[null, string?]> => { 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({ + 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}`]; + } +}; diff --git a/src/api/user.ts b/src/api/user.ts new file mode 100644 index 00000000..f7bf30d2 --- /dev/null +++ b/src/api/user.ts @@ -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({ + 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({ + 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}`]; + } +}; diff --git a/src/pages/QuizAnswersPage/CardAnswer.tsx b/src/pages/QuizAnswersPage/CardAnswer.tsx index d232bb9a..00df02d0 100644 --- a/src/pages/QuizAnswersPage/CardAnswer.tsx +++ b/src/pages/QuizAnswersPage/CardAnswer.tsx @@ -19,9 +19,10 @@ import { useQuizStore } from "@root/quizes/store"; import { useQuestionsStore } from "@root/questions/store"; import AddressIcon from "@icons/ContactFormIcon/AddressIcon"; -import type { AxiosError } from "axios"; import { DeleteModal } from "./DeleteModal"; +import type { AnyTypedQuizQuestion } from "@model/questionTypes/shared"; + interface CardAnswerProps { isNew: boolean; idResult: number; @@ -50,29 +51,31 @@ export const CardAnswer: FC = ({ const theme = useTheme(); const isTablet = useMediaQuery(theme.breakpoints.down(1000)); const [resultsAnswer, setResultsAnswer] = useState([]); - const [questionsResultState, setQuestionsResultState] = useState([]); + const [questionsResultState, setQuestionsResultState] = useState< + AnyTypedQuizQuestion[] + >([]); const { editQuizId } = useQuizStore(); const { questions } = useQuestionsStore(); const openResults = async () => { setIsOpen(!isOpen); if (!isOpen) { - try { - 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; + const [resAnswer, answerError] = await resultApi.getAnswerList(idResult); - if (error.response?.statusText === "Payment Required") { - openPrePaymentModal(); - } + if (answerError || !resAnswer) { + 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); } }; diff --git a/src/pages/QuizAnswersPage/QuizAnswersPage.tsx b/src/pages/QuizAnswersPage/QuizAnswersPage.tsx index c2895ea5..8355218c 100644 --- a/src/pages/QuizAnswersPage/QuizAnswersPage.tsx +++ b/src/pages/QuizAnswersPage/QuizAnswersPage.tsx @@ -78,17 +78,20 @@ export const QuizAnswersPage: FC = () => { const PaginationHC = async (e: ChangeEvent, value: number) => { setPage(value); - try { - if (editQuizId !== null) { - const result = await resultApi.getList( - editQuizId, - value - 1, - parseFilters(filterNew, filterDate), - ); - setResults(result); + if (editQuizId !== null) { + const [result, resultError] = await resultApi.getList( + editQuizId, + value - 1, + parseFilters(filterNew, filterDate), + ); + + 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); } }; diff --git a/src/pages/QuizAnswersPage/QuizSettingsMenu.tsx b/src/pages/QuizAnswersPage/QuizSettingsMenu.tsx index d2167597..9499298a 100644 --- a/src/pages/QuizAnswersPage/QuizSettingsMenu.tsx +++ b/src/pages/QuizAnswersPage/QuizSettingsMenu.tsx @@ -98,11 +98,18 @@ export const QuizSettingsMenu: FC = ({ }} onclickUpdate={async () => { if (editQuizId !== null) { - const result = await resultApi.getList( + const [result, resultError] = await resultApi.getList( editQuizId, page - 1, parseFilters(filterNew, filterDate), ); + + if (resultError || !result) { + console.error(resultError); + + return; + } + setResults(result); } else { console.error("editQuizId is null"); diff --git a/src/pages/QuizAnswersPage/useGetData.ts b/src/pages/QuizAnswersPage/useGetData.ts index 4fbbcfac..184fe500 100644 --- a/src/pages/QuizAnswersPage/useGetData.ts +++ b/src/pages/QuizAnswersPage/useGetData.ts @@ -37,11 +37,18 @@ export const useGetData = (filterNew: string, filterDate: string): void => { setQuestions(questions); - const result = await resultApi.getList( + const [result, resultError] = await resultApi.getList( editQuizId, 0, parseFilters(filterNew, filterDate), ); + + if (resultError || !result) { + console.error(resultError); + + return; + } + if (result.total_count === 0) { console.error("No results found"); } diff --git a/src/pages/Tariffs/Tariffs.tsx b/src/pages/Tariffs/Tariffs.tsx index b5676b2d..44dc051c 100644 --- a/src/pages/Tariffs/Tariffs.tsx +++ b/src/pages/Tariffs/Tariffs.tsx @@ -33,6 +33,9 @@ import { currencyFormatter } from "./tariffsUtils/currencyFormatter"; import { useWallet, setCash } from "@root/cash"; import { handleLogoutClick } from "@utils/HandleLogoutClick"; 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"; @@ -62,25 +65,20 @@ function TariffPage() { const getTariffsList = async (): Promise => { const tariffsList: Tariff[] = []; - const { tariffs, totalPages } = await makeRequest< - never, - GetTariffsResponse - >({ - method: "GET", - url: process.env.REACT_APP_DOMAIN + "/strator/tariff?page=1&limit=100", - }); + const [tariffsResponse, tariffsResponseError] = await getTariffs(1); - tariffsList.push(...tariffs); + if (tariffsResponseError || !tariffsResponse) { + return tariffsList; + } - for (let page = 2; page <= totalPages; page += 1) { - const tariffsResult = await makeRequest({ - method: "GET", - url: - process.env.REACT_APP_DOMAIN + - `/strator/tariff?page=${page}&limit=100`, - }); + tariffsList.push(...tariffsResponse.tariffs); - 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; @@ -88,10 +86,12 @@ function TariffPage() { useEffect(() => { const get = async () => { - const user = await makeRequest({ - method: "GET", - url: process.env.REACT_APP_DOMAIN + "/customer/account", - }); + const [user, userError] = await getUser(); + + if (userError) { + return; + } + const tariffsList = await getTariffsList(); if (userId) { @@ -123,27 +123,22 @@ function TariffPage() { outCart(user.cart); } //Добавляем желаемый тариф в корзину - await makeRequest({ - method: "PATCH", - url: process.env.REACT_APP_DOMAIN + `/customer/cart?id=${id}`, - }); - //Если нам хватает денежек - покупаем тариф + const [_, addError] = await cartApi.add(id); - try { - const data = await makeRequest({ - method: "POST", - url: process.env.REACT_APP_DOMAIN + "/customer/cart/pay", - }); - setCash( - currencyFormatter.format(Number(data.wallet.cash) / 100), - Number(data.wallet.cash), - Number(data.wallet.cash) / 100, - ); - enqueueSnackbar("Тариф успешно приобретён"); - } catch (e) { + if (addError) { + //Развращаем товары в корзину + inCart(); + + return; + } + + //Если нам хватает денежек - покупаем тариф + const [data, payError] = await cartApi.pay(); + + if (payError || !data) { //если денег не хватило - if (e.response.data.message.includes("insufficient funds")) { - let cashDif = Number(e.response.data.message.split(":")[1]); + if (addError) { + let cashDif = Number(addError.split(":")[1]); var link = document.createElement("a"); link.href = `https://${isTestServer ? "s" : ""}hub.pena.digital/quizpayment?action=squizpay&dif=${cashDif}&data=${token}&userid=${userId}`; document.body.appendChild(link); @@ -152,7 +147,18 @@ function TariffPage() { } //другая ошибка enqueueSnackbar("Произошла ошибка. Попробуйте позже"); + + return; } + + setCash( + currencyFormatter.format(Number(data.wallet.cash) / 100), + Number(data.wallet.cash), + Number(data.wallet.cash) / 100, + ); + + enqueueSnackbar("Тариф успешно приобретён"); + //Развращаем товары в корзину inCart(); }; @@ -179,31 +185,32 @@ function TariffPage() { return tariff.privileges[0].privilegeId !== "squizHideBadge"; }); - function handleApplyPromocode() { + async function handleApplyPromocode() { if (!promocodeField) return; - activatePromocode(promocodeField) - .then(async (greetings) => { - enqueueSnackbar(greetings); + const [greetings, error] = await activatePromocode(promocodeField); - if (!userId) { - return; - } + if (error) { + enqueueSnackbar(error); - const [discounts, discountsError] = await getDiscounts(userId); + return; + } - if (discountsError) { - throw new Error(discountsError); - } + enqueueSnackbar(greetings); - if (discounts?.length) { - setDiscounts(discounts); - } - }) - .catch((error) => { - if (error.message !== "" && typeof error.message === "string") - enqueueSnackbar(error.message); - }); + if (!userId) { + return; + } + + const [discounts, discountsError] = await getDiscounts(userId); + + if (discountsError) { + throw new Error(discountsError); + } + + if (discounts?.length) { + setDiscounts(discounts); + } } return ( @@ -396,19 +403,18 @@ export const inCart = () => { let saveCart = JSON.parse(localStorage.getItem("saveCart") || "[]"); if (Array.isArray(saveCart)) { saveCart.forEach(async (id: string) => { - try { - await makeRequest({ - method: "PATCH", - url: process.env.REACT_APP_DOMAIN + `/customer/cart?id=${id}`, - }); + const [_, addError] = await cartApi.add(id); + if (addError) { + console.error(addError); + } else { let index = saveCart.indexOf("green"); + if (index !== -1) { saveCart.splice(index, 1); } + localStorage.setItem("saveCart", JSON.stringify(saveCart)); - } catch (e) { - console.error("Я не смог добавить тариф в корзину :( " + id); } }); } else { @@ -418,16 +424,16 @@ export const inCart = () => { const outCart = (cart: string[]) => { //Сделаем муторно и подольше, зато при прерывании сессии данные потеряются минимально cart.forEach(async (id: string) => { - try { - await makeRequest({ - method: "DELETE", - url: process.env.REACT_APP_DOMAIN + `/customer/cart?id=${id}`, - }); - let saveCart = JSON.parse(localStorage.getItem("saveCart") || "[]"); - saveCart = saveCart.push(id); - localStorage.setItem("saveCart", JSON.stringify(saveCart)); - } catch (e) { - console.error("Я не смог удалить из корзины тариф :("); + const [_, deleteError] = await cartApi.delete(id); + + if (deleteError) { + console.error(deleteError); + + return; } + + let saveCart = JSON.parse(localStorage.getItem("saveCart") || "[]"); + saveCart = saveCart.push(id); + localStorage.setItem("saveCart", JSON.stringify(saveCart)); }); }; diff --git a/src/pages/auth/RecoverPassword.tsx b/src/pages/auth/RecoverPassword.tsx index 4ee52f27..8afd81ca 100644 --- a/src/pages/auth/RecoverPassword.tsx +++ b/src/pages/auth/RecoverPassword.tsx @@ -21,6 +21,7 @@ import { useUserStore } from "@root/user"; import { makeRequest } from "@api/makeRequest"; import { setAuthToken } from "@frontend/kitui"; import { parseAxiosError } from "@utils/parse-error"; +import { recoverUser } from "@api/user"; interface Values { password: string; } @@ -55,21 +56,17 @@ export default function RecoverPassword() { onSubmit: async (values, formikHelpers) => { if (tokenUser) { setAuthToken(tokenUser || ""); - try { - const response = await makeRequest({ - url: process.env.REACT_APP_DOMAIN + "/user/", - method: "PATCH", - body: { password: values.password }, - }); + const [_, recoverError] = await recoverUser(values.password); + + if (recoverError) { + setAuthToken(""); + enqueueSnackbar( + `Извините, произошла ошибка, попробуйте повторить позже. ${recoverError}`, + ); + } else { setIsDialogOpen(false); navigate("/"); enqueueSnackbar("Пароль успешно сменён"); - } catch (nativeError) { - const [error] = parseAxiosError(nativeError); - setAuthToken(""); - enqueueSnackbar( - `Извините, произошла ошибка, попробуйте повторить позже. ${error}`, - ); } } else { enqueueSnackbar("Неверный url-адрес"); diff --git a/src/pages/createQuize/AvailablePrivilege.tsx b/src/pages/createQuize/AvailablePrivilege.tsx index abcdd9e0..c1a0e905 100644 --- a/src/pages/createQuize/AvailablePrivilege.tsx +++ b/src/pages/createQuize/AvailablePrivilege.tsx @@ -7,7 +7,7 @@ import { } from "@root/user"; import { clearAuthToken, getMessageFromFetchError } from "@frontend/kitui"; import { enqueueSnackbar } from "notistack"; -import { useUserAccountFetcher } from "../../App"; +import { useUserAccountFetcher } from "@utils/hooks/useUserAccountFetcher"; import { useNavigate } from "react-router-dom"; import moment from "moment"; @@ -17,7 +17,7 @@ export default function AvailablePrivilege() { const userId = useUserStore((state) => state.userId); const navigate = useNavigate(); useUserAccountFetcher({ - url: process.env.REACT_APP_DOMAIN + "/squiz/account/get", + url: `${process.env.REACT_APP_DOMAIN}/squiz/account/get`, userId, onNewUserAccount: setUserAccount, onError: (error) => { diff --git a/src/pages/createQuize/QuizCard.tsx b/src/pages/createQuize/QuizCard.tsx index dfd083cf..ff114d46 100755 --- a/src/pages/createQuize/QuizCard.tsx +++ b/src/pages/createQuize/QuizCard.tsx @@ -20,6 +20,7 @@ import { enqueueSnackbar } from "notistack"; import { useDomainDefine } from "@utils/hooks/useDomainDefine"; import CopyIcon from "@icons/CopyIcon"; import ChartIcon from "@icons/ChartIcon"; +import { cartApi } from "@api/cart"; interface Props { quiz: Quiz; @@ -55,17 +56,17 @@ export default function QuizCard({ useLayoutEffect(() => { const pay = async () => { - try { - await makeRequest({ - method: "POST", - url: process.env.REACT_APP_DOMAIN + "/customer/cart/pay", - }); - inCart(); - } catch (e) { + const [_, payError] = await cartApi.pay(); + + if (payError) { enqueueSnackbar( "Попробуйте снова купить тариф после зачисления средств", ); + + return; } + + inCart(); }; const params = new URLSearchParams(window.location.search); const fromSquiz = params.get("action"); diff --git a/src/stores/quizes.ts b/src/stores/quizes.ts index 1f481f58..cdee3acc 100644 --- a/src/stores/quizes.ts +++ b/src/stores/quizes.ts @@ -129,7 +129,7 @@ export const quizStore = create()( config: { noStartPage: false, type: "", // quiz или form - logo: process.env.REACT_APP_DOMAIN + "/img/logo", + logo: `${process.env.REACT_APP_DOMAIN}/img/logo`, startpage: { description: "", // приветственный текст опроса button: "", // текст на кнопке начала опроса diff --git a/src/stores/results/actions.ts b/src/stores/results/actions.ts index d1d09ef9..c88a2984 100644 --- a/src/stores/results/actions.ts +++ b/src/stores/results/actions.ts @@ -39,15 +39,16 @@ export const deleteResult = async (resultId: number) => .results.find((r) => r.id === resultId); if (!result) return; - try { - await resultApi.delete(Number(result.id)); - removeResult(resultId); - } catch (error) { - devlog("Error delete result", error); + const [_, deleteError] = await resultApi.delete(Number(result.id)); - const message = getMessageFromFetchError(error) ?? ""; - enqueueSnackbar(`Не удалось удалить результат. ${message}`); + if (deleteError) { + devlog("Error delete result", deleteError); + enqueueSnackbar(deleteError); + + return; } + + removeResult(resultId); }); export const obsolescenceResult = async ( @@ -68,17 +69,24 @@ export const obsolescenceResult = async ( if (typeof lossDebouncer === "number") clearTimeout(lossDebouncer); lossDebouncer = setTimeout(async () => { //стреляем на лишение новизны - try { - await resultApi.obsolescence(lossId); - lossId = []; - } catch (error) { - devlog("Error", error); + const [_, obsolescenceError] = await resultApi.obsolescence(lossId); - const message = getMessageFromFetchError(error) ?? ""; - enqueueSnackbar(`Ошибка. ${message}`); + if (obsolescenceError) { + devlog("Error", obsolescenceError); + + enqueueSnackbar(obsolescenceError); + + return; } + + lossId = []; }, 3000); - const resultList = await resultApi.getList(editQuizId); + const [resultList, resultError] = await resultApi.getList(editQuizId); + + if (resultError || !resultList) { + return; + } + setResults(resultList); }, ); @@ -90,30 +98,28 @@ export const ExportResults = async ( openPrePaymentModal: () => void, editQuizId: number, ) => { - try { - const data = await resultApi.export( - editQuizId, - parseFilters(filterNew, filterDate), - ); + const [data, resultError] = await resultApi.export( + editQuizId, + parseFilters(filterNew, filterDate), + ); - console.log(typeof data); + if (resultError) { + openPrePaymentModal(); - 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(); - } catch (nativeError) { - const error = nativeError as AxiosError; - - if (error.response?.statusText === "Payment Required") { - openPrePaymentModal(); - } + return; } + + 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( diff --git a/src/ui_kit/CheckFastlink.tsx b/src/ui_kit/CheckFastlink.tsx index f646cba9..65c0a820 100644 --- a/src/ui_kit/CheckFastlink.tsx +++ b/src/ui_kit/CheckFastlink.tsx @@ -13,7 +13,7 @@ import { useUserStore, } from "@root/user"; import { parseAxiosError } from "@utils/parse-error"; -import { useUserAccountFetcher } from "../App"; +import { useUserAccountFetcher } from "@utils/hooks/useUserAccountFetcher"; import type { Discount } from "@model/discounts"; import { clearAuthToken, @@ -23,6 +23,8 @@ import { } from "@frontend/kitui"; import { useNavigate } from "react-router-dom"; import { isAxiosError } from "axios"; +import { activatePromocode } from "@api/promocode"; +import { getAccount } from "@api/user"; export function CheckFastlink() { const user = useUserStore(); @@ -50,40 +52,28 @@ export function CheckFastlink() { const fetchPromocode = async () => { if (promocode.length > 0) { - try { - const response = await makeRequest< - { codeword: string } | { fastLink: string }, - { greetings: string } - >({ - 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({ - 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); + const [greetings, activationError] = await activatePromocode(promocode); + + if (activationError || !greetings) { + enqueueSnackbar(activationError); 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; } }; diff --git a/src/ui_kit/FloatingSupportChat/index.tsx b/src/ui_kit/FloatingSupportChat/index.tsx index 5e1bc3ec..3a93efbc 100644 --- a/src/ui_kit/FloatingSupportChat/index.tsx +++ b/src/ui_kit/FloatingSupportChat/index.tsx @@ -1,5 +1,4 @@ import { - createTicket, TicketMessage, useSSESubscription, useTicketMessages, @@ -23,6 +22,7 @@ import { } from "@root/ticket"; import { enqueueSnackbar } from "notistack"; import { getMessageFromFetchError } from "@utils/backendMessageHandler"; +import { createTicket, sendFile as sendFileRequest } from "@api/ticket"; type ModalWarningType = | "errorType" @@ -100,7 +100,7 @@ export default () => { }, [isChatOpened]); useTicketsFetcher({ - url: process.env.REACT_APP_DOMAIN + "/heruvym/getTickets", + url: `${process.env.REACT_APP_DOMAIN}/heruvym/getTickets`, ticketsPerPage: 10, ticketApiPage: 0, onSuccess: (result) => { @@ -128,7 +128,7 @@ export default () => { }); useTicketMessages({ - url: process.env.REACT_APP_DOMAIN + "/heruvym/getMessages", + url: `${process.env.REACT_APP_DOMAIN}/heruvym/getMessages`, isUnauth: true, ticketId: ticket.sessionData?.ticketId, messagesPerPage: ticket.messagesPerPage, @@ -146,9 +146,7 @@ export default () => { useSSESubscription({ enabled: sseEnabled && isActiveSSETab && Boolean(ticket.sessionData?.sessionId), - url: - process.env.REACT_APP_DOMAIN + - `/heruvym/ticket?ticket=${ticket.sessionData?.ticketId}&s=${ticket.sessionData?.sessionId}`, + url: `${process.env.REACT_APP_DOMAIN}/heruvym/ticket?ticket=${ticket.sessionData?.ticketId}&s=${ticket.sessionData?.sessionId}`, onNewData: (ticketMessages) => { const isTicketClosed = ticketMessages.some( (message) => message.session_id === "close", @@ -195,25 +193,21 @@ export default () => { let successful = false; setIsMessageSending(true); if (!ticket.sessionData?.ticketId) { - try { - const data = await createTicket({ - url: process.env.REACT_APP_DOMAIN + "/heruvym/create", - body: { - Title: "Unauth title", - Message: messageField, - }, - useToken: Boolean(user), - }); - successful = true; - setTicketData({ - ticketId: data.Ticket, - sessionId: data.sess, - }); - } catch (error: any) { + const [data, createError] = await createTicket( + messageField, + Boolean(user), + ); + + if (createError || !data) { successful = false; - const errorMessage = getMessageFromFetchError(error); - if (errorMessage) enqueueSnackbar(errorMessage); + + enqueueSnackbar(createError); + } else { + successful = true; + + setTicketData({ ticketId: data.Ticket, sessionId: data.sess }); } + setIsMessageSending(false); } else { const [_, sendTicketMessageError] = await sendTicketMessage( @@ -234,45 +228,29 @@ export default () => { const sendFile = async (file: File) => { if (file === undefined) return true; - let data; + let ticketId = ticket.sessionData?.ticketId; if (!ticket.sessionData?.ticketId) { - try { - data = await createTicket({ - url: process.env.REACT_APP_DOMAIN + "/heruvym/create", - body: { - Title: "Unauth title", - Message: "", - }, - useToken: Boolean(user), - }); - setTicketData({ - ticketId: data.Ticket, - sessionId: data.sess, - }); - } catch (error: any) { - const errorMessage = getMessageFromFetchError(error); - if (errorMessage) enqueueSnackbar(errorMessage); + const [data, createError] = await createTicket("", Boolean(user)); + ticketId = data?.Ticket; + + if (createError || !data) { + enqueueSnackbar(createError); + } else { + setTicketData({ ticketId: data.Ticket, sessionId: data.sess }); } + setIsMessageSending(false); } - const ticketId = ticket.sessionData?.ticketId || data?.Ticket; if (ticketId !== undefined) { if (file.size > MAX_FILE_SIZE) return setModalWarningType("errorSize"); - try { - const body = new FormData(); - body.append(file.name, file); - body.append("ticket", ticketId); - await makeRequest({ - url: process.env.REACT_APP_DOMAIN + "/heruvym/sendFiles", - body: body, - method: "POST", - }); - } catch (error: any) { - const errorMessage = getMessageFromFetchError(error); - if (errorMessage) enqueueSnackbar(errorMessage); + const [_, sendFileError] = await sendFileRequest(ticketId, file); + + if (sendFileError) { + enqueueSnackbar(sendFileError); } + return true; } }; diff --git a/src/utils/hooks/useAfterpay.ts b/src/utils/hooks/useAfterpay.ts index 9f500764..2a481619 100644 --- a/src/utils/hooks/useAfterpay.ts +++ b/src/utils/hooks/useAfterpay.ts @@ -2,7 +2,7 @@ import { useNavigate } from "react-router-dom"; import { setCash } from "@root/cash"; import { useUserStore } from "@root/user"; -import { payCart } from "@api/cart"; +import { cartApi } from "@api/cart"; import { currencyFormatter } from "../../pages/Tariffs/tariffsUtils/currencyFormatter"; const MINUTE = 1000 * 60; @@ -36,7 +36,7 @@ export const useAfterpay = () => { async function tryPayCart() { tryCount += 1; - const [data, payCartError] = await payCart(); + const [data, payCartError] = await cartApi.pay(); if (data !== null) setCash( currencyFormatter.format(Number(data.wallet.cash) / 100), diff --git a/src/utils/hooks/useUserAccountFetcher.ts b/src/utils/hooks/useUserAccountFetcher.ts new file mode 100644 index 00000000..7464e073 --- /dev/null +++ b/src/utils/hooks/useUserAccountFetcher.ts @@ -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 = ({ + 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({ + 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]); +};