diff --git a/.gitea/workflows/deployProd.yml b/.gitea/workflows/deployProd.yml
index ad853873..cd705f0c 100644
--- a/.gitea/workflows/deployProd.yml
+++ b/.gitea/workflows/deployProd.yml
@@ -2,9 +2,8 @@ name: Deploy
run-name: ${{ gitea.actor }} build image and push to container registry
on:
- push:
- branches:
- - 'main'
+ registry_package:
+ types: [published]
jobs:
CreateImage:
diff --git a/.gitea/workflows/deployStaging.yml b/.gitea/workflows/deployStaging.yml
index 88de5f30..97c15694 100644
--- a/.gitea/workflows/deployStaging.yml
+++ b/.gitea/workflows/deployStaging.yml
@@ -2,25 +2,30 @@ name: Deploy
run-name: ${{ gitea.actor }} build image and push to container registry
on:
- push:
- branches:
- - 'staging'
+ registry_package:
+ types: [published]
jobs:
- CreateImage:
- runs-on: [skeris]
- uses: http://gitea.pena/PenaDevops/actions.git/.gitea/workflows/build-image.yml@v1.1.6-p
- with:
- runner: skeris
- secrets:
- REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
- REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
+ # CreateImage:
+ # runs-on: [skeris]
+ # uses: http://gitea.pena/PenaDevops/actions.git/.gitea/workflows/build-image.yml@v1.1.6-p
+ # with:
+ # runner: skeris
+ # secrets:
+ # REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
+ # REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
DeployService:
+ if: contains(github.event.package.name, 'staging')
runs-on: [frontstaging]
- needs: CreateImage
- uses: http://gitea.pena/PenaDevops/actions.git/.gitea/workflows/deploy.yml@v1.1.4-p7
- with:
- runner: frontstaging
- actionid: ${{ gitea.run_id }}
+ container:
+ image: gitea.pena:3000/penadevops/container-images/node-compose:main
+ env:
+ GITHUB_RUN_NUMBER: "${{ inputs.actionid }}"
+ volumes:
+ - /run/user/1000/docker/docker.sock:/run/user/1000/docker/docker.sock
+ steps:
+ - name: Check out repository code
+ uses: http://gitea.pena:3000/PenaDevops/actions.git/checkout@v1
+ - run: compose -f deployments/staging/docker-compose.yaml up -d
diff --git a/Containerfile b/Containerfile
deleted file mode 100644
index 95c38239..00000000
--- a/Containerfile
+++ /dev/null
@@ -1,13 +0,0 @@
-FROM gitea.pena/penadevops/container-images/node:main as build
-
-WORKDIR /usr/app
-COPY . .
-
-
-RUN npm install --force && yarn cache clean
-RUN psstat.sh "npm run build"
-
-FROM gitea.pena/penadevops/container-images/nginx:main as result
-WORKDIR /usr/share/nginx/html
-COPY --from=build /usr/app/build/ /usr/share/nginx/html
-COPY hub.conf /etc/nginx/conf.d/default.conf
diff --git a/Dockerfile b/Dockerfile
index e69de29b..95c38239 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -0,0 +1,13 @@
+FROM gitea.pena/penadevops/container-images/node:main as build
+
+WORKDIR /usr/app
+COPY . .
+
+
+RUN npm install --force && yarn cache clean
+RUN psstat.sh "npm run build"
+
+FROM gitea.pena/penadevops/container-images/nginx:main as result
+WORKDIR /usr/share/nginx/html
+COPY --from=build /usr/app/build/ /usr/share/nginx/html
+COPY hub.conf /etc/nginx/conf.d/default.conf
diff --git a/deployments/staging/docker-compose.yaml b/deployments/staging/docker-compose.yaml
index 77b80c51..e9efd2cc 100644
--- a/deployments/staging/docker-compose.yaml
+++ b/deployments/staging/docker-compose.yaml
@@ -2,6 +2,7 @@ services:
squiz:
container_name: squiz
restart: unless-stopped
- image: gitea.pena/squiz/frontpanel/staging:$GITHUB_RUN_NUMBER
+ image: gitea.pena/squiz/frontpanel/staging:latest
hostname: squiz
tty: true
+ pull_policy: always
diff --git a/package.json b/package.json
index 3f8736f3..e356838d 100755
--- a/package.json
+++ b/package.json
@@ -6,7 +6,7 @@
"@craco/craco": "^7.0.0",
"@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5",
- "@frontend/kitui": "^1.0.108",
+ "@frontend/kitui": "1.0.110",
"@frontend/squzanswerer": "^1.0.57",
"@mui/icons-material": "^5.10.14",
"@mui/material": "^5.10.14",
@@ -69,6 +69,7 @@
"test": "craco test",
"eject": "craco eject",
"code:format": "prettier --write --ignore-unknown",
+ "deploy": "docker login gitea.pena && docker build -t gitea.pena/squiz/frontpanel/$(git branch --show-current):latest . && docker push gitea.pena/squiz/frontpanel/$(git branch --show-current):latest",
"prepare": "husky install",
"cypress:open": "cypress open",
"cypress:run": "cypress run"
diff --git a/public/index.html b/public/index.html
index 0df06af1..005d6ef6 100755
--- a/public/index.html
+++ b/public/index.html
@@ -165,7 +165,7 @@
console.log(params.get("debug"))
if (params.get("debug")) {
console.log(
- "mhgfhdhfjhffhfhjfghjgf"
+ "params.get(debug) is true"
)
let scriptTag = document.createElement('script');
scriptTag.setAttribute('src', "https://markknol.github.io/console-log-viewer/console-log-viewer.js");
diff --git a/src/App.tsx b/src/App.tsx
index d6195646..78bf452b 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,4 +1,4 @@
-import { clearAuthToken, getMessageFromFetchError, UserAccount, useUserFetcher } from "@frontend/kitui";
+import { clearAuthToken, createMakeRequestConfig, getMessageFromFetchError, handleComponentError, UserAccount, useTicketsFetcher, useUserFetcher } from "@frontend/kitui";
import type { OriginalUserAccount } from "@root/user";
import { clearUserData, setCustomerAccount, setUser, setUserAccount, useUserStore } from "@root/user";
import ContactFormModal from "@ui_kit/ContactForm";
@@ -8,7 +8,7 @@ import { useAfterPay } from "@utils/hooks/useAutoPay";
import { useUserAccountFetcher } from "@utils/hooks/useUserAccountFetcher";
import { enqueueSnackbar } from "notistack";
import type { SuspenseProps } from "react";
-import { lazy, Suspense } from "react";
+import { lazy, Suspense, useEffect } from "react";
import { lazily } from "react-lazily";
import { Navigate, Route, Routes, useLocation, useNavigate } from "react-router-dom";
import { useAmoAccount } from "./api/integration";
@@ -23,7 +23,11 @@ import { InfoPrivilege } from "./pages/InfoPrivilege";
import AmoTokenExpiredDialog from "./pages/IntegrationsPage/IntegrationsModal/Amo/AmoTokenExpiredDialog";
import Landing from "./pages/Landing/Landing";
import Main from "./pages/main";
+import Debug from "./pages/Debug";
+import { setTicketData, setTickets, useTicketStore } from "./stores/ticket";
+import { parseAxiosError } from "./utils/parse-error";
import { ErrorBoundary } from "react-error-boundary";
+import { handleLogoutClick } from "./utils/HandleLogoutClick";
const MyQuizzesFull = lazy(() => import("./pages/createQuize/MyQuizzesFull"));
const QuizGallery = lazy(() => import("./pages/createQuize/QuizGallery"));
@@ -39,6 +43,12 @@ const PersonalizationAI = lazy(() => import("./pages/PersonalizationAI/Personali
let params = new URLSearchParams(document.location.search);
const isTest = Boolean(params.get("test"))
+createMakeRequestConfig(
+ handleLogoutClick,
+ (error, info, getTickets) => handleComponentError(error, info, getTickets()),
+ () => useTicketStore.getState().tickets
+);
+
const routeslink = [
{
path: "/edit",
@@ -73,12 +83,16 @@ const LazyLoading = ({ children, fallback }: SuspenseProps) => (
>}>{children}
);
+const ApologyPage = () =>
+
export default function App() {
window.LoadingObserver = false;
const userId = useUserStore((state) => state.userId);
const location = useLocation();
const navigate = useNavigate();
const { data: amoAccount } = useAmoAccount();
+ const tickets = useTicketStore(store => store.tickets);
+
useUserFetcher({
url: `${process.env.REACT_APP_DOMAIN}/user/${userId}`,
@@ -97,8 +111,11 @@ export default function App() {
useUserAccountFetcher({
url: `${process.env.REACT_APP_DOMAIN}/customer/v1.0.1/account`,
userId,
- onNewUserAccount: setCustomerAccount,
+ onNewUserAccount: (account) => {
+ setCustomerAccount(account);
+ },
onError: (error) => {
+ console.error("App: Error in customerAccount fetcher:", error);
const errorMessage = getMessageFromFetchError(error);
if (errorMessage) {
enqueueSnackbar(errorMessage);
@@ -124,6 +141,37 @@ export default function App() {
},
});
+ useTicketsFetcher({
+ url: `${process.env.REACT_APP_DOMAIN}/heruvym/v1.0.0/getTickets`,
+ ticketsPerPage: 10,
+ ticketApiPage: 0,
+ onSuccess: (result) => {
+ if (result.data?.length) {
+ // Записываем все тикеты в стор
+ setTickets(result.data);
+
+ const currentTicket = result.data.find(
+ ({ origin }) => !origin.includes("/support"),
+ );
+
+ if (!currentTicket) {
+ return;
+ }
+
+ setTicketData({
+ ticketId: currentTicket.id,
+ sessionId: currentTicket.sess,
+ });
+ }
+ },
+ onError: (error: Error) => {
+ const message = parseAxiosError(error);
+ if (message) enqueueSnackbar(message);
+ },
+ onFetchStateChange: () => { },
+ enabled: Boolean(userId),
+ });
+
useAfterPay();
if (location.state?.redirectTo)
@@ -136,7 +184,10 @@ export default function App() {
);
return (
- <>
+ handleComponentError(error, info, () => useTicketStore.getState().tickets)}
+ >
{amoAccount && }
@@ -259,6 +310,10 @@ export default function App() {
path={"/image/:srcImage"}
element={}
/>
+ }
+ />
}>
{routeslink.map((e, i) => (
- >
+
+ {/* Компонент отладки ошибок - доступен по Ctrl+Shift+D */}
+
+
);
}
diff --git a/src/api/auth.ts b/src/api/auth.ts
index b47d6fbf..5dfd5833 100644
--- a/src/api/auth.ts
+++ b/src/api/auth.ts
@@ -1,4 +1,4 @@
-import { makeRequest } from "@api/makeRequest";
+import { makeRequest } from "@frontend/kitui";
import { parseAxiosError } from "@utils/parse-error";
diff --git a/src/api/cart.ts b/src/api/cart.ts
index d9ec8a24..a08fb904 100644
--- a/src/api/cart.ts
+++ b/src/api/cart.ts
@@ -1,4 +1,4 @@
-import { makeRequest } from "@api/makeRequest";
+import { makeRequest } from "@frontend/kitui";
import { parseAxiosError } from "@utils/parse-error";
diff --git a/src/api/contactForm.ts b/src/api/contactForm.ts
index 6ed0f71d..df81d211 100644
--- a/src/api/contactForm.ts
+++ b/src/api/contactForm.ts
@@ -1,4 +1,4 @@
-import { makeRequest } from "@api/makeRequest";
+import { makeRequest } from "@frontend/kitui";
import { parseAxiosError } from "@utils/parse-error";
diff --git a/src/api/integration.ts b/src/api/integration.ts
index 40f91b44..177c5bf0 100644
--- a/src/api/integration.ts
+++ b/src/api/integration.ts
@@ -1,5 +1,5 @@
import { QuestionKeys } from "@/pages/IntegrationsPage/IntegrationsModal/Amo/types";
-import { makeRequest } from "@api/makeRequest";
+import { makeRequest } from "@frontend/kitui";
import { useToken } from "@frontend/kitui";
import { parseAxiosError } from "@utils/parse-error";
import useSWR from "swr";
diff --git a/src/api/makeRequest.ts b/src/api/makeRequest.ts
deleted file mode 100644
index ebc554b4..00000000
--- a/src/api/makeRequest.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import * as KIT from "@frontend/kitui";
-import { Method, ResponseType, AxiosError } from "axios";
-import { redirect } from "react-router-dom";
-import { clearAuthToken } from "@frontend/kitui";
-
-import { cleanAuthTicketData } from "@root/ticket";
-import { clearUserData } from "@root/user";
-import { clearQuizData } from "@root/quizes/store";
-
-import type { AxiosResponse } from "axios";
-import { selectSendingMethod } from "@/ui_kit/FloatingSupportChat/utils";
-
-interface MakeRequest {
- method?: Method | undefined;
- url: string;
- body?: unknown;
- useToken?: boolean | undefined;
- contentType?: boolean | undefined;
- responseType?: ResponseType | undefined;
- signal?: AbortSignal | undefined;
- withCredentials?: boolean | undefined;
-}
-
-type ExtendedAxiosResponse = AxiosResponse & { message: string };
-
-export const makeRequest = async (
- data: MakeRequest,
-): Promise => {
- try {
- const response = await KIT.makeRequest(data);
-
- return response;
- } catch (nativeError) {
- const error = nativeError as AxiosError;
-
- // if (window.location.hostname !== 'localhost') selectSendingMethod({
- // messageField: `status: ${error.response?.status}. Message ${(error.response?.data as ExtendedAxiosResponse)?.message}`,
- // isSnackbar: false,
- // systemError: true
- // });
- if (
- error.response?.status === 400 &&
- (error.response?.data as ExtendedAxiosResponse)?.message ===
- "refreshToken is empty"
- ) {
- cleanAuthTicketData();
- clearAuthToken();
- clearUserData();
- clearQuizData();
- redirect("/");
- }
-
- throw nativeError;
- }
-};
diff --git a/src/api/promocode.ts b/src/api/promocode.ts
index 87e90a01..f20a7305 100644
--- a/src/api/promocode.ts
+++ b/src/api/promocode.ts
@@ -1,4 +1,4 @@
-import { makeRequest } from "@api/makeRequest";
+import { makeRequest } from "@frontend/kitui";
import { parseAxiosError } from "@utils/parse-error";
diff --git a/src/api/question.ts b/src/api/question.ts
index f15fcee6..cf4f902b 100644
--- a/src/api/question.ts
+++ b/src/api/question.ts
@@ -1,4 +1,4 @@
-import { makeRequest } from "@api/makeRequest";
+import { makeRequest } from "@frontend/kitui";
import { replaceSpacesToEmptyLines } from "@utils/replaceSpacesToEmptyLines";
import { parseAxiosError } from "@utils/parse-error";
diff --git a/src/api/quiz.ts b/src/api/quiz.ts
index 1bcea534..e189a7be 100644
--- a/src/api/quiz.ts
+++ b/src/api/quiz.ts
@@ -1,4 +1,4 @@
-import { makeRequest } from "@api/makeRequest";
+import { makeRequest } from "@frontend/kitui";
import { defaultQuizConfig } from "@model/quizSettings";
import { parseAxiosError } from "@utils/parse-error";
diff --git a/src/api/result.ts b/src/api/result.ts
index 74d0a5d8..c04edea0 100644
--- a/src/api/result.ts
+++ b/src/api/result.ts
@@ -1,4 +1,4 @@
-import { makeRequest } from "@api/makeRequest";
+import { makeRequest } from "@frontend/kitui";
import { parseAxiosError } from "@utils/parse-error";
diff --git a/src/api/statistic.ts b/src/api/statistic.ts
index 1fd3c245..8310deae 100644
--- a/src/api/statistic.ts
+++ b/src/api/statistic.ts
@@ -1,4 +1,4 @@
-import { makeRequest } from "@api/makeRequest";
+import { makeRequest } from "@frontend/kitui";
import { parseAxiosError } from "@utils/parse-error";
diff --git a/src/api/tariff.ts b/src/api/tariff.ts
index 48efae84..d43b056d 100644
--- a/src/api/tariff.ts
+++ b/src/api/tariff.ts
@@ -1,4 +1,4 @@
-import { makeRequest } from "@api/makeRequest";
+import { makeRequest } from "@frontend/kitui";
import { parseAxiosError } from "@utils/parse-error";
import type { GetTariffsResponse } from "@frontend/kitui";
diff --git a/src/api/ticket.ts b/src/api/ticket.ts
index 5056dc64..f6f5a181 100644
--- a/src/api/ticket.ts
+++ b/src/api/ticket.ts
@@ -1,6 +1,6 @@
import { createTicket as createTicketRequest } from "@frontend/kitui";
-import { makeRequest } from "@api/makeRequest";
+import { makeRequest } from "@frontend/kitui";
import { parseAxiosError } from "@utils/parse-error";
@@ -15,47 +15,7 @@ type SendFileResponse = {
const API_URL = `${process.env.REACT_APP_DOMAIN}/heruvym/v1.0.0`;
-export const sendTicketMessage = async (
- ticketId: string,
- message: string,
- systemError: boolean
-): Promise<[null, string?]> => {
- try {
- const sendTicketMessageResponse = await makeRequest<
- SendTicketMessageRequest,
- null
- >({
- url: `${API_URL}/send`,
- method: "POST",
- useToken: true,
- body: { ticket: ticketId, message: message, lang: "ru", files: [], system: systemError },
-
- });
- return [sendTicketMessageResponse];
- } catch (nativeError) {
- const [error] = parseAxiosError(nativeError);
-
- return [null, `Не удалось отправить сообщение. ${error}`];
- }
-};
-
-export const shownMessage = async (id: string): Promise<[null, string?]> => {
- try {
- const shownMessageResponse = await makeRequest<{ id: string }, null>({
- url: `${API_URL}/shown`,
- method: "POST",
- useToken: true,
- body: { id },
- });
-
- return [shownMessageResponse];
- } catch (nativeError) {
- const [error] = parseAxiosError(nativeError);
-
- return [null, `Не удалось прочесть сообщение. ${error}`];
- }
-};
export const sendFile = async (
ticketId: string,
diff --git a/src/api/user.ts b/src/api/user.ts
index 14b072c3..4f4c0d07 100644
--- a/src/api/user.ts
+++ b/src/api/user.ts
@@ -1,4 +1,4 @@
-import { makeRequest } from "@api/makeRequest";
+import { makeRequest } from "@frontend/kitui";
import { parseAxiosError } from "@utils/parse-error";
diff --git a/src/assets/icons/logo/Postback.tsx b/src/assets/icons/logo/Postback.tsx
new file mode 100644
index 00000000..23956784
--- /dev/null
+++ b/src/assets/icons/logo/Postback.tsx
@@ -0,0 +1,15 @@
+import { Box, SxProps } from "@mui/material";
+
+export default (sx: SxProps) => (
+
+
+
+);
\ No newline at end of file
diff --git a/src/assets/icons/logo/PostbackPC.tsx b/src/assets/icons/logo/PostbackPC.tsx
new file mode 100644
index 00000000..1dff7213
--- /dev/null
+++ b/src/assets/icons/logo/PostbackPC.tsx
@@ -0,0 +1,19 @@
+import { Box, SxProps } from "@mui/material";
+
+export default (sx: SxProps) => (
+
+
+
+
+
+);
\ No newline at end of file
diff --git a/src/assets/icons/logo/zapier.png b/src/assets/icons/logo/zapier.png
new file mode 100644
index 00000000..53f20b39
Binary files /dev/null and b/src/assets/icons/logo/zapier.png differ
diff --git a/src/index.tsx b/src/index.tsx
index 86b5737e..436554ae 100755
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -18,7 +18,7 @@ import CloseIcon from "@icons/CloseBold";
import type { SnackbarKey } from "notistack";
import { CheckFastlink } from "@ui_kit/CheckFastlink";
import { ErrorBoundary } from "react-error-boundary";
-import { handleComponentError } from "./utils/handleComponentError";
+import { handleComponentError } from "@frontend/kitui";
moment.locale("ru");
polyfillCountryFlagEmojis();
@@ -38,7 +38,6 @@ const snackbarAction = (snackbarId: SnackbarKey) => (
);
-const ApologyPage = () =>
const root = createRoot(document.getElementById("root")!);
@@ -65,12 +64,7 @@ root.render(
>
-
-
diff --git a/src/model/quizSettings.ts b/src/model/quizSettings.ts
index 50050301..f04e8151 100644
--- a/src/model/quizSettings.ts
+++ b/src/model/quizSettings.ts
@@ -69,6 +69,8 @@ export type QuizTheme =
export enum QuizMetricType {
yandex = "yandexMetricsNumber",
vk = "vkMetricsNumber",
+ zapier = "zapierIntegration",
+ postback = "postbackIntegration",
}
export type FormContactFieldName = "name" | "email" | "phone" | "text" | "address";
diff --git a/src/pages/Analytics/General.tsx b/src/pages/Analytics/General.tsx
index be9b7b88..78ce3a9f 100644
--- a/src/pages/Analytics/General.tsx
+++ b/src/pages/Analytics/General.tsx
@@ -42,16 +42,9 @@ const GeneralItem = ({
([nextValue], [currentValue]) => Number(nextValue) - Number(currentValue),
);
const days = data.map(([value]) => value);
- const { time } = data.reduce(
- (total, [_, value]) => ({
- defaultValue: value > 0 ? value : total.defaultValue,
- time: [...total.time, value > 0 ? value : total.defaultValue],
- }),
- { defaultValue: 0, time: [] as number[] },
- );
const numberValue = calculateTime
- ? time.reduce((total, value) => total + value, 0) / days.length
+ ? Object.values(general).reduce((total, value) => total + value, 0) / Object.values(general).length
: conversionValue
? conversionValue
: Object.values(general).reduce((total, item) => total + item, 0);
@@ -82,17 +75,17 @@ const GeneralItem = ({
{
const timestamp = Number(value);
if (isNaN(timestamp)) return 'Invalid Date';
- return moment.unix(timestamp).format(statiscticsResult ? "DD/MM/YYYY" : "DD/MM/YYYY HH") + (statiscticsResult ? "" : "ч");
+ return moment.unix(timestamp).format("DD/MM/YYYY");
},
},
]}
series={[
{
- data: Object.values(statiscticsResult ? time : general),
+ data: Object.values(general),
valueFormatter: (value) =>
calculateTime
? getCalculatedTime(value)
@@ -131,11 +124,21 @@ export const General: FC = ({ data, day }) => {
const generalResponse = Object.entries(data).reduce(
(total, [fatherKey, values]) => {
const value = Object.keys(values).reduce((totalValue, key) => {
- if (Number(key) - currentDate < 0) {
+ const keyTimestamp = Number(key);
+ const todayStart = moment().startOf('day').unix();
+ const todayEnd = moment().endOf('day').unix();
+
+ // Включаем данные за сегодня и прошлые дни, исключаем будущие дни
+ if (keyTimestamp >= todayStart && keyTimestamp <= todayEnd) {
+ // Сегодняшний день - включаем
return { ...totalValue, [key]: values[key] };
+ } else if (keyTimestamp < todayStart) {
+ // Прошлые дни - включаем
+ return { ...totalValue, [key]: values[key] };
+ } else {
+ // Будущие дни - исключаем
+ return totalValue;
}
-
- return totalValue;
}, {});
return { ...total, [fatherKey]: value };
diff --git a/src/pages/Debug.tsx b/src/pages/Debug.tsx
new file mode 100644
index 00000000..bd9a64c7
--- /dev/null
+++ b/src/pages/Debug.tsx
@@ -0,0 +1,149 @@
+import React, { useState, useEffect } from 'react';
+import {
+ Box,
+ Button,
+ Typography,
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TableRow,
+ Paper
+} from '@mui/material';
+
+// Функции для создания тестовых ошибок
+const createTestError = (message: string) => {
+ const error = new Error(message);
+ (error as any).__forceSend = true;
+ throw error;
+};
+
+const createReactComponentError = (message: string) => {
+ const error = new Error(message);
+ (error as any).__forceSend = true;
+ throw error;
+};
+
+/**
+ * Простая страница отладки системы обработки ошибок
+ * Активируется по Ctrl+Shift+F
+ */
+const Debug: React.FC = () => {
+ const [isVisible, setIsVisible] = useState(false);
+
+
+ useEffect(() => {
+ const handleKeyDown = (event: KeyboardEvent) => {
+ if (event.ctrlKey && event.shiftKey && event.code === 'KeyF') {
+ event.preventDefault();
+ setIsVisible(!isVisible);
+ }
+ };
+
+ window.addEventListener('keydown', handleKeyDown);
+ return () => window.removeEventListener('keydown', handleKeyDown);
+ }, [isVisible]);
+
+ if (!isVisible) {
+ return null;
+ }
+
+ const errorTests = [
+ {
+ description: 'Простая ошибка',
+ action: () => {
+ createTestError('Тестовая ошибка отладки');
+ }
+ },
+ {
+ description: 'React ошибка компонента',
+ action: () => {
+ createReactComponentError('Тестовая ошибка React компонента');
+ }
+ },
+ {
+ description: 'Прямая ошибка в обработчике',
+ action: () => {
+ const error = new Error('Прямая ошибка в компоненте');
+ (error as any).__forceSend = true;
+ throw error;
+ }
+ },
+ {
+ description: 'Ошибка с длинным стеком',
+ action: () => {
+ const deepError = new Error('Глубокая ошибка с длинным стеком');
+ deepError.stack = 'Error: Глубокая ошибка\n at level1 (debug.ts:10)\n at level2 (debug.ts:15)\n at level3 (debug.ts:20)\n at level4 (debug.ts:25)';
+ (deepError as any).__forceSend = true;
+ throw deepError;
+ }
+ },
+ {
+ description: 'Ошибка с undefined',
+ action: () => {
+ const obj: any = null;
+ try {
+ obj.nonExistentMethod();
+ } catch (error) {
+ (error as any).__forceSend = true;
+ throw error;
+ }
+ }
+ }
+ ];
+
+ return (
+
+
+
+ 🛠️ Отладка ошибок
+
+
+
+ Ctrl+Shift+F для закрытия
+
+
+
+
+
+
+ Описание ошибки
+ Действие
+
+
+
+ {errorTests.map((test, index) => (
+
+ {test.description}
+
+
+
+
+ ))}
+
+
+
+
+
+ );
+};
+
+export default Debug;
\ No newline at end of file
diff --git a/src/pages/IntegrationsPage/IntegrationsModal/Amo/Questions/AmoQuestions.tsx b/src/pages/IntegrationsPage/IntegrationsModal/Amo/Questions/AmoQuestions.tsx
index e73e45ac..084fc813 100644
--- a/src/pages/IntegrationsPage/IntegrationsModal/Amo/Questions/AmoQuestions.tsx
+++ b/src/pages/IntegrationsPage/IntegrationsModal/Amo/Questions/AmoQuestions.tsx
@@ -150,10 +150,6 @@ export const AmoQuestions: FC = ({
}, [activeScope])
const [blockButton, setBlockButton] = useState(false)
- console.log("selectedQuestions")
- console.log(selectedQuestions)
- console.log("SCFworld")
- console.log(SCFworld)
return (
<>
(accountInfo ? "accountInfo" : "amoLogin")
const [openDelete, setOpenDelete] = useState(null);
- console.log("--")
- console.log(selectedQuestions)
const startDeleteTagQuestion = (itemForDelete) => {
setOpenDelete(itemForDelete)
@@ -135,9 +133,6 @@ export const SwitchPages = ({
if (type === "tag") {
setSelectedTags((prevState) => {
- console.log(prevState)
- console.log(scope)
- console.log(id)
return({
...prevState,
[scope]: [...prevState[scope as TagKeys], id],
@@ -147,9 +142,6 @@ export const SwitchPages = ({
if (type === "question") {
const q = questions.find(e => e.backendId === Number(id))
setSelectedQuestions((prevState) => {
- console.log(prevState)
- console.log(scope)
- console.log(id)
return ({
...prevState,
[scope]: [...prevState[scope as QuestionKeys], {
diff --git a/src/pages/IntegrationsPage/IntegrationsModal/Amo/useAmoIntegration.ts b/src/pages/IntegrationsPage/IntegrationsModal/Amo/useAmoIntegration.ts
index 5a983e76..a5a106df 100644
--- a/src/pages/IntegrationsPage/IntegrationsModal/Amo/useAmoIntegration.ts
+++ b/src/pages/IntegrationsPage/IntegrationsModal/Amo/useAmoIntegration.ts
@@ -353,7 +353,7 @@ export const useAmoIntegration = ({ isModalOpen, isTryRemoveAccount, quizID, que
};
};
-import { makeRequest } from "@api/makeRequest";
+import { makeRequest } from "@frontend/kitui";
const API_URL = `${process.env.REACT_APP_DOMAIN}/squiz/amocrm`;
export const resetAmoTagsFields = async () => {
diff --git a/src/pages/IntegrationsPage/IntegrationsModal/Postback/index.tsx b/src/pages/IntegrationsPage/IntegrationsModal/Postback/index.tsx
new file mode 100644
index 00000000..3b7af294
--- /dev/null
+++ b/src/pages/IntegrationsPage/IntegrationsModal/Postback/index.tsx
@@ -0,0 +1,75 @@
+import { FC } from "react";
+import { Dialog, IconButton, Typography, useMediaQuery, useTheme, Box } from "@mui/material";
+import CloseIcon from "@mui/icons-material/Close";
+import { Quiz } from "@/model/quiz/quiz";
+
+type PostbackModalProps = {
+ isModalOpen: boolean;
+ handleCloseModal: () => void;
+ companyName: string | null;
+ quiz: Quiz;
+};
+
+export const PostbackModal: FC = ({
+ isModalOpen,
+ handleCloseModal,
+ companyName,
+ quiz
+}) => {
+ const theme = useTheme();
+ const isMobile = useMediaQuery(theme.breakpoints.down(600));
+ const isTablet = useMediaQuery(theme.breakpoints.down(1000));
+
+ return (
+
+ );
+};
\ No newline at end of file
diff --git a/src/pages/IntegrationsPage/IntegrationsModal/Zapier/index.tsx b/src/pages/IntegrationsPage/IntegrationsModal/Zapier/index.tsx
new file mode 100644
index 00000000..39483a96
--- /dev/null
+++ b/src/pages/IntegrationsPage/IntegrationsModal/Zapier/index.tsx
@@ -0,0 +1,75 @@
+import { FC } from "react";
+import { Dialog, IconButton, Typography, useMediaQuery, useTheme, Box } from "@mui/material";
+import CloseIcon from "@mui/icons-material/Close";
+import { Quiz } from "@/model/quiz/quiz";
+
+type ZapierModalProps = {
+ isModalOpen: boolean;
+ handleCloseModal: () => void;
+ companyName: string | null;
+ quiz: Quiz;
+};
+
+export const ZapierModal: FC = ({
+ isModalOpen,
+ handleCloseModal,
+ companyName,
+ quiz
+}) => {
+ const theme = useTheme();
+ const isMobile = useMediaQuery(theme.breakpoints.down(600));
+ const isTablet = useMediaQuery(theme.breakpoints.down(1000));
+
+ return (
+
+ );
+};
\ No newline at end of file
diff --git a/src/pages/IntegrationsPage/IntegrationsPage.tsx b/src/pages/IntegrationsPage/IntegrationsPage.tsx
index b74e3650..ce2af29a 100644
--- a/src/pages/IntegrationsPage/IntegrationsPage.tsx
+++ b/src/pages/IntegrationsPage/IntegrationsPage.tsx
@@ -27,6 +27,8 @@ export const IntegrationsPage = ({
>(null);
const [isAmoCrmModalOpen, setIsAmoCrmModalOpen] = useState(false);
+ const [isZapierModalOpen, setIsZapierModalOpen] = useState(false);
+ const [isPostbackModalOpen, setIsPostbackModalOpen] = useState(false);
useEffect(() => {
if (editQuizId === null) navigate("/list");
@@ -44,6 +46,12 @@ export const IntegrationsPage = ({
const handleCloseAmoSRMModal = () => {
setIsAmoCrmModalOpen(false);
};
+ const handleCloseZapierModal = () => {
+ setIsZapierModalOpen(false);
+ };
+ const handleClosePostbackModal = () => {
+ setIsPostbackModalOpen(false);
+ };
return (
<>
@@ -60,7 +68,7 @@ export const IntegrationsPage = ({
>
Интеграции
@@ -73,6 +81,12 @@ export const IntegrationsPage = ({
setIsAmoCrmModalOpen={setIsAmoCrmModalOpen}
isAmoCrmModalOpen={isAmoCrmModalOpen}
handleCloseAmoSRMModal={handleCloseAmoSRMModal}
+ setIsZapierModalOpen={setIsZapierModalOpen}
+ isZapierModalOpen={isZapierModalOpen}
+ handleCloseZapierModal={handleCloseZapierModal}
+ setIsPostbackModalOpen={setIsPostbackModalOpen}
+ isPostbackModalOpen={isPostbackModalOpen}
+ handleClosePostbackModal={handleClosePostbackModal}
/>
>
diff --git a/src/pages/IntegrationsPage/PartnersBoard/PartnersBoard.tsx b/src/pages/IntegrationsPage/PartnersBoard/PartnersBoard.tsx
index 6612b444..6cb28b6d 100644
--- a/src/pages/IntegrationsPage/PartnersBoard/PartnersBoard.tsx
+++ b/src/pages/IntegrationsPage/PartnersBoard/PartnersBoard.tsx
@@ -1,13 +1,13 @@
-import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
+import { Box, Typography, useTheme } from "@mui/material";
import React, { FC, lazy, Suspense } from "react";
-import { ServiceButton } from "./ServiceButton/ServiceButton";
+import { ServiceButton } from "./buttons/ServiceButton";
+import { ZapierButton } from "./buttons/ZapierButton";
+import { PostbackButton } from "./buttons/PostbackButton";
import { YandexMetricaLogo } from "../mocks/YandexMetricaLogo";
-// import AnalyticsModal from "./AnalyticsModal/AnalyticsModal";
import { VKPixelLogo } from "../mocks/VKPixelLogo";
import { QuizMetricType } from "@model/quizSettings";
import { AmoCRMLogo } from "../mocks/AmoCRMLogo";
import { useCurrentQuiz } from "@/stores/quizes/hooks";
-import { useUserStore } from "@/stores/user";
const AnalyticsModal = lazy(() =>
import("./AnalyticsModal/AnalyticsModal").then((module) => ({
@@ -21,6 +21,18 @@ const AmoCRMModal = lazy(() =>
}))
);
+const ZapierModal = lazy(() =>
+ import("../IntegrationsModal/Zapier").then((module) => ({
+ default: module.ZapierModal,
+ }))
+);
+
+const PostbackModal = lazy(() =>
+ import("../IntegrationsModal/Postback").then((module) => ({
+ default: module.PostbackModal,
+ }))
+);
+
type PartnersBoardProps = {
setIsModalOpen: (value: boolean) => void;
companyName: keyof typeof QuizMetricType | null;
@@ -30,6 +42,12 @@ type PartnersBoardProps = {
setIsAmoCrmModalOpen: (value: boolean) => void;
isAmoCrmModalOpen: boolean;
handleCloseAmoSRMModal: () => void;
+ setIsZapierModalOpen: (value: boolean) => void;
+ isZapierModalOpen: boolean;
+ handleCloseZapierModal: () => void;
+ setIsPostbackModalOpen: (value: boolean) => void;
+ isPostbackModalOpen: boolean;
+ handleClosePostbackModal: () => void;
};
export const PartnersBoard: FC = ({
@@ -41,13 +59,31 @@ export const PartnersBoard: FC = ({
setIsAmoCrmModalOpen,
isAmoCrmModalOpen,
handleCloseAmoSRMModal,
+ setIsZapierModalOpen,
+ isZapierModalOpen,
+ handleCloseZapierModal,
+ setIsPostbackModalOpen,
+ isPostbackModalOpen,
+ handleClosePostbackModal,
}) => {
const theme = useTheme();
- const isMobile = useMediaQuery(theme.breakpoints.down(600));
-
const quiz = useCurrentQuiz();
- const user = useUserStore();
+ const sectionTitleStyles = {
+ textAlign: { xs: "start", sm: "start", md: "start" } as const,
+ lineHeight: "1",
+ marginBottom: "12px",
+ marginTop: "20px",
+ color: theme.palette.grey3.main,
+ fontSize: "18px",
+ };
+
+ const containerStyles = {
+ display: "flex",
+ flexWrap: "wrap",
+ justifyContent: { xs: "start", sm: "start", md: "start" },
+ gap: { xs: "15px", md: "20px" },
+ };
return (
= ({
}}
>
- <>
-
- CRM
-
-
- }
- setIsModalOpen={setIsAmoCrmModalOpen}
- setCompanyName={setCompanyName}
- name={"amoCRM"}
- />
-
- >
+ CRM
+
+
+ }
+ setIsModalOpen={setIsAmoCrmModalOpen}
+ setCompanyName={setCompanyName}
+ name={"amoCRM"}
+ />
+
+
+
Аналитика
-
+
}
setIsModalOpen={setIsModalOpen}
@@ -114,9 +129,24 @@ export const PartnersBoard: FC = ({
name={"vk"}
setIsModalOpen={setIsModalOpen}
setCompanyName={setCompanyName}
- >
+ />
+
+
+
+ Автоматизация
+
+
+
+
+
{companyName && (
= ({
isModalOpen={isAmoCrmModalOpen}
handleCloseModal={handleCloseAmoSRMModal}
companyName={companyName}
- quiz={quiz}
+ quiz={quiz!}
+ />
+
+ )}
+ {companyName && isZapierModalOpen && (
+
+
+
+ )}
+ {companyName && isPostbackModalOpen && (
+
+
)}
diff --git a/src/pages/IntegrationsPage/PartnersBoard/ServiceButton/ServiceButton.tsx b/src/pages/IntegrationsPage/PartnersBoard/ServiceButton/ServiceButton.tsx
deleted file mode 100644
index 4773e19f..00000000
--- a/src/pages/IntegrationsPage/PartnersBoard/ServiceButton/ServiceButton.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-import { Box, Typography, useTheme } from "@mui/material";
-import { FC } from "react";
-import { QuizMetricType } from "@model/quizSettings";
-
-type PartnerItemProps = {
- setIsModalOpen: (value: boolean) => void;
- setCompanyName: (value: keyof typeof QuizMetricType) => void;
- logo?: JSX.Element;
- title?: string;
- name: string;
-};
-
-export const ServiceButton: FC = ({
- setIsModalOpen,
- logo,
- title,
- name,
- setCompanyName,
-}) => {
- const theme = useTheme();
-
- const handleClick = () => {
- setCompanyName(name as keyof typeof QuizMetricType);
- setIsModalOpen(true);
- };
-
- return (
- <>
-
- {logo && logo}
-
- {title && title}
-
-
- >
- );
-};
diff --git a/src/pages/IntegrationsPage/PartnersBoard/buttons/IntegrationButton.tsx b/src/pages/IntegrationsPage/PartnersBoard/buttons/IntegrationButton.tsx
new file mode 100644
index 00000000..fe22cfd0
--- /dev/null
+++ b/src/pages/IntegrationsPage/PartnersBoard/buttons/IntegrationButton.tsx
@@ -0,0 +1,53 @@
+import { Box } from "@mui/material";
+import { FC, useState, ReactNode } from "react";
+
+type IntegrationButtonProps = {
+ children: ReactNode;
+ onClick: () => void;
+ padding?: string;
+};
+
+export const IntegrationButton: FC = ({
+ children,
+ onClick,
+ padding = "0 20px",
+}) => {
+ const [isPressed, setIsPressed] = useState(false);
+
+ const handleMouseDown = () => setIsPressed(true);
+ const handleMouseUp = () => setIsPressed(false);
+ const handleMouseLeave = () => setIsPressed(false);
+
+ return (
+
+ {children}
+
+ );
+};
\ No newline at end of file
diff --git a/src/pages/IntegrationsPage/PartnersBoard/buttons/PostbackButton.tsx b/src/pages/IntegrationsPage/PartnersBoard/buttons/PostbackButton.tsx
new file mode 100644
index 00000000..faf90d05
--- /dev/null
+++ b/src/pages/IntegrationsPage/PartnersBoard/buttons/PostbackButton.tsx
@@ -0,0 +1,32 @@
+import { Box } from "@mui/material";
+import { FC } from "react";
+import { QuizMetricType } from "@model/quizSettings";
+import PostbackDefault from "@/assets/icons/logo/Postback";
+import PostbackPC from "@/assets/icons/logo/PostbackPC";
+import { IntegrationButton } from "./IntegrationButton";
+
+type PostbackButtonProps = {
+ setIsModalOpen: (value: boolean) => void;
+ setCompanyName: (value: keyof typeof QuizMetricType) => void;
+};
+
+export const PostbackButton: FC = ({
+ setIsModalOpen,
+ setCompanyName,
+}) => {
+ const handleClick = () => {
+ setCompanyName("postback" as keyof typeof QuizMetricType);
+ setIsModalOpen(true);
+ };
+
+ return (
+
+ <>
+ {/* Иконка монитора */}
+
+ {/* Текст Postback */}
+
+ >
+
+ );
+};
\ No newline at end of file
diff --git a/src/pages/IntegrationsPage/PartnersBoard/buttons/ServiceButton.tsx b/src/pages/IntegrationsPage/PartnersBoard/buttons/ServiceButton.tsx
new file mode 100644
index 00000000..88a92fce
--- /dev/null
+++ b/src/pages/IntegrationsPage/PartnersBoard/buttons/ServiceButton.tsx
@@ -0,0 +1,40 @@
+import { Typography } from "@mui/material";
+import { FC } from "react";
+import { QuizMetricType } from "@model/quizSettings";
+import { IntegrationButton } from "./IntegrationButton";
+
+type PartnerItemProps = {
+ setIsModalOpen: (value: boolean) => void;
+ setCompanyName: (value: keyof typeof QuizMetricType) => void;
+ logo?: JSX.Element;
+ title?: string;
+ name: string;
+};
+
+export const ServiceButton: FC = ({
+ setIsModalOpen,
+ logo,
+ title,
+ name,
+ setCompanyName,
+}) => {
+ const handleClick = () => {
+ setCompanyName(name as keyof typeof QuizMetricType);
+ setIsModalOpen(true);
+ };
+
+ return (
+
+ {logo && logo}
+
+ {title && title}
+
+
+ );
+};
\ No newline at end of file
diff --git a/src/pages/IntegrationsPage/PartnersBoard/buttons/ZapierButton.tsx b/src/pages/IntegrationsPage/PartnersBoard/buttons/ZapierButton.tsx
new file mode 100644
index 00000000..8abcc70a
--- /dev/null
+++ b/src/pages/IntegrationsPage/PartnersBoard/buttons/ZapierButton.tsx
@@ -0,0 +1,35 @@
+import { Box } from "@mui/material";
+import { FC } from "react";
+import { QuizMetricType } from "@model/quizSettings";
+import zapierLogo from "@/assets/icons/logo/zapier.png";
+import { IntegrationButton } from "./IntegrationButton";
+
+type ZapierButtonProps = {
+ setIsModalOpen: (value: boolean) => void;
+ setCompanyName: (value: keyof typeof QuizMetricType) => void;
+};
+
+export const ZapierButton: FC = ({
+ setIsModalOpen,
+ setCompanyName,
+}) => {
+ const handleClick = () => {
+ setCompanyName("zapier" as keyof typeof QuizMetricType);
+ setIsModalOpen(true);
+ };
+
+ return (
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/src/pages/Payment/Payment.tsx b/src/pages/Payment/Payment.tsx
new file mode 100644
index 00000000..5a410641
--- /dev/null
+++ b/src/pages/Payment/Payment.tsx
@@ -0,0 +1,12 @@
+import { useAuthRedirect } from "../../utils/hooks/useAuthRedirect";
+
+export default function Payment() {
+ // Используем хук авторизации
+ const { isProcessing } = useAuthRedirect();
+
+ // Если идет обработка авторизации, показываем загрузку
+ if (isProcessing) {
+ return Идёт загрузка...
;
+ }
+
+ // ... existing component code ...
\ No newline at end of file
diff --git a/src/pages/PersonalizationAI/AuditoryList.tsx b/src/pages/PersonalizationAI/AuditoryList.tsx
index a84c7e2a..1050535a 100644
--- a/src/pages/PersonalizationAI/AuditoryList.tsx
+++ b/src/pages/PersonalizationAI/AuditoryList.tsx
@@ -11,8 +11,6 @@ export const AuditoryList = ({utmParams, auditory, onDelete}:{utmParams:string,a
const { isTestServer } = useDomainDefine();
const [linksOpen, setLinksOpen] = useState(true);
- console.log("auditory-___---_auditory__---__-__auditory_------__---__-__---_------__---__-__---_------__---__-____--__")
- console.log(auditory)
return (
<>
diff --git a/src/pages/PersonalizationAI/CreateButtonWithTooltip.tsx b/src/pages/PersonalizationAI/CreateButtonWithTooltip.tsx
new file mode 100644
index 00000000..c92c20d1
--- /dev/null
+++ b/src/pages/PersonalizationAI/CreateButtonWithTooltip.tsx
@@ -0,0 +1,112 @@
+import { Button, ClickAwayListener, Tooltip, useTheme } from "@mui/material";
+import { useState } from "react";
+
+interface CreateButtonWithTooltipProps {
+ gender: string;
+ age: string;
+ ageError: boolean;
+ onClick: () => void;
+}
+
+export default function CreateButtonWithTooltip({
+ gender,
+ age,
+ ageError,
+ onClick
+}: CreateButtonWithTooltipProps) {
+ const theme = useTheme();
+ const [open, setOpen] = useState(false);
+
+ const handleTooltipClose = () => {
+ setOpen(false);
+ };
+
+ const handleTooltipOpen = () => {
+ setOpen(true);
+ };
+
+ // Определяем причины неактивности кнопки
+ const getDisabledReasons = () => {
+ const reasons: string[] = [];
+
+ if (!gender) {
+ reasons.push("выберите пол");
+ }
+
+ if (!age) {
+ reasons.push("заполните поле возраста");
+ }
+
+ if (ageError) {
+ reasons.push("исправьте ошибку в поле возраста");
+ }
+
+ return reasons;
+ };
+
+ const disabledReasons = getDisabledReasons();
+ const isDisabled = !gender || !age || ageError;
+ const tooltipText = isDisabled
+ ? disabledReasons.length === 2
+ ? disabledReasons.join(' и ')
+ : disabledReasons.join('\n')
+ : '';
+
+ return (
+
+
+ {isDisabled && (
+
+ )}
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/pages/PersonalizationAI/GenderAndAgeSelector.tsx b/src/pages/PersonalizationAI/GenderAndAgeSelector.tsx
index 720022f3..765afb3d 100644
--- a/src/pages/PersonalizationAI/GenderAndAgeSelector.tsx
+++ b/src/pages/PersonalizationAI/GenderAndAgeSelector.tsx
@@ -1,7 +1,8 @@
-import { Box, FormControl, FormLabel, Checkbox, FormControlLabel, useTheme, Button, useMediaQuery } from "@mui/material";
+import { Box, FormControl, FormLabel, Checkbox, FormControlLabel, useTheme, useMediaQuery } from "@mui/material";
import CheckboxIcon from "@icons/Checkbox";
import AgeInputWithSelect from "./AgeInputWithSelect";
import { useState, useEffect } from "react";
+import CreateButtonWithTooltip from "./CreateButtonWithTooltip";
interface GenderAndAgeSelectorProps {
gender: string;
@@ -190,23 +191,12 @@ export default function GenderAndAgeSelector({
-
+ />
);
}
\ No newline at end of file
diff --git a/src/pages/PersonalizationAI/PersonalizationAI.tsx b/src/pages/PersonalizationAI/PersonalizationAI.tsx
index d86fede9..a78e8e49 100644
--- a/src/pages/PersonalizationAI/PersonalizationAI.tsx
+++ b/src/pages/PersonalizationAI/PersonalizationAI.tsx
@@ -11,16 +11,17 @@ import { useSnackbar } from "notistack";
import { PayModal } from "./PayModal";
import { useUserStore } from "@/stores/user";
import { cartApi } from "@/api/cart";
-import { outCart } from "../Tariffs/Tariffs";
-import { inCart } from "../Tariffs/Tariffs";
+import { outCart } from "../Tariffs/utils";
+import { inCart } from "../Tariffs/utils";
import { isTestServer } from "@/utils/hooks/useDomainDefine";
import { useToken } from "@frontend/kitui";
import { useSWRConfig } from "swr";
-import { makeRequest } from "@api/makeRequest";
+import { makeRequest } from "@frontend/kitui";
import { setUserAccount, setCustomerAccount } from "@/stores/user";
import { quizApi } from "@api/quiz";
import { setQuizes } from "@root/quizes/actions";
import TooltipClickInfo from "@/ui_kit/Toolbars/TooltipClickInfo";
+import { generateHubWalletRequestURL } from "@/utils/generateHubWalletRequest";
const tariff = isTestServer ? "6844b8858258f5cc35791ef7" : "6851db40acfb4d3e5fcd9b19";
export default function PersonalizationAI() {
@@ -92,7 +93,7 @@ export default function PersonalizationAI() {
useToken: true,
withCredentials: false,
}).catch(error => {
- console.log(error)
+ console.error(error)
enqueueSnackbar("Ошибка при обновлении данных пользователя", { variant: "error" });
return null;
}),
@@ -102,7 +103,7 @@ export default function PersonalizationAI() {
useToken: true,
withCredentials: false,
}).catch(error => {
- console.log(error)
+ console.error(error)
enqueueSnackbar("Ошибка при обновлении данных клиента", { variant: "error" });
return null;
})
@@ -115,7 +116,7 @@ export default function PersonalizationAI() {
setCustomerAccount(customerAccountResult);
}
} catch (error) {
- console.log(error)
+ console.error(error)
enqueueSnackbar("Ошибка при обновлении данных", { variant: "error" });
}
}
@@ -128,8 +129,6 @@ export default function PersonalizationAI() {
(async () => {
if (quiz?.backendId) {
const [result, error] = await auditoryGet({ quizId: quiz.backendId });
- console.log("result-___---_------__---__-__---_------__---__-__---_------__---__-__---_------__---__-____--__")
- console.log(result)
if (result) {
setAuditory(result);
}
@@ -209,8 +208,6 @@ export default function PersonalizationAI() {
setUtmParams(paramString ? `&${paramString}` : "");
};
- console.log("______----giga_chat-----__--_---_--_----__--__-__--_--__--__--_---_______-quiz")
- console.log(quiz?.giga_chat)
const startCreate = async () => {
if (quiz?.giga_chat) {
createNewLink();
@@ -240,7 +237,15 @@ export default function PersonalizationAI() {
//если денег не хватило
if (payError?.includes("insufficient funds") || payError?.includes("Payment Required")) {
var link = document.createElement("a");
- link.href = `https://${isTestServer ? "s" : ""}hub.pena.digital/quizpayment?action=squizpay&dif=50000&data=${token}&userid=${userId}&from=AI&wayback=ai_${quiz?.backendId}`;
+ link.href = generateHubWalletRequestURL({
+ wayback: "personalization-ai",
+ action: "buy",
+ dif: "50000",
+ userid: userId,
+ additionalinformation: quiz?.backendId.toString(),
+ token
+ });
+ //link.href = `https://${isTestServer ? "s" : ""}hub.pena.digital/quizpayment?action=squizpay&dif=50000&data=${token}&userid=${userId}&from=AI&wayback=ai_${quiz?.backendId}`;
document.body.appendChild(link);
link.click();
return;
@@ -263,12 +268,9 @@ export default function PersonalizationAI() {
// Обновляем данные квиза после успешной оплаты
- console.log("Обновляем данные квиза после оплаты");
const [quizes, quizesError] = await quizApi.getList();
- console.log("Получены данные квизов:", quizes);
if (!quizesError) {
setQuizes(quizes);
- console.log("Данные квизов обновлены в сторе");
} else {
console.error("Ошибка при получении данных квизов:", quizesError);
}
@@ -289,7 +291,7 @@ export default function PersonalizationAI() {
lineHeight: "21.4px"
}}>
Данный раздел позволяет вам создавать персонализированный опрос под каждую целевую аудиторию отдельно, наш AI перефразирует ваши вопросы согласно настройкам.
-
Для этого нужно выбрать пол и возраст вашей аудитории и получите персональную ссылку с нужными настройками в списке ниже.
+
Для этого нужно выбрать пол и возраст вашей аудитории и получите персональную ссылку с нужными настройками в списке ниже.
- Так же вы можете обогатить свою ссылку UTM метками в поле "вставьте свою ссылку" и эти метки применятся ко всем вашим ссылкам.
+ Так же вы можете обогатить свою ссылку UTM метками в поле "вставьте свою ссылку" и эти метки применятся ко всем вашим ссылкам.
import("./BranchingMap").then((module) => ({ default: module.BranchingMap })),
);
@@ -55,31 +54,6 @@ export const QuestionSwitchWindowTool = ({
}}
/>
- // {
- // console.log(e)
- // Array.from(e.target.files).forEach((element, i) => {
- // setTimeout(() => {
- // console.log(i)
- // console.log(Number(element.name.replace(/[^0-9,\s]/g, "")))
- // const q = questions.find((q) => q.page + 1 === Number(element.name.replace(/[^0-9,\s]/g, "")))
- // if (q !== undefined) {
- // console.log(q)
- // console.log("-----------------")
- // uploadQuestionImage(
- // q.id,
- // quiz.qid,
- // e.target.files[i],
- // (question, url) => {
- // question.content.back = url;
- // question.content.originalBack = url;
- // },
- // );
- // }
- // }, 1000);
- // });
- // }}
- // />
}
{openBranchingPage ? (
diff --git a/src/pages/Questions/DraggableList/QuestionPageCardTitle.tsx b/src/pages/Questions/DraggableList/QuestionPageCardTitle.tsx
index 6bcfe86b..b424dd35 100644
--- a/src/pages/Questions/DraggableList/QuestionPageCardTitle.tsx
+++ b/src/pages/Questions/DraggableList/QuestionPageCardTitle.tsx
@@ -126,7 +126,6 @@ const QuestionPageCardTitle = memo(function ({
value={title}
placeholder={"Заголовок вопроса"}
onChange={({ target }) => {
- console.log(target.value.length)
if (target.value.length > maxLengthTextField) {
enqueueSnackbar("Превышена длина вводимого текста")
} else {
diff --git a/src/pages/Questions/DraggableList/index.tsx b/src/pages/Questions/DraggableList/index.tsx
index 4b9cf1f4..03346b3f 100644
--- a/src/pages/Questions/DraggableList/index.tsx
+++ b/src/pages/Questions/DraggableList/index.tsx
@@ -31,9 +31,6 @@ export const DraggableList = ({
createUntypedQuestion(Number(quiz.backendId));
}
}, [quiz, filteredQuestions]);
- console.log(quiz)
- console.log(questions)
- // if () {}uploadQuestionImage
return (
diff --git a/src/pages/Questions/Emoji/EmojiAnswerItem/VariantAdornment.tsx b/src/pages/Questions/Emoji/EmojiAnswerItem/VariantAdornment.tsx
index 15e3a3f9..6dd6c494 100644
--- a/src/pages/Questions/Emoji/EmojiAnswerItem/VariantAdornment.tsx
+++ b/src/pages/Questions/Emoji/EmojiAnswerItem/VariantAdornment.tsx
@@ -15,7 +15,6 @@ export default function VariantAdornment({
}) {
const theme = useTheme();
- console.log("VariantAdornment extendedText", extendedText)
return (
diff --git a/src/pages/Questions/QuestionOptions/SliderOptions/SliderOptions.tsx b/src/pages/Questions/QuestionOptions/SliderOptions/SliderOptions.tsx
index 3535cfbb..92ba114a 100644
--- a/src/pages/Questions/QuestionOptions/SliderOptions/SliderOptions.tsx
+++ b/src/pages/Questions/QuestionOptions/SliderOptions/SliderOptions.tsx
@@ -44,11 +44,6 @@ export default function SliderOptions({ question, openBranchingPage, setOpenBran
});
}, 5000);
const updateStepsDebounced = useDebouncedCallback((value: string) => {
- console.log("value")
- console.log(value)
- console.log(value.toString())
- console.log("ReplaceToNotStartZero(Number(value)) _____________________________________")
- console.log(ReplaceToNotStartZero(Number(value)))
updateQuestion(question.id, (question) => {
question.content.step = ReplaceToNotStartZero(Number(value));
});
diff --git a/src/pages/Tariffs/Tabs.tsx b/src/pages/Tariffs/Tabs.tsx
index bcfeec11..1cae19d1 100644
--- a/src/pages/Tariffs/Tabs.tsx
+++ b/src/pages/Tariffs/Tabs.tsx
@@ -1,11 +1,13 @@
import { Tabs as MuiTabs } from "@mui/material";
import { CustomTab } from "./CustomTab";
+import { TypePages } from "./types";
type TabsProps = {
names: string[];
items: string[];
- selectedItem: "day" | "count" | "dop" | "hide" | "create";
- setSelectedItem: (num: "day" | "count" | "dop") => void;
+ selectedItem: TypePages;
+ setSelectedItem: (num: TypePages) => void;
+ toDop: () => void;
};
export const Tabs = ({
@@ -18,7 +20,7 @@ export const Tabs = ({
sx={{ m: "25px" }}
TabIndicatorProps={{ sx: { display: "none" } }}
value={selectedItem}
- onChange={(event, newValue: "day" | "count" | "dop") => {
+ onChange={(event, newValue: TypePages) => {
setSelectedItem(newValue);
}}
variant="scrollable"
diff --git a/src/pages/Tariffs/TariffCardDisplaySelector.tsx b/src/pages/Tariffs/TariffCardDisplaySelector.tsx
new file mode 100644
index 00000000..617febfa
--- /dev/null
+++ b/src/pages/Tariffs/TariffCardDisplaySelector.tsx
@@ -0,0 +1,147 @@
+import { Box, useMediaQuery, useTheme } from "@mui/material"
+import { NavCard } from "./components/NavCard"
+import { createTariffElements } from "./tariffsUtils/createTariffElements"
+import SmallIconPena from "@/assets/icons/SmallIconPena"
+
+interface TariffCardDisplaySelectorProps {
+ content: {
+ title: string,
+ onClick: () => void
+ }[]
+ selectedItem: TypePages
+ tariffs: any[]
+ user: any
+ discounts: any[]
+ openModalHC: (tariffInfo: any) => void
+ userPrivilegies: any
+ startRequestCreate: () => void
+}
+
+export const TariffCardDisplaySelector = ({
+ content,
+ selectedItem,
+ tariffs,
+ user,
+ discounts,
+ openModalHC,
+ userPrivilegies,
+ startRequestCreate
+}: TariffCardDisplaySelectorProps) => {
+ const theme = useTheme()
+ const isTablet = useMediaQuery(theme.breakpoints.down(1000));
+ const sendRequest = userPrivilegies?.quizManual?.amount > 0 ? startRequestCreate : undefined
+
+ switch (selectedItem) {
+ case "dop":
+ return
+ {content.map(data => )}
+
+
+ case "hide":
+ const filteredBadgeTariffs = tariffs.filter((tariff) => {
+ return (
+ tariff.privileges[0].serviceKey === "squiz" &&
+ !tariff.isDeleted &&
+ !tariff.isCustom &&
+ tariff.privileges[0].privilegeId === "squizHideBadge" &&
+ tariff.privileges[0]?.type === "day"
+ );
+ });
+ return createTariffElements(
+ filteredBadgeTariffs,
+ false,
+ user,
+ discounts,
+ openModalHC,
+ )
+
+ case "create":
+ const filteredCreateTariffs = tariffs.filter((tariff) => {
+ return (
+ tariff.privileges[0].serviceKey === "squiz" &&
+ !tariff.isDeleted &&
+ !tariff.isCustom &&
+ tariff.privileges[0].privilegeId === "quizManual" &&
+ tariff.privileges[0]?.type === "count"
+ );
+ });
+ return createTariffElements(
+ filteredCreateTariffs,
+ false,
+ user,
+ discounts,
+ openModalHC,
+ sendRequest,
+ true,
+
+ )
+
+ case "premium":
+ const filteredPremiumTariffs = tariffs.filter((tariff) => {
+ return (
+ tariff.privileges[0].serviceKey === "squiz" &&
+ !tariff.isDeleted &&
+ !tariff.isCustom &&
+ tariff.privileges[0].privilegeId === "squizPremium" &&
+ tariff.privileges[0]?.type === "day"
+ );
+ });
+ return createTariffElements(
+ filteredPremiumTariffs,
+ false,
+ user,
+ discounts,
+ openModalHC,
+ )
+
+ case "analytics":
+ const filteredAnalyticsTariffs = tariffs.filter((tariff) => {
+ return (
+ tariff.privileges[0].serviceKey === "squiz" &&
+ !tariff.isDeleted &&
+ !tariff.isCustom &&
+ tariff.privileges[0].privilegeId === "squizAnalytics" &&
+ tariff.privileges[0]?.type === "count"
+ );
+ });
+ return createTariffElements(
+ filteredAnalyticsTariffs,
+ false,
+ user,
+ discounts,
+ openModalHC,
+ )
+
+ case "custom":
+ const filteredCustomTariffs = tariffs.filter((tariff) => {
+ return (
+ tariff.privileges[0].serviceKey === "squiz" &&
+ !tariff.isDeleted &&
+ tariff.isCustom &&
+ tariff.privileges[0]?.type === "day"
+ );
+ });
+ return createTariffElements(
+ filteredCustomTariffs,
+ false,
+ user,
+ discounts,
+ openModalHC,
+ )
+
+ default:
+ return
+ {content.map(data => )}
+
+ }
+}
\ No newline at end of file
diff --git a/src/pages/Tariffs/Tariffs.tsx b/src/pages/Tariffs/Tariffs.tsx
index 5a886bb0..7ac558e4 100644
--- a/src/pages/Tariffs/Tariffs.tsx
+++ b/src/pages/Tariffs/Tariffs.tsx
@@ -1,39 +1,34 @@
import { activatePromocode } from "@api/promocode";
import { useToken } from "@frontend/kitui";
-import ArrowLeft from "@icons/questionsPage/arrowLeft";
import {
Box,
- Button,
- Container,
- IconButton,
- Modal,
- Paper,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import { useUserStore } from "@root/user";
-import { LogoutButton } from "@ui_kit/LogoutButton";
import { useDomainDefine } from "@utils/hooks/useDomainDefine";
import { enqueueSnackbar } from "notistack";
import { useEffect, useState } from "react";
import { withErrorBoundary } from "react-error-boundary";
-import { Link, useNavigate } from "react-router-dom";
-import Logotip from "../../pages/Landing/images/icons/QuizLogo";
+import { useNavigate } from "react-router-dom";
import CollapsiblePromocodeField from "./CollapsiblePromocodeField";
import { Tabs } from "./Tabs";
import { createTariffElements } from "./tariffsUtils/createTariffElements";
import { currencyFormatter } from "./tariffsUtils/currencyFormatter";
import { useWallet, setCash } from "@root/cash";
-import { handleLogoutClick } from "@utils/HandleLogoutClick";
import { cartApi } from "@api/cart";
-import { Other } from "./pages/Other";
+import { TariffCardDisplaySelector } from "./TariffCardDisplaySelector";
import { ModalRequestCreate } from "./ModalRequestCreate";
import { cancelCC, useCC } from "@/stores/cc";
import { NavSelect } from "./NavSelect";
import { useTariffs } from '@utils/hooks/useTariffs';
import { useDiscounts } from '@utils/hooks/useDiscounts';
+import { PaymentConfirmationModal } from "./components/PaymentConfirmationModal";
+import { TariffsHeader } from "./components/TariffsHeader";
+import { inCart, outCart } from "./utils";
+import { generateHubWalletRequestURL } from "@/utils/generateHubWalletRequest";
const StepperText: Record = {
day: "Тарифы на время",
@@ -50,12 +45,17 @@ function TariffPage() {
const userId = useUserStore((state) => state.userId);
const navigate = useNavigate();
const user = useUserStore((state) => state.customerAccount);
- const a = useUserStore((state) => state.customerAccount); //c wallet
- console.log("________________34563875693785692576_____________USERRRRRRR")
- console.log(a)
- const { data: discounts } = useDiscounts(userId);
+ const userWithWallet = useUserStore((state) => state.customerAccount); //c wallet
+ const userAccount = useUserStore((state) => state.userAccount);
+ // console.info("________________userWithWallet_____________USERRRRRRR")
+ // console.info(userWithWallet)
+ // console.info("________________userAccount_____________")
+ // console.info(userAccount)
+ // console.info("________________customerAccount_____________")
+ // console.info(user)
+ const { data: discounts, error: discountsError, isLoading: discountsLoading } = useDiscounts(userId);
const [isRequestCreate, setIsRequestCreate] = useState(false);
- const [openModal, setOpenModal] = useState({});
+ const [openModal, setOpenModal] = useState<{ id?: string; price?: number }>({});
const { cashString, cashCop, cashRub } = useWallet();
const [selectedItem, setSelectedItem] = useState("day");
const { isTestServer } = useDomainDefine();
@@ -65,17 +65,14 @@ function TariffPage() {
const { data: tariffs, error: tariffsError, isLoading: tariffsLoading } = useTariffs();
-console.log("________34563875693785692576_____ TARIFFS")
-console.log(tariffs)
-
useEffect(() => {
- if (a) {
+ if (userWithWallet && user) {
let cs = currencyFormatter.format(Number(user.wallet.cash) / 100);
let cc = Number(user.wallet.cash);
let cr = Number(user.wallet.cash) / 100;
setCash(cs, cc, cr);
}
- }, [a]);
+ }, [userWithWallet, user]);
useEffect(() => {
if (cc) {
@@ -83,7 +80,26 @@ console.log(tariffs)
cancelCC()
}
}, [])
- if (!user || !tariffs || !discounts) return ;
+
+ // Проверяем, что все данные загружены и нет ошибок
+ const isDataLoading = tariffsLoading || (userId && discountsLoading);
+ const hasErrors = tariffsError || discountsError;
+
+ // Если userId есть, но customerAccount еще не загружен, показываем загрузку
+ const isCustomerAccountLoading = userId && !user;
+ const hasAllData = user && tariffs && (userId ? discounts : true);
+
+ if (isDataLoading || isCustomerAccountLoading) {
+ return ;
+ }
+
+ if (hasErrors) {
+ return ;
+ }
+
+ if (!hasAllData) {
+ return ;
+ }
const openModalHC = (tariffInfo: any) => setOpenModal(tariffInfo);
const tryBuy = async ({ id, price }: { id: string; price: number }) => {
@@ -109,11 +125,20 @@ console.log(tariffs)
//если денег не хватило
if (payError?.includes("insufficient funds") || payError?.includes("Payment Required")) {
let cashDif = Number(payError.split(":")[1]);
- var link = document.createElement("a");
- link.href = `https://${isTestServer ? "s" : ""}hub.pena.digital/quizpayment?action=squizpay&dif=${cashDif}&data=${token}&userid=${userId}`;
- if (cc) link.href = link.href + "&cc=true"//после покупки тарифа и возвращения будем знать что надо открыть модалку
- document.body.appendChild(link);
- link.click();
+
+ if (!userId) {
+ enqueueSnackbar("Ошибка: ID пользователя не найден");
+ return;
+ }
+
+ const l = generateHubWalletRequestURL({
+ action: cc ? "createquizcc" : "buy",
+ dif: cashDif.toString(),
+ userid: userId,
+ wayback: "list",
+ token
+ });
+ window.location.href = l;
return;
}
@@ -169,63 +194,10 @@ console.log(tariffs)
setIsRequestCreate(true)
}
- if (!a) return null;
+ if (!userWithWallet) return null;
return (
<>
-
-
-
-
- navigate("/list")}>
-
-
-
-
-
- Мой баланс
-
- 9 ? "13px" : "16px") : "16px"
- }
- >
- {cashString}
-
-
- {
- navigate("/");
- handleLogoutClick();
- }}
- sx={{
- ml: "20px",
- }}
- />
-
-
+
setSelectedItem("create")
},
+ {
+ title: "Премиум функции",
+ onClick: () => setSelectedItem("premium")
+ },
+ {
+ title: "Расширенная аналитика",
+ onClick: () => setSelectedItem("analytics")
+ },
+ {
+ title: "Кастомные тарифы",
+ onClick: () => setSelectedItem("custom")
+ },
]}
tariffs={tariffs}
user={user}
- discounts={discounts}
+ discounts={discounts || []}
openModalHC={openModalHC}
userPrivilegies={userPrivilegies}
startRequestCreate={startRequestCreate}
/>
)}
+ {selectedItem === "dop" && (
+ setSelectedItem("hide")
+ },
+ {
+ title: "Создать квиз на заказ",
+ onClick: () => setSelectedItem("create")
+ },
+ ]
+ : [
+ {
+ title: `Убрать логотип "PenaQuiz"`,
+ onClick: () => setSelectedItem("hide")
+ },
+ {
+ title: "Создать квиз на заказ",
+ onClick: () => setSelectedItem("create")
+ },
+ {
+ title: "Премиум функции",
+ onClick: () => setSelectedItem("premium")
+ },
+ {
+ title: "Расширенная аналитика",
+ onClick: () => setSelectedItem("analytics")
+ },
+ {
+ title: "Кастомные тарифы",
+ onClick: () => setSelectedItem("custom")
+ },
+ ]
+ }
+
+ tariffs={tariffs}
+ user={user}
+ discounts={discounts || []}
+ openModalHC={openModalHC}
+ userPrivilegies={userPrivilegies}
+ startRequestCreate={startRequestCreate}
+ />
+ )}
- 0}
onClose={() => setOpenModal({})}
- >
-
-
- Вы подтверждаете платёж в сумму{" "}
- {openModal.price ? openModal.price.toFixed(2) : 0} ₽
-
-
-
-
+ onConfirm={() => {
+ if (openModal.id && openModal.price !== undefined) {
+ tryBuy({ id: openModal.id, price: openModal.price });
+ }
+ }}
+ price={openModal.price || 0}
+ />
setIsRequestCreate(false)} />
>
);
@@ -364,47 +374,3 @@ const LoadingPage = () => (
);
-
-export const inCart = () => {
- let saveCart = JSON.parse(localStorage.getItem("saveCart") || "[]");
- if (Array.isArray(saveCart)) {
- saveCart.forEach(async (id: string) => {
- 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));
- }
- });
- } else {
- localStorage.setItem("saveCart", "[]");
- }
-};
-export const outCart = (cart: string[]) => {
- //Сделаем муторно и подольше, зато при прерывании сессии данные потеряются минимально
- if (cart.length > 0) {
- cart.forEach(async (id: string) => {
- const [_, deleteError] = await cartApi.delete(id);
-
- if (deleteError) {
- console.error(deleteError);
- cancelCC()//мы хотели открыть модалку после покупки тарифа на создание квиза, но не вышло и модалку не откроем
-
- return;
- }
-
- let saveCart = JSON.parse(localStorage.getItem("saveCart") || "[]") || [];
- if (!Array.isArray(saveCart)) saveCart = []
- saveCart = saveCart.push(id);
- localStorage.setItem("saveCart", JSON.stringify(saveCart));
- });
- }
-
-};
diff --git a/src/pages/Tariffs/components/PaymentConfirmationModal.tsx b/src/pages/Tariffs/components/PaymentConfirmationModal.tsx
new file mode 100644
index 00000000..0d13eb46
--- /dev/null
+++ b/src/pages/Tariffs/components/PaymentConfirmationModal.tsx
@@ -0,0 +1,51 @@
+import {
+ Button,
+ Modal,
+ Paper,
+ Typography,
+} from "@mui/material";
+
+interface PaymentConfirmationModalProps {
+ open: boolean;
+ onClose: () => void;
+ onConfirm: () => void;
+ price: number;
+}
+
+export const PaymentConfirmationModal = ({
+ open,
+ onClose,
+ onConfirm,
+ price,
+}: PaymentConfirmationModalProps) => {
+ return (
+
+
+
+ Вы подтверждаете платёж в сумму{" "}
+ {price ? price.toFixed(2) : 0} ₽
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/src/pages/Tariffs/components/TariffsHeader.tsx b/src/pages/Tariffs/components/TariffsHeader.tsx
new file mode 100644
index 00000000..63965510
--- /dev/null
+++ b/src/pages/Tariffs/components/TariffsHeader.tsx
@@ -0,0 +1,83 @@
+import { useToken } from "@frontend/kitui";
+import ArrowLeft from "@icons/questionsPage/arrowLeft";
+import {
+ Box,
+ Container,
+ IconButton,
+ Typography,
+ useMediaQuery,
+ useTheme,
+} from "@mui/material";
+import { useUserStore } from "@root/user";
+import { LogoutButton } from "@ui_kit/LogoutButton";
+import { handleLogoutClick } from "@utils/HandleLogoutClick";
+import { Link, useNavigate } from "react-router-dom";
+import Logotip from "../../../pages/Landing/images/icons/QuizLogo";
+
+interface TariffsHeaderProps {
+ cashString: string;
+}
+
+export const TariffsHeader = ({ cashString }: TariffsHeaderProps) => {
+ const theme = useTheme();
+ const navigate = useNavigate();
+ const isMobile = useMediaQuery(theme.breakpoints.down(600));
+ const isTablet = useMediaQuery(theme.breakpoints.down(1000));
+
+ return (
+
+
+
+
+ navigate("/list")}>
+
+
+
+
+
+ Мой баланс
+
+ 9 ? "13px" : "16px") : "16px"
+ }
+ >
+ {cashString}
+
+
+ {
+ navigate("/");
+ handleLogoutClick();
+ }}
+ sx={{
+ ml: "20px",
+ }}
+ />
+
+
+ );
+};
\ No newline at end of file
diff --git a/src/pages/Tariffs/pages/HideLogo.tsx b/src/pages/Tariffs/pages/HideLogo.tsx
deleted file mode 100644
index e69de29b..00000000
diff --git a/src/pages/Tariffs/pages/Other.tsx b/src/pages/Tariffs/pages/Other.tsx
deleted file mode 100644
index 5373da51..00000000
--- a/src/pages/Tariffs/pages/Other.tsx
+++ /dev/null
@@ -1,97 +0,0 @@
-import { Box, useMediaQuery, useTheme } from "@mui/material"
-import { NavCard } from "../components/NavCard"
-import { createTariffElements } from "../tariffsUtils/createTariffElements"
-import SmallIconPena from "@/assets/icons/SmallIconPena"
-
-interface Props {
- content: {
- title: string,
- onClick: () => void
- }[]
- selectedItem: TypePages
-}
-
-export const Other = ({
- content,
- selectedItem,
-
- tariffs,
- user,
- discounts,
- openModalHC,
- userPrivilegies,
- startRequestCreate
-}: any) => {
- const theme = useTheme()
- const isTablet = useMediaQuery(theme.breakpoints.down(1000));
-const sendRequest = userPrivilegies?.quizManual?.amount > 0 ? startRequestCreate : undefined
-
- switch (selectedItem) {
- case "hide":
- const filteredBadgeTariffs = tariffs.filter((tariff) => {
- return (
- tariff.privileges[0].serviceKey === "squiz" &&
- !tariff.isDeleted &&
- !tariff.isCustom &&
- tariff.privileges[0].privilegeId === "squizHideBadge" &&
- tariff.privileges[0]?.type === "day"
- );
- });
- return
- {createTariffElements(
- filteredBadgeTariffs,
- false,
- user,
- discounts,
- openModalHC,
- )}
-
- case "create":
- const filteredCreateTariffs = tariffs.filter((tariff) => {
- return (
- tariff.privileges[0].serviceKey === "squiz" &&
- !tariff.isDeleted &&
- !tariff.isCustom &&
- tariff.privileges[0].privilegeId === "quizManual" &&
- tariff.privileges[0]?.type === "count"
- );
- });
- return
- {createTariffElements(
- filteredCreateTariffs,
- false,
- user,
- discounts,
- openModalHC,
- sendRequest,
- true,
-
- )}
-
- default:
- return
- {content.map(data => )}
-
- }
-}
diff --git a/src/pages/Tariffs/tariffsUtils/createTariffElements.tsx b/src/pages/Tariffs/tariffsUtils/createTariffElements.tsx
index 7999e51f..1511e49c 100644
--- a/src/pages/Tariffs/tariffsUtils/createTariffElements.tsx
+++ b/src/pages/Tariffs/tariffsUtils/createTariffElements.tsx
@@ -17,10 +17,6 @@ export const createTariffElements = (
cc?: boolean,
icon?: ReactNode
) => {
- console.log("start work createTariffElements")
- console.log("filteredTariffs ", filteredTariffs)
- console.log("user ", user)
- console.log("user.isUserNko, ", user.isUserNko)
const tariffElements = filteredTariffs
.filter((tariff) => tariff.privileges.length > 0)
.map((tariff, index) => {
diff --git a/src/pages/Tariffs/types.ts b/src/pages/Tariffs/types.ts
index fd6c0dea..6db0e8d6 100644
--- a/src/pages/Tariffs/types.ts
+++ b/src/pages/Tariffs/types.ts
@@ -1 +1 @@
-type TypePages = "count" | "day" | "dop" | "hide" | "create"
\ No newline at end of file
+type TypePages = "count" | "day" | "dop" | "hide" | "create" | "premium" | "analytics" | "custom"
\ No newline at end of file
diff --git a/src/pages/Tariffs/utils.ts b/src/pages/Tariffs/utils.ts
new file mode 100644
index 00000000..ca612a35
--- /dev/null
+++ b/src/pages/Tariffs/utils.ts
@@ -0,0 +1,46 @@
+import { cartApi } from "@api/cart";
+import { cancelCC } from "@/stores/cc";
+
+export const inCart = () => {
+ let saveCart = JSON.parse(localStorage.getItem("saveCart") || "[]");
+ if (Array.isArray(saveCart)) {
+ saveCart.forEach(async (id: string) => {
+ 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));
+ }
+ });
+ } else {
+ localStorage.setItem("saveCart", "[]");
+ }
+};
+
+export const outCart = (cart: string[]) => {
+ //Сделаем муторно и подольше, зато при прерывании сессии данные потеряются минимально
+ if (cart.length > 0) {
+ cart.forEach(async (id: string) => {
+ const [_, deleteError] = await cartApi.delete(id);
+
+ if (deleteError) {
+ console.error(deleteError);
+ cancelCC()//мы хотели открыть модалку после покупки тарифа на создание квиза, но не вышло и модалку не откроем
+
+ return;
+ }
+
+ let saveCart = JSON.parse(localStorage.getItem("saveCart") || "[]") || [];
+ if (!Array.isArray(saveCart)) saveCart = []
+ saveCart = saveCart.push(id);
+ localStorage.setItem("saveCart", JSON.stringify(saveCart));
+ });
+ }
+};
\ No newline at end of file
diff --git a/src/pages/auth/RecoverPassword.tsx b/src/pages/auth/RecoverPassword.tsx
index 8afd81ca..4849d9e4 100644
--- a/src/pages/auth/RecoverPassword.tsx
+++ b/src/pages/auth/RecoverPassword.tsx
@@ -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 "@frontend/kitui";
import { setAuthToken } from "@frontend/kitui";
import { parseAxiosError } from "@utils/parse-error";
import { recoverUser } from "@api/user";
diff --git a/src/pages/auth/Signup.tsx b/src/pages/auth/Signup.tsx
index 97cc5d12..204fbe88 100644
--- a/src/pages/auth/Signup.tsx
+++ b/src/pages/auth/Signup.tsx
@@ -242,7 +242,7 @@ export default function SignupDialog() {
diff --git a/src/pages/createQuize/AvailablePrivilege.tsx b/src/pages/createQuize/AvailablePrivilege.tsx
index 7b7cd5b0..dbca4762 100644
--- a/src/pages/createQuize/AvailablePrivilege.tsx
+++ b/src/pages/createQuize/AvailablePrivilege.tsx
@@ -53,8 +53,6 @@ export default function AvailablePrivilege() {
}
const quizUnlimDays = getCramps(quizUnlimTime, userPrivileges?.quizUnlimTime?.created_at || "");
const squizBadgeDays = getCramps(squizHideBadge, userPrivileges?.squizHideBadge?.created_at || "");
- console.log(userPrivileges)
- console.log(quizUnlimTime)
return (
()(
@@ -95,7 +98,6 @@ export const addOrUpdateUnauthMessages = (receivedMessages: TicketMessage[]) =>
// if (Array.isArray(sortedMessages)) ticket.lastMessageId = sortedMessages?.at(-1)?.id || "";
if (Array.isArray(sortedMessages)) ticket.lastMessageId = sortedMessages?.[sortedMessages.length - 1]?.id || "";
- console.log("ticketStudy, ", sortedMessages)
});
export const incrementUnauthMessage = () =>
@@ -156,9 +158,17 @@ export const updateTicket = (
},
);
-function setProducedState(
+function setProducedState(
recipe: (state: TicketStore) => void,
action?: A,
) {
useTicketStore.setState((state) => produce(state, recipe), false, action);
}
+
+// Функция для записи тикетов в стор
+export const setTickets = (tickets: Ticket[] | null) => {
+ useTicketStore.setState((state) => ({
+ ...state,
+ tickets: tickets || []
+ }), false);
+};
diff --git a/src/stores/user.ts b/src/stores/user.ts
index 20352de0..4de52d83 100644
--- a/src/stores/user.ts
+++ b/src/stores/user.ts
@@ -71,5 +71,6 @@ export const clearUserData = () => useUserStore.setState({ ...initialState });
export const setUserAccount = (userAccount: OriginalUserAccount) =>
useUserStore.setState({ userAccount });
-export const setCustomerAccount = (customerAccount: UserAccount) =>
+export const setCustomerAccount = (customerAccount: UserAccount) => {
useUserStore.setState({ customerAccount });
+};
diff --git a/src/ui_kit/CheckFastlink.tsx b/src/ui_kit/CheckFastlink.tsx
index ce7cbbaa..f90db289 100644
--- a/src/ui_kit/CheckFastlink.tsx
+++ b/src/ui_kit/CheckFastlink.tsx
@@ -3,7 +3,7 @@ import { Box, Button, Modal, Typography } from "@mui/material";
import { enqueueSnackbar } from "notistack";
import { mutate } from "swr";
-import { makeRequest } from "@api/makeRequest";
+import { makeRequest } from "@frontend/kitui";
import { getDiscounts } from "@api/discounts";
import { clearUserData, OriginalUserAccount, setUserAccount, useUserStore } from "@root/user";
diff --git a/src/ui_kit/FloatingSupportChat/Chat.tsx b/src/ui_kit/FloatingSupportChat/Chat.tsx
index 28e17676..5e6046d1 100644
--- a/src/ui_kit/FloatingSupportChat/Chat.tsx
+++ b/src/ui_kit/FloatingSupportChat/Chat.tsx
@@ -1,9 +1,6 @@
import {
Box,
- FormControl,
IconButton,
- InputAdornment,
- InputBase,
SxProps,
Theme,
Typography,
@@ -17,46 +14,45 @@ import {
useTicketStore,
} from "@root/ticket";
import type { TouchEvent, WheelEvent } from "react";
-import * as React from "react";
-import { useEffect, useMemo, useRef, useState } from "react";
-import ChatMessage from "./ChatMessage";
-import ChatVideo from "./ChatVideo";
-import SendIcon from "@icons/SendIcon";
+import { useEffect, useMemo, useRef } from "react";
+import ChatMessageRenderer from "./ChatMessageRenderer";
+import ChatInput from "./ChatInput";
import UserCircleIcon from "./UserCircleIcon";
import { throttle, TicketMessage } from "@frontend/kitui";
import ArrowLeft from "@icons/questionsPage/arrowLeft";
import { useUserStore } from "@root/user";
-import AttachFileIcon from "@mui/icons-material/AttachFile";
-import ChatImage from "./ChatImage";
-import ChatDocument from "@ui_kit/FloatingSupportChat/ChatDocument";
-import {
- ACCEPT_SEND_MEDIA_TYPES_MAP,
- checkAcceptableMediaType,
-} from "@utils/checkAcceptableMediaType";
-import { enqueueSnackbar } from "notistack";
interface Props {
open: boolean;
sx?: SxProps;
onclickArrow?: () => void;
sendMessage: (a: string) => Promise;
- sendFile: (a: File | undefined) => Promise;
- greetingMessage: TicketMessage;
+ sendFile: (a: File | undefined) => Promise;
}
+const greetingMessage: TicketMessage = {
+ id: "greeting",
+ ticket_id: "",
+ user_id: "system",
+ session_id: "",
+ message: "Здравствуйте, задайте ваш вопрос и наш оператор вам ответит в течение 10 минут",
+ files: [],
+ shown: {},
+ request_screenshot: "",
+ created_at: new Date().toISOString(),
+ system: false
+};
+
export default function Chat({
open = false,
sx,
onclickArrow,
sendMessage,
sendFile,
- greetingMessage,
}: Props) {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const isMobile = useMediaQuery(theme.breakpoints.down(800));
- const [messageField, setMessageField] = useState("");
- const [disableFileButton, setDisableFileButton] = useState(false);
const user = useUserStore((state) => state.user?._id);
const ticket = useTicketStore(
@@ -72,31 +68,11 @@ export default function Chat({
const chatBoxRef = useRef(null);
useEffect(() => {
- addOrUpdateUnauthMessages([greetingMessage]);
if (open) {
scrollToBottom();
}
}, [open]);
- const sendMessageHC = async () => {
- const successful = await sendMessage(messageField);
- if (successful) {
- setMessageField("");
- }
- };
- const sendFileHC = async (file: File) => {
- const check = checkAcceptableMediaType(file);
- if (check.length > 0) {
- enqueueSnackbar(check);
- return;
- }
- setDisableFileButton(true);
- await sendFile(file);
- setDisableFileButton(false);
- };
-
- const fileInputRef = useRef(null);
-
const throttledScrollHandler = useMemo(
() =>
throttle(() => {
@@ -152,14 +128,6 @@ export default function Chat({
behavior,
});
}
- const handleTextfieldKeyPress: React.KeyboardEventHandler<
- HTMLInputElement | HTMLTextAreaElement
- > = (e) => {
- if (e.key === "Enter" && !e.shiftKey) {
- e.preventDefault();
- sendMessageHC();
- }
- };
return (
<>
@@ -240,164 +208,28 @@ export default function Chat({
>
{ticket.sessionData?.ticketId &&
messages.map((message) => {
- const isFileVideo = () => {
- if (message.files) {
- return ACCEPT_SEND_MEDIA_TYPES_MAP.video.some(
- (fileType) =>
- message.files[0].toLowerCase().endsWith(fileType),
- );
- }
- };
- const isFileImage = () => {
- if (message.files) {
- return ACCEPT_SEND_MEDIA_TYPES_MAP.picture.some(
- (fileType) =>
- message.files[0].toLowerCase().endsWith(fileType),
- );
- }
- };
- const isFileDocument = () => {
- if (message.files) {
- return ACCEPT_SEND_MEDIA_TYPES_MAP.document.some(
- (fileType) =>
- message.files[0].toLowerCase().endsWith(fileType),
- );
- }
- };
- if (message.files.length > 0 && isFileImage()) {
- return (
-
- );
- }
- if (message.files.length > 0 && isFileVideo()) {
- return (
-
- );
- }
- if (message.files.length > 0 && isFileDocument()) {
- return (
-
- );
- }
+ const isSelf = (ticket.sessionData?.sessionId || user) === message.user_id;
+
return (
-
);
})}
{!ticket.sessionData?.ticketId && (
-
)}
-
- setMessageField(e.target.value)}
- endAdornment={
-
- {
- if (!disableFileButton) fileInputRef.current?.click();
- }}
- >
-
-
- {
- if (e.target.files?.[0])
- sendFileHC(e.target.files?.[0]);
- }}
- style={{ display: "none" }}
- type="file"
- />
-
-
-
-
- }
- />
-
+
)}
diff --git a/src/ui_kit/FloatingSupportChat/ChatInput.tsx b/src/ui_kit/FloatingSupportChat/ChatInput.tsx
new file mode 100644
index 00000000..ba17b903
--- /dev/null
+++ b/src/ui_kit/FloatingSupportChat/ChatInput.tsx
@@ -0,0 +1,137 @@
+import { useCallback, useRef, useState } from "react";
+import {
+ FormControl,
+ IconButton,
+ InputAdornment,
+ InputBase,
+ useMediaQuery,
+ useTheme,
+} from "@mui/material";
+import SendIcon from "@icons/SendIcon";
+import AttachFileIcon from "@mui/icons-material/AttachFile";
+import { checkAcceptableMediaType } from "@utils/checkAcceptableMediaType";
+import { enqueueSnackbar } from "notistack";
+
+interface ChatInputProps {
+ sendMessage: (message: string) => Promise;
+ sendFile: (file: File | undefined) => Promise;
+ isMessageSending: boolean;
+}
+
+const ChatInput = ({ sendMessage, sendFile, isMessageSending }: ChatInputProps) => {
+ const theme = useTheme();
+ const upMd = useMediaQuery(theme.breakpoints.up("md"));
+ const [messageField, setMessageField] = useState("");
+ const [disableFileButton, setDisableFileButton] = useState(false);
+ const fileInputRef = useRef(null);
+
+ const handleSendMessage = useCallback(async () => {
+ const successful = await sendMessage(messageField);
+ if (successful) {
+ setMessageField("");
+ }
+ }, [sendMessage, messageField]);
+
+ const handleSendFile = useCallback(async (file: File) => {
+ const check = checkAcceptableMediaType(file);
+ if (check.length > 0) {
+ enqueueSnackbar(check);
+ return;
+ }
+ setDisableFileButton(true);
+ await sendFile(file);
+ setDisableFileButton(false);
+ }, [sendFile]);
+
+ const handleFileInputChange = useCallback((e: React.ChangeEvent) => {
+ if (e.target.files?.[0]) {
+ handleSendFile(e.target.files[0]);
+ }
+ }, [handleSendFile]);
+
+ const handleFileButtonClick = useCallback(() => {
+ if (!disableFileButton) {
+ fileInputRef.current?.click();
+ }
+ }, [disableFileButton]);
+
+ const handleTextfieldKeyPress: React.KeyboardEventHandler<
+ HTMLInputElement | HTMLTextAreaElement
+ > = useCallback((e) => {
+ if (e.key === "Enter" && !e.shiftKey) {
+ e.preventDefault();
+ handleSendMessage();
+ }
+ }, [handleSendMessage]);
+
+ const handleInputChange = useCallback((e: React.ChangeEvent) => {
+ setMessageField(e.target.value);
+ }, []);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ }
+ />
+
+ );
+};
+
+export default ChatInput;
\ No newline at end of file
diff --git a/src/ui_kit/FloatingSupportChat/ChatMessageRenderer.tsx b/src/ui_kit/FloatingSupportChat/ChatMessageRenderer.tsx
new file mode 100644
index 00000000..4955915e
--- /dev/null
+++ b/src/ui_kit/FloatingSupportChat/ChatMessageRenderer.tsx
@@ -0,0 +1,70 @@
+import { memo, useMemo } from "react";
+import { TicketMessage } from "@frontend/kitui";
+import ChatMessage from "./ChatMessage";
+import ChatImage from "./ChatImage";
+import ChatVideo from "./ChatVideo";
+import ChatDocument from "./ChatDocument";
+import { ACCEPT_SEND_MEDIA_TYPES_MAP } from "@utils/checkAcceptableMediaType";
+
+interface ChatMessageRendererProps {
+ message: TicketMessage;
+ isSelf: boolean;
+}
+
+const ChatMessageRenderer = memo(({ message, isSelf }: ChatMessageRendererProps) => {
+ const fileType = useMemo(() => {
+ if (!message.files?.length) return null;
+
+ const fileName = message.files[0].toLowerCase();
+
+ if (ACCEPT_SEND_MEDIA_TYPES_MAP.video.some(fileType => fileName.endsWith(fileType))) {
+ return 'video';
+ }
+
+ if (ACCEPT_SEND_MEDIA_TYPES_MAP.picture.some(fileType => fileName.endsWith(fileType))) {
+ return 'image';
+ }
+
+ if (ACCEPT_SEND_MEDIA_TYPES_MAP.document.some(fileType => fileName.endsWith(fileType))) {
+ return 'document';
+ }
+
+ return null;
+ }, [message.files]);
+
+ // Если есть файлы и определён тип
+ if (message.files?.length > 0 && fileType) {
+ const commonProps = {
+ unAuthenticated: true,
+ key: message.id,
+ file: message.files[0],
+ createdAt: message.created_at,
+ isSelf,
+ };
+
+ switch (fileType) {
+ case 'image':
+ return ;
+ case 'video':
+ return ;
+ case 'document':
+ return ;
+ default:
+ break;
+ }
+ }
+
+ // Текстовое сообщение
+ return (
+
+ );
+});
+
+ChatMessageRenderer.displayName = 'ChatMessageRenderer';
+
+export default ChatMessageRenderer;
\ No newline at end of file
diff --git a/src/ui_kit/FloatingSupportChat/FloatingSupportChat.tsx b/src/ui_kit/FloatingSupportChat/FloatingSupportChat.tsx
index c59aa418..e133464f 100644
--- a/src/ui_kit/FloatingSupportChat/FloatingSupportChat.tsx
+++ b/src/ui_kit/FloatingSupportChat/FloatingSupportChat.tsx
@@ -1,5 +1,5 @@
import type { ReactNode, Ref } from "react";
-import { forwardRef, useEffect, useState } from "react";
+import { forwardRef, useEffect, useState, useMemo } from "react";
import {
Badge,
Box,
@@ -44,10 +44,9 @@ interface Props {
handleChatClickClose: () => void;
handleChatClickSwitch: () => void;
sendMessage: (a: string) => Promise;
- sendFile: (a: File | undefined) => Promise;
+ sendFile: (a: File | undefined) => Promise;
modalWarningType: string | null;
setModalWarningType: any;
- greetingMessage: TicketMessage;
}
export default function FloatingSupportChat({
@@ -59,7 +58,6 @@ export default function FloatingSupportChat({
sendFile,
modalWarningType,
setModalWarningType,
- greetingMessage,
}: Props) {
const [monitorType, setMonitorType] = useState<"desktop" | "mobile" | "">("");
const theme = useTheme();
@@ -72,6 +70,48 @@ export default function FloatingSupportChat({
(state) => state[user ? "authData" : "unauthData"],
);
+ const ticket = useTicketStore(
+ (state) => state[user ? "authData" : "unauthData"],
+ );
+
+ // Функция для подсчёта непрочитанных сообщений согласно новой логике
+ const unreadCount = useMemo(() => {
+ if (messages.length === 0) return 0;
+
+ const currentUserId = user || ticket?.sessionData?.sessionId;
+ if (!currentUserId) return 0;
+
+ // Если последнее сообщение моё - не показываем количество
+ const lastMessage = messages[messages.length - 1];
+ if (lastMessage.user_id === currentUserId) {
+ return 0;
+ }
+
+ // Если последнее сообщение не моё и оно не прочитано - считаем его как +1
+ if (lastMessage.shown?.me !== 1) {
+ let count = 1;
+
+ // Считаем все последующие сообщения до тех пор пока не воткнёмся в моё сообщение
+ for (let i = messages.length - 2; i >= 0; i--) {
+ const message = messages[i];
+
+ // Если встретили моё сообщение - останавливаемся
+ if (message.user_id === currentUserId) {
+ break;
+ }
+
+ // Если сообщение не прочитано - добавляем к счётчику
+ if (message.shown?.me !== 1) {
+ count++;
+ }
+ }
+
+ return count;
+ }
+
+ return 0;
+ }, [messages, user, ticket?.sessionData?.sessionId]);
+
useEffect(() => {
const onResize = () => {
if (document.fullscreenElement) {
@@ -108,7 +148,6 @@ export default function FloatingSupportChat({
sx={{ alignSelf: "start", width: "clamp(200px, 100%, 400px)" }}
sendMessage={sendMessage}
sendFile={sendFile}
- greetingMessage={greetingMessage}
/>
)}
shown?.me !== 1).length || 0
- }
+ badgeContent={unreadCount}
sx={{
"& .MuiBadge-badge": {
display: isChatOpened ? "none" : "flex",
diff --git a/src/ui_kit/FloatingSupportChat/index.tsx b/src/ui_kit/FloatingSupportChat/index.tsx
index eb17a552..dc0830b5 100644
--- a/src/ui_kit/FloatingSupportChat/index.tsx
+++ b/src/ui_kit/FloatingSupportChat/index.tsx
@@ -1,13 +1,15 @@
+import { useCallback, useEffect, useMemo, useState } from "react";
import {
TicketMessage,
+ createTicket,
+ shownMessage,
useSSESubscription,
useTicketMessages,
useTicketsFetcher,
+ sendFile as sf
} from "@frontend/kitui";
import FloatingSupportChat from "./FloatingSupportChat";
import { useUserStore } from "@root/user";
-import { useCallback, useEffect, useMemo, useState } from "react";
-import { sendTicketMessage, shownMessage } from "../../api/ticket";
import { useSSETab } from "../../utils/hooks/useSSETab";
import {
addOrUpdateUnauthMessages,
@@ -21,7 +23,6 @@ import {
} from "@root/ticket";
import { enqueueSnackbar } from "notistack";
import { parseAxiosError } from "@utils/parse-error";
-import { createTicket, sendFile as sendFileRequest } from "@api/ticket";
import { selectSendingMethod } from "./utils";
type ModalWarningType =
@@ -71,60 +72,6 @@ export default () => {
setIsChatOpened((state) => !state);
};
- const getGreetingMessage: TicketMessage = useMemo(() => {
- const workingHoursMessage =
- "Здравствуйте, задайте ваш вопрос и наш оператор вам ответит в течение 10 минут";
- const offHoursMessage =
- "Здравствуйте, задайте ваш вопрос и наш оператор вам ответит в течение 10 минут";
- const date = new Date();
- const currentHourUTC = date.getUTCHours();
- const MscTime = 3; // Москва UTC+3;
- const moscowHour = (currentHourUTC + MscTime) % 24;
- const greetingMessage =
- moscowHour >= 3 && moscowHour < 10
- ? offHoursMessage
- : workingHoursMessage;
-
- return {
- created_at: new Date().toISOString(),
- files: [],
- id: "111",
- message: greetingMessage,
- request_screenshot: "",
- session_id: "greetingMessage",
- shown: { me: 1 },
- ticket_id: "111",
- user_id: "greetingMessage",
- };
- }, [isChatOpened]);
-
- useTicketsFetcher({
- url: `${process.env.REACT_APP_DOMAIN}/heruvym/v1.0.0/getTickets`,
- ticketsPerPage: 10,
- ticketApiPage: 0,
- onSuccess: (result) => {
- if (result.data?.length) {
- const currentTicket = result.data.find(
- ({ origin }) => !origin.includes("/support"),
- );
-
- if (!currentTicket) {
- return;
- }
-
- setTicketData({
- ticketId: currentTicket.id,
- sessionId: currentTicket.sess,
- });
- }
- },
- onError: (error: Error) => {
- const message = parseAxiosError(error);
- if (message) enqueueSnackbar(message);
- },
- onFetchStateChange: () => {},
- enabled: Boolean(user),
- });
useTicketMessages({
url: `${process.env.REACT_APP_DOMAIN}/heruvym/v1.0.0/getMessages`,
@@ -156,7 +103,6 @@ export default () => {
);
if (isTicketClosed) {
cleanAuthTicketData();
- addOrUpdateUnauthMessages([getGreetingMessage]);
if (!user) {
cleanUnauthTicketData();
localStorage.removeItem("unauth-ticket");
@@ -184,33 +130,44 @@ export default () => {
({ shown }) => shown?.me !== 1,
);
- newMessages.map(async ({ id }) => {
- await shownMessage(id);
- });
+ // Находим последнее сообщение, которое не от пользователя
+ const lastNonUserMessage = newMessages
+ .filter(({ user_id }) => (ticket.sessionData?.sessionId || user) !== user_id)
+ .pop();
+
+ // Отправляем shown только на последнее сообщение, которое не от пользователя
+ if (lastNonUserMessage) {
+ shownMessage(lastNonUserMessage.id);
+ }
}
}, [isChatOpened, ticket.messages]);
const sendMessage = async (messageField: string) => {
if (!messageField || ticket.isMessageSending) return false;
-
+
setSseEnabled(true);
setIsMessageSending(true);
- let successful = await selectSendingMethod({messageField});
+ let successful = await selectSendingMethod({ messageField });
setIsMessageSending(false);
return successful;
};
- const sendFile = async (file: File) => {
- if (file === undefined) return true;
+ const sendFile = async (file: File | undefined): Promise => {
+ if (file === undefined) return;
let ticketId = ticket.sessionData?.ticketId;
if (!ticket.sessionData?.ticketId) {
- const [data, createError] = await createTicket("", Boolean(user));
+ const [data, createError] = await createTicket({
+ message: "",
+ useToken: Boolean(user),
+ systemError: false
+ });
ticketId = data?.Ticket;
if (createError || !data) {
- enqueueSnackbar(createError);
+ enqueueSnackbar(`Не удалось создать диалог ${parseAxiosError(createError)}`);
+ return;
} else {
setTicketData({ ticketId: data.Ticket, sessionId: data.sess });
}
@@ -219,15 +176,16 @@ export default () => {
}
if (ticketId !== undefined) {
- if (file.size > MAX_FILE_SIZE) return setModalWarningType("errorSize");
-
- const [_, sendFileError] = await sendFileRequest(ticketId, file);
-
- if (sendFileError) {
- enqueueSnackbar(sendFileError);
+ if (file.size > MAX_FILE_SIZE) {
+ setModalWarningType("errorSize");
+ return;
}
- return true;
+ const [_, sendFileError] = await sf({ticketId, file});
+
+ if (sendFileError) {
+ enqueueSnackbar(`Не удалось отправить файл ${parseAxiosError(sendFileError)}`);
+ }
}
};
@@ -241,7 +199,6 @@ export default () => {
sendFile={sendFile}
modalWarningType={modalWarningType}
setModalWarningType={setModalWarningType}
- greetingMessage={getGreetingMessage}
/>
);
};
diff --git a/src/ui_kit/FloatingSupportChat/useTechnicalSupport.ts b/src/ui_kit/FloatingSupportChat/useTechnicalSupport.ts
deleted file mode 100644
index 21430c7e..00000000
--- a/src/ui_kit/FloatingSupportChat/useTechnicalSupport.ts
+++ /dev/null
@@ -1,266 +0,0 @@
-import { sendTicketMessage, shownMessage } from "@/api/ticket";
-import { useSSETab } from "@/utils/hooks/useSSETab";
-import { parseAxiosError } from "@/utils/parse-error";
-import { TicketMessage, createTicket, useSSESubscription, useTicketMessages, useTicketsFetcher, sendFile as sf } from "@frontend/kitui";
-
-import {
- addOrUpdateUnauthMessages,
- cleanAuthTicketData,
- cleanUnauthTicketData,
- setIsMessageSending,
- setTicketData,
- setUnauthIsPreventAutoscroll,
- setUnauthTicketMessageFetchState,
- useTicketStore,
-} from "@root/ticket";
-import { enqueueSnackbar } from "notistack";
-import { useCallback, useEffect, useMemo, useState } from "react";
-
-interface Props {
- userId?: string;
-
-}
-
-type ModalWarningType =
- | "errorType"
- | "errorSize"
- | "picture"
- | "video"
- | "audio"
- | "document"
- | null;
-const MAX_FILE_SIZE = 419430400;
-const ACCEPT_SEND_FILE_TYPES_MAP = [
- ".jpeg",
- ".jpg",
- ".png",
- ".mp4",
- ".doc",
- ".docx",
- ".pdf",
- ".txt",
- ".xlsx",
- ".csv",
-] as const;
-export default ({ userId }:Props) => {
- const ticket = useTicketStore((state) => state[userId ? "authData" : "unauthData"]);
-
- const { isActiveSSETab, updateSSEValue } = useSSETab(
- "ticket",
- addOrUpdateUnauthMessages,
- );
-
- const [modalWarningType, setModalWarningType] =
- useState(null);
- const [isChatOpened, setIsChatOpened] = useState(false);
- const [sseEnabled, setSseEnabled] = useState(true);
-
- const handleChatClickOpen = () => {
- setIsChatOpened(true);
- };
- const handleChatClickClose = () => {
- setIsChatOpened(false);
- };
- const handleChatClickSwitch = () => {
- setIsChatOpened((state) => !state);
- };
-
- const getGreetingMessage: TicketMessage = useMemo(() => {
- const workingHoursMessage =
- "Здравствуйте, задайте ваш вопрос и наш оператор вам ответит в течение 10 минут";
- const offHoursMessage =
- "Здравствуйте, задайте ваш вопрос и наш оператор вам ответит в течение 10 минут";
- const date = new Date();
- const currentHourUTC = date.getUTCHours();
- const MscTime = 3; // Москва UTC+3;
- const moscowHour = (currentHourUTC + MscTime) % 24;
- const greetingMessage =
- moscowHour >= 3 && moscowHour < 10
- ? offHoursMessage
- : workingHoursMessage;
-
- return {
- created_at: new Date().toISOString(),
- files: [],
- id: "111",
- message: greetingMessage,
- request_screenshot: "",
- session_id: "greetingMessage",
- shown: { me: 1 },
- ticket_id: "111",
- user_id: "greetingMessage",
- };
- }, [isChatOpened]);
-
- useTicketsFetcher({
- url: `${process.env.REACT_APP_DOMAIN}/heruvym/v1.0.0/getTickets`,
- ticketsPerPage: 10,
- ticketApiPage: 0,
- onSuccess: (result) => {
- if (result.data?.length) {
- const currentTicket = result.data.find(
- ({ origin }) => !origin.includes("/support"),
- );
-
- if (!currentTicket) {
- return;
- }
-
- setTicketData({
- ticketId: currentTicket.id,
- sessionId: currentTicket.sess,
- });
- }
- },
- onError: (error: Error) => {
- const message = parseAxiosError(error);
- if (message) enqueueSnackbar(message);
- },
- onFetchStateChange: () => {},
- enabled: Boolean(userId),
- });
-
- useTicketMessages({
- url: `${process.env.REACT_APP_DOMAIN}/heruvym/v1.0.0/getMessages`,
- isUnauth: true,
- ticketId: ticket.sessionData?.ticketId,
- messagesPerPage: ticket.messagesPerPage,
- messageApiPage: ticket.apiPage,
- onSuccess: useCallback((messages) => {
- addOrUpdateUnauthMessages(messages);
- }, []),
- onError: useCallback((error: Error) => {
- if (error.name === "CanceledError") {
- return;
- }
-
- const [message] = parseAxiosError(error);
- if (message) enqueueSnackbar(message);
- }, []),
- onFetchStateChange: setUnauthTicketMessageFetchState,
- });
-
- useSSESubscription({
- enabled:
- sseEnabled && isActiveSSETab && Boolean(ticket.sessionData?.sessionId),
- url: `${process.env.REACT_APP_DOMAIN}/heruvym/v1.0.0/ticket?ticket=${ticket.sessionData?.ticketId}&s=${ticket.sessionData?.sessionId}`,
- onNewData: (ticketMessages) => {
- const isTicketClosed = ticketMessages.some(
- (message) => message.session_id === "close",
- );
- if (isTicketClosed) {
- cleanAuthTicketData();
- addOrUpdateUnauthMessages([getGreetingMessage]);
- if (!userId) {
- cleanUnauthTicketData();
- localStorage.removeItem("unauth-ticket");
- }
- return;
- }
- updateSSEValue(ticketMessages);
- addOrUpdateUnauthMessages(ticketMessages);
- },
- onDisconnect: useCallback(() => {
- setUnauthIsPreventAutoscroll(false);
- setSseEnabled(false);
- }, []),
- marker: "ticket",
- });
-
- useEffect(() => {
- cleanAuthTicketData();
- setSseEnabled(true);
- }, [userId]);
-
- useEffect(() => {
- if (isChatOpened) {
- const newMessages = ticket.messages.filter(
- ({ shown }) => shown?.me !== 1,
- );
-
- newMessages.map(async ({ id }) => {
- await shownMessage(id);
- });
- }
- }, [isChatOpened, ticket.messages]);
-
- const sendMessage = async (messageField: string) => {
- if (!messageField || ticket.isMessageSending) return false;
- setSseEnabled(true);
- let successful = false;
- setIsMessageSending(true);
- if (!ticket.sessionData?.ticketId) {
- const [data, createError] = await createTicket(
- messageField,
- Boolean(userId),
- );
-
- if (createError || !data) {
- successful = false;
-
- enqueueSnackbar(createError);
- } else {
- successful = true;
-
- setTicketData({ ticketId: data.Ticket, sessionId: data.sess });
- }
-
- setIsMessageSending(false);
- } else {
- const [_, sendTicketMessageError] = await sendTicketMessage(
- ticket.sessionData?.ticketId,
- messageField,
- );
- successful = true;
-
- if (sendTicketMessageError) {
- successful = false;
- enqueueSnackbar(sendTicketMessageError);
- }
- setIsMessageSending(false);
- }
-
- return successful;
- };
- const sendFile = async (file: File) => {
- if (file === undefined) return true;
-
- let ticketId = ticket.sessionData?.ticketId;
- if (!ticket.sessionData?.ticketId) {
- const [data, createError] = await createTicket("", Boolean(userId));
- ticketId = data?.Ticket;
-
- if (createError || !data) {
- enqueueSnackbar(createError);
- } else {
- setTicketData({ ticketId: data.Ticket, sessionId: data.sess });
- }
-
- setIsMessageSending(false);
- }
-
- if (ticketId !== undefined) {
- if (file.size > MAX_FILE_SIZE) return setModalWarningType("errorSize");
-
- const [_, sendFileError] = await sf(ticketId, file);
-
- if (sendFileError) {
- enqueueSnackbar(sendFileError);
- }
-
- return true;
- }
- };
-
- return {
- isChatOpened,
- handleChatClickOpen,
- handleChatClickClose,
- handleChatClickSwitch,
- sendMessage,
- sendFile,
- modalWarningType,
- setModalWarningType,
- getGreetingMessage
- };
-};
\ No newline at end of file
diff --git a/src/ui_kit/FloatingSupportChat/utils.ts b/src/ui_kit/FloatingSupportChat/utils.ts
index a603e348..430030ac 100644
--- a/src/ui_kit/FloatingSupportChat/utils.ts
+++ b/src/ui_kit/FloatingSupportChat/utils.ts
@@ -1,55 +1,51 @@
-import { sendTicketMessage } from "@/api/ticket";
import { setTicketData, useTicketStore } from "@/stores/ticket";
import { useUserStore } from "@root/user";
-import { createTicket, sendFile as sendFileRequest } from "@api/ticket";
import { enqueueSnackbar } from "notistack";
+import { createTicket, sendTicketMessage } from "@frontend/kitui";
+import { parseAxiosError } from "@/utils/parse-error";
interface SelectSendingMethod {
- messageField: string;
- isSnackbar?: boolean;
- systemError?: boolean;
-
- }
-export const selectSendingMethod = async ({messageField, isSnackbar = true, systemError = false}: SelectSendingMethod) => {
- console.log("click")
+ messageField: string;
+ isSnackbar?: boolean;
+ systemError?: boolean;
+
+}
+export const selectSendingMethod = async ({ messageField, isSnackbar = true, systemError = false }: SelectSendingMethod) => {
const user = useUserStore.getState().user?._id;
const ticket = useTicketStore.getState()[user ? "authData" : "unauthData"];
- console.log(ticket)
- console.log("click 2")
let successful = false;
- if (!(window.location.hostname == 'localhost' && systemError )) { //предупреждать о системных ошибках вне локалхост
- if (!ticket.sessionData?.ticketId) {
- console.log("autorisated 2")
- const [data, createError] = await createTicket(
- messageField,
- Boolean(user),
- false,
- );
+ if (!(window.location.hostname == 'localhost' && systemError)) { //предупреждать о системных ошибках вне локалхост
+ if (!ticket.sessionData?.ticketId) {
+ const [data, createError] = await createTicket({
+ message: messageField,
+ useToken: Boolean(user),
+ systemError: false,
+ });
- if (createError || !data) {
- successful = false;
-
- if (isSnackbar) enqueueSnackbar(createError);
- } else {
- successful = true;
-
- setTicketData({ ticketId: data.Ticket, sessionId: data.sess });
- }
+ if (createError || !data) {
+ successful = false;
+ if (isSnackbar) enqueueSnackbar(`Не удалось открыть диалог ${parseAxiosError(createError)}`);
} else {
- const [_, sendTicketMessageError] = await sendTicketMessage(
- ticket.sessionData?.ticketId,
- messageField,
- false,
- );
successful = true;
- if (sendTicketMessageError) {
- successful = false;
- if (isSnackbar) enqueueSnackbar(sendTicketMessageError);
- }
+ setTicketData({ ticketId: data.Ticket, sessionId: data.sess });
}
- }
+
+ } else {
+ const [_, sendTicketMessageError] = await sendTicketMessage({
+ ticketId: ticket.sessionData?.ticketId,
+ message: messageField,
+ systemError: false,
+ });
+ successful = true;
+
+ if (sendTicketMessageError) {
+ successful = false;
+ if (isSnackbar) enqueueSnackbar(sendTicketMessageError);
+ }
+ }
+ }
return successful;
}
\ No newline at end of file
diff --git a/src/ui_kit/Modal/CropModal/NavigationPanel.tsx b/src/ui_kit/Modal/CropModal/NavigationPanel.tsx
index 580a8a0a..95872caa 100644
--- a/src/ui_kit/Modal/CropModal/NavigationPanel.tsx
+++ b/src/ui_kit/Modal/CropModal/NavigationPanel.tsx
@@ -29,9 +29,6 @@ export const NavigationPanel: FC = ({
const isMobile = useMediaQuery(theme.breakpoints.down(786));
const lastStep = currentStep + 1 === totalSteps;
- console.log("nextStepName")
- console.log(nextStepName)
-
const handlePrevStep = () => {
if (currentStep === 0) return;
setCurrentStep(currentStep - 1);
diff --git a/src/ui_kit/Modal/CropModal/WorkSpace.tsx b/src/ui_kit/Modal/CropModal/WorkSpace.tsx
index 057d5975..e9f1a483 100644
--- a/src/ui_kit/Modal/CropModal/WorkSpace.tsx
+++ b/src/ui_kit/Modal/CropModal/WorkSpace.tsx
@@ -46,8 +46,6 @@ export default function WorkSpace({
modalModels[currentStepName]
), [currentStepName]);
- // console.log(" промежуточный рендер которому должно быть похуй")
-
return (
<>
{
const [open, setOpen] = useState(false);
const quiz = useCurrentQuiz();
const account = useUserStore()
- console.log(account.userId)
if (account.userId === "6755b1ddd5802e9f13663f56") {
return (
<>
diff --git a/src/utils/generateHubWalletRequest.ts b/src/utils/generateHubWalletRequest.ts
new file mode 100644
index 00000000..b7f6bfdb
--- /dev/null
+++ b/src/utils/generateHubWalletRequest.ts
@@ -0,0 +1,42 @@
+import { isTestServer } from "./hooks/useDomainDefine";
+
+export const generateHubWalletRequestURL = ({
+ wayback,
+ action,
+ dif,
+ userid,
+ additionalinformation,
+ token
+}:{
+ wayback?: string;
+ action: "topupwallet" | "createquizcc" | "buy";
+ dif: string;
+ userid: string;
+ additionalinformation?: string;
+ token: string;
+}) => {
+ let currentDomain = window.location.host;
+ if (currentDomain === "localhost") currentDomain += ":3000";
+
+console.log("Я здесь для отладки и спешу сообщить, что деплой был успешно завершен!")
+
+ // Используем более надежный способ генерации URL
+ const baseUrl = `http://${isTestServer ? "s" : ""}hub.pena.digital/anyservicepayment`;
+ const params = new URLSearchParams({
+ fromdomain: currentDomain,
+ action: action,
+ dif: dif,
+ userid: userid,
+ sec: token
+ });
+
+ if (additionalinformation) params.append('additionalinformation', additionalinformation);
+ if (wayback) params.append('wayback', wayback);
+
+ let url = `${baseUrl}?${params.toString()}`;
+
+ // Для продакшена раскомментировать эту строку:
+ // let url = `https://${isTestServer ? "s" : ""}hub.pena.digital/payment?${params.toString()}`;
+
+ return url;
+}
\ No newline at end of file
diff --git a/src/utils/handleComponentError.ts b/src/utils/handleComponentError.ts
index 94369f24..2687d868 100644
--- a/src/utils/handleComponentError.ts
+++ b/src/utils/handleComponentError.ts
@@ -1,5 +1,8 @@
-import { selectSendingMethod } from "@/ui_kit/FloatingSupportChat/utils";
import { ErrorInfo } from "react";
+import { Ticket, createTicket, getAuthToken, sendTicketMessage } from "..";
+
+let errorsQueue: ComponentError[] = [];
+let timeoutId: ReturnType;
interface ComponentError {
timestamp: number;
@@ -8,40 +11,99 @@ interface ComponentError {
componentStack: string | null | undefined;
}
-export function handleComponentError(error: Error, info: ErrorInfo) {
- const componentError: ComponentError = {
+function isErrorReportingAllowed(error?: Error): boolean {
+ // Если ошибка помечена как debug-override — всегда отправлять
+ if (error && (error as any).__forceSend) return true;
+ // Проверяем домен
+ const currentDomain = window.location.hostname;
+ return currentDomain !== 'localhost';
+}
+
+// Новый API: getTickets — callback, возвращающий актуальные тикеты
+export function handleComponentError(error: Error, info: ErrorInfo, getTickets: () => Ticket[]) {
+ //репортим только о авторизонышах
+ if (!getAuthToken()) return;
+ // Проверяем разрешение на отправку ошибок (по домену)
+ if (!isErrorReportingAllowed(error)) {
+ console.log('❌ Отправка ошибки заблокирована:', error.message);
+ return;
+ }
+ console.log(`✅ Обработка ошибки: ${error.message}`);
+ // Копируем __forceSend если есть
+ const componentError: ComponentError & { __forceSend?: boolean } = {
timestamp: Math.floor(Date.now() / 1000),
message: error.message,
callStack: error.stack,
componentStack: info.componentStack,
+ ...(error && (error as any).__forceSend ? { __forceSend: true } : {})
};
-
- queueErrorRequest(componentError);
+ queueErrorRequest(componentError, getTickets);
}
-let errorsQueue: ComponentError[] = [];
-let timeoutId: ReturnType;
-
-function queueErrorRequest(error: ComponentError) {
+// Ставит ошибку в очередь для отправки, через 1 секунду вызывает sendErrorsToServer
+export function queueErrorRequest(error: ComponentError, getTickets: () => Ticket[]) {
errorsQueue.push(error);
-
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
- sendErrorsToServer();
+ sendErrorsToServer(getTickets);
}, 1000);
}
-async function sendErrorsToServer() {
- // makeRequest({
- // url: "",
- // method: "POST",
- // body: errorsQueue,
- // useToken: true,
- // });
- // selectSendingMethod({
- // messageField: `Fake-sending ${errorsQueue.length} errors to server ${JSON.stringify(errorsQueue)}`,
- // isSnackbar: false,
- // systemError: true
- // });
-// errorsQueue = [];
+// Отправляет накопленные ошибки в тикеты, ищет существующий тикет с system: true или создает новый
+export async function sendErrorsToServer(getTickets: () => Ticket[]) {
+ if (errorsQueue.length === 0) return;
+ // Проверяем разрешение на отправку ошибок (по домену и debug-override)
+ // Если хотя бы одна ошибка в очереди с __forceSend, отправляем всё
+ const forceSend = errorsQueue.some(e => (e as any).__forceSend);
+ if (!forceSend && !isErrorReportingAllowed()) {
+ console.log('❌ Отправка ошибок заблокирована, очищаем очередь');
+ errorsQueue = [];
+ return;
+ }
+ const tickets = getTickets();
+ try {
+ // Формируем сообщение об ошибке
+ const errorMessage = errorsQueue.map(error => {
+ return `[${new Date(error.timestamp * 1000).toISOString()}] ${error.message}\n\nCall Stack:\n${error.callStack || 'N/A'}\n\nComponent Stack:\n${error.componentStack || 'N/A'}`;
+ }).join('\n\n---\n\n');
+ // ВСЕГДА ищем тикет через API
+ const existingSystemTicket = await findSystemTicket(tickets);
+ if (existingSystemTicket) {
+ sendTicketMessage({
+ ticketId: existingSystemTicket,
+ message: errorMessage,
+ systemError: true,
+ });
+ } else {
+ // Создаем новый тикет для ошибки
+ createTicket({
+ message: errorMessage,
+ useToken: true,
+ systemError: true,
+ });
+ }
+ } catch (error) {
+ console.error('Error in sendErrorsToServer:', error);
+ } finally {
+ // Очищаем очередь ошибок
+ errorsQueue = [];
+ }
}
+
+// Ищет существующий тикет с system: true
+export async function findSystemTicket(tickets: Ticket[]) {
+ for (const ticket of tickets) {
+ console.log("[findSystemTicket] Проверяем тикет:", ticket);
+ if (!('messages' in ticket)) {
+ if (ticket.top_message && ticket.top_message.system === true) {
+ console.log("[findSystemTicket] Найден тикет по top_message.system:true:", ticket.id);
+ return ticket.id;
+ }
+ }
+ }
+}
+
+export function clearErrorHandlingConfig () {
+ clearTimeout(timeoutId);
+ errorsQueue = [];
+}
\ No newline at end of file
diff --git a/src/utils/hooks/useAuthRedirect.ts b/src/utils/hooks/useAuthRedirect.ts
new file mode 100644
index 00000000..7f9ff030
--- /dev/null
+++ b/src/utils/hooks/useAuthRedirect.ts
@@ -0,0 +1,80 @@
+import { useEffect, useState } from "react";
+import { useNavigate, useSearchParams } from "react-router-dom";
+import {
+ clearAuthToken,
+ getMessageFromFetchError,
+ setAuthToken,
+ getAuthToken,
+} from "@frontend/kitui";
+import {
+ clearUserData,
+ setUser,
+ setUserAccount,
+ setUserId,
+ useUserStore,
+} from "@root/stores/user";
+import { logout } from "@root/api/auth";
+import { clearCustomTariffs } from "@root/stores/customTariffs";
+import { clearTickets } from "@root/stores/tickets";
+import { setNotEnoughMoneyAmount } from "@stores/cart";
+
+export const useAuthRedirect = () => {
+ const navigate = useNavigate();
+ const [searchParams] = useSearchParams();
+ const [isProcessing, setIsProcessing] = useState(false);
+ const user = useUserStore((state) => state.user);
+
+ const action = searchParams.get("action");
+ const dif = searchParams.get("dif");
+ const token = searchParams.get("data");
+ const userId = searchParams.get("userid");
+ const wayback = searchParams.get("wayback");
+
+ useEffect(() => {
+ if (isProcessing) return;
+
+ // Если пользователь уже авторизован и это тот же пользователь
+ if (user?._id === userId) {
+ let returnUrl = `/payment?action=${action}&dif=${dif}&user=${userId}`;
+ if (wayback) returnUrl += `&wayback=${wayback}`;
+ navigate(returnUrl, { replace: true });
+ return;
+ }
+
+ // Если есть все необходимые параметры для авторизации
+ if (action && dif && token && userId) {
+ setIsProcessing(true);
+
+ (async () => {
+ try {
+ // Очищаем старые данные если есть токен
+ if (getAuthToken()) {
+ clearAuthToken();
+ clearUserData();
+ clearCustomTariffs();
+ clearTickets();
+ setNotEnoughMoneyAmount(0);
+ await logout();
+ }
+
+ // Устанавливаем новый токен и ID пользователя
+ setAuthToken(token);
+ setUserId(userId);
+
+ // Перенаправляем на страницу оплаты
+ let returnUrl = `/payment?action=${action}&dif=${dif}&user=${userId}`;
+ if (wayback) returnUrl += `&wayback=${wayback}`;
+ navigate(returnUrl, { replace: true });
+ } catch (error) {
+ console.error("Auth redirect error:", error);
+ // В случае ошибки перенаправляем на главную страницу тарифов
+ navigate("/tariffs", { replace: true });
+ } finally {
+ setIsProcessing(false);
+ }
+ })();
+ }
+ }, [user, action, dif, token, userId, wayback, navigate, isProcessing]);
+
+ return { isProcessing };
+};
\ No newline at end of file
diff --git a/src/utils/hooks/useAutoPay.ts b/src/utils/hooks/useAutoPay.ts
index cc46fde5..a78d88d8 100644
--- a/src/utils/hooks/useAutoPay.ts
+++ b/src/utils/hooks/useAutoPay.ts
@@ -1,44 +1,42 @@
import { cartApi } from "@api/cart";
import { useUserStore } from "@/stores/user";
-import moment from "moment";
import { enqueueSnackbar } from "notistack";
import { useEffect } from "react";
-import { redirect, useNavigate, useSearchParams } from "react-router-dom";
+import { useNavigate, useSearchParams } from "react-router-dom";
import { calcTimeOfReadyPayCart, cancelPayCartProcess, startPayCartProcess, useNotEnoughMoneyAmount } from "@/stores/notEnoughMoneyAmount";
import { startCC } from "@/stores/cc";
import { setEditQuizId, setCurrentStep } from "@root/quizes/actions";
+/*
+Есть три пути по которому мы ходили из квиза в хаб. Нам нехватило денег при:
+1)Покупке обычного тарифа
+2)Покупке тарифа-заказ-квиза
+3)Покупке тарифа в настройке квиза в вкладке ИИ
+*/
+
export const useAfterPay = () => {
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
+
const userId = useUserStore(store => store.userId)
const userAccount = useUserStore(state => state.userAccount);
+ const userWithWallet = useUserStore((state) => state.customerAccount); //c wallet
const siteReadyPayCart = useNotEnoughMoneyAmount(state => state.siteReadyPayCart);
- const purpose = searchParams.get("purpose");
- const paymentUserId = searchParams.get("userid");
- const currentCC = searchParams.get("cc");
- const wayback = searchParams.get("wayback");
-
- // Обработка wayback параметра
- useEffect(() => {
- if (wayback) {
- const quizId = wayback.split("_")[1];
- if (quizId) {
- setEditQuizId(Number(quizId));
- setCurrentStep(17); // Шаг для персонализации AI
- navigate("/personalization-ai");
- }
- }
- }, [wayback, navigate]);
+ let URLaction = searchParams.get("action");//что мы, собсна, хотим: оплатить, пополнить, заказать квиз
+ let URLuserId = searchParams.get("userid");//тот кто начал всё это действо
+ let URLadditionalinformation = searchParams.get("additionalinformation");//его токен
useEffect(() => {
- //Звёзды сошлись, будем оплачивать корзину
- if (paymentUserId && paymentUserId === userId) {
+
+ setSearchParams({}, { replace: true });
- if (purpose === "paycart") {
- setSearchParams({}, { replace: true });
- if (currentCC) startCC()
+ if (userId && URLuserId && userId === URLuserId) {
+
+ if (URLaction === "buy") startPayCartProcess(URLuserId);
+
+ if (URLaction === "createquizcc") {
+ startCC();
(async () => {
//Проверяем можем ли мы оплатить корзину здесь и сейчас
@@ -46,19 +44,46 @@ export const useAfterPay = () => {
if (payCartError) {
//Не получилось купить корзину. Ставим флаг, что сайт в состоянии ожидания пополнения счёта для оплаты
- startPayCartProcess(paymentUserId)
+ startPayCartProcess(URLuserId);
} else {
- if (currentCC) navigate("/tariffs")
- cancelPayCartProcess()
+ navigate("/tariffs");
+ cancelPayCartProcess();
}
})()
}
- }
- }, [purpose, paymentUserId])
+
+ //Покупка ИИ тарифа из настройки квиза ИИ вкладки
+ if (URLaction === "buy" && URLadditionalinformation) {
+ const quizId = Number(URLadditionalinformation);
+
+ if (quizId) {
+ setEditQuizId(Number(quizId)); //Выбираем квиз
+ setCurrentStep(17); // Шаг для персонализации AI
+
+ // Проверяем wayback параметр для определения куда переходить
+ const wayback = searchParams.get("wayback");
+ if (wayback === "edit") {
+ // Сначала переходим на /edit, затем на /personalization-ai
+ navigate("/edit");
+ // Используем setTimeout чтобы дать время для загрузки /edit
+ setTimeout(() => {
+ navigate("/personalization-ai");
+ }, 100);
+ } else {
+ // Прямой переход на /personalization-ai
+ navigate("/personalization-ai");
+ }
+ }
+ }
+
+ }
+ }, []);
+
+ //Обработка необходимости купить после пополнения
useEffect(() => {
if (userId !== null && siteReadyPayCart !== null && siteReadyPayCart[userId] !== undefined) {
- const deadline = siteReadyPayCart[userId]
+ const deadline = siteReadyPayCart[userId];
if (calcTimeOfReadyPayCart(deadline)) {
//Время ещё не вышло. У нас стоит флаг покупать корзину если время не вышло.
@@ -66,11 +91,13 @@ export const useAfterPay = () => {
const [, payCartError] = await cartApi.pay();
if (!payCartError) {
- enqueueSnackbar("Товары успешно приобретены")
- cancelPayCartProcess()
+ enqueueSnackbar("Товары успешно приобретены");
+ cancelPayCartProcess();
}
})()
}
}
- }, [userAccount, userId, siteReadyPayCart])
+ }, [userAccount, userId, siteReadyPayCart, userWithWallet])
+
+
}
\ No newline at end of file
diff --git a/src/utils/hooks/usePipeSubscriber.ts b/src/utils/hooks/usePipeSubscriber.ts
index 3c450e6b..b6b58d63 100644
--- a/src/utils/hooks/usePipeSubscriber.ts
+++ b/src/utils/hooks/usePipeSubscriber.ts
@@ -4,7 +4,7 @@ import { useSSETab } from "./useSSETab";
import { cancelPayCartProcess } from "@/stores/notEnoughMoneyAmount";
import { setCash } from "@/stores/cash";
import { currencyFormatter } from "@/pages/Tariffs/tariffsUtils/currencyFormatter";
-import { inCart } from "@/pages/Tariffs/Tariffs";
+import { inCart } from "@/pages/Tariffs/utils";
type Ping = [{ event: "ping" }]
@@ -43,8 +43,6 @@ export const usePipeSubscriber = () => {
`/customer/v1.0.1/account/pipe?Authorization=${token}`,
onNewData: (data) => {
let message = data[0] as PipeMessage
- console.log("truba")
- console.log(message)
updateSSEValue(message)
//Пропускаем пингование
diff --git a/src/utils/hooks/useUserAccountFetcher.ts b/src/utils/hooks/useUserAccountFetcher.ts
index f057114f..948c5aa4 100644
--- a/src/utils/hooks/useUserAccountFetcher.ts
+++ b/src/utils/hooks/useUserAccountFetcher.ts
@@ -1,7 +1,7 @@
import { useEffect, useLayoutEffect, useRef } from "react";
import { createUserAccount, devlog } from "@frontend/kitui";
import { isAxiosError } from "axios";
-import { makeRequest } from "@api/makeRequest";
+import { makeRequest } from "@frontend/kitui";
import type { UserAccount } from "@frontend/kitui";
import { setUserAccount } from "@/stores/user";
@@ -37,27 +37,52 @@ export const useUserAccountFetcher = ({
})
.then((result) => {
devlog("User account", result);
- console.log(result)
if (result) onNewUserAccountRef.current(result);
})
.catch((error) => {
devlog("Error fetching user account", error);
if (error.response?.status === 409) return;
if (isAxiosError(error) && error.response?.status === 404) {
- createUserAccount(controller.signal, url.replace("get", "create"))
+
+ // Формируем правильный URL для создания аккаунта
+ let createUrl = url;
+ if (url.includes("/customer/v1.0.1/account")) {
+ // Для customerAccount используем тот же URL (POST запрос)
+ createUrl = url;
+ } else if (url.includes("/squiz/account/get")) {
+ // Для userAccount заменяем get на create
+ createUrl = url.replace("get", "create");
+ }
+
+
+ createUserAccount(controller.signal, createUrl)
.then((result) => {
devlog("Created user account", result);
- console.log("это пойдёт в стор: ")
- console.log(result)
- if (result) onNewUserAccountRef.current(result.created_account as T);
+
+ // Проверяем структуру ответа и записываем в стор
+ if (result) {
+ // Если результат содержит created_account, используем его
+ if (result.created_account) {
+ onNewUserAccountRef.current(result.created_account as T);
+ }
+ // Если результат сам является аккаунтом (для customerAccount)
+ else if (result.userId && result.wallet) {
+ onNewUserAccountRef.current(result as T);
+ }
+ // Если ничего не подходит, логируем для диагностики
+ else {
+ onNewUserAccountRef.current(result as T);
+ }
+ }
})
.catch((error) => {
if (error.response?.status === 409) return;
devlog("Error creating user account", error);
+ console.error("useUserAccountFetcher: Error creating account:", error);
onErrorRef.current?.(error);
});
} else {
- console.log(error)
+ console.error(error)
onErrorRef.current?.(error);
}
});
diff --git a/src/utils/parse-error.ts b/src/utils/parse-error.ts
index 9d689731..fd2bb26a 100644
--- a/src/utils/parse-error.ts
+++ b/src/utils/parse-error.ts
@@ -29,7 +29,7 @@ export const parseAxiosError = (nativeError: unknown): [string, number?] => {
if (error.message === "Failed to fetch") return ["Ошибка сети"];
//ДЛЯ ОПЛАТЫ ТАРИФА
- if(error.response.status === 402) {
+ if(error.response?.status === 402) {
console.error(error.response?.data.message)
return error.response?.data.message
}
@@ -40,8 +40,8 @@ export const parseAxiosError = (nativeError: unknown): [string, number?] => {
const status = error.response.status;
if(status === 409 || status === 401 || status === 404) {
- const serverErrorMessage = error.response.data.message
- console.log(serverErrorMessage)
+ const responseData = error.response.data as any;
+ const serverErrorMessage = responseData?.message || responseData?.error;
const translatedMessage = translateMessage[serverErrorMessage?.toLowerCase() || ""]
return [translatedMessage || "", serverError.statusCode];
}