Compare commits

...

61 Commits

Author SHA1 Message Date
d959b706ec systemerror - Error
Some checks failed
Deploy / CreateImage (push) Failing after 42m21s
Deploy / DeployService (push) Failing after 21s
2025-03-19 06:37:35 +03:00
a7b37f0cf7 Merge branch 'clonestaging' into staging
Some checks failed
Deploy / CreateImage (push) Failing after 14m55s
Deploy / DeployService (push) Failing after 23s
2025-03-03 03:54:45 +03:00
ea8d0f8161 update kit
All checks were successful
Deploy / CreateImage (push) Successful in 8m29s
Deploy / DeployService (push) Successful in 23s
2025-02-18 23:46:33 +03:00
8b1e73ea49 гетлист запросам
All checks were successful
Deploy / CreateImage (push) Successful in 10m32s
Deploy / DeployService (push) Successful in 24s
2025-02-16 20:04:01 +03:00
9aad5c0ac3 det new container image in deploymenst compose
All checks were successful
Deploy / CreateImage (push) Successful in 6m21s
Deploy / DeployService (push) Successful in 23s
2025-02-01 17:14:45 +03:00
851af39e18 add deploy workflow
Some checks failed
Deploy / CreateImage (push) Successful in 6m34s
Deploy / DeployService (push) Failing after 24s
2025-02-01 16:31:56 +03:00
9c557dff2a update kit 2025-01-24 12:58:42 +03:00
2c00d1c5dd remove get /privileges 2025-01-19 17:16:23 +03:00
b305c75892 plus getlis with gettariffs 2025-01-12 14:11:19 +03:00
800015f530 сообщение для РС 2024-10-21 21:14:47 +03:00
a5570e29c4 enable service discovery artefacts creation 2024-09-03 22:03:23 +03:00
2614d9c81b Merge branch 'dev' into 'staging'
fix changepwd

See merge request frontend/marketplace!238
2024-09-01 00:41:26 +00:00
80f8d051d4 fix changepwd 2024-09-01 03:40:47 +03:00
5540c2a07d модалка восстановления пароля открывается 2024-08-31 04:39:40 +03:00
1ef531f365 Merge branch 'dev' into 'staging'
translate url verif && remove egrule

See merge request frontend/marketplace!235
2024-08-28 22:25:31 +00:00
5117edfa47 translate url verif && remove egrule 2024-08-29 01:24:38 +03:00
d74e64b180 Merge branch 'dev' into 'staging'
убрать пробелы на _ в pdf верификации

See merge request frontend/marketplace!234
2024-08-28 20:17:41 +00:00
0ad89dd05f убрать пробелы на _ в pdf верификации 2024-08-28 23:16:59 +03:00
d904c9df6f Merge branch 'dev' into 'staging'
адаптивность карточки запроса квиза

See merge request frontend/marketplace!233
2024-08-28 00:10:57 +00:00
160ec29f16 адаптивность карточки запроса квиза 2024-08-28 03:10:18 +03:00
46d53df0d3 Merge branch 'dev' into 'staging'
crutch в запросе и создании квиз аккаунта

See merge request frontend/marketplace!232
2024-08-24 19:20:25 +00:00
76c416ac8c crutch в запросе и создании квиз аккаунта 2024-08-24 22:19:22 +03:00
fbe32b1544 Merge branch 'dev' into 'staging'
fix cc way

See merge request frontend/marketplace!231
2024-08-22 09:08:33 +00:00
9394e78206 fix cc way 2024-08-22 12:07:57 +03:00
b7b6769f17 Merge branch 'dev' into 'staging'
fix cc way

See merge request frontend/marketplace!230
2024-08-22 08:42:52 +00:00
8796b35600 fix cc way 2024-08-22 11:41:42 +03:00
019fc37c86 Merge branch 'dev' into 'staging'
Dev

See merge request frontend/marketplace!229
2024-08-22 06:53:44 +00:00
d571ba3a0e fix way od cc from squiz 2024-08-22 09:52:10 +03:00
7ca5c7fa51 модалка запроса на создание квиза открывается после покупки соответствующего тарифа 2024-08-22 09:48:53 +03:00
5594e59219 модалка запроса создания квиза открывается от стора 2024-08-22 09:18:25 +03:00
27a428e515 карточки при наличии привилегии предлагают запросить создание квиза 2024-08-21 17:57:09 +03:00
b7f2ee2e86 quiz в квиз 2024-08-21 09:39:31 +03:00
841352a698 Merge branch 'dev' into 'staging'
Dev

See merge request frontend/marketplace!228
2024-08-18 21:47:14 +00:00
c10a663609 clear logs 2024-08-19 00:44:33 +03:00
2d251979c8 модалка запроса на создание квиза 2024-08-19 00:23:41 +03:00
cbbeaf7771 Merge branch 'dev' into 'staging'
--

See merge request frontend/marketplace!227
2024-08-15 18:26:14 +00:00
28c583607e -- 2024-08-15 21:25:07 +03:00
e816cfcf45 Merge branch 'dev' into 'staging'
переход на стр оплаты с квиза после полученя юзера

See merge request frontend/marketplace!226
2024-08-15 12:34:53 +00:00
dbbd779741 переход на стр оплаты с квиза после полученя юзера 2024-08-15 15:33:47 +03:00
67865e2eb5 Merge branch 'dev' into 'staging'
--

See merge request frontend/marketplace!225
2024-08-11 07:02:10 +00:00
e170ea6c5c -- 2024-08-11 10:01:31 +03:00
414427c28a Merge branch 'dev' into 'staging'
после переавторизации с квиза кидает на стр оплаты

See merge request frontend/marketplace!224
2024-08-11 05:08:57 +00:00
1e085b2fe3 после переавторизации с квиза кидает на стр оплаты 2024-08-11 08:08:17 +03:00
d31c45ef1b Merge branch 'dev' into 'staging'
fix анализ с квиза ли мы в /payment

See merge request frontend/marketplace!223
2024-08-02 21:10:52 +00:00
ea54fa47fa fix анализ с квиза ли мы в /payment 2024-08-03 00:10:05 +03:00
e62b2d8d5d Merge branch 'dev' into 'staging'
fix quiz way

See merge request frontend/marketplace!222
2024-08-02 15:50:58 +00:00
0eea5735b5 fix quiz way 2024-08-02 18:50:01 +03:00
3eb1c8ee43 Merge branch 'dev' into 'staging'
fix from quiz

See merge request frontend/marketplace!221
2024-08-02 14:37:20 +00:00
c3af476651 fix from quiz 2024-08-02 17:36:49 +03:00
088f268672 Merge branch 'dev' into 'staging'
логика состояния покупки сайта вынесена в стор в 2 поля

See merge request frontend/marketplace!220
2024-08-02 13:08:00 +00:00
4ad6a64ccb логика состояния покупки сайта вынесена в стор в 2 поля 2024-08-02 16:04:19 +03:00
80a5550fb1 Merge branch 'dev' into 'staging'
fix

See merge request frontend/marketplace!219
2024-07-29 04:07:20 +00:00
6c865aaf5d fix 2024-07-29 07:06:40 +03:00
8c12c7fb98 Merge branch 'dev' into 'staging'
fix автопокупка не конфликтует с переходом из квиза

See merge request frontend/marketplace!218
2024-07-29 03:43:29 +00:00
0b4c22e671 fix автопокупка не конфликтует с переходом из квиза 2024-07-29 06:42:54 +03:00
06ca5ba877 Merge branch 'dev' into 'staging'
Dev

See merge request frontend/marketplace!217
2024-07-29 03:01:12 +00:00
434cd7f7ea Единый стор нехватающих денег. Оптимизация автопокупки с использованием pipe 2024-07-29 05:56:25 +03:00
74a28dfaab pipe труба обновляет сторы 2024-07-28 03:17:16 +03:00
479531c45f update kit && fix settings fields 2024-07-25 04:52:53 +03:00
6d00aa7dd6 Merge branch 'dev' into 'staging'
customer v1.0.1

See merge request frontend/marketplace!216
2024-07-22 18:33:36 +00:00
44577d19fd customer v1.0.1 2024-07-22 21:32:41 +03:00
59 changed files with 13554 additions and 729 deletions

@ -0,0 +1,34 @@
name: Deploy
run-name: ${{ gitea.actor }} build image and push to container registry
on:
push:
branches:
- 'main'
- 'staging'
jobs:
CreateImage:
runs-on: [frontstaging]
uses: http://gitea.pena/PenaDevops/actions.git/.gitea/workflows/build-image.yml@v1.1.6-p
with:
runner: frontstaging
secrets:
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
DeployService:
runs-on: [frontstaging]
container:
image: gitea.pena:3000/penadevops/container-images/node-compose:main
env:
GITHUB_RUN_NUMBER: "${{ inputs.actionid }}"
volumes:
- /run/user/1000/podman/podman.sock:/run/user/1000/podman/podman.sock
steps:
- name: Check out repository code
uses: http://gitea.pena:3000/PenaDevops/actions.git/checkout@v1
- run: printenv
- run: GITHUB_RUN_NUMBER=${{ gitea.run_id }} compose -f deployments/${{ gitea.ref_name }}/docker-compose.yaml up -d
# uses: http://gitea.pena/PenaDevops/actions.git/.gitea/workflows/deploy.yml@v1.1.6-p
# with:
# runner: frontstaging

14
.gitea/workflows/lint.yml Normal file

@ -0,0 +1,14 @@
name: Lint
run-name: ${{ gitea.actor }} produce linting
on:
push:
branches:
- 'sdev'
jobs:
Lint:
runs-on: [hubstaging]
uses: http://gitea.pena/PenaDevops/actions.git/.gitea/workflows/lint.yml@v1.1.2
with:
runner: hubstaging

@ -3,9 +3,12 @@ include:
file: "/templates/docker/build-template.gitlab-ci.yml"
- project: "devops/pena-continuous-integration"
file: "/templates/docker/deploy-template.gitlab-ci.yml"
- project: "devops/pena-continuous-integration"
file: "/templates/docker/service-discovery.gitlab-ci.yml"
stages:
- build
- deploy
- service-discovery
build-app:
tags:
@ -32,3 +35,6 @@ deploy-to-prod:
rules:
- if: "$CI_COMMIT_BRANCH == $PRODUCTION_BRANCH"
extends: .deploy_template
service-discovery:
extends: .sd_artefacts_template

@ -1 +1 @@
"@frontend:registry" "https://penahub.gitlab.yandexcloud.net/api/v4/packages/npm/"
"@frontend:registry" "http://gitea.pena/api/packages/skeris/npm/"

@ -1,4 +1,4 @@
FROM penahub.gitlab.yandexcloud.net:5050/devops/dockerhub-backup/node as build
FROM gitea.pena/penadevops/container-images/node:main as build
RUN apk update && rm -rf /var/cache/apk/*
WORKDIR /usr/app
@ -8,7 +8,7 @@ RUN yarn install --ignore-scripts --non-interactive --frozen-lockfile && yarn ca
RUN yarn build
FROM penahub.gitlab.yandexcloud.net:5050/devops/dockerhub-backup/nginx as result
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

@ -3,12 +3,9 @@ services:
hub:
container_name: hub
restart: unless-stopped
image: $CI_REGISTRY_IMAGE/staging:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID
networks:
- marketplace_penahub_frontend
image: gitea.pena:3000/penaside/front-hub/staging:$GITHUB_RUN_NUMBER
labels:
com.pena.domains: shub.pena.digital
hostname: hub
tty: true
networks:
marketplace_penahub_frontend:
external: true

@ -16,9 +16,10 @@
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5",
"@frontend/kitui": "^1.0.82",
"@frontend/kitui": "^1.0.91",
"@mui/icons-material": "^5.10.14",
"@mui/material": "^5.10.14",
"@mui/x-date-pickers": "^7.13.0",
"@popperjs/core": "^2.11.8",
"axios": "^1.4.0",
"buffer": "^6.0.3",
@ -29,16 +30,18 @@
"immer": "^10.0.2",
"isomorphic-fetch": "^3.0.0",
"js-big-decimal": "^2.0.7",
"moment": "^2.30.1",
"notistack": "^3.0.1",
"pdfjs-dist": "3.6.172",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-error-boundary": "^4.0.11",
"react-pdf": "^7.1.2",
"react-router-dom": "^6.15.0",
"react-slick": "^0.29.0",
"slick-carousel": "^1.8.1",
"swr": "^2.2.5",
"transliteration": "^2.3.5",
"use-debounce": "^10.0.0",
"web-vitals": "^2.1.0",
"yup": "^1.1.1",
@ -57,6 +60,7 @@
"@typescript-eslint/eslint-plugin": "^6.9.1",
"@typescript-eslint/parser": "^6.9.1",
"craco-alias": "^3.0.1",
"docz": "^2.4.0",
"eslint": "^8.53.0",
"eslint-plugin-react": "^7.33.2",
"jest": "^29.5.0",

@ -1,9 +1,10 @@
import { UserAccount } from "@frontend/kitui";
import makeRequest from "@api/makeRequest";
import Cart from "@root/pages/Cart/Cart";
import { parseAxiosError } from "@root/utils/parse-error";
const API_URL = `${process.env.REACT_APP_DOMAIN}/customer/v1.0.0`;
const API_URL = `${process.env.REACT_APP_DOMAIN}/customer/v1.0.1`;
export const patchCart = async (
tariffId: string

@ -44,7 +44,7 @@ export type RawDetails = {
Value: string | number | KeyValue[][];
};
const API_URL = `${process.env.REACT_APP_DOMAIN}/customer/v1.0.0`;
const API_URL = `${process.env.REACT_APP_DOMAIN}/customer/v1.0.1`;
export const getHistory = async (): Promise<
[GetHistoryResponse | GetHistoryResponse2 | null, string?]

@ -5,7 +5,7 @@ import { clearUserData } from "@root/stores/user";
import { clearCustomTariffs } from "@root/stores/customTariffs";
import { clearTickets } from "@root/stores/tickets";
import { redirect } from "react-router-dom";
import { setNotEnoughMoneyAmount } from "@stores/cart";
import { setNotEnoughMoneyAmount } from "@stores/notEnoughMoneyAmount";
interface MakeRequest {
method?: Method | undefined;

@ -1,7 +1,7 @@
import makeRequest from "@api/makeRequest";
import { parseAxiosError } from "@root/utils/parse-error";
const API_URL = `${process.env.REACT_APP_DOMAIN}/customer/v1.0.0`;
const API_URL = `${process.env.REACT_APP_DOMAIN}/customer/v1.0.1`;
type GetRecentlyPurchasedTariffsResponse = {
id: string;

@ -8,6 +8,7 @@ import type {
} from "@root/model/privilege";
import type { GetTariffsResponse } from "@root/model/tariff";
import { removeTariffFromCart } from "@root/stores/user";
import axios from "axios";
interface CreateTariffBody {
name: string;
@ -26,7 +27,7 @@ export const getTariffs = async (
try {
const tariffsResponse = await makeRequest<never, GetTariffsResponse>({
method: "GET",
url: `${API_URL}/tariff?page=${apiPage}&limit=${tariffsPerPage}`,
url: `${API_URL}/tariff/getList?page=${apiPage}&limit=${tariffsPerPage}`,
useToken: true,
signal,
});
@ -134,3 +135,24 @@ export const getTariffArray = async (tariffIds: string[] | undefined) => {
return tariffs;
};
const apiUrl = process.env.REACT_APP_DOMAIN + "/requestquiz";
export async function sendContactFormRequest(body: {
contact: string;
whoami: string;
}) {
try {
const a = await axios(apiUrl + "/callme", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
data: body,
});
return [a];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Ошибка при отправке запроса. ${error}`];
}
}

@ -82,7 +82,7 @@ export const createTicket = async (
try {
const createTicketResponse = await createTicketRequest({
url: `${API_URL}/create`,
body: { Title: ticketNameField, Message: ticketBodyField },
body: { Title: ticketNameField, Message: ticketBodyField, System: false },
});
return [createTicketResponse];

@ -55,7 +55,7 @@ export const sendDocuments = async (
const sendDocumentsResponse = await makeRequest<FormData, Verification>({
method: "POST",
url: API_URL,
body: jsonToFormdata({ ...documents, egrule: documents.inn }),
body: jsonToFormdata({ ...documents}),
useToken: true,
withCredentials: true,
});
@ -72,11 +72,23 @@ export const updateDocuments = async (
documents: UpdateDocumentsArgs
): Promise<[Verification | "OK" | null, string?]> => {
try {
// .replace(/\s/g, '_')
// if (documents.inn) {
// documents.inn.append("name", "blob");
// }
// if (documents.rule) {
// documents.rule.append("name", "blob");
// }
// if (documents.certificate) {
// documents.certificate.append("name", "blob");
// }
console.log("documents")
console.log(documents)
const updateDocumentsResponse = await makeRequest<FormData, Verification>({
method: "PUT",
url: `${API_URL}`,
body: jsonToFormdata(
documents.inn ? { ...documents, egrule: documents.inn } : documents
documents.inn ? { ...documents} : documents
),
useToken: true,
withCredentials: true,
@ -93,6 +105,8 @@ export const updateDocuments = async (
export const updateDocument = async (
body: FormData
): Promise<[Verification | "OK" | null, string?]> => {
console.log("body")
console.log(body)
try {
const updateDocumentResponse = await makeRequest<FormData, Verification>({
method: "PATCH",

@ -6,8 +6,12 @@ const isStaging = (() => {
const host = window.location.hostname;
return host.includes("s") ? "s" : "";
})();
const isLocalhost = (() => {
const host = window.location.hostname;
return host.includes("localhost");
})();
const API_URL = `${process.env.REACT_APP_DOMAIN}/customer/v1.0.0`;
const API_URL = `${process.env.REACT_APP_DOMAIN}/customer/v1.0.1`;
interface PaymentBody {
type: string;
@ -19,12 +23,20 @@ export const sendPayment = async ({
body,
fromSquiz,
paymentPurpose,
cc
}: {
userId: string;
body: PaymentBody;
fromSquiz: boolean;
paymentPurpose: "paycart" | "replenishwallet";
cc?: boolean
}): Promise<[SendPaymentResponse | null, string?]> => {
let returnLink = `${isLocalhost ? "localhost:3000" : `https://${isStaging}hub.pena.digital`}/afterpay?from=${
fromSquiz ? "quiz" : "hub"
}&purpose=${paymentPurpose}&userid=${userId}`
if (cc) returnLink = returnLink + "&cc=true"
const reqeustBody = {
currency: "RUB",
bankCard: {
@ -36,9 +48,7 @@ export const sendPayment = async ({
},
phoneNumber: "79000000000",
login: "login_test",
returnUrl: `https://${isStaging}hub.pena.digital/afterpay?from=${
fromSquiz ? "quiz" : "hub"
}&purpose=${paymentPurpose}&userid=${userId}`,
returnUrl: returnLink,
...body,
};

@ -0,0 +1,38 @@
import { useLocation } from "react-router-dom";
import { Box, SxProps, Theme } from "@mui/material";
interface Props {
sx?: SxProps<Theme>;
}
export default function CloseIcon({ sx }: Props) {
const location = useLocation();
return (
<Box
sx={{
width: "30px",
height: "30px",
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
"&:hover path": {
stroke: "#7E2AEA",
},
...sx
}}
>
<svg
width="26"
height="26"
viewBox="0 0 26 26"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M1 1L25 25M1 25L25 1"
stroke={location.pathname === "/" ? "white" : "black"}
/>
</svg>
</Box>
);
}

@ -0,0 +1,22 @@
import { useLocation } from "react-router-dom";
import { Box, SxProps, Theme } from "@mui/material";
interface Props {
sx?: SxProps<Theme>;
}
export default function NotebookWithPencil({ sx }: Props) {
return (
<Box
sx={{
height: "24px",
...sx
}}
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.88766 6.5124H15.3537M5.88766 10.4075H12.0172M5.88766 14.398H9.18792M18.988 4.41492V3.69678C18.988 3.14449 18.5403 2.69678 17.988 2.69678H3.19922C2.64693 2.69678 2.19922 3.14449 2.19922 3.69678V19.7038C2.19922 20.5407 3.16577 21.0074 3.8211 20.4869L6.25047 18.5577C6.42733 18.4173 6.64652 18.3408 6.87236 18.3408H17.988C18.5403 18.3408 18.988 17.8931 18.988 17.3408V15.2147" stroke="#7E2AEA" stroke-width="1.5" stroke-linecap="round" />
<path d="M20.383 7.29034L20.9133 6.76001L20.383 6.22968L19.8527 6.76001L20.383 7.29034ZM15.9919 11.6815L15.4615 11.1511L14.9312 11.6815L15.4615 12.2118L15.9919 11.6815ZM16.7185 12.4081L16.1882 12.9384L16.7185 13.4688L17.2489 12.9384L16.7185 12.4081ZM21.1096 8.01698L21.64 8.54731L22.1703 8.01698L21.64 7.48665L21.1096 8.01698ZM14.4001 13.2732L14.9304 12.7429L14.4001 12.2126L13.8698 12.7429L14.4001 13.2732ZM14.0817 13.5917L13.5514 13.0613C13.5011 13.1115 13.4583 13.1686 13.4242 13.2309L14.0817 13.5917ZM13.1982 15.2018L12.5407 14.841C12.3803 15.1333 12.4321 15.4964 12.6678 15.7321C12.9036 15.9679 13.2667 16.0197 13.559 15.8593L13.1982 15.2018ZM14.8083 14.3183L15.1691 14.9758C15.2314 14.9417 15.2885 14.8988 15.3387 14.8486L14.8083 14.3183ZM15.1267 13.9999L15.6571 14.5302L16.1874 13.9999L15.6571 13.4696L15.1267 13.9999ZM21.9075 5.76585L21.3772 5.23552L20.8468 5.76585L21.3772 6.29618L21.9075 5.76585ZM22.2812 5.39209L22.8116 4.86176C22.5187 4.56887 22.0438 4.56887 21.7509 4.86176L22.2812 5.39209ZM23.0079 6.11874L23.5382 6.64907C23.8311 6.35617 23.8311 5.8813 23.5382 5.58841L23.0079 6.11874ZM22.6341 6.4925L22.1038 7.02283L22.6341 7.55316L23.1645 7.02283L22.6341 6.4925ZM19.8527 6.76001L15.4615 11.1511L16.5222 12.2118L20.9133 7.82067L19.8527 6.76001ZM15.4615 12.2118L16.1882 12.9384L17.2489 11.8778L16.5222 11.1511L15.4615 12.2118ZM17.2489 12.9384L21.64 8.54731L20.5793 7.48665L16.1882 11.8778L17.2489 12.9384ZM21.64 7.48665L20.9133 6.76001L19.8527 7.82067L20.5793 8.54731L21.64 7.48665ZM13.8698 12.7429L13.5514 13.0613L14.612 14.122L14.9304 13.8036L13.8698 12.7429ZM13.4242 13.2309L12.5407 14.841L13.8557 15.5626L14.7392 13.9524L13.4242 13.2309ZM13.559 15.8593L15.1691 14.9758L14.4475 13.6608L12.8374 14.5443L13.559 15.8593ZM15.3387 14.8486L15.6571 14.5302L14.5964 13.4696L14.278 13.788L15.3387 14.8486ZM15.6571 13.4696L14.9304 12.7429L13.8698 13.8036L14.5964 14.5302L15.6571 13.4696ZM22.4378 6.29618L22.8116 5.92242L21.7509 4.86176L21.3772 5.23552L22.4378 6.29618ZM21.7509 5.92242L22.4776 6.64907L23.5382 5.58841L22.8116 4.86176L21.7509 5.92242ZM22.4776 5.58841L22.1038 5.96217L23.1645 7.02283L23.5382 6.64907L22.4776 5.58841ZM23.1645 5.96217L22.4378 5.23552L21.3772 6.29618L22.1038 7.02283L23.1645 5.96217Z" fill="#7E2AEA" />
</svg>
</Box>
);
}

@ -0,0 +1,20 @@
import { useLocation } from "react-router-dom";
import { Box, SxProps, Theme } from "@mui/material";
interface Props {
sx?: SxProps<Theme>;
}
export default function CloseIcon({ sx }: Props) {
const location = useLocation();
return (
<Box
sx={sx}
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.88766 6.5124H15.3537M5.88766 10.4075H12.0172M5.88766 14.398H9.18792M18.988 4.41492V3.69678C18.988 3.14449 18.5403 2.69678 17.988 2.69678H3.19922C2.64693 2.69678 2.19922 3.14449 2.19922 3.69678V19.7038C2.19922 20.5407 3.16577 21.0074 3.8211 20.4869L6.25047 18.5577C6.42733 18.4173 6.64652 18.3408 6.87236 18.3408H17.988C18.5403 18.3408 18.988 17.8931 18.988 17.3408V15.2147" stroke="#7E2AEA" stroke-width="1.5" stroke-linecap="round" />
<path d="M20.383 7.29034L20.9133 6.76001L20.383 6.22968L19.8527 6.76001L20.383 7.29034ZM15.9919 11.6815L15.4615 11.1511L14.9312 11.6815L15.4615 12.2118L15.9919 11.6815ZM16.7185 12.4081L16.1882 12.9384L16.7185 13.4688L17.2489 12.9384L16.7185 12.4081ZM21.1096 8.01698L21.64 8.54731L22.1703 8.01698L21.64 7.48665L21.1096 8.01698ZM14.4001 13.2732L14.9304 12.7429L14.4001 12.2126L13.8698 12.7429L14.4001 13.2732ZM14.0817 13.5917L13.5514 13.0613C13.5011 13.1115 13.4583 13.1686 13.4242 13.2309L14.0817 13.5917ZM13.1982 15.2018L12.5407 14.841C12.3803 15.1333 12.4321 15.4964 12.6678 15.7321C12.9036 15.9679 13.2667 16.0197 13.559 15.8593L13.1982 15.2018ZM14.8083 14.3183L15.1691 14.9758C15.2314 14.9417 15.2885 14.8988 15.3387 14.8486L14.8083 14.3183ZM15.1267 13.9999L15.6571 14.5302L16.1874 13.9999L15.6571 13.4696L15.1267 13.9999ZM21.9075 5.76585L21.3772 5.23552L20.8468 5.76585L21.3772 6.29618L21.9075 5.76585ZM22.2812 5.39209L22.8116 4.86176C22.5187 4.56887 22.0438 4.56887 21.7509 4.86176L22.2812 5.39209ZM23.0079 6.11874L23.5382 6.64907C23.8311 6.35617 23.8311 5.8813 23.5382 5.58841L23.0079 6.11874ZM22.6341 6.4925L22.1038 7.02283L22.6341 7.55316L23.1645 7.02283L22.6341 6.4925ZM19.8527 6.76001L15.4615 11.1511L16.5222 12.2118L20.9133 7.82067L19.8527 6.76001ZM15.4615 12.2118L16.1882 12.9384L17.2489 11.8778L16.5222 11.1511L15.4615 12.2118ZM17.2489 12.9384L21.64 8.54731L20.5793 7.48665L16.1882 11.8778L17.2489 12.9384ZM21.64 7.48665L20.9133 6.76001L19.8527 7.82067L20.5793 8.54731L21.64 7.48665ZM13.8698 12.7429L13.5514 13.0613L14.612 14.122L14.9304 13.8036L13.8698 12.7429ZM13.4242 13.2309L12.5407 14.841L13.8557 15.5626L14.7392 13.9524L13.4242 13.2309ZM13.559 15.8593L15.1691 14.9758L14.4475 13.6608L12.8374 14.5443L13.559 15.8593ZM15.3387 14.8486L15.6571 14.5302L14.5964 13.4696L14.278 13.788L15.3387 14.8486ZM15.6571 13.4696L14.9304 12.7429L13.8698 13.8036L14.5964 14.5302L15.6571 13.4696ZM22.4378 6.29618L22.8116 5.92242L21.7509 4.86176L21.3772 5.23552L22.4378 6.29618ZM21.7509 5.92242L22.4776 6.64907L23.5382 5.58841L22.8116 4.86176L21.7509 5.92242ZM22.4776 5.58841L22.1038 5.96217L23.1645 7.02283L23.5382 6.64907L22.4776 5.58841ZM23.1645 5.96217L22.4378 5.23552L21.3772 6.29618L22.1038 7.02283L23.1645 5.96217Z" fill="#7E2AEA" />
</svg>
</Box>
);
}

@ -0,0 +1,152 @@
import type { ChangeEvent, FocusEvent, KeyboardEvent } from "react";
import React, { useEffect, useState } from "react";
import type { InputProps, SxProps, Theme } from "@mui/material";
import {
Box,
FormControl,
Input,
InputLabel,
Typography,
useTheme,
} from "@mui/material";
interface CustomTextFieldProps {
placeholder: string;
id?: string;
value?: string;
error?: string;
emptyError?: boolean;
onChange?: (event: ChangeEvent<HTMLInputElement>) => void;
onKeyDown?: (event: KeyboardEvent<HTMLInputElement>) => void;
onBlur?: (event: FocusEvent<HTMLInputElement>) => void;
text?: string;
maxLength?: number;
sx?: SxProps<Theme>;
sxForm?: SxProps<Theme>;
InputProps?: Partial<InputProps>;
type?: string;
rows?: number;
className?: string;
disabled?: boolean;
}
export default function CustomTextField({
placeholder,
id,
value = "",
onChange,
onKeyDown,
onBlur,
text,
sx,
error,
emptyError,
InputProps,
maxLength = 200,
type = "",
rows = 0,
sxForm,
className,
disabled,
}: CustomTextFieldProps) {
const theme = useTheme();
const [inputValue, setInputValue] = useState("");
const [isInputActive, setIsInputActive] = useState(false);
useEffect(() => {
setInputValue(value);
}, [value]);
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.value.length <= maxLength) {
const inputValue = event.target.value;
if (type === "number") {
setInputValue(inputValue.replace(/\D/g, ""));
} else {
setInputValue(inputValue);
}
if (onChange) {
onChange(event);
}
}
};
const handleInputFocus = () => {
setIsInputActive(true);
};
const handleInputBlur = (event: React.FocusEvent<HTMLInputElement>) => {
setIsInputActive(false);
if (onBlur) {
onBlur(event);
}
};
return (
<FormControl
fullWidth
variant="standard"
sx={{ p: 0, ...sxForm }}
className={className || ""}
>
{error && (
<InputLabel
sx={{
fontSize: "13.5px",
marginTop: "3px",
}}
>
{error}
</InputLabel>
)}
<Input
id={id}
defaultValue={text}
fullWidth
value={inputValue}
placeholder={placeholder}
onChange={handleInputChange}
error={!!error || emptyError}
onFocus={handleInputFocus}
onBlur={handleInputBlur}
onKeyDown={onKeyDown}
multiline={rows > 0}
rows={rows}
disabled={disabled}
disableUnderline
sx={{
maxLength: maxLength,
borderRadius: "10px",
fontSize: "18px",
lineHeight: "21px",
p: "13px",
border: `${isInputActive ? "black 2px" : "#9A9AAF 1px"} solid`,
backgroundColor: theme.palette.background.default,
height: "48px",
...sx,
}}
data-cy="textfield"
/>
{isInputActive && inputValue.length >= maxLength - 7 && (
<Box
sx={{
display: "flex",
marginTop: "5px",
marginLeft: "auto",
position: "absolute",
bottom: "-25px",
right: "0",
}}
>
<Typography fontSize="14px">{inputValue.length}</Typography>
<span>/</span>
<Typography fontSize="14px">{maxLength}</Typography>
</Box>
)}
</FormControl>
);
}

@ -13,7 +13,7 @@ import { ReactComponent as CrossIcon } from "@root/assets/Icons/cross.svg";
import type { MouseEvent } from "react";
import CustomTariffAccordion from "@root/components/CustomTariffAccordion";
import { setNotEnoughMoneyAmount } from "@root/stores/cart";
import { setNotEnoughMoneyAmount } from "@root/stores/notEnoughMoneyAmount";
const name: Record<string, string> = {
templategen: "Шаблонизатор",

@ -19,8 +19,8 @@ import { Link, useNavigate } from "react-router-dom";
import { withErrorBoundary } from "react-error-boundary";
import { handleComponentError } from "@root/utils/handleComponentError";
import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline";
import { setNotEnoughMoneyAmount, useCartStore } from "@root/stores/cart";
import { useDiffMoney } from "@root/stores/diffMoney";
import { setNotEnoughMoneyAmount, startPayCartProcess, useNotEnoughMoneyAmount } from "@root/stores/notEnoughMoneyAmount";
import { RSCOpen } from "@root/stores/requestSquizCreate";
function Drawers() {
const [openNotificationsModal, setOpenNotificationsModal] = useState<boolean>(false);
@ -34,9 +34,10 @@ function Drawers() {
const cart = useCart();
console.log("боковой cart", cart.priceAfterDiscounts)
const userAccount = useUserStore((state) => state.userAccount);
const userId = useUserStore((state) => state.userId) || "";
const tickets = useTicketStore((state) => state.tickets);
const notEnoughMoneyAmount = useCartStore(state => state.notEnoughMoneyAmount);
const { setNewDiff } = useDiffMoney()
const notEnoughMoneyAmount = useNotEnoughMoneyAmount(state => state.notEnoughMoneyAmount);
const siteReadyPayCart = useNotEnoughMoneyAmount(state => state.siteReadyPayCart)
const notificationsCount = tickets.filter(
({ user, top_message }) => user !== top_message.user_id && top_message.shown.me !== 1
@ -45,12 +46,15 @@ function Drawers() {
async function handlePayClick() {
setLoading(true);
const isCC = cart.services.length > 0 && cart.services[0].tariffs.some(t => t.privileges[0].privilegeId === "quizManual")
const [payCartResponse, payCartError] = await payCart();
if (payCartError) {
if (payCartError.includes("insufficient funds: ")) {
const notEnoughMoneyAmount = parseInt(payCartError.replace(/^.*insufficient\sfunds:\s(?=\d+$)/, ""));
setNotEnoughMoneyAmount(notEnoughMoneyAmount);
startPayCartProcess(userId)
}
setLoading(false);
@ -60,6 +64,7 @@ function Drawers() {
}
if (payCartResponse) {
if (isCC) RSCOpen()
setUserAccount(payCartResponse);
}
@ -69,10 +74,8 @@ function Drawers() {
function handleReplenishWallet() {
setIsDrawerOpen(false);
if (location.pathname.includes("/payment")) {
setNewDiff(notEnoughMoneyAmount)
}
navigate("/payment", { state: { notEnoughMoneyAmount } });
if (siteReadyPayCart === null) startPayCartProcess(userId)
navigate("/payment");
}
return (
@ -180,7 +183,6 @@ function Drawers() {
</IconButton>
<Drawer anchor={"right"} open={isDrawerOpen} onClose={() => {
setIsDrawerOpen(false)
setNotEnoughMoneyAmount(0)
}} sx={{ background: "rgba(0, 0, 0, 0.55)" }}>
<SectionWrapper
maxWidth="lg"
@ -248,7 +250,7 @@ function Drawers() {
Здесь написана окончательная стоимость всех услуг сложенных в корзину с учётом всех скидок.
</Typography>
<Typography color={theme.palette.gray.dark}>
После нажатия кнопки оплатить, вы будете перенаправлены на форму оплаты, для оплаты ВСЕЙ корзины (рекомендуем перед оплатой, убрать все лишнее)
После нажатия кнопки оплатить (пополнить), вы будете перенаправлены на форму оплаты, для оплаты ВСЕЙ корзины (рекомендуем перед оплатой, убрать все лишнее)
</Typography>
</Box>
<Box

@ -421,14 +421,14 @@ export default function Chat({ open = false, onclickArrow, sx }: Props) {
>
онлайн-консультант
</Typography>
<Typography
{/* <Typography
sx={{
fontSize: "16px",
lineHeight: "19px",
}}
>
время работы 10:00-3:00 по мск
</Typography>
</Typography> */}
</Box>
</Box>
<Box

@ -0,0 +1,50 @@
import { IconButton, SxProps } from "@mui/material";
type InfoProps = {
width?: number;
height?: number;
sx?: SxProps;
onClick?: any;
className?: string;
color?: string;
};
export default function InfoButton({
width = 20,
height = 20,
sx,
onClick,
className,
color = "#7e2aea",
}: InfoProps) {
return (
<IconButton sx={sx} className={className} onClick={onClick}>
<svg
width={width}
height={height}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10 19C14.9706 19 19 14.9706 19 10C19 5.02944 14.9706 1 10 1C5.02944 1 1 5.02944 1 10C1 14.9706 5.02944 19 10 19Z"
stroke={color}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M9.25 9.25H10V14.5H10.75"
stroke={color}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M9.8125 7C10.4338 7 10.9375 6.49632 10.9375 5.875C10.9375 5.25368 10.4338 4.75 9.8125 4.75C9.19118 4.75 8.6875 5.25368 8.6875 5.875C8.6875 6.49632 9.19118 7 9.8125 7Z"
fill={color}
/>
</svg>
</IconButton>
);
}

@ -28,7 +28,7 @@ import {
import { currencyFormatter } from "@root/utils/currencyFormatter";
import { clearCustomTariffs } from "@root/stores/customTariffs";
import { clearTickets } from "@root/stores/tickets";
import {setNotEnoughMoneyAmount} from "@stores/cart"
import {setNotEnoughMoneyAmount} from "@stores/notEnoughMoneyAmount"
interface Props {
isLoggedIn: boolean;

@ -17,7 +17,7 @@ import { logout } from "@root/api/auth";
import { enqueueSnackbar } from "notistack";
import { clearCustomTariffs } from "@root/stores/customTariffs";
import { clearTickets } from "@root/stores/tickets";
import {setNotEnoughMoneyAmount} from "@stores/cart"
import {setNotEnoughMoneyAmount} from "@stores/notEnoughMoneyAmount"
type MenuItem = {
name: string;

@ -17,7 +17,7 @@ import { currencyFormatter } from "@root/utils/currencyFormatter";
import { clearTickets } from "@root/stores/tickets";
import type { ReactNode } from "react";
import {setNotEnoughMoneyAmount} from "@stores/cart"
import {setNotEnoughMoneyAmount} from "@stores/notEnoughMoneyAmount"
interface Props {
children: ReactNode;

@ -18,7 +18,7 @@ import { clearTickets } from "@root/stores/tickets";
import { currencyFormatter } from "@root/utils/currencyFormatter";
import walletIcon from "@root/assets/Icons/wallet_icon.svg";
import {setNotEnoughMoneyAmount} from "@stores/cart"
import {setNotEnoughMoneyAmount} from "@stores/notEnoughMoneyAmount"
export const NavbarPanel = () => {
const navigate = useNavigate();

@ -63,6 +63,7 @@ export default function ProtectedLayout() {
});
useAllTariffsFetcher({
baseUrl: process.env.REACT_APP_DOMAIN + "/strator/tariff/getList",
onSuccess: updateTariffs,
onError: (error) => {
const errorMessage = getMessageFromFetchError(error);
@ -78,6 +79,7 @@ export default function ProtectedLayout() {
});
usePrivilegeFetcher({
url: process.env.REACT_APP_DOMAIN + "/strator/privilege/service",
onSuccess: setPrivileges,
onError: (error) => {
console.error("usePrivilegeFetcher error :>> ", error);

@ -72,10 +72,10 @@ export const Select = ({ items, selectedItem, setSelectedItem }: SelectProps) =>
className="select"
value=""
open={opened}
onClick={() => setOpened((isOpened) => !isOpened)}
MenuProps={{ disablePortal: true }}
sx={{ width: "100%",zIndex: 1, }}
onChange={selectItem}
onClick={() => setOpened((isOpened) => !isOpened)}
>
{items.map((item, index) => (
<MenuItem key={item + index} value={index} sx={{ padding: "12px" }}>

@ -7,8 +7,10 @@ import { Loader } from "./Loader";
import { currencyFormatter } from "@root/utils/currencyFormatter";
import { payCart } from "@root/api/cart";
import { setUserAccount } from "@root/stores/user";
import { setNotEnoughMoneyAmount, useCartStore } from "@root/stores/cart";
import { setUserAccount, useUserStore } from "@root/stores/user";
import { setNotEnoughMoneyAmount, startPayCartProcess, useNotEnoughMoneyAmount } from "@root/stores/notEnoughMoneyAmount";
import { RSCOpen } from "@root/stores/requestSquizCreate";
import { useCart } from "@root/utils/hooks/useCart";
interface Props {
priceBeforeDiscounts: number;
@ -19,13 +21,16 @@ interface Props {
export default function TotalPrice({ priceAfterDiscounts, priceBeforeDiscounts, isConstructor = false }: Props) {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const notEnoughMoneyAmount = useCartStore(state => state.notEnoughMoneyAmount);
const notEnoughMoneyAmount = useNotEnoughMoneyAmount(state => state.notEnoughMoneyAmount);
const userId = useUserStore(store => store.userId) || ""
const [loading, setLoading] = useState<boolean>(false);
const navigate = useNavigate();
const cart = useCart();
async function handlePayClick() {
setLoading(true);
const isCC = cart.services.length > 0 && cart.services[0].tariffs.some(t => t.privileges[0].privilegeId === "quizManual")
const [payCartResponse, payCartError] = await payCart();
if (payCartError) {
@ -41,6 +46,7 @@ export default function TotalPrice({ priceAfterDiscounts, priceBeforeDiscounts,
}
if (payCartResponse) {
if (isCC) RSCOpen()
setUserAccount(payCartResponse);
}
@ -48,6 +54,7 @@ export default function TotalPrice({ priceAfterDiscounts, priceBeforeDiscounts,
}
function handleReplenishWallet() {
startPayCartProcess(userId)
navigate("/payment", { state: { notEnoughMoneyAmount } });
}

@ -1,4 +1,4 @@
import React from "react";
import React, { useEffect } from "react";
import ReactDOM from "react-dom/client";
import {
BrowserRouter,
@ -9,6 +9,7 @@ import {
useNavigate,
} from "react-router-dom";
import { CssBaseline, ThemeProvider } from "@mui/material";
import { AdapterMoment } from "@mui/x-date-pickers/AdapterMoment";
import Faq from "./pages/Faq/Faq";
import Wallet from "./pages/Wallet";
import Payment from "./pages/Payment/Payment";
@ -35,6 +36,8 @@ import {
setUser,
setUserAccount,
useUserStore,
OriginalUserSquizAccount,
setQuizUserAccount
} from "./stores/user";
import TariffConstructor from "./pages/TariffConstructor/TariffConstructor";
import {
@ -44,6 +47,7 @@ import {
useUserFetcher,
} from "@frontend/kitui";
import { pdfjs } from "react-pdf";
import { ruRU } from "@mui/x-date-pickers/locales";
import { theme } from "./utils/theme";
import PPofData from "@root/docs/PPofData";
import Docs from "@root/docs/docs";
@ -55,15 +59,22 @@ import OutdatedLink from "@root/pages/auth/OutdatedLink";
import { verify } from "./pages/AccountSettings/helper";
import AfterPay from "./pages/AfterPay";
import { PageNotFound } from "./pages/PageNotFound";
import {setNotEnoughMoneyAmount} from "@stores/cart"
import { useNotEnoughMoneyAmount } from "@stores/notEnoughMoneyAmount"
import { usePipeSubscriber } from "./utils/hooks/usePipeSubscriber";
import { useAfterPay } from "./utils/hooks/useAutoPay";
import { LocalizationProvider } from "@mui/x-date-pickers";
import { ModalRequestCreate } from "./pages/Tariffs/ModalRequestCreate";
import * as crutch from "./useUserAccountFetcher";
import { useCart } from "./utils/hooks/useCart";
pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.js`;
const localeText =
ruRU.components.MuiLocalizationProvider.defaultProps.localeText;
const App = () => {
const location = useLocation();
const userId = useUserStore((state) => state.userId);
const userId = useUserStore(state => state.userId);
const navigate = useNavigate();
if(location.pathname !== "/cart"){setNotEnoughMoneyAmount(0)}
useUserFetcher({
url: process.env.REACT_APP_DOMAIN + `/user/${userId}`,
userId,
@ -79,7 +90,7 @@ const App = () => {
});
useUserAccountFetcher({
url: process.env.REACT_APP_DOMAIN + "/customer/v1.0.0/account",
url: process.env.REACT_APP_DOMAIN + "/customer/v1.0.1/account",
userId,
onNewUserAccount: setUserAccount,
onError: (error) => {
@ -93,8 +104,28 @@ const App = () => {
},
});
crutch.useUserAccountFetcher<OriginalUserSquizAccount>({
url: `${process.env.REACT_APP_DOMAIN}/squiz/account/get`,
userId,
onNewUserAccount: setQuizUserAccount,
onError: (error) => {
const errorMessage = getMessageFromFetchError(error);
if (errorMessage) {
enqueueSnackbar(errorMessage);
clearUserData();
clearAuthToken();
navigate("/signin");
}
},
});
usePipeSubscriber()
verify(userId);
useAfterPay()
if (location.state?.redirectTo)
return (
<Navigate
@ -115,6 +146,9 @@ const App = () => {
<Route path="/changepwd/expired" element={<OutdatedLink />} />
</Routes>
)}
<ModalRequestCreate />
<Routes location={location.state?.backgroundLocation || location}>
<Route path="/" element={<Landing />} />
<Route
@ -142,7 +176,7 @@ const App = () => {
to="/"
replace
state={{
redirectTo: window.location.pathname + window.location.search,
redirectTo: `/changepwd${location.search}`,
}}
/>
}
@ -202,11 +236,17 @@ const root = ReactDOM.createRoot(
root.render(
// <React.StrictMode>
<ThemeProvider theme={theme}>
<BrowserRouter>
<CssBaseline />
<SnackbarProvider />
<App />
</BrowserRouter>
<LocalizationProvider
dateAdapter={AdapterMoment}
adapterLocale="ru"
localeText={localeText}
>
<BrowserRouter>
<CssBaseline />
<SnackbarProvider />
<App />
</BrowserRouter>
</LocalizationProvider>
</ThemeProvider>
// </React.StrictMode>
);

@ -1,4 +1,5 @@
import type { Attachment } from "@root/model/attachment"
import { UserAccount } from "@frontend/kitui"
export type File = {
name: "inn" | "rule" | "egrule" | "certificate";
@ -8,21 +9,21 @@ export type File = {
export interface Verification {
_id: string;
accepted: boolean;
status: "org" | "nko";
status: UserAccount["status"];
updated_at: string;
comment: string;
files: File[];
}
export type SendDocumentsArgs = {
status: "org" | "nko";
status: UserAccount["status"];
inn: Attachment;
rule: Attachment;
certificate?: Attachment;
};
export type UpdateDocumentsArgs = {
status: "org" | "nko";
status: UserAccount["status"];
inn?: Attachment;
rule?: Attachment;
certificate?: Attachment;

@ -33,6 +33,8 @@ function AccountSettings() {
const comment = useUserStore((state) => state.comment)
const userId = useUserStore((state) => state.userId) ?? ""
console.log(user)
const [onChangeTypeLP, setOnChangeTypeLP] = useState<LP>("")
const [readySend, setReadySend] = useState(false)
@ -51,10 +53,10 @@ function AccountSettings() {
const type = onChangeTypeLP
setOnChangeTypeLP("")
setReadySend(false)
if (type === "email" && user?.email !== settingsFields.email.value) setSettingsField("email", user?.email || "")
if (type === "email" && user?.email !== settingsFields.email.value) setSettingsField("email", user?.email || user?.login || "")
if (type === "password") setSettingsField("password", "")
if (type === "all") {
setSettingsField("email", user?.email || "")
setSettingsField("email", user?.email || user?.login || "")
setSettingsField("password", "")
}
}

@ -63,10 +63,8 @@ export default function DocumentItem({
useEffect(() => {
if (typeof documentUrl === 'string') {
if (!documentUrl.includes("pena.digital")) {
console.log(documentUrl)
fetch(documentUrl)
.then(e => {
console.log(e)
setReadyShowDocument(true)
})
.catch(e => console.log(e))

@ -43,10 +43,8 @@ export default function DocumentUploadItem({
useEffect(() => {
if (typeof urlOrFile === 'string') {
if (!urlOrFile.includes("pena.digital")) {
console.log(documentUrl)
fetch(documentUrl)
.then(e => {
console.log(e)
setReadyShowDocument(true)
})
.catch(e => console.log(e))

@ -19,7 +19,7 @@ export default function UserFields({
const upMd = useMediaQuery(theme.breakpoints.up("md"))
const { settingsFields, user } = useUserStore((state) => state)
const a = useUserStore((state) => state)
const textFieldProps = {
gap: upMd ? "16px" : "10px",

@ -1,135 +1,28 @@
import { useState, useEffect } from "react";
import {
Box,
Button,
Typography,
useTheme,
useMediaQuery,
} from "@mui/material";
import { payCart } from "@root/api/cart";
import { useUserStore } from "@root/stores/user";
import wallet_icon from "@root/assets/Icons/ColorWallet.svg";
import { Link, useSearchParams } from "react-router-dom";
import { useNavigate, useSearchParams } from "react-router-dom";
const MINUTE = 1000 * 60;
//Раньше эта страничка благодарила за покупку и перенаправляла сталкера своей дорогой.
//Чтобы не мелькать хабом перед перенаправлением на другие домены - логика останется на этом отдельном url адресе
//Эта страничка ТОЛЬКО перенаправляет на нужный адрес. Процесс автопокупки после пополнения лежит на хуке useAutoPay
export default () => {
const [redirectUrl, setRedirectUrl] = useState<string>("/");
const theme = useTheme();
const phone = useMediaQuery(theme.breakpoints.down(375));
const userId = useUserStore((state) => state.user?._id);
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const paymentUserId = searchParams.get("userid");
useEffect(() => {
const from = searchParams.get("from") || "hub";
const purpose = searchParams.get("purpose");
const userId = searchParams.get("userid");
const from = searchParams.get("from") || "hub";
const purpose = searchParams.get("purpose")
const isCC = searchParams.get("cc")
if (purpose === "paycart" || from !== "quiz") {
let tryCount = 0;
const host = window.location.hostname;
const domain = (host.includes("s") ? "s" : "") + from;
const pathname = from.includes("hub") ? "/tariffs" : "/list";
if (userId !== paymentUserId) {
return;
}
let link = `https://${domain}.pena.digital${pathname}?purpose=${purpose}&userid=${userId}`
const payCartPendingRequestDeadline = localStorage.getItem(
"payCartPendingRequestDeadline"
);
const deadline = payCartPendingRequestDeadline
? Number(payCartPendingRequestDeadline)
: Date.now() + 20 * MINUTE;
console.log("isCC")
if (isCC) link = link + "&cc=true"
localStorage.setItem(
"payCartPendingRequestDeadline",
deadline.toString()
);
tryPayCart();
async function tryPayCart() {
tryCount += 1;
if (Date.now() > deadline) {
localStorage.removeItem("payCartPendingRequestDeadline");
return;
}
const [, payCartError] = await payCart();
if (!payCartError) {
localStorage.removeItem("payCartPendingRequestDeadline");
return;
}
setTimeout(tryPayCart, tryCount > 5 ? MINUTE : MINUTE / 6);
}
}
const host = window.location.hostname;
const domain = (host.includes("s") ? "s" : "") + from;
const pathname = from === "hub" ? "/tariffs" : "/list";
setRedirectUrl(
`https://${domain}.pena.digital${pathname}?afterpay=${true}&userid=${userId}`
);
}, []);
return (
<Box
sx={{
width: "100vw",
height: "100vh",
p: phone ? "76px 16px" : "117px 0 0 0",
}}
>
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<Box
component="img"
src={wallet_icon}
sx={{
width: phone ? "338px" : "auto",
height: phone ? "215px" : "auto",
mb: "60px",
}}
/>
<Typography
sx={{
fontSize: phone ? "24px" : "36px",
fontWeight: 500,
lineHeight: "28.44px",
textAlign: "center",
mb: "10px",
}}
>
Спасибо за оплату!
</Typography>
<Typography
sx={{
fontSize: "18px",
lineHeight: "21.33px",
textAlign: "center",
color: theme.palette.gray.dark,
mb: "30px",
}}
>
Ваш платеж принят и в течение 10 минут товары будут зачислены
</Typography>
<Button
component={Link}
to={redirectUrl.toString()}
replace={true}
variant="pena-contained-dark"
>
На главную
</Button>
</Box>
</Box>
);
document.location.href = link
return <></>;
};

@ -63,7 +63,7 @@ export default function Section2() {
</Box>
{/* {upMd ? <WideTemplCard name="PenaDoc" desc="Сервис автоматизации, избавляющий от рутины */}
{/*в документообороте. 1 раз делаешь шаблон и используешь всю жизнь " light={false} sx={{ marginTop: "126px" }} /> : <TemplCardPhonePink />}*/}
{upMd ? <WideTemplCard name="PenaQuiz" desc="Конструктор quiz опросов, для любых видов исследований и quiz маркетинга, арбитража трафика" image={card2ImageBig} light={false} sx={{ marginTop: "126px" }} />
{upMd ? <WideTemplCard name="PenaQuiz" desc="Конструктор квиз опросов, для любых видов исследований и квиз маркетинга, арбитража трафика" image={card2ImageBig} light={false} sx={{ marginTop: "126px" }} />
:
<Box
sx={{
@ -78,7 +78,7 @@ export default function Section2() {
>
<CardWithLink
headerText="PenaQuiz"
text="Конструктор quiz опросов, для любых видов исследований и quiz маркетинга, арбитража трафика"
text="Конструктор квиз опросов, для любых видов исследований и квиз маркетинга, арбитража трафика"
linkHref="#"
image={card2Image}
isHighlighted={!upMd}

@ -32,8 +32,7 @@ import PaymentMethodCard from "./PaymentMethodCard";
import { SorryModal } from "./SorryModal";
import { WarnModal } from "./WarnModal";
import { mutate } from "swr";
import { useCartStore } from "@root/stores/cart";
import { useDiffMoney } from "@root/stores/diffMoney";
import { calcTimeOfReadyPayCart, cancelPayCartProcess, setNotEnoughMoneyAmount, startPayCartProcess, useNotEnoughMoneyAmount } from "@root/stores/notEnoughMoneyAmount";
type PaymentMethod = {
label: string;
@ -55,119 +54,133 @@ type PaymentMethodType = (typeof paymentMethods)[number]["name"];
export default function Payment() {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const upSm = useMediaQuery(theme.breakpoints.up("sm"));
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const [promocodeField, setPromocodeField] = useState<string>("");
const [selectedPaymentMethod, setSelectedPaymentMethod] =
useState<PaymentMethodType | null>("");
const [warnModalOpen, setWarnModalOpen] = useState<boolean>(false);
const [sorryModalOpen, setSorryModalOpen] = useState<boolean>(false);
const [paymentValueField, setPaymentValueField] = useState<string>("0");
const [paymentLink, setPaymentLink] = useState<string>("");
const [fromSquiz, setIsFromSquiz] = useState<boolean>(false);
const location = useLocation();
console.log("location", location);
console.log("location", location.state);
const verificationStatus = useUserStore((state) => state.verificationStatus);
const userId = useUserStore((state) => state.userId);
const navigate = useNavigate();
const handleCustomBackNavigation = useHistoryTracker();
const {diffMoney, setNewDiff} = useDiffMoney()
const userId = useUserStore((state) => state.userId) || "";
const verificationStatus = useUserStore((state) => state.verificationStatus);
const notEnoughMoneyAmount =
(location.state?.notEnoughMoneyAmount as number) ?? 0;
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const upSm = useMediaQuery(theme.breakpoints.up("sm"));
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const [promocodeField, setPromocodeField] = useState<string>("");
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState<PaymentMethodType | null>("");
const [paymentValueField, setPaymentValueField] = useState<string>("0");
const [fromSquiz, setIsFromSquiz] = useState<boolean>(false);
const [cc, setCC] = useState<boolean>(false);
const [warnModalOpen, setWarnModalOpen] = useState<boolean>(false);
const [sorryModalOpen, setSorryModalOpen] = useState<boolean>(false);
const notEnoughMoneyAmount = useNotEnoughMoneyAmount(state => state.notEnoughMoneyAmount)
const siteReadyPayCart = useNotEnoughMoneyAmount(state => state.siteReadyPayCart)
const paymentValue = parseFloat(
bigDecimal.multiply(parseFloat(paymentValueField), 100)
);
useEffect(() => {
console.log(diffMoney)
if (diffMoney > 0) {
setNewDiff(0)
setPaymentValueField((diffMoney / 100).toString())
}
}, [diffMoney])
//Отмена состояния сайта "в процессе покупки корзины, просто нехватило деняк"
useEffect(() => {
return () => {
//При выходе со страницы оплаты мы точно не в состоянии покупки корзины
cancelPayCartProcess()
}
}, [])
//Тут записываем начальную сумму в инпут (если стоит флаг что мы в процессе покупки)
useEffect(() => {
if (siteReadyPayCart !== null && siteReadyPayCart[userId] !== undefined && calcTimeOfReadyPayCart(siteReadyPayCart[userId]))
setPaymentValueField((notEnoughMoneyAmount / 100).toString());//Сколько нехватило на хабе
}, [notEnoughMoneyAmount, siteReadyPayCart])
useLayoutEffect(() => {
setPaymentValueField((notEnoughMoneyAmount / 100).toString());
//Предустановленное значение - это либо 0, либо сколько нам нехватило на хабе, либо сколько нам нехватило на квизе
const params = new URLSearchParams(window.location.search);
const fromSquiz = params.get("action");
if (fromSquiz === "squizpay") {
const userid = params.get("user");
const currentCC = params.get("cc");
if (currentCC) setCC(true)
if (fromSquiz === "squizpay" && userid !== null) {
setIsFromSquiz(true);
setPaymentValueField((Number(params.get("dif") || "0") / 100).toString());
}
//Принимаем параметры из странички и очищаем url адрес до красивого состояния
navigate(`/payment`, {
replace: true,
});
}, [ ]);
async function handleChoosePaymentClick() {
if (!selectedPaymentMethod) {
enqueueSnackbar("Введите метод оплаты");
return;
}
}, []);
//https://shub.pena.digital/quizpayment?action=squizpay&dif=9800&data=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY2ODVhNTc4OTgzZWU3N2Y4ZTFlNjNkYyIsImF1ZCI6InBlbmEiLCJpc3MiOiJwZW5hLWF1dGgtc2VydmljZSIsImlhdCI6MTcyMjIyMjgzMywiZXhwIjoxNzI3NDA2ODMzfQ.My1KJWFk034MiMdImQSlzf5p4Sn5Dhboj2VvPQteh59tD_CwXyPtePEyev3thV_58IbOOgJ5cgeBm0JKn7atgMgRMpNQVdeYKtf6HYvVoAqkrMcT1LHgAlEQ0TcaXssFKCQGuiCVltHY3UE-kQv5TeydBpO3U9BDKvMqRqv5-Xo&userid=6685a578983ee77f8e1e63dc
const handlePaymentClick = () => {
if (Number(paymentValueField) === 0) {
enqueueSnackbar("Введите сумму");
return;
}
if (selectedPaymentMethod !== "rspay") {
const [sendPaymentResponse, sendPaymentError] = await sendPayment({
userId: userId ?? "",
fromSquiz,
body: {
type: selectedPaymentMethod,
amount: Number(
bigDecimal.floor(
bigDecimal.multiply(Number(paymentValueField), 100)
)
),
},
paymentPurpose: notEnoughMoneyAmount ? "paycart" : "replenishwallet",
});
if (sendPaymentError) {
return enqueueSnackbar(sendPaymentError);
}
if (sendPaymentResponse) {
setPaymentLink(sendPaymentResponse.link);
}
return;
} else {
if (verificationStatus !== VerificationStatus.VERIFICATED) {
setWarnModalOpen(true);
return;
}
if (selectedPaymentMethod === "rspay") {
if (Number(paymentValueField) < 900) {
enqueueSnackbar("Минимальная сумма 900р");
return;
}
const [sendRSPaymentResponse] = await sendRSPayment(
Number(paymentValueField)
);
if (sendRSPaymentResponse) {
return enqueueSnackbar(sendRSPaymentResponse);
if (verificationStatus !== VerificationStatus.VERIFICATED) {
setWarnModalOpen(true);
return;
}
enqueueSnackbar(
"Cпасибо за заявку, в течении 24 часов вам будет выставлен счёт для оплаты услуг."
);
navigate("/settings");
startPayRS()
} else {
startPayCard()
}
}
const startPayRS = async () => {
const [sendRSPaymentResponse] = await sendRSPayment(
Number(paymentValueField)
);
if (sendRSPaymentResponse) {
return enqueueSnackbar(sendRSPaymentResponse === "OK" ? "Cпасибо за заявку, в течении 24 часов вам будет выставлен счёт для оплаты услуг." : sendRSPaymentResponse);
}
enqueueSnackbar("Проверьте статус верификации или обратитесь в техподдержку");
navigate("/settings");
}
const startPayCard = async () => {
if (!selectedPaymentMethod) {
enqueueSnackbar("Введите метод оплаты");
return
}
const [sendPaymentResponse, sendPaymentError] = await sendPayment({
userId: userId ?? "",
fromSquiz,
body: {
type: selectedPaymentMethod,
amount: Number(
bigDecimal.floor(
bigDecimal.multiply(Number(paymentValueField), 100)
)
),
},
paymentPurpose: notEnoughMoneyAmount ? "paycart" : "replenishwallet",
cc
});
if (sendPaymentError) {
return enqueueSnackbar(sendPaymentError);
}
//Произошёл запрос на пополнение счёта. Нам вернули ссылку для перехода на страницу пополнения.
if (sendPaymentResponse) {
document.location.href = sendPaymentResponse.link;
}
}
async function handleApplyPromocode() {
if (!promocodeField) return;
@ -250,7 +263,6 @@ export default function Payment() {
image={image}
onClick={() => {
setSelectedPaymentMethod(name);
setPaymentLink("");
}}
unpopular={false}
/>
@ -261,7 +273,6 @@ export default function Payment() {
image={rsPayLogo}
onClick={async () => {
setSelectedPaymentMethod("rspay");
setPaymentLink("");
}}
unpopular={false}
/>
@ -297,76 +308,59 @@ export default function Payment() {
>
{upMd && <Typography mb="56px">Выберите способ оплаты</Typography>}
<Typography mb="20px">К оплате</Typography>
{paymentLink ? (
<Typography
sx={{
fontWeight: 500,
fontSize: "20px",
lineHeight: "48px",
mb: "28px",
}}
>
{currencyFormatter.format(
Number(bigDecimal.divide(bigDecimal.floor(paymentValue), 100))
)}
</Typography>
) : (
<InputTextfield
TextfieldProps={{
placeholder: "К оплате",
value: paymentValueField,
type: "number",
}}
onChange={(e) => {
const value = parseFloat(
e.target.value.replace(/^0+(?=\d\.)/, "")
);
setPaymentValueField(isNaN(value) ? "" : value.toString());
}}
id="payment-amount"
gap={upMd ? "16px" : "10px"}
color={"#F2F3F7"}
FormInputSx={{ mb: "28px" }}
/>
)}
{
siteReadyPayCart?.[userId] && notEnoughMoneyAmount > 0 ?
<Typography
sx={{
fontWeight: 500,
fontSize: "20px",
lineHeight: "48px",
mb: "28px",
}}
>
{currencyFormatter.format(
Number(bigDecimal.divide(bigDecimal.floor(paymentValue), 100))
)}
</Typography>
:
<InputTextfield
TextfieldProps={{
placeholder: "К оплате",
value: paymentValueField,
type: "number",
}}
onChange={(e) => {
const value = parseFloat(
e.target.value.replace(/^0+(?=\d\.)/, "")
);
setPaymentValueField(isNaN(value) ? "" : value.toString());
}}
id="payment-amount"
gap={upMd ? "16px" : "10px"}
color={"#F2F3F7"}
FormInputSx={{ mb: "28px" }}
/>
}
</Box>
{paymentLink ? (
<Button
variant="pena-outlined-light"
component="a"
href={paymentLink}
sx={{
mt: "auto",
color: "black",
border: `1px solid ${theme.palette.purple.main}`,
"&:hover": {
backgroundColor: theme.palette.purple.dark,
border: `1px solid ${theme.palette.purple.dark}`,
},
}}
>
Оплатить
</Button>
) : (
<Button
variant="pena-outlined-light"
disabled={!isFinite(paymentValue)}
onClick={handleChoosePaymentClick}
sx={{
mt: "auto",
color: "black",
border: `1px solid ${theme.palette.purple.main}`,
"&:hover": {
color: "white",
},
"&:active": {
color: "white",
},
}}
>
Выбрать
</Button>
)}
<Button
variant="pena-outlined-light"
disabled={!isFinite(paymentValue)}
onClick={handlePaymentClick}
sx={{
mt: "auto",
color: "black",
border: `1px solid ${theme.palette.purple.main}`,
"&:hover": {
color: "white",
},
"&:active": {
color: "white",
},
}}
>
Оплатить
</Button>
</Box>
</Box>
<WarnModal open={warnModalOpen} setOpen={setWarnModalOpen} />

@ -1,6 +1,6 @@
import { useEffect, useState } from "react";
import { ApologyPage } from "../ApologyPage";
import { useNavigate } from "react-router-dom";
import { redirect, useNavigate } from "react-router-dom";
import {
clearAuthToken,
getMessageFromFetchError,
@ -19,13 +19,14 @@ import {
import { logout } from "@root/api/auth";
import { clearCustomTariffs } from "@root/stores/customTariffs";
import { clearTickets } from "@root/stores/tickets";
import {setNotEnoughMoneyAmount} from "@stores/cart"
import { setNotEnoughMoneyAmount, startPayCartProcess } from "@stores/notEnoughMoneyAmount"
const params = new URLSearchParams(window.location.search);
const action = params.get("action");
const dif = params.get("dif");
const token = params.get("data");
const userId = params.get("userid");
let action = params.get("action");
let dif = params.get("dif");
let token = params.get("data");
let userId = params.get("userid");
let currentCC = params.get("cc");
let first = true;
@ -33,15 +34,23 @@ export default function QuizPayment() {
const navigate = useNavigate();
const [message, setMessage] = useState("Идёт загрузка");
const user = useUserStore((state) => state.user);
const currentUserId = useUserStore((state) => state.user?._id);
const fromSquiz = params.get("action");
useEffect(
function redirectIfSignedIn() {
if (!first && user?._id === userId)
navigate(`/payment?action=${action}&dif=${dif}&user=${userId}`, {
useEffect(() => {
if (!first && userId !== null && currentUserId === userId) {
if (fromSquiz === "squizpay") {
setNotEnoughMoneyAmount(Number(params.get("dif") || 0));//Сколько нехватило на квизе
startPayCartProcess(userId)
let link = `/payment?action=${action}&dif=${dif}&user=${userId}`
if (currentCC) link = link + "&cc=true"
navigate(link, {
replace: true,
});
},
[navigate, user]
}
}
},
[currentUserId]
);
@ -64,24 +73,13 @@ export default function QuizPayment() {
clearCustomTariffs();
clearTickets();
setNotEnoughMoneyAmount(0)
await logout();
logout();
}
setUserId(userId);
setAuthToken(token);
// setAuthToken(data.data.accessToken)
setUserId(userId);
// useUserFetcher({
// url: process.env.REACT_APP_DOMAIN + `/user/${userId}`,
// userId,
// onNewUser: (user) => {
// setUser(user)
// navigate(`/payment?action=${action}&dif=${dif}`, { replace: true })
// },
// onError: () => { },
// })
return;
})();
} else {

@ -42,8 +42,6 @@ function TariffConstructor() {
}}
>
{Object.entries(customTariffs).filter(([serviceKey]) => serviceKey === "squiz").map(([serviceKey, privileges], index) => {
console.log("privileges")
console.log(privileges)
return (
<Box key={serviceKey}>
<Box

@ -53,8 +53,6 @@ export default function TariffPrivilegeSlider({ privilege }: Props) {
);
function handleSliderChange(measurement: PrivilegeName) {
console.log(measurement)
console.log(sliderSettingsByType)
return (value: number | number[]) => {
if (Number(value) < Number(sliderSettingsByType[measurement]?.min)) {

@ -0,0 +1,329 @@
import { Box, Button, IconButton, Modal, Typography, useMediaQuery, useTheme } from "@mui/material"
import { Form, Formik, useFormik } from "formik"
import CustomTextField from "@components/CustomTextField"
import InfoButton from "@components/InfoButton";
import { sendContactFormRequest } from "@api/tariff"
import { TimePicker } from "@mui/x-date-pickers"
import moment, { Moment } from "moment"
import { enqueueSnackbar } from "notistack"
import { useState } from "react"
import CloseIcon from "@root/assets/Icons/CloseIcon";
import { useRequestSquizCreate, RSCClose } from "@root/stores/requestSquizCreate"
interface Values {
contact: string;
dogiebusiness: string;
imagination: string;
name: string;
time?: Moment | null;
}
const initialValues: Values = {
contact: "",//phone number
// whoami: {},
dogiebusiness: "",
imagination: "",
name: "",
time: null
};
interface FP {
title: string
desc?: string
placeholder: string
value: string
onChange: any
rows?: number
}
const Field = ({
title,
desc,
placeholder,
value,
onChange,
rows,
}: FP) => {
return (
<Box
sx={{
m: "15px 0"
}}
>
<Typography
sx={{
fontSize: "18px",
fontWeight: 500,
lineHeight: "21.33px",
mb: "10px",
}}
>{title}</Typography>
{desc && <Typography
sx={{
fontSize: "18px",
mb: "10px",
color: "#9A9AAF"
}}
>{desc}</Typography>}
<CustomTextField
value={value}
placeholder={placeholder}
maxLength={200}
onChange={onChange}
rows={rows || 0}
sx={rows !== undefined ? { height: "68px" } : {}}
/>
</Box>
)
}
export const ModalRequestCreate = () => {
const theme = useTheme()
const isMobile = useMediaQuery(theme.breakpoints.down(650));
const [isSending, setIsSending] = useState(false)
const open = useRequestSquizCreate(store => store.isRSCOpen)
console.log(open)
if (isSending) return (
<Modal
open={open}
onClose={() => {
RSCClose()
setIsSending(false)
}}
>
<Box sx={{
position: 'absolute' as 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
bgcolor: 'background.paper',
boxShadow: 24,
width: isMobile ? "343px" : "418px",
borderRadius: "10px",
p: "50px"
}}>
<Typography
sx={{
fontSize: "24px",
fontWeight: 500,
lineHeight: "28.44px",
textAlign: "center"
}}
>Спасибо за заявку!</Typography>
<Typography
sx={{
fontSize: "18px",
lineHeight: "21.33px",
color: "#4D4D4D",
m: "5px 0 30px 0"
}}
>С вами свяжутся в течение 24 часов</Typography>
<Button
variant="contained"
fullWidth
onClick={() => {
RSCClose()
setIsSending(false)
}}
sx={{
py: "12px",
bgcolor: "rgb(126, 42, 234)",
borderRadius: "8px",
"&:hover": {
backgroundColor: "#581CA7",
},
"&:active": {
color: "white",
backgroundColor: "black",
},
}}
>Ок</Button>
</Box>
</Modal>
)
return (
<Modal
open={open}
onClose={RSCClose}
>
<Box sx={{
position: 'absolute' as 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
bgcolor: 'background.paper',
boxShadow: 24,
width: isMobile ? "344px" : "620px",
borderRadius: "10px"
}}>
<Box>
<Box
sx={{
width: "100%",
height: "68px",
backgroundColor: theme.palette.background.default,
borderRadius: "10px 10px 0 0"
}}
>
<Typography
sx={{
fontSize: "18px",
padding: "20px",
color: "rgb(154, 154, 175)",
borderRadius: "10px 10px 0 0"
}}
>
Заполните форму, чтобы оставить заявку на создание квиза
</Typography>
</Box>
<IconButton
onClick={RSCClose}
sx={{
width: "12px",
height: "12px",
position: "absolute",
right: "15px",
top: "15px",
}}
>
<CloseIcon sx={{ width: "12px", height: "12px", transform: "scale(1.5)" }} />
</IconButton>
</Box>
<Box
sx={{
p: "15px 70px 50px 50px",
}}
>
<Formik
initialValues={initialValues}
onSubmit={async (values, formikHelpers) => {
if (values.contact.length < 8) return enqueueSnackbar("Пожалуйста, оставьте контактные данные")
const resp = await sendContactFormRequest({
contact: values.contact,
whoami: JSON.stringify({
dogiebusiness: values.dogiebusiness,
imagination: values.imagination,
name: values.name,
time: moment(values.time).format("hh:mm")
})
})
//@ts-ignore
if (resp[0]?.status === 200) {
enqueueSnackbar("Запрос успешно отправлен")
setIsSending(true)
}
if (resp[1]) {
//@ts-ignore
enqueueSnackbar(resp[1])
}
}}
>
{({ values, isSubmitting, setFieldValue }) => (<>
<Form>
<Field
title="Ваше имя"
placeholder="Иван"
value={values.name}
onChange={({ target }: any) => setFieldValue("name", target.value)}
/>
<Field
title="Какой у вас бизнес?"
placeholder="Логистика"
value={values.dogiebusiness}
onChange={({ target }: any) => setFieldValue("dogiebusiness", target.value)}
/>
<Field
title="Ваши контактные данные"
placeholder="Не менее 8 символов"
value={values.contact}
onChange={({ target }: any) => setFieldValue("contact", target.value)}
desc="(Telegram, WhatsApp, номер телефона)"
/>
<Field
title="Ваши пожелания к квизу"
placeholder="Введите свой текст здесь"
value={values.imagination}
onChange={({ target }: any) => setFieldValue("imagination", target.value)}
rows={2}
/>
<Box
sx={{
m: "15px 0"
}}
>
<Typography
sx={{
fontSize: "18px",
fontWeight: 500,
lineHeight: "21.33px",
mb: "10px",
}}
>Во сколько вам можно позвонить? </Typography>
<Typography
sx={{
fontSize: "18px",
mb: "10px",
color: "#9A9AAF"
}}
>Москва (GMT+3)</Typography>
<TimePicker
onChange={(e) => setFieldValue("time", e)}
ampm={false}
value={values.time}
views={['hours', 'minutes']} format="hh:mm"
sx={{
border: "#c4c4c4 1px solid",
borderRadius: "4px",
"& .MuiInputBase-root": {
display: "flex"
}
}}
/>
</Box>
<Box
sx={{
display: "flex"
}}>
<Button
variant="contained"
fullWidth
type="submit"
disabled={isSubmitting}
sx={{
py: "12px",
bgcolor: "rgb(126, 42, 234)",
borderRadius: "8px",
"&:hover": {
backgroundColor: "#581CA7",
},
"&:active": {
color: "white",
backgroundColor: "black",
},
}}
>Отправить</Button>
<InfoButton sx={{
ml: "15px",
width: "48px"
}} />
</Box>
</Form>
</>)}
</Formik>
</Box>
</Box>
</Modal >
)
}

@ -1,7 +1,8 @@
import { Box, Typography, Tooltip, SxProps, Theme, Button, Badge, useTheme, useMediaQuery, } from "@mui/material";
import { Box, Typography, Tooltip, SxProps, Theme, Button, Badge, useTheme, useMediaQuery, IconButton, } from "@mui/material";
import { MouseEventHandler, ReactNode } from "react";
import { cardShadow } from "@root/utils/theme";
import { relative } from "path";
import NotebookWithPencil from "@root/assets/Icons/NotebookWithPencil copy";
interface Props {
icon: ReactNode;
@ -15,11 +16,12 @@ interface Props {
text?: string;
};
price?: ReactNode;
sendRequestToCreate?: () => void;
}
export default function TariffCard({ icon, headerText, text, sx, price, buttonProps, discount }: Props) {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
export default function TariffCard({ icon, headerText, text, sx, price, buttonProps, discount, sendRequestToCreate }: Props) {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
return (
<Box
sx={{
@ -79,20 +81,20 @@ export default function TariffCard({ icon, headerText, text, sx, price, buttonPr
</Box>
)}
</Box>
{/* <Tooltip title={<Typography>{headerText}</Typography>} placement="top"> */}
<Typography
variant="h5"
sx={{
mt: "14px",
mb: "10px",
textOverflow: "ellipsis",
overflow: "hidden",
whiteSpace: "nowrap",
width: "100%",
}}
>
{headerText}
</Typography>
{/* <Tooltip title={<Typography>{headerText}</Typography>} placement="top"> */}
<Typography
variant="h5"
sx={{
mt: "14px",
mb: "10px",
textOverflow: "ellipsis",
overflow: "hidden",
whiteSpace: "nowrap",
width: "100%",
}}
>
{headerText}
</Typography>
{/* </Tooltip>
<Tooltip
title={text.map((line, index) => (
@ -100,29 +102,67 @@ export default function TariffCard({ icon, headerText, text, sx, price, buttonPr
))}
placement="top"
> */}
<Box
sx={{
width: "100%",
mb: "auto",
}}
>
<Typography sx={{ overflow: "hidden", textOverflow: "ellipsis" }}>
{text}
</Typography>
</Box>
<Box
sx={{
width: "100%",
mb: "auto",
}}
>
<Typography sx={{ overflow: "hidden", textOverflow: "ellipsis" }}>
{text}
</Typography>
</Box>
{/* </Tooltip> */}
{buttonProps && (
<Button
onClick={buttonProps.onClick}
variant="pena-outlined-purple"
sx={{
mt: "10px",
...buttonProps.sx,
}}
>
{buttonProps.text}
</Button>
)}
<Box
sx={{
display: "flex",
width: "100%",
alignItems: "center",
}}>
{buttonProps && (
<Button
onClick={buttonProps.onClick}
variant="pena-outlined-purple"
sx={{
mt: "10px",
...buttonProps.sx,
}}
>
{buttonProps.text}
</Button>
)}
{Boolean(sendRequestToCreate) && (
<Box
sx={{
ml: isMobile ? "10px" : "20px",
bgcolor: "#F2F3F7",
borderRadius: "8px",
}}
>
{isMobile ?
<IconButton
onClick={sendRequestToCreate}
sx={{
p:"12px",
}}
>
<NotebookWithPencil />
</IconButton>
:
<Button
sx={{
color: "#7E2AEA",
p: "10px 14px",
}}
onClick={sendRequestToCreate}
>
Запросить
</Button>
}
</Box>
)}
</Box>
</Box>
);
}

@ -89,14 +89,14 @@ export default function Tariffs() {
<WideTemplCard
sx={{ marginTop: "55px" }}
name={"PenaQuiz"}
desc={"Конструктор quiz опросов, для любых видов исследований и quiz маркетинга, арбитража трафика"}
desc={"Конструктор квиз опросов, для любых видов исследований и квиз маркетинга, арбитража трафика"}
image={CardImage2}
href={"https://quiz.pena.digital"}
/>
:
<TemplCardPhoneLight
name={"PenaQuiz"}
desc={"Конструктор quiz опросов, для любых видов исследований и quiz маркетинга, арбитража трафика"}
desc={"Конструктор квиз опросов, для любых видов исследований и квиз маркетинга, арбитража трафика"}
image={CardImage2}
href={"https://quiz.pena.digital"}
/>

@ -28,8 +28,10 @@ import TariffCard from "./TariffCard";
import { useDiscounts } from "@root/api/price";
import { Select } from "@components/Select";
import { Tabs } from "@components/Tabs";
import { useRequestSquizCreate, RSCOpen, RSCClose } from "@root/stores/requestSquizCreate"
const subPages = ["Базовый тариф PenaQuiz", 'Убрать логотип "PenaQuiz"'];
const subPagesTime = ["Базовый тариф PenaQuiz", 'Убрать логотип "PenaQuiz"'];
const subPagesVolume = ["Заявки квиз", 'Заказать создание квиза'];
const StepperText: Record<string, string> = {
volume: "Тарифы на объём",
@ -43,10 +45,13 @@ function TariffPage() {
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const location = useLocation();
const tariffs = useTariffStore((state) => state.tariffs);
const [selectedItem, setSelectedItem] = useState<number>(0);
const [selectedItemTime, setSelectedItemTime] = useState<number>(0);
const [selectedItemVolume, setSelectedItemVolume] = useState<number>(0);
const purchasesAmount =
useUserStore((state) => state.userAccount?.wallet.spent) ?? 0;
const userId = useUserStore((state) => state.user?._id) ?? "";
const userPrivilegies = useUserStore(store => store.quizUserAccount?.privileges);
console.log(userPrivilegies)
const discounts = useDiscounts(userId);
const isUserNko =
useUserStore((state) => state.userAccount?.status) === "nko";
@ -68,25 +73,14 @@ function TariffPage() {
}
const filteredTariffs = tariffs.filter((tariff) => {
if (tariff.privileges[0] === undefined) return false;
if (
(tariff.privileges[0].type === "day") === (unit === "time") &&
!tariff.isDeleted &&
!tariff.isCustom
) {
if (
((selectedItem === 0 && unit === "time") || unit !== "time") &&
tariff.privileges[0].serviceKey === "squiz" &&
tariff.privileges[0].privilegeId !== "squizHideBadge"
)
return true;
if (tariff.privileges[0] === undefined || tariff.isDeleted || tariff.isCustom) return false;
if (unit === "time") {
if (selectedItemTime === 0 && tariff.privileges[0].privilegeId === "quizUnlimTime") return true
if (selectedItemTime === 1 && tariff.privileges[0].privilegeId === "squizHideBadge") return true
}
if (
selectedItem === 1 &&
unit === "time" &&
tariff.privileges[0].privilegeId === "squizHideBadge"
) {
return true;
if (unit === "volume") {
if (selectedItemVolume === 0 && tariff.privileges[0].privilegeId === "quizCnt") return true
if (selectedItemVolume === 1 && tariff.privileges[0].privilegeId === "quizManual") return true
}
return false;
});
@ -95,6 +89,7 @@ function TariffPage() {
filteredTariffs: Tariff[],
addFreeTariff = false
) => {
const isCC = userPrivilegies?.quizManual?.amount !== undefined && userPrivilegies?.quizManual?.amount > 0 && unit === "volume" && selectedItemVolume === 1
const tariffElements = filteredTariffs
.filter((tariff) => tariff.privileges.length > 0)
.map((tariff, index) => {
@ -104,7 +99,7 @@ function TariffPage() {
purchasesAmount,
currentTariffs ?? [],
isUserNko,
userId
userId,
);
return (
@ -133,6 +128,7 @@ function TariffPage() {
text: "Выбрать",
onClick: () => handleTariffItemClick(tariff._id),
}}
sendRequestToCreate={isCC ? RSCOpen : undefined}
headerText={tariff.name}
text={tariff.description || ""}
price={
@ -206,15 +202,32 @@ function TariffPage() {
<>
{isMobile ? (
<Select
items={subPages}
selectedItem={selectedItem}
setSelectedItem={setSelectedItem}
items={subPagesTime}
selectedItem={selectedItemTime}
setSelectedItem={setSelectedItemTime}
/>
) : (
<Tabs
items={subPages}
selectedItem={selectedItem}
setSelectedItem={setSelectedItem}
items={subPagesTime}
selectedItem={selectedItemTime}
setSelectedItem={setSelectedItemTime}
/>
)}
</>
)}
{unit === "volume" && (
<>
{isMobile ? (
<Select
items={subPagesVolume}
selectedItem={selectedItemVolume}
setSelectedItem={setSelectedItemVolume}
/>
) : (
<Tabs
items={subPagesVolume}
selectedItem={selectedItemVolume}
setSelectedItem={setSelectedItemVolume}
/>
)}
</>
@ -231,7 +244,7 @@ function TariffPage() {
}))`,
}}
>
{createTariffElements(filteredTariffs, true)}
{createTariffElements(filteredTariffs, unit === "time" || (unit === "volume" && selectedItemVolume !== 1))}
</Box>
{/*{recentlyPurchased.length > 0 && (*/}
{/* <>*/}

@ -75,6 +75,8 @@ export default function RecoverPassword() {
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const authToken = params.get("auth");
console.log("authToken")
console.log(authToken)
setTokenUser(authToken);
history.pushState(null, document.title, "/changepwd");

@ -1,22 +0,0 @@
import { create } from "zustand";
import { devtools } from "zustand/middleware";
interface CartStore {
notEnoughMoneyAmount: number;
}
export const useCartStore = create<CartStore>()(
devtools(
(get, set) => ({
notEnoughMoneyAmount: 0,
}),
{
name: "Cart",
enabled: process.env.NODE_ENV === "development",
trace: true,
actionsBlacklist: "rejected",
}
)
);
export const setNotEnoughMoneyAmount = (amount: number) => useCartStore.setState({ notEnoughMoneyAmount: amount });

@ -1,14 +0,0 @@
import { HistoryRecord, HistoryRecord2 } from "@root/api/history";
import { create } from "zustand";
import { devtools, persist } from "zustand/middleware";
type DiffMoneyType = {
diffMoney: number;
setNewDiff: (diff: number) => void
};
export const useDiffMoney = create<DiffMoneyType>((set) => ({
diffMoney: 0,
setNewDiff: (diff: number) => set({diffMoney: diff})
}));

@ -0,0 +1,38 @@
import moment from "moment";
import { create } from "zustand";
import { devtools } from "zustand/middleware";
interface CartStore {
notEnoughMoneyAmount: number;
siteReadyPayCart: Record<string, string> | null;
}
const initialState: CartStore = {
notEnoughMoneyAmount: 0,
siteReadyPayCart: null
}
//Была попытка оплатить корзину. Тут записанна недостающая сумма.
export const useNotEnoughMoneyAmount = create<CartStore>()(
devtools(
(get, set) => initialState,
{
name: "notEnoughMoneyAmount",
enabled: process.env.NODE_ENV === "development",
trace: true,
actionsBlacklist: "rejected",
}
)
);
export const setNotEnoughMoneyAmount = (amount: number) => useNotEnoughMoneyAmount.setState({ notEnoughMoneyAmount: amount });
export const setSiteReadyPayCart = (flag: Record<string, string> | null) => useNotEnoughMoneyAmount.setState({ siteReadyPayCart: flag });
export const startPayCartProcess = (userId: string) => setSiteReadyPayCart({ [userId]: moment().add(20, 'minutes').format("X") });
export const cancelPayCartProcess = () => setSiteReadyPayCart(null);
export const calcTimeOfReadyPayCart = (deadline: string) => {
const ready = Number(deadline) > Number(moment().format("X"))
if (!ready) {
cancelPayCartProcess()
}
return ready
}

@ -0,0 +1,25 @@
import { Privilege } from "@frontend/kitui";
import { create } from "zustand"
import { devtools } from "zustand/middleware"
interface RequestSquizCreate {
isRSCOpen: boolean;
}
const initialState: RequestSquizCreate = {
isRSCOpen: false
}
export const useRequestSquizCreate = create<RequestSquizCreate>()(
devtools(
(get, set) => initialState,
{
name: "Privileges",
enabled: process.env.NODE_ENV === "development",
}
)
)
export const RSCOpen = () => useRequestSquizCreate.setState({ isRSCOpen: true })
export const RSCClose = () => useRequestSquizCreate.setState({ isRSCOpen: false })

@ -14,23 +14,42 @@ import { patchUser } from "@root/api/user"
import { UserAccountSettingsFieldStatus, VerificationStatus } from "@root/model/account"
import { patchCurrency, deleteCart, patchCart } from "@root/api/cart"
import { User, UserAccount, UserName, getInitials, patchUserAccount } from "@frontend/kitui"
import { setNotEnoughMoneyAmount } from "./cart";
import { cancelPayCartProcess, setNotEnoughMoneyAmount, setSiteReadyPayCart, useNotEnoughMoneyAmount } from "./notEnoughMoneyAmount";
type Privilege = {
amount: number;
created_at: string;
id: string;
privilege_id: string;
privilege_name: string;
};
interface UserStore {
userId: string | null;
user: User | null;
userAccount: UserAccount | null;
settingsFields: UserSettingsFieldStatus & UserAccountSettingsFieldStatus & { hasError: boolean };
verificationStatus: VerificationStatus;
verificationType: "juridical" | "nko";
isDocumentsDialogOpen: boolean;
dialogType: "juridical" | "nko" | "";
documents: UserDocuments;
documentsUrl: UserDocumentsUrl;
comment: string;
initials: string;
userId: string | null;
user: User | null;
userAccount: UserAccount | null;
settingsFields: UserSettingsFieldStatus & UserAccountSettingsFieldStatus & { hasError: boolean };
verificationStatus: VerificationStatus;
verificationType: "juridical" | "nko";
isDocumentsDialogOpen: boolean;
dialogType: "juridical" | "nko" | "";
documents: UserDocuments;
documentsUrl: UserDocumentsUrl;
comment: string;
initials: string;
quizUserAccount: OriginalUserSquizAccount | null;
}
export type OriginalUserSquizAccount = {
created_at: string;
deleted: boolean;
email: string;
id: string;
privileges: Record<string, Privilege>;
privilege_name: string;
};
const defaultFieldValues = {
value: "",
error: null,
@ -58,6 +77,7 @@ const initialState: UserStore = {
userId: null,
user: null,
userAccount: null,
quizUserAccount: null,
settingsFields: { ...defaultFields },
verificationStatus: VerificationStatus.NOT_VERIFICATED,
verificationType: "juridical",
@ -103,10 +123,22 @@ export const useUserStore = create<UserStore>()(
export const setVerificationStatus = (verificationStatus: VerificationStatus) =>
useUserStore.setState({ verificationStatus })
export const setVerificationType = (verificationType: "nko" | "org") =>
export const setVerificationType = (verificationType: UserAccount["status"]) =>
useUserStore.setState({
verificationType: verificationType === "org" ? "juridical" : "nko",
})
export const setUserStatus = (status: UserAccount["status"]) =>
useUserStore.setState(
produce<UserStore>((state) => {
if (state.userAccount !== null) state.userAccount.status = status
})
)
export const setWallet = (wallet: UserAccount["wallet"]) =>
useUserStore.setState(
produce<UserStore>((state) => {
if (state.userAccount !== null) state.userAccount.wallet = wallet
})
)
export const setUserId = (userId: string | null) => useUserStore.setState({ userId })
export const setUser = (user: User) =>
@ -114,7 +146,7 @@ export const setUser = (user: User) =>
produce<UserStore>((state) => {
state.user = user
state.settingsFields.email.value = user?.email ?? ""
state.settingsFields.email.value = user?.email || user?.login || ""
state.settingsFields.phoneNumber.value = user?.phoneNumber ?? ""
state.settingsFields.password.value = ""
})
@ -139,12 +171,36 @@ export const setUserAccount = (user: UserAccount) =>
}
)
export const setCart = (cart: string[]) =>
export const setQuizUserAccount = (quizUserAccount: OriginalUserSquizAccount) => useUserStore.setState({ quizUserAccount });
export const setNewNames = (name: UserName) =>
useUserStore.setState(
produce<UserStore>((state) => {
if (state.userAccount) state.userAccount.cart = cart
state.settingsFields.firstname.value = name.firstname ?? ""
state.settingsFields.secondname.value = name.secondname ?? ""
state.settingsFields.middlename.value = name.middlename ?? ""
state.settingsFields.orgname.value = name.orgname ?? ""
}),
false,
{
type: "setNewNames",
payload: name,
}
)
export const setCart = (cart: string[]) => {
useUserStore.setState(
produce<UserStore>((state) => {
if (state.userAccount) {
state.userAccount.cart = cart
}
})
)
//Изменение корзины ведёт к отмене этих 2 параметров всегда
setNotEnoughMoneyAmount(0)
cancelPayCartProcess()
}
export const setComment = (comment: string) => useUserStore.setState({ comment })
@ -216,7 +272,7 @@ export const setUploadedDocument = (type: UserDocumentTypes, fileName: string, u
})
)
export const setSettingsField = (fieldName: UserSettingsField | keyof UserName, value: string) =>
export const setSettingsField = (fieldName: UserSettingsField | keyof UserName, value: string) =>
useUserStore.setState(
produce<UserStore>((state) => {
if (!state.settingsFields) return
@ -247,15 +303,15 @@ export const sendUserData = async () => {
if (!state.settingsFields) return
const isPatchingUser =
state.settingsFields.email.touched ||
state.settingsFields.password.touched ||
state.settingsFields.phoneNumber.touched
state.settingsFields.email.touched ||
state.settingsFields.password.touched ||
state.settingsFields.phoneNumber.touched
const isPatchingUserAccount =
state.settingsFields.firstname.touched ||
state.settingsFields.secondname.touched ||
state.settingsFields.middlename.touched ||
state.settingsFields.orgname.touched
state.settingsFields.firstname.touched ||
state.settingsFields.secondname.touched ||
state.settingsFields.middlename.touched ||
state.settingsFields.orgname.touched
const userPayload: PatchUserRequest = {}
@ -273,7 +329,7 @@ export const sendUserData = async () => {
await Promise.all([
isPatchingUser && patchUser(userPayload).then(([user]) => user && setUser(user)),
isPatchingUserAccount && patchUserAccount(userAccountPayload).then(setUserAccount),
isPatchingUserAccount && patchUserAccount(userAccountPayload, "v1.0.1").then(setUserAccount),
])
}
@ -283,11 +339,11 @@ export const addTariffToCart = async (tariffId: string) => {
if (patchCartError === undefined) {
setCart(patchCartResponse)
}
return({patchCartResponse, patchCartError})
return ({ patchCartResponse, patchCartError })
}
export const removeTariffFromCart = async (tariffId: string) => {
setNotEnoughMoneyAmount(0);
setNotEnoughMoneyAmount(0);
const [deleteCartResponse, deleteCartError] = await deleteCart(tariffId)
if (!deleteCartError) {

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

@ -0,0 +1,71 @@
import { payCart } from "@root/api/cart";
import { calcTimeOfReadyPayCart, cancelPayCartProcess, setNotEnoughMoneyAmount, setSiteReadyPayCart, startPayCartProcess, useNotEnoughMoneyAmount } from "@root/stores/notEnoughMoneyAmount";
import { useUserStore } from "@root/stores/user";
import moment from "moment";
import { enqueueSnackbar } from "notistack";
import { useEffect } from "react";
import { useSearchParams } from "react-router-dom";
import { RSCOpen } from "@root/stores/requestSquizCreate";
import { useCartTariffs } from "./useCartTariffs";
export const useAfterPay = () => {
const [searchParams, setSearchParams] = useSearchParams();
const userId = useUserStore(store => store.userId)
const userAccount = useUserStore(state => state.userAccount);
const siteReadyPayCart = useNotEnoughMoneyAmount(state => state.siteReadyPayCart);
const cartTariffs = useCartTariffs();
// console.log(cartTariffs)
// const isCC = cartTariffs !== null && cartTariffs !== undefined && cartTariffs.length > 0 && cartTariffs.some(t => t.privileges[0].privilegeId === "quizManual")
const purpose = searchParams.get("purpose");
const from = searchParams.get("from");
const action = searchParams.get("action");
const paymentUserId = searchParams.get("userid");
useEffect(() => {
//Звёзды сошлись, будем оплачивать корзину
if (from !== "quiz" && paymentUserId && paymentUserId === userId) {
//Чистим url адрес от параметров. (Если нет action. Если есть - значит мы пришли из квиза)
if (action === null) setSearchParams({}, { replace: true })
// navigate(`/tariffs`, {
// replace: true,
// });
if (purpose === "paycart") {
(async () => {
//Проверяем можем ли мы оплатить корзину здесь и сейчас
const [, payCartError] = await payCart();
if (payCartError) {
//Не получилось купить корзину. Ставим флаг, что сайт в состоянии ожидания пополнения счёта для оплаты
startPayCartProcess(paymentUserId)
} else {
enqueueSnackbar("Товары успешно приобретены")
cancelPayCartProcess()
if (true) RSCOpen()
}
})()
}
}
}, [purpose, from, paymentUserId])
useEffect(() => {
if (userId !== null && siteReadyPayCart !== null && siteReadyPayCart[userId] !== undefined) {
const deadline = siteReadyPayCart[userId]
if (calcTimeOfReadyPayCart(deadline)) {
//Время ещё не вышло. У нас стоит флаг покупать корзину если время не вышло.
(async () => {
const [, payCartError] = await payCart();
if (!payCartError) {
enqueueSnackbar("Товары успешно приобретены")
cancelPayCartProcess()
}
})()
}
}
}, [userAccount, userId, siteReadyPayCart])
}

@ -0,0 +1,66 @@
import { Ticket, UserAccount, UserName, getAuthToken, useSSESubscription } from "@frontend/kitui"
import { setCart, setNewNames, setUserStatus, setWallet, useUserStore } from "@root/stores/user";
import { useSSETab } from "./useSSETab";
import { cancelPayCartProcess } from "@root/stores/notEnoughMoneyAmount";
type Ping = [{ event: "ping" }]
type SomeChange = [{
"id": UserAccount["_id"],
"userId": UserAccount["userId"],
"cart": UserAccount["cart"],
"wallet": {
"cash": UserAccount["wallet"]["cash"],
"currency": UserAccount["wallet"]["currency"],
"spent": UserAccount["wallet"]["spent"],
"purchasesAmount": UserAccount["wallet"]["purchasesAmount"],
"money": UserAccount["wallet"]["money"],
"lastPaymentId": string;
},
"name": UserName,
"status": UserAccount["status"],
"isDeleted": UserAccount["isDeleted"],
"createdAt": UserAccount["createdAt"];
"updatedAt": UserAccount["updatedAt"];
"from": string;
"partner": string;
}]
type PipeMessage = Ping | SomeChange
export const usePipeSubscriber = () => {
const token = getAuthToken();
const userId = useUserStore((state) => state.userId);
const { isActiveSSETab, updateSSEValue } = useSSETab<Ticket[]>("pipe");
useSSESubscription({
enabled: Boolean(token) && Boolean(userId) && isActiveSSETab,
url:
process.env.REACT_APP_DOMAIN +
`/customer/v1.0.1/account/pipe?Authorization=${token}`,
onNewData: (data) => {
let message = data[0] as PipeMessage
updateSSEValue(message)
//Пропускаем пингование
if ('event' in message && message.event === "ping") return
if ('cart' in message) {
setCart(message.cart as string[])
cancelPayCartProcess()
}
else if ('wallet' in message) {
setWallet(message.wallet as UserAccount["wallet"])
cancelPayCartProcess()
}
else if ('name' in message) {
setNewNames(message.name as UserName)
}
else if ('status' in message) {
setUserStatus(message.status as UserAccount["status"])
}
},
marker: "pipe",
});
}

@ -74,6 +74,7 @@ export const useSSETab = <T = unknown>(
};
const setOpenTime = () => {
//Время установлено - пропускаем
if (openTimeSetted) {
return;
}

@ -1,4 +1,5 @@
import type { Attachment } from "@root/model/attachment"
import { transliterate } from 'transliteration';
type KeyValue<T = string> = {
[key: string]: T;
@ -24,11 +25,13 @@ export const jsonToFormdata = (
json: KeyValue<string | Attachment>
): FormData => {
const formData = new FormData()
if (json.egrule !== undefined) delete json.egrule
for (const key in json) {
if (!key) continue
const value = json[key]
if (typeof value !== "string") value.name = transliterate(value.name.replace(/\s/g, '_'))
if (typeof value !== "string") {
formData.append(key, convertBinaryStringToFile(value))
@ -38,6 +41,5 @@ export const jsonToFormdata = (
formData.append(key, value)
}
return formData
}

@ -1,8 +1,9 @@
import { Navigate, Outlet } from "react-router-dom"
import { Navigate, Outlet, useLocation } from "react-router-dom"
import { useUserStore } from "@root/stores/user"
export default function PrivateRoute() {
const location = useLocation()
const user = useUserStore(state => state.user)
return user ? <Outlet /> : <Navigate to="/" replace />

12271
yarn.lock

File diff suppressed because it is too large Load Diff