Merge branch 'staging'
This commit is contained in:
commit
8a1067bfd5
@ -2,9 +2,8 @@ name: Deploy
|
|||||||
run-name: ${{ gitea.actor }} build image and push to container registry
|
run-name: ${{ gitea.actor }} build image and push to container registry
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
registry_package:
|
||||||
branches:
|
types: [published]
|
||||||
- 'main'
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
CreateImage:
|
CreateImage:
|
||||||
|
@ -2,25 +2,30 @@ name: Deploy
|
|||||||
run-name: ${{ gitea.actor }} build image and push to container registry
|
run-name: ${{ gitea.actor }} build image and push to container registry
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
registry_package:
|
||||||
branches:
|
types: [published]
|
||||||
- 'staging'
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
CreateImage:
|
# CreateImage:
|
||||||
runs-on: [skeris]
|
# runs-on: [skeris]
|
||||||
uses: http://gitea.pena/PenaDevops/actions.git/.gitea/workflows/build-image.yml@v1.1.6-p
|
# uses: http://gitea.pena/PenaDevops/actions.git/.gitea/workflows/build-image.yml@v1.1.6-p
|
||||||
with:
|
# with:
|
||||||
runner: skeris
|
# runner: skeris
|
||||||
secrets:
|
# secrets:
|
||||||
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
# REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
||||||
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
# REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||||
DeployService:
|
DeployService:
|
||||||
|
if: contains(github.event.package.name, 'staging')
|
||||||
runs-on: [frontstaging]
|
runs-on: [frontstaging]
|
||||||
needs: CreateImage
|
container:
|
||||||
uses: http://gitea.pena/PenaDevops/actions.git/.gitea/workflows/deploy.yml@v1.1.4-p7
|
image: gitea.pena:3000/penadevops/container-images/node-compose:main
|
||||||
with:
|
env:
|
||||||
runner: frontstaging
|
GITHUB_RUN_NUMBER: "${{ inputs.actionid }}"
|
||||||
actionid: ${{ gitea.run_id }}
|
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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
|
13
Dockerfile
13
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
|
@ -2,6 +2,7 @@ services:
|
|||||||
squiz:
|
squiz:
|
||||||
container_name: squiz
|
container_name: squiz
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
image: gitea.pena/squiz/frontpanel/staging:$GITHUB_RUN_NUMBER
|
image: gitea.pena/squiz/frontpanel/staging:latest
|
||||||
hostname: squiz
|
hostname: squiz
|
||||||
tty: true
|
tty: true
|
||||||
|
pull_policy: always
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
"@craco/craco": "^7.0.0",
|
"@craco/craco": "^7.0.0",
|
||||||
"@emotion/react": "^11.10.5",
|
"@emotion/react": "^11.10.5",
|
||||||
"@emotion/styled": "^11.10.5",
|
"@emotion/styled": "^11.10.5",
|
||||||
"@frontend/kitui": "^1.0.108",
|
"@frontend/kitui": "1.0.110",
|
||||||
"@frontend/squzanswerer": "^1.0.57",
|
"@frontend/squzanswerer": "^1.0.57",
|
||||||
"@mui/icons-material": "^5.10.14",
|
"@mui/icons-material": "^5.10.14",
|
||||||
"@mui/material": "^5.10.14",
|
"@mui/material": "^5.10.14",
|
||||||
@ -69,6 +69,7 @@
|
|||||||
"test": "craco test",
|
"test": "craco test",
|
||||||
"eject": "craco eject",
|
"eject": "craco eject",
|
||||||
"code:format": "prettier --write --ignore-unknown",
|
"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",
|
"prepare": "husky install",
|
||||||
"cypress:open": "cypress open",
|
"cypress:open": "cypress open",
|
||||||
"cypress:run": "cypress run"
|
"cypress:run": "cypress run"
|
||||||
|
@ -165,7 +165,7 @@
|
|||||||
console.log(params.get("debug"))
|
console.log(params.get("debug"))
|
||||||
if (params.get("debug")) {
|
if (params.get("debug")) {
|
||||||
console.log(
|
console.log(
|
||||||
"mhgfhdhfjhffhfhjfghjgf"
|
"params.get(debug) is true"
|
||||||
)
|
)
|
||||||
let scriptTag = document.createElement('script');
|
let scriptTag = document.createElement('script');
|
||||||
scriptTag.setAttribute('src', "https://markknol.github.io/console-log-viewer/console-log-viewer.js");
|
scriptTag.setAttribute('src', "https://markknol.github.io/console-log-viewer/console-log-viewer.js");
|
||||||
|
68
src/App.tsx
68
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 type { OriginalUserAccount } from "@root/user";
|
||||||
import { clearUserData, setCustomerAccount, setUser, setUserAccount, useUserStore } from "@root/user";
|
import { clearUserData, setCustomerAccount, setUser, setUserAccount, useUserStore } from "@root/user";
|
||||||
import ContactFormModal from "@ui_kit/ContactForm";
|
import ContactFormModal from "@ui_kit/ContactForm";
|
||||||
@ -8,7 +8,7 @@ import { useAfterPay } from "@utils/hooks/useAutoPay";
|
|||||||
import { useUserAccountFetcher } from "@utils/hooks/useUserAccountFetcher";
|
import { useUserAccountFetcher } from "@utils/hooks/useUserAccountFetcher";
|
||||||
import { enqueueSnackbar } from "notistack";
|
import { enqueueSnackbar } from "notistack";
|
||||||
import type { SuspenseProps } from "react";
|
import type { SuspenseProps } from "react";
|
||||||
import { lazy, Suspense } from "react";
|
import { lazy, Suspense, useEffect } from "react";
|
||||||
import { lazily } from "react-lazily";
|
import { lazily } from "react-lazily";
|
||||||
import { Navigate, Route, Routes, useLocation, useNavigate } from "react-router-dom";
|
import { Navigate, Route, Routes, useLocation, useNavigate } from "react-router-dom";
|
||||||
import { useAmoAccount } from "./api/integration";
|
import { useAmoAccount } from "./api/integration";
|
||||||
@ -23,7 +23,11 @@ import { InfoPrivilege } from "./pages/InfoPrivilege";
|
|||||||
import AmoTokenExpiredDialog from "./pages/IntegrationsPage/IntegrationsModal/Amo/AmoTokenExpiredDialog";
|
import AmoTokenExpiredDialog from "./pages/IntegrationsPage/IntegrationsModal/Amo/AmoTokenExpiredDialog";
|
||||||
import Landing from "./pages/Landing/Landing";
|
import Landing from "./pages/Landing/Landing";
|
||||||
import Main from "./pages/main";
|
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 { ErrorBoundary } from "react-error-boundary";
|
||||||
|
import { handleLogoutClick } from "./utils/HandleLogoutClick";
|
||||||
|
|
||||||
const MyQuizzesFull = lazy(() => import("./pages/createQuize/MyQuizzesFull"));
|
const MyQuizzesFull = lazy(() => import("./pages/createQuize/MyQuizzesFull"));
|
||||||
const QuizGallery = lazy(() => import("./pages/createQuize/QuizGallery"));
|
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);
|
let params = new URLSearchParams(document.location.search);
|
||||||
const isTest = Boolean(params.get("test"))
|
const isTest = Boolean(params.get("test"))
|
||||||
|
|
||||||
|
createMakeRequestConfig(
|
||||||
|
handleLogoutClick,
|
||||||
|
(error, info, getTickets) => handleComponentError(error, info, getTickets()),
|
||||||
|
() => useTicketStore.getState().tickets
|
||||||
|
);
|
||||||
|
|
||||||
const routeslink = [
|
const routeslink = [
|
||||||
{
|
{
|
||||||
path: "/edit",
|
path: "/edit",
|
||||||
@ -73,12 +83,16 @@ const LazyLoading = ({ children, fallback }: SuspenseProps) => (
|
|||||||
<Suspense fallback={fallback ?? <></>}>{children}</Suspense>
|
<Suspense fallback={fallback ?? <></>}>{children}</Suspense>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const ApologyPage = () => <div><p>Что-то пошло не так</p></div>
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
window.LoadingObserver = false;
|
window.LoadingObserver = false;
|
||||||
const userId = useUserStore((state) => state.userId);
|
const userId = useUserStore((state) => state.userId);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { data: amoAccount } = useAmoAccount();
|
const { data: amoAccount } = useAmoAccount();
|
||||||
|
const tickets = useTicketStore(store => store.tickets);
|
||||||
|
|
||||||
|
|
||||||
useUserFetcher({
|
useUserFetcher({
|
||||||
url: `${process.env.REACT_APP_DOMAIN}/user/${userId}`,
|
url: `${process.env.REACT_APP_DOMAIN}/user/${userId}`,
|
||||||
@ -97,8 +111,11 @@ export default function App() {
|
|||||||
useUserAccountFetcher<UserAccount>({
|
useUserAccountFetcher<UserAccount>({
|
||||||
url: `${process.env.REACT_APP_DOMAIN}/customer/v1.0.1/account`,
|
url: `${process.env.REACT_APP_DOMAIN}/customer/v1.0.1/account`,
|
||||||
userId,
|
userId,
|
||||||
onNewUserAccount: setCustomerAccount,
|
onNewUserAccount: (account) => {
|
||||||
|
setCustomerAccount(account);
|
||||||
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
|
console.error("App: Error in customerAccount fetcher:", error);
|
||||||
const errorMessage = getMessageFromFetchError(error);
|
const errorMessage = getMessageFromFetchError(error);
|
||||||
if (errorMessage) {
|
if (errorMessage) {
|
||||||
enqueueSnackbar(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();
|
useAfterPay();
|
||||||
|
|
||||||
if (location.state?.redirectTo)
|
if (location.state?.redirectTo)
|
||||||
@ -136,7 +184,10 @@ export default function App() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<ErrorBoundary
|
||||||
|
FallbackComponent={ApologyPage}
|
||||||
|
onError={(error, info) => handleComponentError(error, info, () => useTicketStore.getState().tickets)}
|
||||||
|
>
|
||||||
{amoAccount && <AmoTokenExpiredDialog isAmoTokenExpired={amoAccount.stale} />}
|
{amoAccount && <AmoTokenExpiredDialog isAmoTokenExpired={amoAccount.stale} />}
|
||||||
|
|
||||||
<ContactFormModal />
|
<ContactFormModal />
|
||||||
@ -259,6 +310,10 @@ export default function App() {
|
|||||||
path={"/image/:srcImage"}
|
path={"/image/:srcImage"}
|
||||||
element={<ChatImageNewWindow />}
|
element={<ChatImageNewWindow />}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path={"/debug"}
|
||||||
|
element={<div></div>}
|
||||||
|
/>
|
||||||
<Route element={<PrivateRoute />}>
|
<Route element={<PrivateRoute />}>
|
||||||
{routeslink.map((e, i) => (
|
{routeslink.map((e, i) => (
|
||||||
<Route
|
<Route
|
||||||
@ -280,6 +335,9 @@ export default function App() {
|
|||||||
))}
|
))}
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
</>
|
|
||||||
|
{/* Компонент отладки ошибок - доступен по Ctrl+Shift+D */}
|
||||||
|
<Debug />
|
||||||
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { makeRequest } from "@api/makeRequest";
|
import { makeRequest } from "@frontend/kitui";
|
||||||
|
|
||||||
import { parseAxiosError } from "@utils/parse-error";
|
import { parseAxiosError } from "@utils/parse-error";
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { makeRequest } from "@api/makeRequest";
|
import { makeRequest } from "@frontend/kitui";
|
||||||
|
|
||||||
import { parseAxiosError } from "@utils/parse-error";
|
import { parseAxiosError } from "@utils/parse-error";
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { makeRequest } from "@api/makeRequest";
|
import { makeRequest } from "@frontend/kitui";
|
||||||
|
|
||||||
import { parseAxiosError } from "@utils/parse-error";
|
import { parseAxiosError } from "@utils/parse-error";
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { QuestionKeys } from "@/pages/IntegrationsPage/IntegrationsModal/Amo/types";
|
import { QuestionKeys } from "@/pages/IntegrationsPage/IntegrationsModal/Amo/types";
|
||||||
import { makeRequest } from "@api/makeRequest";
|
import { makeRequest } from "@frontend/kitui";
|
||||||
import { useToken } from "@frontend/kitui";
|
import { useToken } from "@frontend/kitui";
|
||||||
import { parseAxiosError } from "@utils/parse-error";
|
import { parseAxiosError } from "@utils/parse-error";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
@ -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 <TRequest = unknown, TResponse = unknown>(
|
|
||||||
data: MakeRequest,
|
|
||||||
): Promise<TResponse> => {
|
|
||||||
try {
|
|
||||||
const response = await KIT.makeRequest<unknown, TResponse>(data);
|
|
||||||
|
|
||||||
return response;
|
|
||||||
} catch (nativeError) {
|
|
||||||
const error = nativeError as AxiosError;
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,4 +1,4 @@
|
|||||||
import { makeRequest } from "@api/makeRequest";
|
import { makeRequest } from "@frontend/kitui";
|
||||||
|
|
||||||
import { parseAxiosError } from "@utils/parse-error";
|
import { parseAxiosError } from "@utils/parse-error";
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { makeRequest } from "@api/makeRequest";
|
import { makeRequest } from "@frontend/kitui";
|
||||||
|
|
||||||
import { replaceSpacesToEmptyLines } from "@utils/replaceSpacesToEmptyLines";
|
import { replaceSpacesToEmptyLines } from "@utils/replaceSpacesToEmptyLines";
|
||||||
import { parseAxiosError } from "@utils/parse-error";
|
import { parseAxiosError } from "@utils/parse-error";
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { makeRequest } from "@api/makeRequest";
|
import { makeRequest } from "@frontend/kitui";
|
||||||
import { defaultQuizConfig } from "@model/quizSettings";
|
import { defaultQuizConfig } from "@model/quizSettings";
|
||||||
|
|
||||||
import { parseAxiosError } from "@utils/parse-error";
|
import { parseAxiosError } from "@utils/parse-error";
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { makeRequest } from "@api/makeRequest";
|
import { makeRequest } from "@frontend/kitui";
|
||||||
|
|
||||||
import { parseAxiosError } from "@utils/parse-error";
|
import { parseAxiosError } from "@utils/parse-error";
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { makeRequest } from "@api/makeRequest";
|
import { makeRequest } from "@frontend/kitui";
|
||||||
|
|
||||||
import { parseAxiosError } from "@utils/parse-error";
|
import { parseAxiosError } from "@utils/parse-error";
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { makeRequest } from "@api/makeRequest";
|
import { makeRequest } from "@frontend/kitui";
|
||||||
import { parseAxiosError } from "@utils/parse-error";
|
import { parseAxiosError } from "@utils/parse-error";
|
||||||
import type { GetTariffsResponse } from "@frontend/kitui";
|
import type { GetTariffsResponse } from "@frontend/kitui";
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { createTicket as createTicketRequest } from "@frontend/kitui";
|
import { createTicket as createTicketRequest } from "@frontend/kitui";
|
||||||
|
|
||||||
import { makeRequest } from "@api/makeRequest";
|
import { makeRequest } from "@frontend/kitui";
|
||||||
|
|
||||||
import { parseAxiosError } from "@utils/parse-error";
|
import { parseAxiosError } from "@utils/parse-error";
|
||||||
|
|
||||||
@ -15,47 +15,7 @@ type SendFileResponse = {
|
|||||||
|
|
||||||
const API_URL = `${process.env.REACT_APP_DOMAIN}/heruvym/v1.0.0`;
|
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 (
|
export const sendFile = async (
|
||||||
ticketId: string,
|
ticketId: string,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { makeRequest } from "@api/makeRequest";
|
import { makeRequest } from "@frontend/kitui";
|
||||||
|
|
||||||
import { parseAxiosError } from "@utils/parse-error";
|
import { parseAxiosError } from "@utils/parse-error";
|
||||||
|
|
||||||
|
15
src/assets/icons/logo/Postback.tsx
Normal file
15
src/assets/icons/logo/Postback.tsx
Normal file
File diff suppressed because one or more lines are too long
19
src/assets/icons/logo/PostbackPC.tsx
Normal file
19
src/assets/icons/logo/PostbackPC.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { Box, SxProps } from "@mui/material";
|
||||||
|
|
||||||
|
export default (sx: SxProps) => (
|
||||||
|
<Box
|
||||||
|
component="svg"
|
||||||
|
width="40px"
|
||||||
|
height="34px"
|
||||||
|
viewBox="0 0 40 34"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
sx={{
|
||||||
|
mr: "15px"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<path d="M37 0H3C1.34315 0 0 1.34314 0 3V20.967H40V3C40 1.34315 38.6569 0 37 0Z" fill="#9A9AAF" fillOpacity="0.2" />
|
||||||
|
<path d="M0 24.1244V21.967H40V24.1244C40 25.7813 38.6569 27.1244 37 27.1244H25.5472L26.8066 33.5412H13.2534L14.3928 27.1244H3C1.34315 27.1244 0 25.7813 0 24.1244Z" fill="#9A9AAF" fillOpacity="0.2" />
|
||||||
|
<path d="M12.9803 9.56785L16.8969 12.7545C17.2236 13.0203 17.7125 12.7878 17.7125 12.3666V10.6689C19.4084 10.3847 22.8834 10.4557 23.2157 13.0131C23.631 16.2098 20.9105 17.2115 20.1629 17.318C19.4153 17.4246 19.7476 18 19.9137 18C22.115 18 25.5 16.2098 25.5 12.8213C25.3505 7.77475 20.246 7.19508 17.7125 7.53607V6.04142C17.7125 5.62197 17.2271 5.38894 16.8998 5.65124L12.9832 8.78983C12.7346 8.98903 12.7332 9.3668 12.9803 9.56785Z" fill="#7E2AEA" fillOpacity="0.5" />
|
||||||
|
</Box>
|
||||||
|
);
|
BIN
src/assets/icons/logo/zapier.png
Normal file
BIN
src/assets/icons/logo/zapier.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.4 KiB |
@ -18,7 +18,7 @@ import CloseIcon from "@icons/CloseBold";
|
|||||||
import type { SnackbarKey } from "notistack";
|
import type { SnackbarKey } from "notistack";
|
||||||
import { CheckFastlink } from "@ui_kit/CheckFastlink";
|
import { CheckFastlink } from "@ui_kit/CheckFastlink";
|
||||||
import { ErrorBoundary } from "react-error-boundary";
|
import { ErrorBoundary } from "react-error-boundary";
|
||||||
import { handleComponentError } from "./utils/handleComponentError";
|
import { handleComponentError } from "@frontend/kitui";
|
||||||
|
|
||||||
moment.locale("ru");
|
moment.locale("ru");
|
||||||
polyfillCountryFlagEmojis();
|
polyfillCountryFlagEmojis();
|
||||||
@ -38,7 +38,6 @@ const snackbarAction = (snackbarId: SnackbarKey) => (
|
|||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|
||||||
const ApologyPage = () => <div><p>Что-то пошло не так</p></div>
|
|
||||||
|
|
||||||
const root = createRoot(document.getElementById("root")!);
|
const root = createRoot(document.getElementById("root")!);
|
||||||
|
|
||||||
@ -65,12 +64,7 @@ root.render(
|
|||||||
>
|
>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
|
|
||||||
<ErrorBoundary
|
|
||||||
FallbackComponent={ApologyPage}
|
|
||||||
onError={handleComponentError}
|
|
||||||
>
|
|
||||||
<App />
|
<App />
|
||||||
</ErrorBoundary>
|
|
||||||
<CheckFastlink />
|
<CheckFastlink />
|
||||||
</SnackbarProvider>
|
</SnackbarProvider>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
@ -69,6 +69,8 @@ export type QuizTheme =
|
|||||||
export enum QuizMetricType {
|
export enum QuizMetricType {
|
||||||
yandex = "yandexMetricsNumber",
|
yandex = "yandexMetricsNumber",
|
||||||
vk = "vkMetricsNumber",
|
vk = "vkMetricsNumber",
|
||||||
|
zapier = "zapierIntegration",
|
||||||
|
postback = "postbackIntegration",
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FormContactFieldName = "name" | "email" | "phone" | "text" | "address";
|
export type FormContactFieldName = "name" | "email" | "phone" | "text" | "address";
|
||||||
|
@ -42,16 +42,9 @@ const GeneralItem = ({
|
|||||||
([nextValue], [currentValue]) => Number(nextValue) - Number(currentValue),
|
([nextValue], [currentValue]) => Number(nextValue) - Number(currentValue),
|
||||||
);
|
);
|
||||||
const days = data.map(([value]) => value);
|
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
|
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
|
||||||
? conversionValue
|
? conversionValue
|
||||||
: Object.values(general).reduce((total, item) => total + item, 0);
|
: Object.values(general).reduce((total, item) => total + item, 0);
|
||||||
@ -82,17 +75,17 @@ const GeneralItem = ({
|
|||||||
<LineChart
|
<LineChart
|
||||||
xAxis={[
|
xAxis={[
|
||||||
{
|
{
|
||||||
data: statiscticsResult ? days : Object.keys(general),
|
data: Object.keys(general),
|
||||||
valueFormatter: (value) => {
|
valueFormatter: (value) => {
|
||||||
const timestamp = Number(value);
|
const timestamp = Number(value);
|
||||||
if (isNaN(timestamp)) return 'Invalid Date';
|
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={[
|
series={[
|
||||||
{
|
{
|
||||||
data: Object.values(statiscticsResult ? time : general),
|
data: Object.values(general),
|
||||||
valueFormatter: (value) =>
|
valueFormatter: (value) =>
|
||||||
calculateTime
|
calculateTime
|
||||||
? getCalculatedTime(value)
|
? getCalculatedTime(value)
|
||||||
@ -131,11 +124,21 @@ export const General: FC<GeneralProps> = ({ data, day }) => {
|
|||||||
const generalResponse = Object.entries(data).reduce(
|
const generalResponse = Object.entries(data).reduce(
|
||||||
(total, [fatherKey, values]) => {
|
(total, [fatherKey, values]) => {
|
||||||
const value = Object.keys(values).reduce((totalValue, key) => {
|
const value = Object.keys(values).reduce((totalValue, key) => {
|
||||||
if (Number(key) - currentDate < 0) {
|
const keyTimestamp = Number(key);
|
||||||
return { ...totalValue, [key]: values[key] };
|
const todayStart = moment().startOf('day').unix();
|
||||||
}
|
const todayEnd = moment().endOf('day').unix();
|
||||||
|
|
||||||
return totalValue;
|
// Включаем данные за сегодня и прошлые дни, исключаем будущие дни
|
||||||
|
if (keyTimestamp >= todayStart && keyTimestamp <= todayEnd) {
|
||||||
|
// Сегодняшний день - включаем
|
||||||
|
return { ...totalValue, [key]: values[key] };
|
||||||
|
} else if (keyTimestamp < todayStart) {
|
||||||
|
// Прошлые дни - включаем
|
||||||
|
return { ...totalValue, [key]: values[key] };
|
||||||
|
} else {
|
||||||
|
// Будущие дни - исключаем
|
||||||
|
return totalValue;
|
||||||
|
}
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
return { ...total, [fatherKey]: value };
|
return { ...total, [fatherKey]: value };
|
||||||
|
149
src/pages/Debug.tsx
Normal file
149
src/pages/Debug.tsx
Normal file
@ -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 (
|
||||||
|
<Box sx={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
bgcolor: 'rgba(0,0,0,0.9)',
|
||||||
|
zIndex: 9999,
|
||||||
|
p: 4,
|
||||||
|
overflow: 'auto'
|
||||||
|
}}>
|
||||||
|
<Box sx={{ maxWidth: 800, mx: 'auto', bgcolor: 'background.paper', p: 3, borderRadius: 2 }}>
|
||||||
|
<Typography variant="h4" gutterBottom>
|
||||||
|
🛠️ Отладка ошибок
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography variant="body2" sx={{ mb: 3, color: 'text.secondary' }}>
|
||||||
|
Ctrl+Shift+F для закрытия
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<TableContainer component={Paper}>
|
||||||
|
<Table>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>Описание ошибки</TableCell>
|
||||||
|
<TableCell align="right">Действие</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{errorTests.map((test, index) => (
|
||||||
|
<TableRow key={index}>
|
||||||
|
<TableCell>{test.description}</TableCell>
|
||||||
|
<TableCell align="right">
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="error"
|
||||||
|
size="small"
|
||||||
|
onClick={test.action}
|
||||||
|
>
|
||||||
|
Вызвать
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Debug;
|
@ -150,10 +150,6 @@ export const AmoQuestions: FC<Props> = ({
|
|||||||
}, [activeScope])
|
}, [activeScope])
|
||||||
const [blockButton, setBlockButton] = useState(false)
|
const [blockButton, setBlockButton] = useState(false)
|
||||||
|
|
||||||
console.log("selectedQuestions")
|
|
||||||
console.log(selectedQuestions)
|
|
||||||
console.log("SCFworld")
|
|
||||||
console.log(SCFworld)
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box
|
<Box
|
||||||
|
@ -95,8 +95,6 @@ export const SwitchPages = ({
|
|||||||
const [specialPage, setSpecialPage] = useState<"deleteCell" | "removeAccount" | "settingsBlock" | "accountInfo" | "amoLogin" | "">(accountInfo ? "accountInfo" : "amoLogin")
|
const [specialPage, setSpecialPage] = useState<"deleteCell" | "removeAccount" | "settingsBlock" | "accountInfo" | "amoLogin" | "">(accountInfo ? "accountInfo" : "amoLogin")
|
||||||
const [openDelete, setOpenDelete] = useState<TagQuestionHC | null>(null);
|
const [openDelete, setOpenDelete] = useState<TagQuestionHC | null>(null);
|
||||||
|
|
||||||
console.log("--")
|
|
||||||
console.log(selectedQuestions)
|
|
||||||
|
|
||||||
const startDeleteTagQuestion = (itemForDelete) => {
|
const startDeleteTagQuestion = (itemForDelete) => {
|
||||||
setOpenDelete(itemForDelete)
|
setOpenDelete(itemForDelete)
|
||||||
@ -135,9 +133,6 @@ export const SwitchPages = ({
|
|||||||
|
|
||||||
if (type === "tag") {
|
if (type === "tag") {
|
||||||
setSelectedTags((prevState) => {
|
setSelectedTags((prevState) => {
|
||||||
console.log(prevState)
|
|
||||||
console.log(scope)
|
|
||||||
console.log(id)
|
|
||||||
return({
|
return({
|
||||||
...prevState,
|
...prevState,
|
||||||
[scope]: [...prevState[scope as TagKeys], id],
|
[scope]: [...prevState[scope as TagKeys], id],
|
||||||
@ -147,9 +142,6 @@ export const SwitchPages = ({
|
|||||||
if (type === "question") {
|
if (type === "question") {
|
||||||
const q = questions.find(e => e.backendId === Number(id))
|
const q = questions.find(e => e.backendId === Number(id))
|
||||||
setSelectedQuestions((prevState) => {
|
setSelectedQuestions((prevState) => {
|
||||||
console.log(prevState)
|
|
||||||
console.log(scope)
|
|
||||||
console.log(id)
|
|
||||||
return ({
|
return ({
|
||||||
...prevState,
|
...prevState,
|
||||||
[scope]: [...prevState[scope as QuestionKeys], {
|
[scope]: [...prevState[scope as QuestionKeys], {
|
||||||
|
@ -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`;
|
const API_URL = `${process.env.REACT_APP_DOMAIN}/squiz/amocrm`;
|
||||||
|
|
||||||
export const resetAmoTagsFields = async () => {
|
export const resetAmoTagsFields = async () => {
|
||||||
|
@ -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<PostbackModalProps> = ({
|
||||||
|
isModalOpen,
|
||||||
|
handleCloseModal,
|
||||||
|
companyName,
|
||||||
|
quiz
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const isMobile = useMediaQuery(theme.breakpoints.down(600));
|
||||||
|
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={isModalOpen}
|
||||||
|
onClose={handleCloseModal}
|
||||||
|
fullWidth
|
||||||
|
PaperProps={{
|
||||||
|
sx: {
|
||||||
|
maxWidth: isTablet ? "100%" : "919px",
|
||||||
|
height: "658px",
|
||||||
|
borderRadius: "12px",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
height: "68px",
|
||||||
|
backgroundColor: theme.palette.background.default,
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: "0 20px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: isMobile ? "20px" : "24px",
|
||||||
|
fontWeight: "500",
|
||||||
|
color: theme.palette.grey2.main,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Интеграция с {companyName ? companyName : "Postback"}
|
||||||
|
</Typography>
|
||||||
|
<IconButton onClick={handleCloseModal}>
|
||||||
|
<CloseIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
padding: "20px",
|
||||||
|
height: "calc(100% - 68px)",
|
||||||
|
overflow: "auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="body1">
|
||||||
|
Интеграция с Postback находится в разработке.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
@ -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<ZapierModalProps> = ({
|
||||||
|
isModalOpen,
|
||||||
|
handleCloseModal,
|
||||||
|
companyName,
|
||||||
|
quiz
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const isMobile = useMediaQuery(theme.breakpoints.down(600));
|
||||||
|
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={isModalOpen}
|
||||||
|
onClose={handleCloseModal}
|
||||||
|
fullWidth
|
||||||
|
PaperProps={{
|
||||||
|
sx: {
|
||||||
|
maxWidth: isTablet ? "100%" : "919px",
|
||||||
|
height: "658px",
|
||||||
|
borderRadius: "12px",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
height: "68px",
|
||||||
|
backgroundColor: theme.palette.background.default,
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: "0 20px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: isMobile ? "20px" : "24px",
|
||||||
|
fontWeight: "500",
|
||||||
|
color: theme.palette.grey2.main,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Интеграция с {companyName ? companyName : "Zapier"}
|
||||||
|
</Typography>
|
||||||
|
<IconButton onClick={handleCloseModal}>
|
||||||
|
<CloseIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
padding: "20px",
|
||||||
|
height: "calc(100% - 68px)",
|
||||||
|
overflow: "auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="body1">
|
||||||
|
Интеграция с Zapier находится в разработке.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
@ -27,6 +27,8 @@ export const IntegrationsPage = ({
|
|||||||
>(null);
|
>(null);
|
||||||
|
|
||||||
const [isAmoCrmModalOpen, setIsAmoCrmModalOpen] = useState<boolean>(false);
|
const [isAmoCrmModalOpen, setIsAmoCrmModalOpen] = useState<boolean>(false);
|
||||||
|
const [isZapierModalOpen, setIsZapierModalOpen] = useState<boolean>(false);
|
||||||
|
const [isPostbackModalOpen, setIsPostbackModalOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (editQuizId === null) navigate("/list");
|
if (editQuizId === null) navigate("/list");
|
||||||
@ -44,6 +46,12 @@ export const IntegrationsPage = ({
|
|||||||
const handleCloseAmoSRMModal = () => {
|
const handleCloseAmoSRMModal = () => {
|
||||||
setIsAmoCrmModalOpen(false);
|
setIsAmoCrmModalOpen(false);
|
||||||
};
|
};
|
||||||
|
const handleCloseZapierModal = () => {
|
||||||
|
setIsZapierModalOpen(false);
|
||||||
|
};
|
||||||
|
const handleClosePostbackModal = () => {
|
||||||
|
setIsPostbackModalOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -60,7 +68,7 @@ export const IntegrationsPage = ({
|
|||||||
>
|
>
|
||||||
<Typography
|
<Typography
|
||||||
variant="h5"
|
variant="h5"
|
||||||
sx={{ marginBottom: "40px", color: "#333647" }}
|
sx={{ marginBottom: "40px", color: theme.palette.grey3.main }}
|
||||||
>
|
>
|
||||||
Интеграции
|
Интеграции
|
||||||
</Typography>
|
</Typography>
|
||||||
@ -73,6 +81,12 @@ export const IntegrationsPage = ({
|
|||||||
setIsAmoCrmModalOpen={setIsAmoCrmModalOpen}
|
setIsAmoCrmModalOpen={setIsAmoCrmModalOpen}
|
||||||
isAmoCrmModalOpen={isAmoCrmModalOpen}
|
isAmoCrmModalOpen={isAmoCrmModalOpen}
|
||||||
handleCloseAmoSRMModal={handleCloseAmoSRMModal}
|
handleCloseAmoSRMModal={handleCloseAmoSRMModal}
|
||||||
|
setIsZapierModalOpen={setIsZapierModalOpen}
|
||||||
|
isZapierModalOpen={isZapierModalOpen}
|
||||||
|
handleCloseZapierModal={handleCloseZapierModal}
|
||||||
|
setIsPostbackModalOpen={setIsPostbackModalOpen}
|
||||||
|
isPostbackModalOpen={isPostbackModalOpen}
|
||||||
|
handleClosePostbackModal={handleClosePostbackModal}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
|
@ -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 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 { YandexMetricaLogo } from "../mocks/YandexMetricaLogo";
|
||||||
// import AnalyticsModal from "./AnalyticsModal/AnalyticsModal";
|
|
||||||
import { VKPixelLogo } from "../mocks/VKPixelLogo";
|
import { VKPixelLogo } from "../mocks/VKPixelLogo";
|
||||||
import { QuizMetricType } from "@model/quizSettings";
|
import { QuizMetricType } from "@model/quizSettings";
|
||||||
import { AmoCRMLogo } from "../mocks/AmoCRMLogo";
|
import { AmoCRMLogo } from "../mocks/AmoCRMLogo";
|
||||||
import { useCurrentQuiz } from "@/stores/quizes/hooks";
|
import { useCurrentQuiz } from "@/stores/quizes/hooks";
|
||||||
import { useUserStore } from "@/stores/user";
|
|
||||||
|
|
||||||
const AnalyticsModal = lazy(() =>
|
const AnalyticsModal = lazy(() =>
|
||||||
import("./AnalyticsModal/AnalyticsModal").then((module) => ({
|
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 = {
|
type PartnersBoardProps = {
|
||||||
setIsModalOpen: (value: boolean) => void;
|
setIsModalOpen: (value: boolean) => void;
|
||||||
companyName: keyof typeof QuizMetricType | null;
|
companyName: keyof typeof QuizMetricType | null;
|
||||||
@ -30,6 +42,12 @@ type PartnersBoardProps = {
|
|||||||
setIsAmoCrmModalOpen: (value: boolean) => void;
|
setIsAmoCrmModalOpen: (value: boolean) => void;
|
||||||
isAmoCrmModalOpen: boolean;
|
isAmoCrmModalOpen: boolean;
|
||||||
handleCloseAmoSRMModal: () => void;
|
handleCloseAmoSRMModal: () => void;
|
||||||
|
setIsZapierModalOpen: (value: boolean) => void;
|
||||||
|
isZapierModalOpen: boolean;
|
||||||
|
handleCloseZapierModal: () => void;
|
||||||
|
setIsPostbackModalOpen: (value: boolean) => void;
|
||||||
|
isPostbackModalOpen: boolean;
|
||||||
|
handleClosePostbackModal: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PartnersBoard: FC<PartnersBoardProps> = ({
|
export const PartnersBoard: FC<PartnersBoardProps> = ({
|
||||||
@ -41,13 +59,31 @@ export const PartnersBoard: FC<PartnersBoardProps> = ({
|
|||||||
setIsAmoCrmModalOpen,
|
setIsAmoCrmModalOpen,
|
||||||
isAmoCrmModalOpen,
|
isAmoCrmModalOpen,
|
||||||
handleCloseAmoSRMModal,
|
handleCloseAmoSRMModal,
|
||||||
|
setIsZapierModalOpen,
|
||||||
|
isZapierModalOpen,
|
||||||
|
handleCloseZapierModal,
|
||||||
|
setIsPostbackModalOpen,
|
||||||
|
isPostbackModalOpen,
|
||||||
|
handleClosePostbackModal,
|
||||||
}) => {
|
}) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down(600));
|
|
||||||
|
|
||||||
const quiz = useCurrentQuiz();
|
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 (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@ -59,49 +95,28 @@ export const PartnersBoard: FC<PartnersBoardProps> = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box>
|
<Box>
|
||||||
<>
|
|
||||||
<Typography
|
|
||||||
variant="h6"
|
|
||||||
sx={{
|
|
||||||
textAlign: { xs: "start", sm: "start", md: "start" },
|
|
||||||
lineHeight: "1",
|
|
||||||
marginBottom: "12px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
CRM
|
|
||||||
</Typography>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
flexWrap: "wrap",
|
|
||||||
justifyContent: { xs: "start", sm: "start", md: "start" },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ServiceButton
|
|
||||||
logo={<AmoCRMLogo />}
|
|
||||||
setIsModalOpen={setIsAmoCrmModalOpen}
|
|
||||||
setCompanyName={setCompanyName}
|
|
||||||
name={"amoCRM"}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</>
|
|
||||||
<Typography
|
<Typography
|
||||||
variant="h6"
|
variant="h6"
|
||||||
sx={{
|
sx={{
|
||||||
textAlign: { xs: "start", sm: "start", md: "start" },
|
...sectionTitleStyles,
|
||||||
lineHeight: "1",
|
marginTop: 0,
|
||||||
marginBottom: "12px",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
CRM
|
||||||
|
</Typography>
|
||||||
|
<Box sx={containerStyles}>
|
||||||
|
<ServiceButton
|
||||||
|
logo={<AmoCRMLogo />}
|
||||||
|
setIsModalOpen={setIsAmoCrmModalOpen}
|
||||||
|
setCompanyName={setCompanyName}
|
||||||
|
name={"amoCRM"}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Typography variant="h6" sx={sectionTitleStyles}>
|
||||||
Аналитика
|
Аналитика
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box
|
<Box sx={containerStyles}>
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
flexWrap: "wrap",
|
|
||||||
justifyContent: { xs: "start", sm: "start", md: "start" },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ServiceButton
|
<ServiceButton
|
||||||
logo={<YandexMetricaLogo />}
|
logo={<YandexMetricaLogo />}
|
||||||
setIsModalOpen={setIsModalOpen}
|
setIsModalOpen={setIsModalOpen}
|
||||||
@ -114,9 +129,24 @@ export const PartnersBoard: FC<PartnersBoardProps> = ({
|
|||||||
name={"vk"}
|
name={"vk"}
|
||||||
setIsModalOpen={setIsModalOpen}
|
setIsModalOpen={setIsModalOpen}
|
||||||
setCompanyName={setCompanyName}
|
setCompanyName={setCompanyName}
|
||||||
></ServiceButton>
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Typography variant="h6" sx={sectionTitleStyles}>
|
||||||
|
Автоматизация
|
||||||
|
</Typography>
|
||||||
|
<Box sx={containerStyles}>
|
||||||
|
<ZapierButton
|
||||||
|
setIsModalOpen={setIsZapierModalOpen}
|
||||||
|
setCompanyName={setCompanyName}
|
||||||
|
/>
|
||||||
|
<PostbackButton
|
||||||
|
setIsModalOpen={setIsPostbackModalOpen}
|
||||||
|
setCompanyName={setCompanyName}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{companyName && (
|
{companyName && (
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<AnalyticsModal
|
<AnalyticsModal
|
||||||
@ -132,7 +162,27 @@ export const PartnersBoard: FC<PartnersBoardProps> = ({
|
|||||||
isModalOpen={isAmoCrmModalOpen}
|
isModalOpen={isAmoCrmModalOpen}
|
||||||
handleCloseModal={handleCloseAmoSRMModal}
|
handleCloseModal={handleCloseAmoSRMModal}
|
||||||
companyName={companyName}
|
companyName={companyName}
|
||||||
quiz={quiz}
|
quiz={quiz!}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
|
)}
|
||||||
|
{companyName && isZapierModalOpen && (
|
||||||
|
<Suspense>
|
||||||
|
<ZapierModal
|
||||||
|
isModalOpen={isZapierModalOpen}
|
||||||
|
handleCloseModal={handleCloseZapierModal}
|
||||||
|
companyName={companyName}
|
||||||
|
quiz={quiz!}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
|
)}
|
||||||
|
{companyName && isPostbackModalOpen && (
|
||||||
|
<Suspense>
|
||||||
|
<PostbackModal
|
||||||
|
isModalOpen={isPostbackModalOpen}
|
||||||
|
handleCloseModal={handleClosePostbackModal}
|
||||||
|
companyName={companyName}
|
||||||
|
quiz={quiz!}
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
)}
|
)}
|
||||||
|
@ -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<PartnerItemProps> = ({
|
|
||||||
setIsModalOpen,
|
|
||||||
logo,
|
|
||||||
title,
|
|
||||||
name,
|
|
||||||
setCompanyName,
|
|
||||||
}) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
const handleClick = () => {
|
|
||||||
setCompanyName(name as keyof typeof QuizMetricType);
|
|
||||||
setIsModalOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
width: 250,
|
|
||||||
height: 60,
|
|
||||||
backgroundColor: "white",
|
|
||||||
borderRadius: "8px",
|
|
||||||
padding: "0 20px",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
marginBottom: "2%",
|
|
||||||
marginRight: "2%",
|
|
||||||
cursor: "pointer",
|
|
||||||
}}
|
|
||||||
onClick={handleClick}
|
|
||||||
>
|
|
||||||
{logo && logo}
|
|
||||||
<Typography
|
|
||||||
sx={{
|
|
||||||
fontSize: "18px",
|
|
||||||
fontWeight: "400",
|
|
||||||
marginLeft: "15px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{title && title}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
@ -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<IntegrationButtonProps> = ({
|
||||||
|
children,
|
||||||
|
onClick,
|
||||||
|
padding = "0 20px",
|
||||||
|
}) => {
|
||||||
|
const [isPressed, setIsPressed] = useState(false);
|
||||||
|
|
||||||
|
const handleMouseDown = () => setIsPressed(true);
|
||||||
|
const handleMouseUp = () => setIsPressed(false);
|
||||||
|
const handleMouseLeave = () => setIsPressed(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: 250,
|
||||||
|
height: 60,
|
||||||
|
backgroundColor: "white",
|
||||||
|
borderRadius: "8px",
|
||||||
|
padding,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
cursor: "pointer",
|
||||||
|
transition: "all 0.2s ease",
|
||||||
|
boxShadow: "0 2px 4px rgba(0, 0, 0, 0.1)",
|
||||||
|
transform: isPressed ? "translateY(1px)" : "translateY(0)",
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: "#f8f9fa",
|
||||||
|
boxShadow: "0 4px 8px rgba(0, 0, 0, 0.15)",
|
||||||
|
transform: "translateY(-1px)",
|
||||||
|
},
|
||||||
|
"&:active": {
|
||||||
|
transform: "translateY(1px)",
|
||||||
|
boxShadow: "0 1px 2px rgba(0, 0, 0, 0.1)",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onClick={onClick}
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
onMouseUp={handleMouseUp}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
@ -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<PostbackButtonProps> = ({
|
||||||
|
setIsModalOpen,
|
||||||
|
setCompanyName,
|
||||||
|
}) => {
|
||||||
|
const handleClick = () => {
|
||||||
|
setCompanyName("postback" as keyof typeof QuizMetricType);
|
||||||
|
setIsModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IntegrationButton onClick={handleClick} padding="0 0 0 20px">
|
||||||
|
<>
|
||||||
|
{/* Иконка монитора */}
|
||||||
|
<PostbackPC />
|
||||||
|
{/* Текст Postback */}
|
||||||
|
<PostbackDefault />
|
||||||
|
</>
|
||||||
|
</IntegrationButton>
|
||||||
|
);
|
||||||
|
};
|
@ -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<PartnerItemProps> = ({
|
||||||
|
setIsModalOpen,
|
||||||
|
logo,
|
||||||
|
title,
|
||||||
|
name,
|
||||||
|
setCompanyName,
|
||||||
|
}) => {
|
||||||
|
const handleClick = () => {
|
||||||
|
setCompanyName(name as keyof typeof QuizMetricType);
|
||||||
|
setIsModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IntegrationButton onClick={handleClick}>
|
||||||
|
{logo && logo}
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "18px",
|
||||||
|
fontWeight: "400",
|
||||||
|
marginLeft: "15px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{title && title}
|
||||||
|
</Typography>
|
||||||
|
</IntegrationButton>
|
||||||
|
);
|
||||||
|
};
|
@ -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<ZapierButtonProps> = ({
|
||||||
|
setIsModalOpen,
|
||||||
|
setCompanyName,
|
||||||
|
}) => {
|
||||||
|
const handleClick = () => {
|
||||||
|
setCompanyName("zapier" as keyof typeof QuizMetricType);
|
||||||
|
setIsModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IntegrationButton onClick={handleClick} padding="14px 128px 14px 20px">
|
||||||
|
<Box
|
||||||
|
component="img"
|
||||||
|
src={zapierLogo}
|
||||||
|
alt="Zapier"
|
||||||
|
sx={{
|
||||||
|
width: "103px",
|
||||||
|
height: "33px",
|
||||||
|
objectFit: "contain",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</IntegrationButton>
|
||||||
|
);
|
||||||
|
};
|
12
src/pages/Payment/Payment.tsx
Normal file
12
src/pages/Payment/Payment.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { useAuthRedirect } from "../../utils/hooks/useAuthRedirect";
|
||||||
|
|
||||||
|
export default function Payment() {
|
||||||
|
// Используем хук авторизации
|
||||||
|
const { isProcessing } = useAuthRedirect();
|
||||||
|
|
||||||
|
// Если идет обработка авторизации, показываем загрузку
|
||||||
|
if (isProcessing) {
|
||||||
|
return <div>Идёт загрузка...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... existing component code ...
|
@ -11,8 +11,6 @@ export const AuditoryList = ({utmParams, auditory, onDelete}:{utmParams:string,a
|
|||||||
const { isTestServer } = useDomainDefine();
|
const { isTestServer } = useDomainDefine();
|
||||||
const [linksOpen, setLinksOpen] = useState(true);
|
const [linksOpen, setLinksOpen] = useState(true);
|
||||||
|
|
||||||
console.log("auditory-___---_auditory__---__-__auditory_------__---__-__---_------__---__-__---_------__---__-____--__")
|
|
||||||
console.log(auditory)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
112
src/pages/PersonalizationAI/CreateButtonWithTooltip.tsx
Normal file
112
src/pages/PersonalizationAI/CreateButtonWithTooltip.tsx
Normal file
@ -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 (
|
||||||
|
<ClickAwayListener onClickAway={handleTooltipClose}>
|
||||||
|
<div style={{ position: 'relative' }}>
|
||||||
|
{isDisabled && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
zIndex: 1,
|
||||||
|
cursor: 'help'
|
||||||
|
}}
|
||||||
|
onMouseEnter={handleTooltipOpen}
|
||||||
|
onMouseLeave={handleTooltipClose}
|
||||||
|
onClick={handleTooltipOpen}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Tooltip
|
||||||
|
PopperProps={{
|
||||||
|
disablePortal: true,
|
||||||
|
sx: {
|
||||||
|
"& .MuiTooltip-tooltip": {
|
||||||
|
fontSize: "14px",
|
||||||
|
padding: "12px",
|
||||||
|
maxWidth: "400px",
|
||||||
|
whiteSpace: "pre-line"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placement="top"
|
||||||
|
onClose={handleTooltipClose}
|
||||||
|
open={open}
|
||||||
|
title={tooltipText}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
onClick={onClick}
|
||||||
|
variant="contained"
|
||||||
|
disabled={isDisabled}
|
||||||
|
sx={{
|
||||||
|
bgcolor: theme.palette.brightPurple.main,
|
||||||
|
borderRadius: "8px",
|
||||||
|
width: "130px",
|
||||||
|
height: "48px",
|
||||||
|
boxShadow: "none",
|
||||||
|
textTransform: "none",
|
||||||
|
fontSize: "18px",
|
||||||
|
'&:hover': { bgcolor: theme.palette.brightPurple.main },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Ок
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</ClickAwayListener>
|
||||||
|
);
|
||||||
|
}
|
@ -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 CheckboxIcon from "@icons/Checkbox";
|
||||||
import AgeInputWithSelect from "./AgeInputWithSelect";
|
import AgeInputWithSelect from "./AgeInputWithSelect";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
|
import CreateButtonWithTooltip from "./CreateButtonWithTooltip";
|
||||||
|
|
||||||
interface GenderAndAgeSelectorProps {
|
interface GenderAndAgeSelectorProps {
|
||||||
gender: string;
|
gender: string;
|
||||||
@ -190,23 +191,12 @@ export default function GenderAndAgeSelector({
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Button
|
<CreateButtonWithTooltip
|
||||||
|
gender={gender}
|
||||||
|
age={age}
|
||||||
|
ageError={ageError}
|
||||||
onClick={onStartCreate}
|
onClick={onStartCreate}
|
||||||
variant="contained"
|
/>
|
||||||
disabled={!gender || !age || ageError}
|
|
||||||
sx={{
|
|
||||||
bgcolor: theme.palette.brightPurple.main,
|
|
||||||
borderRadius: "8px",
|
|
||||||
width: "130px",
|
|
||||||
height: "48px",
|
|
||||||
boxShadow: "none",
|
|
||||||
textTransform: "none",
|
|
||||||
fontSize: "18px",
|
|
||||||
'&:hover': { bgcolor: theme.palette.brightPurple.main },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Ок
|
|
||||||
</Button>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -11,16 +11,17 @@ import { useSnackbar } from "notistack";
|
|||||||
import { PayModal } from "./PayModal";
|
import { PayModal } from "./PayModal";
|
||||||
import { useUserStore } from "@/stores/user";
|
import { useUserStore } from "@/stores/user";
|
||||||
import { cartApi } from "@/api/cart";
|
import { cartApi } from "@/api/cart";
|
||||||
import { outCart } from "../Tariffs/Tariffs";
|
import { outCart } from "../Tariffs/utils";
|
||||||
import { inCart } from "../Tariffs/Tariffs";
|
import { inCart } from "../Tariffs/utils";
|
||||||
import { isTestServer } from "@/utils/hooks/useDomainDefine";
|
import { isTestServer } from "@/utils/hooks/useDomainDefine";
|
||||||
import { useToken } from "@frontend/kitui";
|
import { useToken } from "@frontend/kitui";
|
||||||
import { useSWRConfig } from "swr";
|
import { useSWRConfig } from "swr";
|
||||||
import { makeRequest } from "@api/makeRequest";
|
import { makeRequest } from "@frontend/kitui";
|
||||||
import { setUserAccount, setCustomerAccount } from "@/stores/user";
|
import { setUserAccount, setCustomerAccount } from "@/stores/user";
|
||||||
import { quizApi } from "@api/quiz";
|
import { quizApi } from "@api/quiz";
|
||||||
import { setQuizes } from "@root/quizes/actions";
|
import { setQuizes } from "@root/quizes/actions";
|
||||||
import TooltipClickInfo from "@/ui_kit/Toolbars/TooltipClickInfo";
|
import TooltipClickInfo from "@/ui_kit/Toolbars/TooltipClickInfo";
|
||||||
|
import { generateHubWalletRequestURL } from "@/utils/generateHubWalletRequest";
|
||||||
|
|
||||||
const tariff = isTestServer ? "6844b8858258f5cc35791ef7" : "6851db40acfb4d3e5fcd9b19";
|
const tariff = isTestServer ? "6844b8858258f5cc35791ef7" : "6851db40acfb4d3e5fcd9b19";
|
||||||
export default function PersonalizationAI() {
|
export default function PersonalizationAI() {
|
||||||
@ -92,7 +93,7 @@ export default function PersonalizationAI() {
|
|||||||
useToken: true,
|
useToken: true,
|
||||||
withCredentials: false,
|
withCredentials: false,
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
console.log(error)
|
console.error(error)
|
||||||
enqueueSnackbar("Ошибка при обновлении данных пользователя", { variant: "error" });
|
enqueueSnackbar("Ошибка при обновлении данных пользователя", { variant: "error" });
|
||||||
return null;
|
return null;
|
||||||
}),
|
}),
|
||||||
@ -102,7 +103,7 @@ export default function PersonalizationAI() {
|
|||||||
useToken: true,
|
useToken: true,
|
||||||
withCredentials: false,
|
withCredentials: false,
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
console.log(error)
|
console.error(error)
|
||||||
enqueueSnackbar("Ошибка при обновлении данных клиента", { variant: "error" });
|
enqueueSnackbar("Ошибка при обновлении данных клиента", { variant: "error" });
|
||||||
return null;
|
return null;
|
||||||
})
|
})
|
||||||
@ -115,7 +116,7 @@ export default function PersonalizationAI() {
|
|||||||
setCustomerAccount(customerAccountResult);
|
setCustomerAccount(customerAccountResult);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.error(error)
|
||||||
enqueueSnackbar("Ошибка при обновлении данных", { variant: "error" });
|
enqueueSnackbar("Ошибка при обновлении данных", { variant: "error" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -128,8 +129,6 @@ export default function PersonalizationAI() {
|
|||||||
(async () => {
|
(async () => {
|
||||||
if (quiz?.backendId) {
|
if (quiz?.backendId) {
|
||||||
const [result, error] = await auditoryGet({ quizId: quiz.backendId });
|
const [result, error] = await auditoryGet({ quizId: quiz.backendId });
|
||||||
console.log("result-___---_------__---__-__---_------__---__-__---_------__---__-__---_------__---__-____--__")
|
|
||||||
console.log(result)
|
|
||||||
if (result) {
|
if (result) {
|
||||||
setAuditory(result);
|
setAuditory(result);
|
||||||
}
|
}
|
||||||
@ -209,8 +208,6 @@ export default function PersonalizationAI() {
|
|||||||
setUtmParams(paramString ? `&${paramString}` : "");
|
setUtmParams(paramString ? `&${paramString}` : "");
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("______----giga_chat-----__--_---_--_----__--__-__--_--__--__--_---_______-quiz")
|
|
||||||
console.log(quiz?.giga_chat)
|
|
||||||
const startCreate = async () => {
|
const startCreate = async () => {
|
||||||
if (quiz?.giga_chat) {
|
if (quiz?.giga_chat) {
|
||||||
createNewLink();
|
createNewLink();
|
||||||
@ -240,7 +237,15 @@ export default function PersonalizationAI() {
|
|||||||
//если денег не хватило
|
//если денег не хватило
|
||||||
if (payError?.includes("insufficient funds") || payError?.includes("Payment Required")) {
|
if (payError?.includes("insufficient funds") || payError?.includes("Payment Required")) {
|
||||||
var link = document.createElement("a");
|
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);
|
document.body.appendChild(link);
|
||||||
link.click();
|
link.click();
|
||||||
return;
|
return;
|
||||||
@ -263,12 +268,9 @@ export default function PersonalizationAI() {
|
|||||||
|
|
||||||
|
|
||||||
// Обновляем данные квиза после успешной оплаты
|
// Обновляем данные квиза после успешной оплаты
|
||||||
console.log("Обновляем данные квиза после оплаты");
|
|
||||||
const [quizes, quizesError] = await quizApi.getList();
|
const [quizes, quizesError] = await quizApi.getList();
|
||||||
console.log("Получены данные квизов:", quizes);
|
|
||||||
if (!quizesError) {
|
if (!quizesError) {
|
||||||
setQuizes(quizes);
|
setQuizes(quizes);
|
||||||
console.log("Данные квизов обновлены в сторе");
|
|
||||||
} else {
|
} else {
|
||||||
console.error("Ошибка при получении данных квизов:", quizesError);
|
console.error("Ошибка при получении данных квизов:", quizesError);
|
||||||
}
|
}
|
||||||
@ -289,7 +291,7 @@ export default function PersonalizationAI() {
|
|||||||
lineHeight: "21.4px"
|
lineHeight: "21.4px"
|
||||||
}}>
|
}}>
|
||||||
Данный раздел позволяет вам создавать персонализированный опрос под каждую целевую аудиторию отдельно, наш AI перефразирует ваши вопросы согласно настройкам.
|
Данный раздел позволяет вам создавать персонализированный опрос под каждую целевую аудиторию отдельно, наш AI перефразирует ваши вопросы согласно настройкам.
|
||||||
<br/>Для этого нужно выбрать пол и возраст вашей аудитории и получите персональную ссылку с нужными настройками в списке ниже.
|
<br />Для этого нужно выбрать пол и возраст вашей аудитории и получите персональную ссылку с нужными настройками в списке ниже.
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography sx={{
|
<Typography sx={{
|
||||||
color: theme.palette.grey3.main, fontSize: "18px", maxWidth: 796, m: 0,
|
color: theme.palette.grey3.main, fontSize: "18px", maxWidth: 796, m: 0,
|
||||||
|
@ -8,7 +8,6 @@ import { uploadQuestionImage } from "@/stores/questions/actions";
|
|||||||
import { useCurrentQuiz } from "@/stores/quizes/hooks";
|
import { useCurrentQuiz } from "@/stores/quizes/hooks";
|
||||||
import { updateQuestion } from "@root/questions/actions";
|
import { updateQuestion } from "@root/questions/actions";
|
||||||
let params = (new URL(document.location)).searchParams;
|
let params = (new URL(document.location)).searchParams;
|
||||||
console.log(params.get("data"));
|
|
||||||
const BranchingMap = lazy(() =>
|
const BranchingMap = lazy(() =>
|
||||||
import("./BranchingMap").then((module) => ({ default: module.BranchingMap })),
|
import("./BranchingMap").then((module) => ({ default: module.BranchingMap })),
|
||||||
);
|
);
|
||||||
@ -55,31 +54,6 @@ export const QuestionSwitchWindowTool = ({
|
|||||||
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
// <input type="file" multiple
|
|
||||||
// onChange={(e) => {
|
|
||||||
// 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);
|
|
||||||
// });
|
|
||||||
// }}
|
|
||||||
// />
|
|
||||||
}
|
}
|
||||||
<Box sx={{ width: isTablet ? "100%" : "796px" }}>
|
<Box sx={{ width: isTablet ? "100%" : "796px" }}>
|
||||||
{openBranchingPage ? (
|
{openBranchingPage ? (
|
||||||
|
@ -126,7 +126,6 @@ const QuestionPageCardTitle = memo<Props>(function ({
|
|||||||
value={title}
|
value={title}
|
||||||
placeholder={"Заголовок вопроса"}
|
placeholder={"Заголовок вопроса"}
|
||||||
onChange={({ target }) => {
|
onChange={({ target }) => {
|
||||||
console.log(target.value.length)
|
|
||||||
if (target.value.length > maxLengthTextField) {
|
if (target.value.length > maxLengthTextField) {
|
||||||
enqueueSnackbar("Превышена длина вводимого текста")
|
enqueueSnackbar("Превышена длина вводимого текста")
|
||||||
} else {
|
} else {
|
||||||
|
@ -31,9 +31,6 @@ export const DraggableList = ({
|
|||||||
createUntypedQuestion(Number(quiz.backendId));
|
createUntypedQuestion(Number(quiz.backendId));
|
||||||
}
|
}
|
||||||
}, [quiz, filteredQuestions]);
|
}, [quiz, filteredQuestions]);
|
||||||
console.log(quiz)
|
|
||||||
console.log(questions)
|
|
||||||
// if () {}uploadQuestionImage
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DragDropContext onDragEnd={onDragEnd}>
|
<DragDropContext onDragEnd={onDragEnd}>
|
||||||
|
@ -15,7 +15,6 @@ export default function VariantAdornment({
|
|||||||
}) {
|
}) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
console.log("VariantAdornment extendedText", extendedText)
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ cursor: "pointer" }}>
|
<Box sx={{ cursor: "pointer" }}>
|
||||||
<Box data-cy="choose-emoji-button" onClick={onClick}>
|
<Box data-cy="choose-emoji-button" onClick={onClick}>
|
||||||
|
@ -44,11 +44,6 @@ export default function SliderOptions({ question, openBranchingPage, setOpenBran
|
|||||||
});
|
});
|
||||||
}, 5000);
|
}, 5000);
|
||||||
const updateStepsDebounced = useDebouncedCallback((value: string) => {
|
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<QuizQuestionNumber>(question.id, (question) => {
|
updateQuestion<QuizQuestionNumber>(question.id, (question) => {
|
||||||
question.content.step = ReplaceToNotStartZero(Number(value));
|
question.content.step = ReplaceToNotStartZero(Number(value));
|
||||||
});
|
});
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
import { Tabs as MuiTabs } from "@mui/material";
|
import { Tabs as MuiTabs } from "@mui/material";
|
||||||
import { CustomTab } from "./CustomTab";
|
import { CustomTab } from "./CustomTab";
|
||||||
|
import { TypePages } from "./types";
|
||||||
|
|
||||||
type TabsProps = {
|
type TabsProps = {
|
||||||
names: string[];
|
names: string[];
|
||||||
items: string[];
|
items: string[];
|
||||||
selectedItem: "day" | "count" | "dop" | "hide" | "create";
|
selectedItem: TypePages;
|
||||||
setSelectedItem: (num: "day" | "count" | "dop") => void;
|
setSelectedItem: (num: TypePages) => void;
|
||||||
|
toDop: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Tabs = ({
|
export const Tabs = ({
|
||||||
@ -18,7 +20,7 @@ export const Tabs = ({
|
|||||||
sx={{ m: "25px" }}
|
sx={{ m: "25px" }}
|
||||||
TabIndicatorProps={{ sx: { display: "none" } }}
|
TabIndicatorProps={{ sx: { display: "none" } }}
|
||||||
value={selectedItem}
|
value={selectedItem}
|
||||||
onChange={(event, newValue: "day" | "count" | "dop") => {
|
onChange={(event, newValue: TypePages) => {
|
||||||
setSelectedItem(newValue);
|
setSelectedItem(newValue);
|
||||||
}}
|
}}
|
||||||
variant="scrollable"
|
variant="scrollable"
|
||||||
|
147
src/pages/Tariffs/TariffCardDisplaySelector.tsx
Normal file
147
src/pages/Tariffs/TariffCardDisplaySelector.tsx
Normal file
@ -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 <Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
width: "100%"
|
||||||
|
}}>
|
||||||
|
{content.map(data => <NavCard {...data} key={data.title} />)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
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,
|
||||||
|
<SmallIconPena />
|
||||||
|
)
|
||||||
|
|
||||||
|
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 <Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
width: "100%"
|
||||||
|
}}>
|
||||||
|
{content.map(data => <NavCard {...data} key={data.title} />)}
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
}
|
@ -1,39 +1,34 @@
|
|||||||
import { activatePromocode } from "@api/promocode";
|
import { activatePromocode } from "@api/promocode";
|
||||||
import { useToken } from "@frontend/kitui";
|
import { useToken } from "@frontend/kitui";
|
||||||
import ArrowLeft from "@icons/questionsPage/arrowLeft";
|
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
|
||||||
Container,
|
|
||||||
IconButton,
|
|
||||||
Modal,
|
|
||||||
Paper,
|
|
||||||
Typography,
|
Typography,
|
||||||
useMediaQuery,
|
useMediaQuery,
|
||||||
useTheme,
|
useTheme,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { useUserStore } from "@root/user";
|
import { useUserStore } from "@root/user";
|
||||||
import { LogoutButton } from "@ui_kit/LogoutButton";
|
|
||||||
import { useDomainDefine } from "@utils/hooks/useDomainDefine";
|
import { useDomainDefine } from "@utils/hooks/useDomainDefine";
|
||||||
import { enqueueSnackbar } from "notistack";
|
import { enqueueSnackbar } from "notistack";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { withErrorBoundary } from "react-error-boundary";
|
import { withErrorBoundary } from "react-error-boundary";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import Logotip from "../../pages/Landing/images/icons/QuizLogo";
|
|
||||||
import CollapsiblePromocodeField from "./CollapsiblePromocodeField";
|
import CollapsiblePromocodeField from "./CollapsiblePromocodeField";
|
||||||
import { Tabs } from "./Tabs";
|
import { Tabs } from "./Tabs";
|
||||||
import { createTariffElements } from "./tariffsUtils/createTariffElements";
|
import { createTariffElements } from "./tariffsUtils/createTariffElements";
|
||||||
import { currencyFormatter } from "./tariffsUtils/currencyFormatter";
|
import { currencyFormatter } from "./tariffsUtils/currencyFormatter";
|
||||||
import { useWallet, setCash } from "@root/cash";
|
import { useWallet, setCash } from "@root/cash";
|
||||||
import { handleLogoutClick } from "@utils/HandleLogoutClick";
|
|
||||||
import { cartApi } from "@api/cart";
|
import { cartApi } from "@api/cart";
|
||||||
|
|
||||||
import { Other } from "./pages/Other";
|
import { TariffCardDisplaySelector } from "./TariffCardDisplaySelector";
|
||||||
import { ModalRequestCreate } from "./ModalRequestCreate";
|
import { ModalRequestCreate } from "./ModalRequestCreate";
|
||||||
import { cancelCC, useCC } from "@/stores/cc";
|
import { cancelCC, useCC } from "@/stores/cc";
|
||||||
import { NavSelect } from "./NavSelect";
|
import { NavSelect } from "./NavSelect";
|
||||||
import { useTariffs } from '@utils/hooks/useTariffs';
|
import { useTariffs } from '@utils/hooks/useTariffs';
|
||||||
import { useDiscounts } from '@utils/hooks/useDiscounts';
|
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<string, string> = {
|
const StepperText: Record<string, string> = {
|
||||||
day: "Тарифы на время",
|
day: "Тарифы на время",
|
||||||
@ -50,12 +45,17 @@ function TariffPage() {
|
|||||||
const userId = useUserStore((state) => state.userId);
|
const userId = useUserStore((state) => state.userId);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const user = useUserStore((state) => state.customerAccount);
|
const user = useUserStore((state) => state.customerAccount);
|
||||||
const a = useUserStore((state) => state.customerAccount); //c wallet
|
const userWithWallet = useUserStore((state) => state.customerAccount); //c wallet
|
||||||
console.log("________________34563875693785692576_____________USERRRRRRR")
|
const userAccount = useUserStore((state) => state.userAccount);
|
||||||
console.log(a)
|
// console.info("________________userWithWallet_____________USERRRRRRR")
|
||||||
const { data: discounts } = useDiscounts(userId);
|
// 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 [isRequestCreate, setIsRequestCreate] = useState(false);
|
||||||
const [openModal, setOpenModal] = useState({});
|
const [openModal, setOpenModal] = useState<{ id?: string; price?: number }>({});
|
||||||
const { cashString, cashCop, cashRub } = useWallet();
|
const { cashString, cashCop, cashRub } = useWallet();
|
||||||
const [selectedItem, setSelectedItem] = useState<TypePages>("day");
|
const [selectedItem, setSelectedItem] = useState<TypePages>("day");
|
||||||
const { isTestServer } = useDomainDefine();
|
const { isTestServer } = useDomainDefine();
|
||||||
@ -65,17 +65,14 @@ function TariffPage() {
|
|||||||
|
|
||||||
const { data: tariffs, error: tariffsError, isLoading: tariffsLoading } = useTariffs();
|
const { data: tariffs, error: tariffsError, isLoading: tariffsLoading } = useTariffs();
|
||||||
|
|
||||||
console.log("________34563875693785692576_____ TARIFFS")
|
|
||||||
console.log(tariffs)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (a) {
|
if (userWithWallet && user) {
|
||||||
let cs = currencyFormatter.format(Number(user.wallet.cash) / 100);
|
let cs = currencyFormatter.format(Number(user.wallet.cash) / 100);
|
||||||
let cc = Number(user.wallet.cash);
|
let cc = Number(user.wallet.cash);
|
||||||
let cr = Number(user.wallet.cash) / 100;
|
let cr = Number(user.wallet.cash) / 100;
|
||||||
setCash(cs, cc, cr);
|
setCash(cs, cc, cr);
|
||||||
}
|
}
|
||||||
}, [a]);
|
}, [userWithWallet, user]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (cc) {
|
if (cc) {
|
||||||
@ -83,7 +80,26 @@ console.log(tariffs)
|
|||||||
cancelCC()
|
cancelCC()
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
if (!user || !tariffs || !discounts) return <LoadingPage />;
|
|
||||||
|
// Проверяем, что все данные загружены и нет ошибок
|
||||||
|
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 <LoadingPage />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasErrors) {
|
||||||
|
return <LoadingPage />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasAllData) {
|
||||||
|
return <LoadingPage />;
|
||||||
|
}
|
||||||
|
|
||||||
const openModalHC = (tariffInfo: any) => setOpenModal(tariffInfo);
|
const openModalHC = (tariffInfo: any) => setOpenModal(tariffInfo);
|
||||||
const tryBuy = async ({ id, price }: { id: string; price: number }) => {
|
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")) {
|
if (payError?.includes("insufficient funds") || payError?.includes("Payment Required")) {
|
||||||
let cashDif = Number(payError.split(":")[1]);
|
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 (!userId) {
|
||||||
if (cc) link.href = link.href + "&cc=true"//после покупки тарифа и возвращения будем знать что надо открыть модалку
|
enqueueSnackbar("Ошибка: ID пользователя не найден");
|
||||||
document.body.appendChild(link);
|
return;
|
||||||
link.click();
|
}
|
||||||
|
|
||||||
|
const l = generateHubWalletRequestURL({
|
||||||
|
action: cc ? "createquizcc" : "buy",
|
||||||
|
dif: cashDif.toString(),
|
||||||
|
userid: userId,
|
||||||
|
wayback: "list",
|
||||||
|
token
|
||||||
|
});
|
||||||
|
window.location.href = l;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,63 +194,10 @@ console.log(tariffs)
|
|||||||
setIsRequestCreate(true)
|
setIsRequestCreate(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!a) return null;
|
if (!userWithWallet) return null;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Container
|
<TariffsHeader cashString={cashString} />
|
||||||
component="nav"
|
|
||||||
disableGutters
|
|
||||||
maxWidth={false}
|
|
||||||
sx={{
|
|
||||||
px: "16px",
|
|
||||||
display: "flex",
|
|
||||||
height: "80px",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: isMobile ? "7px" : isTablet ? "20px" : "60px",
|
|
||||||
flexDirection: "row",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
bgcolor: "white",
|
|
||||||
borderBottom: "1px solid #E3E3E3",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Link to="/">
|
|
||||||
<Logotip width={124} />
|
|
||||||
</Link>
|
|
||||||
<IconButton onClick={() => navigate("/list")}>
|
|
||||||
<ArrowLeft color="black" />
|
|
||||||
</IconButton>
|
|
||||||
<Box sx={{ display: "flex", ml: "auto" }}>
|
|
||||||
<Box sx={{ whiteSpace: "nowrap" }}>
|
|
||||||
<Typography
|
|
||||||
sx={{
|
|
||||||
fontSize: "12px",
|
|
||||||
lineHeight: "14px",
|
|
||||||
color: "gray",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Мой баланс
|
|
||||||
</Typography>
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
color={"#7e2aea"}
|
|
||||||
fontSize={
|
|
||||||
isMobile ? (cashString.length > 9 ? "13px" : "16px") : "16px"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{cashString}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
<LogoutButton
|
|
||||||
onClick={() => {
|
|
||||||
navigate("/");
|
|
||||||
handleLogoutClick();
|
|
||||||
}}
|
|
||||||
sx={{
|
|
||||||
ml: "20px",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</Container>
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
p: "25px",
|
p: "25px",
|
||||||
@ -281,9 +253,9 @@ console.log(tariffs)
|
|||||||
discounts,
|
discounts,
|
||||||
openModalHC,
|
openModalHC,
|
||||||
)}
|
)}
|
||||||
{(selectedItem === "dop" || selectedItem === "hide" || selectedItem === "create")
|
{(selectedItem === "hide" || selectedItem === "create" || selectedItem === "premium" || selectedItem === "analytics" || selectedItem === "custom")
|
||||||
&& (
|
&& (
|
||||||
<Other
|
<TariffCardDisplaySelector
|
||||||
selectedItem={selectedItem}
|
selectedItem={selectedItem}
|
||||||
content={[
|
content={[
|
||||||
{
|
{
|
||||||
@ -294,48 +266,86 @@ console.log(tariffs)
|
|||||||
title: "Создать квиз на заказ",
|
title: "Создать квиз на заказ",
|
||||||
onClick: () => setSelectedItem("create")
|
onClick: () => setSelectedItem("create")
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Премиум функции",
|
||||||
|
onClick: () => setSelectedItem("premium")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Расширенная аналитика",
|
||||||
|
onClick: () => setSelectedItem("analytics")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Кастомные тарифы",
|
||||||
|
onClick: () => setSelectedItem("custom")
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
|
|
||||||
tariffs={tariffs}
|
tariffs={tariffs}
|
||||||
user={user}
|
user={user}
|
||||||
discounts={discounts}
|
discounts={discounts || []}
|
||||||
openModalHC={openModalHC}
|
openModalHC={openModalHC}
|
||||||
userPrivilegies={userPrivilegies}
|
userPrivilegies={userPrivilegies}
|
||||||
startRequestCreate={startRequestCreate}
|
startRequestCreate={startRequestCreate}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{selectedItem === "dop" && (
|
||||||
|
<TariffCardDisplaySelector
|
||||||
|
selectedItem={selectedItem}
|
||||||
|
content={
|
||||||
|
selectedItem === "dop"
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
title: `Убрать логотип "PenaQuiz"`,
|
||||||
|
onClick: () => 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}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<Modal
|
<PaymentConfirmationModal
|
||||||
open={Object.values(openModal).length > 0}
|
open={Object.values(openModal).length > 0}
|
||||||
onClose={() => setOpenModal({})}
|
onClose={() => setOpenModal({})}
|
||||||
>
|
onConfirm={() => {
|
||||||
<Paper
|
if (openModal.id && openModal.price !== undefined) {
|
||||||
sx={{
|
tryBuy({ id: openModal.id, price: openModal.price });
|
||||||
position: "absolute" as "absolute",
|
}
|
||||||
top: "50%",
|
}}
|
||||||
left: "50%",
|
price={openModal.price || 0}
|
||||||
transform: "translate(-50%, -50%)",
|
/>
|
||||||
boxShadow: 24,
|
|
||||||
p: 4,
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
flexDirection: "column",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
id="modal-modal-title"
|
|
||||||
variant="h6"
|
|
||||||
component="h2"
|
|
||||||
mb="20px"
|
|
||||||
>
|
|
||||||
Вы подтверждаете платёж в сумму{" "}
|
|
||||||
{openModal.price ? openModal.price.toFixed(2) : 0} ₽
|
|
||||||
</Typography>
|
|
||||||
<Button variant="contained" onClick={() => tryBuy(openModal)}>
|
|
||||||
купить
|
|
||||||
</Button>
|
|
||||||
</Paper>
|
|
||||||
</Modal>
|
|
||||||
<ModalRequestCreate open={isRequestCreate} onClose={() => setIsRequestCreate(false)} />
|
<ModalRequestCreate open={isRequestCreate} onClose={() => setIsRequestCreate(false)} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -364,47 +374,3 @@ const LoadingPage = () => (
|
|||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
||||||
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));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
51
src/pages/Tariffs/components/PaymentConfirmationModal.tsx
Normal file
51
src/pages/Tariffs/components/PaymentConfirmationModal.tsx
Normal file
@ -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 (
|
||||||
|
<Modal open={open} onClose={onClose}>
|
||||||
|
<Paper
|
||||||
|
sx={{
|
||||||
|
position: "absolute" as "absolute",
|
||||||
|
top: "50%",
|
||||||
|
left: "50%",
|
||||||
|
transform: "translate(-50%, -50%)",
|
||||||
|
boxShadow: 24,
|
||||||
|
p: 4,
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
flexDirection: "column",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
id="modal-modal-title"
|
||||||
|
variant="h6"
|
||||||
|
component="h2"
|
||||||
|
mb="20px"
|
||||||
|
>
|
||||||
|
Вы подтверждаете платёж в сумму{" "}
|
||||||
|
{price ? price.toFixed(2) : 0} ₽
|
||||||
|
</Typography>
|
||||||
|
<Button variant="contained" onClick={onConfirm}>
|
||||||
|
купить
|
||||||
|
</Button>
|
||||||
|
</Paper>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
83
src/pages/Tariffs/components/TariffsHeader.tsx
Normal file
83
src/pages/Tariffs/components/TariffsHeader.tsx
Normal file
@ -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 (
|
||||||
|
<Container
|
||||||
|
component="nav"
|
||||||
|
disableGutters
|
||||||
|
maxWidth={false}
|
||||||
|
sx={{
|
||||||
|
px: "16px",
|
||||||
|
display: "flex",
|
||||||
|
height: "80px",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: isMobile ? "7px" : isTablet ? "20px" : "60px",
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
bgcolor: "white",
|
||||||
|
borderBottom: "1px solid #E3E3E3",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Link to="/">
|
||||||
|
<Logotip width={124} />
|
||||||
|
</Link>
|
||||||
|
<IconButton onClick={() => navigate("/list")}>
|
||||||
|
<ArrowLeft color="black" />
|
||||||
|
</IconButton>
|
||||||
|
<Box sx={{ display: "flex", ml: "auto" }}>
|
||||||
|
<Box sx={{ whiteSpace: "nowrap" }}>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "12px",
|
||||||
|
lineHeight: "14px",
|
||||||
|
color: "gray",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Мой баланс
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
color={"#7e2aea"}
|
||||||
|
fontSize={
|
||||||
|
isMobile ? (cashString.length > 9 ? "13px" : "16px") : "16px"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{cashString}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<LogoutButton
|
||||||
|
onClick={() => {
|
||||||
|
navigate("/");
|
||||||
|
handleLogoutClick();
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
ml: "20px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
@ -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 <Box
|
|
||||||
sx={{
|
|
||||||
justifyContent: "left",
|
|
||||||
display: "grid",
|
|
||||||
gap: "40px",
|
|
||||||
gridTemplateColumns: `repeat(auto-fit, minmax(300px, ${isTablet ? "436px" : "360px"
|
|
||||||
}))`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{createTariffElements(
|
|
||||||
filteredBadgeTariffs,
|
|
||||||
false,
|
|
||||||
user,
|
|
||||||
discounts,
|
|
||||||
openModalHC,
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
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 <Box
|
|
||||||
sx={{
|
|
||||||
justifyContent: "left",
|
|
||||||
display: "grid",
|
|
||||||
gap: "40px",
|
|
||||||
gridTemplateColumns: `repeat(auto-fit, minmax(300px, ${isTablet ? "436px" : "360px"
|
|
||||||
}))`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{createTariffElements(
|
|
||||||
filteredCreateTariffs,
|
|
||||||
false,
|
|
||||||
user,
|
|
||||||
discounts,
|
|
||||||
openModalHC,
|
|
||||||
sendRequest,
|
|
||||||
true,
|
|
||||||
<SmallIconPena/>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
default:
|
|
||||||
return <Box
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
flexWrap: "wrap",
|
|
||||||
width: "100%"
|
|
||||||
}}>
|
|
||||||
{content.map(data => <NavCard {...data} key={data.title} />)}
|
|
||||||
</Box>
|
|
||||||
}
|
|
||||||
}
|
|
@ -17,10 +17,6 @@ export const createTariffElements = (
|
|||||||
cc?: boolean,
|
cc?: boolean,
|
||||||
icon?: ReactNode
|
icon?: ReactNode
|
||||||
) => {
|
) => {
|
||||||
console.log("start work createTariffElements")
|
|
||||||
console.log("filteredTariffs ", filteredTariffs)
|
|
||||||
console.log("user ", user)
|
|
||||||
console.log("user.isUserNko, ", user.isUserNko)
|
|
||||||
const tariffElements = filteredTariffs
|
const tariffElements = filteredTariffs
|
||||||
.filter((tariff) => tariff.privileges.length > 0)
|
.filter((tariff) => tariff.privileges.length > 0)
|
||||||
.map((tariff, index) => {
|
.map((tariff, index) => {
|
||||||
|
@ -1 +1 @@
|
|||||||
type TypePages = "count" | "day" | "dop" | "hide" | "create"
|
type TypePages = "count" | "day" | "dop" | "hide" | "create" | "premium" | "analytics" | "custom"
|
46
src/pages/Tariffs/utils.ts
Normal file
46
src/pages/Tariffs/utils.ts
Normal file
@ -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));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
@ -18,7 +18,7 @@ import { object, string } from "yup";
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useUserStore } from "@root/user";
|
import { useUserStore } from "@root/user";
|
||||||
|
|
||||||
import { makeRequest } from "@api/makeRequest";
|
import { makeRequest } from "@frontend/kitui";
|
||||||
import { setAuthToken } from "@frontend/kitui";
|
import { setAuthToken } from "@frontend/kitui";
|
||||||
import { parseAxiosError } from "@utils/parse-error";
|
import { parseAxiosError } from "@utils/parse-error";
|
||||||
import { recoverUser } from "@api/user";
|
import { recoverUser } from "@api/user";
|
||||||
|
@ -242,7 +242,7 @@ export default function SignupDialog() {
|
|||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
component={RouterLink}
|
component={RouterLink}
|
||||||
to="/restore"
|
to="/recover"
|
||||||
state={{ backgroundLocation: location.state.backgroundLocation }}
|
state={{ backgroundLocation: location.state.backgroundLocation }}
|
||||||
sx={{ color: "#7E2AEA" }}
|
sx={{ color: "#7E2AEA" }}
|
||||||
>
|
>
|
||||||
|
@ -53,8 +53,6 @@ export default function AvailablePrivilege() {
|
|||||||
}
|
}
|
||||||
const quizUnlimDays = getCramps(quizUnlimTime, userPrivileges?.quizUnlimTime?.created_at || "");
|
const quizUnlimDays = getCramps(quizUnlimTime, userPrivileges?.quizUnlimTime?.created_at || "");
|
||||||
const squizBadgeDays = getCramps(squizHideBadge, userPrivileges?.squizHideBadge?.created_at || "");
|
const squizBadgeDays = getCramps(squizHideBadge, userPrivileges?.squizHideBadge?.created_at || "");
|
||||||
console.log(userPrivileges)
|
|
||||||
console.log(quizUnlimTime)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
|
@ -6,8 +6,8 @@ import MoreHorizIcon from "@mui/icons-material/MoreHoriz";
|
|||||||
import { Box, Button, IconButton, Popover, Typography, useMediaQuery, useTheme } from "@mui/material";
|
import { Box, Button, IconButton, Popover, Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||||
import { deleteQuiz, setEditQuizId } from "@root/quizes/actions";
|
import { deleteQuiz, setEditQuizId } from "@root/quizes/actions";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import { inCart } from "../../pages/Tariffs/Tariffs";
|
import { inCart } from "../../pages/Tariffs/utils";
|
||||||
import { makeRequest } from "@api/makeRequest";
|
import { makeRequest } from "@frontend/kitui";
|
||||||
import { enqueueSnackbar } from "notistack";
|
import { enqueueSnackbar } from "notistack";
|
||||||
import { useDomainDefine } from "@utils/hooks/useDomainDefine";
|
import { useDomainDefine } from "@utils/hooks/useDomainDefine";
|
||||||
import CopyIcon from "@icons/CopyIcon";
|
import CopyIcon from "@icons/CopyIcon";
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { FetchState, TicketMessage } from "@frontend/kitui";
|
import { FetchState, Ticket, TicketMessage } from "@frontend/kitui";
|
||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { createJSONStorage, devtools, persist } from "zustand/middleware";
|
import { createJSONStorage, devtools, persist } from "zustand/middleware";
|
||||||
import { useUserStore } from "./user";
|
import { useUserStore } from "./user";
|
||||||
@ -21,11 +21,12 @@ interface AuthData {
|
|||||||
interface TicketStore {
|
interface TicketStore {
|
||||||
unauthData: AuthData;
|
unauthData: AuthData;
|
||||||
authData: AuthData;
|
authData: AuthData;
|
||||||
|
tickets: Ticket[];
|
||||||
}
|
}
|
||||||
|
|
||||||
let params = new URLSearchParams(document.location.search);
|
let params = new URLSearchParams(document.location.search);
|
||||||
|
|
||||||
const debug = params.get("debug");
|
|
||||||
const initAuthData = {
|
const initAuthData = {
|
||||||
sessionData: null,
|
sessionData: null,
|
||||||
isMessageSending: false,
|
isMessageSending: false,
|
||||||
@ -35,10 +36,12 @@ const initAuthData = {
|
|||||||
lastMessageId: undefined,
|
lastMessageId: undefined,
|
||||||
isPreventAutoscroll: false,
|
isPreventAutoscroll: false,
|
||||||
unauthTicketMessageFetchState: "idle" as FetchState,
|
unauthTicketMessageFetchState: "idle" as FetchState,
|
||||||
|
tickets: []
|
||||||
};
|
};
|
||||||
const initState = {
|
const initState = {
|
||||||
unauthData: initAuthData,
|
unauthData: initAuthData,
|
||||||
authData: initAuthData,
|
authData: initAuthData,
|
||||||
|
tickets: []
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useTicketStore = create<TicketStore>()(
|
export const useTicketStore = create<TicketStore>()(
|
||||||
@ -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?.at(-1)?.id || "";
|
||||||
if (Array.isArray(sortedMessages)) ticket.lastMessageId = sortedMessages?.[sortedMessages.length - 1]?.id || "";
|
if (Array.isArray(sortedMessages)) ticket.lastMessageId = sortedMessages?.[sortedMessages.length - 1]?.id || "";
|
||||||
console.log("ticketStudy, ", sortedMessages)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const incrementUnauthMessage = () =>
|
export const incrementUnauthMessage = () =>
|
||||||
@ -156,9 +158,17 @@ export const updateTicket = <T extends AuthData>(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
function setProducedState<A extends string | { type: unknown }>(
|
function setProducedState<A extends string | { type: string }>(
|
||||||
recipe: (state: TicketStore) => void,
|
recipe: (state: TicketStore) => void,
|
||||||
action?: A,
|
action?: A,
|
||||||
) {
|
) {
|
||||||
useTicketStore.setState((state) => produce(state, recipe), false, action);
|
useTicketStore.setState((state) => produce(state, recipe), false, action);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Функция для записи тикетов в стор
|
||||||
|
export const setTickets = (tickets: Ticket[] | null) => {
|
||||||
|
useTicketStore.setState((state) => ({
|
||||||
|
...state,
|
||||||
|
tickets: tickets || []
|
||||||
|
}), false);
|
||||||
|
};
|
||||||
|
@ -71,5 +71,6 @@ export const clearUserData = () => useUserStore.setState({ ...initialState });
|
|||||||
export const setUserAccount = (userAccount: OriginalUserAccount) =>
|
export const setUserAccount = (userAccount: OriginalUserAccount) =>
|
||||||
useUserStore.setState({ userAccount });
|
useUserStore.setState({ userAccount });
|
||||||
|
|
||||||
export const setCustomerAccount = (customerAccount: UserAccount) =>
|
export const setCustomerAccount = (customerAccount: UserAccount) => {
|
||||||
useUserStore.setState({ customerAccount });
|
useUserStore.setState({ customerAccount });
|
||||||
|
};
|
||||||
|
@ -3,7 +3,7 @@ import { Box, Button, Modal, Typography } from "@mui/material";
|
|||||||
import { enqueueSnackbar } from "notistack";
|
import { enqueueSnackbar } from "notistack";
|
||||||
import { mutate } from "swr";
|
import { mutate } from "swr";
|
||||||
|
|
||||||
import { makeRequest } from "@api/makeRequest";
|
import { makeRequest } from "@frontend/kitui";
|
||||||
import { getDiscounts } from "@api/discounts";
|
import { getDiscounts } from "@api/discounts";
|
||||||
|
|
||||||
import { clearUserData, OriginalUserAccount, setUserAccount, useUserStore } from "@root/user";
|
import { clearUserData, OriginalUserAccount, setUserAccount, useUserStore } from "@root/user";
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
FormControl,
|
|
||||||
IconButton,
|
IconButton,
|
||||||
InputAdornment,
|
|
||||||
InputBase,
|
|
||||||
SxProps,
|
SxProps,
|
||||||
Theme,
|
Theme,
|
||||||
Typography,
|
Typography,
|
||||||
@ -17,46 +14,45 @@ import {
|
|||||||
useTicketStore,
|
useTicketStore,
|
||||||
} from "@root/ticket";
|
} from "@root/ticket";
|
||||||
import type { TouchEvent, WheelEvent } from "react";
|
import type { TouchEvent, WheelEvent } from "react";
|
||||||
import * as React from "react";
|
import { useEffect, useMemo, useRef } from "react";
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import ChatMessageRenderer from "./ChatMessageRenderer";
|
||||||
import ChatMessage from "./ChatMessage";
|
import ChatInput from "./ChatInput";
|
||||||
import ChatVideo from "./ChatVideo";
|
|
||||||
import SendIcon from "@icons/SendIcon";
|
|
||||||
import UserCircleIcon from "./UserCircleIcon";
|
import UserCircleIcon from "./UserCircleIcon";
|
||||||
import { throttle, TicketMessage } from "@frontend/kitui";
|
import { throttle, TicketMessage } from "@frontend/kitui";
|
||||||
import ArrowLeft from "@icons/questionsPage/arrowLeft";
|
import ArrowLeft from "@icons/questionsPage/arrowLeft";
|
||||||
import { useUserStore } from "@root/user";
|
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 {
|
interface Props {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
sx?: SxProps<Theme>;
|
sx?: SxProps<Theme>;
|
||||||
onclickArrow?: () => void;
|
onclickArrow?: () => void;
|
||||||
sendMessage: (a: string) => Promise<boolean>;
|
sendMessage: (a: string) => Promise<boolean>;
|
||||||
sendFile: (a: File | undefined) => Promise<true>;
|
sendFile: (a: File | undefined) => Promise<void>;
|
||||||
greetingMessage: TicketMessage;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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({
|
export default function Chat({
|
||||||
open = false,
|
open = false,
|
||||||
sx,
|
sx,
|
||||||
onclickArrow,
|
onclickArrow,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
sendFile,
|
sendFile,
|
||||||
greetingMessage,
|
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down(800));
|
const isMobile = useMediaQuery(theme.breakpoints.down(800));
|
||||||
const [messageField, setMessageField] = useState<string>("");
|
|
||||||
const [disableFileButton, setDisableFileButton] = useState(false);
|
|
||||||
|
|
||||||
const user = useUserStore((state) => state.user?._id);
|
const user = useUserStore((state) => state.user?._id);
|
||||||
const ticket = useTicketStore(
|
const ticket = useTicketStore(
|
||||||
@ -72,31 +68,11 @@ export default function Chat({
|
|||||||
const chatBoxRef = useRef<HTMLDivElement>(null);
|
const chatBoxRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
addOrUpdateUnauthMessages([greetingMessage]);
|
|
||||||
if (open) {
|
if (open) {
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
}
|
}
|
||||||
}, [open]);
|
}, [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<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
const throttledScrollHandler = useMemo(
|
const throttledScrollHandler = useMemo(
|
||||||
() =>
|
() =>
|
||||||
throttle(() => {
|
throttle(() => {
|
||||||
@ -152,14 +128,6 @@ export default function Chat({
|
|||||||
behavior,
|
behavior,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const handleTextfieldKeyPress: React.KeyboardEventHandler<
|
|
||||||
HTMLInputElement | HTMLTextAreaElement
|
|
||||||
> = (e) => {
|
|
||||||
if (e.key === "Enter" && !e.shiftKey) {
|
|
||||||
e.preventDefault();
|
|
||||||
sendMessageHC();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -240,164 +208,28 @@ export default function Chat({
|
|||||||
>
|
>
|
||||||
{ticket.sessionData?.ticketId &&
|
{ticket.sessionData?.ticketId &&
|
||||||
messages.map((message) => {
|
messages.map((message) => {
|
||||||
const isFileVideo = () => {
|
const isSelf = (ticket.sessionData?.sessionId || user) === message.user_id;
|
||||||
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 (
|
|
||||||
<ChatImage
|
|
||||||
unAuthenticated
|
|
||||||
key={message.id}
|
|
||||||
file={message.files[0]}
|
|
||||||
createdAt={message.created_at}
|
|
||||||
isSelf={
|
|
||||||
(ticket.sessionData?.sessionId || user) ===
|
|
||||||
message.user_id
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (message.files.length > 0 && isFileVideo()) {
|
|
||||||
return (
|
|
||||||
<ChatVideo
|
|
||||||
unAuthenticated
|
|
||||||
key={message.id}
|
|
||||||
file={message.files[0]}
|
|
||||||
createdAt={message.created_at}
|
|
||||||
isSelf={
|
|
||||||
(ticket.sessionData?.sessionId || user) ===
|
|
||||||
message.user_id
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (message.files.length > 0 && isFileDocument()) {
|
|
||||||
return (
|
|
||||||
<ChatDocument
|
|
||||||
unAuthenticated
|
|
||||||
key={message.id}
|
|
||||||
file={message.files[0]}
|
|
||||||
createdAt={message.created_at}
|
|
||||||
isSelf={
|
|
||||||
(ticket.sessionData?.sessionId || user) ===
|
|
||||||
message.user_id
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<ChatMessage
|
<ChatMessageRenderer
|
||||||
unAuthenticated
|
|
||||||
key={message.id}
|
key={message.id}
|
||||||
text={message.message}
|
message={message}
|
||||||
createdAt={message.created_at}
|
isSelf={isSelf}
|
||||||
isSelf={
|
|
||||||
(ticket.sessionData?.sessionId || user) ===
|
|
||||||
message.user_id
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{!ticket.sessionData?.ticketId && (
|
{!ticket.sessionData?.ticketId && (
|
||||||
<ChatMessage
|
<ChatMessageRenderer
|
||||||
unAuthenticated
|
message={greetingMessage}
|
||||||
text={greetingMessage.message}
|
isSelf={false}
|
||||||
createdAt={greetingMessage.created_at}
|
|
||||||
isSelf={
|
|
||||||
(ticket.sessionData?.sessionId || user) ===
|
|
||||||
greetingMessage.user_id
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<FormControl fullWidth sx={{ borderTop: "1px solid black" }}>
|
<ChatInput
|
||||||
<InputBase
|
sendMessage={sendMessage}
|
||||||
value={messageField}
|
sendFile={sendFile}
|
||||||
fullWidth
|
isMessageSending={isMessageSending}
|
||||||
placeholder="Введите сообщение..."
|
/>
|
||||||
id="message"
|
|
||||||
multiline
|
|
||||||
onKeyDown={handleTextfieldKeyPress}
|
|
||||||
sx={{
|
|
||||||
width: "100%",
|
|
||||||
p: 0,
|
|
||||||
}}
|
|
||||||
inputProps={{
|
|
||||||
sx: {
|
|
||||||
fontWeight: 400,
|
|
||||||
fontSize: "16px",
|
|
||||||
lineHeight: "19px",
|
|
||||||
pt: upMd ? "30px" : "28px",
|
|
||||||
pb: upMd ? "30px" : "24px",
|
|
||||||
px: "19px",
|
|
||||||
maxHeight: "calc(19px * 5)",
|
|
||||||
color: "black",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
onChange={(e) => setMessageField(e.target.value)}
|
|
||||||
endAdornment={
|
|
||||||
<InputAdornment position="end">
|
|
||||||
<IconButton
|
|
||||||
disabled={disableFileButton}
|
|
||||||
onClick={() => {
|
|
||||||
if (!disableFileButton) fileInputRef.current?.click();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<AttachFileIcon />
|
|
||||||
</IconButton>
|
|
||||||
<input
|
|
||||||
ref={fileInputRef}
|
|
||||||
id="fileinput"
|
|
||||||
onChange={(e) => {
|
|
||||||
if (e.target.files?.[0])
|
|
||||||
sendFileHC(e.target.files?.[0]);
|
|
||||||
}}
|
|
||||||
style={{ display: "none" }}
|
|
||||||
type="file"
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
disabled={isMessageSending}
|
|
||||||
onClick={sendMessageHC}
|
|
||||||
sx={{
|
|
||||||
height: "53px",
|
|
||||||
width: "53px",
|
|
||||||
mr: "13px",
|
|
||||||
p: 0,
|
|
||||||
opacity: isMessageSending ? 0.3 : 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SendIcon
|
|
||||||
style={{
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</IconButton>
|
|
||||||
</InputAdornment>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
137
src/ui_kit/FloatingSupportChat/ChatInput.tsx
Normal file
137
src/ui_kit/FloatingSupportChat/ChatInput.tsx
Normal file
@ -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<boolean>;
|
||||||
|
sendFile: (file: File | undefined) => Promise<void>;
|
||||||
|
isMessageSending: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChatInput = ({ sendMessage, sendFile, isMessageSending }: ChatInputProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||||
|
const [messageField, setMessageField] = useState<string>("");
|
||||||
|
const [disableFileButton, setDisableFileButton] = useState(false);
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(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<HTMLInputElement>) => {
|
||||||
|
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<HTMLInputElement>) => {
|
||||||
|
setMessageField(e.target.value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormControl fullWidth sx={{ borderTop: "1px solid black" }}>
|
||||||
|
<InputBase
|
||||||
|
value={messageField}
|
||||||
|
fullWidth
|
||||||
|
placeholder="Введите сообщение..."
|
||||||
|
id="message"
|
||||||
|
multiline
|
||||||
|
onKeyDown={handleTextfieldKeyPress}
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
p: 0,
|
||||||
|
}}
|
||||||
|
inputProps={{
|
||||||
|
sx: {
|
||||||
|
fontWeight: 400,
|
||||||
|
fontSize: "16px",
|
||||||
|
lineHeight: "19px",
|
||||||
|
pt: upMd ? "30px" : "28px",
|
||||||
|
pb: upMd ? "30px" : "24px",
|
||||||
|
px: "19px",
|
||||||
|
maxHeight: "calc(19px * 5)",
|
||||||
|
color: "black",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
endAdornment={
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<IconButton
|
||||||
|
disabled={disableFileButton}
|
||||||
|
onClick={handleFileButtonClick}
|
||||||
|
>
|
||||||
|
<AttachFileIcon />
|
||||||
|
</IconButton>
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
id="fileinput"
|
||||||
|
onChange={handleFileInputChange}
|
||||||
|
style={{ display: "none" }}
|
||||||
|
type="file"
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
disabled={isMessageSending}
|
||||||
|
onClick={handleSendMessage}
|
||||||
|
sx={{
|
||||||
|
height: "53px",
|
||||||
|
width: "53px",
|
||||||
|
mr: "13px",
|
||||||
|
p: 0,
|
||||||
|
opacity: isMessageSending ? 0.3 : 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SendIcon
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
</InputAdornment>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChatInput;
|
70
src/ui_kit/FloatingSupportChat/ChatMessageRenderer.tsx
Normal file
70
src/ui_kit/FloatingSupportChat/ChatMessageRenderer.tsx
Normal file
@ -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 <ChatImage {...commonProps} />;
|
||||||
|
case 'video':
|
||||||
|
return <ChatVideo {...commonProps} />;
|
||||||
|
case 'document':
|
||||||
|
return <ChatDocument {...commonProps} />;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Текстовое сообщение
|
||||||
|
return (
|
||||||
|
<ChatMessage
|
||||||
|
unAuthenticated
|
||||||
|
text={message.message}
|
||||||
|
createdAt={message.created_at}
|
||||||
|
isSelf={isSelf}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
ChatMessageRenderer.displayName = 'ChatMessageRenderer';
|
||||||
|
|
||||||
|
export default ChatMessageRenderer;
|
@ -1,5 +1,5 @@
|
|||||||
import type { ReactNode, Ref } from "react";
|
import type { ReactNode, Ref } from "react";
|
||||||
import { forwardRef, useEffect, useState } from "react";
|
import { forwardRef, useEffect, useState, useMemo } from "react";
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Box,
|
Box,
|
||||||
@ -44,10 +44,9 @@ interface Props {
|
|||||||
handleChatClickClose: () => void;
|
handleChatClickClose: () => void;
|
||||||
handleChatClickSwitch: () => void;
|
handleChatClickSwitch: () => void;
|
||||||
sendMessage: (a: string) => Promise<boolean>;
|
sendMessage: (a: string) => Promise<boolean>;
|
||||||
sendFile: (a: File | undefined) => Promise<true>;
|
sendFile: (a: File | undefined) => Promise<void>;
|
||||||
modalWarningType: string | null;
|
modalWarningType: string | null;
|
||||||
setModalWarningType: any;
|
setModalWarningType: any;
|
||||||
greetingMessage: TicketMessage;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function FloatingSupportChat({
|
export default function FloatingSupportChat({
|
||||||
@ -59,7 +58,6 @@ export default function FloatingSupportChat({
|
|||||||
sendFile,
|
sendFile,
|
||||||
modalWarningType,
|
modalWarningType,
|
||||||
setModalWarningType,
|
setModalWarningType,
|
||||||
greetingMessage,
|
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const [monitorType, setMonitorType] = useState<"desktop" | "mobile" | "">("");
|
const [monitorType, setMonitorType] = useState<"desktop" | "mobile" | "">("");
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
@ -72,6 +70,48 @@ export default function FloatingSupportChat({
|
|||||||
(state) => state[user ? "authData" : "unauthData"],
|
(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(() => {
|
useEffect(() => {
|
||||||
const onResize = () => {
|
const onResize = () => {
|
||||||
if (document.fullscreenElement) {
|
if (document.fullscreenElement) {
|
||||||
@ -108,7 +148,6 @@ export default function FloatingSupportChat({
|
|||||||
sx={{ alignSelf: "start", width: "clamp(200px, 100%, 400px)" }}
|
sx={{ alignSelf: "start", width: "clamp(200px, 100%, 400px)" }}
|
||||||
sendMessage={sendMessage}
|
sendMessage={sendMessage}
|
||||||
sendFile={sendFile}
|
sendFile={sendFile}
|
||||||
greetingMessage={greetingMessage}
|
|
||||||
/>
|
/>
|
||||||
<Dialog
|
<Dialog
|
||||||
fullScreen
|
fullScreen
|
||||||
@ -121,7 +160,6 @@ export default function FloatingSupportChat({
|
|||||||
onclickArrow={handleChatClickClose}
|
onclickArrow={handleChatClickClose}
|
||||||
sendMessage={sendMessage}
|
sendMessage={sendMessage}
|
||||||
sendFile={sendFile}
|
sendFile={sendFile}
|
||||||
greetingMessage={greetingMessage}
|
|
||||||
/>
|
/>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
<Fab
|
<Fab
|
||||||
@ -162,9 +200,7 @@ export default function FloatingSupportChat({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Badge
|
<Badge
|
||||||
badgeContent={
|
badgeContent={unreadCount}
|
||||||
messages.filter(({ shown }) => shown?.me !== 1).length || 0
|
|
||||||
}
|
|
||||||
sx={{
|
sx={{
|
||||||
"& .MuiBadge-badge": {
|
"& .MuiBadge-badge": {
|
||||||
display: isChatOpened ? "none" : "flex",
|
display: isChatOpened ? "none" : "flex",
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
TicketMessage,
|
TicketMessage,
|
||||||
|
createTicket,
|
||||||
|
shownMessage,
|
||||||
useSSESubscription,
|
useSSESubscription,
|
||||||
useTicketMessages,
|
useTicketMessages,
|
||||||
useTicketsFetcher,
|
useTicketsFetcher,
|
||||||
|
sendFile as sf
|
||||||
} from "@frontend/kitui";
|
} from "@frontend/kitui";
|
||||||
import FloatingSupportChat from "./FloatingSupportChat";
|
import FloatingSupportChat from "./FloatingSupportChat";
|
||||||
import { useUserStore } from "@root/user";
|
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 { useSSETab } from "../../utils/hooks/useSSETab";
|
||||||
import {
|
import {
|
||||||
addOrUpdateUnauthMessages,
|
addOrUpdateUnauthMessages,
|
||||||
@ -21,7 +23,6 @@ import {
|
|||||||
} from "@root/ticket";
|
} from "@root/ticket";
|
||||||
import { enqueueSnackbar } from "notistack";
|
import { enqueueSnackbar } from "notistack";
|
||||||
import { parseAxiosError } from "@utils/parse-error";
|
import { parseAxiosError } from "@utils/parse-error";
|
||||||
import { createTicket, sendFile as sendFileRequest } from "@api/ticket";
|
|
||||||
import { selectSendingMethod } from "./utils";
|
import { selectSendingMethod } from "./utils";
|
||||||
|
|
||||||
type ModalWarningType =
|
type ModalWarningType =
|
||||||
@ -71,60 +72,6 @@ export default () => {
|
|||||||
setIsChatOpened((state) => !state);
|
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({
|
useTicketMessages({
|
||||||
url: `${process.env.REACT_APP_DOMAIN}/heruvym/v1.0.0/getMessages`,
|
url: `${process.env.REACT_APP_DOMAIN}/heruvym/v1.0.0/getMessages`,
|
||||||
@ -156,7 +103,6 @@ export default () => {
|
|||||||
);
|
);
|
||||||
if (isTicketClosed) {
|
if (isTicketClosed) {
|
||||||
cleanAuthTicketData();
|
cleanAuthTicketData();
|
||||||
addOrUpdateUnauthMessages([getGreetingMessage]);
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
cleanUnauthTicketData();
|
cleanUnauthTicketData();
|
||||||
localStorage.removeItem("unauth-ticket");
|
localStorage.removeItem("unauth-ticket");
|
||||||
@ -184,9 +130,15 @@ export default () => {
|
|||||||
({ shown }) => shown?.me !== 1,
|
({ 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]);
|
}, [isChatOpened, ticket.messages]);
|
||||||
|
|
||||||
@ -196,21 +148,26 @@ export default () => {
|
|||||||
setSseEnabled(true);
|
setSseEnabled(true);
|
||||||
setIsMessageSending(true);
|
setIsMessageSending(true);
|
||||||
|
|
||||||
let successful = await selectSendingMethod({messageField});
|
let successful = await selectSendingMethod({ messageField });
|
||||||
|
|
||||||
setIsMessageSending(false);
|
setIsMessageSending(false);
|
||||||
return successful;
|
return successful;
|
||||||
};
|
};
|
||||||
const sendFile = async (file: File) => {
|
const sendFile = async (file: File | undefined): Promise<void> => {
|
||||||
if (file === undefined) return true;
|
if (file === undefined) return;
|
||||||
|
|
||||||
let ticketId = ticket.sessionData?.ticketId;
|
let ticketId = ticket.sessionData?.ticketId;
|
||||||
if (!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;
|
ticketId = data?.Ticket;
|
||||||
|
|
||||||
if (createError || !data) {
|
if (createError || !data) {
|
||||||
enqueueSnackbar(createError);
|
enqueueSnackbar(`Не удалось создать диалог ${parseAxiosError(createError)}`);
|
||||||
|
return;
|
||||||
} else {
|
} else {
|
||||||
setTicketData({ ticketId: data.Ticket, sessionId: data.sess });
|
setTicketData({ ticketId: data.Ticket, sessionId: data.sess });
|
||||||
}
|
}
|
||||||
@ -219,15 +176,16 @@ export default () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (ticketId !== undefined) {
|
if (ticketId !== undefined) {
|
||||||
if (file.size > MAX_FILE_SIZE) return setModalWarningType("errorSize");
|
if (file.size > MAX_FILE_SIZE) {
|
||||||
|
setModalWarningType("errorSize");
|
||||||
const [_, sendFileError] = await sendFileRequest(ticketId, file);
|
return;
|
||||||
|
|
||||||
if (sendFileError) {
|
|
||||||
enqueueSnackbar(sendFileError);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
const [_, sendFileError] = await sf({ticketId, file});
|
||||||
|
|
||||||
|
if (sendFileError) {
|
||||||
|
enqueueSnackbar(`Не удалось отправить файл ${parseAxiosError(sendFileError)}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -241,7 +199,6 @@ export default () => {
|
|||||||
sendFile={sendFile}
|
sendFile={sendFile}
|
||||||
modalWarningType={modalWarningType}
|
modalWarningType={modalWarningType}
|
||||||
setModalWarningType={setModalWarningType}
|
setModalWarningType={setModalWarningType}
|
||||||
greetingMessage={getGreetingMessage}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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<TicketMessage[]>(
|
|
||||||
"ticket",
|
|
||||||
addOrUpdateUnauthMessages,
|
|
||||||
);
|
|
||||||
|
|
||||||
const [modalWarningType, setModalWarningType] =
|
|
||||||
useState<ModalWarningType>(null);
|
|
||||||
const [isChatOpened, setIsChatOpened] = useState<boolean>(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<TicketMessage>({
|
|
||||||
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
|
|
||||||
};
|
|
||||||
};
|
|
@ -1,55 +1,51 @@
|
|||||||
import { sendTicketMessage } from "@/api/ticket";
|
|
||||||
import { setTicketData, useTicketStore } from "@/stores/ticket";
|
import { setTicketData, useTicketStore } from "@/stores/ticket";
|
||||||
import { useUserStore } from "@root/user";
|
import { useUserStore } from "@root/user";
|
||||||
import { createTicket, sendFile as sendFileRequest } from "@api/ticket";
|
|
||||||
import { enqueueSnackbar } from "notistack";
|
import { enqueueSnackbar } from "notistack";
|
||||||
|
import { createTicket, sendTicketMessage } from "@frontend/kitui";
|
||||||
|
import { parseAxiosError } from "@/utils/parse-error";
|
||||||
|
|
||||||
interface SelectSendingMethod {
|
interface SelectSendingMethod {
|
||||||
messageField: string;
|
messageField: string;
|
||||||
isSnackbar?: boolean;
|
isSnackbar?: boolean;
|
||||||
systemError?: boolean;
|
systemError?: boolean;
|
||||||
|
|
||||||
}
|
}
|
||||||
export const selectSendingMethod = async ({messageField, isSnackbar = true, systemError = false}: SelectSendingMethod) => {
|
export const selectSendingMethod = async ({ messageField, isSnackbar = true, systemError = false }: SelectSendingMethod) => {
|
||||||
console.log("click")
|
|
||||||
const user = useUserStore.getState().user?._id;
|
const user = useUserStore.getState().user?._id;
|
||||||
const ticket = useTicketStore.getState()[user ? "authData" : "unauthData"];
|
const ticket = useTicketStore.getState()[user ? "authData" : "unauthData"];
|
||||||
console.log(ticket)
|
|
||||||
|
|
||||||
console.log("click 2")
|
|
||||||
let successful = false;
|
let successful = false;
|
||||||
if (!(window.location.hostname == 'localhost' && systemError )) { //предупреждать о системных ошибках вне локалхост
|
if (!(window.location.hostname == 'localhost' && systemError)) { //предупреждать о системных ошибках вне локалхост
|
||||||
if (!ticket.sessionData?.ticketId) {
|
if (!ticket.sessionData?.ticketId) {
|
||||||
console.log("autorisated 2")
|
const [data, createError] = await createTicket({
|
||||||
const [data, createError] = await createTicket(
|
message: messageField,
|
||||||
messageField,
|
useToken: Boolean(user),
|
||||||
Boolean(user),
|
systemError: false,
|
||||||
false,
|
});
|
||||||
);
|
|
||||||
|
|
||||||
if (createError || !data) {
|
if (createError || !data) {
|
||||||
successful = false;
|
successful = false;
|
||||||
|
|
||||||
if (isSnackbar) enqueueSnackbar(createError);
|
|
||||||
} else {
|
|
||||||
successful = true;
|
|
||||||
|
|
||||||
setTicketData({ ticketId: data.Ticket, sessionId: data.sess });
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (isSnackbar) enqueueSnackbar(`Не удалось открыть диалог ${parseAxiosError(createError)}`);
|
||||||
} else {
|
} else {
|
||||||
const [_, sendTicketMessageError] = await sendTicketMessage(
|
|
||||||
ticket.sessionData?.ticketId,
|
|
||||||
messageField,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
successful = true;
|
successful = true;
|
||||||
|
|
||||||
if (sendTicketMessageError) {
|
setTicketData({ ticketId: data.Ticket, sessionId: data.sess });
|
||||||
successful = false;
|
|
||||||
if (isSnackbar) enqueueSnackbar(sendTicketMessageError);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
} 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;
|
return successful;
|
||||||
}
|
}
|
@ -29,9 +29,6 @@ export const NavigationPanel: FC<Props> = ({
|
|||||||
const isMobile = useMediaQuery(theme.breakpoints.down(786));
|
const isMobile = useMediaQuery(theme.breakpoints.down(786));
|
||||||
const lastStep = currentStep + 1 === totalSteps;
|
const lastStep = currentStep + 1 === totalSteps;
|
||||||
|
|
||||||
console.log("nextStepName")
|
|
||||||
console.log(nextStepName)
|
|
||||||
|
|
||||||
const handlePrevStep = () => {
|
const handlePrevStep = () => {
|
||||||
if (currentStep === 0) return;
|
if (currentStep === 0) return;
|
||||||
setCurrentStep(currentStep - 1);
|
setCurrentStep(currentStep - 1);
|
||||||
|
@ -46,8 +46,6 @@ export default function WorkSpace({
|
|||||||
modalModels[currentStepName]
|
modalModels[currentStepName]
|
||||||
), [currentStepName]);
|
), [currentStepName]);
|
||||||
|
|
||||||
// console.log(" промежуточный рендер которому должно быть похуй")
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box
|
<Box
|
||||||
|
@ -13,7 +13,6 @@ export const SwitchAI = () => {
|
|||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const quiz = useCurrentQuiz();
|
const quiz = useCurrentQuiz();
|
||||||
const account = useUserStore()
|
const account = useUserStore()
|
||||||
console.log(account.userId)
|
|
||||||
if (account.userId === "6755b1ddd5802e9f13663f56") {
|
if (account.userId === "6755b1ddd5802e9f13663f56") {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
42
src/utils/generateHubWalletRequest.ts
Normal file
42
src/utils/generateHubWalletRequest.ts
Normal file
@ -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;
|
||||||
|
}
|
@ -1,5 +1,8 @@
|
|||||||
import { selectSendingMethod } from "@/ui_kit/FloatingSupportChat/utils";
|
|
||||||
import { ErrorInfo } from "react";
|
import { ErrorInfo } from "react";
|
||||||
|
import { Ticket, createTicket, getAuthToken, sendTicketMessage } from "..";
|
||||||
|
|
||||||
|
let errorsQueue: ComponentError[] = [];
|
||||||
|
let timeoutId: ReturnType<typeof setTimeout>;
|
||||||
|
|
||||||
interface ComponentError {
|
interface ComponentError {
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
@ -8,40 +11,99 @@ interface ComponentError {
|
|||||||
componentStack: string | null | undefined;
|
componentStack: string | null | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleComponentError(error: Error, info: ErrorInfo) {
|
function isErrorReportingAllowed(error?: Error): boolean {
|
||||||
const componentError: ComponentError = {
|
// Если ошибка помечена как 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),
|
timestamp: Math.floor(Date.now() / 1000),
|
||||||
message: error.message,
|
message: error.message,
|
||||||
callStack: error.stack,
|
callStack: error.stack,
|
||||||
componentStack: info.componentStack,
|
componentStack: info.componentStack,
|
||||||
|
...(error && (error as any).__forceSend ? { __forceSend: true } : {})
|
||||||
};
|
};
|
||||||
|
queueErrorRequest(componentError, getTickets);
|
||||||
queueErrorRequest(componentError);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let errorsQueue: ComponentError[] = [];
|
// Ставит ошибку в очередь для отправки, через 1 секунду вызывает sendErrorsToServer
|
||||||
let timeoutId: ReturnType<typeof setTimeout>;
|
export function queueErrorRequest(error: ComponentError, getTickets: () => Ticket[]) {
|
||||||
|
|
||||||
function queueErrorRequest(error: ComponentError) {
|
|
||||||
errorsQueue.push(error);
|
errorsQueue.push(error);
|
||||||
|
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
timeoutId = setTimeout(() => {
|
timeoutId = setTimeout(() => {
|
||||||
sendErrorsToServer();
|
sendErrorsToServer(getTickets);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendErrorsToServer() {
|
// Отправляет накопленные ошибки в тикеты, ищет существующий тикет с system: true или создает новый
|
||||||
// makeRequest({
|
export async function sendErrorsToServer(getTickets: () => Ticket[]) {
|
||||||
// url: "",
|
if (errorsQueue.length === 0) return;
|
||||||
// method: "POST",
|
// Проверяем разрешение на отправку ошибок (по домену и debug-override)
|
||||||
// body: errorsQueue,
|
// Если хотя бы одна ошибка в очереди с __forceSend, отправляем всё
|
||||||
// useToken: true,
|
const forceSend = errorsQueue.some(e => (e as any).__forceSend);
|
||||||
// });
|
if (!forceSend && !isErrorReportingAllowed()) {
|
||||||
// selectSendingMethod({
|
console.log('❌ Отправка ошибок заблокирована, очищаем очередь');
|
||||||
// messageField: `Fake-sending ${errorsQueue.length} errors to server ${JSON.stringify(errorsQueue)}`,
|
errorsQueue = [];
|
||||||
// isSnackbar: false,
|
return;
|
||||||
// systemError: true
|
}
|
||||||
// });
|
const tickets = getTickets();
|
||||||
// errorsQueue = [];
|
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 = [];
|
||||||
}
|
}
|
80
src/utils/hooks/useAuthRedirect.ts
Normal file
80
src/utils/hooks/useAuthRedirect.ts
Normal file
@ -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 };
|
||||||
|
};
|
@ -1,44 +1,42 @@
|
|||||||
import { cartApi } from "@api/cart";
|
import { cartApi } from "@api/cart";
|
||||||
import { useUserStore } from "@/stores/user";
|
import { useUserStore } from "@/stores/user";
|
||||||
import moment from "moment";
|
|
||||||
import { enqueueSnackbar } from "notistack";
|
import { enqueueSnackbar } from "notistack";
|
||||||
import { useEffect } from "react";
|
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 { calcTimeOfReadyPayCart, cancelPayCartProcess, startPayCartProcess, useNotEnoughMoneyAmount } from "@/stores/notEnoughMoneyAmount";
|
||||||
import { startCC } from "@/stores/cc";
|
import { startCC } from "@/stores/cc";
|
||||||
import { setEditQuizId, setCurrentStep } from "@root/quizes/actions";
|
import { setEditQuizId, setCurrentStep } from "@root/quizes/actions";
|
||||||
|
|
||||||
|
/*
|
||||||
|
Есть три пути по которому мы ходили из квиза в хаб. Нам нехватило денег при:
|
||||||
|
1)Покупке обычного тарифа
|
||||||
|
2)Покупке тарифа-заказ-квиза
|
||||||
|
3)Покупке тарифа в настройке квиза в вкладке ИИ
|
||||||
|
*/
|
||||||
|
|
||||||
export const useAfterPay = () => {
|
export const useAfterPay = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
const userId = useUserStore(store => store.userId)
|
const userId = useUserStore(store => store.userId)
|
||||||
const userAccount = useUserStore(state => state.userAccount);
|
const userAccount = useUserStore(state => state.userAccount);
|
||||||
|
const userWithWallet = useUserStore((state) => state.customerAccount); //c wallet
|
||||||
const siteReadyPayCart = useNotEnoughMoneyAmount(state => state.siteReadyPayCart);
|
const siteReadyPayCart = useNotEnoughMoneyAmount(state => state.siteReadyPayCart);
|
||||||
|
|
||||||
const purpose = searchParams.get("purpose");
|
let URLaction = searchParams.get("action");//что мы, собсна, хотим: оплатить, пополнить, заказать квиз
|
||||||
const paymentUserId = searchParams.get("userid");
|
let URLuserId = searchParams.get("userid");//тот кто начал всё это действо
|
||||||
const currentCC = searchParams.get("cc");
|
let URLadditionalinformation = searchParams.get("additionalinformation");//его токен
|
||||||
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]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
//Звёзды сошлись, будем оплачивать корзину
|
|
||||||
if (paymentUserId && paymentUserId === userId) {
|
|
||||||
|
|
||||||
if (purpose === "paycart") {
|
setSearchParams({}, { replace: true });
|
||||||
setSearchParams({}, { replace: true });
|
|
||||||
if (currentCC) startCC()
|
if (userId && URLuserId && userId === URLuserId) {
|
||||||
|
|
||||||
|
if (URLaction === "buy") startPayCartProcess(URLuserId);
|
||||||
|
|
||||||
|
if (URLaction === "createquizcc") {
|
||||||
|
startCC();
|
||||||
(async () => {
|
(async () => {
|
||||||
|
|
||||||
//Проверяем можем ли мы оплатить корзину здесь и сейчас
|
//Проверяем можем ли мы оплатить корзину здесь и сейчас
|
||||||
@ -46,19 +44,46 @@ export const useAfterPay = () => {
|
|||||||
|
|
||||||
if (payCartError) {
|
if (payCartError) {
|
||||||
//Не получилось купить корзину. Ставим флаг, что сайт в состоянии ожидания пополнения счёта для оплаты
|
//Не получилось купить корзину. Ставим флаг, что сайт в состоянии ожидания пополнения счёта для оплаты
|
||||||
startPayCartProcess(paymentUserId)
|
startPayCartProcess(URLuserId);
|
||||||
} else {
|
} else {
|
||||||
if (currentCC) navigate("/tariffs")
|
navigate("/tariffs");
|
||||||
cancelPayCartProcess()
|
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(() => {
|
useEffect(() => {
|
||||||
if (userId !== null && siteReadyPayCart !== null && siteReadyPayCart[userId] !== undefined) {
|
if (userId !== null && siteReadyPayCart !== null && siteReadyPayCart[userId] !== undefined) {
|
||||||
const deadline = siteReadyPayCart[userId]
|
const deadline = siteReadyPayCart[userId];
|
||||||
if (calcTimeOfReadyPayCart(deadline)) {
|
if (calcTimeOfReadyPayCart(deadline)) {
|
||||||
|
|
||||||
//Время ещё не вышло. У нас стоит флаг покупать корзину если время не вышло.
|
//Время ещё не вышло. У нас стоит флаг покупать корзину если время не вышло.
|
||||||
@ -66,11 +91,13 @@ export const useAfterPay = () => {
|
|||||||
const [, payCartError] = await cartApi.pay();
|
const [, payCartError] = await cartApi.pay();
|
||||||
|
|
||||||
if (!payCartError) {
|
if (!payCartError) {
|
||||||
enqueueSnackbar("Товары успешно приобретены")
|
enqueueSnackbar("Товары успешно приобретены");
|
||||||
cancelPayCartProcess()
|
cancelPayCartProcess();
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [userAccount, userId, siteReadyPayCart])
|
}, [userAccount, userId, siteReadyPayCart, userWithWallet])
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
@ -4,7 +4,7 @@ import { useSSETab } from "./useSSETab";
|
|||||||
import { cancelPayCartProcess } from "@/stores/notEnoughMoneyAmount";
|
import { cancelPayCartProcess } from "@/stores/notEnoughMoneyAmount";
|
||||||
import { setCash } from "@/stores/cash";
|
import { setCash } from "@/stores/cash";
|
||||||
import { currencyFormatter } from "@/pages/Tariffs/tariffsUtils/currencyFormatter";
|
import { currencyFormatter } from "@/pages/Tariffs/tariffsUtils/currencyFormatter";
|
||||||
import { inCart } from "@/pages/Tariffs/Tariffs";
|
import { inCart } from "@/pages/Tariffs/utils";
|
||||||
|
|
||||||
type Ping = [{ event: "ping" }]
|
type Ping = [{ event: "ping" }]
|
||||||
|
|
||||||
@ -43,8 +43,6 @@ export const usePipeSubscriber = () => {
|
|||||||
`/customer/v1.0.1/account/pipe?Authorization=${token}`,
|
`/customer/v1.0.1/account/pipe?Authorization=${token}`,
|
||||||
onNewData: (data) => {
|
onNewData: (data) => {
|
||||||
let message = data[0] as PipeMessage
|
let message = data[0] as PipeMessage
|
||||||
console.log("truba")
|
|
||||||
console.log(message)
|
|
||||||
updateSSEValue(message)
|
updateSSEValue(message)
|
||||||
|
|
||||||
//Пропускаем пингование
|
//Пропускаем пингование
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useLayoutEffect, useRef } from "react";
|
import { useEffect, useLayoutEffect, useRef } from "react";
|
||||||
import { createUserAccount, devlog } from "@frontend/kitui";
|
import { createUserAccount, devlog } from "@frontend/kitui";
|
||||||
import { isAxiosError } from "axios";
|
import { isAxiosError } from "axios";
|
||||||
import { makeRequest } from "@api/makeRequest";
|
import { makeRequest } from "@frontend/kitui";
|
||||||
import type { UserAccount } from "@frontend/kitui";
|
import type { UserAccount } from "@frontend/kitui";
|
||||||
import { setUserAccount } from "@/stores/user";
|
import { setUserAccount } from "@/stores/user";
|
||||||
|
|
||||||
@ -37,27 +37,52 @@ export const useUserAccountFetcher = <T = UserAccount>({
|
|||||||
})
|
})
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
devlog("User account", result);
|
devlog("User account", result);
|
||||||
console.log(result)
|
|
||||||
if (result) onNewUserAccountRef.current(result);
|
if (result) onNewUserAccountRef.current(result);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
devlog("Error fetching user account", error);
|
devlog("Error fetching user account", error);
|
||||||
if (error.response?.status === 409) return;
|
if (error.response?.status === 409) return;
|
||||||
if (isAxiosError(error) && error.response?.status === 404) {
|
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) => {
|
.then((result) => {
|
||||||
devlog("Created user account", 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) => {
|
.catch((error) => {
|
||||||
if (error.response?.status === 409) return;
|
if (error.response?.status === 409) return;
|
||||||
devlog("Error creating user account", error);
|
devlog("Error creating user account", error);
|
||||||
|
console.error("useUserAccountFetcher: Error creating account:", error);
|
||||||
onErrorRef.current?.(error);
|
onErrorRef.current?.(error);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.log(error)
|
console.error(error)
|
||||||
onErrorRef.current?.(error);
|
onErrorRef.current?.(error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -29,7 +29,7 @@ export const parseAxiosError = (nativeError: unknown): [string, number?] => {
|
|||||||
if (error.message === "Failed to fetch") return ["Ошибка сети"];
|
if (error.message === "Failed to fetch") return ["Ошибка сети"];
|
||||||
|
|
||||||
//ДЛЯ ОПЛАТЫ ТАРИФА
|
//ДЛЯ ОПЛАТЫ ТАРИФА
|
||||||
if(error.response.status === 402) {
|
if(error.response?.status === 402) {
|
||||||
console.error(error.response?.data.message)
|
console.error(error.response?.data.message)
|
||||||
return 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;
|
const status = error.response.status;
|
||||||
if(status === 409 || status === 401 || status === 404) {
|
if(status === 409 || status === 401 || status === 404) {
|
||||||
const serverErrorMessage = error.response.data.message
|
const responseData = error.response.data as any;
|
||||||
console.log(serverErrorMessage)
|
const serverErrorMessage = responseData?.message || responseData?.error;
|
||||||
const translatedMessage = translateMessage[serverErrorMessage?.toLowerCase() || ""]
|
const translatedMessage = translateMessage[serverErrorMessage?.toLowerCase() || ""]
|
||||||
return [translatedMessage || "", serverError.statusCode];
|
return [translatedMessage || "", serverError.statusCode];
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user