add husky && eslinter

This commit is contained in:
Nastya 2023-11-06 02:33:40 +03:00
parent 69e677ccaa
commit d69d27acb4
164 changed files with 12282 additions and 11471 deletions

34
.eslintrc.json Normal file

@ -0,0 +1,34 @@
{
"env": {
"browser": true,
"es2021": true
},
"extends": [],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": [
"@typescript-eslint",
"react"
],
"rules": {
"indent": [
"error",
"tab"
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"double"
],
"semi": [
"error",
"never"
]
}
}

4
.husky/pre-commit Executable file

@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
yarn eslint . --fix

@ -1,6 +1,6 @@
module.exports = {
presets: [
["@babel/preset-env", { targets: { node: "current" } }],
"@babel/preset-typescript",
],
};
presets: [
["@babel/preset-env", { targets: { node: "current" } }],
"@babel/preset-typescript",
],
}

@ -1,17 +1,17 @@
const CracoAlias = require("craco-alias");
const CracoAlias = require("craco-alias")
module.exports = {
plugins: [
{
plugin: CracoAlias,
options: {
source: "tsconfig",
// baseUrl SHOULD be specified
// plugin does not take it from tsconfig
baseUrl: "./src",
// tsConfigPath should point to the file where "baseUrl" and "paths" are specified
tsConfigPath: "./tsconfig.extend.json"
}
}
]
};
plugins: [
{
plugin: CracoAlias,
options: {
source: "tsconfig",
// baseUrl SHOULD be specified
// plugin does not take it from tsconfig
baseUrl: "./src",
// tsConfigPath should point to the file where "baseUrl" and "paths" are specified
tsConfigPath: "./tsconfig.extend.json"
}
}
]
}

@ -1,11 +1,11 @@
import { defineConfig } from "cypress";
import { defineConfig } from "cypress"
export default defineConfig({
e2e: {
viewportWidth: 1200,
viewportHeight: 800,
fixturesFolder: "tests/e2e/fixtures",
supportFile: false,
defaultCommandTimeout: 100,
},
});
e2e: {
viewportWidth: 1200,
viewportHeight: 800,
fixturesFolder: "tests/e2e/fixtures",
supportFile: false,
defaultCommandTimeout: 100,
},
})

@ -1,125 +1,125 @@
describe("Форма Входа", () => {
beforeEach(() => {
cy.visit("http://localhost:3000");
cy.wait(1000);
cy.contains("Личный кабинет").click();
});
beforeEach(() => {
cy.visit("http://localhost:3000")
cy.wait(1000)
cy.contains("Личный кабинет").click()
})
it("должна успешно входить с правильными учетными данными", () => {
const login = "valid_user@example.com";
const password = "valid_password";
it("должна успешно входить с правильными учетными данными", () => {
const login = "valid_user@example.com"
const password = "valid_password"
cy.get("#login").type(login);
cy.get("#password").type(password);
cy.get('button[type="submit"]').click();
cy.get("#login").type(login)
cy.get("#password").type(password)
cy.get("button[type=\"submit\"]").click()
cy.wait(2000);
cy.url().should("include", "http://localhost:3000/tariffs");
});
cy.wait(2000)
cy.url().should("include", "http://localhost:3000/tariffs")
})
it("должна отображать два сообщение об ошибке при отсутствии полей", () => {
cy.get('button[type="submit"]').click();
it("должна отображать два сообщение об ошибке при отсутствии полей", () => {
cy.get("button[type=\"submit\"]").click()
cy.wait(2000);
cy.get("#password-helper-text").should("contain", "Поле обязательно");
cy.get("#login-helper-text").should("contain", "Поле обязательно");
});
cy.wait(2000)
cy.get("#password-helper-text").should("contain", "Поле обязательно")
cy.get("#login-helper-text").should("contain", "Поле обязательно")
})
it("должна отображать сообщение об ошибке при отсутствии пароля", () => {
cy.get("#login").type("valid_email@example.com");
cy.get('button[type="submit"]').click();
it("должна отображать сообщение об ошибке при отсутствии пароля", () => {
cy.get("#login").type("valid_email@example.com")
cy.get("button[type=\"submit\"]").click()
cy.get("#password-helper-text").should("contain", "Поле обязательно");
});
cy.get("#password-helper-text").should("contain", "Поле обязательно")
})
it("должна отображать сообщение об ошибке при отсутствии Логина", () => {
cy.get("#password").type("valid_password");
cy.get('button[type="submit"]').click();
it("должна отображать сообщение об ошибке при отсутствии Логина", () => {
cy.get("#password").type("valid_password")
cy.get("button[type=\"submit\"]").click()
cy.get("#login-helper-text").should("contain", "Поле обязательно");
});
});
cy.get("#login-helper-text").should("contain", "Поле обязательно")
})
})
describe("Форма регистрации", () => {
beforeEach(() => {
cy.visit("http://localhost:3000");
cy.wait(1000);
cy.contains("Личный кабинет").click();
cy.contains("Регистрация").click();
});
beforeEach(() => {
cy.visit("http://localhost:3000")
cy.wait(1000)
cy.contains("Личный кабинет").click()
cy.contains("Регистрация").click()
})
it("должна регистрировать нового пользователя с правильными данными", () => {
const login = Cypress._.random(1000) + "@example.com";
const password = "valid_password";
it("должна регистрировать нового пользователя с правильными данными", () => {
const login = Cypress._.random(1000) + "@example.com"
const password = "valid_password"
cy.get("#login").type(login);
cy.get("#password").type(password);
cy.get("#repeatPassword").type(password);
cy.get("#login").type(login)
cy.get("#password").type(password)
cy.get("#repeatPassword").type(password)
cy.get('button[type="submit"]').click();
cy.wait(5000);
cy.get("button[type=\"submit\"]").click()
cy.wait(5000)
cy.url().should("include", "http://localhost:3000/tariffs");
});
cy.url().should("include", "http://localhost:3000/tariffs")
})
it("должна отображать ошибку при отсутсвии логина", () => {
cy.get("#password").type("valid_password");
cy.get("#repeatPassword").type("valid_password");
it("должна отображать ошибку при отсутсвии логина", () => {
cy.get("#password").type("valid_password")
cy.get("#repeatPassword").type("valid_password")
cy.get('button[type="submit"]').click();
cy.get("button[type=\"submit\"]").click()
cy.get("#login-helper-text").should("contain", "Поле обязательно");
});
cy.get("#login-helper-text").should("contain", "Поле обязательно")
})
it("должна отображать ошибку при отсутствии пароля", () => {
cy.get("#login").type("valid_login");
it("должна отображать ошибку при отсутствии пароля", () => {
cy.get("#login").type("valid_login")
cy.get('button[type="submit"]').click();
cy.get("button[type=\"submit\"]").click()
cy.get("#password-helper-text").should("contain", "Поле обязательно");
});
cy.get("#password-helper-text").should("contain", "Поле обязательно")
})
it("должна отображать ошибку при отсутствии поля Повторения пароля", () => {
cy.get("#login").type("valid_login");
cy.get("#password").type("valid_password");
it("должна отображать ошибку при отсутствии поля Повторения пароля", () => {
cy.get("#login").type("valid_login")
cy.get("#password").type("valid_password")
cy.get('button[type="submit"]').click();
cy.get("button[type=\"submit\"]").click()
cy.get("#repeatPassword-helper-text").should("contain", "Повторите пароль");
});
cy.get("#repeatPassword-helper-text").should("contain", "Повторите пароль")
})
it("должна отображать ошибку при некоректном пароле", () => {
cy.get("#login").type("valid_log");
cy.get("#password").type("valid@12_-_@@password");
it("должна отображать ошибку при некоректном пароле", () => {
cy.get("#login").type("valid_log")
cy.get("#password").type("valid@12_-_@@password")
cy.get('button[type="submit"]').click();
cy.get("button[type=\"submit\"]").click()
cy.get("#password-helper-text").should("contain", "Некорректные символы");
cy.get("#password-helper-text").should("contain", "Некорректные символы")
cy.get("#repeatPassword-helper-text").should("contain", "Повторите пароль");
});
cy.get("#repeatPassword-helper-text").should("contain", "Повторите пароль")
})
it("должна отображать ошибку при несовпадении паролей", () => {
cy.get("#login").type("valid_login");
cy.get("#password").type("valid_password");
cy.get("#repeatPassword").type("invalidPassword");
it("должна отображать ошибку при несовпадении паролей", () => {
cy.get("#login").type("valid_login")
cy.get("#password").type("valid_password")
cy.get("#repeatPassword").type("invalidPassword")
cy.get('button[type="submit"]').click();
cy.get("button[type=\"submit\"]").click()
cy.get("#repeatPassword-helper-text").should("contain", "Пароли не совпадают");
});
cy.get("#repeatPassword-helper-text").should("contain", "Пароли не совпадают")
})
it("попытка отправки запроса при уже зарегистрированном пользователе", () => {
const login = "valid_user@example.com";
const password = "valid_password";
it("попытка отправки запроса при уже зарегистрированном пользователе", () => {
const login = "valid_user@example.com"
const password = "valid_password"
cy.get("#login").type(login);
cy.get("#password").type(password);
cy.get("#repeatPassword").type(password);
cy.get("#login").type(login)
cy.get("#password").type(password)
cy.get("#repeatPassword").type(password)
cy.get('button[type="submit"]').click();
cy.get("button[type=\"submit\"]").click()
cy.wait(5000);
cy.contains("user with this login is exist");
});
});
cy.wait(5000)
cy.contains("user with this login is exist")
})
})

@ -9,7 +9,8 @@
"test:cart": "craco test src/utils/calcCart --transformIgnorePatterns \"node_modules/(?!@frontend)/\"",
"eject": "craco eject",
"test:cypress": "start-server-and-test start http://localhost:3000 cypress",
"cypress": "cypress open"
"cypress": "cypress open",
"prepare": "husky install"
},
"dependencies": {
"@emotion/react": "^11.10.5",
@ -23,6 +24,7 @@
"classnames": "^2.3.2",
"cypress": "^12.17.3",
"formik": "^2.2.9",
"husky": "^8.0.3",
"immer": "^10.0.2",
"isomorphic-fetch": "^3.0.0",
"notistack": "^3.0.1",
@ -48,17 +50,15 @@
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"@types/react-slick": "^0.23.10",
"@typescript-eslint/eslint-plugin": "^6.9.1",
"@typescript-eslint/parser": "^6.9.1",
"craco-alias": "^3.0.1",
"eslint": "^8.53.0",
"eslint-plugin-react": "^7.33.2",
"jest": "^29.5.0",
"react-scripts": "5.0.1",
"typescript": "^4.9.3"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",

@ -1,5 +1,5 @@
export const _mocsk_: { name: string; type: "templ" | "squiz" | "reducer" }[] = [
{ name: "Шаблонизатор", type: "templ" },
{ name: "Опросник", type: "squiz" },
{ name: "Сокращатель ссылок", type: "reducer" },
];
{ name: "Шаблонизатор", type: "templ" },
{ name: "Опросник", type: "squiz" },
{ name: "Сокращатель ссылок", type: "reducer" },
]

@ -1,5 +1,5 @@
import { showCaseTime } from "./showCaseTime";
import { showCaseVolume } from "./showCaseVolume";
import { showCaseTime } from "./showCaseTime"
import { showCaseVolume } from "./showCaseVolume"
export const showCaseObject: Record<
string,
@ -20,7 +20,7 @@ export const showCaseObject: Record<
}[]
>
> = {
templ: { volume: showCaseVolume, time: showCaseTime },
squiz: { volume: showCaseVolume, time: showCaseTime },
reducer: { volume: showCaseVolume, time: showCaseTime },
};
templ: { volume: showCaseVolume, time: showCaseTime },
squiz: { volume: showCaseVolume, time: showCaseTime },
reducer: { volume: showCaseVolume, time: showCaseTime },
}

@ -1,53 +1,53 @@
import Infinity from "../assets/Icons/tariffs-time/Infinity.svg";
import OneIcons from "../assets/Icons/tariffs-time/OneIcons.svg";
import ThreeIcons from "../assets/Icons/tariffs-time/ThreeIcons.svg";
import SixIcons from "../assets/Icons/tariffs-time/SixIcons.svg";
import NineIcons from "../assets/Icons/tariffs-time/NineIcons.svg";
import Infinity from "../assets/Icons/tariffs-time/Infinity.svg"
import OneIcons from "../assets/Icons/tariffs-time/OneIcons.svg"
import ThreeIcons from "../assets/Icons/tariffs-time/ThreeIcons.svg"
import SixIcons from "../assets/Icons/tariffs-time/SixIcons.svg"
import NineIcons from "../assets/Icons/tariffs-time/NineIcons.svg"
export const showCaseTime = [
{
name: "Безлимит",
desc: "Текст-заполнитель — это текст, который имеет",
style: { icon: Infinity, bgcolor: "#D6F3E9" },
id: "id1",
privelegeid: "1",
amount: 10,
price: 100,
},
{
name: "1 месяц",
desc: "Текст-заполнитель — это текст, который имеет",
style: { icon: OneIcons, bgcolor: "#EEE4FC" },
id: "id2",
privelegeid: "2",
amount: 10,
price: 1000,
},
{
name: "3 месяц",
desc: "Текст-заполнитель — это текст, который имеет",
style: { icon: ThreeIcons, bgcolor: "#EEE4FC" },
id: "id3",
privelegeid: "3",
amount: 10,
price: 1000,
},
{
name: "6 месяц",
desc: "Текст-заполнитель — это текст, который имеет",
style: { icon: SixIcons, bgcolor: "#EEE4FC" },
id: "id4",
privelegeid: "4",
amount: 10,
price: 1000,
},
{
name: "9 месяц",
desc: "Текст-заполнитель — это текст, который имеет",
style: { icon: NineIcons, bgcolor: "#EEE4FC" },
id: "id5",
privelegeid: "5",
amount: 10,
price: 1000,
},
];
{
name: "Безлимит",
desc: "Текст-заполнитель — это текст, который имеет",
style: { icon: Infinity, bgcolor: "#D6F3E9" },
id: "id1",
privelegeid: "1",
amount: 10,
price: 100,
},
{
name: "1 месяц",
desc: "Текст-заполнитель — это текст, который имеет",
style: { icon: OneIcons, bgcolor: "#EEE4FC" },
id: "id2",
privelegeid: "2",
amount: 10,
price: 1000,
},
{
name: "3 месяц",
desc: "Текст-заполнитель — это текст, который имеет",
style: { icon: ThreeIcons, bgcolor: "#EEE4FC" },
id: "id3",
privelegeid: "3",
amount: 10,
price: 1000,
},
{
name: "6 месяц",
desc: "Текст-заполнитель — это текст, который имеет",
style: { icon: SixIcons, bgcolor: "#EEE4FC" },
id: "id4",
privelegeid: "4",
amount: 10,
price: 1000,
},
{
name: "9 месяц",
desc: "Текст-заполнитель — это текст, который имеет",
style: { icon: NineIcons, bgcolor: "#EEE4FC" },
id: "id5",
privelegeid: "5",
amount: 10,
price: 1000,
},
]

@ -1,53 +1,53 @@
import OneIcons from "../assets/Icons/tariffs-volume/OneIcons.svg";
import TwoIcons from "../assets/Icons/tariffs-volume/TwoIcons.svg";
import ThreeIcons from "../assets/Icons/tariffs-volume/ThreeIcons.svg";
import FourIcons from "../assets/Icons/tariffs-volume/FourIcons.svg";
import FiveIcons from "../assets/Icons/tariffs-volume/FiveIcons.svg";
import OneIcons from "../assets/Icons/tariffs-volume/OneIcons.svg"
import TwoIcons from "../assets/Icons/tariffs-volume/TwoIcons.svg"
import ThreeIcons from "../assets/Icons/tariffs-volume/ThreeIcons.svg"
import FourIcons from "../assets/Icons/tariffs-volume/FourIcons.svg"
import FiveIcons from "../assets/Icons/tariffs-volume/FiveIcons.svg"
export const showCaseVolume = [
{
name: "Безлимит",
desc: "Текст-заполнитель — это текст, который имеет",
style: { icon: OneIcons, bgcolor: "#FEDFD0" },
id: "id1",
privelegeid: "1",
amount: 10,
price: 100,
},
{
name: "1 месяц",
desc: "Текст-заполнитель — это текст, который имеет",
style: { icon: TwoIcons, bgcolor: "#FEDFD0" },
id: "id2",
privelegeid: "2",
amount: 10,
price: 1000,
},
{
name: "3 месяц",
desc: "Текст-заполнитель — это текст, который имеет",
style: { icon: ThreeIcons, bgcolor: "#FEDFD0" },
id: "id3",
privelegeid: "3",
amount: 10,
price: 1000,
},
{
name: "6 месяц",
desc: "Текст-заполнитель — это текст, который имеет",
style: { icon: FourIcons, bgcolor: "#FEDFD0" },
id: "id4",
privelegeid: "4",
amount: 10,
price: 1000,
},
{
name: "9 месяц",
desc: "Текст-заполнитель — это текст, который имеет",
style: { icon: FiveIcons, bgcolor: "#FEDFD0" },
id: "id5",
privelegeid: "5",
amount: 10,
price: 1000,
},
];
{
name: "Безлимит",
desc: "Текст-заполнитель — это текст, который имеет",
style: { icon: OneIcons, bgcolor: "#FEDFD0" },
id: "id1",
privelegeid: "1",
amount: 10,
price: 100,
},
{
name: "1 месяц",
desc: "Текст-заполнитель — это текст, который имеет",
style: { icon: TwoIcons, bgcolor: "#FEDFD0" },
id: "id2",
privelegeid: "2",
amount: 10,
price: 1000,
},
{
name: "3 месяц",
desc: "Текст-заполнитель — это текст, который имеет",
style: { icon: ThreeIcons, bgcolor: "#FEDFD0" },
id: "id3",
privelegeid: "3",
amount: 10,
price: 1000,
},
{
name: "6 месяц",
desc: "Текст-заполнитель — это текст, который имеет",
style: { icon: FourIcons, bgcolor: "#FEDFD0" },
id: "id4",
privelegeid: "4",
amount: 10,
price: 1000,
},
{
name: "9 месяц",
desc: "Текст-заполнитель — это текст, который имеет",
style: { icon: FiveIcons, bgcolor: "#FEDFD0" },
id: "id5",
privelegeid: "5",
amount: 10,
price: 1000,
},
]

@ -1,76 +1,76 @@
import { makeRequest } from "@frontend/kitui";
import { makeRequest } from "@frontend/kitui"
import { parseAxiosError } from "@root/utils/parse-error";
import { parseAxiosError } from "@root/utils/parse-error"
import type {
LoginRequest,
LoginResponse,
RegisterRequest,
RegisterResponse,
} from "@frontend/kitui";
LoginRequest,
LoginResponse,
RegisterRequest,
RegisterResponse,
} from "@frontend/kitui"
const apiUrl =
process.env.NODE_ENV === "production"
? "/auth"
: "https://hub.pena.digital/auth";
? "/auth"
: "https://hub.pena.digital/auth"
export async function register(
login: string,
password: string,
phoneNumber: string
login: string,
password: string,
phoneNumber: string
): Promise<[RegisterResponse | null, string?]> {
try {
const registerResponse = await makeRequest<
try {
const registerResponse = await makeRequest<
RegisterRequest,
RegisterResponse
>({
url: apiUrl + "/register",
body: { login, password, phoneNumber },
useToken: false,
withCredentials: true,
});
url: apiUrl + "/register",
body: { login, password, phoneNumber },
useToken: false,
withCredentials: true,
})
return [registerResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [registerResponse]
} catch (nativeError) {
const [error] = parseAxiosError(nativeError)
return [null, `Не удалось зарегестрировать аккаунт. ${error}`];
}
return [null, `Не удалось зарегестрировать аккаунт. ${error}`]
}
}
export async function login(
login: string,
password: string
login: string,
password: string
): Promise<[LoginResponse | null, string?]> {
try {
const loginResponse = await makeRequest<LoginRequest, LoginResponse>({
url: apiUrl + "/login",
body: { login, password },
useToken: false,
withCredentials: true,
});
try {
const loginResponse = await makeRequest<LoginRequest, LoginResponse>({
url: apiUrl + "/login",
body: { login, password },
useToken: false,
withCredentials: true,
})
return [loginResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [loginResponse]
} catch (nativeError) {
const [error] = parseAxiosError(nativeError)
return [null, `Не удалось войти. ${error}`];
}
return [null, `Не удалось войти. ${error}`]
}
}
export async function logout(): Promise<[unknown, string?]> {
try {
const logoutResponse = await makeRequest<never, void>({
url: apiUrl + "/logout",
method: "POST",
useToken: true,
withCredentials: true,
});
try {
const logoutResponse = await makeRequest<never, void>({
url: apiUrl + "/logout",
method: "POST",
useToken: true,
withCredentials: true,
})
return [logoutResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [logoutResponse]
} catch (nativeError) {
const [error] = parseAxiosError(nativeError)
return [null, `Не удалось выйти. ${error}`];
}
return [null, `Не удалось выйти. ${error}`]
}
}

@ -1,86 +1,86 @@
import { UserAccount, makeRequest } from "@frontend/kitui";
import { AxiosError } from 'axios';
import { UserAccount, makeRequest } from "@frontend/kitui"
import { AxiosError } from "axios"
import { parseAxiosError } from "@root/utils/parse-error";
import { parseAxiosError } from "@root/utils/parse-error"
const apiUrl =
process.env.NODE_ENV === "production"
? "/customer"
: "https://hub.pena.digital/customer";
? "/customer"
: "https://hub.pena.digital/customer"
export async function patchCart(
tariffId: string
tariffId: string
): Promise<[string[], string?]> {
try {
const patchCartResponse = await makeRequest<never, UserAccount>({
url: apiUrl + `/cart?id=${tariffId}`,
method: "PATCH",
useToken: true,
});
try {
const patchCartResponse = await makeRequest<never, UserAccount>({
url: apiUrl + `/cart?id=${tariffId}`,
method: "PATCH",
useToken: true,
})
return [patchCartResponse.cart];
} catch (nativeError) {
let [error, status] = parseAxiosError(nativeError);
if (status === 400 && error.indexOf("invalid id") !== -1) error = "Данный тариф более недоступен"
return [patchCartResponse.cart]
} catch (nativeError) {
let [error, status] = parseAxiosError(nativeError)
if (status === 400 && error.indexOf("invalid id") !== -1) error = "Данный тариф более недоступен"
return [[], `Не удалось добавить товар в корзину. ${error}`];
}
return [[], `Не удалось добавить товар в корзину. ${error}`]
}
}
export async function deleteCart(
tariffId: string
tariffId: string
): Promise<[string[], string?]> {
try {
const deleteCartResponse = await makeRequest<never, UserAccount>({
url: apiUrl + `/cart?id=${tariffId}`,
method: "DELETE",
useToken: true,
});
try {
const deleteCartResponse = await makeRequest<never, UserAccount>({
url: apiUrl + `/cart?id=${tariffId}`,
method: "DELETE",
useToken: true,
})
return [deleteCartResponse.cart];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [deleteCartResponse.cart]
} catch (nativeError) {
const [error] = parseAxiosError(nativeError)
return [[], `Не удалось удалить товар из корзины. ${error}`];
}
return [[], `Не удалось удалить товар из корзины. ${error}`]
}
}
export async function payCart(): Promise<[UserAccount | null, string?]> {
try {
const payCartResponse = await makeRequest<never, UserAccount>({
url: apiUrl + "/cart/pay",
method: "POST",
useToken: true,
});
try {
const payCartResponse = await makeRequest<never, UserAccount>({
url: apiUrl + "/cart/pay",
method: "POST",
useToken: true,
})
return [payCartResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [payCartResponse]
} catch (nativeError) {
const [error] = parseAxiosError(nativeError)
return [null, `Не удалось оплатить товар из корзины. ${error}`];
}
return [null, `Не удалось оплатить товар из корзины. ${error}`]
}
}
export async function patchCurrency(
currency: string
currency: string
): Promise<[UserAccount | null, string?]> {
try {
const patchCurrencyResponse = await makeRequest<
try {
const patchCurrencyResponse = await makeRequest<
{ currency: string },
UserAccount
>({
url: apiUrl + "/wallet",
method: "PATCH",
useToken: true,
body: {
currency,
},
});
url: apiUrl + "/wallet",
method: "PATCH",
useToken: true,
body: {
currency,
},
})
return [patchCurrencyResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [patchCurrencyResponse]
} catch (nativeError) {
const [error] = parseAxiosError(nativeError)
return [null, `Не удалось изменить валюту. ${error}`];
}
return [null, `Не удалось изменить валюту. ${error}`]
}
}

@ -1,5 +1,5 @@
import { Tariff, makeRequest } from "@frontend/kitui";
import { parseAxiosError } from "@root/utils/parse-error";
import { Tariff, makeRequest } from "@frontend/kitui"
import { parseAxiosError } from "@root/utils/parse-error"
export interface GetHistoryResponse {
totalPages: number;
@ -21,17 +21,17 @@ type RawDetails = { Key: string; Value: KeyValue[][] }
type KeyValue = { Key: string; Value: string | number };
export async function getHistory(): Promise<[GetHistoryResponse | null, string?]> {
try {
const historyResponse = await makeRequest<never, GetHistoryResponse>({
url: "https://hub.pena.digital/customer/history?page=1&limit=100&type=payCart",
method: "get",
useToken: true,
});
try {
const historyResponse = await makeRequest<never, GetHistoryResponse>({
url: "https://hub.pena.digital/customer/history?page=1&limit=100&type=payCart",
method: "get",
useToken: true,
})
return [historyResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [historyResponse]
} catch (nativeError) {
const [error] = parseAxiosError(nativeError)
return [null, `Не удалось получить историю. ${error}`];
}
return [null, `Не удалось получить историю. ${error}`]
}
}

@ -1,24 +1,24 @@
import { makeRequest } from "@frontend/kitui";
import { makeRequest } from "@frontend/kitui"
import { parseAxiosError } from "@root/utils/parse-error";
import { parseAxiosError } from "@root/utils/parse-error"
import type { GetDiscountsResponse } from "@root/model/discount";
import type { GetDiscountsResponse } from "@root/model/discount"
const apiUrl = process.env.NODE_ENV === "production" ? "/price" : "https://hub.pena.digital/price";
const apiUrl = process.env.NODE_ENV === "production" ? "/price" : "https://hub.pena.digital/price"
export async function getDiscounts(signal: AbortSignal | undefined): Promise<[GetDiscountsResponse | null, string?]> {
try {
const discountsResponse = await makeRequest<never, GetDiscountsResponse>({
url: apiUrl + "/discounts",
method: "get",
useToken: true,
signal,
});
try {
const discountsResponse = await makeRequest<never, GetDiscountsResponse>({
url: apiUrl + "/discounts",
method: "get",
useToken: true,
signal,
})
return [discountsResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [discountsResponse]
} catch (nativeError) {
const [error] = parseAxiosError(nativeError)
return [null, `Ошибка получения списка скидок. ${error}`];
}
return [null, `Ошибка получения списка скидок. ${error}`]
}
}

@ -1,81 +1,81 @@
import { Tariff, makeRequest } from "@frontend/kitui";
import { CreateTariffBody } from "@root/model/customTariffs";
import { parseAxiosError } from "@root/utils/parse-error";
import { Tariff, makeRequest } from "@frontend/kitui"
import { CreateTariffBody } from "@root/model/customTariffs"
import { parseAxiosError } from "@root/utils/parse-error"
import type { ServiceKeyToPrivilegesMap } from "@root/model/privilege";
import type { GetTariffsResponse } from "@root/model/tariff";
import type { ServiceKeyToPrivilegesMap } from "@root/model/privilege"
import type { GetTariffsResponse } from "@root/model/tariff"
const apiUrl = process.env.NODE_ENV === "production" ? "/strator" : "https://hub.pena.digital/strator";
const apiUrl = process.env.NODE_ENV === "production" ? "/strator" : "https://hub.pena.digital/strator"
export async function getTariffs(
apiPage: number,
tariffsPerPage: number,
signal: AbortSignal | undefined
apiPage: number,
tariffsPerPage: number,
signal: AbortSignal | undefined
): Promise<[GetTariffsResponse | null, string?]> {
try {
const tariffsResponse = await makeRequest<never, GetTariffsResponse>({
url: apiUrl + `/tariff?page=${apiPage}&limit=${tariffsPerPage}`,
method: "get",
useToken: true,
signal,
});
try {
const tariffsResponse = await makeRequest<never, GetTariffsResponse>({
url: apiUrl + `/tariff?page=${apiPage}&limit=${tariffsPerPage}`,
method: "get",
useToken: true,
signal,
})
return [tariffsResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [tariffsResponse]
} catch (nativeError) {
const [error] = parseAxiosError(nativeError)
return [null, `Не удалось получить список тарифов. ${error}`];
}
return [null, `Не удалось получить список тарифов. ${error}`]
}
}
export async function createTariff(tariff: CreateTariffBody): Promise<[Tariff | null, string?]> {
try {
const createTariffResponse = await makeRequest<CreateTariffBody, Tariff>({
url: `${apiUrl}/tariff`,
method: "post",
useToken: true,
body: tariff,
});
try {
const createTariffResponse = await makeRequest<CreateTariffBody, Tariff>({
url: `${apiUrl}/tariff`,
method: "post",
useToken: true,
body: tariff,
})
return [createTariffResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [createTariffResponse]
} catch (nativeError) {
const [error] = parseAxiosError(nativeError)
return [null, `Не удалось создать тариф. ${error}`];
}
return [null, `Не удалось создать тариф. ${error}`]
}
}
export async function getTariffById(tariffId: string): Promise<[Tariff | null, string?, number?]> {
try {
const getTariffByIdResponse = await makeRequest<never, Tariff>({
url: `${apiUrl}/tariff/${tariffId}`,
method: "get",
useToken: true,
});
try {
const getTariffByIdResponse = await makeRequest<never, Tariff>({
url: `${apiUrl}/tariff/${tariffId}`,
method: "get",
useToken: true,
})
return [getTariffByIdResponse];
} catch (nativeError) {
const [error, status] = parseAxiosError(nativeError);
return [getTariffByIdResponse]
} catch (nativeError) {
const [error, status] = parseAxiosError(nativeError)
return [null, `Не удалось получить тарифы. ${error}`, status];
}
return [null, `Не удалось получить тарифы. ${error}`, status]
}
}
export async function getCustomTariffs(
signal: AbortSignal | undefined
signal: AbortSignal | undefined
): Promise<[ServiceKeyToPrivilegesMap | null, string?]> {
try {
const getCustomTariffsResponse = await makeRequest<null, ServiceKeyToPrivilegesMap>({
url: apiUrl + "/privilege/service",
signal,
method: "get",
useToken: true,
});
try {
const getCustomTariffsResponse = await makeRequest<null, ServiceKeyToPrivilegesMap>({
url: apiUrl + "/privilege/service",
signal,
method: "get",
useToken: true,
})
return [getCustomTariffsResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [getCustomTariffsResponse]
} catch (nativeError) {
const [error] = parseAxiosError(nativeError)
return [null, `Не удалось получить кастомные тарифы. ${error}`];
}
return [null, `Не удалось получить кастомные тарифы. ${error}`]
}
}

@ -1,49 +1,49 @@
import { makeRequest } from "@frontend/kitui";
import { parseAxiosError } from "@root/utils/parse-error";
import { makeRequest } from "@frontend/kitui"
import { parseAxiosError } from "@root/utils/parse-error"
import { SendTicketMessageRequest } from "@frontend/kitui";
import { SendTicketMessageRequest } from "@frontend/kitui"
const apiUrl =
process.env.NODE_ENV === "production"
? "/heruvym"
: "https://hub.pena.digital/heruvym";
? "/heruvym"
: "https://hub.pena.digital/heruvym"
export async function sendTicketMessage(
ticketId: string,
message: string
ticketId: string,
message: string
): Promise<[null, string?]> {
try {
const sendTicketMessageResponse = await makeRequest<
try {
const sendTicketMessageResponse = await makeRequest<
SendTicketMessageRequest,
null
>({
url: `${apiUrl}/send`,
method: "POST",
useToken: true,
body: { ticket: ticketId, message: message, lang: "ru", files: [] },
});
url: `${apiUrl}/send`,
method: "POST",
useToken: true,
body: { ticket: ticketId, message: message, lang: "ru", files: [] },
})
return [sendTicketMessageResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [sendTicketMessageResponse]
} catch (nativeError) {
const [error] = parseAxiosError(nativeError)
return [null, `Не удалось отправить сообщение. ${error}`];
}
return [null, `Не удалось отправить сообщение. ${error}`]
}
}
export async function shownMessage(id: string): Promise<[null, string?]> {
try {
const shownMessageResponse = await makeRequest<{ id: string }, null>({
url: apiUrl + "/shown",
method: "POST",
useToken: true,
body: { id },
});
try {
const shownMessageResponse = await makeRequest<{ id: string }, null>({
url: apiUrl + "/shown",
method: "POST",
useToken: true,
body: { id },
})
return [shownMessageResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [shownMessageResponse]
} catch (nativeError) {
const [error] = parseAxiosError(nativeError)
return [null, `Не удалось прочесть сообщение. ${error}`];
}
return [null, `Не удалось прочесть сообщение. ${error}`]
}
}

@ -1,29 +1,29 @@
import { User, makeRequest } from "@frontend/kitui";
import { PatchUserRequest } from "@root/model/user";
import { parseAxiosError } from "@root/utils/parse-error";
import { User, makeRequest } from "@frontend/kitui"
import { PatchUserRequest } from "@root/model/user"
import { parseAxiosError } from "@root/utils/parse-error"
const apiUrl =
process.env.NODE_ENV === "production"
? "/user"
: "https://hub.pena.digital/user";
? "/user"
: "https://hub.pena.digital/user"
export async function patchUser(
user: PatchUserRequest
user: PatchUserRequest
): Promise<[User | null, string?]> {
try {
const patchUserResponse = await makeRequest<PatchUserRequest, User>({
url: apiUrl,
contentType: true,
method: "PATCH",
useToken: true,
withCredentials: false,
body: user,
});
try {
const patchUserResponse = await makeRequest<PatchUserRequest, User>({
url: apiUrl,
contentType: true,
method: "PATCH",
useToken: true,
withCredentials: false,
body: user,
})
return [patchUserResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [patchUserResponse]
} catch (nativeError) {
const [error] = parseAxiosError(nativeError)
return [null, `Не удалось изменить пользователя. ${error}`];
}
return [null, `Не удалось изменить пользователя. ${error}`]
}
}

@ -1,76 +1,76 @@
import { makeRequest } from "@frontend/kitui";
import { makeRequest } from "@frontend/kitui"
import { jsonToFormdata } from "@root/utils/jsonToFormdata";
import { parseAxiosError } from "@root/utils/parse-error";
import { jsonToFormdata } from "@root/utils/jsonToFormdata"
import { parseAxiosError } from "@root/utils/parse-error"
import type {
Verification,
SendDocumentsArgs,
UpdateDocumentsArgs,
} from "@root/model/auth";
Verification,
SendDocumentsArgs,
UpdateDocumentsArgs,
} from "@root/model/auth"
const apiUrl =
process.env.NODE_ENV === "production"
? "/verification"
: "https://hub.pena.digital/verification";
? "/verification"
: "https://hub.pena.digital/verification"
export async function verification(
userId: string
userId: string
): Promise<[Verification | null, string?]> {
try {
const verificationResponse = await makeRequest<never, Verification>({
url: apiUrl + "/verification/" + userId,
method: "GET",
useToken: true,
withCredentials: true,
});
try {
const verificationResponse = await makeRequest<never, Verification>({
url: apiUrl + "/verification/" + userId,
method: "GET",
useToken: true,
withCredentials: true,
})
return [verificationResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [verificationResponse]
} catch (nativeError) {
const [error] = parseAxiosError(nativeError)
return [null, `Ошибка запроса верификации. ${error}`];
}
return [null, `Ошибка запроса верификации. ${error}`]
}
}
export async function sendDocuments(
documents: SendDocumentsArgs
documents: SendDocumentsArgs
): Promise<[Verification | "OK" | null, string?]> {
try {
const sendDocumentsResponse = await makeRequest<FormData, Verification>({
url: apiUrl + "/verification",
method: "POST",
useToken: true,
withCredentials: true,
body: jsonToFormdata({ ...documents, egrule: documents.inn }),
});
try {
const sendDocumentsResponse = await makeRequest<FormData, Verification>({
url: apiUrl + "/verification",
method: "POST",
useToken: true,
withCredentials: true,
body: jsonToFormdata({ ...documents, egrule: documents.inn }),
})
return [sendDocumentsResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [sendDocumentsResponse]
} catch (nativeError) {
const [error] = parseAxiosError(nativeError)
return [null, `Ошибка отправки документов. ${error}`];
}
return [null, `Ошибка отправки документов. ${error}`]
}
}
export async function updateDocuments(
documents: UpdateDocumentsArgs
documents: UpdateDocumentsArgs
): Promise<[Verification | "OK" | null, string? ]> {
try {
const updateDocumentsResponse = await makeRequest<FormData, Verification>({
url: apiUrl + "/verification/file",
method: "PATCH",
useToken: true,
withCredentials: true,
body: jsonToFormdata(
documents.inn ? { ...documents, egrule: documents.inn } : documents
),
});
try {
const updateDocumentsResponse = await makeRequest<FormData, Verification>({
url: apiUrl + "/verification/file",
method: "PATCH",
useToken: true,
withCredentials: true,
body: jsonToFormdata(
documents.inn ? { ...documents, egrule: documents.inn } : documents
),
})
return [updateDocumentsResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [updateDocumentsResponse]
} catch (nativeError) {
const [error] = parseAxiosError(nativeError)
return [null, `Ошибка обновления документов. ${error}`];
}
return [null, `Ошибка обновления документов. ${error}`]
}
}

@ -1,48 +1,48 @@
import { makeRequest } from "@frontend/kitui";
import { SendPaymentRequest, SendPaymentResponse } from "@root/model/wallet";
import { parseAxiosError } from "@root/utils/parse-error";
import { makeRequest } from "@frontend/kitui"
import { SendPaymentRequest, SendPaymentResponse } from "@root/model/wallet"
import { parseAxiosError } from "@root/utils/parse-error"
const apiUrl =
process.env.NODE_ENV === "production"
? "/customer"
: "https://hub.pena.digital/customer";
? "/customer"
: "https://hub.pena.digital/customer"
const testPaymentBody: SendPaymentRequest = {
type: "bankCard",
amount: 15020,
currency: "RUB",
bankCard: {
number: "RUB",
expiryYear: "2021",
expiryMonth: "05",
csc: "05",
cardholder: "IVAN IVANOV",
},
phoneNumber: "79000000000",
login: "login_test",
returnUrl: window.location.origin + "/wallet",
};
type: "bankCard",
amount: 15020,
currency: "RUB",
bankCard: {
number: "RUB",
expiryYear: "2021",
expiryMonth: "05",
csc: "05",
cardholder: "IVAN IVANOV",
},
phoneNumber: "79000000000",
login: "login_test",
returnUrl: window.location.origin + "/wallet",
}
export async function sendPayment(
body: SendPaymentRequest = testPaymentBody
body: SendPaymentRequest = testPaymentBody
): Promise<[SendPaymentResponse | null, string?]> {
try {
const sendPaymentResponse = await makeRequest<
try {
const sendPaymentResponse = await makeRequest<
SendPaymentRequest,
SendPaymentResponse
>({
url: apiUrl + "/wallet",
contentType: true,
method: "POST",
useToken: true,
withCredentials: false,
body,
});
url: apiUrl + "/wallet",
contentType: true,
method: "POST",
useToken: true,
withCredentials: false,
body,
})
return [sendPaymentResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [sendPaymentResponse]
} catch (nativeError) {
const [error] = parseAxiosError(nativeError)
return [null, `Ошибка оплаты. ${error}`];
}
return [null, `Ошибка оплаты. ${error}`]
}
}

@ -1,6 +1,6 @@
import { Box, SxProps, Theme, Typography, useMediaQuery, useTheme } from "@mui/material";
import { PenaLink } from "@frontend/kitui";
import { Link as RouterLink } from "react-router-dom";
import { Box, SxProps, Theme, Typography, useMediaQuery, useTheme } from "@mui/material"
import { PenaLink } from "@frontend/kitui"
import { Link as RouterLink } from "react-router-dom"
interface Props {
image?: string;
@ -12,21 +12,21 @@ interface Props {
}
export default function CardWithLink({ image, headerText, text, linkHref, isHighlighted = false, sx }: Props) {
const theme = useTheme();
const theme = useTheme()
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
flexGrow: 1,
alignItems: "start",
p: "20px",
maxWidth: "360px",
backgroundColor: isHighlighted ? theme.palette.purple.main : "#434657",
borderRadius: "12px",
color: "white",
boxShadow: `
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
flexGrow: 1,
alignItems: "start",
p: "20px",
maxWidth: "360px",
backgroundColor: isHighlighted ? theme.palette.purple.main : "#434657",
borderRadius: "12px",
color: "white",
boxShadow: `
0px 100px 309px rgba(37, 39, 52, 0.24),
0px 41.7776px 129.093px rgba(37, 39, 52, 0.172525),
0px 22.3363px 69.0192px rgba(37, 39, 52, 0.143066),
@ -34,38 +34,38 @@ export default function CardWithLink({ image, headerText, text, linkHref, isHigh
0px 6.6501px 20.5488px rgba(37, 39, 52, 0.0969343),
0px 2.76726px 8.55082px rgba(37, 39, 52, 0.0674749)
`,
...sx,
}}
>
{image && (
<img
src={image}
alt=""
style={{
objectFit: "contain",
width: "100%",
display: "block",
marginTop: "calc(-18px - 11%)",
pointerEvents: "none",
}}
/>
)}
<Typography variant="h5">{headerText}</Typography>
<Typography mt="20px" mb="29px">
{text}
</Typography>
<PenaLink
component={RouterLink}
to={linkHref}
sx={{
color: "white",
textDecoration: "underline",
mt: "auto",
mb: "15px",
}}
>
...sx,
}}
>
{image && (
<img
src={image}
alt=""
style={{
objectFit: "contain",
width: "100%",
display: "block",
marginTop: "calc(-18px - 11%)",
pointerEvents: "none",
}}
/>
)}
<Typography variant="h5">{headerText}</Typography>
<Typography mt="20px" mb="29px">
{text}
</Typography>
<PenaLink
component={RouterLink}
to={linkHref}
sx={{
color: "white",
textDecoration: "underline",
mt: "auto",
mb: "15px",
}}
>
Подробнее
</PenaLink>
</Box>
);
</PenaLink>
</Box>
)
}

@ -1,5 +1,5 @@
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
import { isDateToday } from "@root/utils/date";
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material"
import { isDateToday } from "@root/utils/date"
interface Props {
@ -10,81 +10,81 @@ interface Props {
}
export default function ChatMessage({ unAuthenticated = false, isSelf, text, createdAt }: Props) {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const theme = useTheme()
const upMd = useMediaQuery(theme.breakpoints.up("md"))
const messageBackgroundColor = isSelf ? "white" : unAuthenticated ? "#EFF0F5" : theme.palette.gray.main;
const messageBackgroundColor = isSelf ? "white" : unAuthenticated ? "#EFF0F5" : theme.palette.gray.main
const date = new Date(createdAt);
const time = date.toLocaleString([], {
hour: "2-digit",
minute: "2-digit",
...(!isDateToday(date) && { year: "2-digit", month: "2-digit", day: "2-digit" })
});
const date = new Date(createdAt)
const time = date.toLocaleString([], {
hour: "2-digit",
minute: "2-digit",
...(!isDateToday(date) && { year: "2-digit", month: "2-digit", day: "2-digit" })
})
return (
<Box
sx={{
display: "flex",
alignSelf: isSelf ? "end" : "start",
gap: "9px",
pl: isSelf ? undefined : "8px",
pr: isSelf ? "8px" : undefined,
}}
>
<Typography
sx={{
alignSelf: "end",
fontWeight: 400,
fontSize: "14px",
lineHeight: "17px",
order: isSelf ? 1 : 2,
color: theme.palette.gray.main,
mb: "-4px",
whiteSpace: "nowrap",
}}
>{time}</Typography>
<Box
sx={{
backgroundColor: messageBackgroundColor,
border: unAuthenticated ? `1px solid #E3E3E3` : `1px solid ${theme.palette.gray.main}`,
order: isSelf ? 2 : 1,
p: upMd ? "18px" : "12px",
borderRadius: "8px",
maxWidth: "464px",
color: (isSelf || unAuthenticated) ? theme.palette.gray.dark : "white",
position: "relative",
}}
>
<svg
style={{
position: "absolute",
top: "-1px",
right: isSelf ? "-8px" : undefined,
left: isSelf ? undefined : "-8px",
transform: isSelf ? undefined : "scale(-1, 1)",
}}
xmlns="http://www.w3.org/2000/svg"
width="16"
height="8"
viewBox="0 0 16 8"
fill="none"
>
<path d="M0.5 0.5L15.5 0.500007
return (
<Box
sx={{
display: "flex",
alignSelf: isSelf ? "end" : "start",
gap: "9px",
pl: isSelf ? undefined : "8px",
pr: isSelf ? "8px" : undefined,
}}
>
<Typography
sx={{
alignSelf: "end",
fontWeight: 400,
fontSize: "14px",
lineHeight: "17px",
order: isSelf ? 1 : 2,
color: theme.palette.gray.main,
mb: "-4px",
whiteSpace: "nowrap",
}}
>{time}</Typography>
<Box
sx={{
backgroundColor: messageBackgroundColor,
border: unAuthenticated ? "1px solid #E3E3E3" : `1px solid ${theme.palette.gray.main}`,
order: isSelf ? 2 : 1,
p: upMd ? "18px" : "12px",
borderRadius: "8px",
maxWidth: "464px",
color: (isSelf || unAuthenticated) ? theme.palette.gray.dark : "white",
position: "relative",
}}
>
<svg
style={{
position: "absolute",
top: "-1px",
right: isSelf ? "-8px" : undefined,
left: isSelf ? undefined : "-8px",
transform: isSelf ? undefined : "scale(-1, 1)",
}}
xmlns="http://www.w3.org/2000/svg"
width="16"
height="8"
viewBox="0 0 16 8"
fill="none"
>
<path d="M0.5 0.5L15.5 0.500007
C10 0.500006 7.5 8 7.5 7.5H7.5H0.5V0.5Z"
fill={messageBackgroundColor}
stroke={unAuthenticated ? "#E3E3E3" : theme.palette.gray.main}
/>
<rect y="1" width="8" height="8" fill={messageBackgroundColor} />
</svg>
<Typography
sx={{
fontWeight: 400,
fontSize: "16px",
lineHeight: "19px",
}}
>{text}</Typography>
</Box>
</Box >
);
fill={messageBackgroundColor}
stroke={unAuthenticated ? "#E3E3E3" : theme.palette.gray.main}
/>
<rect y="1" width="8" height="8" fill={messageBackgroundColor} />
</svg>
<Typography
sx={{
fontWeight: 400,
fontSize: "16px",
lineHeight: "19px",
}}
>{text}</Typography>
</Box>
</Box >
)
}

@ -1,4 +1,4 @@
import { SxProps, Theme, Typography, useTheme } from "@mui/material";
import { SxProps, Theme, Typography, useTheme } from "@mui/material"
interface Props {
@ -8,24 +8,24 @@ interface Props {
}
export default function ComplexHeader({ text1, text2, sx }: Props) {
const theme = useTheme();
const theme = useTheme()
return (
<Typography variant="h4" sx={sx}>
{text1}
{text2 &&
return (
<Typography variant="h4" sx={sx}>
{text1}
{text2 &&
<Typography
component="span"
sx={{
fontWeight: "inherit",
fontSize: "inherit",
lineHeight: "inherit",
color: theme.palette.purple.main,
}}
component="span"
sx={{
fontWeight: "inherit",
fontSize: "inherit",
lineHeight: "inherit",
color: theme.palette.purple.main,
}}
>
{text2}
{text2}
</Typography>
}
</Typography>
);
}
</Typography>
)
}

@ -1,7 +1,7 @@
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
import { useState } from "react";
import ExpandIcon from "./icons/ExpandIcon";
import type { ReactNode } from "react";
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material"
import { useState } from "react"
import ExpandIcon from "./icons/ExpandIcon"
import type { ReactNode } from "react"
interface Props {
header: ReactNode;
@ -13,91 +13,91 @@ interface Props {
}
export default function CustomAccordion({ header, text, divide = false, price, last, first }: Props) {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const upXs = useMediaQuery(theme.breakpoints.up("xs"));
const [isExpanded, setIsExpanded] = useState<boolean>(false);
const theme = useTheme()
const upMd = useMediaQuery(theme.breakpoints.up("md"))
const upXs = useMediaQuery(theme.breakpoints.up("xs"))
const [isExpanded, setIsExpanded] = useState<boolean>(false)
return (
<Box
sx={{
backgroundColor: "white",
borderTopLeftRadius: first ? "12px" : "0",
borderTopRightRadius: first ? "12px" : "0",
borderBottomLeftRadius: last ? "12px" : "0",
borderBottomRightRadius: last ? "12px" : "0",
borderBottom: `1px solid ${theme.palette.gray.main}`,
return (
<Box
sx={{
backgroundColor: "white",
borderTopLeftRadius: first ? "12px" : "0",
borderTopRightRadius: first ? "12px" : "0",
borderBottomLeftRadius: last ? "12px" : "0",
borderBottomRightRadius: last ? "12px" : "0",
borderBottom: `1px solid ${theme.palette.gray.main}`,
...(last && { borderBottom: "none" }),
}}
>
<Box
onClick={() => setIsExpanded((prev) => !prev)}
sx={{
minHeight: "72px",
px: "20px",
display: "flex",
alignItems: "stretch",
justifyContent: "space-between",
cursor: "pointer",
userSelect: "none",
rowGap: "10px",
flexDirection: upXs ? undefined : "column",
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
width: "100%",
fontSize: upMd ? undefined : "16px",
lineHeight: upMd ? undefined : "19px",
fontWeight: 500,
color: theme.palette.gray.dark,
px: 0,
}}
>
{header}
</Box>
<Box
sx={{
pl: "20px",
width: "52px",
display: "flex",
alignItems: "center",
justifyContent: "center",
borderLeft: divide ? "1px solid #000000" : "none",
}}
>
<ExpandIcon isExpanded={isExpanded} />
</Box>
</Box>
{isExpanded && (
<Box
sx={{
display: "flex",
justifyContent: "space-between",
px: "20px",
py: upMd ? "25px" : undefined,
pt: upMd ? undefined : "15px",
pb: upMd ? undefined : "25px",
backgroundColor: "#F1F2F6",
}}
>
<Typography
sx={{
fontSize: upMd ? undefined : "16px",
lineHeight: upMd ? undefined : "19px",
color: theme.palette.gray.dark,
}}
>
{text}
</Typography>
<Typography sx={{ display: price ? "block" : "none", fontSize: "18px", mr: "120px" }}>
{price} руб.
</Typography>
</Box>
)}
</Box>
);
...(last && { borderBottom: "none" }),
}}
>
<Box
onClick={() => setIsExpanded((prev) => !prev)}
sx={{
minHeight: "72px",
px: "20px",
display: "flex",
alignItems: "stretch",
justifyContent: "space-between",
cursor: "pointer",
userSelect: "none",
rowGap: "10px",
flexDirection: upXs ? undefined : "column",
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
width: "100%",
fontSize: upMd ? undefined : "16px",
lineHeight: upMd ? undefined : "19px",
fontWeight: 500,
color: theme.palette.gray.dark,
px: 0,
}}
>
{header}
</Box>
<Box
sx={{
pl: "20px",
width: "52px",
display: "flex",
alignItems: "center",
justifyContent: "center",
borderLeft: divide ? "1px solid #000000" : "none",
}}
>
<ExpandIcon isExpanded={isExpanded} />
</Box>
</Box>
{isExpanded && (
<Box
sx={{
display: "flex",
justifyContent: "space-between",
px: "20px",
py: upMd ? "25px" : undefined,
pt: upMd ? undefined : "15px",
pb: upMd ? undefined : "25px",
backgroundColor: "#F1F2F6",
}}
>
<Typography
sx={{
fontSize: upMd ? undefined : "16px",
lineHeight: upMd ? undefined : "19px",
color: theme.palette.gray.dark,
}}
>
{text}
</Typography>
<Typography sx={{ display: price ? "block" : "none", fontSize: "18px", mr: "120px" }}>
{price} руб.
</Typography>
</Box>
)}
</Box>
)
}

@ -1,7 +1,7 @@
import { Box, useMediaQuery, useTheme } from "@mui/material";
import { useState } from "react";
import ExpandIcon from "./icons/ExpandIcon";
import type { ReactNode } from "react";
import { Box, useMediaQuery, useTheme } from "@mui/material"
import { useState } from "react"
import ExpandIcon from "./icons/ExpandIcon"
import type { ReactNode } from "react"
interface Props {
header: ReactNode;
@ -12,66 +12,66 @@ interface Props {
}
export default function CustomSaveAccordion({ header, divide = false, privilege, last, first }: Props) {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const upXs = useMediaQuery(theme.breakpoints.up("xs"));
const [isExpanded, setIsExpanded] = useState<boolean>(false);
const theme = useTheme()
const upMd = useMediaQuery(theme.breakpoints.up("md"))
const upXs = useMediaQuery(theme.breakpoints.up("xs"))
const [isExpanded, setIsExpanded] = useState<boolean>(false)
return (
<Box
sx={{
backgroundColor: "white",
borderTopLeftRadius: first ? "12px" : "0",
borderTopRightRadius: first ? "12px" : "0",
borderBottomLeftRadius: last ? "12px" : "0",
borderBottomRightRadius: last ? "12px" : "0",
borderBottom: `1px solid ${theme.palette.gray.main}`,
return (
<Box
sx={{
backgroundColor: "white",
borderTopLeftRadius: first ? "12px" : "0",
borderTopRightRadius: first ? "12px" : "0",
borderBottomLeftRadius: last ? "12px" : "0",
borderBottomRightRadius: last ? "12px" : "0",
borderBottom: `1px solid ${theme.palette.gray.main}`,
...(last && { borderBottom: "none" }),
}}
>
<Box
onClick={() => setIsExpanded((prev) => !prev)}
sx={{
minHeight: "72px",
px: "20px",
display: "flex",
alignItems: "stretch",
justifyContent: "space-between",
cursor: "pointer",
userSelect: "none",
rowGap: "10px",
flexDirection: upXs ? undefined : "column",
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
width: "100%",
fontSize: upMd ? undefined : "16px",
lineHeight: upMd ? undefined : "19px",
fontWeight: 500,
color: theme.palette.gray.dark,
px: 0,
}}
>
{header}
</Box>
<Box
sx={{
pl: "20px",
width: "52px",
display: "flex",
alignItems: "center",
justifyContent: "center",
borderLeft: divide ? "1px solid #000000" : "none",
}}
>
<ExpandIcon isExpanded={isExpanded} />
</Box>
</Box>
{isExpanded && privilege}
</Box>
);
...(last && { borderBottom: "none" }),
}}
>
<Box
onClick={() => setIsExpanded((prev) => !prev)}
sx={{
minHeight: "72px",
px: "20px",
display: "flex",
alignItems: "stretch",
justifyContent: "space-between",
cursor: "pointer",
userSelect: "none",
rowGap: "10px",
flexDirection: upXs ? undefined : "column",
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
width: "100%",
fontSize: upMd ? undefined : "16px",
lineHeight: upMd ? undefined : "19px",
fontWeight: 500,
color: theme.palette.gray.dark,
px: 0,
}}
>
{header}
</Box>
<Box
sx={{
pl: "20px",
width: "52px",
display: "flex",
alignItems: "center",
justifyContent: "center",
borderLeft: divide ? "1px solid #000000" : "none",
}}
>
<ExpandIcon isExpanded={isExpanded} />
</Box>
</Box>
{isExpanded && privilege}
</Box>
)
}

@ -1,5 +1,5 @@
import { useState, useEffect } from "react";
import { Slider, useTheme } from "@mui/material";
import { useState, useEffect } from "react"
import { Slider, useTheme } from "@mui/material"
type CustomSliderProps = {
value: number;
@ -9,68 +9,68 @@ type CustomSliderProps = {
};
export const CustomSlider = ({
value,
min = 0,
max = 100,
onChange,
value,
min = 0,
max = 100,
onChange,
}: CustomSliderProps) => {
const theme = useTheme();
const [step, setStep] = useState<number>(1);
const theme = useTheme()
const [step, setStep] = useState<number>(1)
useEffect(() => {
if (value < 100) {
return setStep(10);
}
useEffect(() => {
if (value < 100) {
return setStep(10)
}
if (value < 500) {
return setStep(20);
}
if (value < 500) {
return setStep(20)
}
if (value < 2000) {
return setStep(50);
}
if (value < 2000) {
return setStep(50)
}
setStep(150);
}, [value]);
setStep(150)
}, [value])
const handleChange = ({ type }: Event, newValue: number | number[]) => {
// Для корректной работы слайдера в FireFox
if (type !== "change") {
onChange(newValue);
}
};
const handleChange = ({ type }: Event, newValue: number | number[]) => {
// Для корректной работы слайдера в FireFox
if (type !== "change") {
onChange(newValue)
}
}
return (
<Slider
value={value}
defaultValue={0}
min={min}
max={max}
step={step}
onChange={handleChange}
sx={{
color: theme.palette.purple.main,
height: "12px",
"& .MuiSlider-track": {
border: "none",
},
"& .MuiSlider-rail": {
backgroundColor: "#F2F3F7",
border: `1px solid ${theme.palette.gray.main}`,
},
"& .MuiSlider-thumb": {
height: 32,
width: 32,
border: `6px solid ${theme.palette.purple.main}`,
backgroundColor: "white",
boxShadow: `0px 0px 0px 3px white,
return (
<Slider
value={value}
defaultValue={0}
min={min}
max={max}
step={step}
onChange={handleChange}
sx={{
color: theme.palette.purple.main,
height: "12px",
"& .MuiSlider-track": {
border: "none",
},
"& .MuiSlider-rail": {
backgroundColor: "#F2F3F7",
border: `1px solid ${theme.palette.gray.main}`,
},
"& .MuiSlider-thumb": {
height: 32,
width: 32,
border: `6px solid ${theme.palette.purple.main}`,
backgroundColor: "white",
boxShadow: `0px 0px 0px 3px white,
0px 4px 4px 3px #C3C8DD`,
"&:focus, &:hover, &.Mui-active, &.Mui-focusVisible": {
boxShadow: `0px 0px 0px 3px white,
"&:focus, &:hover, &.Mui-active, &.Mui-focusVisible": {
boxShadow: `0px 0px 0px 3px white,
0px 4px 4px 3px #C3C8DD`,
},
},
}}
/>
);
};
},
},
}}
/>
)
}

@ -1,19 +1,19 @@
import { Tab } from "@mui/material";
import { styled } from "@mui/material";
import { Tab } from "@mui/material"
import { styled } from "@mui/material"
export const CustomTab = styled(Tab)(({ theme }) => ({
...theme.typography.body2,
color: theme.palette.primary.main,
padding: "15px",
minWidth: 0,
"&.Mui-selected": {
color: theme.palette.purple.main,
textUnderlinePosition: "under",
textDecoration: "underline",
textUnderlineOffset: "3px",
},
"&:first-of-type": {
paddingLeft: 0,
}
}));
...theme.typography.body2,
color: theme.palette.primary.main,
padding: "15px",
minWidth: 0,
"&.Mui-selected": {
color: theme.palette.purple.main,
textUnderlinePosition: "under",
textDecoration: "underline",
textUnderlineOffset: "3px",
},
"&:first-of-type": {
paddingLeft: 0,
}
}))

@ -1,159 +1,159 @@
import { useState } from "react";
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
import ExpandIcon from "@components/icons/ExpandIcon";
import { currencyFormatter } from "@root/utils/currencyFormatter";
import { removeTariffFromCart } from "@root/stores/user";
import { enqueueSnackbar } from "notistack";
import { useState } from "react"
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material"
import ExpandIcon from "@components/icons/ExpandIcon"
import { currencyFormatter } from "@root/utils/currencyFormatter"
import { removeTariffFromCart } from "@root/stores/user"
import { enqueueSnackbar } from "notistack"
import {
CloseButton,
TariffCartData,
getMessageFromFetchError,
} from "@frontend/kitui";
CloseButton,
TariffCartData,
getMessageFromFetchError,
} from "@frontend/kitui"
import type { MouseEvent } from "react";
import type { MouseEvent } from "react"
interface Props {
tariffCartData: TariffCartData;
}
export default function CustomTariffAccordion({ tariffCartData }: Props) {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const upSm = useMediaQuery(theme.breakpoints.up("sm"));
const [isExpanded, setIsExpanded] = useState<boolean>(false);
const theme = useTheme()
const upMd = useMediaQuery(theme.breakpoints.up("md"))
const upSm = useMediaQuery(theme.breakpoints.up("sm"))
const [isExpanded, setIsExpanded] = useState<boolean>(false)
function handleDeleteClick(event: MouseEvent<HTMLButtonElement>) {
event.stopPropagation();
function handleDeleteClick(event: MouseEvent<HTMLButtonElement>) {
event.stopPropagation()
removeTariffFromCart(tariffCartData.id)
.then(() => {
enqueueSnackbar("Тариф удален");
})
.catch((error) => {
const message = getMessageFromFetchError(error);
if (message) enqueueSnackbar(message);
});
}
removeTariffFromCart(tariffCartData.id)
.then(() => {
enqueueSnackbar("Тариф удален")
})
.catch((error) => {
const message = getMessageFromFetchError(error)
if (message) enqueueSnackbar(message)
})
}
return (
<Box
sx={{
overflow: "hidden",
}}
>
<Box
sx={{
backgroundColor: "white",
}}
>
<Box
onClick={() => setIsExpanded((prev) => !prev)}
sx={{
height: "72px",
pr: "20px",
pl: "30px",
display: "flex",
gap: "15px",
alignItems: "center",
justifyContent: "space-between",
cursor: "pointer",
userSelect: "none",
}}
>
<Box
sx={{
width: "50px",
height: "100%",
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<ExpandIcon isExpanded={isExpanded} />
</Box>
<Typography
sx={{
width: "100%",
fontSize: upMd ? "20px" : "16px",
lineHeight: upMd ? undefined : "19px",
fontWeight: 400,
color: theme.palette.text.secondary,
px: 0,
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
return (
<Box
sx={{
overflow: "hidden",
}}
>
<Box
sx={{
backgroundColor: "white",
}}
>
<Box
onClick={() => setIsExpanded((prev) => !prev)}
sx={{
height: "72px",
pr: "20px",
pl: "30px",
display: "flex",
gap: "15px",
alignItems: "center",
justifyContent: "space-between",
cursor: "pointer",
userSelect: "none",
}}
>
<Box
sx={{
width: "50px",
height: "100%",
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<ExpandIcon isExpanded={isExpanded} />
</Box>
<Typography
sx={{
width: "100%",
fontSize: upMd ? "20px" : "16px",
lineHeight: upMd ? undefined : "19px",
fontWeight: 400,
color: theme.palette.text.secondary,
px: 0,
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
Кастомный тариф
</Typography>
<Box
sx={{
display: "flex",
justifyContent: "flex-end",
height: "100%",
alignItems: "center",
gap: upSm ? "111px" : "17px",
}}
>
<Typography
sx={{
color: theme.palette.gray.dark,
fontSize: upSm ? "20px" : "16px",
fontWeight: 500,
}}
>
{currencyFormatter.format(tariffCartData.price / 100)}
</Typography>
</Box>
<CloseButton
style={{ height: "22 px", width: "22px" }}
onClick={handleDeleteClick}
/>
</Box>
{isExpanded &&
</Typography>
<Box
sx={{
display: "flex",
justifyContent: "flex-end",
height: "100%",
alignItems: "center",
gap: upSm ? "111px" : "17px",
}}
>
<Typography
sx={{
color: theme.palette.gray.dark,
fontSize: upSm ? "20px" : "16px",
fontWeight: 500,
}}
>
{currencyFormatter.format(tariffCartData.price / 100)}
</Typography>
</Box>
<CloseButton
style={{ height: "22 px", width: "22px" }}
onClick={handleDeleteClick}
/>
</Box>
{isExpanded &&
tariffCartData.privileges.map((privilege) => (
<Box
key={privilege.privilegeId}
sx={{
px: "50px",
py: upMd ? "15px" : undefined,
pt: upMd ? undefined : "15px",
pb: upMd ? undefined : "25px",
backgroundColor: "#F1F2F6",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
gap: "15px",
}}
>
<Typography
sx={{
fontSize: upMd ? undefined : "16px",
lineHeight: upMd ? undefined : "19px",
color: theme.palette.gray.dark,
width: "100%",
}}
>
{privilege.description}
</Typography>
<Box
sx={{
width: upSm ? "140px" : "123px",
marginRight: upSm ? "65px" : 0,
}}
>
<Typography
sx={{
color: theme.palette.gray.dark,
fontSize: upSm ? "20px" : "16px",
fontWeight: 500,
}}
>
{currencyFormatter.format(privilege.price / 100)}
</Typography>
</Box>
</Box>
<Box
key={privilege.privilegeId}
sx={{
px: "50px",
py: upMd ? "15px" : undefined,
pt: upMd ? undefined : "15px",
pb: upMd ? undefined : "25px",
backgroundColor: "#F1F2F6",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
gap: "15px",
}}
>
<Typography
sx={{
fontSize: upMd ? undefined : "16px",
lineHeight: upMd ? undefined : "19px",
color: theme.palette.gray.dark,
width: "100%",
}}
>
{privilege.description}
</Typography>
<Box
sx={{
width: upSm ? "140px" : "123px",
marginRight: upSm ? "65px" : 0,
}}
>
<Typography
sx={{
color: theme.palette.gray.dark,
fontSize: upSm ? "20px" : "16px",
fontWeight: 500,
}}
>
{currencyFormatter.format(privilege.price / 100)}
</Typography>
</Box>
</Box>
))}
</Box>
</Box>
);
</Box>
</Box>
)
}

@ -1,244 +1,244 @@
import { useState } from "react";
import { useState } from "react"
import {
Box,
SvgIcon,
IconButton,
Typography,
CircularProgress,
useMediaQuery,
useTheme,
} from "@mui/material";
Box,
SvgIcon,
IconButton,
Typography,
CircularProgress,
useMediaQuery,
useTheme,
} from "@mui/material"
import ClearIcon from "@mui/icons-material/Clear";
import { currencyFormatter } from "@root/utils/currencyFormatter";
import { useUserStore, removeTariffFromCart, setCart } from "@root/stores/user";
import { enqueueSnackbar } from "notistack";
import { ServiceCartData, getMessageFromFetchError } from "@frontend/kitui";
import ExpandIcon from "@components/icons/ExpandIcon";
import { deleteCart } from "@root/api/cart";
import ClearIcon from "@mui/icons-material/Clear"
import { currencyFormatter } from "@root/utils/currencyFormatter"
import { useUserStore, removeTariffFromCart, setCart } from "@root/stores/user"
import { enqueueSnackbar } from "notistack"
import { ServiceCartData, getMessageFromFetchError } from "@frontend/kitui"
import ExpandIcon from "@components/icons/ExpandIcon"
import { deleteCart } from "@root/api/cart"
import { ReactComponent as CrossIcon } from "@root/assets/Icons/cross.svg";
import { ReactComponent as CrossIcon } from "@root/assets/Icons/cross.svg"
import type { MouseEvent } from "react";
import CustomTariffAccordion from "@root/components/CustomTariffAccordion";
import type { MouseEvent } from "react"
import CustomTariffAccordion from "@root/components/CustomTariffAccordion"
const name: Record<string, string> = {
templategen: "Шаблонизатор",
squiz: "Опросник",
reducer: "Скоращатель ссылок",
custom: "Кастомные тарифы",
};
templategen: "Шаблонизатор",
squiz: "Опросник",
reducer: "Скоращатель ссылок",
custom: "Кастомные тарифы",
}
interface Props {
serviceData: ServiceCartData;
}
export default function CustomWrapperDrawer({ serviceData }: Props) {
const [isExpanded, setIsExpanded] = useState<boolean>(false);
const [isLoading, setIsLoading] = useState<boolean>(false);
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const upSm = useMediaQuery(theme.breakpoints.up("sm"));
const userAccount = useUserStore((state) => state.userAccount);
const [isExpanded, setIsExpanded] = useState<boolean>(false)
const [isLoading, setIsLoading] = useState<boolean>(false)
const theme = useTheme()
const upMd = useMediaQuery(theme.breakpoints.up("md"))
const upSm = useMediaQuery(theme.breakpoints.up("sm"))
const userAccount = useUserStore((state) => state.userAccount)
function handleItemDeleteClick(tariffId: string) {
removeTariffFromCart(tariffId)
.then(() => {
enqueueSnackbar("Тариф удален");
})
.catch((error) => {
const message = getMessageFromFetchError(error);
if (message) enqueueSnackbar(message);
});
}
function handleItemDeleteClick(tariffId: string) {
removeTariffFromCart(tariffId)
.then(() => {
enqueueSnackbar("Тариф удален")
})
.catch((error) => {
const message = getMessageFromFetchError(error)
if (message) enqueueSnackbar(message)
})
}
const deleteService = async (event: MouseEvent<HTMLButtonElement>) => {
event.stopPropagation();
let cartItems: string[] = userAccount?.cart || [];
const errors: string[] = [];
const deleteService = async (event: MouseEvent<HTMLButtonElement>) => {
event.stopPropagation()
let cartItems: string[] = userAccount?.cart || []
const errors: string[] = []
setIsLoading(true);
setIsLoading(true)
for (const { id } of serviceData.tariffs) {
const [cartItemsResponse, deleteError] = await deleteCart(id);
for (const { id } of serviceData.tariffs) {
const [cartItemsResponse, deleteError] = await deleteCart(id)
if (deleteError) {
errors.push(deleteError);
} else {
cartItems = cartItemsResponse;
}
}
if (deleteError) {
errors.push(deleteError)
} else {
cartItems = cartItemsResponse
}
}
setCart(cartItems);
setIsLoading(false);
errors.forEach(enqueueSnackbar);
};
setCart(cartItems)
setIsLoading(false)
errors.forEach(enqueueSnackbar)
}
return (
<Box sx={{ display: "flex", alignItems: "center" }}>
<Box
sx={{
overflow: "hidden",
borderRadius: "12px",
width: "100%",
}}
>
<Box
sx={{
backgroundColor: "white",
"&:first-of-type": {
borderTopLeftRadius: "12px",
borderTopRightRadius: "12px",
},
"&:last-of-type": {
borderBottomLeftRadius: "12px",
borderBottomRightRadius: "12px",
},
"&:not(:last-of-type)": {
borderBottom: `1px solid ${theme.palette.gray.main}`,
},
}}
>
<Box
onClick={() => setIsExpanded((prev) => !prev)}
sx={{
height: "72px",
display: "flex",
gap: "10px",
alignItems: "center",
justifyContent: "space-between",
cursor: "pointer",
userSelect: "none",
}}
>
<ExpandIcon isExpanded={isExpanded} />
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "space-between",
}}
>
<Typography
sx={{
fontSize: upMd ? "20px" : "16px",
lineHeight: upMd ? undefined : "19px",
fontWeight: 500,
color: theme.palette.text.secondary,
px: 0,
}}
>
{name[serviceData.serviceKey]}
</Typography>
<Box
sx={{
display: "flex",
justifyContent: "flex-end",
height: "100%",
alignItems: "center",
}}
>
<Typography
sx={{
color: theme.palette.gray.dark,
fontSize: upSm ? "20px" : "16px",
fontWeight: 500,
}}
>
{currencyFormatter.format(serviceData.price / 100)}
</Typography>
</Box>
</Box>
{isLoading ? (
<Box>
<CircularProgress sx={{ color: "#666666" }} />
</Box>
) : (
<IconButton
onClick={deleteService}
sx={{
padding: "3px",
height: "30px",
width: "30px",
}}
>
<CrossIcon
style={{
height: "24px",
width: "24px",
stroke: theme.palette.purple.main,
}}
/>
</IconButton>
)}
</Box>
{isExpanded &&
return (
<Box sx={{ display: "flex", alignItems: "center" }}>
<Box
sx={{
overflow: "hidden",
borderRadius: "12px",
width: "100%",
}}
>
<Box
sx={{
backgroundColor: "white",
"&:first-of-type": {
borderTopLeftRadius: "12px",
borderTopRightRadius: "12px",
},
"&:last-of-type": {
borderBottomLeftRadius: "12px",
borderBottomRightRadius: "12px",
},
"&:not(:last-of-type)": {
borderBottom: `1px solid ${theme.palette.gray.main}`,
},
}}
>
<Box
onClick={() => setIsExpanded((prev) => !prev)}
sx={{
height: "72px",
display: "flex",
gap: "10px",
alignItems: "center",
justifyContent: "space-between",
cursor: "pointer",
userSelect: "none",
}}
>
<ExpandIcon isExpanded={isExpanded} />
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "space-between",
}}
>
<Typography
sx={{
fontSize: upMd ? "20px" : "16px",
lineHeight: upMd ? undefined : "19px",
fontWeight: 500,
color: theme.palette.text.secondary,
px: 0,
}}
>
{name[serviceData.serviceKey]}
</Typography>
<Box
sx={{
display: "flex",
justifyContent: "flex-end",
height: "100%",
alignItems: "center",
}}
>
<Typography
sx={{
color: theme.palette.gray.dark,
fontSize: upSm ? "20px" : "16px",
fontWeight: 500,
}}
>
{currencyFormatter.format(serviceData.price / 100)}
</Typography>
</Box>
</Box>
{isLoading ? (
<Box>
<CircularProgress sx={{ color: "#666666" }} />
</Box>
) : (
<IconButton
onClick={deleteService}
sx={{
padding: "3px",
height: "30px",
width: "30px",
}}
>
<CrossIcon
style={{
height: "24px",
width: "24px",
stroke: theme.palette.purple.main,
}}
/>
</IconButton>
)}
</Box>
{isExpanded &&
serviceData.tariffs.map((tariff) => {
const privilege = tariff.privileges[0];
const privilege = tariff.privileges[0]
return tariff.privileges.length > 1 ? (
<CustomTariffAccordion
key={tariff.id}
tariffCartData={tariff}
/>
) : (
<Box
key={tariff.id + privilege.privilegeId}
sx={{
py: upMd ? "10px" : undefined,
pt: upMd ? undefined : "15px",
pb: upMd ? undefined : "20px",
return tariff.privileges.length > 1 ? (
<CustomTariffAccordion
key={tariff.id}
tariffCartData={tariff}
/>
) : (
<Box
key={tariff.id + privilege.privilegeId}
sx={{
py: upMd ? "10px" : undefined,
pt: upMd ? undefined : "15px",
pb: upMd ? undefined : "20px",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
gap: "15px",
}}
>
<Typography
sx={{
width: "200px",
fontSize: upMd ? undefined : "16px",
lineHeight: upMd ? undefined : "19px",
color: theme.palette.gray.dark,
}}
>
{privilege.description}
</Typography>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
gap: "10px",
alignItems: "center",
}}
>
<Typography
sx={{
color: theme.palette.gray.dark,
fontSize: "20px",
fontWeight: 500,
}}
>
{currencyFormatter.format(
(tariff.price || privilege.price) / 100
)}
</Typography>
<SvgIcon
sx={{
cursor: "pointer",
width: "30px",
color: theme.palette.purple.main,
}}
onClick={() => handleItemDeleteClick(tariff.id)}
component={ClearIcon}
/>
</Box>
</Box>
);
display: "flex",
justifyContent: "space-between",
alignItems: "center",
gap: "15px",
}}
>
<Typography
sx={{
width: "200px",
fontSize: upMd ? undefined : "16px",
lineHeight: upMd ? undefined : "19px",
color: theme.palette.gray.dark,
}}
>
{privilege.description}
</Typography>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
gap: "10px",
alignItems: "center",
}}
>
<Typography
sx={{
color: theme.palette.gray.dark,
fontSize: "20px",
fontWeight: 500,
}}
>
{currencyFormatter.format(
(tariff.price || privilege.price) / 100
)}
</Typography>
<SvgIcon
sx={{
cursor: "pointer",
width: "30px",
color: theme.palette.purple.main,
}}
onClick={() => handleItemDeleteClick(tariff.id)}
component={ClearIcon}
/>
</Box>
</Box>
)
})}
</Box>
</Box>
</Box>
);
</Box>
</Box>
</Box>
)
}

@ -1,341 +1,341 @@
import { useState, useRef } from "react";
import { useState, useRef } from "react"
import {
Typography,
Drawer,
useMediaQuery,
useTheme,
Box,
IconButton,
Badge,
Button,
} from "@mui/material";
import SectionWrapper from "./SectionWrapper";
import CustomWrapperDrawer from "./CustomWrapperDrawer";
import { NotificationsModal } from "./NotificationsModal";
import { Loader } from "./Loader";
import { useCart } from "@root/utils/hooks/useCart";
import { currencyFormatter } from "@root/utils/currencyFormatter";
Typography,
Drawer,
useMediaQuery,
useTheme,
Box,
IconButton,
Badge,
Button,
} from "@mui/material"
import SectionWrapper from "./SectionWrapper"
import CustomWrapperDrawer from "./CustomWrapperDrawer"
import { NotificationsModal } from "./NotificationsModal"
import { Loader } from "./Loader"
import { useCart } from "@root/utils/hooks/useCart"
import { currencyFormatter } from "@root/utils/currencyFormatter"
import {
closeCartDrawer,
openCartDrawer,
useCartStore,
} from "@root/stores/cart";
import { setUserAccount, useUserStore } from "@root/stores/user";
import { useTicketStore } from "@root/stores/tickets";
closeCartDrawer,
openCartDrawer,
useCartStore,
} from "@root/stores/cart"
import { setUserAccount, useUserStore } from "@root/stores/user"
import { useTicketStore } from "@root/stores/tickets"
import { ReactComponent as BellIcon } from "@root/assets/Icons/bell.svg";
import { ReactComponent as CartIcon } from "@root/assets/Icons/cart.svg";
import { ReactComponent as CrossIcon } from "@root/assets/Icons/cross.svg";
import { ReactComponent as BellIcon } from "@root/assets/Icons/bell.svg"
import { ReactComponent as CartIcon } from "@root/assets/Icons/cart.svg"
import { ReactComponent as CrossIcon } from "@root/assets/Icons/cross.svg"
import { payCart } from "@root/api/cart";
import { enqueueSnackbar } from "notistack";
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 { payCart } from "@root/api/cart"
import { enqueueSnackbar } from "notistack"
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"
function Drawers() {
const [openNotificationsModal, setOpenNotificationsModal] =
useState<boolean>(false);
const [loading, setLoading] = useState<boolean>(false);
const bellRef = useRef<HTMLButtonElement | null>(null);
const navigate = useNavigate();
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const isDrawerOpen = useCartStore((state) => state.isDrawerOpen);
const cart = useCart();
const userAccount = useUserStore((state) => state.userAccount);
const tickets = useTicketStore((state) => state.tickets);
const [notEnoughMoneyAmount, setNotEnoughMoneyAmount] = useState<number>(0);
const [openNotificationsModal, setOpenNotificationsModal] =
useState<boolean>(false)
const [loading, setLoading] = useState<boolean>(false)
const bellRef = useRef<HTMLButtonElement | null>(null)
const navigate = useNavigate()
const theme = useTheme()
const upMd = useMediaQuery(theme.breakpoints.up("md"))
const isTablet = useMediaQuery(theme.breakpoints.down(1000))
const isDrawerOpen = useCartStore((state) => state.isDrawerOpen)
const cart = useCart()
const userAccount = useUserStore((state) => state.userAccount)
const tickets = useTicketStore((state) => state.tickets)
const [notEnoughMoneyAmount, setNotEnoughMoneyAmount] = useState<number>(0)
const notificationsCount = tickets.filter(
({ user, top_message }) =>
user !== top_message.user_id && top_message.shown.me !== 1
).length;
const notificationsCount = tickets.filter(
({ user, top_message }) =>
user !== top_message.user_id && top_message.shown.me !== 1
).length
async function handlePayClick() {
setLoading(true);
async function handlePayClick() {
setLoading(true)
const [payCartResponse, payCartError] = await payCart();
const [payCartResponse, payCartError] = await payCart()
if (payCartError) {
if (payCartError.includes("insufficient funds: ")) {
const notEnoughMoneyAmount = parseInt(
payCartError.replace(/^.*insufficient\sfunds:\s(?=\d+$)/, "")
);
if (payCartError) {
if (payCartError.includes("insufficient funds: ")) {
const notEnoughMoneyAmount = parseInt(
payCartError.replace(/^.*insufficient\sfunds:\s(?=\d+$)/, "")
)
setNotEnoughMoneyAmount(notEnoughMoneyAmount);
}
setNotEnoughMoneyAmount(notEnoughMoneyAmount)
}
setLoading(false);
setLoading(false)
return enqueueSnackbar(payCartError);
}
return enqueueSnackbar(payCartError)
}
if (payCartResponse) {
setUserAccount(payCartResponse);
}
if (payCartResponse) {
setUserAccount(payCartResponse)
}
setLoading(false);
closeCartDrawer();
}
setLoading(false)
closeCartDrawer()
}
function handleReplenishWallet() {
navigate("/payment", { state: { notEnoughMoneyAmount } });
}
function handleReplenishWallet() {
navigate("/payment", { state: { notEnoughMoneyAmount } })
}
return (
<Box sx={{ display: "flex", gap: isTablet ? "10px" : "20px" }}>
<IconButton
ref={bellRef}
aria-label="cart"
onClick={() => setOpenNotificationsModal((isOpened) => !isOpened)}
sx={{
cursor: "pointer",
borderRadius: "6px",
background: openNotificationsModal
? theme.palette.purple.main
: theme.palette.background.default,
"& .MuiBadge-badge": {
background: openNotificationsModal
? theme.palette.background.default
: theme.palette.purple.main,
color: openNotificationsModal
? theme.palette.purple.main
: theme.palette.background.default,
},
"& svg > path:first-of-type": {
fill: openNotificationsModal ? "#FFFFFF" : "#9A9AAF",
},
"& svg > path:last-child": {
stroke: openNotificationsModal ? "#FFFFFF" : "#9A9AAF",
},
"&:hover": {
background: theme.palette.purple.main,
"& .MuiBox-root": {
background: theme.palette.purple.main,
},
"& .MuiBadge-badge": {
background: theme.palette.background.default,
color: theme.palette.purple.main,
},
"& svg > path:first-of-type": { fill: "#FFFFFF" },
"& svg > path:last-child": { stroke: "#FFFFFF" },
},
}}
>
<Badge
badgeContent={notificationsCount}
sx={{
"& .MuiBadge-badge": {
display: notificationsCount ? "flex" : "none",
color: "#FFFFFF",
background: theme.palette.purple.main,
transform: "scale(0.8) translate(50%, -50%)",
top: "2px",
right: "2px",
fontWeight: 400,
},
}}
>
<BellIcon />
</Badge>
</IconButton>
<NotificationsModal
open={openNotificationsModal}
setOpen={setOpenNotificationsModal}
anchorElement={bellRef.current}
notifications={tickets
.filter(({ user, top_message }) => user !== top_message.user_id)
.map((ticket) => ({
text: "У вас новое сообщение от техподдержки",
date: new Date(ticket.updated_at).toLocaleDateString(),
url: `/support/${ticket.id}`,
watched:
return (
<Box sx={{ display: "flex", gap: isTablet ? "10px" : "20px" }}>
<IconButton
ref={bellRef}
aria-label="cart"
onClick={() => setOpenNotificationsModal((isOpened) => !isOpened)}
sx={{
cursor: "pointer",
borderRadius: "6px",
background: openNotificationsModal
? theme.palette.purple.main
: theme.palette.background.default,
"& .MuiBadge-badge": {
background: openNotificationsModal
? theme.palette.background.default
: theme.palette.purple.main,
color: openNotificationsModal
? theme.palette.purple.main
: theme.palette.background.default,
},
"& svg > path:first-of-type": {
fill: openNotificationsModal ? "#FFFFFF" : "#9A9AAF",
},
"& svg > path:last-child": {
stroke: openNotificationsModal ? "#FFFFFF" : "#9A9AAF",
},
"&:hover": {
background: theme.palette.purple.main,
"& .MuiBox-root": {
background: theme.palette.purple.main,
},
"& .MuiBadge-badge": {
background: theme.palette.background.default,
color: theme.palette.purple.main,
},
"& svg > path:first-of-type": { fill: "#FFFFFF" },
"& svg > path:last-child": { stroke: "#FFFFFF" },
},
}}
>
<Badge
badgeContent={notificationsCount}
sx={{
"& .MuiBadge-badge": {
display: notificationsCount ? "flex" : "none",
color: "#FFFFFF",
background: theme.palette.purple.main,
transform: "scale(0.8) translate(50%, -50%)",
top: "2px",
right: "2px",
fontWeight: 400,
},
}}
>
<BellIcon />
</Badge>
</IconButton>
<NotificationsModal
open={openNotificationsModal}
setOpen={setOpenNotificationsModal}
anchorElement={bellRef.current}
notifications={tickets
.filter(({ user, top_message }) => user !== top_message.user_id)
.map((ticket) => ({
text: "У вас новое сообщение от техподдержки",
date: new Date(ticket.updated_at).toLocaleDateString(),
url: `/support/${ticket.id}`,
watched:
ticket.user === ticket.top_message.user_id ||
ticket.top_message.shown.me === 1,
}))}
/>
<IconButton
onClick={openCartDrawer}
component="div"
sx={{
cursor: "pointer",
background: theme.palette.background.default,
borderRadius: "6px",
"&:hover": {
background: theme.palette.purple.main,
"& .MuiBox-root": {
background: theme.palette.purple.main,
},
"& .MuiBadge-badge": {
background: theme.palette.background.default,
color: theme.palette.purple.main,
},
"& svg > path:nth-of-type(1)": { fill: "#FFFFFF" },
"& svg > path:nth-of-type(2)": { fill: "#FFFFFF" },
"& svg > path:nth-of-type(3)": { stroke: "#FFFFFF" },
},
}}
>
<Badge
badgeContent={userAccount?.cart.length}
sx={{
"& .MuiBadge-badge": {
display: userAccount?.cart.length ? "flex" : "none",
color: "#FFFFFF",
background: theme.palette.purple.main,
transform: "scale(0.8) translate(50%, -50%)",
top: "2px",
right: "2px",
fontWeight: 400,
},
}}
>
<CartIcon />
</Badge>
</IconButton>
<Drawer
anchor={"right"}
open={isDrawerOpen}
onClose={closeCartDrawer}
sx={{ background: "rgba(0, 0, 0, 0.55)" }}
>
<SectionWrapper
maxWidth="lg"
sx={{
pl: "0px",
pr: "0px",
width: "450px",
}}
>
<Box
sx={{
width: "100%",
pt: "12px",
pb: "12px",
display: "flex",
justifyContent: "space-between",
bgcolor: "#F2F3F7",
gap: "10px",
pl: "20px",
pr: "20px",
}}
>
<Typography
component="div"
sx={{
fontSize: "18px",
lineHeight: "21px",
font: "Rubick",
}}
>
}))}
/>
<IconButton
onClick={openCartDrawer}
component="div"
sx={{
cursor: "pointer",
background: theme.palette.background.default,
borderRadius: "6px",
"&:hover": {
background: theme.palette.purple.main,
"& .MuiBox-root": {
background: theme.palette.purple.main,
},
"& .MuiBadge-badge": {
background: theme.palette.background.default,
color: theme.palette.purple.main,
},
"& svg > path:nth-of-type(1)": { fill: "#FFFFFF" },
"& svg > path:nth-of-type(2)": { fill: "#FFFFFF" },
"& svg > path:nth-of-type(3)": { stroke: "#FFFFFF" },
},
}}
>
<Badge
badgeContent={userAccount?.cart.length}
sx={{
"& .MuiBadge-badge": {
display: userAccount?.cart.length ? "flex" : "none",
color: "#FFFFFF",
background: theme.palette.purple.main,
transform: "scale(0.8) translate(50%, -50%)",
top: "2px",
right: "2px",
fontWeight: 400,
},
}}
>
<CartIcon />
</Badge>
</IconButton>
<Drawer
anchor={"right"}
open={isDrawerOpen}
onClose={closeCartDrawer}
sx={{ background: "rgba(0, 0, 0, 0.55)" }}
>
<SectionWrapper
maxWidth="lg"
sx={{
pl: "0px",
pr: "0px",
width: "450px",
}}
>
<Box
sx={{
width: "100%",
pt: "12px",
pb: "12px",
display: "flex",
justifyContent: "space-between",
bgcolor: "#F2F3F7",
gap: "10px",
pl: "20px",
pr: "20px",
}}
>
<Typography
component="div"
sx={{
fontSize: "18px",
lineHeight: "21px",
font: "Rubick",
}}
>
Корзина
</Typography>
<IconButton onClick={closeCartDrawer} sx={{ p: 0 }}>
<CrossIcon />
</IconButton>
</Box>
<Box sx={{ pl: "20px", pr: "20px" }}>
{cart.services.map((serviceData) => (
<CustomWrapperDrawer
key={serviceData.serviceKey}
serviceData={serviceData}
/>
))}
<Box
sx={{
mt: "40px",
pt: upMd ? "30px" : undefined,
borderTop: upMd
? `1px solid ${theme.palette.gray.main}`
: undefined,
}}
>
<Box
sx={{
width: upMd ? "100%" : undefined,
display: "flex",
flexWrap: "wrap",
flexDirection: "column",
}}
>
<Typography variant="h4" mb={upMd ? "18px" : "30px"}>
</Typography>
<IconButton onClick={closeCartDrawer} sx={{ p: 0 }}>
<CrossIcon />
</IconButton>
</Box>
<Box sx={{ pl: "20px", pr: "20px" }}>
{cart.services.map((serviceData) => (
<CustomWrapperDrawer
key={serviceData.serviceKey}
serviceData={serviceData}
/>
))}
<Box
sx={{
mt: "40px",
pt: upMd ? "30px" : undefined,
borderTop: upMd
? `1px solid ${theme.palette.gray.main}`
: undefined,
}}
>
<Box
sx={{
width: upMd ? "100%" : undefined,
display: "flex",
flexWrap: "wrap",
flexDirection: "column",
}}
>
<Typography variant="h4" mb={upMd ? "18px" : "30px"}>
Итоговая цена
</Typography>
<Typography color={theme.palette.gray.dark}>
</Typography>
<Typography color={theme.palette.gray.dark}>
Текст-заполнитель это текст, который имеет Текст-заполнитель
это текст, который имеет Текст-заполнитель это текст,
который имеет Текст-заполнитель это текст, который имеет
Текст-заполнитель
</Typography>
</Box>
<Box
sx={{
color: theme.palette.gray.dark,
pb: "100px",
pt: "38px",
}}
>
<Box
sx={{
display: "flex",
flexDirection: upMd ? "column" : "row",
alignItems: upMd ? "start" : "center",
mt: upMd ? "10px" : "30px",
gap: "15px",
}}
>
<Typography
color={theme.palette.orange.main}
sx={{
textDecoration: "line-through",
order: upMd ? 1 : 2,
}}
>
{currencyFormatter.format(cart.priceBeforeDiscounts / 100)}
</Typography>
<Typography
variant="p1"
sx={{
fontWeight: 500,
fontSize: "26px",
lineHeight: "31px",
order: upMd ? 2 : 1,
}}
>
{currencyFormatter.format(cart.priceAfterDiscounts / 100)}
</Typography>
</Box>
<Button
variant="pena-contained-dark"
onClick={() =>
notEnoughMoneyAmount === 0
? !loading && handlePayClick()
: handleReplenishWallet()
}
sx={{ mt: "25px" }}
>
{loading ? <Loader size={24} /> : "Оплатить"}
</Button>
</Box>
</Box>
</Box>
</SectionWrapper>
</Drawer>
</Box>
);
</Typography>
</Box>
<Box
sx={{
color: theme.palette.gray.dark,
pb: "100px",
pt: "38px",
}}
>
<Box
sx={{
display: "flex",
flexDirection: upMd ? "column" : "row",
alignItems: upMd ? "start" : "center",
mt: upMd ? "10px" : "30px",
gap: "15px",
}}
>
<Typography
color={theme.palette.orange.main}
sx={{
textDecoration: "line-through",
order: upMd ? 1 : 2,
}}
>
{currencyFormatter.format(cart.priceBeforeDiscounts / 100)}
</Typography>
<Typography
variant="p1"
sx={{
fontWeight: 500,
fontSize: "26px",
lineHeight: "31px",
order: upMd ? 2 : 1,
}}
>
{currencyFormatter.format(cart.priceAfterDiscounts / 100)}
</Typography>
</Box>
<Button
variant="pena-contained-dark"
onClick={() =>
notEnoughMoneyAmount === 0
? !loading && handlePayClick()
: handleReplenishWallet()
}
sx={{ mt: "25px" }}
>
{loading ? <Loader size={24} /> : "Оплатить"}
</Button>
</Box>
</Box>
</Box>
</SectionWrapper>
</Drawer>
</Box>
)
}
export default withErrorBoundary(Drawers, {
fallback: (
<Box sx={{
display: "flex",
alignItems: "center",
}}>
<ErrorOutlineIcon color="error" />
</Box>
),
onError: handleComponentError,
fallback: (
<Box sx={{
display: "flex",
alignItems: "center",
}}>
<ErrorOutlineIcon color="error" />
</Box>
),
onError: handleComponentError,
})

@ -1,319 +1,319 @@
import {
Box,
FormControl,
IconButton,
InputAdornment,
InputBase,
SxProps,
Theme,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import { TicketMessage } from "@frontend/kitui";
Box,
FormControl,
IconButton,
InputAdornment,
InputBase,
SxProps,
Theme,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material"
import { TicketMessage } from "@frontend/kitui"
import {
addOrUpdateUnauthMessages,
useUnauthTicketStore,
incrementUnauthMessageApiPage,
setUnauthIsPreventAutoscroll,
setUnauthSessionData,
setIsMessageSending,
setUnauthTicketMessageFetchState,
} from "@root/stores/unauthTicket";
import { enqueueSnackbar } from "notistack";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import ChatMessage from "../ChatMessage";
import SendIcon from "../icons/SendIcon";
import UserCircleIcon from "./UserCircleIcon";
import { throttle } from "@frontend/kitui";
addOrUpdateUnauthMessages,
useUnauthTicketStore,
incrementUnauthMessageApiPage,
setUnauthIsPreventAutoscroll,
setUnauthSessionData,
setIsMessageSending,
setUnauthTicketMessageFetchState,
} from "@root/stores/unauthTicket"
import { enqueueSnackbar } from "notistack"
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import ChatMessage from "../ChatMessage"
import SendIcon from "../icons/SendIcon"
import UserCircleIcon from "./UserCircleIcon"
import { throttle } from "@frontend/kitui"
import {
useTicketMessages,
getMessageFromFetchError,
useSSESubscription,
useEventListener,
createTicket,
} from "@frontend/kitui";
import { sendTicketMessage } from "@root/api/ticket";
useTicketMessages,
getMessageFromFetchError,
useSSESubscription,
useEventListener,
createTicket,
} from "@frontend/kitui"
import { sendTicketMessage } from "@root/api/ticket"
interface Props {
sx?: SxProps<Theme>;
}
export default function Chat({ sx }: Props) {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const [messageField, setMessageField] = useState<string>("");
const sessionData = useUnauthTicketStore((state) => state.sessionData);
const messages = useUnauthTicketStore((state) => state.messages);
const messageApiPage = useUnauthTicketStore((state) => state.apiPage);
const messagesPerPage = useUnauthTicketStore(
(state) => state.messagesPerPage
);
const isMessageSending = useUnauthTicketStore(
(state) => state.isMessageSending
);
const isPreventAutoscroll = useUnauthTicketStore(
(state) => state.isPreventAutoscroll
);
const lastMessageId = useUnauthTicketStore((state) => state.lastMessageId);
const fetchState = useUnauthTicketStore(
(state) => state.unauthTicketMessageFetchState
);
const chatBoxRef = useRef<HTMLDivElement>(null);
const theme = useTheme()
const upMd = useMediaQuery(theme.breakpoints.up("md"))
const [messageField, setMessageField] = useState<string>("")
const sessionData = useUnauthTicketStore((state) => state.sessionData)
const messages = useUnauthTicketStore((state) => state.messages)
const messageApiPage = useUnauthTicketStore((state) => state.apiPage)
const messagesPerPage = useUnauthTicketStore(
(state) => state.messagesPerPage
)
const isMessageSending = useUnauthTicketStore(
(state) => state.isMessageSending
)
const isPreventAutoscroll = useUnauthTicketStore(
(state) => state.isPreventAutoscroll
)
const lastMessageId = useUnauthTicketStore((state) => state.lastMessageId)
const fetchState = useUnauthTicketStore(
(state) => state.unauthTicketMessageFetchState
)
const chatBoxRef = useRef<HTMLDivElement>(null)
useTicketMessages({
url: "https://hub.pena.digital/heruvym/getMessages",
isUnauth: true,
ticketId: sessionData?.ticketId,
messagesPerPage,
messageApiPage,
onSuccess: useCallback((messages) => {
if (chatBoxRef.current && chatBoxRef.current.scrollTop < 1)
chatBoxRef.current.scrollTop = 1;
addOrUpdateUnauthMessages(messages);
}, []),
onError: useCallback((error: Error) => {
const message = getMessageFromFetchError(error);
if (message) enqueueSnackbar(message);
}, []),
onFetchStateChange: setUnauthTicketMessageFetchState,
});
useTicketMessages({
url: "https://hub.pena.digital/heruvym/getMessages",
isUnauth: true,
ticketId: sessionData?.ticketId,
messagesPerPage,
messageApiPage,
onSuccess: useCallback((messages) => {
if (chatBoxRef.current && chatBoxRef.current.scrollTop < 1)
chatBoxRef.current.scrollTop = 1
addOrUpdateUnauthMessages(messages)
}, []),
onError: useCallback((error: Error) => {
const message = getMessageFromFetchError(error)
if (message) enqueueSnackbar(message)
}, []),
onFetchStateChange: setUnauthTicketMessageFetchState,
})
useSSESubscription<TicketMessage>({
enabled: Boolean(sessionData),
url: `https://hub.pena.digital/heruvym/ticket?ticket=${sessionData?.ticketId}&s=${sessionData?.sessionId}`,
onNewData: addOrUpdateUnauthMessages,
onDisconnect: useCallback(() => {
setUnauthIsPreventAutoscroll(false);
}, []),
marker: "ticket",
});
useSSESubscription<TicketMessage>({
enabled: Boolean(sessionData),
url: `https://hub.pena.digital/heruvym/ticket?ticket=${sessionData?.ticketId}&s=${sessionData?.sessionId}`,
onNewData: addOrUpdateUnauthMessages,
onDisconnect: useCallback(() => {
setUnauthIsPreventAutoscroll(false)
}, []),
marker: "ticket",
})
const throttledScrollHandler = useMemo(
() =>
throttle(() => {
const chatBox = chatBoxRef.current;
if (!chatBox) return;
const throttledScrollHandler = useMemo(
() =>
throttle(() => {
const chatBox = chatBoxRef.current
if (!chatBox) return
const scrollBottom =
chatBox.scrollHeight - chatBox.scrollTop - chatBox.clientHeight;
const isPreventAutoscroll = scrollBottom > chatBox.clientHeight;
setUnauthIsPreventAutoscroll(isPreventAutoscroll);
const scrollBottom =
chatBox.scrollHeight - chatBox.scrollTop - chatBox.clientHeight
const isPreventAutoscroll = scrollBottom > chatBox.clientHeight
setUnauthIsPreventAutoscroll(isPreventAutoscroll)
if (fetchState !== "idle") return;
if (fetchState !== "idle") return
if (chatBox.scrollTop < chatBox.clientHeight) {
incrementUnauthMessageApiPage();
}
}, 200),
[fetchState]
);
if (chatBox.scrollTop < chatBox.clientHeight) {
incrementUnauthMessageApiPage()
}
}, 200),
[fetchState]
)
useEventListener("scroll", throttledScrollHandler, chatBoxRef);
useEventListener("scroll", throttledScrollHandler, chatBoxRef)
useEffect(
function scrollOnNewMessage() {
if (!chatBoxRef.current) return;
useEffect(
function scrollOnNewMessage() {
if (!chatBoxRef.current) return
if (!isPreventAutoscroll) {
setTimeout(() => {
scrollToBottom();
}, 50);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
},
[lastMessageId]
);
if (!isPreventAutoscroll) {
setTimeout(() => {
scrollToBottom()
}, 50)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
},
[lastMessageId]
)
async function handleSendMessage() {
if (!messageField || isMessageSending) return;
async function handleSendMessage() {
if (!messageField || isMessageSending) return
if (!sessionData) {
setIsMessageSending(true);
createTicket({
url: "https://hub.pena.digital/heruvym/create",
body: {
Title: "Unauth title",
Message: messageField,
},
useToken: false,
})
.then((response) => {
setUnauthSessionData({
ticketId: response.Ticket,
sessionId: response.sess,
});
})
.catch((error) => {
const errorMessage = getMessageFromFetchError(error);
if (errorMessage) enqueueSnackbar(errorMessage);
})
.finally(() => {
setMessageField("");
setIsMessageSending(false);
});
} else {
setIsMessageSending(true);
if (!sessionData) {
setIsMessageSending(true)
createTicket({
url: "https://hub.pena.digital/heruvym/create",
body: {
Title: "Unauth title",
Message: messageField,
},
useToken: false,
})
.then((response) => {
setUnauthSessionData({
ticketId: response.Ticket,
sessionId: response.sess,
})
})
.catch((error) => {
const errorMessage = getMessageFromFetchError(error)
if (errorMessage) enqueueSnackbar(errorMessage)
})
.finally(() => {
setMessageField("")
setIsMessageSending(false)
})
} else {
setIsMessageSending(true)
const [_, sendTicketMessageError] = await sendTicketMessage(
sessionData.ticketId,
messageField
);
const [_, sendTicketMessageError] = await sendTicketMessage(
sessionData.ticketId,
messageField
)
if (sendTicketMessageError) {
enqueueSnackbar(sendTicketMessageError);
}
if (sendTicketMessageError) {
enqueueSnackbar(sendTicketMessageError)
}
setMessageField("");
setIsMessageSending(false);
}
}
setMessageField("")
setIsMessageSending(false)
}
}
function scrollToBottom(behavior?: ScrollBehavior) {
if (!chatBoxRef.current) return;
function scrollToBottom(behavior?: ScrollBehavior) {
if (!chatBoxRef.current) return
const chatBox = chatBoxRef.current;
chatBox.scroll({
left: 0,
top: chatBox.scrollHeight,
behavior,
});
}
const chatBox = chatBoxRef.current
chatBox.scroll({
left: 0,
top: chatBox.scrollHeight,
behavior,
})
}
const handleTextfieldKeyPress: React.KeyboardEventHandler<
const handleTextfieldKeyPress: React.KeyboardEventHandler<
HTMLInputElement | HTMLTextAreaElement
> = (e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSendMessage();
}
};
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault()
handleSendMessage()
}
}
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
height: "clamp(250px, calc(100vh - 90px), 600px)",
backgroundColor: "#944FEE",
borderRadius: "8px",
...sx,
}}
>
<Box
sx={{
display: "flex",
gap: "9px",
pl: "22px",
pt: "12px",
pb: "20px",
filter: "drop-shadow(0px 3px 12px rgba(37, 39, 52, 0.3))",
}}
>
<UserCircleIcon />
<Box
sx={{
mt: "5px",
display: "flex",
flexDirection: "column",
gap: "3px",
}}
>
<Typography>Мария</Typography>
<Typography
sx={{
fontSize: "16px",
lineHeight: "19px",
}}
>
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
height: "clamp(250px, calc(100vh - 90px), 600px)",
backgroundColor: "#944FEE",
borderRadius: "8px",
...sx,
}}
>
<Box
sx={{
display: "flex",
gap: "9px",
pl: "22px",
pt: "12px",
pb: "20px",
filter: "drop-shadow(0px 3px 12px rgba(37, 39, 52, 0.3))",
}}
>
<UserCircleIcon />
<Box
sx={{
mt: "5px",
display: "flex",
flexDirection: "column",
gap: "3px",
}}
>
<Typography>Мария</Typography>
<Typography
sx={{
fontSize: "16px",
lineHeight: "19px",
}}
>
онлайн-консультант
</Typography>
</Box>
</Box>
<Box
sx={{
flexGrow: 1,
backgroundColor: "white",
borderRadius: "8px",
display: "flex",
flexDirection: "column",
}}
>
<Box
ref={chatBoxRef}
sx={{
display: "flex",
width: "100%",
flexBasis: 0,
flexDirection: "column",
gap: upMd ? "20px" : "16px",
px: upMd ? "20px" : "5px",
py: upMd ? "20px" : "13px",
overflowY: "auto",
flexGrow: 1,
}}
>
{sessionData &&
</Typography>
</Box>
</Box>
<Box
sx={{
flexGrow: 1,
backgroundColor: "white",
borderRadius: "8px",
display: "flex",
flexDirection: "column",
}}
>
<Box
ref={chatBoxRef}
sx={{
display: "flex",
width: "100%",
flexBasis: 0,
flexDirection: "column",
gap: upMd ? "20px" : "16px",
px: upMd ? "20px" : "5px",
py: upMd ? "20px" : "13px",
overflowY: "auto",
flexGrow: 1,
}}
>
{sessionData &&
messages.map((message) => (
<ChatMessage
unAuthenticated
key={message.id}
text={message.message}
createdAt={message.created_at}
isSelf={sessionData.sessionId === message.user_id}
/>
<ChatMessage
unAuthenticated
key={message.id}
text={message.message}
createdAt={message.created_at}
isSelf={sessionData.sessionId === message.user_id}
/>
))}
</Box>
<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={(e) => setMessageField(e.target.value)}
endAdornment={
<InputAdornment position="end">
<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>
</Box>
</Box>
);
</Box>
<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={(e) => setMessageField(e.target.value)}
endAdornment={
<InputAdornment position="end">
<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>
</Box>
</Box>
)
}

@ -1,4 +1,4 @@
import { Box } from "@mui/material";
import { Box } from "@mui/material"
interface Props {
@ -6,21 +6,21 @@ interface Props {
}
export default function CircleDoubleDown({ isUp = false }: Props) {
return (
<Box sx={{
width: "32px",
height: "32px",
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
transform: isUp ? "scale(1, -1)" : undefined,
}}>
<svg width="33" height="32" viewBox="0 0 33 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.9004 4C10.273 4 4.90039 9.37258 4.90039 16C4.90039 22.6274 10.273 28 16.9004 28C23.5278 28 28.9004 22.6274 28.9004 16C28.9004 9.37258 23.5278 4 16.9004 4Z" stroke="#252734" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M12.9004 21L16.9004 17L20.9004 21" stroke="#252734" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M12.9004 14L16.9004 10L20.9004 14" stroke="#252734" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</Box>
);
return (
<Box sx={{
width: "32px",
height: "32px",
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
transform: isUp ? "scale(1, -1)" : undefined,
}}>
<svg width="33" height="32" viewBox="0 0 33 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.9004 4C10.273 4 4.90039 9.37258 4.90039 16C4.90039 22.6274 10.273 28 16.9004 28C23.5278 28 28.9004 22.6274 28.9004 16C28.9004 9.37258 23.5278 4 16.9004 4Z" stroke="#252734" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M12.9004 21L16.9004 17L20.9004 21" stroke="#252734" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M12.9004 14L16.9004 10L20.9004 14" stroke="#252734" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</Box>
)
}

@ -1,94 +1,94 @@
import { Box, Fab, Typography } from "@mui/material";
import { useState } from "react";
import CircleDoubleDown from "./CircleDoubleDownIcon";
import Chat from "./Chat";
import { Box, Fab, Typography } from "@mui/material"
import { useState } from "react"
import CircleDoubleDown from "./CircleDoubleDownIcon"
import Chat from "./Chat"
export default function FloatingSupportChat() {
const [isChatOpened, setIsChatOpened] = useState<boolean>(false);
const [isChatOpened, setIsChatOpened] = useState<boolean>(false)
const animation = {
"@keyframes runningStripe": {
"0%": {
left: "10%",
backgroundColor: "transparent",
},
"10%": {
backgroundColor: "#ffffff",
},
"50%": {
backgroundColor: "#ffffff",
transform: "translate(400px, 0)",
},
"80%": {
backgroundColor: "#ffffff",
},
const animation = {
"@keyframes runningStripe": {
"0%": {
left: "10%",
backgroundColor: "transparent",
},
"10%": {
backgroundColor: "#ffffff",
},
"50%": {
backgroundColor: "#ffffff",
transform: "translate(400px, 0)",
},
"80%": {
backgroundColor: "#ffffff",
},
"100%": {
backgroundColor: "transparent",
boxShadow: "none",
left: "100%",
},
},
};
return (
<Box
sx={{
position: "fixed",
right: "20px",
bottom: "10px",
display: "flex",
flexDirection: "column",
gap: "8px",
width: "clamp(200px, 100% - 40px, 454px)",
zIndex: 10,
}}
>
{isChatOpened && (
<Chat
sx={{
alignSelf: "start",
width: "clamp(200px, 100%, 400px)",
}}
/>
)}
<Fab
disableRipple
sx={{
position: "relative",
backgroundColor: "rgba(255, 255, 255, 0.7)",
pl: "11px",
pr: !isChatOpened ? "15px" : "11px",
gap: "11px",
height: "54px",
borderRadius: "27px",
alignSelf: "end",
overflow: "hidden",
"&:hover": {
background: "rgba(255, 255, 255, 0.7)",
},
}}
variant={"extended"}
onClick={() => setIsChatOpened((prev) => !prev)}
>
{!isChatOpened && (
<Box
sx={{
position: "absolute",
bgcolor: "#FFFFFF",
height: "100px",
width: "25px",
animation: "runningStripe linear 3s infinite",
transform: " skew(-10deg) rotate(70deg) skewX(20deg) skewY(10deg)",
boxShadow: "0px 3px 12px rgba(126, 42, 234, 0.1)",
opacity: "0.4",
...animation,
}}
/>
)}
"100%": {
backgroundColor: "transparent",
boxShadow: "none",
left: "100%",
},
},
}
return (
<Box
sx={{
position: "fixed",
right: "20px",
bottom: "10px",
display: "flex",
flexDirection: "column",
gap: "8px",
width: "clamp(200px, 100% - 40px, 454px)",
zIndex: 10,
}}
>
{isChatOpened && (
<Chat
sx={{
alignSelf: "start",
width: "clamp(200px, 100%, 400px)",
}}
/>
)}
<Fab
disableRipple
sx={{
position: "relative",
backgroundColor: "rgba(255, 255, 255, 0.7)",
pl: "11px",
pr: !isChatOpened ? "15px" : "11px",
gap: "11px",
height: "54px",
borderRadius: "27px",
alignSelf: "end",
overflow: "hidden",
"&:hover": {
background: "rgba(255, 255, 255, 0.7)",
},
}}
variant={"extended"}
onClick={() => setIsChatOpened((prev) => !prev)}
>
{!isChatOpened && (
<Box
sx={{
position: "absolute",
bgcolor: "#FFFFFF",
height: "100px",
width: "25px",
animation: "runningStripe linear 3s infinite",
transform: " skew(-10deg) rotate(70deg) skewX(20deg) skewY(10deg)",
boxShadow: "0px 3px 12px rgba(126, 42, 234, 0.1)",
opacity: "0.4",
...animation,
}}
/>
)}
<CircleDoubleDown isUp={isChatOpened} />
{!isChatOpened && <Typography sx={{ zIndex: "10000" }}>Задайте нам вопрос</Typography>}
</Fab>
</Box>
);
<CircleDoubleDown isUp={isChatOpened} />
{!isChatOpened && <Typography sx={{ zIndex: "10000" }}>Задайте нам вопрос</Typography>}
</Fab>
</Box>
)
}

@ -1,22 +1,22 @@
import { Box } from "@mui/material";
import { Box } from "@mui/material"
export default function UserCircleIcon() {
return (
<Box sx={{
width: "32px",
height: "32px",
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
}}>
<svg width="32" height="33" viewBox="0 0 32 33" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16 28.5C22.6274 28.5 28 23.1274 28 16.5C28 9.87258 22.6274 4.5 16 4.5C9.37258 4.5 4 9.87258 4 16.5C4 23.1274 9.37258 28.5 16 28.5Z" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M16 20.5C18.7614 20.5 21 18.2614 21 15.5C21 12.7386 18.7614 10.5 16 10.5C13.2386 10.5 11 12.7386 11 15.5C11 18.2614 13.2386 20.5 16 20.5Z" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M7.97461 25.425C8.727 23.943 9.87506 22.6983 11.2915 21.8289C12.708 20.9595 14.3376 20.4992 15.9996 20.4992C17.6616 20.4992 19.2912 20.9595 20.7077 21.8289C22.1242 22.6983 23.2722 23.943 24.0246 25.425" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</Box>
);
return (
<Box sx={{
width: "32px",
height: "32px",
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
}}>
<svg width="32" height="33" viewBox="0 0 32 33" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16 28.5C22.6274 28.5 28 23.1274 28 16.5C28 9.87258 22.6274 4.5 16 4.5C9.37258 4.5 4 9.87258 4 16.5C4 23.1274 9.37258 28.5 16 28.5Z" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M16 20.5C18.7614 20.5 21 18.2614 21 15.5C21 12.7386 18.7614 10.5 16 10.5C13.2386 10.5 11 12.7386 11 15.5C11 18.2614 13.2386 20.5 16 20.5Z" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M7.97461 25.425C8.727 23.943 9.87506 22.6983 11.2915 21.8289C12.708 20.9595 14.3376 20.4992 15.9996 20.4992C17.6616 20.4992 19.2912 20.9595 20.7077 21.8289C22.1242 22.6983 23.2722 23.943 24.0246 25.425" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</Box>
)
}

@ -1,66 +1,66 @@
import { Box, Button, Divider, Typography, useMediaQuery, useTheme } from "@mui/material";
import PenaLogo from "./PenaLogo";
import SectionWrapper from "./SectionWrapper";
import { Link } from "react-router-dom";
import { Box, Button, Divider, Typography, useMediaQuery, useTheme } from "@mui/material"
import PenaLogo from "./PenaLogo"
import SectionWrapper from "./SectionWrapper"
import { Link } from "react-router-dom"
export default function Footer() {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const theme = useTheme()
const upMd = useMediaQuery(theme.breakpoints.up("md"))
return (
<SectionWrapper
component="footer"
maxWidth="lg"
outerContainerSx={{
backgroundColor: theme.palette.bg.dark,
}}
sx={{
pt: upMd ? "100px" : "80px",
pb: upMd ? "75px" : "24px",
px: upMd ? "40px" : "20px",
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: upMd ? "90px" : undefined,
}}
>
<Box
sx={{
display: "flex",
flexDirection: upMd ? "row" : "column",
width: "100%",
justifyContent: "space-between",
alignItems: "center"
}}
>
<Box
sx={{
flexBasis: upMd ? "98px" : "98px",
}}
>
<PenaLogo width={124} color="white" />
</Box>
<Box
sx={{
display: "flex",
flexDirection: upMd ? "row" : "column",
flexWrap: "wrap",
flexGrow: 1,
flexShrink: 1,
flexBasis: upMd ? "600px" : undefined,
alignItems: upMd ? undefined : "start",
mb: upMd ? undefined : "30px",
mx: upMd ? "70px" : undefined,
gap: upMd ? undefined : "18px",
"& > *": {
width: upMd ? "33%" : undefined,
},
}}
>
<Link to="https://docs.pena.digital/docs">
<Button variant="pena-navitem-dark">Оферта</Button>
</Link>
{/* <Button variant="pena-navitem-dark">Меню 1</Button>
return (
<SectionWrapper
component="footer"
maxWidth="lg"
outerContainerSx={{
backgroundColor: theme.palette.bg.dark,
}}
sx={{
pt: upMd ? "100px" : "80px",
pb: upMd ? "75px" : "24px",
px: upMd ? "40px" : "20px",
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: upMd ? "90px" : undefined,
}}
>
<Box
sx={{
display: "flex",
flexDirection: upMd ? "row" : "column",
width: "100%",
justifyContent: "space-between",
alignItems: "center"
}}
>
<Box
sx={{
flexBasis: upMd ? "98px" : "98px",
}}
>
<PenaLogo width={124} color="white" />
</Box>
<Box
sx={{
display: "flex",
flexDirection: upMd ? "row" : "column",
flexWrap: "wrap",
flexGrow: 1,
flexShrink: 1,
flexBasis: upMd ? "600px" : undefined,
alignItems: upMd ? undefined : "start",
mb: upMd ? undefined : "30px",
mx: upMd ? "70px" : undefined,
gap: upMd ? undefined : "18px",
"& > *": {
width: upMd ? "33%" : undefined,
},
}}
>
<Link to="https://docs.pena.digital/docs">
<Button variant="pena-navitem-dark">Оферта</Button>
</Link>
{/* <Button variant="pena-navitem-dark">Меню 1</Button>
<Button variant="pena-navitem-dark">Меню 1</Button>
<Button variant="pena-navitem-dark">Меню 1</Button>
<Button variant="pena-navitem-dark">Меню 1</Button>
@ -68,18 +68,18 @@ export default function Footer() {
<Button variant="pena-navitem-dark">Меню 1</Button>
<Button variant="pena-navitem-dark">Меню 1</Button>
<Button variant="pena-navitem-dark">Меню 1</Button> */}
</Box>
<Typography
variant="body2"
sx={{
flexBasis: upMd ? "300px" : undefined,
pr: "5%",
mb: upMd ? undefined : "36px",
}}
>
</Box>
<Typography
variant="body2"
sx={{
flexBasis: upMd ? "300px" : undefined,
pr: "5%",
mb: upMd ? undefined : "36px",
}}
>
ООО Пена© 2023 {new Date().getFullYear() === 2023 ? "" : " - " + new Date().getFullYear()}
</Typography>
</Box>
</SectionWrapper>
);
</Typography>
</Box>
</SectionWrapper>
)
}

@ -1,15 +1,15 @@
import {
FormControl,
InputLabel,
SxProps,
TextField,
TextFieldProps,
Theme,
useMediaQuery,
useTheme,
} from "@mui/material";
FormControl,
InputLabel,
SxProps,
TextField,
TextFieldProps,
Theme,
useMediaQuery,
useTheme,
} from "@mui/material"
import "./text-input.css";
import "./text-input.css"
interface Props {
id: string;
@ -23,74 +23,74 @@ interface Props {
}
export default function InputTextfield({
id,
label,
bold = false,
gap = "10px",
onChange,
TextfieldProps,
color,
FormInputSx,
id,
label,
bold = false,
gap = "10px",
onChange,
TextfieldProps,
color,
FormInputSx,
}: Props) {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const theme = useTheme()
const upMd = useMediaQuery(theme.breakpoints.up("md"))
const labelFont = upMd
? bold
? theme.typography.p1
: { ...theme.typography.body1, fontWeight: 500 }
: theme.typography.body2;
const labelFont = upMd
? bold
? theme.typography.p1
: { ...theme.typography.body1, fontWeight: 500 }
: theme.typography.body2
const placeholderFont = upMd ? undefined : { fontWeight: 400, fontSize: "16px", lineHeight: "19px" };
const placeholderFont = upMd ? undefined : { fontWeight: 400, fontSize: "16px", lineHeight: "19px" }
return (
<FormControl
fullWidth
variant="standard"
sx={{
gap,
// mt: "10px",
...FormInputSx,
}}
>
{label && (
<InputLabel
shrink
htmlFor={id}
sx={{
position: "inherit",
color: "black",
transform: "none",
...labelFont,
}}
>
{label}
</InputLabel>
)}
<TextField
{...TextfieldProps}
fullWidth
id={id}
sx={{
"& .MuiInputBase-root": {
height: "48px",
borderRadius: "8px",
},
}}
inputProps={{
sx: {
boxSizing: "border-box",
backgroundColor: color,
border: "1px solid" + theme.palette.gray.main,
borderRadius: "8px",
height: "48px",
py: 0,
color: "black",
...placeholderFont,
},
}}
onChange={onChange}
/>
</FormControl>
);
return (
<FormControl
fullWidth
variant="standard"
sx={{
gap,
// mt: "10px",
...FormInputSx,
}}
>
{label && (
<InputLabel
shrink
htmlFor={id}
sx={{
position: "inherit",
color: "black",
transform: "none",
...labelFont,
}}
>
{label}
</InputLabel>
)}
<TextField
{...TextfieldProps}
fullWidth
id={id}
sx={{
"& .MuiInputBase-root": {
height: "48px",
borderRadius: "8px",
},
}}
inputProps={{
sx: {
boxSizing: "border-box",
backgroundColor: color,
border: "1px solid" + theme.palette.gray.main,
borderRadius: "8px",
height: "48px",
py: 0,
color: "black",
...placeholderFont,
},
}}
onChange={onChange}
/>
</FormControl>
)
}

@ -1,30 +1,30 @@
import { Box, useTheme } from "@mui/material";
import { Box, useTheme } from "@mui/material"
type LoaderProps = {
size?: number;
};
export const Loader = ({ size = 40 }: LoaderProps) => {
const theme = useTheme();
const theme = useTheme()
return (
<Box
sx={{
width: size,
height: size,
border: `${size / 6}px solid #ffffff`,
borderRadius: "50%",
borderTop: `${size / 6}px solid ${theme.palette.purple.main}`,
animation: "spin 2s linear infinite",
"@-webkit-keyframes spin": {
"0%": { transform: "rotate(0deg)" },
"100%": { transform: "rotate(360deg)" },
},
"@keyframes spin": {
"0%": { transform: "rotate(0deg)" },
"100%": { transform: "rotate(360deg)" },
},
}}
/>
);
};
return (
<Box
sx={{
width: size,
height: size,
border: `${size / 6}px solid #ffffff`,
borderRadius: "50%",
borderTop: `${size / 6}px solid ${theme.palette.purple.main}`,
animation: "spin 2s linear infinite",
"@-webkit-keyframes spin": {
"0%": { transform: "rotate(0deg)" },
"100%": { transform: "rotate(360deg)" },
},
"@keyframes spin": {
"0%": { transform: "rotate(0deg)" },
"100%": { transform: "rotate(360deg)" },
},
}}
/>
)
}

@ -1,6 +1,6 @@
import { useState } from "react";
import { Box, Typography, useTheme } from "@mui/material";
import { Link, useLocation } from "react-router-dom";
import { useState } from "react"
import { Box, Typography, useTheme } from "@mui/material"
import { Link, useLocation } from "react-router-dom"
type MenuItem = {
name: string;
@ -9,123 +9,123 @@ type MenuItem = {
};
export default function Menu() {
const [activeSubMenu, setActiveSubMenu] = useState<MenuItem[]>([]);
const theme = useTheme();
const location = useLocation();
const [activeSubMenu, setActiveSubMenu] = useState<MenuItem[]>([])
const theme = useTheme()
const location = useLocation()
const color = location.pathname === "/" ? "white" : "black";
const color = location.pathname === "/" ? "white" : "black"
const arrayMenu: MenuItem[] = location.pathname === "/" ? [
{ name: "Наши продукты", url: "/faq" },
{ name: "Наши услуги", url: "/cart" }
] : [
{
name: "Тарифы",
url: "/tariffs",
subMenu: [
{ name: "Тарифы на время", url: "/tariffs/time" },
{ name: "Тарифы на объём", url: "/tariffs/volume" },
{ name: "Кастомный тариф", url: "/tariffconstructor" },
],
},
{ name: "Вопросы и ответы", url: "/faq" },
{ name: "Корзина", url: "/cart" },
{ name: "Поддержка", url: "/support" },
{ name: "История", url: "/history" },
];
const arrayMenu: MenuItem[] = location.pathname === "/" ? [
{ name: "Наши продукты", url: "/faq" },
{ name: "Наши услуги", url: "/cart" }
] : [
{
name: "Тарифы",
url: "/tariffs",
subMenu: [
{ name: "Тарифы на время", url: "/tariffs/time" },
{ name: "Тарифы на объём", url: "/tariffs/volume" },
{ name: "Кастомный тариф", url: "/tariffconstructor" },
],
},
{ name: "Вопросы и ответы", url: "/faq" },
{ name: "Корзина", url: "/cart" },
{ name: "Поддержка", url: "/support" },
{ name: "История", url: "/history" },
]
return (
<Box
sx={{
display: "flex",
alignItems: "center",
height: "100%",
gap: "30px",
overflow: "hidden",
overflowX: "auto",
}}
>
{location.pathname !== "/"
? arrayMenu.map(({ name, url, subMenu = [] }) => (
<Link
key={name}
style={{
textDecoration: "none",
height: "100%",
display: "flex",
alignItems: "center",
}}
to={url}
onMouseEnter={() => setActiveSubMenu(subMenu)}
state={{ previousUrl: location.pathname }}
>
<Typography
color={
location.pathname === url
? theme.palette.purple.main
: color
}
variant="body2"
sx={{
whiteSpace: "nowrap",
}}
>
{name}
</Typography>
</Link>
))
: arrayMenu.map(({ name, url, subMenu = [] }, index) => (
<Typography
color={
location.pathname === url
? theme.palette.purple.main
: color
}
variant="body2"
sx={{
whiteSpace: "nowrap",
}}
>
{name}
</Typography>
))}
<Box
sx={{
zIndex: "10",
position: "absolute",
top: "80px",
left: 0,
backgroundColor: theme.palette.background.paper,
width: "100%",
}}
onMouseLeave={() => setActiveSubMenu([])}
>
{location.pathname !== "/" &&
return (
<Box
sx={{
display: "flex",
alignItems: "center",
height: "100%",
gap: "30px",
overflow: "hidden",
overflowX: "auto",
}}
>
{location.pathname !== "/"
? arrayMenu.map(({ name, url, subMenu = [] }) => (
<Link
key={name}
style={{
textDecoration: "none",
height: "100%",
display: "flex",
alignItems: "center",
}}
to={url}
onMouseEnter={() => setActiveSubMenu(subMenu)}
state={{ previousUrl: location.pathname }}
>
<Typography
color={
location.pathname === url
? theme.palette.purple.main
: color
}
variant="body2"
sx={{
whiteSpace: "nowrap",
}}
>
{name}
</Typography>
</Link>
))
: arrayMenu.map(({ name, url, subMenu = [] }, index) => (
<Typography
color={
location.pathname === url
? theme.palette.purple.main
: color
}
variant="body2"
sx={{
whiteSpace: "nowrap",
}}
>
{name}
</Typography>
))}
<Box
sx={{
zIndex: "10",
position: "absolute",
top: "80px",
left: 0,
backgroundColor: theme.palette.background.paper,
width: "100%",
}}
onMouseLeave={() => setActiveSubMenu([])}
>
{location.pathname !== "/" &&
activeSubMenu.map(({ name, url }) => (
<Link key={name} style={{ textDecoration: "none" }} to={url}>
<Typography
color={
location.pathname === url
? theme.palette.purple.main
: color
}
variant="body2"
sx={{
padding: "15px",
whiteSpace: "nowrap",
paddingLeft: "185px",
"&:hover": {
color: theme.palette.purple.main,
background: theme.palette.background.default,
},
}}
>
{name}
</Typography>
</Link>
<Link key={name} style={{ textDecoration: "none" }} to={url}>
<Typography
color={
location.pathname === url
? theme.palette.purple.main
: color
}
variant="body2"
sx={{
padding: "15px",
whiteSpace: "nowrap",
paddingLeft: "185px",
"&:hover": {
color: theme.palette.purple.main,
background: theme.palette.background.default,
},
}}
>
{name}
</Typography>
</Link>
))}
</Box>
</Box>
);
</Box>
</Box>
)
}

@ -1,42 +1,42 @@
import { Avatar, Box, IconButton, SxProps, Theme, Typography, useTheme } from "@mui/material";
import { useNavigate } from "react-router-dom";
import { Avatar, Box, IconButton, SxProps, Theme, Typography, useTheme } from "@mui/material"
import { useNavigate } from "react-router-dom"
interface Props {
sx?: SxProps<Theme>;
}
export default function CustomAvatar({ sx }: Props) {
const theme = useTheme();
const navigate = useNavigate()
const theme = useTheme()
const navigate = useNavigate()
return (
<IconButton
onClick={() => navigate("/settings")}
sx={{
ml: "27px",
height: "36px", width: "36px",
...sx,
}}
>
<Avatar sx={{
backgroundColor: theme.palette.orange.main,
}}>
<Typography
sx={{
fontWeight: 500,
fontSize: "14px",
lineHeight: "20px",
zIndex: 1,
}}
>AA</Typography>
<Box sx={{ position: "absolute" }}>
<svg xmlns="http://www.w3.org/2000/svg" width="37" height="36" viewBox="0 0 37 36" fill="none">
<path fillRule="evenodd" clipRule="evenodd" d="M16.0896 15.3939C16.1897 9.41281 22.9128 5.35966 28.711 3.9153C34.7649 2.40721 41.974 3.19598 46.0209 7.93784C49.6931 12.2405 46.8503 18.5029 45.9355 24.0976C45.2565 28.2502 44.7264 32.5083 41.552 35.2692C38.1345 38.2416 32.8105 41.3312 29.1224 38.7209C25.459 36.1281 30.5336 29.8417 28.3428 25.9204C25.5777 20.9711 15.9947 21.0705 16.0896 15.3939Z" fill="#FC712F" />
<circle cx="28.7954" cy="-4.08489" r="5.51855" transform="rotate(-32.339 28.7954 -4.08489)" fill="#FC712F" />
<circle cx="25.1065" cy="28.2781" r="1.26958" transform="rotate(-32.339 25.1065 28.2781)" fill="#FC712F" />
</svg>
</Box>
</Avatar>
</IconButton>
);
return (
<IconButton
onClick={() => navigate("/settings")}
sx={{
ml: "27px",
height: "36px", width: "36px",
...sx,
}}
>
<Avatar sx={{
backgroundColor: theme.palette.orange.main,
}}>
<Typography
sx={{
fontWeight: 500,
fontSize: "14px",
lineHeight: "20px",
zIndex: 1,
}}
>AA</Typography>
<Box sx={{ position: "absolute" }}>
<svg xmlns="http://www.w3.org/2000/svg" width="37" height="36" viewBox="0 0 37 36" fill="none">
<path fillRule="evenodd" clipRule="evenodd" d="M16.0896 15.3939C16.1897 9.41281 22.9128 5.35966 28.711 3.9153C34.7649 2.40721 41.974 3.19598 46.0209 7.93784C49.6931 12.2405 46.8503 18.5029 45.9355 24.0976C45.2565 28.2502 44.7264 32.5083 41.552 35.2692C38.1345 38.2416 32.8105 41.3312 29.1224 38.7209C25.459 36.1281 30.5336 29.8417 28.3428 25.9204C25.5777 20.9711 15.9947 21.0705 16.0896 15.3939Z" fill="#FC712F" />
<circle cx="28.7954" cy="-4.08489" r="5.51855" transform="rotate(-32.339 28.7954 -4.08489)" fill="#FC712F" />
<circle cx="25.1065" cy="28.2781" r="1.26958" transform="rotate(-32.339 25.1065 28.2781)" fill="#FC712F" />
</svg>
</Box>
</Avatar>
</IconButton>
)
}

@ -1,28 +1,28 @@
import { TransitionProps } from "@mui/material/transitions";
import logotip from "../../assets/Icons/logoPenaHab.svg";
import logotipBlack from "../../assets/Icons/black_logo_PenaHab.svg";
import CustomAvatar from "./Avatar";
import React from "react";
import { Box, Button, Dialog, List, ListItem, Slide, Typography, useMediaQuery, useTheme } from "@mui/material";
import { Link, useLocation } from "react-router-dom";
import { useUserStore } from "@root/stores/user";
import { currencyFormatter } from "@root/utils/currencyFormatter";
import { TransitionProps } from "@mui/material/transitions"
import logotip from "../../assets/Icons/logoPenaHab.svg"
import logotipBlack from "../../assets/Icons/black_logo_PenaHab.svg"
import CustomAvatar from "./Avatar"
import React from "react"
import { Box, Button, Dialog, List, ListItem, Slide, Typography, useMediaQuery, useTheme } from "@mui/material"
import { Link, useLocation } from "react-router-dom"
import { useUserStore } from "@root/stores/user"
import { currencyFormatter } from "@root/utils/currencyFormatter"
const arrayMenu = [
{ name: "Наши продукты", url: "/" },
{ name: "Наши услуги", url: "/" },
];
{ name: "Наши продукты", url: "/" },
{ name: "Наши услуги", url: "/" },
]
const Transition = React.forwardRef(function Transition(
props: TransitionProps & {
props: TransitionProps & {
children: React.ReactElement;
},
ref: React.Ref<null>
ref: React.Ref<null>
) {
return <Slide direction={"left"} ref={ref} {...props} />;
});
return <Slide direction={"left"} ref={ref} {...props} />
})
interface DialogMenuProps {
open: boolean;
@ -30,129 +30,129 @@ interface DialogMenuProps {
}
export default function DialogMenu({ open, handleClose }: DialogMenuProps) {
const theme = useTheme();
const location = useLocation();
const isTablet = useMediaQuery(theme.breakpoints.down(900));
const user = useUserStore((state) => state.user);
const cash = useUserStore((state) => state.userAccount?.wallet.cash) ?? 0;
const theme = useTheme()
const location = useLocation()
const isTablet = useMediaQuery(theme.breakpoints.down(900))
const user = useUserStore((state) => state.user)
const cash = useUserStore((state) => state.userAccount?.wallet.cash) ?? 0
const isMobileHeight = useMediaQuery("(max-height: 400px)");
const isMobileHeight = useMediaQuery("(max-height: 400px)")
return (
<Dialog
fullScreen
sx={{
width: isTablet ? "100%" : "320px",
ml: "auto",
mt: isTablet ? "50px" : "80px",
".MuiBackdrop-root.MuiModal-backdrop": {
background: "transparent",
},
".MuiPaper-root.MuiPaper-rounded": {
background: "#333647",
},
}}
open={open}
onClose={handleClose}
TransitionComponent={Transition}
>
<List
sx={{
background: location.pathname === "/" ? "#333647" : "#FFFFFF",
height: "58%",
overflow: "scroll",
p: "0",
paddingTop: "20px",
}}
>
<ListItem
sx={{
pl: "40px",
flexDirection: "column",
alignItems: isTablet ? "start" : "end",
}}
>
{arrayMenu.map(({ name, url }, index) => (
<Button
key={index}
component={Link}
to={url}
onClick={handleClose}
state={user ? undefined : { backgroundLocation: location }}
disableRipple
variant="text"
sx={{
fontWeight: "500",
// color: location.pathname === url ? theme.palette.purple.main : location.pathname === "/" ? "white" : "black",
color: "white",
height: "20px",
textTransform: "none",
marginBottom: "25px",
fontSize: "16px",
"&:hover": {
background: "none",
color: theme.palette.purple.main,
},
}}
>
{name}
</Button>
))}
</ListItem>
</List>
{isTablet ? (
location.pathname === "/" ? (
<Button
component={Link}
to={user ? "/tariffs" : "/signin"}
state={user ? undefined : { backgroundLocation: location }}
variant="pena-contained-dark"
sx={{px: "30px", ml: "40px" , width: "245px"}}
>
return (
<Dialog
fullScreen
sx={{
width: isTablet ? "100%" : "320px",
ml: "auto",
mt: isTablet ? "50px" : "80px",
".MuiBackdrop-root.MuiModal-backdrop": {
background: "transparent",
},
".MuiPaper-root.MuiPaper-rounded": {
background: "#333647",
},
}}
open={open}
onClose={handleClose}
TransitionComponent={Transition}
>
<List
sx={{
background: location.pathname === "/" ? "#333647" : "#FFFFFF",
height: "58%",
overflow: "scroll",
p: "0",
paddingTop: "20px",
}}
>
<ListItem
sx={{
pl: "40px",
flexDirection: "column",
alignItems: isTablet ? "start" : "end",
}}
>
{arrayMenu.map(({ name, url }, index) => (
<Button
key={index}
component={Link}
to={url}
onClick={handleClose}
state={user ? undefined : { backgroundLocation: location }}
disableRipple
variant="text"
sx={{
fontWeight: "500",
// color: location.pathname === url ? theme.palette.purple.main : location.pathname === "/" ? "white" : "black",
color: "white",
height: "20px",
textTransform: "none",
marginBottom: "25px",
fontSize: "16px",
"&:hover": {
background: "none",
color: theme.palette.purple.main,
},
}}
>
{name}
</Button>
))}
</ListItem>
</List>
{isTablet ? (
location.pathname === "/" ? (
<Button
component={Link}
to={user ? "/tariffs" : "/signin"}
state={user ? undefined : { backgroundLocation: location }}
variant="pena-contained-dark"
sx={{px: "30px", ml: "40px" , width: "245px"}}
>
Регистрация / Войти
</Button>
) : (
<Box
sx={{
width: "100%",
height: "72px",
background: "#F2F3F7",
display: "flex",
alignItems: "center",
position: "absolute",
bottom: "0",
}}
>
<CustomAvatar />
<Box sx={{ ml: "8px", whiteSpace: "nowrap" }}>
<Typography
sx={{
fontSize: "12px",
lineHeight: "14px",
color: theme.palette.gray.dark,
}}
>
</Button>
) : (
<Box
sx={{
width: "100%",
height: "72px",
background: "#F2F3F7",
display: "flex",
alignItems: "center",
position: "absolute",
bottom: "0",
}}
>
<CustomAvatar />
<Box sx={{ ml: "8px", whiteSpace: "nowrap" }}>
<Typography
sx={{
fontSize: "12px",
lineHeight: "14px",
color: theme.palette.gray.dark,
}}
>
Мой баланс
</Typography>
<Typography variant="body2" color={theme.palette.purple.main}>
{currencyFormatter.format(cash / 100)}
</Typography>
</Box>
</Box>
)
) : (
<>
<Box
sx={{
position: "absolute",
right: "40px",
bottom: "60px",
}}
>
<img src={location.pathname === "/" ? logotip : logotipBlack} alt="icon" />
</Box>
</>
)}
</Dialog>
);
</Typography>
<Typography variant="body2" color={theme.palette.purple.main}>
{currencyFormatter.format(cash / 100)}
</Typography>
</Box>
</Box>
)
) : (
<>
<Box
sx={{
position: "absolute",
right: "40px",
bottom: "60px",
}}
>
<img src={location.pathname === "/" ? logotip : logotipBlack} alt="icon" />
</Box>
</>
)}
</Dialog>
)
}

@ -1,6 +1,6 @@
import { useMediaQuery, useTheme } from "@mui/material";
import NavbarCollapsed from "./NavbarCollapsed";
import NavbarFull from "./NavbarFull";
import { useMediaQuery, useTheme } from "@mui/material"
import NavbarCollapsed from "./NavbarCollapsed"
import NavbarFull from "./NavbarFull"
interface Props {
isCollapsed?: boolean;
@ -8,8 +8,8 @@ interface Props {
}
export default function Navbar({ isLoggedIn, isCollapsed = false }: Props) {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const theme = useTheme()
const upMd = useMediaQuery(theme.breakpoints.up("md"))
return upMd ? <NavbarFull isLoggedIn={isLoggedIn} /> : <NavbarCollapsed isLoggedIn={isLoggedIn} />;
return upMd ? <NavbarFull isLoggedIn={isLoggedIn} /> : <NavbarCollapsed isLoggedIn={isLoggedIn} />
}

@ -1,46 +1,46 @@
import { useState } from "react";
import { Button, useMediaQuery, useTheme } from "@mui/material";
import { Link, useLocation } from "react-router-dom";
import { useState } from "react"
import { Button, useMediaQuery, useTheme } from "@mui/material"
import { Link, useLocation } from "react-router-dom"
import SectionWrapper from "../SectionWrapper";
import SectionWrapper from "../SectionWrapper"
import PenaLogo from "../PenaLogo";
import DialogMenu from "./DialogMenu";
import { BurgerButton } from "@frontend/kitui";
import PenaLogo from "../PenaLogo"
import DialogMenu from "./DialogMenu"
import { BurgerButton } from "@frontend/kitui"
interface Props {
isLoggedIn: boolean;
}
export default function NavbarCollapsed({ isLoggedIn }: Props) {
const [open, setOpen] = useState(false);
const theme = useTheme();
const [open, setOpen] = useState(false)
const theme = useTheme()
return (
<SectionWrapper
component="nav"
maxWidth="lg"
outerContainerSx={{
zIndex: "111111111111",
position: "fixed",
top: "0",
backgroundColor: theme.palette.bg.main,
borderBottom: "1px solid #E3E3E3",
}}
sx={{
height: "51px",
py: "6px",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Link to="/">
<PenaLogo width={100} color="white" />
</Link>
return (
<SectionWrapper
component="nav"
maxWidth="lg"
outerContainerSx={{
zIndex: "111111111111",
position: "fixed",
top: "0",
backgroundColor: theme.palette.bg.main,
borderBottom: "1px solid #E3E3E3",
}}
sx={{
height: "51px",
py: "6px",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Link to="/">
<PenaLogo width={100} color="white" />
</Link>
<BurgerButton onClick={() => setOpen(!open)} sx={{ color: "white" }} />
<DialogMenu open={open} handleClose={() => setOpen(false)} />
</SectionWrapper>
);
<BurgerButton onClick={() => setOpen(!open)} sx={{ color: "white" }} />
<DialogMenu open={open} handleClose={() => setOpen(false)} />
</SectionWrapper>
)
}

@ -1,164 +1,164 @@
import { useState } from "react";
import { Link, useLocation, useNavigate } from "react-router-dom";
import { useState } from "react"
import { Link, useLocation, useNavigate } from "react-router-dom"
import {
Box,
Button,
Container,
IconButton,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import SectionWrapper from "../SectionWrapper";
import LogoutIcon from "../icons/LogoutIcon";
import DialogMenu from "./DialogMenu";
import WalletIcon from "../icons/WalletIcon";
import CustomAvatar from "./Avatar";
import Drawers from "../Drawers";
import PenaLogo from "../PenaLogo";
import Menu from "../Menu";
import { logout } from "@root/api/auth";
import { enqueueSnackbar } from "notistack";
import { clearUserData, useUserStore } from "@root/stores/user";
Box,
Button,
Container,
IconButton,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material"
import SectionWrapper from "../SectionWrapper"
import LogoutIcon from "../icons/LogoutIcon"
import DialogMenu from "./DialogMenu"
import WalletIcon from "../icons/WalletIcon"
import CustomAvatar from "./Avatar"
import Drawers from "../Drawers"
import PenaLogo from "../PenaLogo"
import Menu from "../Menu"
import { logout } from "@root/api/auth"
import { enqueueSnackbar } from "notistack"
import { clearUserData, useUserStore } from "@root/stores/user"
import {
BurgerButton,
clearAuthToken,
getMessageFromFetchError,
} from "@frontend/kitui";
import { currencyFormatter } from "@root/utils/currencyFormatter";
import { clearCustomTariffs } from "@root/stores/customTariffs";
import { clearTickets } from "@root/stores/tickets";
BurgerButton,
clearAuthToken,
getMessageFromFetchError,
} from "@frontend/kitui"
import { currencyFormatter } from "@root/utils/currencyFormatter"
import { clearCustomTariffs } from "@root/stores/customTariffs"
import { clearTickets } from "@root/stores/tickets"
interface Props {
isLoggedIn: boolean;
}
export default function NavbarFull({ isLoggedIn }: Props) {
const theme = useTheme();
const location = useLocation();
const navigate = useNavigate();
const user = useUserStore((state) => state.user);
const cash = useUserStore((state) => state.userAccount?.wallet.cash) ?? 0;
const isTablet = useMediaQuery(theme.breakpoints.up(1000));
const [open, setOpen] = useState(false);
const theme = useTheme()
const location = useLocation()
const navigate = useNavigate()
const user = useUserStore((state) => state.user)
const cash = useUserStore((state) => state.userAccount?.wallet.cash) ?? 0
const isTablet = useMediaQuery(theme.breakpoints.up(1000))
const [open, setOpen] = useState(false)
async function handleLogoutClick() {
const [_, logoutError] = await logout();
async function handleLogoutClick() {
const [_, logoutError] = await logout()
if (logoutError) {
return enqueueSnackbar(logoutError);
}
if (logoutError) {
return enqueueSnackbar(logoutError)
}
clearAuthToken();
clearUserData();
clearCustomTariffs();
clearTickets();
navigate("/");
}
clearAuthToken()
clearUserData()
clearCustomTariffs()
clearTickets()
navigate("/")
}
return isLoggedIn ? (
<Container
component="nav"
disableGutters
maxWidth={false}
sx={{
zIndex: "1",
px: "16px",
display: "flex",
height: "79px",
alignItems: "center",
gap: "60px",
bgcolor: "white",
borderBottom: "1px solid #E3E3E3",
}}
>
<Link to="/">
<PenaLogo width={124} color="white" />
</Link>
<Menu />
<Box
sx={{
display: "flex",
ml: "auto",
}}
>
<Drawers />
<IconButton
sx={{ p: 0, ml: "8px" }}
onClick={() => navigate("/wallet")}
>
<WalletIcon color={theme.palette.gray.main} bgcolor="#F2F3F7" />
</IconButton>
<Box sx={{ ml: "8px", whiteSpace: "nowrap" }}>
<Typography
sx={{
fontSize: "12px",
lineHeight: "14px",
color: theme.palette.gray.dark,
}}
>
return isLoggedIn ? (
<Container
component="nav"
disableGutters
maxWidth={false}
sx={{
zIndex: "1",
px: "16px",
display: "flex",
height: "79px",
alignItems: "center",
gap: "60px",
bgcolor: "white",
borderBottom: "1px solid #E3E3E3",
}}
>
<Link to="/">
<PenaLogo width={124} color="white" />
</Link>
<Menu />
<Box
sx={{
display: "flex",
ml: "auto",
}}
>
<Drawers />
<IconButton
sx={{ p: 0, ml: "8px" }}
onClick={() => navigate("/wallet")}
>
<WalletIcon color={theme.palette.gray.main} bgcolor="#F2F3F7" />
</IconButton>
<Box sx={{ ml: "8px", whiteSpace: "nowrap" }}>
<Typography
sx={{
fontSize: "12px",
lineHeight: "14px",
color: theme.palette.gray.dark,
}}
>
Мой баланс
</Typography>
<Typography variant="body2" color={theme.palette.purple.main}>
{currencyFormatter.format(cash / 100)}
</Typography>
</Box>
<CustomAvatar />
<IconButton
onClick={handleLogoutClick}
sx={{
ml: "20px",
bgcolor: "#F2F3F7",
borderRadius: "6px",
height: "36px",
width: "36px",
}}
>
<LogoutIcon />
</IconButton>
</Box>
</Container>
) : (
<>
<SectionWrapper
component="nav"
maxWidth="lg"
outerContainerSx={{
zIndex: "1",
position: "fixed",
top: "0",
backgroundColor: theme.palette.bg.main,
borderBottom: "1px solid #E3E3E3",
}}
sx={{
px: "20px",
display: "flex",
justifyContent: "space-between",
height: "79px",
alignItems: "center",
gap: "50px",
}}
>
<Box width="457px" justifyContent="space-between" display="inline-flex" alignItems="center" >
<PenaLogo width={150} color="white" />
{!isTablet ? null : <Menu />}
</Typography>
<Typography variant="body2" color={theme.palette.purple.main}>
{currencyFormatter.format(cash / 100)}
</Typography>
</Box>
<CustomAvatar />
<IconButton
onClick={handleLogoutClick}
sx={{
ml: "20px",
bgcolor: "#F2F3F7",
borderRadius: "6px",
height: "36px",
width: "36px",
}}
>
<LogoutIcon />
</IconButton>
</Box>
</Container>
) : (
<>
<SectionWrapper
component="nav"
maxWidth="lg"
outerContainerSx={{
zIndex: "1",
position: "fixed",
top: "0",
backgroundColor: theme.palette.bg.main,
borderBottom: "1px solid #E3E3E3",
}}
sx={{
px: "20px",
display: "flex",
justifyContent: "space-between",
height: "79px",
alignItems: "center",
gap: "50px",
}}
>
<Box width="457px" justifyContent="space-between" display="inline-flex" alignItems="center" >
<PenaLogo width={150} color="white" />
{!isTablet ? null : <Menu />}
</Box>
<Button
component={Link}
to={user ? "/tariffs" : "/signin"}
state={user ? undefined : { backgroundLocation: location }}
variant="pena-contained-dark"
sx={{px: "30px", ml: !isTablet ? "auto" : "none" }}
>
</Box>
<Button
component={Link}
to={user ? "/tariffs" : "/signin"}
state={user ? undefined : { backgroundLocation: location }}
variant="pena-contained-dark"
sx={{px: "30px", ml: !isTablet ? "auto" : "none" }}
>
Регистрация / Войти
</Button>
<BurgerButton
onClick={() => setOpen(!open)}
sx={{ color: "white", display: !isTablet ? "block" : "none" }}
/>
</SectionWrapper>
<DialogMenu open={open} handleClose={() => setOpen(false)} />
</>
);
</Button>
<BurgerButton
onClick={() => setOpen(!open)}
sx={{ color: "white", display: !isTablet ? "block" : "none" }}
/>
</SectionWrapper>
<DialogMenu open={open} handleClose={() => setOpen(false)} />
</>
)
}

@ -1,10 +1,10 @@
import { useState } from "react";
import { Link, useLocation } from "react-router-dom";
import { Box, Button, List, ListItem, Typography, useMediaQuery, useTheme } from "@mui/material";
import { useUserStore } from "@root/stores/user";
import { currencyFormatter } from "@root/utils/currencyFormatter";
import { cardShadow } from "@root/utils/theme";
import { AvatarButton } from "@frontend/kitui";
import { useState } from "react"
import { Link, useLocation } from "react-router-dom"
import { Box, Button, List, ListItem, Typography, useMediaQuery, useTheme } from "@mui/material"
import { useUserStore } from "@root/stores/user"
import { currencyFormatter } from "@root/utils/currencyFormatter"
import { cardShadow } from "@root/utils/theme"
import { AvatarButton } from "@frontend/kitui"
type MenuItem = {
@ -14,186 +14,186 @@ type MenuItem = {
};
const arrayMenu: MenuItem[] = [
{
name: "Тарифы",
url: "/tariffs",
subMenu: [
{ name: "Тарифы на время", url: "/tariffs/time" },
{ name: "Тарифы на объём", url: "/tariffs/volume" },
{ name: "Кастомный тариф", url: "/tariffconstructor" },
],
},
{ name: "Профиль", url: "/settings" },
{ name: "Вопросы и ответы", url: "/faq" },
{ name: "Корзина", url: "/cart" },
{ name: "Поддержка", url: "/support" },
{ name: "История", url: "/history" },
];
{
name: "Тарифы",
url: "/tariffs",
subMenu: [
{ name: "Тарифы на время", url: "/tariffs/time" },
{ name: "Тарифы на объём", url: "/tariffs/volume" },
{ name: "Кастомный тариф", url: "/tariffconstructor" },
],
},
{ name: "Профиль", url: "/settings" },
{ name: "Вопросы и ответы", url: "/faq" },
{ name: "Корзина", url: "/cart" },
{ name: "Поддержка", url: "/support" },
{ name: "История", url: "/history" },
]
interface DialogMenuProps {
handleClose: () => void;
}
export default function DialogMenu({ handleClose }: DialogMenuProps) {
const [activeSubMenuIndex, setActiveSubMenuIndex] = useState<number>(-1);
const theme = useTheme();
const location = useLocation();
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const isMobile = useMediaQuery(theme.breakpoints.down(600));
const user = useUserStore((state) => state.user);
const cash = useUserStore((state) => state.userAccount?.wallet.cash) ?? 0;
const initials = useUserStore(state => state.initials);
const [activeSubMenuIndex, setActiveSubMenuIndex] = useState<number>(-1)
const theme = useTheme()
const location = useLocation()
const isTablet = useMediaQuery(theme.breakpoints.down(1000))
const isMobile = useMediaQuery(theme.breakpoints.down(600))
const user = useUserStore((state) => state.user)
const cash = useUserStore((state) => state.userAccount?.wallet.cash) ?? 0
const initials = useUserStore(state => state.initials)
const closeDialogMenu = () => {
setActiveSubMenuIndex(-1);
const closeDialogMenu = () => {
setActiveSubMenuIndex(-1)
handleClose();
};
handleClose()
}
const handleSubMenu = (index: number) => setActiveSubMenuIndex((activeIndex) => (activeIndex !== index ? index : -1));
const handleSubMenu = (index: number) => setActiveSubMenuIndex((activeIndex) => (activeIndex !== index ? index : -1))
return (
<Box sx={{ height: "100%", maxHeight: "calc(100vh - 51px)" }}>
<List
sx={{
maxWidth: "250px",
background: location.pathname === "/" ? "#333647" : "#FFFFFF",
height: "100%",
}}
>
<ListItem
sx={{
pl: 0,
pr: 0,
flexDirection: "column",
alignItems: isMobile ? "start" : "end",
}}
>
{arrayMenu.map(({ name, url, subMenu = [] }, index) => (
<Box sx={{ width: "100%" }} key={name + index}>
<Button
key={index}
component={Link}
to={url}
state={{ previousUrl: location.pathname }}
disableRipple
variant="text"
onClick={() => (!subMenu.length ? closeDialogMenu() : handleSubMenu(index))}
sx={{
padding: "10px 10px 10px 20px",
display: "block",
fontWeight: 500,
color: location.pathname === url ? "#7E2AEA" : location.pathname === "/" ? "white" : "black",
textTransform: "none",
fontSize: "16px",
borderRadius: 0,
"&:hover, &:active": {
color: "#7E2AEA",
background: index === activeSubMenuIndex ? theme.palette.background.default : "none",
},
}}
>
<Box>{name}</Box>
</Button>
{subMenu.length ? (
<Box
sx={{
backgroundColor: theme.palette.background.paper,
width: "100%",
boxShadow: !isTablet ? cardShadow : null,
}}
>
{index === activeSubMenuIndex &&
return (
<Box sx={{ height: "100%", maxHeight: "calc(100vh - 51px)" }}>
<List
sx={{
maxWidth: "250px",
background: location.pathname === "/" ? "#333647" : "#FFFFFF",
height: "100%",
}}
>
<ListItem
sx={{
pl: 0,
pr: 0,
flexDirection: "column",
alignItems: isMobile ? "start" : "end",
}}
>
{arrayMenu.map(({ name, url, subMenu = [] }, index) => (
<Box sx={{ width: "100%" }} key={name + index}>
<Button
key={index}
component={Link}
to={url}
state={{ previousUrl: location.pathname }}
disableRipple
variant="text"
onClick={() => (!subMenu.length ? closeDialogMenu() : handleSubMenu(index))}
sx={{
padding: "10px 10px 10px 20px",
display: "block",
fontWeight: 500,
color: location.pathname === url ? "#7E2AEA" : location.pathname === "/" ? "white" : "black",
textTransform: "none",
fontSize: "16px",
borderRadius: 0,
"&:hover, &:active": {
color: "#7E2AEA",
background: index === activeSubMenuIndex ? theme.palette.background.default : "none",
},
}}
>
<Box>{name}</Box>
</Button>
{subMenu.length ? (
<Box
sx={{
backgroundColor: theme.palette.background.paper,
width: "100%",
boxShadow: !isTablet ? cardShadow : null,
}}
>
{index === activeSubMenuIndex &&
subMenu.map(({ name, url }) => (
<Link
key={name + url}
style={{
paddingLeft: "30px",
display: "block",
textDecoration: "none",
}}
to={url}
onClick={closeDialogMenu}
state={{ previousUrl: location.pathname }}
>
<Typography
variant="body2"
sx={{
padding: "12px",
whiteSpace: "nowrap",
fontWeight: 400,
color:
<Link
key={name + url}
style={{
paddingLeft: "30px",
display: "block",
textDecoration: "none",
}}
to={url}
onClick={closeDialogMenu}
state={{ previousUrl: location.pathname }}
>
<Typography
variant="body2"
sx={{
padding: "12px",
whiteSpace: "nowrap",
fontWeight: 400,
color:
location.pathname === url ? "#7E2AEA" : location.pathname === "/" ? "white" : "black",
}}
>
{name}
</Typography>
</Link>
}}
>
{name}
</Typography>
</Link>
))}
</Box>
) : (
<></>
)}
</Box>
))}
</ListItem>
{location.pathname === "/" ? (
<Button
component={Link}
to={user ? "/tariffs" : "/signin"}
state={user ? undefined : { backgroundLocation: location }}
variant="outlined"
sx={{
width: "188px",
color: "white",
border: "1px solid white",
ml: "40px",
mt: "35px",
textTransform: "none",
fontWeight: "400",
fontSize: "18px",
lineHeight: "24px",
borderRadius: "8px",
padding: "8px 17px",
}}
>
</Box>
) : (
<></>
)}
</Box>
))}
</ListItem>
{location.pathname === "/" ? (
<Button
component={Link}
to={user ? "/tariffs" : "/signin"}
state={user ? undefined : { backgroundLocation: location }}
variant="outlined"
sx={{
width: "188px",
color: "white",
border: "1px solid white",
ml: "40px",
mt: "35px",
textTransform: "none",
fontWeight: "400",
fontSize: "18px",
lineHeight: "24px",
borderRadius: "8px",
padding: "8px 17px",
}}
>
Личный кабинет
</Button>
) : (
<Box
sx={{
width: "100%",
height: "72px",
background: "#F2F3F7",
display: "flex",
alignItems: "center",
position: "absolute",
bottom: "0",
}}
>
<AvatarButton
component={Link}
to="/settings"
sx={{ ml: "27px" }}
>{initials}</AvatarButton>
<Box sx={{ ml: "8px", whiteSpace: "nowrap" }}>
<Typography
sx={{
fontSize: "12px",
lineHeight: "14px",
color: theme.palette.gray.dark,
}}
>
</Button>
) : (
<Box
sx={{
width: "100%",
height: "72px",
background: "#F2F3F7",
display: "flex",
alignItems: "center",
position: "absolute",
bottom: "0",
}}
>
<AvatarButton
component={Link}
to="/settings"
sx={{ ml: "27px" }}
>{initials}</AvatarButton>
<Box sx={{ ml: "8px", whiteSpace: "nowrap" }}>
<Typography
sx={{
fontSize: "12px",
lineHeight: "14px",
color: theme.palette.gray.dark,
}}
>
Мой баланс
</Typography>
<Typography variant="body2" color={theme.palette.purple.main}>
{currencyFormatter.format(cash / 100)}
</Typography>
</Box>
</Box>
)}
</List>
</Box>
);
</Typography>
<Typography variant="body2" color={theme.palette.purple.main}>
{currencyFormatter.format(cash / 100)}
</Typography>
</Box>
</Box>
)}
</List>
</Box>
)
}

@ -1,28 +1,28 @@
import { Box, useMediaQuery, useTheme } from "@mui/material";
import NavbarCollapsed from "./NavbarCollapsed";
import NavbarFull from "./NavbarFull";
import { Box, useMediaQuery, useTheme } from "@mui/material"
import NavbarCollapsed from "./NavbarCollapsed"
import NavbarFull from "./NavbarFull"
import type { ReactNode } from "react";
import type { ReactNode } from "react"
interface Props {
children: ReactNode;
}
export default function Navbar({ children }: Props) {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up(1000));
const theme = useTheme()
const upMd = useMediaQuery(theme.breakpoints.up(1000))
return (
<Box
sx={{
paddingTop: upMd ? "80px" : 0,
width: "100%",
}}
>
{upMd ? (
<NavbarFull>{children}</NavbarFull>
) : (
<NavbarCollapsed>{children}</NavbarCollapsed>
)}
</Box>
);
return (
<Box
sx={{
paddingTop: upMd ? "80px" : 0,
width: "100%",
}}
>
{upMd ? (
<NavbarFull>{children}</NavbarFull>
) : (
<NavbarCollapsed>{children}</NavbarCollapsed>
)}
</Box>
)
}

@ -1,248 +1,248 @@
import { useState, useRef, useEffect } from "react";
import { useState, useRef, useEffect } from "react"
import {
Box,
Badge,
Drawer,
IconButton,
useTheme,
useMediaQuery,
} from "@mui/material";
import { Link } from "react-router-dom";
Box,
Badge,
Drawer,
IconButton,
useTheme,
useMediaQuery,
} from "@mui/material"
import { Link } from "react-router-dom"
import SectionWrapper from "../SectionWrapper";
import { NotificationsModal } from "../NotificationsModal";
import { NavbarPanel } from "./NavbarPanel";
import SectionWrapper from "../SectionWrapper"
import { NotificationsModal } from "../NotificationsModal"
import { NavbarPanel } from "./NavbarPanel"
import { useUserStore } from "@root/stores/user";
import { useUserStore } from "@root/stores/user"
import {
useTicketStore
} from "@root/stores/tickets";
useTicketStore
} from "@root/stores/tickets"
import { ReactComponent as CartIcon } from "@root/assets/Icons/cart.svg";
import { ReactComponent as BellIcon } from "@root/assets/Icons/bell.svg";
import { ReactComponent as CartIcon } from "@root/assets/Icons/cart.svg"
import { ReactComponent as BellIcon } from "@root/assets/Icons/bell.svg"
import DialogMenu from "./DialogMenu";
import PenaLogo from "../PenaLogo";
import CloseIcon from "../icons/CloseIcons";
import MenuIcon from "@mui/icons-material/Menu";
import DialogMenu from "./DialogMenu"
import PenaLogo from "../PenaLogo"
import CloseIcon from "../icons/CloseIcons"
import MenuIcon from "@mui/icons-material/Menu"
import type { ReactNode } from "react";
import type { ReactNode } from "react"
interface Props {
children: ReactNode;
}
export default function NavbarCollapsed({ children }: Props) {
const [open, setOpen] = useState(false);
const [openNotificationsModal, setOpenNotificationsModal] =
useState<boolean>(false);
const bellRef = useRef<HTMLButtonElement | null>(null);
const userAccount = useUserStore((state) => state.userAccount);
const tickets = useTicketStore((state) => state.tickets);
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(550));
const [open, setOpen] = useState(false)
const [openNotificationsModal, setOpenNotificationsModal] =
useState<boolean>(false)
const bellRef = useRef<HTMLButtonElement | null>(null)
const userAccount = useUserStore((state) => state.userAccount)
const tickets = useTicketStore((state) => state.tickets)
const theme = useTheme()
const isMobile = useMediaQuery(theme.breakpoints.down(550))
const handleClose = () => {
setOpen(false);
};
const handleClose = () => {
setOpen(false)
}
const notificationsCount = tickets.filter(
({ user, top_message }) =>
user !== top_message.user_id && top_message.shown.me !== 1
).length;
const notificationsCount = tickets.filter(
({ user, top_message }) =>
user !== top_message.user_id && top_message.shown.me !== 1
).length
useEffect(() => {
if (open) {
document.body.style.overflow = "hidden";
useEffect(() => {
if (open) {
document.body.style.overflow = "hidden"
return;
}
return
}
document.body.style.overflow = "unset";
}, [open]);
document.body.style.overflow = "unset"
}, [open])
return (
<SectionWrapper
component="nav"
maxWidth="lg"
outerContainerSx={{
backgroundColor: theme.palette.bg.main,
}}
sx={{ height: isMobile ? "51px" : "80px", padding: "0" }}
>
<Box sx={{ height: "100%" }}>
<Box
sx={{
zIndex: 2,
position: "fixed",
top: 0,
left: 0,
width: "100%",
display: "flex",
columnGap: "10px",
alignItems: "center",
height: isMobile ? "51px" : "80px",
padding: "0 18px",
background: "#FFFFFF",
}}
>
<IconButton
onClick={() => setOpen((isOpened) => !isOpened)}
sx={{
p: 0,
width: "30px",
color: theme.palette.primary.main,
}}
>
{open ? (
<CloseIcon />
) : (
<MenuIcon sx={{ height: "30px", width: "30px" }} />
)}
</IconButton>
{isMobile && (
<>
<Link to="/cart">
<IconButton
aria-label="cart"
sx={{
width: "30px",
height: "30px",
background: theme.palette.background.default,
borderRadius: "6px",
"&:hover": {
background: theme.palette.purple.main,
"& .MuiBadge-badge": {
background: theme.palette.background.default,
color: theme.palette.purple.main,
},
"& svg > path:nth-of-type(1)": { fill: "#FFFFFF" },
"& svg > path:nth-of-type(2)": { fill: "#FFFFFF" },
"& svg > path:nth-of-type(3)": { stroke: "#FFFFFF" },
},
}}
>
<Badge
badgeContent={userAccount?.cart.length}
sx={{
"& .MuiBadge-badge": {
display: userAccount?.cart.length ? "flex" : "none",
color: "#FFFFFF",
background: theme.palette.purple.main,
transform: "scale(0.7) translate(50%, -50%)",
top: "2px",
right: "3px",
fontWeight: 400,
},
}}
>
<CartIcon />
</Badge>
</IconButton>
</Link>
<IconButton
ref={bellRef}
onClick={() =>
setOpenNotificationsModal((isOpened) => !isOpened)
}
aria-label="cart"
sx={{
width: "30px",
height: "30px",
background: theme.palette.background.default,
borderRadius: "6px",
"&:hover": {
background: theme.palette.purple.main,
"& .MuiBadge-badge": {
background: theme.palette.background.default,
color: theme.palette.purple.main,
},
"& svg > path:first-of-type": { fill: "#FFFFFF" },
"& svg > path:last-child": { stroke: "#FFFFFF" },
},
}}
>
<Badge
badgeContent={notificationsCount}
sx={{
"& .MuiBadge-badge": {
display: notificationsCount ? "flex" : "none",
color: "#FFFFFF",
background: theme.palette.purple.main,
transform: "scale(0.7) translate(50%, -50%)",
top: "3px",
right: "3px",
fontWeight: 400,
},
}}
>
<BellIcon />
</Badge>
</IconButton>
</>
)}
<NotificationsModal
open={openNotificationsModal}
setOpen={setOpenNotificationsModal}
anchorElement={bellRef.current}
notifications={tickets
.filter(({ user, top_message }) => user !== top_message.user_id)
.map((ticket) => ({
text: "У вас новое сообщение от техподдержки",
date: new Date(ticket.updated_at).toLocaleDateString(),
url: `/support/${ticket.id}`,
watched: ticket.top_message.shown.me === 1,
}))}
/>
<Link to="/" style={{ marginLeft: isMobile ? "auto" : 0 }}>
<PenaLogo width={isMobile ? 100 : 124} color="black" />
</Link>
{!isMobile && <NavbarPanel />}
</Box>
</Box>
<Box sx={{ display: "flex", overflow: open ? "hidden" : "unset" }}>
<Drawer
sx={{
width: 210,
position: "relative",
zIndex: open ? "none" : "-1",
"& .MuiDrawer-paper": {
position: "fixed",
top: "0",
width: 210,
height: "100%",
marginTop: isMobile ? "51px" : "80px",
},
}}
variant="persistent"
anchor="left"
open={open}
>
<DialogMenu handleClose={handleClose} />
</Drawer>
<Box
sx={{
width: "100%",
minWidth: "100%",
minHeight: `calc(100vh - ${isMobile ? 51 : 80}px)`,
flexGrow: 1,
transition: theme.transitions.create("margin", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
marginLeft: `-${210}px`,
...(open && {
transition: theme.transitions.create("margin", {
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.enteringScreen,
}),
marginLeft: 0,
}),
}}
>
{children}
</Box>
</Box>
</SectionWrapper>
);
return (
<SectionWrapper
component="nav"
maxWidth="lg"
outerContainerSx={{
backgroundColor: theme.palette.bg.main,
}}
sx={{ height: isMobile ? "51px" : "80px", padding: "0" }}
>
<Box sx={{ height: "100%" }}>
<Box
sx={{
zIndex: 2,
position: "fixed",
top: 0,
left: 0,
width: "100%",
display: "flex",
columnGap: "10px",
alignItems: "center",
height: isMobile ? "51px" : "80px",
padding: "0 18px",
background: "#FFFFFF",
}}
>
<IconButton
onClick={() => setOpen((isOpened) => !isOpened)}
sx={{
p: 0,
width: "30px",
color: theme.palette.primary.main,
}}
>
{open ? (
<CloseIcon />
) : (
<MenuIcon sx={{ height: "30px", width: "30px" }} />
)}
</IconButton>
{isMobile && (
<>
<Link to="/cart">
<IconButton
aria-label="cart"
sx={{
width: "30px",
height: "30px",
background: theme.palette.background.default,
borderRadius: "6px",
"&:hover": {
background: theme.palette.purple.main,
"& .MuiBadge-badge": {
background: theme.palette.background.default,
color: theme.palette.purple.main,
},
"& svg > path:nth-of-type(1)": { fill: "#FFFFFF" },
"& svg > path:nth-of-type(2)": { fill: "#FFFFFF" },
"& svg > path:nth-of-type(3)": { stroke: "#FFFFFF" },
},
}}
>
<Badge
badgeContent={userAccount?.cart.length}
sx={{
"& .MuiBadge-badge": {
display: userAccount?.cart.length ? "flex" : "none",
color: "#FFFFFF",
background: theme.palette.purple.main,
transform: "scale(0.7) translate(50%, -50%)",
top: "2px",
right: "3px",
fontWeight: 400,
},
}}
>
<CartIcon />
</Badge>
</IconButton>
</Link>
<IconButton
ref={bellRef}
onClick={() =>
setOpenNotificationsModal((isOpened) => !isOpened)
}
aria-label="cart"
sx={{
width: "30px",
height: "30px",
background: theme.palette.background.default,
borderRadius: "6px",
"&:hover": {
background: theme.palette.purple.main,
"& .MuiBadge-badge": {
background: theme.palette.background.default,
color: theme.palette.purple.main,
},
"& svg > path:first-of-type": { fill: "#FFFFFF" },
"& svg > path:last-child": { stroke: "#FFFFFF" },
},
}}
>
<Badge
badgeContent={notificationsCount}
sx={{
"& .MuiBadge-badge": {
display: notificationsCount ? "flex" : "none",
color: "#FFFFFF",
background: theme.palette.purple.main,
transform: "scale(0.7) translate(50%, -50%)",
top: "3px",
right: "3px",
fontWeight: 400,
},
}}
>
<BellIcon />
</Badge>
</IconButton>
</>
)}
<NotificationsModal
open={openNotificationsModal}
setOpen={setOpenNotificationsModal}
anchorElement={bellRef.current}
notifications={tickets
.filter(({ user, top_message }) => user !== top_message.user_id)
.map((ticket) => ({
text: "У вас новое сообщение от техподдержки",
date: new Date(ticket.updated_at).toLocaleDateString(),
url: `/support/${ticket.id}`,
watched: ticket.top_message.shown.me === 1,
}))}
/>
<Link to="/" style={{ marginLeft: isMobile ? "auto" : 0 }}>
<PenaLogo width={isMobile ? 100 : 124} color="black" />
</Link>
{!isMobile && <NavbarPanel />}
</Box>
</Box>
<Box sx={{ display: "flex", overflow: open ? "hidden" : "unset" }}>
<Drawer
sx={{
width: 210,
position: "relative",
zIndex: open ? "none" : "-1",
"& .MuiDrawer-paper": {
position: "fixed",
top: "0",
width: 210,
height: "100%",
marginTop: isMobile ? "51px" : "80px",
},
}}
variant="persistent"
anchor="left"
open={open}
>
<DialogMenu handleClose={handleClose} />
</Drawer>
<Box
sx={{
width: "100%",
minWidth: "100%",
minHeight: `calc(100vh - ${isMobile ? 51 : 80}px)`,
flexGrow: 1,
transition: theme.transitions.create("margin", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
marginLeft: `-${210}px`,
...(open && {
transition: theme.transitions.create("margin", {
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.enteringScreen,
}),
marginLeft: 0,
}),
}}
>
{children}
</Box>
</Box>
</SectionWrapper>
)
}

@ -1,94 +1,94 @@
import { Link, useNavigate } from "react-router-dom";
import { Box, Container, Typography, useTheme } from "@mui/material";
import Drawers from "../Drawers";
import PenaLogo from "../PenaLogo";
import Menu from "../Menu";
import { logout } from "@root/api/auth";
import { enqueueSnackbar } from "notistack";
import { clearUserData, useUserStore } from "@root/stores/user";
import { Link, useNavigate } from "react-router-dom"
import { Box, Container, Typography, useTheme } from "@mui/material"
import Drawers from "../Drawers"
import PenaLogo from "../PenaLogo"
import Menu from "../Menu"
import { logout } from "@root/api/auth"
import { enqueueSnackbar } from "notistack"
import { clearUserData, useUserStore } from "@root/stores/user"
import {
AvatarButton,
LogoutButton,
WalletButton,
clearAuthToken,
} from "@frontend/kitui";
import { clearCustomTariffs } from "@root/stores/customTariffs";
import { currencyFormatter } from "@root/utils/currencyFormatter";
import { clearTickets } from "@root/stores/tickets";
AvatarButton,
LogoutButton,
WalletButton,
clearAuthToken,
} from "@frontend/kitui"
import { clearCustomTariffs } from "@root/stores/customTariffs"
import { currencyFormatter } from "@root/utils/currencyFormatter"
import { clearTickets } from "@root/stores/tickets"
import type { ReactNode } from "react";
import type { ReactNode } from "react"
interface Props {
children: ReactNode;
}
export default function NavbarFull({ children }: Props) {
const theme = useTheme();
const navigate = useNavigate();
const cash = useUserStore((state) => state.userAccount?.wallet.cash) ?? 0;
const initials = useUserStore((state) => state.initials);
const theme = useTheme()
const navigate = useNavigate()
const cash = useUserStore((state) => state.userAccount?.wallet.cash) ?? 0
const initials = useUserStore((state) => state.initials)
async function handleLogoutClick() {
const [_, logoutError] = await logout();
async function handleLogoutClick() {
const [_, logoutError] = await logout()
if (logoutError) {
return enqueueSnackbar(logoutError);
}
if (logoutError) {
return enqueueSnackbar(logoutError)
}
clearAuthToken();
clearUserData();
clearCustomTariffs();
clearTickets();
navigate("/");
}
clearAuthToken()
clearUserData()
clearCustomTariffs()
clearTickets()
navigate("/")
}
return (
<Box>
<Container
component="nav"
disableGutters
maxWidth={false}
sx={{
zIndex: 1,
position: "fixed",
top: "0",
px: "16px",
display: "flex",
height: "80px",
alignItems: "center",
gap: "60px",
bgcolor: "white",
borderBottom: "1px solid #E3E3E3",
}}
>
<Link to="/">
<PenaLogo width={124} color="black" />
</Link>
<Menu />
<Box sx={{ display: "flex", ml: "auto" }}>
<Drawers />
<WalletButton component={Link} to="/wallet" sx={{ ml: "20px" }} />
<Box sx={{ ml: "8px", whiteSpace: "nowrap" }}>
<Typography
sx={{
fontSize: "12px",
lineHeight: "14px",
color: theme.palette.gray.dark,
}}
>
return (
<Box>
<Container
component="nav"
disableGutters
maxWidth={false}
sx={{
zIndex: 1,
position: "fixed",
top: "0",
px: "16px",
display: "flex",
height: "80px",
alignItems: "center",
gap: "60px",
bgcolor: "white",
borderBottom: "1px solid #E3E3E3",
}}
>
<Link to="/">
<PenaLogo width={124} color="black" />
</Link>
<Menu />
<Box sx={{ display: "flex", ml: "auto" }}>
<Drawers />
<WalletButton component={Link} to="/wallet" sx={{ ml: "20px" }} />
<Box sx={{ ml: "8px", whiteSpace: "nowrap" }}>
<Typography
sx={{
fontSize: "12px",
lineHeight: "14px",
color: theme.palette.gray.dark,
}}
>
Мой баланс
</Typography>
<Typography variant="body2" color={theme.palette.purple.main}>
{currencyFormatter.format(cash / 100)}
</Typography>
</Box>
<AvatarButton component={Link} to="/settings" sx={{ ml: "27px" }}>
{initials}
</AvatarButton>
<LogoutButton onClick={handleLogoutClick} sx={{ ml: "20px" }} />
</Box>
</Container>
<Box>{children}</Box>
</Box>
);
</Typography>
<Typography variant="body2" color={theme.palette.purple.main}>
{currencyFormatter.format(cash / 100)}
</Typography>
</Box>
<AvatarButton component={Link} to="/settings" sx={{ ml: "27px" }}>
{initials}
</AvatarButton>
<LogoutButton onClick={handleLogoutClick} sx={{ ml: "20px" }} />
</Box>
</Container>
<Box>{children}</Box>
</Box>
)
}

@ -1,91 +1,91 @@
import { Link, useNavigate } from "react-router-dom";
import { Link, useNavigate } from "react-router-dom"
import {
Box,
IconButton,
Typography,
useTheme,
useMediaQuery,
} from "@mui/material";
import LogoutIcon from "../icons/LogoutIcon";
import Drawers from "../Drawers";
import { logout } from "@root/api/auth";
import { enqueueSnackbar } from "notistack";
import { clearUserData, useUserStore } from "@root/stores/user";
import { AvatarButton, clearAuthToken } from "@frontend/kitui";
import { clearCustomTariffs } from "@root/stores/customTariffs";
import { clearTickets } from "@root/stores/tickets";
Box,
IconButton,
Typography,
useTheme,
useMediaQuery,
} from "@mui/material"
import LogoutIcon from "../icons/LogoutIcon"
import Drawers from "../Drawers"
import { logout } from "@root/api/auth"
import { enqueueSnackbar } from "notistack"
import { clearUserData, useUserStore } from "@root/stores/user"
import { AvatarButton, clearAuthToken } from "@frontend/kitui"
import { clearCustomTariffs } from "@root/stores/customTariffs"
import { clearTickets } from "@root/stores/tickets"
import { currencyFormatter } from "@root/utils/currencyFormatter";
import { currencyFormatter } from "@root/utils/currencyFormatter"
import walletIcon from "@root/assets/Icons/wallet_icon.svg";
import walletIcon from "@root/assets/Icons/wallet_icon.svg"
export const NavbarPanel = () => {
const navigate = useNavigate();
const theme = useTheme();
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const cash = useUserStore((state) => state.userAccount?.wallet.cash) ?? 0;
const initials = useUserStore((state) => state.initials);
const navigate = useNavigate()
const theme = useTheme()
const isTablet = useMediaQuery(theme.breakpoints.down(1000))
const cash = useUserStore((state) => state.userAccount?.wallet.cash) ?? 0
const initials = useUserStore((state) => state.initials)
async function handleLogoutClick() {
const [_, logoutError] = await logout();
async function handleLogoutClick() {
const [_, logoutError] = await logout()
if (logoutError) {
return enqueueSnackbar(logoutError);
}
if (logoutError) {
return enqueueSnackbar(logoutError)
}
clearAuthToken();
clearUserData();
clearCustomTariffs();
clearTickets();
navigate("/");
}
clearAuthToken()
clearUserData()
clearCustomTariffs()
clearTickets()
navigate("/")
}
return (
<Box sx={{ display: "flex", ml: "auto" }}>
<Drawers />
<IconButton
sx={{
display: "flex",
alignItems: "center",
ml: isTablet ? "10px" : "20px",
bgcolor: "#F2F3F7",
borderRadius: "6px",
height: "36px",
width: "36px",
}}
onClick={() => navigate("/wallet")}
>
<img src={walletIcon} alt="wallet" />
</IconButton>
<Box sx={{ ml: "8px", whiteSpace: "nowrap" }}>
<Typography
sx={{
fontSize: "12px",
lineHeight: "14px",
color: theme.palette.gray.dark,
}}
>
return (
<Box sx={{ display: "flex", ml: "auto" }}>
<Drawers />
<IconButton
sx={{
display: "flex",
alignItems: "center",
ml: isTablet ? "10px" : "20px",
bgcolor: "#F2F3F7",
borderRadius: "6px",
height: "36px",
width: "36px",
}}
onClick={() => navigate("/wallet")}
>
<img src={walletIcon} alt="wallet" />
</IconButton>
<Box sx={{ ml: "8px", whiteSpace: "nowrap" }}>
<Typography
sx={{
fontSize: "12px",
lineHeight: "14px",
color: theme.palette.gray.dark,
}}
>
Мой баланс
</Typography>
<Typography variant="body2" color={theme.palette.purple.main}>
{currencyFormatter.format(cash / 100)}
</Typography>
</Box>
<AvatarButton component={Link} to="/settings" sx={{ ml: "27px" }}>
{initials}
</AvatarButton>
<IconButton
onClick={handleLogoutClick}
sx={{
ml: "20px",
bgcolor: "#F2F3F7",
borderRadius: "6px",
height: "36px",
width: "36px",
}}
>
<LogoutIcon />
</IconButton>
</Box>
);
};
</Typography>
<Typography variant="body2" color={theme.palette.purple.main}>
{currencyFormatter.format(cash / 100)}
</Typography>
</Box>
<AvatarButton component={Link} to="/settings" sx={{ ml: "27px" }}>
{initials}
</AvatarButton>
<IconButton
onClick={handleLogoutClick}
sx={{
ml: "20px",
bgcolor: "#F2F3F7",
borderRadius: "6px",
height: "36px",
width: "36px",
}}
>
<LogoutIcon />
</IconButton>
</Box>
)
}

@ -1,12 +1,12 @@
import {
Popover,
List,
ListItem,
Typography,
useTheme,
useMediaQuery,
} from "@mui/material";
import { Link } from "react-router-dom";
Popover,
List,
ListItem,
Typography,
useTheme,
useMediaQuery,
} from "@mui/material"
import { Link } from "react-router-dom"
type Notification = {
text: string;
@ -23,123 +23,123 @@ type NotificationsModalProps = {
};
export const NotificationsModal = ({
open,
setOpen,
anchorElement,
notifications,
open,
setOpen,
anchorElement,
notifications,
}: NotificationsModalProps) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(650));
const theme = useTheme()
const isMobile = useMediaQuery(theme.breakpoints.down(650))
return (
<Popover
open={open}
anchorEl={anchorElement}
onClose={() => setOpen(false)}
anchorOrigin={{ vertical: "bottom", horizontal: "left" }}
transformOrigin={{ vertical: "top", horizontal: "right" }}
sx={{
"& .MuiPopover-paper": {
maxWidth: isMobile ? "calc(100vw - 30px)" : 600,
width: "100%",
maxHeight: "310px",
borderRadius: "8px",
boxShadow:
return (
<Popover
open={open}
anchorEl={anchorElement}
onClose={() => setOpen(false)}
anchorOrigin={{ vertical: "bottom", horizontal: "left" }}
transformOrigin={{ vertical: "top", horizontal: "right" }}
sx={{
"& .MuiPopover-paper": {
maxWidth: isMobile ? "calc(100vw - 30px)" : 600,
width: "100%",
maxHeight: "310px",
borderRadius: "8px",
boxShadow:
"0px 3px 18px rgba(49, 28, 77, 0.1), 0px 3px 34px rgba(49, 28, 77, 0.15)",
"&::-webkit-scrollbar": { width: "6px" },
"&::-webkit-scrollbar-track": {
background: "#F0F0F6",
margin: "5px",
borderRadius: "5px",
},
"&::-webkit-scrollbar-thumb": {
width: "4px",
background: "#9A9AAF",
borderRadius: "5px",
},
},
}}
>
<List sx={{ width: "100%", padding: "5px" }}>
{notifications.length ? (
<>
{notifications.map(({ text, date, url, watched = true }) => (
<Link
key={url}
to={url}
onClick={() => setOpen(false)}
style={{
textDecoration: "none",
color: "inherit",
}}
>
<ListItem
key={text + date}
sx={{
display: "flex",
alignItems: isMobile ? "normal" : "center",
justifyContent: "space-between",
flexDirection: isMobile ? "column-reverse" : "unset",
borderBottom: "1px solid #F2F3F7",
padding: "20px 10px",
background: watched ? "none" : "#F0F0F6",
borderRadius: watched ? "0" : "8px",
"&:first-of-type": {
borderTop: "1px solid #F2F3F7",
},
}}
>
<Typography
sx={{
position: "relative",
fontSize: "16px",
lineHeight: "19px",
paddingLeft: watched ? "0" : "35px",
fontWeight: watched ? "normal" : "bold",
"&::before": {
content: '""',
display: watched ? "none" : "block",
position: "absolute",
left: "10px",
top: isMobile ? "5px" : "50%",
transform: isMobile ? "none" : "translateY(-50%)",
height: "8px",
width: "8px",
borderRadius: "50%",
background: theme.palette.purple.main,
},
}}
>
{text}
</Typography>
<Typography
sx={{
fontSize: "14px",
lineHeight: "19px",
color: "#9A9AAF",
fontWeight: watched ? "normal" : "bold",
paddingLeft: isMobile ? (watched ? "0" : "35px") : "0",
marginBottom: isMobile ? "5px" : "0",
}}
>
{date}
</Typography>
</ListItem>
</Link>
))}
</>
) : (
<Typography
sx={{
textAlign: "center",
color: "#9A9AAF",
padding: "30px 0",
}}
>
"&::-webkit-scrollbar": { width: "6px" },
"&::-webkit-scrollbar-track": {
background: "#F0F0F6",
margin: "5px",
borderRadius: "5px",
},
"&::-webkit-scrollbar-thumb": {
width: "4px",
background: "#9A9AAF",
borderRadius: "5px",
},
},
}}
>
<List sx={{ width: "100%", padding: "5px" }}>
{notifications.length ? (
<>
{notifications.map(({ text, date, url, watched = true }) => (
<Link
key={url}
to={url}
onClick={() => setOpen(false)}
style={{
textDecoration: "none",
color: "inherit",
}}
>
<ListItem
key={text + date}
sx={{
display: "flex",
alignItems: isMobile ? "normal" : "center",
justifyContent: "space-between",
flexDirection: isMobile ? "column-reverse" : "unset",
borderBottom: "1px solid #F2F3F7",
padding: "20px 10px",
background: watched ? "none" : "#F0F0F6",
borderRadius: watched ? "0" : "8px",
"&:first-of-type": {
borderTop: "1px solid #F2F3F7",
},
}}
>
<Typography
sx={{
position: "relative",
fontSize: "16px",
lineHeight: "19px",
paddingLeft: watched ? "0" : "35px",
fontWeight: watched ? "normal" : "bold",
"&::before": {
content: "\"\"",
display: watched ? "none" : "block",
position: "absolute",
left: "10px",
top: isMobile ? "5px" : "50%",
transform: isMobile ? "none" : "translateY(-50%)",
height: "8px",
width: "8px",
borderRadius: "50%",
background: theme.palette.purple.main,
},
}}
>
{text}
</Typography>
<Typography
sx={{
fontSize: "14px",
lineHeight: "19px",
color: "#9A9AAF",
fontWeight: watched ? "normal" : "bold",
paddingLeft: isMobile ? (watched ? "0" : "35px") : "0",
marginBottom: isMobile ? "5px" : "0",
}}
>
{date}
</Typography>
</ListItem>
</Link>
))}
</>
) : (
<Typography
sx={{
textAlign: "center",
color: "#9A9AAF",
padding: "30px 0",
}}
>
Нет оповещений
</Typography>
)}
</List>
</Popover>
);
};
</Typography>
)}
</List>
</Popover>
)
}

@ -1,5 +1,5 @@
import { Box, SxProps, Theme } from "@mui/material";
import { ReactElement } from "react";
import { Box, SxProps, Theme } from "@mui/material"
import { ReactElement } from "react"
interface Props {
@ -10,86 +10,86 @@ interface Props {
}
export default function NumberIcon({ number, backgroundColor = "rgb(0 0 0 / 0)", color, sx }: Props) {
number = number % 100;
number = number % 100
const firstDigit = Math.floor(number / 10);
const secondDigit = number % 10;
const firstDigit = Math.floor(number / 10)
const secondDigit = number % 10
const firstDigitTranslateX = 6;
const secondDigitTranslateX = number < 10
? 9
: number < 20
? 11
: 12;
const firstDigitTranslateX = 6
const secondDigitTranslateX = number < 10
? 9
: number < 20
? 11
: 12
const firstDigitElement = digitSvgs[firstDigit](firstDigitTranslateX);
const secondDigitElement = digitSvgs[secondDigit](secondDigitTranslateX);
const firstDigitElement = digitSvgs[firstDigit](firstDigitTranslateX)
const secondDigitElement = digitSvgs[secondDigit](secondDigitTranslateX)
return (
<Box sx={{
backgroundColor,
color,
width: "36px",
height: "36px",
borderRadius: "6px",
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
...sx,
}}>
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
{circleSvg}
{number > 9 && firstDigitElement}
{secondDigitElement}
</svg>
</Box>
);
return (
<Box sx={{
backgroundColor,
color,
width: "36px",
height: "36px",
borderRadius: "6px",
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
...sx,
}}>
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
{circleSvg}
{number > 9 && firstDigitElement}
{secondDigitElement}
</svg>
</Box>
)
}
const circleSvg = <path d="M12 21C16.9706 21 21 16.9706 21 12C21 7.02944 16.9706 3 12 3C7.02944 3 3 7.02944 3 12C3 16.9706 7.02944 21 12 21Z" stroke="currentColor" strokeWidth="1.5" strokeMiterlimit="10" />;
const circleSvg = <path d="M12 21C16.9706 21 21 16.9706 21 12C21 7.02944 16.9706 3 12 3C7.02944 3 3 7.02944 3 12C3 16.9706 7.02944 21 12 21Z" stroke="currentColor" strokeWidth="1.5" strokeMiterlimit="10" />
const digitSvgs: Record<number, (translateX: number) => ReactElement> = {
0: (translateX: number) => (
<path transform={`translate(${translateX} 7)`} d="M3 8.75C4.24264 8.75 5.25 7.07107 5.25 5C5.25 2.92893 4.24264 1.25 3 1.25C1.75736 1.25 0.75 2.92893 0.75 5C0.75 7.07107 1.75736 8.75 3 8.75Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
),
1: (translateX: number) => (
<path transform={`translate(${translateX} 7)`} d="M1.125 2.75L3.375 1.25V8.75015" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
),
2: (translateX: number) => (
<path transform={`translate(${translateX} 7)`} d="M1.27158 2.39455C1.44019 1.99638 1.74115 1.66868 2.12357 1.46688C2.50598 1.26507 2.94636 1.20155 3.37021 1.28705C3.79407 1.37256 4.17538 1.60185 4.44964 1.93613C4.7239 2.27041 4.87428 2.68916 4.87534 3.12156C4.87703 3.49512 4.76526 3.8604 4.55483 4.16907V4.16907L1.12305 8.75H4.87534" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
),
3: (translateX: number) => (
<path transform={`translate(${translateX} 7)`} d="M1.125 1.25H4.875L2.6875 4.375C3.04723 4.37503 3.40139 4.46376 3.71863 4.63336C4.03587 4.80295 4.30639 5.04816 4.50623 5.34727C4.70607 5.64637 4.82906 5.99015 4.86431 6.34814C4.89956 6.70614 4.84598 7.0673 4.70832 7.39964C4.57066 7.73198 4.35316 8.02525 4.07509 8.25345C3.79702 8.48166 3.46696 8.63777 3.11415 8.70796C2.76133 8.77815 2.39666 8.76024 2.05242 8.65583C1.70818 8.55142 1.395 8.36373 1.14062 8.10938" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
),
4: (translateX: number) => (
<>
<path transform={`translate(${translateX} 7)`} d="M2.62508 1.07788L0.75 6.3906H4.50015" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path transform={`translate(${translateX} 7)`} d="M4.5 3.89038V8.89058" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</>
),
5: (translateX: number) => (
<path transform={`translate(${translateX} 7)`} d="M5.0625 1.25H1.92188L1.3125 5.01562C1.61844 4.70972 2.00821 4.5014 2.43254 4.41702C2.85687 4.33263 3.29669 4.37596 3.69639 4.54153C4.09609 4.70711 4.43772 4.98749 4.67807 5.34721C4.91843 5.70694 5.04672 6.12986 5.04672 6.5625C5.04672 6.99514 4.91843 7.41806 4.67807 7.77779C4.43772 8.13751 4.09609 8.41789 3.69639 8.58346C3.29669 8.74904 2.85687 8.79237 2.43254 8.70798C2.00821 8.6236 1.61844 8.41528 1.3125 8.10937" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
),
6: (translateX: number) => (
<>
<path transform={`translate(${translateX - 1} 7)`} d="M2.00977 5.30469L4.65117 0.875" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path transform={`translate(${translateX - 1} 7)`} d="M3.99609 8.75C5.26462 8.75 6.29297 7.72165 6.29297 6.45312C6.29297 5.1846 5.26462 4.15625 3.99609 4.15625C2.72756 4.15625 1.69922 5.1846 1.69922 6.45312C1.69922 7.72165 2.72756 8.75 3.99609 8.75Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</>
),
7: (translateX: number) => (
<path transform={`translate(${translateX} 7)`} d="M1.125 1.25H4.875L2.375 8.75" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
),
8: (translateX: number) => (
<>
<path transform={`translate(${translateX} 7)`} d="M4.72779 2.97356C4.72547 3.36962 4.58601 3.75265 4.33315 4.05749C4.08029 4.36233 3.72963 4.57017 3.34082 4.64564C2.95201 4.72111 2.54906 4.65956 2.20051 4.47146C1.85196 4.28336 1.57934 3.98032 1.42901 3.6139C1.27868 3.24747 1.25993 2.84028 1.37595 2.46158C1.49196 2.08289 1.73559 1.75608 2.06537 1.53675C2.39516 1.31741 2.79075 1.21909 3.18485 1.25851C3.57895 1.29793 3.94722 1.47266 4.22703 1.75298C4.54727 2.07862 4.72705 2.51684 4.72779 2.97356Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path transform={`translate(${translateX} 7)`} d="M5.04125 6.72925C5.03995 7.19778 4.87634 7.65138 4.57827 8.01287C4.28019 8.37436 3.86607 8.62139 3.40637 8.71193C2.94667 8.80247 2.4698 8.73092 2.05691 8.50946C1.64403 8.288 1.32063 7.93031 1.14177 7.49727C0.962899 7.06422 0.939612 6.58258 1.07587 6.1343C1.21213 5.68602 1.49951 5.2988 1.8891 5.03854C2.2787 4.77829 2.74645 4.66107 3.21273 4.70684C3.67902 4.75261 4.11505 4.95854 4.44661 5.28958C4.63578 5.47847 4.78572 5.70292 4.88777 5.95C4.98983 6.19709 5.04199 6.46192 5.04125 6.72925Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</>
),
9: (translateX: number) => (
<>
<path transform={`translate(${translateX} 7)`} d="M5.03203 4.47046L2.39062 8.90015" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path transform={`translate(${translateX} 7)`} d="M3.04688 5.6189C4.3154 5.6189 5.34375 4.59055 5.34375 3.32202C5.34375 2.05349 4.3154 1.02515 3.04688 1.02515C1.77835 1.02515 0.75 2.05349 0.75 3.32202C0.75 4.59055 1.77835 5.6189 3.04688 5.6189Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</>
),
};
0: (translateX: number) => (
<path transform={`translate(${translateX} 7)`} d="M3 8.75C4.24264 8.75 5.25 7.07107 5.25 5C5.25 2.92893 4.24264 1.25 3 1.25C1.75736 1.25 0.75 2.92893 0.75 5C0.75 7.07107 1.75736 8.75 3 8.75Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
),
1: (translateX: number) => (
<path transform={`translate(${translateX} 7)`} d="M1.125 2.75L3.375 1.25V8.75015" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
),
2: (translateX: number) => (
<path transform={`translate(${translateX} 7)`} d="M1.27158 2.39455C1.44019 1.99638 1.74115 1.66868 2.12357 1.46688C2.50598 1.26507 2.94636 1.20155 3.37021 1.28705C3.79407 1.37256 4.17538 1.60185 4.44964 1.93613C4.7239 2.27041 4.87428 2.68916 4.87534 3.12156C4.87703 3.49512 4.76526 3.8604 4.55483 4.16907V4.16907L1.12305 8.75H4.87534" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
),
3: (translateX: number) => (
<path transform={`translate(${translateX} 7)`} d="M1.125 1.25H4.875L2.6875 4.375C3.04723 4.37503 3.40139 4.46376 3.71863 4.63336C4.03587 4.80295 4.30639 5.04816 4.50623 5.34727C4.70607 5.64637 4.82906 5.99015 4.86431 6.34814C4.89956 6.70614 4.84598 7.0673 4.70832 7.39964C4.57066 7.73198 4.35316 8.02525 4.07509 8.25345C3.79702 8.48166 3.46696 8.63777 3.11415 8.70796C2.76133 8.77815 2.39666 8.76024 2.05242 8.65583C1.70818 8.55142 1.395 8.36373 1.14062 8.10938" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
),
4: (translateX: number) => (
<>
<path transform={`translate(${translateX} 7)`} d="M2.62508 1.07788L0.75 6.3906H4.50015" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path transform={`translate(${translateX} 7)`} d="M4.5 3.89038V8.89058" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</>
),
5: (translateX: number) => (
<path transform={`translate(${translateX} 7)`} d="M5.0625 1.25H1.92188L1.3125 5.01562C1.61844 4.70972 2.00821 4.5014 2.43254 4.41702C2.85687 4.33263 3.29669 4.37596 3.69639 4.54153C4.09609 4.70711 4.43772 4.98749 4.67807 5.34721C4.91843 5.70694 5.04672 6.12986 5.04672 6.5625C5.04672 6.99514 4.91843 7.41806 4.67807 7.77779C4.43772 8.13751 4.09609 8.41789 3.69639 8.58346C3.29669 8.74904 2.85687 8.79237 2.43254 8.70798C2.00821 8.6236 1.61844 8.41528 1.3125 8.10937" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
),
6: (translateX: number) => (
<>
<path transform={`translate(${translateX - 1} 7)`} d="M2.00977 5.30469L4.65117 0.875" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path transform={`translate(${translateX - 1} 7)`} d="M3.99609 8.75C5.26462 8.75 6.29297 7.72165 6.29297 6.45312C6.29297 5.1846 5.26462 4.15625 3.99609 4.15625C2.72756 4.15625 1.69922 5.1846 1.69922 6.45312C1.69922 7.72165 2.72756 8.75 3.99609 8.75Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</>
),
7: (translateX: number) => (
<path transform={`translate(${translateX} 7)`} d="M1.125 1.25H4.875L2.375 8.75" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
),
8: (translateX: number) => (
<>
<path transform={`translate(${translateX} 7)`} d="M4.72779 2.97356C4.72547 3.36962 4.58601 3.75265 4.33315 4.05749C4.08029 4.36233 3.72963 4.57017 3.34082 4.64564C2.95201 4.72111 2.54906 4.65956 2.20051 4.47146C1.85196 4.28336 1.57934 3.98032 1.42901 3.6139C1.27868 3.24747 1.25993 2.84028 1.37595 2.46158C1.49196 2.08289 1.73559 1.75608 2.06537 1.53675C2.39516 1.31741 2.79075 1.21909 3.18485 1.25851C3.57895 1.29793 3.94722 1.47266 4.22703 1.75298C4.54727 2.07862 4.72705 2.51684 4.72779 2.97356Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path transform={`translate(${translateX} 7)`} d="M5.04125 6.72925C5.03995 7.19778 4.87634 7.65138 4.57827 8.01287C4.28019 8.37436 3.86607 8.62139 3.40637 8.71193C2.94667 8.80247 2.4698 8.73092 2.05691 8.50946C1.64403 8.288 1.32063 7.93031 1.14177 7.49727C0.962899 7.06422 0.939612 6.58258 1.07587 6.1343C1.21213 5.68602 1.49951 5.2988 1.8891 5.03854C2.2787 4.77829 2.74645 4.66107 3.21273 4.70684C3.67902 4.75261 4.11505 4.95854 4.44661 5.28958C4.63578 5.47847 4.78572 5.70292 4.88777 5.95C4.98983 6.19709 5.04199 6.46192 5.04125 6.72925Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</>
),
9: (translateX: number) => (
<>
<path transform={`translate(${translateX} 7)`} d="M5.03203 4.47046L2.39062 8.90015" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path transform={`translate(${translateX} 7)`} d="M3.04688 5.6189C4.3154 5.6189 5.34375 4.59055 5.34375 3.32202C5.34375 2.05349 4.3154 1.02515 3.04688 1.02515C1.77835 1.02515 0.75 2.05349 0.75 3.32202C0.75 4.59055 1.77835 5.6189 3.04688 5.6189Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</>
),
}

@ -1,7 +1,7 @@
import { useState } from "react";
import { InputAdornment, TextField, Typography, useTheme } from "@mui/material";
import { useState } from "react"
import { InputAdornment, TextField, Typography, useTheme } from "@mui/material"
import type { ChangeEvent } from "react";
import type { ChangeEvent } from "react"
interface Props {
id: string;
@ -11,92 +11,92 @@ interface Props {
}
export default function NumberInputWithUnitAdornment({ id, value, adornmentText, onChange }: Props) {
const theme = useTheme();
const [changed, setChanged] = useState<boolean>(false);
const theme = useTheme()
const [changed, setChanged] = useState<boolean>(false)
return (
<TextField
type="number"
size="small"
placeholder="Введите вручную"
id={id}
value={changed ? (value !== 0 ? value : "") : ""}
onChange={({ target }: ChangeEvent<HTMLInputElement>) => {
if (!changed) {
setChanged(true);
}
return (
<TextField
type="number"
size="small"
placeholder="Введите вручную"
id={id}
value={changed ? (value !== 0 ? value : "") : ""}
onChange={({ target }: ChangeEvent<HTMLInputElement>) => {
if (!changed) {
setChanged(true)
}
if (Number(target.value) > 999999) {
target.value = "999999";
}
if (Number(target.value) > 999999) {
target.value = "999999"
}
const newNumber = parseInt(target.value);
const newNumber = parseInt(target.value)
if (!isFinite(newNumber) || newNumber < 0) {
onChange(0);
if (!isFinite(newNumber) || newNumber < 0) {
onChange(0)
return;
}
return
}
onChange(newNumber);
}}
sx={{
maxWidth: "200px",
minWidth: "200px",
".MuiInputBase-root": {
display: "flex",
pr: 0,
height: "48px",
borderRadius: "8px",
backgroundColor: "#F2F3F7",
fieldset: {
border: "1px solid" + theme.palette.gray.main,
},
"&.Mui-focused fieldset": {
borderColor: theme.palette.purple.main,
},
input: {
height: "31px",
borderRight: !changed ? "none" : "1px solid #9A9AAF",
},
"&.Mui-focused input": {
borderRight: "1px solid #9A9AAF",
},
"&:not(.Mui-focused) .MuiInputAdornment-root": {
display: !changed ? "none" : undefined,
},
"&.Mui-focused ::-webkit-input-placeholder": {
color: "transparent",
},
onChange(newNumber)
}}
sx={{
maxWidth: "200px",
minWidth: "200px",
".MuiInputBase-root": {
display: "flex",
pr: 0,
height: "48px",
borderRadius: "8px",
backgroundColor: "#F2F3F7",
fieldset: {
border: "1px solid" + theme.palette.gray.main,
},
"&.Mui-focused fieldset": {
borderColor: theme.palette.purple.main,
},
input: {
height: "31px",
borderRight: !changed ? "none" : "1px solid #9A9AAF",
},
"&.Mui-focused input": {
borderRight: "1px solid #9A9AAF",
},
"&:not(.Mui-focused) .MuiInputAdornment-root": {
display: !changed ? "none" : undefined,
},
"&.Mui-focused ::-webkit-input-placeholder": {
color: "transparent",
},
// Hiding arrows
"input::-webkit-outer-spin-button, input::-webkit-inner-spin-button": {
WebkitAppearance: "none",
margin: 0,
},
"input[type = number]": {
MozAppearance: "textfield",
},
},
}}
InputProps={{
endAdornment: (
<InputAdornment
position="end"
sx={{
userSelect: "none",
pointerEvents: "none",
pl: "2px",
pr: "13px",
}}
>
<Typography variant="body2" color="#4D4D4D">
{adornmentText}
</Typography>
</InputAdornment>
),
}}
/>
);
// Hiding arrows
"input::-webkit-outer-spin-button, input::-webkit-inner-spin-button": {
WebkitAppearance: "none",
margin: 0,
},
"input[type = number]": {
MozAppearance: "textfield",
},
},
}}
InputProps={{
endAdornment: (
<InputAdornment
position="end"
sx={{
userSelect: "none",
pointerEvents: "none",
pl: "2px",
pr: "13px",
}}
>
<Typography variant="body2" color="#4D4D4D">
{adornmentText}
</Typography>
</InputAdornment>
),
}}
/>
)
}

@ -1,4 +1,4 @@
import { useTheme } from "@mui/material";
import { useTheme } from "@mui/material"
interface Props {
@ -7,29 +7,29 @@ interface Props {
}
export default function PenaLogo({ width, color }: Props) {
const theme = useTheme();
const theme = useTheme()
return (
<svg style={{ minWidth: width }} width={width} viewBox="0 0 180 70" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clipPath="url(#clip0_122_333)">
<path fillRule="evenodd" clipRule="evenodd" d="M25.9138 3.31953C18.594 2.47163 13.5439 10.3345 8.84663 16.0182C4.72431 21.0062 1.6549 26.6402 1.29838 33.1042C0.919075 39.9813 2.16658 47.1434 6.85174 52.1872C11.6777 57.3826 18.9068 60.6653 25.9138 59.604C32.3391 58.6308 35.1822 51.5749 39.9658 47.1716C45.16 42.3905 54.837 40.1667 54.7027 33.1042C54.5683 26.0308 44.3552 24.6462 39.441 19.5621C34.3509 14.2959 33.1853 4.16182 25.9138 3.31953Z" fill={theme.palette.purple.main} />
<circle cx="44.126" cy="56.9181" r="4.03906" fill={theme.palette.purple.main} />
<circle cx="40.0865" cy="12.1038" r="1.53869" fill={theme.palette.purple.main} />
<path d="M64.699 31.4509C64.2503 27.0891 62.1983 23.0492 58.9405 20.1143C55.6828 17.1794 51.4514 15.5585 47.0666 15.5658C46.4441 15.5661 45.822 15.5986 45.2028 15.6634C40.8429 16.1211 36.807 18.1771 33.8735 21.4347C30.9399 24.6923 29.3165 28.9208 29.3164 33.3046V33.3046V58.6457H36.9188V47.8758C39.8912 49.9436 43.4267 51.0493 47.0476 51.0434C47.6702 51.0432 48.2923 51.0106 48.9115 50.9458C51.2282 50.7024 53.4744 50.0049 55.5216 48.8934C57.5688 47.7818 59.3771 46.2779 60.8431 44.4675C62.3091 42.6571 63.4042 40.5757 64.0658 38.3421C64.7274 36.1084 64.9425 33.7664 64.699 31.4496V31.4509ZM54.935 39.6868C54.0999 40.7241 53.0673 41.5855 51.897 42.2211C50.7266 42.8566 49.4418 43.2536 48.117 43.3891C47.7617 43.426 47.4048 43.4446 47.0476 43.4449C44.7485 43.4427 42.5183 42.6591 40.7233 41.2225C38.9282 39.7859 37.6749 37.7817 37.1689 35.5389C36.663 33.2961 36.9346 30.9479 37.9391 28.8798C38.9436 26.8117 40.6213 25.1465 42.6969 24.1576C44.7725 23.1686 47.1226 22.9147 49.3616 23.4374C51.6005 23.9601 53.5952 25.2285 55.0183 27.0343C56.4414 28.8401 57.2083 31.076 57.1932 33.3751C57.1781 35.6742 56.3818 37.8999 54.935 39.6868Z" fill={color} />
<path d="M84.5348 15.5659C83.9123 15.5661 83.2902 15.5987 82.671 15.6634C78.1535 16.1392 73.9907 18.3298 71.0405 21.7839C68.0903 25.2379 66.5776 29.6921 66.8141 34.2284C67.0507 38.7647 69.0184 43.0374 72.3119 46.1658C75.6053 49.2943 79.9734 51.0401 84.5158 51.0435C85.1384 51.0433 85.7605 51.0107 86.3797 50.9459C89.6368 50.5992 92.7351 49.3601 95.3331 47.3652C97.9312 45.3704 99.9282 42.6971 101.104 39.6399H92.4388L92.4033 39.6843C91.2933 41.0563 89.8444 42.1147 88.1999 42.7548C86.5554 43.395 84.7722 43.5947 83.0268 43.3343C81.2814 43.0738 79.6342 42.3622 78.2482 41.2698C76.8622 40.1774 75.7855 38.7421 75.1244 37.1058H101.859C102.422 34.5159 102.399 31.8328 101.79 29.2532C101.181 26.6736 100.003 24.2628 98.3424 22.1975C96.6813 20.1322 94.5791 18.4648 92.19 17.3173C89.8009 16.1698 87.1853 15.5714 84.5348 15.5659V15.5659ZM75.1244 29.5035C75.8165 27.8 76.9578 26.3163 78.4267 25.2104C79.8956 24.1046 81.6371 23.418 83.4655 23.224C83.8207 23.1871 84.1777 23.1685 84.5348 23.1682C86.5541 23.1648 88.528 23.7666 90.202 24.8958C91.876 26.025 93.1732 27.6299 93.9263 29.5035H75.1244Z" fill={color} />
<path d="M120.638 15.5659C117.732 15.5613 114.882 16.3602 112.402 17.8745V15.5659H104.8V51.0435H112.402V31.4041C112.402 29.2198 113.27 27.125 114.814 25.5805C116.359 24.0359 118.454 23.1682 120.638 23.1682C122.822 23.1682 124.917 24.0359 126.462 25.5805C128.006 27.125 128.874 29.2198 128.874 31.4041V51.0435H136.476V31.4041C136.476 27.2035 134.808 23.175 131.837 20.2048C128.867 17.2345 124.839 15.5659 120.638 15.5659Z" fill={color} />
<path d="M174.491 35.5715V15.5659H166.889V18.7335C163.917 16.6648 160.381 15.559 156.76 15.5659C156.138 15.5662 155.516 15.5987 154.896 15.6635C150.379 16.1392 146.216 18.3299 143.266 21.7839C140.316 25.2379 138.803 29.6921 139.039 34.2284C139.276 38.7647 141.244 43.0374 144.537 46.1659C147.831 49.2944 152.199 51.0402 156.741 51.0435C157.364 51.0432 157.986 51.0107 158.605 50.9459C163.023 50.4938 167.108 48.3888 170.04 45.0529C172.319 48.1011 175.618 50.2275 179.335 51.0435V43.0737C177.893 42.4204 176.669 41.3655 175.81 40.0351C174.951 38.7047 174.493 37.1551 174.491 35.5715ZM164.629 39.6843C163.793 40.7215 162.761 41.5828 161.59 42.2182C160.42 42.8537 159.135 43.2509 157.811 43.3867C157.455 43.4236 157.098 43.4422 156.741 43.4424C154.144 43.4423 151.646 42.4452 149.762 40.6567C147.879 38.8683 146.753 36.4251 146.619 33.8312C146.484 31.2374 147.35 28.6908 149.039 26.717C150.727 24.7433 153.109 23.4929 155.692 23.224C156.047 23.1871 156.403 23.1684 156.76 23.1682C158.674 23.1699 160.548 23.7133 162.166 24.7356C163.784 25.7579 165.079 27.2173 165.903 28.9451C166.726 30.6728 167.043 32.5983 166.817 34.4988C166.592 36.3993 165.833 38.1971 164.629 39.6843Z" fill={color} />
<g display={width < 120 ? "none" : undefined}>
<path d="M147.519 54.8936V59.1273C148.231 58.2947 149.082 57.8784 150.072 57.8784C150.58 57.8784 151.038 57.9727 151.446 58.1612C151.855 58.3497 152.161 58.5906 152.365 58.8838C152.575 59.177 152.716 59.5017 152.79 59.8578C152.868 60.2139 152.907 60.7663 152.907 61.5151V66.4085H150.7V62.0021C150.7 61.1276 150.658 60.5725 150.575 60.3369C150.491 60.1013 150.342 59.9154 150.127 59.7792C149.917 59.6378 149.653 59.5672 149.334 59.5672C148.967 59.5672 148.64 59.6562 148.352 59.8342C148.064 60.0122 147.852 60.2819 147.716 60.6432C147.585 60.9993 147.519 61.5282 147.519 62.2299V66.4085H145.312V54.8936H147.519Z" fill={color} />
<path d="M160.644 66.4085V65.1597C160.341 65.6048 159.94 65.9556 159.442 66.2122C158.95 66.4688 158.429 66.5971 157.879 66.5971C157.319 66.5971 156.816 66.474 156.371 66.2279C155.926 65.9818 155.604 65.6362 155.405 65.1911C155.206 64.746 155.107 64.1307 155.107 63.3452V58.0669H157.314V61.9C157.314 63.0729 157.353 63.7929 157.432 64.06C157.515 64.3218 157.665 64.5313 157.879 64.6884C158.094 64.8402 158.366 64.9162 158.696 64.9162C159.073 64.9162 159.411 64.814 159.71 64.6098C160.008 64.4004 160.212 64.1438 160.322 63.8401C160.432 63.5311 160.487 62.7797 160.487 61.5858V58.0669H162.694V66.4085H160.644Z" fill={color} />
<path d="M164.894 66.4085V54.8936H167.101V59.0409C167.782 58.2659 168.588 57.8784 169.52 57.8784C170.536 57.8784 171.376 58.2476 172.041 58.9859C172.706 59.719 173.039 60.7741 173.039 62.1513C173.039 63.5756 172.699 64.6727 172.018 65.4424C171.342 66.2122 170.52 66.5971 169.551 66.5971C169.075 66.5971 168.604 66.4792 168.138 66.2436C167.677 66.0027 167.279 65.6493 166.944 65.1832V66.4085H164.894ZM167.085 62.0571C167.085 62.9211 167.221 63.5599 167.493 63.9736C167.876 64.5601 168.384 64.8533 169.017 64.8533C169.504 64.8533 169.918 64.6465 170.258 64.2328C170.604 63.8139 170.777 63.1567 170.777 62.2613C170.777 61.3083 170.604 60.6223 170.258 60.2034C169.913 59.7792 169.47 59.5672 168.931 59.5672C168.402 59.5672 167.962 59.774 167.611 60.1877C167.26 60.5961 167.085 61.2192 167.085 62.0571Z" fill={color} />
</g>
</g>
<defs>
<clipPath id="clip0_122_333">
<rect width="179.509" height="69.4872" fill="white" />
</clipPath>
</defs>
</svg>
);
return (
<svg style={{ minWidth: width }} width={width} viewBox="0 0 180 70" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clipPath="url(#clip0_122_333)">
<path fillRule="evenodd" clipRule="evenodd" d="M25.9138 3.31953C18.594 2.47163 13.5439 10.3345 8.84663 16.0182C4.72431 21.0062 1.6549 26.6402 1.29838 33.1042C0.919075 39.9813 2.16658 47.1434 6.85174 52.1872C11.6777 57.3826 18.9068 60.6653 25.9138 59.604C32.3391 58.6308 35.1822 51.5749 39.9658 47.1716C45.16 42.3905 54.837 40.1667 54.7027 33.1042C54.5683 26.0308 44.3552 24.6462 39.441 19.5621C34.3509 14.2959 33.1853 4.16182 25.9138 3.31953Z" fill={theme.palette.purple.main} />
<circle cx="44.126" cy="56.9181" r="4.03906" fill={theme.palette.purple.main} />
<circle cx="40.0865" cy="12.1038" r="1.53869" fill={theme.palette.purple.main} />
<path d="M64.699 31.4509C64.2503 27.0891 62.1983 23.0492 58.9405 20.1143C55.6828 17.1794 51.4514 15.5585 47.0666 15.5658C46.4441 15.5661 45.822 15.5986 45.2028 15.6634C40.8429 16.1211 36.807 18.1771 33.8735 21.4347C30.9399 24.6923 29.3165 28.9208 29.3164 33.3046V33.3046V58.6457H36.9188V47.8758C39.8912 49.9436 43.4267 51.0493 47.0476 51.0434C47.6702 51.0432 48.2923 51.0106 48.9115 50.9458C51.2282 50.7024 53.4744 50.0049 55.5216 48.8934C57.5688 47.7818 59.3771 46.2779 60.8431 44.4675C62.3091 42.6571 63.4042 40.5757 64.0658 38.3421C64.7274 36.1084 64.9425 33.7664 64.699 31.4496V31.4509ZM54.935 39.6868C54.0999 40.7241 53.0673 41.5855 51.897 42.2211C50.7266 42.8566 49.4418 43.2536 48.117 43.3891C47.7617 43.426 47.4048 43.4446 47.0476 43.4449C44.7485 43.4427 42.5183 42.6591 40.7233 41.2225C38.9282 39.7859 37.6749 37.7817 37.1689 35.5389C36.663 33.2961 36.9346 30.9479 37.9391 28.8798C38.9436 26.8117 40.6213 25.1465 42.6969 24.1576C44.7725 23.1686 47.1226 22.9147 49.3616 23.4374C51.6005 23.9601 53.5952 25.2285 55.0183 27.0343C56.4414 28.8401 57.2083 31.076 57.1932 33.3751C57.1781 35.6742 56.3818 37.8999 54.935 39.6868Z" fill={color} />
<path d="M84.5348 15.5659C83.9123 15.5661 83.2902 15.5987 82.671 15.6634C78.1535 16.1392 73.9907 18.3298 71.0405 21.7839C68.0903 25.2379 66.5776 29.6921 66.8141 34.2284C67.0507 38.7647 69.0184 43.0374 72.3119 46.1658C75.6053 49.2943 79.9734 51.0401 84.5158 51.0435C85.1384 51.0433 85.7605 51.0107 86.3797 50.9459C89.6368 50.5992 92.7351 49.3601 95.3331 47.3652C97.9312 45.3704 99.9282 42.6971 101.104 39.6399H92.4388L92.4033 39.6843C91.2933 41.0563 89.8444 42.1147 88.1999 42.7548C86.5554 43.395 84.7722 43.5947 83.0268 43.3343C81.2814 43.0738 79.6342 42.3622 78.2482 41.2698C76.8622 40.1774 75.7855 38.7421 75.1244 37.1058H101.859C102.422 34.5159 102.399 31.8328 101.79 29.2532C101.181 26.6736 100.003 24.2628 98.3424 22.1975C96.6813 20.1322 94.5791 18.4648 92.19 17.3173C89.8009 16.1698 87.1853 15.5714 84.5348 15.5659V15.5659ZM75.1244 29.5035C75.8165 27.8 76.9578 26.3163 78.4267 25.2104C79.8956 24.1046 81.6371 23.418 83.4655 23.224C83.8207 23.1871 84.1777 23.1685 84.5348 23.1682C86.5541 23.1648 88.528 23.7666 90.202 24.8958C91.876 26.025 93.1732 27.6299 93.9263 29.5035H75.1244Z" fill={color} />
<path d="M120.638 15.5659C117.732 15.5613 114.882 16.3602 112.402 17.8745V15.5659H104.8V51.0435H112.402V31.4041C112.402 29.2198 113.27 27.125 114.814 25.5805C116.359 24.0359 118.454 23.1682 120.638 23.1682C122.822 23.1682 124.917 24.0359 126.462 25.5805C128.006 27.125 128.874 29.2198 128.874 31.4041V51.0435H136.476V31.4041C136.476 27.2035 134.808 23.175 131.837 20.2048C128.867 17.2345 124.839 15.5659 120.638 15.5659Z" fill={color} />
<path d="M174.491 35.5715V15.5659H166.889V18.7335C163.917 16.6648 160.381 15.559 156.76 15.5659C156.138 15.5662 155.516 15.5987 154.896 15.6635C150.379 16.1392 146.216 18.3299 143.266 21.7839C140.316 25.2379 138.803 29.6921 139.039 34.2284C139.276 38.7647 141.244 43.0374 144.537 46.1659C147.831 49.2944 152.199 51.0402 156.741 51.0435C157.364 51.0432 157.986 51.0107 158.605 50.9459C163.023 50.4938 167.108 48.3888 170.04 45.0529C172.319 48.1011 175.618 50.2275 179.335 51.0435V43.0737C177.893 42.4204 176.669 41.3655 175.81 40.0351C174.951 38.7047 174.493 37.1551 174.491 35.5715ZM164.629 39.6843C163.793 40.7215 162.761 41.5828 161.59 42.2182C160.42 42.8537 159.135 43.2509 157.811 43.3867C157.455 43.4236 157.098 43.4422 156.741 43.4424C154.144 43.4423 151.646 42.4452 149.762 40.6567C147.879 38.8683 146.753 36.4251 146.619 33.8312C146.484 31.2374 147.35 28.6908 149.039 26.717C150.727 24.7433 153.109 23.4929 155.692 23.224C156.047 23.1871 156.403 23.1684 156.76 23.1682C158.674 23.1699 160.548 23.7133 162.166 24.7356C163.784 25.7579 165.079 27.2173 165.903 28.9451C166.726 30.6728 167.043 32.5983 166.817 34.4988C166.592 36.3993 165.833 38.1971 164.629 39.6843Z" fill={color} />
<g display={width < 120 ? "none" : undefined}>
<path d="M147.519 54.8936V59.1273C148.231 58.2947 149.082 57.8784 150.072 57.8784C150.58 57.8784 151.038 57.9727 151.446 58.1612C151.855 58.3497 152.161 58.5906 152.365 58.8838C152.575 59.177 152.716 59.5017 152.79 59.8578C152.868 60.2139 152.907 60.7663 152.907 61.5151V66.4085H150.7V62.0021C150.7 61.1276 150.658 60.5725 150.575 60.3369C150.491 60.1013 150.342 59.9154 150.127 59.7792C149.917 59.6378 149.653 59.5672 149.334 59.5672C148.967 59.5672 148.64 59.6562 148.352 59.8342C148.064 60.0122 147.852 60.2819 147.716 60.6432C147.585 60.9993 147.519 61.5282 147.519 62.2299V66.4085H145.312V54.8936H147.519Z" fill={color} />
<path d="M160.644 66.4085V65.1597C160.341 65.6048 159.94 65.9556 159.442 66.2122C158.95 66.4688 158.429 66.5971 157.879 66.5971C157.319 66.5971 156.816 66.474 156.371 66.2279C155.926 65.9818 155.604 65.6362 155.405 65.1911C155.206 64.746 155.107 64.1307 155.107 63.3452V58.0669H157.314V61.9C157.314 63.0729 157.353 63.7929 157.432 64.06C157.515 64.3218 157.665 64.5313 157.879 64.6884C158.094 64.8402 158.366 64.9162 158.696 64.9162C159.073 64.9162 159.411 64.814 159.71 64.6098C160.008 64.4004 160.212 64.1438 160.322 63.8401C160.432 63.5311 160.487 62.7797 160.487 61.5858V58.0669H162.694V66.4085H160.644Z" fill={color} />
<path d="M164.894 66.4085V54.8936H167.101V59.0409C167.782 58.2659 168.588 57.8784 169.52 57.8784C170.536 57.8784 171.376 58.2476 172.041 58.9859C172.706 59.719 173.039 60.7741 173.039 62.1513C173.039 63.5756 172.699 64.6727 172.018 65.4424C171.342 66.2122 170.52 66.5971 169.551 66.5971C169.075 66.5971 168.604 66.4792 168.138 66.2436C167.677 66.0027 167.279 65.6493 166.944 65.1832V66.4085H164.894ZM167.085 62.0571C167.085 62.9211 167.221 63.5599 167.493 63.9736C167.876 64.5601 168.384 64.8533 169.017 64.8533C169.504 64.8533 169.918 64.6465 170.258 64.2328C170.604 63.8139 170.777 63.1567 170.777 62.2613C170.777 61.3083 170.604 60.6223 170.258 60.2034C169.913 59.7792 169.47 59.5672 168.931 59.5672C168.402 59.5672 167.962 59.774 167.611 60.1877C167.26 60.5961 167.085 61.2192 167.085 62.0571Z" fill={color} />
</g>
</g>
<defs>
<clipPath id="clip0_122_333">
<rect width="179.509" height="69.4872" fill="white" />
</clipPath>
</defs>
</svg>
)
}

@ -1,85 +1,85 @@
import { Outlet } from "react-router-dom";
import Navbar from "./NavbarSite/Navbar";
import { Outlet } from "react-router-dom"
import Navbar from "./NavbarSite/Navbar"
import {
Ticket,
getMessageFromFetchError,
useAllTariffsFetcher,
usePrivilegeFetcher,
useSSESubscription,
useTicketsFetcher,
useToken,
} from "@frontend/kitui";
import { updateTickets, setTicketCount, useTicketStore, setTicketsFetchState } from "@root/stores/tickets";
import { enqueueSnackbar } from "notistack";
import { updateTariffs } from "@root/stores/tariffs";
import { useCustomTariffs } from "@root/utils/hooks/useCustomTariffs";
import { setCustomTariffs } from "@root/stores/customTariffs";
import { useDiscounts } from "@root/utils/hooks/useDiscounts";
import { setDiscounts } from "@root/stores/discounts";
import { setPrivileges } from "@root/stores/privileges";
Ticket,
getMessageFromFetchError,
useAllTariffsFetcher,
usePrivilegeFetcher,
useSSESubscription,
useTicketsFetcher,
useToken,
} from "@frontend/kitui"
import { updateTickets, setTicketCount, useTicketStore, setTicketsFetchState } from "@root/stores/tickets"
import { enqueueSnackbar } from "notistack"
import { updateTariffs } from "@root/stores/tariffs"
import { useCustomTariffs } from "@root/utils/hooks/useCustomTariffs"
import { setCustomTariffs } from "@root/stores/customTariffs"
import { useDiscounts } from "@root/utils/hooks/useDiscounts"
import { setDiscounts } from "@root/stores/discounts"
import { setPrivileges } from "@root/stores/privileges"
export default function ProtectedLayout() {
const token = useToken();
const ticketApiPage = useTicketStore((state) => state.apiPage);
const ticketsPerPage = useTicketStore((state) => state.ticketsPerPage);
const token = useToken()
const ticketApiPage = useTicketStore((state) => state.apiPage)
const ticketsPerPage = useTicketStore((state) => state.ticketsPerPage)
useSSESubscription<Ticket>({
url: `https://hub.pena.digital/heruvym/subscribe?Authorization=${token}`,
onNewData: (data) => {
updateTickets(data.filter((d) => Boolean(d.id)));
setTicketCount(data.length);
},
marker: "ticket",
});
useSSESubscription<Ticket>({
url: `https://hub.pena.digital/heruvym/subscribe?Authorization=${token}`,
onNewData: (data) => {
updateTickets(data.filter((d) => Boolean(d.id)))
setTicketCount(data.length)
},
marker: "ticket",
})
useTicketsFetcher({
url: "https://hub.pena.digital/heruvym/getTickets",
ticketsPerPage,
ticketApiPage,
onSuccess: (result) => {
if (result.data) updateTickets(result.data);
setTicketCount(result.count);
},
onError: (error: Error) => {
const message = getMessageFromFetchError(error);
if (message) enqueueSnackbar(message);
},
onFetchStateChange: setTicketsFetchState,
});
useTicketsFetcher({
url: "https://hub.pena.digital/heruvym/getTickets",
ticketsPerPage,
ticketApiPage,
onSuccess: (result) => {
if (result.data) updateTickets(result.data)
setTicketCount(result.count)
},
onError: (error: Error) => {
const message = getMessageFromFetchError(error)
if (message) enqueueSnackbar(message)
},
onFetchStateChange: setTicketsFetchState,
})
useAllTariffsFetcher({
onSuccess: updateTariffs,
onError: (error) => {
const errorMessage = getMessageFromFetchError(error);
if (errorMessage) enqueueSnackbar(errorMessage);
},
});
useAllTariffsFetcher({
onSuccess: updateTariffs,
onError: (error) => {
const errorMessage = getMessageFromFetchError(error)
if (errorMessage) enqueueSnackbar(errorMessage)
},
})
useCustomTariffs({
onNewUser: setCustomTariffs,
onError: (error) => {
if (error) enqueueSnackbar(error);
},
});
useCustomTariffs({
onNewUser: setCustomTariffs,
onError: (error) => {
if (error) enqueueSnackbar(error)
},
})
useDiscounts({
onNewDiscounts: setDiscounts,
onError: (error) => {
const message = getMessageFromFetchError(error);
if (message) enqueueSnackbar(message);
},
});
useDiscounts({
onNewDiscounts: setDiscounts,
onError: (error) => {
const message = getMessageFromFetchError(error)
if (message) enqueueSnackbar(message)
},
})
usePrivilegeFetcher({
onSuccess: setPrivileges,
onError: (error) => {
console.log("usePrivilegeFetcher error :>> ", error);
},
});
usePrivilegeFetcher({
onSuccess: setPrivileges,
onError: (error) => {
console.log("usePrivilegeFetcher error :>> ", error)
},
})
return (
<Navbar>
<Outlet />
</Navbar>
);
return (
<Navbar>
<Outlet />
</Navbar>
)
}

@ -1,5 +1,5 @@
import { Breakpoint, Container, SxProps, Theme, useMediaQuery, useTheme } from "@mui/material";
import React, { ElementType } from "react";
import { Breakpoint, Container, SxProps, Theme, useMediaQuery, useTheme } from "@mui/material"
import React, { ElementType } from "react"
interface Props {
component?: ElementType;
@ -11,23 +11,23 @@ interface Props {
}
export default function SectionWrapper({ component, outerContainerSx: sx, sx: innerSx, children, maxWidth }: Props) {
const theme = useTheme();
const matchMd = useMediaQuery(theme.breakpoints.up("md"));
const theme = useTheme()
const matchMd = useMediaQuery(theme.breakpoints.up("md"))
const isMobile = useMediaQuery(theme.breakpoints.down(380));
const isMobile = useMediaQuery(theme.breakpoints.down(380))
return (
<Container component={component || "div"} maxWidth={false} disableGutters sx={sx}>
<Container
disableGutters
maxWidth={maxWidth}
sx={{
px: matchMd ? (isMobile ? "0px" : "20px") : "18px",
...innerSx,
}}
>
{children}
</Container>
</Container>
);
return (
<Container component={component || "div"} maxWidth={false} disableGutters sx={sx}>
<Container
disableGutters
maxWidth={maxWidth}
sx={{
px: matchMd ? (isMobile ? "0px" : "20px") : "18px",
...innerSx,
}}
>
{children}
</Container>
</Container>
)
}

@ -1,12 +1,12 @@
import { useState, useRef } from "react";
import { Select as MuiSelect, MenuItem, Box, Typography, useTheme } from "@mui/material";
import classnames from "classnames";
import { useState, useRef } from "react"
import { Select as MuiSelect, MenuItem, Box, Typography, useTheme } from "@mui/material"
import classnames from "classnames"
import checkIcon from "@root/assets/Icons/check.svg";
import checkIcon from "@root/assets/Icons/check.svg"
import "./select.css";
import "./select.css"
import type { SelectChangeEvent } from "@mui/material";
import type { SelectChangeEvent } from "@mui/material"
type SelectProps = {
items: string[];
@ -15,74 +15,74 @@ type SelectProps = {
};
export const Select = ({ items, selectedItem, setSelectedItem }: SelectProps) => {
const [opened, setOpened] = useState<boolean>(false);
const [currentValue, setCurrentValue] = useState<string>(items[selectedItem]);
const ref = useRef<HTMLDivElement | null>(null);
const theme = useTheme();
const [opened, setOpened] = useState<boolean>(false)
const [currentValue, setCurrentValue] = useState<string>(items[selectedItem])
const ref = useRef<HTMLDivElement | null>(null)
const theme = useTheme()
const selectItem = (event: SelectChangeEvent<HTMLDivElement>) => {
setCurrentValue(items[Number(event.target.value)]);
setSelectedItem(Number(event.target.value));
};
const selectItem = (event: SelectChangeEvent<HTMLDivElement>) => {
setCurrentValue(items[Number(event.target.value)])
setSelectedItem(Number(event.target.value))
}
return (
<Box>
<Box
sx={{
zIndex: 1,
position: "relative",
width: "100%",
height: "56px",
padding: "16px 50px 16px 14px",
color: theme.palette.purple.main,
border: "2px solid #ffffff",
borderRadius: "30px",
background: "#EFF0F5",
boxShadow: "0px 5px 40px #d2d0e194, 0px 2.76726px 8.55082px rgba(210, 208, 225, 0.4)",
}}
onClick={() => ref.current?.click()}
>
<Typography
sx={{
fontWeight: "bold",
fontSize: "16px",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
overflow: "hidden",
}}
>
{currentValue}
</Typography>
<img
src={checkIcon}
alt="check"
style={{
position: "absolute",
top: "50%",
right: "10px",
transform: "translateY(-50%)",
height: "36px",
width: "36px",
}}
className={classnames("select-icon", { opened })}
/>
</Box>
<MuiSelect
ref={ref}
className="select"
value=""
open={opened}
MenuProps={{ disablePortal: true }}
sx={{ width: "100%" }}
onChange={selectItem}
onClick={() => setOpened((isOpened) => !isOpened)}
>
{items.map((item, index) => (
<MenuItem key={item + index} value={index} sx={{ padding: "12px" }}>
{item}
</MenuItem>
))}
</MuiSelect>
</Box>
);
};
return (
<Box>
<Box
sx={{
zIndex: 1,
position: "relative",
width: "100%",
height: "56px",
padding: "16px 50px 16px 14px",
color: theme.palette.purple.main,
border: "2px solid #ffffff",
borderRadius: "30px",
background: "#EFF0F5",
boxShadow: "0px 5px 40px #d2d0e194, 0px 2.76726px 8.55082px rgba(210, 208, 225, 0.4)",
}}
onClick={() => ref.current?.click()}
>
<Typography
sx={{
fontWeight: "bold",
fontSize: "16px",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
overflow: "hidden",
}}
>
{currentValue}
</Typography>
<img
src={checkIcon}
alt="check"
style={{
position: "absolute",
top: "50%",
right: "10px",
transform: "translateY(-50%)",
height: "36px",
width: "36px",
}}
className={classnames("select-icon", { opened })}
/>
</Box>
<MuiSelect
ref={ref}
className="select"
value=""
open={opened}
MenuProps={{ disablePortal: true }}
sx={{ width: "100%" }}
onChange={selectItem}
onClick={() => setOpened((isOpened) => !isOpened)}
>
{items.map((item, index) => (
<MenuItem key={item + index} value={index} sx={{ padding: "12px" }}>
{item}
</MenuItem>
))}
</MuiSelect>
</Box>
)
}

@ -1,5 +1,5 @@
import { Tabs as MuiTabs } from "@mui/material";
import { CustomTab } from "@root/components/CustomTab";
import { Tabs as MuiTabs } from "@mui/material"
import { CustomTab } from "@root/components/CustomTab"
type TabsProps = {
items: string[];
@ -8,15 +8,15 @@ type TabsProps = {
};
export const Tabs = ({ items, selectedItem, setSelectedItem }: TabsProps) => (
<MuiTabs
TabIndicatorProps={{ sx: { display: "none" } }}
value={selectedItem}
onChange={(event, newValue: number) => setSelectedItem(newValue)}
variant="scrollable"
scrollButtons={false}
>
{items.map((item, index) => (
<CustomTab key={item + index} value={index} label={item} />
))}
</MuiTabs>
);
<MuiTabs
TabIndicatorProps={{ sx: { display: "none" } }}
value={selectedItem}
onChange={(event, newValue: number) => setSelectedItem(newValue)}
variant="scrollable"
scrollButtons={false}
>
{items.map((item, index) => (
<CustomTab key={item + index} value={index} label={item} />
))}
</MuiTabs>
)

@ -1,20 +1,20 @@
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { enqueueSnackbar } from "notistack";
import { useState } from "react"
import { useNavigate } from "react-router-dom"
import { enqueueSnackbar } from "notistack"
import {
Alert,
Box,
Button,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
Alert,
Box,
Button,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material"
import { Loader } from "./Loader";
import { Loader } from "./Loader"
import { currencyFormatter } from "@root/utils/currencyFormatter";
import { payCart } from "@root/api/cart";
import { setUserAccount } from "@root/stores/user";
import { currencyFormatter } from "@root/utils/currencyFormatter"
import { payCart } from "@root/api/cart"
import { setUserAccount } from "@root/stores/user"
interface Props {
priceBeforeDiscounts: number;
@ -22,129 +22,129 @@ interface Props {
}
export default function TotalPrice({
priceAfterDiscounts,
priceBeforeDiscounts,
priceAfterDiscounts,
priceBeforeDiscounts,
}: Props) {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const isMobile = useMediaQuery(theme.breakpoints.down(550));
const [notEnoughMoneyAmount, setNotEnoughMoneyAmount] = useState<number>(0);
const [loading, setLoading] = useState<boolean>(false);
const navigate = useNavigate();
const theme = useTheme()
const upMd = useMediaQuery(theme.breakpoints.up("md"))
const isMobile = useMediaQuery(theme.breakpoints.down(550))
const [notEnoughMoneyAmount, setNotEnoughMoneyAmount] = useState<number>(0)
const [loading, setLoading] = useState<boolean>(false)
const navigate = useNavigate()
async function handlePayClick() {
setLoading(true);
async function handlePayClick() {
setLoading(true)
const [payCartResponse, payCartError] = await payCart();
const [payCartResponse, payCartError] = await payCart()
if (payCartError) {
if (payCartError.includes("insufficient funds: ")) {
const notEnoughMoneyAmount = parseInt(
payCartError.replace(/^.*insufficient\sfunds:\s(?=\d+$)/, "")
);
if (payCartError) {
if (payCartError.includes("insufficient funds: ")) {
const notEnoughMoneyAmount = parseInt(
payCartError.replace(/^.*insufficient\sfunds:\s(?=\d+$)/, "")
)
setNotEnoughMoneyAmount(notEnoughMoneyAmount);
}
setNotEnoughMoneyAmount(notEnoughMoneyAmount)
}
setLoading(false);
setLoading(false)
return enqueueSnackbar(payCartError);
}
return enqueueSnackbar(payCartError)
}
if (payCartResponse) {
setUserAccount(payCartResponse);
}
if (payCartResponse) {
setUserAccount(payCartResponse)
}
setLoading(false);
}
setLoading(false)
}
function handleReplenishWallet() {
navigate("/payment", { state: { notEnoughMoneyAmount } });
}
function handleReplenishWallet() {
navigate("/payment", { state: { notEnoughMoneyAmount } })
}
return (
<Box
sx={{
display: "flex",
flexDirection: upMd ? "row" : "column",
mt: upMd ? "80px" : "70px",
pt: upMd ? "30px" : undefined,
borderTop: upMd ? `1px solid ${theme.palette.gray.dark}` : undefined,
}}
>
<Box
sx={{
width: upMd ? "68.5%" : undefined,
pr: upMd ? "15%" : undefined,
display: "flex",
flexWrap: "wrap",
flexDirection: "column",
}}
>
<Typography variant="h4" mb={upMd ? "18px" : "30px"}>
return (
<Box
sx={{
display: "flex",
flexDirection: upMd ? "row" : "column",
mt: upMd ? "80px" : "70px",
pt: upMd ? "30px" : undefined,
borderTop: upMd ? `1px solid ${theme.palette.gray.dark}` : undefined,
}}
>
<Box
sx={{
width: upMd ? "68.5%" : undefined,
pr: upMd ? "15%" : undefined,
display: "flex",
flexWrap: "wrap",
flexDirection: "column",
}}
>
<Typography variant="h4" mb={upMd ? "18px" : "30px"}>
Итоговая цена
</Typography>
<Typography color={theme.palette.gray.main}>
</Typography>
<Typography color={theme.palette.gray.main}>
Текст-заполнитель это текст, который имеет Текст-заполнитель это
текст, который имеет Текст-заполнитель это текст, который имеет
Текст-заполнитель это текст, который имеет Текст-заполнитель
</Typography>
</Box>
<Box
sx={{
color: theme.palette.gray.dark,
width: upMd ? "31.5%" : undefined,
pl: upMd ? "33px" : undefined,
}}
>
<Box
sx={{
display: "flex",
flexDirection: upMd ? "column" : "row",
alignItems: upMd ? "start" : "center",
mt: upMd ? "10px" : "30px",
mb: "15px",
gap: "15px",
}}
>
<Typography variant="oldPrice" sx={{ order: upMd ? 1 : 2 }}>
{currencyFormatter.format(priceBeforeDiscounts / 100)}
</Typography>
<Typography
variant="price"
sx={{
fontWeight: 500,
fontSize: "26px",
lineHeight: "31px",
order: upMd ? 2 : 1,
}}
>
{currencyFormatter.format(priceAfterDiscounts / 100)}
</Typography>
</Box>
{notEnoughMoneyAmount > 0 && (
<Alert severity="error" variant="filled">
</Typography>
</Box>
<Box
sx={{
color: theme.palette.gray.dark,
width: upMd ? "31.5%" : undefined,
pl: upMd ? "33px" : undefined,
}}
>
<Box
sx={{
display: "flex",
flexDirection: upMd ? "column" : "row",
alignItems: upMd ? "start" : "center",
mt: upMd ? "10px" : "30px",
mb: "15px",
gap: "15px",
}}
>
<Typography variant="oldPrice" sx={{ order: upMd ? 1 : 2 }}>
{currencyFormatter.format(priceBeforeDiscounts / 100)}
</Typography>
<Typography
variant="price"
sx={{
fontWeight: 500,
fontSize: "26px",
lineHeight: "31px",
order: upMd ? 2 : 1,
}}
>
{currencyFormatter.format(priceAfterDiscounts / 100)}
</Typography>
</Box>
{notEnoughMoneyAmount > 0 && (
<Alert severity="error" variant="filled">
Нехватает {currencyFormatter.format(notEnoughMoneyAmount / 100)}
</Alert>
)}
<Button
variant="pena-contained-dark"
onClick={() =>
notEnoughMoneyAmount === 0
? !loading && handlePayClick()
: handleReplenishWallet()
}
sx={{ mt: "10px" }}
>
{loading ? (
<Loader size={24} />
) : notEnoughMoneyAmount === 0 ? (
"Оплатить"
) : (
"Пополнить"
)}
</Button>
</Box>
</Box>
);
</Alert>
)}
<Button
variant="pena-contained-dark"
onClick={() =>
notEnoughMoneyAmount === 0
? !loading && handlePayClick()
: handleReplenishWallet()
}
sx={{ mt: "10px" }}
>
{loading ? (
<Loader size={24} />
) : notEnoughMoneyAmount === 0 ? (
"Оплатить"
) : (
"Пополнить"
)}
</Button>
</Box>
</Box>
)
}

@ -1,5 +1,5 @@
import { Button, ButtonProps, SxProps, Theme, useMediaQuery, useTheme } from "@mui/material";
import { MouseEventHandler, ReactNode } from "react";
import { Button, ButtonProps, SxProps, Theme, useMediaQuery, useTheme } from "@mui/material"
import { MouseEventHandler, ReactNode } from "react"
interface Props {
@ -11,35 +11,35 @@ interface Props {
}
export default function UnderlinedButtonWithIcon({ ButtonProps, icon, children, sx, onClick }: Props) {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const theme = useTheme()
const upMd = useMediaQuery(theme.breakpoints.up("md"))
return (
<Button
variant="text"
startIcon={icon}
disableTouchRipple
sx={{
p: 0,
fontWeight: 400,
fontSize: upMd ? "18px" : "16px",
lineHeight: "21px",
textDecorationLine: "underline",
color: theme.palette.purple.main,
textAlign: "start",
textUnderlineOffset: "2px",
"& .MuiButton-startIcon": {
alignSelf: "start",
},
"&:hover": {
backgroundColor: "rgb(0 0 0 / 0)",
},
...sx,
}}
onClick={onClick}
{...ButtonProps}
>
{children}
</Button>
);
return (
<Button
variant="text"
startIcon={icon}
disableTouchRipple
sx={{
p: 0,
fontWeight: 400,
fontSize: upMd ? "18px" : "16px",
lineHeight: "21px",
textDecorationLine: "underline",
color: theme.palette.purple.main,
textAlign: "start",
textUnderlineOffset: "2px",
"& .MuiButton-startIcon": {
alignSelf: "start",
},
"&:hover": {
backgroundColor: "rgb(0 0 0 / 0)",
},
...sx,
}}
onClick={onClick}
{...ButtonProps}
>
{children}
</Button>
)
}

@ -1,4 +1,4 @@
import { Box } from "@mui/material";
import { Box } from "@mui/material"
interface Props {
color: string;
@ -6,63 +6,63 @@ interface Props {
}
export default function CalendarIcon({ color, bgcolor }: Props) {
return (
<Box
sx={{
bgcolor,
height: "36px",
width: "36px",
minWidth: "36px",
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "6px",
}}
>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path
d="M19.502 3.75455H4.50195C4.08774 3.75455 3.75195 4.09033 3.75195 4.50455V19.5045C3.75195 19.9188 4.08774 20.2545 4.50195 20.2545H19.502C19.9162 20.2545 20.252 19.9188 20.252 19.5045V4.50455C20.252 4.09033 19.9162 3.75455 19.502 3.75455Z"
stroke={color}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M16.502 2.25455V5.25455"
stroke={color}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M7.50195 2.25455V5.25455"
stroke={color}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M3.75195 8.25455H20.252"
stroke={color}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M8.62695 12.0045H11.252L9.75195 13.8795C9.99881 13.8791 10.242 13.9396 10.4598 14.0557C10.6777 14.1718 10.8636 14.3398 11.0009 14.545C11.1383 14.7501 11.2229 14.9859 11.2472 15.2316C11.2716 15.4773 11.2349 15.7251 11.1405 15.9532C11.0461 16.1813 10.8968 16.3826 10.706 16.5392C10.5151 16.6958 10.2886 16.8028 10.0465 16.8509C9.80431 16.8989 9.55405 16.8864 9.31788 16.8146C9.08171 16.7427 8.86692 16.6137 8.69258 16.4389"
stroke={color}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M13.502 13.1295L15.002 12.0045V16.8795"
stroke={color}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</Box>
);
return (
<Box
sx={{
bgcolor,
height: "36px",
width: "36px",
minWidth: "36px",
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "6px",
}}
>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path
d="M19.502 3.75455H4.50195C4.08774 3.75455 3.75195 4.09033 3.75195 4.50455V19.5045C3.75195 19.9188 4.08774 20.2545 4.50195 20.2545H19.502C19.9162 20.2545 20.252 19.9188 20.252 19.5045V4.50455C20.252 4.09033 19.9162 3.75455 19.502 3.75455Z"
stroke={color}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M16.502 2.25455V5.25455"
stroke={color}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M7.50195 2.25455V5.25455"
stroke={color}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M3.75195 8.25455H20.252"
stroke={color}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M8.62695 12.0045H11.252L9.75195 13.8795C9.99881 13.8791 10.242 13.9396 10.4598 14.0557C10.6777 14.1718 10.8636 14.3398 11.0009 14.545C11.1383 14.7501 11.2229 14.9859 11.2472 15.2316C11.2716 15.4773 11.2349 15.7251 11.1405 15.9532C11.0461 16.1813 10.8968 16.3826 10.706 16.5392C10.5151 16.6958 10.2886 16.8028 10.0465 16.8509C9.80431 16.8989 9.55405 16.8864 9.31788 16.8146C9.08171 16.7427 8.86692 16.6137 8.69258 16.4389"
stroke={color}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M13.502 13.1295L15.002 12.0045V16.8795"
stroke={color}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</Box>
)
}

@ -1,23 +1,23 @@
import { Box } from "@mui/material";
import { Box } from "@mui/material"
export default function CloseIcon() {
return (
<Box
sx={{
width: "30px",
height: "30px",
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
"&:hover path": {
stroke: "#7E2AEA",
},
}}
>
<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="black" />
</svg>
</Box>
);
return (
<Box
sx={{
width: "30px",
height: "30px",
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
"&:hover path": {
stroke: "#7E2AEA",
},
}}
>
<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="black" />
</svg>
</Box>
)
}

@ -1,21 +1,21 @@
import { Box } from "@mui/material";
import { Box } from "@mui/material"
export default function CloseSmallIcon() {
return (
<Box sx={{
width: "24px",
height: "24px",
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
}}>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 6L6 18" stroke="#A9AAB1" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M6 6L18 18" stroke="#A9AAB1" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</Box>
);
return (
<Box sx={{
width: "24px",
height: "24px",
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
}}>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 6L6 18" stroke="#A9AAB1" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M6 6L18 18" stroke="#A9AAB1" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</Box>
)
}

@ -1,4 +1,4 @@
import { Box } from "@mui/material";
import { Box } from "@mui/material"
interface Props {
color: string;
@ -6,55 +6,55 @@ interface Props {
}
export default function CustomIcon({ color, bgcolor }: Props) {
return (
<Box
sx={{
bgcolor,
height: "36px",
width: "36px",
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "6px",
}}
>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path
d="M7.12695 10.5045C8.99091 10.5045 10.502 8.99351 10.502 7.12955C10.502 5.26559 8.99091 3.75455 7.12695 3.75455C5.26299 3.75455 3.75195 5.26559 3.75195 7.12955C3.75195 8.99351 5.26299 10.5045 7.12695 10.5045Z"
stroke={color}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M16.877 10.5045C18.7409 10.5045 20.252 8.99351 20.252 7.12955C20.252 5.26559 18.7409 3.75455 16.877 3.75455C15.013 3.75455 13.502 5.26559 13.502 7.12955C13.502 8.99351 15.013 10.5045 16.877 10.5045Z"
stroke={color}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M7.12695 20.2545C8.99091 20.2545 10.502 18.7435 10.502 16.8795C10.502 15.0156 8.99091 13.5045 7.12695 13.5045C5.26299 13.5045 3.75195 15.0156 3.75195 16.8795C3.75195 18.7435 5.26299 20.2545 7.12695 20.2545Z"
stroke={color}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M16.877 14.2545V19.5045"
stroke={color}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M19.502 16.8795H14.252"
stroke={color}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</Box>
);
return (
<Box
sx={{
bgcolor,
height: "36px",
width: "36px",
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "6px",
}}
>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path
d="M7.12695 10.5045C8.99091 10.5045 10.502 8.99351 10.502 7.12955C10.502 5.26559 8.99091 3.75455 7.12695 3.75455C5.26299 3.75455 3.75195 5.26559 3.75195 7.12955C3.75195 8.99351 5.26299 10.5045 7.12695 10.5045Z"
stroke={color}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M16.877 10.5045C18.7409 10.5045 20.252 8.99351 20.252 7.12955C20.252 5.26559 18.7409 3.75455 16.877 3.75455C15.013 3.75455 13.502 5.26559 13.502 7.12955C13.502 8.99351 15.013 10.5045 16.877 10.5045Z"
stroke={color}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M7.12695 20.2545C8.99091 20.2545 10.502 18.7435 10.502 16.8795C10.502 15.0156 8.99091 13.5045 7.12695 13.5045C5.26299 13.5045 3.75195 15.0156 3.75195 16.8795C3.75195 18.7435 5.26299 20.2545 7.12695 20.2545Z"
stroke={color}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M16.877 14.2545V19.5045"
stroke={color}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M19.502 16.8795H14.252"
stroke={color}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</Box>
)
}

@ -1,4 +1,4 @@
import { useTheme , Box} from "@mui/material";
import { useTheme , Box} from "@mui/material"
interface Props {
@ -6,21 +6,21 @@ interface Props {
}
export default function ExpandIcon({ isExpanded }: Props) {
const theme = useTheme();
const theme = useTheme()
return (
<Box sx={{
width: "33px",
height: "33px",
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
}}>
<svg style={{ transform: isExpanded ? "rotate(180deg)" : undefined }} xmlns="http://www.w3.org/2000/svg" width="32" height="33" viewBox="0 0 32 33" fill="none">
<path stroke={isExpanded ? theme.palette.orange.main : theme.palette.purple.main} d="M16 28.7949C22.6274 28.7949 28 23.4223 28 16.7949C28 10.1675 22.6274 4.79492 16 4.79492C9.37258 4.79492 4 10.1675 4 16.7949C4 23.4223 9.37258 28.7949 16 28.7949Z" strokeWidth="2" strokeMiterlimit="10" />
<path stroke={isExpanded ? theme.palette.orange.main : theme.palette.purple.main} d="M20.5 15.2949L16 20.2949L11.5 15.2949" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</Box>
)
return (
<Box sx={{
width: "33px",
height: "33px",
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
}}>
<svg style={{ transform: isExpanded ? "rotate(180deg)" : undefined }} xmlns="http://www.w3.org/2000/svg" width="32" height="33" viewBox="0 0 32 33" fill="none">
<path stroke={isExpanded ? theme.palette.orange.main : theme.palette.purple.main} d="M16 28.7949C22.6274 28.7949 28 23.4223 28 16.7949C28 10.1675 22.6274 4.79492 16 4.79492C9.37258 4.79492 4 10.1675 4 16.7949C4 23.4223 9.37258 28.7949 16 28.7949Z" strokeWidth="2" strokeMiterlimit="10" />
<path stroke={isExpanded ? theme.palette.orange.main : theme.palette.purple.main} d="M20.5 15.2949L16 20.2949L11.5 15.2949" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</Box>
)
}

@ -1,21 +1,21 @@
import { Box } from "@mui/material";
import { Box } from "@mui/material"
export default function EyeIcon() {
return (
<Box sx={{
width: "24px",
height: "24px",
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
}}>
<svg width="22" height="15" viewBox="0 0 22 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.9502 14C16.3669 14 19.6169 9.66667 20.7002 7.5C19.6169 5.33333 16.3669 1 10.9502 1C5.53353 1 2.28353 5.33333 1.2002 7.5C2.64464 9.66667 5.53353 14 10.9502 14Z" stroke="#7E2AEA" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<circle cx="10.9495" cy="7.50033" r="3.58333" stroke="#7E2AEA" strokeWidth="1.5" />
</svg>
</Box>
);
return (
<Box sx={{
width: "24px",
height: "24px",
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
}}>
<svg width="22" height="15" viewBox="0 0 22 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.9502 14C16.3669 14 19.6169 9.66667 20.7002 7.5C19.6169 5.33333 16.3669 1 10.9502 1C5.53353 1 2.28353 5.33333 1.2002 7.5C2.64464 9.66667 5.53353 14 10.9502 14Z" stroke="#7E2AEA" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<circle cx="10.9495" cy="7.50033" r="3.58333" stroke="#7E2AEA" strokeWidth="1.5" />
</svg>
</Box>
)
}

@ -1,4 +1,4 @@
import { Box } from "@mui/material";
import { Box } from "@mui/material"
interface Props {
color?: string;
@ -8,23 +8,23 @@ interface Props {
}
export default function CustomIcon() {
return (
<Box
sx={{
stroke: "inherit",
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "6px"
}}
return (
<Box
sx={{
stroke: "inherit",
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "6px"
}}
>
<svg width="24" height="24" viewBox="0 0 24 24" stroke="inherit" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="3.2002" y="0.75" width="17.8" height="22.25" rx="4" strokeWidth="1.5"/>
<path d="M7.65039 6.3125H16.5504" strokeWidth="1.5" strokeLinecap="round"/>
<path d="M7.65039 11.875H16.5504" strokeWidth="1.5" strokeLinecap="round"/>
<path d="M7.65039 17.4375H12.1004" strokeWidth="1.5" strokeLinecap="round"/>
</svg>
</Box>
);
>
<svg width="24" height="24" viewBox="0 0 24 24" stroke="inherit" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="3.2002" y="0.75" width="17.8" height="22.25" rx="4" strokeWidth="1.5"/>
<path d="M7.65039 6.3125H16.5504" strokeWidth="1.5" strokeLinecap="round"/>
<path d="M7.65039 11.875H16.5504" strokeWidth="1.5" strokeLinecap="round"/>
<path d="M7.65039 17.4375H12.1004" strokeWidth="1.5" strokeLinecap="round"/>
</svg>
</Box>
)
}

@ -1,14 +1,14 @@
import { useTheme } from "@mui/material";
import { useTheme } from "@mui/material"
export default function LogoutIcon() {
const theme = useTheme();
const theme = useTheme()
return (
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.5601 12.3V15.25C12.5601 16.3546 11.6646 17.25 10.5601 17.25H3.69596C2.59139 17.25 1.69596 16.3546 1.69596 15.25V2.75C1.69596 1.64543 2.59139 0.75 3.69596 0.75H10.5601C11.6646 0.75 12.5601 1.64543 12.5601 2.75V5.7" stroke={theme.palette.gray.main} strokeWidth="1.5" strokeLinecap="round" />
<path d="M15.067 11.475L16.8532 9.71165C17.2498 9.32011 17.2498 8.6799 16.8532 8.28836L15.067 6.52501" stroke={theme.palette.gray.main} strokeWidth="1.5" strokeLinecap="round" />
<path d="M16.7384 9L6.70996 9" stroke={theme.palette.gray.main} strokeWidth="1.5" strokeLinecap="round" />
</svg>
);
return (
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.5601 12.3V15.25C12.5601 16.3546 11.6646 17.25 10.5601 17.25H3.69596C2.59139 17.25 1.69596 16.3546 1.69596 15.25V2.75C1.69596 1.64543 2.59139 0.75 3.69596 0.75H10.5601C11.6646 0.75 12.5601 1.64543 12.5601 2.75V5.7" stroke={theme.palette.gray.main} strokeWidth="1.5" strokeLinecap="round" />
<path d="M15.067 11.475L16.8532 9.71165C17.2498 9.32011 17.2498 8.6799 16.8532 8.28836L15.067 6.52501" stroke={theme.palette.gray.main} strokeWidth="1.5" strokeLinecap="round" />
<path d="M16.7384 9L6.70996 9" stroke={theme.palette.gray.main} strokeWidth="1.5" strokeLinecap="round" />
</svg>
)
}

@ -1,4 +1,4 @@
import { Box } from "@mui/material";
import { Box } from "@mui/material"
interface Props {
@ -7,18 +7,18 @@ interface Props {
export default function PaperClipIcon({ color = "#7E2AEA" }: Props) {
return (
<Box sx={{
width: "24px",
height: "24px",
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
}}>
<svg width="17" height="19" viewBox="0 0 17 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.7541 9.80636C13.714 11.8464 9.42986 16.1306 8.61382 16.9467C7.59378 17.9667 5.14568 19.0548 3.10559 17.0147C1.0655 14.9746 1.47352 12.5265 2.83358 11.1664C4.19364 9.80636 10.9939 3.00608 11.674 2.32605C12.694 1.30601 14.1901 1.44201 15.2101 2.46205C16.2301 3.4821 16.5686 4.91167 15.4141 6.06621C14.0541 7.42626 7.45777 14.1585 6.77775 14.8386C6.09772 15.5186 5.31107 15.276 4.90767 14.8726C4.50426 14.4692 4.26164 13.6825 4.94167 13.0025C5.48569 12.4585 9.79254 8.15163 11.946 5.9982" stroke={color} strokeWidth="1.5" strokeLinecap="round" />
</svg>
</Box>
);
return (
<Box sx={{
width: "24px",
height: "24px",
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
}}>
<svg width="17" height="19" viewBox="0 0 17 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.7541 9.80636C13.714 11.8464 9.42986 16.1306 8.61382 16.9467C7.59378 17.9667 5.14568 19.0548 3.10559 17.0147C1.0655 14.9746 1.47352 12.5265 2.83358 11.1664C4.19364 9.80636 10.9939 3.00608 11.674 2.32605C12.694 1.30601 14.1901 1.44201 15.2101 2.46205C16.2301 3.4821 16.5686 4.91167 15.4141 6.06621C14.0541 7.42626 7.45777 14.1585 6.77775 14.8386C6.09772 15.5186 5.31107 15.276 4.90767 14.8726C4.50426 14.4692 4.26164 13.6825 4.94167 13.0025C5.48569 12.4585 9.79254 8.15163 11.946 5.9982" stroke={color} strokeWidth="1.5" strokeLinecap="round" />
</svg>
</Box>
)
}

@ -1,4 +1,4 @@
import { Box } from "@mui/material";
import { Box } from "@mui/material"
interface Props {
@ -8,25 +8,25 @@ interface Props {
export default function PieChartIcon({ color, bgcolor }: Props) {
return (
<Box
sx={{
bgcolor,
height: "36px",
width: "36px",
minWidth: "36px",
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "6px",
}}
>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M10.0024 10.0045V1.00455" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M17.7932 5.50455L2.21191 14.5045" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M1.15253 11.6545C1.05021 11.1106 0.999987 10.5581 1.00253 10.0045C1.00135 8.14323 1.57789 6.32742 2.6526 4.80771C3.72731 3.288 5.24721 2.13932 7.00253 1.52017V8.27017L1.15253 11.6545Z" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M10.0025 1.00455C11.5795 1.00473 13.1288 1.41928 14.4952 2.2067C15.8616 2.99411 16.9971 4.12674 17.7879 5.49112C18.5788 6.85551 18.9973 8.40375 19.0014 9.98078C19.0056 11.5578 18.5953 13.1082 17.8117 14.4768C17.028 15.8453 15.8985 16.9839 14.5363 17.7785C13.1741 18.5732 11.627 18.9959 10.05 19.0044C8.47303 19.0129 6.92147 18.6069 5.55077 17.827C4.18007 17.0472 3.03836 15.9208 2.23999 14.5608" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</Box>
);
return (
<Box
sx={{
bgcolor,
height: "36px",
width: "36px",
minWidth: "36px",
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "6px",
}}
>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M10.0024 10.0045V1.00455" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M17.7932 5.50455L2.21191 14.5045" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M1.15253 11.6545C1.05021 11.1106 0.999987 10.5581 1.00253 10.0045C1.00135 8.14323 1.57789 6.32742 2.6526 4.80771C3.72731 3.288 5.24721 2.13932 7.00253 1.52017V8.27017L1.15253 11.6545Z" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M10.0025 1.00455C11.5795 1.00473 13.1288 1.41928 14.4952 2.2067C15.8616 2.99411 16.9971 4.12674 17.7879 5.49112C18.5788 6.85551 18.9973 8.40375 19.0014 9.98078C19.0056 11.5578 18.5953 13.1082 17.8117 14.4768C17.028 15.8453 15.8985 16.9839 14.5363 17.7785C13.1741 18.5732 11.627 18.9959 10.05 19.0044C8.47303 19.0129 6.92147 18.6069 5.55077 17.827C4.18007 17.0472 3.03836 15.9208 2.23999 14.5608" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</Box>
)
}

@ -1,4 +1,4 @@
import { CSSProperties } from "react";
import { CSSProperties } from "react"
interface Props {
@ -7,11 +7,11 @@ interface Props {
export default function SendIcon({ style }: Props) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="45" height="45" viewBox="0 0 45 45" fill="none" style={style}>
<circle cx="22.5" cy="22.5" r="22.5" fill="#944FEE" />
<path d="M33.8489 22.1816L15.9232 12.1415C15.7722 12.0581 15.5994 12.0227 15.4277 12.0399C15.2561 12.0571 15.0938 12.1263 14.9624 12.2381C14.831 12.3498 14.7368 12.499 14.6923 12.6657C14.6478 12.8323 14.6551 13.0086 14.7133 13.171L18.0883 22.638C18.1627 22.8218 18.1627 23.0273 18.0883 23.2111L14.7133 32.6781C14.6551 32.8405 14.6478 33.0167 14.6923 33.1834C14.7368 33.3501 14.831 33.4992 14.9624 33.611C15.0938 33.7228 15.2561 33.7919 15.4277 33.8092C15.5994 33.8264 15.7722 33.791 15.9232 33.7076L33.8489 23.6675C33.9816 23.594 34.0922 23.4864 34.1693 23.3558C34.2463 23.2251 34.2869 23.0762 34.2869 22.9245C34.2869 22.7729 34.2463 22.624 34.1693 22.4933C34.0922 22.3627 33.9816 22.255 33.8489 22.1816V22.1816Z" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M18.1943 22.9248H24.9868" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
return (
<svg xmlns="http://www.w3.org/2000/svg" width="45" height="45" viewBox="0 0 45 45" fill="none" style={style}>
<circle cx="22.5" cy="22.5" r="22.5" fill="#944FEE" />
<path d="M33.8489 22.1816L15.9232 12.1415C15.7722 12.0581 15.5994 12.0227 15.4277 12.0399C15.2561 12.0571 15.0938 12.1263 14.9624 12.2381C14.831 12.3498 14.7368 12.499 14.6923 12.6657C14.6478 12.8323 14.6551 13.0086 14.7133 13.171L18.0883 22.638C18.1627 22.8218 18.1627 23.0273 18.0883 23.2111L14.7133 32.6781C14.6551 32.8405 14.6478 33.0167 14.6923 33.1834C14.7368 33.3501 14.831 33.4992 14.9624 33.611C15.0938 33.7228 15.2561 33.7919 15.4277 33.8092C15.5994 33.8264 15.7722 33.791 15.9232 33.7076L33.8489 23.6675C33.9816 23.594 34.0922 23.4864 34.1693 23.3558C34.2463 23.2251 34.2869 23.0762 34.2869 22.9245C34.2869 22.7729 34.2463 22.624 34.1693 22.4933C34.0922 22.3627 33.9816 22.255 33.8489 22.1816V22.1816Z" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M18.1943 22.9248H24.9868" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
)
}

@ -1,22 +1,22 @@
import { Box } from "@mui/material";
import { Box } from "@mui/material"
export default function SendIcon() {
return (
<Box sx={{
width: "24px",
height: "24px",
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
}}>
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.0625 7.80957L12 3.80957L15.9375 7.80957" stroke="#7E2AEA" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M12 14.4762V3.80957" stroke="#7E2AEA" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M20.25 14.4761V19.8094C20.25 20.0115 20.171 20.2053 20.0303 20.3482C19.8897 20.491 19.6989 20.5713 19.5 20.5713H4.5C4.30109 20.5713 4.11032 20.491 3.96967 20.3482C3.82902 20.2053 3.75 20.0115 3.75 19.8094V14.4761" stroke="#7E2AEA" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</Box>
);
return (
<Box sx={{
width: "24px",
height: "24px",
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
}}>
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.0625 7.80957L12 3.80957L15.9375 7.80957" stroke="#7E2AEA" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M12 14.4762V3.80957" stroke="#7E2AEA" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M20.25 14.4761V19.8094C20.25 20.0115 20.171 20.2053 20.0303 20.3482C19.8897 20.491 19.6989 20.5713 19.5 20.5713H4.5C4.30109 20.5713 4.11032 20.491 3.96967 20.3482C3.82902 20.2053 3.75 20.0115 3.75 19.8094V14.4761" stroke="#7E2AEA" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</Box>
)
}

@ -1,4 +1,4 @@
import { Box } from "@mui/material";
import { Box } from "@mui/material"
interface Props {
color: string;
@ -6,32 +6,32 @@ interface Props {
}
export default function SendIcon({ color, bgcolor }: Props) {
return (
<Box
sx={{
bgcolor,
borderRadius: "6px",
height: "36px",
width: "36px",
display: "flex",
justifyContent: "center",
alignItems: "center",
ml: "8px",
}}
>
<svg
width="22"
height="19"
viewBox="0 0 22 19"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19.5714 7.29857V2.29857C19.5714 1.50959 18.9318 0.869996 18.1429 0.869996L2.42857 0.869995C1.63959 0.869995 1 1.50959 1 2.29857L1 16.5843C1 17.3733 1.63959 18.0128 2.42857 18.0128L18.1429 18.0128C18.9318 18.0128 19.5714 17.3733 19.5714 16.5843V11.5843M20.901 6.58428H13.8571C12.2792 6.58428 11 7.86347 11 9.44142C11 11.0194 12.2792 12.2986 13.8571 12.2986H20.901C20.9557 12.2986 21 12.2542 21 12.1996V6.68329C21 6.62861 20.9557 6.58428 20.901 6.58428Z"
stroke={color}
strokeWidth="1.5"
/>
</svg>
</Box>
);
return (
<Box
sx={{
bgcolor,
borderRadius: "6px",
height: "36px",
width: "36px",
display: "flex",
justifyContent: "center",
alignItems: "center",
ml: "8px",
}}
>
<svg
width="22"
height="19"
viewBox="0 0 22 19"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19.5714 7.29857V2.29857C19.5714 1.50959 18.9318 0.869996 18.1429 0.869996L2.42857 0.869995C1.63959 0.869995 1 1.50959 1 2.29857L1 16.5843C1 17.3733 1.63959 18.0128 2.42857 18.0128L18.1429 18.0128C18.9318 18.0128 19.5714 17.3733 19.5714 16.5843V11.5843M20.901 6.58428H13.8571C12.2792 6.58428 11 7.86347 11 9.44142C11 11.0194 12.2792 12.2986 13.8571 12.2986H20.901C20.9557 12.2986 21 12.2542 21 12.1996V6.68329C21 6.62861 20.9557 6.58428 20.901 6.58428Z"
stroke={color}
strokeWidth="1.5"
/>
</svg>
</Box>
)
}

@ -1,18 +1,18 @@
import {
FormControl,
IconButton,
InputLabel,
SxProps,
TextField,
TextFieldProps,
Theme,
useMediaQuery,
useTheme,
} from "@mui/material";
import * as React from "react";
import InputAdornment from "@mui/material/InputAdornment";
import Visibility from "@mui/icons-material/Visibility";
import VisibilityOff from "@mui/icons-material/VisibilityOff";
FormControl,
IconButton,
InputLabel,
SxProps,
TextField,
TextFieldProps,
Theme,
useMediaQuery,
useTheme,
} from "@mui/material"
import * as React from "react"
import InputAdornment from "@mui/material/InputAdornment"
import Visibility from "@mui/icons-material/Visibility"
import VisibilityOff from "@mui/icons-material/VisibilityOff"
interface Props {
id: string;
@ -26,103 +26,103 @@ interface Props {
}
export default function PasswordInput({
id,
label,
bold = false,
gap = "10px",
onChange,
TextfieldProps,
color,
FormInputSx,
id,
label,
bold = false,
gap = "10px",
onChange,
TextfieldProps,
color,
FormInputSx,
}: Props) {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const theme = useTheme()
const upMd = useMediaQuery(theme.breakpoints.up("md"))
const labelFont = upMd
? bold
? theme.typography.p1
: { ...theme.typography.body1, fontWeight: 500 }
: theme.typography.body2;
const labelFont = upMd
? bold
? theme.typography.p1
: { ...theme.typography.body1, fontWeight: 500 }
: theme.typography.body2
const placeholderFont = upMd ? undefined : { fontWeight: 400, fontSize: "16px", lineHeight: "19px" };
const placeholderFont = upMd ? undefined : { fontWeight: 400, fontSize: "16px", lineHeight: "19px" }
const [showPassword, setShowPassword] = React.useState(false);
const [showPassword, setShowPassword] = React.useState(false)
const handleClickShowPassword = () => setShowPassword((show) => !show);
const handleClickShowPassword = () => setShowPassword((show) => !show)
const handleMouseDownPassword = (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
};
const handleMouseDownPassword = (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault()
}
return (
<FormControl
fullWidth
variant="standard"
sx={{
gap,
// mt: "10px",
...FormInputSx,
position: "relative",
}}
>
<InputLabel
shrink
htmlFor={id}
sx={{
position: "inherit",
color: "black",
transform: "none",
...labelFont,
}}
>
{label}
</InputLabel>
<TextField
{...TextfieldProps}
fullWidth
id={id}
sx={{
"& .MuiInputBase-root": {
height: "48px",
borderRadius: "8px",
},
}}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="toggle password visibility"
onClick={handleClickShowPassword}
onMouseDown={handleMouseDownPassword}
edge="end"
sx={{
position: "absolute",
right: "15px",
top: "5px",
}}
>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
),
sx: {
padding: "0px",
border: "1px solid" + theme.palette.gray.main,
backgroundColor: color,
borderRadius: "8px",
height: "48px",
color: "black",
...placeholderFont,
"& .MuiInputBase-input": {
boxSizing: "border-box",
height: "100%",
padding: "14px",
},
},
}}
onChange={onChange}
type={showPassword ? "text" : "password"}
/>
</FormControl>
);
return (
<FormControl
fullWidth
variant="standard"
sx={{
gap,
// mt: "10px",
...FormInputSx,
position: "relative",
}}
>
<InputLabel
shrink
htmlFor={id}
sx={{
position: "inherit",
color: "black",
transform: "none",
...labelFont,
}}
>
{label}
</InputLabel>
<TextField
{...TextfieldProps}
fullWidth
id={id}
sx={{
"& .MuiInputBase-root": {
height: "48px",
borderRadius: "8px",
},
}}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="toggle password visibility"
onClick={handleClickShowPassword}
onMouseDown={handleMouseDownPassword}
edge="end"
sx={{
position: "absolute",
right: "15px",
top: "5px",
}}
>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
),
sx: {
padding: "0px",
border: "1px solid" + theme.palette.gray.main,
backgroundColor: color,
borderRadius: "8px",
height: "48px",
color: "black",
...placeholderFont,
"& .MuiInputBase-input": {
boxSizing: "border-box",
height: "100%",
padding: "14px",
},
},
}}
onChange={onChange}
type={showPassword ? "text" : "password"}
/>
</FormControl>
)
}

@ -1,56 +1,56 @@
import { Box, Button, Typography, useMediaQuery, useTheme } from "@mui/material";
import card1Image from "@root/assets/landing/card1.png";
import { Box, Button, Typography, useMediaQuery, useTheme } from "@mui/material"
import card1Image from "@root/assets/landing/card1.png"
export default function TemplCardPhoneLight() {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const isMobile = useMediaQuery(theme.breakpoints.down(600));
return (
<Box
sx={{
mt: upMd ? "93px" : "55px",
display: "flex",
flexWrap: "wrap",
justifyContent: "space-evenly",
columnGap: "40px",
rowGap: "50px",
backgroundColor: "inherit",
}}
>
<Box
sx={{
display: "flex",
flexDirection: "column",
flexGrow: 1,
alignItems: "start",
p: "20px",
maxWidth: "360px",
backgroundColor: " #E6E6EB",
borderRadius: "12px",
boxShadow: "0 10px 0 -5px #BABBC8",
color: "black",
height: "520px",
justifyContent: "space-between",
}}
>
<img
src={card1Image}
alt=""
style={{
objectFit: "contain",
width: "100%",
display: "block",
pointerEvents: "none",
}}
/>
<Typography variant="h5">Шаблонизатор</Typography>
<Typography mt="10px" mb="20px">
const theme = useTheme()
const upMd = useMediaQuery(theme.breakpoints.up("md"))
const isMobile = useMediaQuery(theme.breakpoints.down(600))
return (
<Box
sx={{
mt: upMd ? "93px" : "55px",
display: "flex",
flexWrap: "wrap",
justifyContent: "space-evenly",
columnGap: "40px",
rowGap: "50px",
backgroundColor: "inherit",
}}
>
<Box
sx={{
display: "flex",
flexDirection: "column",
flexGrow: 1,
alignItems: "start",
p: "20px",
maxWidth: "360px",
backgroundColor: " #E6E6EB",
borderRadius: "12px",
boxShadow: "0 10px 0 -5px #BABBC8",
color: "black",
height: "520px",
justifyContent: "space-between",
}}
>
<img
src={card1Image}
alt=""
style={{
objectFit: "contain",
width: "100%",
display: "block",
pointerEvents: "none",
}}
/>
<Typography variant="h5">Шаблонизатор</Typography>
<Typography mt="10px" mb="20px">
Текст-заполнитель {isMobile && <br />} это текст, который имеет некоторые характеристики реального
письменного текста, но является
</Typography>
</Typography>
<Button variant="pena-contained-light">Подробнее</Button>
</Box>
</Box>
);
<Button variant="pena-contained-light">Подробнее</Button>
</Box>
</Box>
)
}

@ -1,30 +1,30 @@
import { Box, useMediaQuery, useTheme } from "@mui/material";
import CardWithLink from "@components/CardWithLink";
import card1Image from "@root/assets/landing/card1.png";
import { Box, useMediaQuery, useTheme } from "@mui/material"
import CardWithLink from "@components/CardWithLink"
import card1Image from "@root/assets/landing/card1.png"
export default function () {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const theme = useTheme()
const upMd = useMediaQuery(theme.breakpoints.up("md"))
return (
<Box
sx={{
mt: upMd ? "93px" : "55px",
display: "flex",
flexWrap: "wrap",
justifyContent: "space-evenly",
columnGap: "40px",
rowGap: "50px",
backgroundColor: '"#E6E6EB',
}}
>
<CardWithLink
headerText="Шаблонизатор"
text="Текст- это текст, который имеет некоторые характеристики реального письменного текс"
linkHref="#"
image={card1Image}
isHighlighted={!upMd}
/>
</Box>
);
return (
<Box
sx={{
mt: upMd ? "93px" : "55px",
display: "flex",
flexWrap: "wrap",
justifyContent: "space-evenly",
columnGap: "40px",
rowGap: "50px",
backgroundColor: "\"#E6E6EB",
}}
>
<CardWithLink
headerText="Шаблонизатор"
text="Текст- это текст, который имеет некоторые характеристики реального письменного текс"
linkHref="#"
image={card1Image}
isHighlighted={!upMd}
/>
</Box>
)
}

@ -1,7 +1,7 @@
import { Box, Typography, SxProps, Theme, Button, useTheme, useMediaQuery } from "@mui/material";
import cardImageBig from "@root/assets/landing/card1big.png";
import { PenaLink } from "@frontend/kitui";
import { Link as RouterLink } from "react-router-dom";
import { Box, Typography, SxProps, Theme, Button, useTheme, useMediaQuery } from "@mui/material"
import cardImageBig from "@root/assets/landing/card1big.png"
import { PenaLink } from "@frontend/kitui"
import { Link as RouterLink } from "react-router-dom"
interface Props {
light?: boolean;
@ -11,46 +11,46 @@ interface Props {
}
export default function WideTemplCard({ light = true, sx, name="Шаблонизатор", desc="тект заполнитель это текст который имеет" }: Props) {
const theme = useTheme();
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const theme = useTheme()
const isTablet = useMediaQuery(theme.breakpoints.down(1000))
return (
<Box
sx={{
position: "relative",
display: "flex",
justifyContent: "space-between",
py: "40px",
px: "20px",
backgroundColor: light ? "#E6E6EB" : "#434657",
borderRadius: "12px",
...sx,
}}
>
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "start",
}}
>
<Typography variant="h5">{name}</Typography>
<Typography sx={{ marginTop: isTablet ? "10px" : "20px" }} maxWidth="552px">
{desc}
</Typography>
</Box>
<img
src={cardImageBig}
alt=""
style={{
display: "block",
objectFit: "contain",
pointerEvents: "none",
marginBottom: "-40px",
marginTop: "-110px",
maxWidth: "390px",
}}
/>
</Box>
);
return (
<Box
sx={{
position: "relative",
display: "flex",
justifyContent: "space-between",
py: "40px",
px: "20px",
backgroundColor: light ? "#E6E6EB" : "#434657",
borderRadius: "12px",
...sx,
}}
>
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "start",
}}
>
<Typography variant="h5">{name}</Typography>
<Typography sx={{ marginTop: isTablet ? "10px" : "20px" }} maxWidth="552px">
{desc}
</Typography>
</Box>
<img
src={cardImageBig}
alt=""
style={{
display: "block",
objectFit: "contain",
pointerEvents: "none",
marginBottom: "-40px",
marginTop: "-110px",
maxWidth: "390px",
}}
/>
</Box>
)
}

@ -1,11 +1,11 @@
import type { UserDocumentTypes } from "@root/model/user";
import type { UserDocumentTypes } from "@root/model/user"
export const DOCUMENT_TYPE_MAP: Record<
"inn" | "rule" | "egrule" | "certificate",
UserDocumentTypes
> = {
inn: "ИНН",
rule: "Устав",
certificate: "Свидетельство о регистрации НКО",
egrule: "ИНН",
};
inn: "ИНН",
rule: "Устав",
certificate: "Свидетельство о регистрации НКО",
egrule: "ИНН",
}

@ -1,119 +1,119 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter, Navigate, Route, Routes, useLocation, useNavigate } from "react-router-dom";
import { CssBaseline, ThemeProvider } from "@mui/material";
import Faq from "./pages/Faq/Faq";
import Wallet from "./pages/Wallet";
import Payment from "./pages/Payment/Payment";
import Support from "./pages/Support/Support";
import AccountSettings from "./pages/AccountSettings/AccountSettings";
import Landing from "./pages/Landing/Landing";
import Tariffs from "./pages/Tariffs/Tariffs";
import SigninDialog from "./pages/auth/Signin";
import SignupDialog from "./pages/auth/Signup";
import History from "./pages/History";
import Cart from "./pages/Cart/Cart";
import TariffPage from "./pages/Tariffs/TariffsPage";
import SavedTariffs from "./pages/SavedTariffs";
import PrivateRoute from "@root/utils/routes/ProtectedRoute";
import reportWebVitals from "./reportWebVitals";
import { SnackbarProvider, enqueueSnackbar } from "notistack";
import "./index.css";
import ProtectedLayout from "./components/ProtectedLayout";
import { clearUserData, setUser, setUserAccount, useUserStore } from "./stores/user";
import TariffConstructor from "./pages/TariffConstructor/TariffConstructor";
import { clearAuthToken, getMessageFromFetchError, useUserAccountFetcher, useUserFetcher } from "@frontend/kitui";
import { pdfjs } from "react-pdf";
import { theme } from "./utils/theme";
import React from "react"
import ReactDOM from "react-dom/client"
import { BrowserRouter, Navigate, Route, Routes, useLocation, useNavigate } from "react-router-dom"
import { CssBaseline, ThemeProvider } from "@mui/material"
import Faq from "./pages/Faq/Faq"
import Wallet from "./pages/Wallet"
import Payment from "./pages/Payment/Payment"
import Support from "./pages/Support/Support"
import AccountSettings from "./pages/AccountSettings/AccountSettings"
import Landing from "./pages/Landing/Landing"
import Tariffs from "./pages/Tariffs/Tariffs"
import SigninDialog from "./pages/auth/Signin"
import SignupDialog from "./pages/auth/Signup"
import History from "./pages/History"
import Cart from "./pages/Cart/Cart"
import TariffPage from "./pages/Tariffs/TariffsPage"
import SavedTariffs from "./pages/SavedTariffs"
import PrivateRoute from "@root/utils/routes/ProtectedRoute"
import reportWebVitals from "./reportWebVitals"
import { SnackbarProvider, enqueueSnackbar } from "notistack"
import "./index.css"
import ProtectedLayout from "./components/ProtectedLayout"
import { clearUserData, setUser, setUserAccount, useUserStore } from "./stores/user"
import TariffConstructor from "./pages/TariffConstructor/TariffConstructor"
import { clearAuthToken, getMessageFromFetchError, useUserAccountFetcher, useUserFetcher } from "@frontend/kitui"
import { pdfjs } from "react-pdf"
import { theme } from "./utils/theme"
pdfjs.GlobalWorkerOptions.workerSrc = new URL("pdfjs-dist/build/pdf.worker.min.js", import.meta.url).toString();
pdfjs.GlobalWorkerOptions.workerSrc = new URL("pdfjs-dist/build/pdf.worker.min.js", import.meta.url).toString()
const App = () => {
console.log("render app")
const location = useLocation();
const userId = useUserStore((state) => state.userId);
const navigate = useNavigate();
console.log("render app")
const location = useLocation()
const userId = useUserStore((state) => state.userId)
const navigate = useNavigate()
useUserFetcher({
url: `https://hub.pena.digital/user/${userId}`,
userId,
onNewUser: setUser,
onError: (error) => {
const errorMessage = getMessageFromFetchError(error);
if (errorMessage) {
enqueueSnackbar(errorMessage);
clearUserData();
clearAuthToken();
}
},
});
useUserFetcher({
url: `https://hub.pena.digital/user/${userId}`,
userId,
onNewUser: setUser,
onError: (error) => {
const errorMessage = getMessageFromFetchError(error)
if (errorMessage) {
enqueueSnackbar(errorMessage)
clearUserData()
clearAuthToken()
}
},
})
useUserAccountFetcher({
url: "https://hub.pena.digital/customer/account",
userId,
onNewUserAccount: setUserAccount,
onError: (error) => {
const errorMessage = getMessageFromFetchError(error);
if (errorMessage) {
enqueueSnackbar(errorMessage);
clearUserData();
clearAuthToken();
navigate("/signin");
}
},
});
useUserAccountFetcher({
url: "https://hub.pena.digital/customer/account",
userId,
onNewUserAccount: setUserAccount,
onError: (error) => {
const errorMessage = getMessageFromFetchError(error)
if (errorMessage) {
enqueueSnackbar(errorMessage)
clearUserData()
clearAuthToken()
navigate("/signin")
}
},
})
if (location.state?.redirectTo)
return <Navigate to={location.state.redirectTo} replace state={{ backgroundLocation: location }} />;
if (location.state?.redirectTo)
return <Navigate to={location.state.redirectTo} replace state={{ backgroundLocation: location }} />
return (
<>
{location.state?.backgroundLocation && (
<Routes>
<Route path="/signin" element={<SigninDialog />} />
<Route path="/signup" element={<SignupDialog />} />
</Routes>
)}
<Routes location={location.state?.backgroundLocation || location}>
<Route path="/" element={<Landing />} />
<Route path="/signin" element={<Navigate to="/" replace state={{ redirectTo: "/signin" }} />} />
<Route path="/signup" element={<Navigate to="/" replace state={{ redirectTo: "/signup" }} />} />
<Route element={<PrivateRoute />}>
<Route element={<ProtectedLayout />}>
<Route path="/tariffs" element={<Tariffs />} />
<Route path="/tariffs/time" element={<TariffPage />} />
<Route path="/tariffs/volume" element={<TariffPage />} />
<Route path="/faq" element={<Faq />} />
<Route path="/support" element={<Support />} />
<Route path="/support/:ticketId" element={<Support />} />
<Route path="/tariffconstructor" element={<TariffConstructor />} />
<Route path="/cart" element={<Cart />} />
<Route path="/wallet" element={<Wallet />} />
<Route path="/payment" element={<Payment />} />
<Route path="/settings" element={<AccountSettings />} />
<Route path="/history" element={<History />} />
<Route path="/tariffconstructor/savedtariffs" element={<SavedTariffs />} />
</Route>
</Route>
</Routes>
</>
);
};
return (
<>
{location.state?.backgroundLocation && (
<Routes>
<Route path="/signin" element={<SigninDialog />} />
<Route path="/signup" element={<SignupDialog />} />
</Routes>
)}
<Routes location={location.state?.backgroundLocation || location}>
<Route path="/" element={<Landing />} />
<Route path="/signin" element={<Navigate to="/" replace state={{ redirectTo: "/signin" }} />} />
<Route path="/signup" element={<Navigate to="/" replace state={{ redirectTo: "/signup" }} />} />
<Route element={<PrivateRoute />}>
<Route element={<ProtectedLayout />}>
<Route path="/tariffs" element={<Tariffs />} />
<Route path="/tariffs/time" element={<TariffPage />} />
<Route path="/tariffs/volume" element={<TariffPage />} />
<Route path="/faq" element={<Faq />} />
<Route path="/support" element={<Support />} />
<Route path="/support/:ticketId" element={<Support />} />
<Route path="/tariffconstructor" element={<TariffConstructor />} />
<Route path="/cart" element={<Cart />} />
<Route path="/wallet" element={<Wallet />} />
<Route path="/payment" element={<Payment />} />
<Route path="/settings" element={<AccountSettings />} />
<Route path="/history" element={<History />} />
<Route path="/tariffconstructor/savedtariffs" element={<SavedTariffs />} />
</Route>
</Route>
</Routes>
</>
)
}
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement);
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement)
root.render(
// <React.StrictMode>
<ThemeProvider theme={theme}>
<BrowserRouter>
<CssBaseline />
<SnackbarProvider />
<App />
</BrowserRouter>
</ThemeProvider>
// </React.StrictMode>
);
// <React.StrictMode>
<ThemeProvider theme={theme}>
<BrowserRouter>
<CssBaseline />
<SnackbarProvider />
<App />
</BrowserRouter>
</ThemeProvider>
// </React.StrictMode>
)
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
reportWebVitals()

@ -1,4 +1,4 @@
import { UserName } from "@frontend/kitui";
import { UserName } from "@frontend/kitui"
export enum VerificationStatus {

@ -1,4 +1,4 @@
import type { Attachment } from "@root/model/attachment";
import type { Attachment } from "@root/model/attachment"
export type File = {
name: "inn" | "rule" | "egrule" | "certificate";

@ -1,5 +1,5 @@
import { PrivilegeWithAmount } from "@frontend/kitui";
import { PrivilegeWithoutPrice } from "./privilege";
import { PrivilegeWithAmount } from "@frontend/kitui"
import { PrivilegeWithoutPrice } from "./privilege"
type ServiceKey = string;

@ -1,4 +1,4 @@
import { Discount } from "@frontend/kitui";
import { Discount } from "@frontend/kitui"
export interface GetDiscountsResponse {

@ -1,4 +1,4 @@
import { Privilege, PrivilegeWithAmount } from "@frontend/kitui";
import { Privilege, PrivilegeWithAmount } from "@frontend/kitui"
export type ServiceKeyToPrivilegesMap = Record<string, Privilege[]>;

@ -1,4 +1,4 @@
import { Tariff } from "@frontend/kitui";
import { Tariff } from "@frontend/kitui"
export interface GetTariffsResponse {

@ -1,180 +1,180 @@
import { useEffect } from "react";
import { Box, Button, SxProps, Theme, Typography, useMediaQuery, useTheme } from "@mui/material";
import InputTextfield from "@components/InputTextfield";
import PasswordInput from "@components/passwordInput";
import UserFields from "./UserFields";
import SectionWrapper from "@components/SectionWrapper";
import { openDocumentsDialog, sendUserData, setSettingsField, useUserStore } from "@root/stores/user";
import UnderlinedButtonWithIcon from "@root/components/UnderlinedButtonWithIcon";
import UploadIcon from "@root/components/icons/UploadIcon";
import DocumentsDialog from "./DocumentsDialog/DocumentsDialog";
import EyeIcon from "@root/components/icons/EyeIcon";
import { cardShadow } from "@root/utils/theme";
import { getMessageFromFetchError } from "@frontend/kitui";
import { enqueueSnackbar } from "notistack";
import { VerificationStatus } from "@root/model/account";
import { verify } from "./helper";
import { withErrorBoundary } from "react-error-boundary";
import { handleComponentError } from "@root/utils/handleComponentError";
import { useEffect } from "react"
import { Box, Button, SxProps, Theme, Typography, useMediaQuery, useTheme } from "@mui/material"
import InputTextfield from "@components/InputTextfield"
import PasswordInput from "@components/passwordInput"
import UserFields from "./UserFields"
import SectionWrapper from "@components/SectionWrapper"
import { openDocumentsDialog, sendUserData, setSettingsField, useUserStore } from "@root/stores/user"
import UnderlinedButtonWithIcon from "@root/components/UnderlinedButtonWithIcon"
import UploadIcon from "@root/components/icons/UploadIcon"
import DocumentsDialog from "./DocumentsDialog/DocumentsDialog"
import EyeIcon from "@root/components/icons/EyeIcon"
import { cardShadow } from "@root/utils/theme"
import { getMessageFromFetchError } from "@frontend/kitui"
import { enqueueSnackbar } from "notistack"
import { VerificationStatus } from "@root/model/account"
import { verify } from "./helper"
import { withErrorBoundary } from "react-error-boundary"
import { handleComponentError } from "@root/utils/handleComponentError"
function AccountSettings() {
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 isMobile = useMediaQuery(theme.breakpoints.down(600));
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 isMobile = useMediaQuery(theme.breakpoints.down(600))
const fields = useUserStore((state) => state.settingsFields);
const verificationStatus = useUserStore((state) => state.verificationStatus);
const verificationType = useUserStore((state) => state.verificationType);
const comment = useUserStore((state) => state.comment);
const userId = useUserStore((state) => state.userId) ?? "";
const fields = useUserStore((state) => state.settingsFields)
const verificationStatus = useUserStore((state) => state.verificationStatus)
const verificationType = useUserStore((state) => state.verificationType)
const comment = useUserStore((state) => state.comment)
const userId = useUserStore((state) => state.userId) ?? ""
useEffect(() => {
verify(userId);
}, []);
useEffect(() => {
verify(userId)
}, [])
const textFieldProps = {
gap: upMd ? "16px" : "10px",
color: "#F2F3F7",
bold: true,
};
const textFieldProps = {
gap: upMd ? "16px" : "10px",
color: "#F2F3F7",
bold: true,
}
function handleSendDataClick() {
sendUserData()
.then(() => {
enqueueSnackbar("Информация обновлена");
})
.catch((error) => {
const message = getMessageFromFetchError(error);
if (message) enqueueSnackbar(message);
});
}
function handleSendDataClick() {
sendUserData()
.then(() => {
enqueueSnackbar("Информация обновлена")
})
.catch((error) => {
const message = getMessageFromFetchError(error)
if (message) enqueueSnackbar(message)
})
}
return (
<SectionWrapper
maxWidth="lg"
sx={{
mt: "25px",
mb: "70px",
px: isTablet ? (isMobile ? "18px" : "40px") : "20px",
}}
>
<DocumentsDialog />
<Typography variant="h4" mt="20px">
return (
<SectionWrapper
maxWidth="lg"
sx={{
mt: "25px",
mb: "70px",
px: isTablet ? (isMobile ? "18px" : "40px") : "20px",
}}
>
<DocumentsDialog />
<Typography variant="h4" mt="20px">
Настройки аккаунта
</Typography>
<Box
sx={{
mt: "40px",
mb: "40px",
backgroundColor: "white",
display: "flex",
flexDirection: "column",
borderRadius: "12px",
p: "20px",
gap: "40px",
boxShadow: cardShadow,
}}
>
<Box
sx={{
display: "flex",
gap: "31px",
justifyContent: "space-between",
flexDirection: upMd ? "row" : "column",
}}
>
<UserFields/>
<Box
sx={{
maxWidth: "246px",
}}
>
<Typography variant="p1">Статус</Typography>
<VerificationIndicator verificationStatus={verificationStatus} sx={{ mt: "16px", p: "14px 7.5px" }} />
{verificationStatus === VerificationStatus.NOT_VERIFICATED && (
<>
<UnderlinedButtonWithIcon
icon={<UploadIcon />}
sx={{ mt: "55px" }}
ButtonProps={{
onClick: () => openDocumentsDialog("juridical"),
}}
>
</Typography>
<Box
sx={{
mt: "40px",
mb: "40px",
backgroundColor: "white",
display: "flex",
flexDirection: "column",
borderRadius: "12px",
p: "20px",
gap: "40px",
boxShadow: cardShadow,
}}
>
<Box
sx={{
display: "flex",
gap: "31px",
justifyContent: "space-between",
flexDirection: upMd ? "row" : "column",
}}
>
<UserFields/>
<Box
sx={{
maxWidth: "246px",
}}
>
<Typography variant="p1">Статус</Typography>
<VerificationIndicator verificationStatus={verificationStatus} sx={{ mt: "16px", p: "14px 7.5px" }} />
{verificationStatus === VerificationStatus.NOT_VERIFICATED && (
<>
<UnderlinedButtonWithIcon
icon={<UploadIcon />}
sx={{ mt: "55px" }}
ButtonProps={{
onClick: () => openDocumentsDialog("juridical"),
}}
>
Загрузить документы для юр лиц
</UnderlinedButtonWithIcon>
<UnderlinedButtonWithIcon
icon={<UploadIcon />}
sx={{ mt: "15px" }}
ButtonProps={{
onClick: () => openDocumentsDialog("nko"),
}}
>
</UnderlinedButtonWithIcon>
<UnderlinedButtonWithIcon
icon={<UploadIcon />}
sx={{ mt: "15px" }}
ButtonProps={{
onClick: () => openDocumentsDialog("nko"),
}}
>
Загрузить документы для НКО
</UnderlinedButtonWithIcon>
</>
)}
{verificationStatus === VerificationStatus.VERIFICATED && (
<UnderlinedButtonWithIcon
icon={<EyeIcon />}
sx={{ mt: "55px" }}
ButtonProps={{
onClick: () => openDocumentsDialog(verificationType),
}}
>
</UnderlinedButtonWithIcon>
</>
)}
{verificationStatus === VerificationStatus.VERIFICATED && (
<UnderlinedButtonWithIcon
icon={<EyeIcon />}
sx={{ mt: "55px" }}
ButtonProps={{
onClick: () => openDocumentsDialog(verificationType),
}}
>
Посмотреть свою верификацию
</UnderlinedButtonWithIcon>
)}
{comment && <p>{comment}</p>}
</Box>
</Box>
<Button
variant="pena-contained-dark"
onClick={handleSendDataClick}
disabled={fields.hasError}
sx={{ alignSelf: "end" }}
>
</UnderlinedButtonWithIcon>
)}
{comment && <p>{comment}</p>}
</Box>
</Box>
<Button
variant="pena-contained-dark"
onClick={handleSendDataClick}
disabled={fields.hasError}
sx={{ alignSelf: "end" }}
>
Сохранить
</Button>
</Box>
</SectionWrapper>
);
</Button>
</Box>
</SectionWrapper>
)
}
export default withErrorBoundary(AccountSettings, {
fallback: <Typography mt="8px" textAlign="center">Ошибка при отображении настроек аккаунта</Typography>,
onError: handleComponentError,
fallback: <Typography mt="8px" textAlign="center">Ошибка при отображении настроек аккаунта</Typography>,
onError: handleComponentError,
})
const verificationStatusData: Record<VerificationStatus, { text: string; color: string; }> = {
verificated: { text: "Верификация пройдена", color: "#0D9F00" },
waiting: { text: "В ожидании верификации", color: "#F18956" },
notVerificated: { text: "Не верифицирован", color: "#E02C2C" },
};
verificated: { text: "Верификация пройдена", color: "#0D9F00" },
waiting: { text: "В ожидании верификации", color: "#F18956" },
notVerificated: { text: "Не верифицирован", color: "#E02C2C" },
}
function VerificationIndicator({
verificationStatus,
sx,
verificationStatus,
sx,
}: {
verificationStatus: VerificationStatus;
sx?: SxProps<Theme>;
}) {
return (
<Box
sx={{
py: "14px",
px: "8.5px",
borderWidth: "1px",
borderStyle: "solid",
color: verificationStatusData[verificationStatus].color,
borderColor: verificationStatusData[verificationStatus].color,
borderRadius: "8px",
textAlign: "center",
...sx,
}}
>
<Typography lineHeight="100%">{verificationStatusData[verificationStatus].text}</Typography>
</Box>
);
return (
<Box
sx={{
py: "14px",
px: "8.5px",
borderWidth: "1px",
borderStyle: "solid",
color: verificationStatusData[verificationStatus].color,
borderColor: verificationStatusData[verificationStatus].color,
borderRadius: "8px",
textAlign: "center",
...sx,
}}
>
<Typography lineHeight="100%">{verificationStatusData[verificationStatus].text}</Typography>
</Box>
)
}

@ -1,8 +1,8 @@
import axios from "axios";
import { Box, SxProps, Theme, Typography, useTheme } from "@mui/material";
import { Document, Page } from "react-pdf";
import { Buffer } from "buffer";
import { downloadFileToDevice } from "@root/utils/downloadFileToDevice";
import axios from "axios"
import { Box, SxProps, Theme, Typography, useTheme } from "@mui/material"
import { Document, Page } from "react-pdf"
import { Buffer } from "buffer"
import { downloadFileToDevice } from "@root/utils/downloadFileToDevice"
interface Props {
text: string;
@ -11,62 +11,62 @@ interface Props {
}
export default function DocumentItem({ text, documentUrl = "", sx }: Props) {
const theme = useTheme();
const theme = useTheme()
const downloadFile = async () => {
const { data } = await axios.get<ArrayBuffer>(documentUrl, {
responseType: "arraybuffer",
});
const downloadFile = async () => {
const { data } = await axios.get<ArrayBuffer>(documentUrl, {
responseType: "arraybuffer",
})
if (!data) {
return;
}
if (!data) {
return
}
downloadFileToDevice(
`${documentUrl.split("/").pop()?.split(".")?.[0] || "document"}.pdf`,
Buffer.from(data)
);
downloadFileToDevice(
`${documentUrl.split("/").pop()?.split(".")?.[0] || "document"}.pdf`,
Buffer.from(data)
)
return;
};
return
}
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "start",
gap: "10px",
...sx,
}}
>
<Typography
sx={{
color: "#4D4D4D",
fontWeight: 500,
fontVariantNumeric: "tabular-nums",
}}
>
{text}
</Typography>
{documentUrl && (
<>
<Typography
sx={{ color: theme.palette.purple.main, cursor: "pointer" }}
onClick={downloadFile}
>
{documentUrl.split("/").pop()?.split(".")?.[0]}
</Typography>
<Document file={documentUrl}>
<Page
pageNumber={1}
width={200}
renderTextLayer={false}
renderAnnotationLayer={false}
/>
</Document>
</>
)}
</Box>
);
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "start",
gap: "10px",
...sx,
}}
>
<Typography
sx={{
color: "#4D4D4D",
fontWeight: 500,
fontVariantNumeric: "tabular-nums",
}}
>
{text}
</Typography>
{documentUrl && (
<>
<Typography
sx={{ color: theme.palette.purple.main, cursor: "pointer" }}
onClick={downloadFile}
>
{documentUrl.split("/").pop()?.split(".")?.[0]}
</Typography>
<Document file={documentUrl}>
<Page
pageNumber={1}
width={200}
renderTextLayer={false}
renderAnnotationLayer={false}
/>
</Document>
</>
)}
</Box>
)
}

@ -1,16 +1,16 @@
import { ChangeEvent, useRef } from "react";
import axios from "axios";
import { Document, Page, pdfjs } from "react-pdf";
import { Box, SxProps, Theme, Typography } from "@mui/material";
import { Buffer } from "buffer";
import { ChangeEvent, useRef } from "react"
import axios from "axios"
import { Document, Page, pdfjs } from "react-pdf"
import { Box, SxProps, Theme, Typography } from "@mui/material"
import { Buffer } from "buffer"
import UnderlinedButtonWithIcon from "@root/components/UnderlinedButtonWithIcon";
import PaperClipIcon from "@root/components/icons/PaperClipIcon";
import UnderlinedButtonWithIcon from "@root/components/UnderlinedButtonWithIcon"
import PaperClipIcon from "@root/components/icons/PaperClipIcon"
import { UserDocument } from "@root/model/user";
import { UserDocument } from "@root/model/user"
import { readFile } from "@root/utils/readFile";
import { downloadFileToDevice } from "@root/utils/downloadFileToDevice";
import { readFile } from "@root/utils/readFile"
import { downloadFileToDevice } from "@root/utils/downloadFileToDevice"
interface Props {
text: string;
@ -22,89 +22,89 @@ interface Props {
}
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
"pdfjs-dist/build/pdf.worker.min.js",
import.meta.url
).toString();
"pdfjs-dist/build/pdf.worker.min.js",
import.meta.url
).toString()
export default function DocumentUploadItem({
text,
document,
documentUrl = "",
onFileChange,
sx,
accept = "image/*",
text,
document,
documentUrl = "",
onFileChange,
sx,
accept = "image/*",
}: Props) {
const fileInputRef = useRef<HTMLInputElement>(null);
const fileInputRef = useRef<HTMLInputElement>(null)
function handleChooseFileClick() {
fileInputRef.current?.click();
}
function handleChooseFileClick() {
fileInputRef.current?.click()
}
const downloadFile = async () => {
if (!document.file && documentUrl) {
const { data } = await axios.get<ArrayBuffer>(documentUrl, {
responseType: "arraybuffer",
});
const downloadFile = async () => {
if (!document.file && documentUrl) {
const { data } = await axios.get<ArrayBuffer>(documentUrl, {
responseType: "arraybuffer",
})
if (!data) {
return;
}
if (!data) {
return
}
downloadFileToDevice("document.pdf", Buffer.from(data));
downloadFileToDevice("document.pdf", Buffer.from(data))
return;
}
return
}
if (document.file) {
const fileArrayBuffer = await readFile(document.file, "array");
if (document.file) {
const fileArrayBuffer = await readFile(document.file, "array")
downloadFileToDevice(document.file.name, Buffer.from(fileArrayBuffer));
}
};
downloadFileToDevice(document.file.name, Buffer.from(fileArrayBuffer))
}
}
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "start",
gap: "10px",
...sx,
}}
>
<Typography
sx={{
color: "#4D4D4D",
fontWeight: 500,
fontVariantNumeric: "tabular-nums",
}}
>
{text}
</Typography>
<UnderlinedButtonWithIcon
icon={<PaperClipIcon />}
onClick={handleChooseFileClick}
>
{document.file ? document.file.name : "Выберите файл"}
</UnderlinedButtonWithIcon>
<input
ref={fileInputRef}
style={{ display: "none" }}
onChange={onFileChange}
type="file"
id="image-file"
multiple
accept={accept}
/>
<Document file={document.file || (documentUrl && { url: documentUrl })}>
<Page
pageNumber={1}
width={200}
renderTextLayer={false}
renderAnnotationLayer={false}
onClick={downloadFile}
/>
</Document>
</Box>
);
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "start",
gap: "10px",
...sx,
}}
>
<Typography
sx={{
color: "#4D4D4D",
fontWeight: 500,
fontVariantNumeric: "tabular-nums",
}}
>
{text}
</Typography>
<UnderlinedButtonWithIcon
icon={<PaperClipIcon />}
onClick={handleChooseFileClick}
>
{document.file ? document.file.name : "Выберите файл"}
</UnderlinedButtonWithIcon>
<input
ref={fileInputRef}
style={{ display: "none" }}
onChange={onFileChange}
type="file"
id="image-file"
multiple
accept={accept}
/>
<Document file={document.file || (documentUrl && { url: documentUrl })}>
<Page
pageNumber={1}
width={200}
renderTextLayer={false}
renderAnnotationLayer={false}
onClick={downloadFile}
/>
</Document>
</Box>
)
}

@ -1,17 +1,17 @@
import { useUserStore } from "@root/stores/user";
import NkoDocumentsDialog from "./NkoDocumentsDialog";
import JuridicalDocumentsDialog from "./JuridicalDocumentsDialog";
import { useUserStore } from "@root/stores/user"
import NkoDocumentsDialog from "./NkoDocumentsDialog"
import JuridicalDocumentsDialog from "./JuridicalDocumentsDialog"
export default function DocumentsDialog() {
switch(useUserStore(state => state.dialogType)) {
case 'juridical':
return <JuridicalDocumentsDialog />
switch(useUserStore(state => state.dialogType)) {
case "juridical":
return <JuridicalDocumentsDialog />
case "nko":
return <NkoDocumentsDialog />
case "nko":
return <NkoDocumentsDialog />
default:
return <></>
}
default:
return <></>
}
}

@ -1,227 +1,227 @@
import { Box, Button, Dialog, IconButton, Typography } from "@mui/material";
import { enqueueSnackbar } from "notistack";
import CloseSmallIcon from "@root/components/icons/CloseSmallIcon";
import { Box, Button, Dialog, IconButton, Typography } from "@mui/material"
import { enqueueSnackbar } from "notistack"
import CloseSmallIcon from "@root/components/icons/CloseSmallIcon"
import {
closeDocumentsDialog,
setDocument,
useUserStore,
} from "@root/stores/user";
import DocumentUploadItem from "./DocumentUploadItem";
import DocumentItem from "./DocumentItem";
import { VerificationStatus } from "@root/model/account";
import { sendDocuments, updateDocuments } from "@root/api/verification";
import { readFile } from "@root/utils/readFile";
import { deleteEmptyKeys } from "@root/utils/deleteEmptyKeys";
import { verify } from "../helper";
import { useState } from "react";
import { theme } from "@root/utils/theme";
closeDocumentsDialog,
setDocument,
useUserStore,
} from "@root/stores/user"
import DocumentUploadItem from "./DocumentUploadItem"
import DocumentItem from "./DocumentItem"
import { VerificationStatus } from "@root/model/account"
import { sendDocuments, updateDocuments } from "@root/api/verification"
import { readFile } from "@root/utils/readFile"
import { deleteEmptyKeys } from "@root/utils/deleteEmptyKeys"
import { verify } from "../helper"
import { useState } from "react"
import { theme } from "@root/utils/theme"
const dialogContainerStyle = {
height: "100%",
overflowY: "scroll",
"::-webkit-scrollbar": {
display: "none",
},
};
height: "100%",
overflowY: "scroll",
"::-webkit-scrollbar": {
display: "none",
},
}
export default function JuridicalDocumentsDialog() {
const isOpen = useUserStore((state) => state.isDocumentsDialogOpen);
const verificationStatus = useUserStore((state) => state.verificationStatus);
const documents = useUserStore((state) => state.documents);//загруженные юзером файлы
const documentsUrl = useUserStore((state) => state.documentsUrl);//ссылки с бекенда
const userId = useUserStore((state) => state.userId) ?? "";
const isOpen = useUserStore((state) => state.isDocumentsDialogOpen)
const verificationStatus = useUserStore((state) => state.verificationStatus)
const documents = useUserStore((state) => state.documents)//загруженные юзером файлы
const documentsUrl = useUserStore((state) => state.documentsUrl)//ссылки с бекенда
const userId = useUserStore((state) => state.userId) ?? ""
const sendUploadedDocuments = async () => {
const sendUploadedDocuments = async () => {
if (documents["ИНН"].file && documents["Устав"].file && !documentsUrl["ИНН"] && !documentsUrl["Устав"]) {
closeDocumentsDialog();
//Пользователь заполнил все поля и на беке пусто
const inn = await readFile(documents["ИНН"].file, "binary");
const rule = await readFile(documents["Устав"].file, "binary");
if (documents["ИНН"].file && documents["Устав"].file && !documentsUrl["ИНН"] && !documentsUrl["Устав"]) {
closeDocumentsDialog()
//Пользователь заполнил все поля и на беке пусто
const inn = await readFile(documents["ИНН"].file, "binary")
const rule = await readFile(documents["Устав"].file, "binary")
const [_, sendDocumentsError] = await sendDocuments({
status: "org",
inn,
rule,
});
const [_, sendDocumentsError] = await sendDocuments({
status: "org",
inn,
rule,
})
if (sendDocumentsError) {
enqueueSnackbar(sendDocumentsError);
return;
}
if (_ === "OK") {
enqueueSnackbar("Информация доставлена")
}
if (sendDocumentsError) {
enqueueSnackbar(sendDocumentsError)
return
}
if (_ === "OK") {
enqueueSnackbar("Информация доставлена")
}
setDocument("ИНН", null);
setDocument("Устав", null);
setDocument("ИНН", null)
setDocument("Устав", null)
await verify(userId);
} else { //Пользователь заполнил не все, или на беке что-то есть
if ((documents["ИНН"].file || documents["Устав"].file) && (documentsUrl["ИНН"] || documentsUrl["Устав"])) { //минимум 1 поле заполнено на фронте и минимум 1 поле на беке записано
closeDocumentsDialog();
const inn = documents["ИНН"].file
? await readFile(documents["ИНН"].file, "binary")
: undefined;
const rule = documents["Устав"].file
? await readFile(documents["Устав"].file, "binary")
: undefined;
await verify(userId)
} else { //Пользователь заполнил не все, или на беке что-то есть
if ((documents["ИНН"].file || documents["Устав"].file) && (documentsUrl["ИНН"] || documentsUrl["Устав"])) { //минимум 1 поле заполнено на фронте и минимум 1 поле на беке записано
closeDocumentsDialog()
const inn = documents["ИНН"].file
? await readFile(documents["ИНН"].file, "binary")
: undefined
const rule = documents["Устав"].file
? await readFile(documents["Устав"].file, "binary")
: undefined
const [_, updateDocumentsError] = await updateDocuments(
deleteEmptyKeys({
status: "org",
inn,
rule,
})
);
const [_, updateDocumentsError] = await updateDocuments(
deleteEmptyKeys({
status: "org",
inn,
rule,
})
)
if (updateDocumentsError) {
enqueueSnackbar(updateDocumentsError);
if (updateDocumentsError) {
enqueueSnackbar(updateDocumentsError)
return;
}
if (_ === "OK") {
enqueueSnackbar("Информация доставлена")
}
return
}
if (_ === "OK") {
enqueueSnackbar("Информация доставлена")
}
setDocument("ИНН", null);
setDocument("Устав", null);
setDocument("ИНН", null)
setDocument("Устав", null)
await verify(userId);
}
}
};
await verify(userId)
}
}
}
const disbutt = () => {
if (documents["ИНН"].file && documents["Устав"].file && !documentsUrl["ИНН"] && !documentsUrl["Устав"]) { //post
//все поля заполнены и на беке пусто
return false
} else {//patch
if (documents["ИНН"].file || documents["Устав"].file) {
//минимум одно поле заполнено
return false
}
return true
}
}
const disbutt = () => {
if (documents["ИНН"].file && documents["Устав"].file && !documentsUrl["ИНН"] && !documentsUrl["Устав"]) { //post
//все поля заполнены и на беке пусто
return false
} else {//patch
if (documents["ИНН"].file || documents["Устав"].file) {
//минимум одно поле заполнено
return false
}
return true
}
}
const documentElements =
const documentElements =
verificationStatus === VerificationStatus.VERIFICATED ? (
<>
<DocumentItem
text="1. Скан ИНН организации НКО (выписка из ЕГЮРЛ)"
documentUrl={documentsUrl["ИНН"]}
/>
<DocumentItem
text="2. Устав организации"
documentUrl={documentsUrl["Устав"]}
/>
</>
<>
<DocumentItem
text="1. Скан ИНН организации НКО (выписка из ЕГЮРЛ)"
documentUrl={documentsUrl["ИНН"]}
/>
<DocumentItem
text="2. Устав организации"
documentUrl={documentsUrl["Устав"]}
/>
</>
) : (
<>
<DocumentUploadItem
document={documents["ИНН"]}
documentUrl={documentsUrl["ИНН"]}
text="1. Скан ИНН организации (выписка из ЕГЮРЛ)"
accept="application/pdf"
onFileChange={(e) => setDocument("ИНН", e.target?.files?.[0] || null)}
/>
<DocumentUploadItem
document={documents["Устав"]}
documentUrl={documentsUrl["Устав"]}
text="2. Устав организации"
accept="application/pdf"
onFileChange={(e) =>
setDocument("Устав", e.target?.files?.[0] || null)
}
/>
</>
);
<>
<DocumentUploadItem
document={documents["ИНН"]}
documentUrl={documentsUrl["ИНН"]}
text="1. Скан ИНН организации (выписка из ЕГЮРЛ)"
accept="application/pdf"
onFileChange={(e) => setDocument("ИНН", e.target?.files?.[0] || null)}
/>
<DocumentUploadItem
document={documents["Устав"]}
documentUrl={documentsUrl["Устав"]}
text="2. Устав организации"
accept="application/pdf"
onFileChange={(e) =>
setDocument("Устав", e.target?.files?.[0] || null)
}
/>
</>
)
return (
<Dialog
open={isOpen}
onClose={closeDocumentsDialog}
PaperProps={{
sx: {
width: "600px",
maxWidth: "600px",
backgroundColor: "white",
position: "relative",
display: "flex",
flexDirection: "column",
gap: "20px",
borderRadius: "12px",
boxShadow: "none",
overflowY: "hidden",
},
}}
slotProps={{
backdrop: { style: { backgroundColor: "rgb(0 0 0 / 0.7)" } },
}}
>
<IconButton
onClick={closeDocumentsDialog}
sx={{
position: "absolute",
right: "7px",
top: "7px",
}}
>
<CloseSmallIcon />
</IconButton>
<Box sx={{ p: "40px", overflowY: "scroll" }}>
<Typography variant="h5" lineHeight="100%">
{verificationStatus === VerificationStatus.VERIFICATED
? "Ваши документы"
: "Загрузите документы"}
</Typography>
<Typography
sx={{
fontWeight: 400,
fontSize: "16px",
lineHeight: "100%",
mt: "12px",
}}
>
return (
<Dialog
open={isOpen}
onClose={closeDocumentsDialog}
PaperProps={{
sx: {
width: "600px",
maxWidth: "600px",
backgroundColor: "white",
position: "relative",
display: "flex",
flexDirection: "column",
gap: "20px",
borderRadius: "12px",
boxShadow: "none",
overflowY: "hidden",
},
}}
slotProps={{
backdrop: { style: { backgroundColor: "rgb(0 0 0 / 0.7)" } },
}}
>
<IconButton
onClick={closeDocumentsDialog}
sx={{
position: "absolute",
right: "7px",
top: "7px",
}}
>
<CloseSmallIcon />
</IconButton>
<Box sx={{ p: "40px", overflowY: "scroll" }}>
<Typography variant="h5" lineHeight="100%">
{verificationStatus === VerificationStatus.VERIFICATED
? "Ваши документы"
: "Загрузите документы"}
</Typography>
<Typography
sx={{
fontWeight: 400,
fontSize: "16px",
lineHeight: "100%",
mt: "12px",
}}
>
для верификации юридических лиц в формате PDF
</Typography>
{Boolean(!documentsUrl["ИНН"] && !documentsUrl["Устав"]) && <Typography
sx={{
fontWeight: 400,
fontSize: "16px",
lineHeight: "100%",
mt: "12px",
color: theme.palette.purple.main
}}
>
</Typography>
{Boolean(!documentsUrl["ИНН"] && !documentsUrl["Устав"]) && <Typography
sx={{
fontWeight: 400,
fontSize: "16px",
lineHeight: "100%",
mt: "12px",
color: theme.palette.purple.main
}}
>
Пожалуйста, заполните все поля!
</Typography>}
</Typography>}
<Box sx={dialogContainerStyle}>
<Box
sx={{
maxHeight: "200px",
mt: "30px",
display: "flex",
flexDirection: "column",
gap: "25px",
}}
>
{documentElements}
</Box>
</Box>
</Box>
{verificationStatus === VerificationStatus.NOT_VERIFICATED && (
<Button
sx={{ position: "absolute", bottom: "20px", right: "20px" }}
onClick={sendUploadedDocuments}
variant="pena-contained-dark"
disabled={disbutt()}
>
<Box sx={dialogContainerStyle}>
<Box
sx={{
maxHeight: "200px",
mt: "30px",
display: "flex",
flexDirection: "column",
gap: "25px",
}}
>
{documentElements}
</Box>
</Box>
</Box>
{verificationStatus === VerificationStatus.NOT_VERIFICATED && (
<Button
sx={{ position: "absolute", bottom: "20px", right: "20px" }}
onClick={sendUploadedDocuments}
variant="pena-contained-dark"
disabled={disbutt()}
>
Отправить
</Button>
)}
</Dialog>
);
</Button>
)}
</Dialog>
)
}

@ -1,263 +1,263 @@
import { Box, Button, Dialog, IconButton, Typography } from "@mui/material";
import { enqueueSnackbar } from "notistack";
import CloseSmallIcon from "@root/components/icons/CloseSmallIcon";
import { Box, Button, Dialog, IconButton, Typography } from "@mui/material"
import { enqueueSnackbar } from "notistack"
import CloseSmallIcon from "@root/components/icons/CloseSmallIcon"
import {
closeDocumentsDialog,
setDocument,
useUserStore,
} from "@root/stores/user";
import DocumentUploadItem from "./DocumentUploadItem";
import DocumentItem from "./DocumentItem";
import { verify } from "../helper";
import { VerificationStatus } from "@root/model/account";
import { sendDocuments, updateDocuments } from "@root/api/verification";
import { readFile } from "@root/utils/readFile";
import { deleteEmptyKeys } from "@root/utils/deleteEmptyKeys";
import { useState } from "react";
import { theme } from "@root/utils/theme";
closeDocumentsDialog,
setDocument,
useUserStore,
} from "@root/stores/user"
import DocumentUploadItem from "./DocumentUploadItem"
import DocumentItem from "./DocumentItem"
import { verify } from "../helper"
import { VerificationStatus } from "@root/model/account"
import { sendDocuments, updateDocuments } from "@root/api/verification"
import { readFile } from "@root/utils/readFile"
import { deleteEmptyKeys } from "@root/utils/deleteEmptyKeys"
import { useState } from "react"
import { theme } from "@root/utils/theme"
const dialogContainerStyle = {
height: "100%",
overflowY: "scroll",
"::-webkit-scrollbar": {
display: "none",
},
};
height: "100%",
overflowY: "scroll",
"::-webkit-scrollbar": {
display: "none",
},
}
export default function NkoDocumentsDialog() {
const isOpen = useUserStore((state) => state.isDocumentsDialogOpen);
const verificationStatus = useUserStore((state) => state.verificationStatus);
const documents = useUserStore((state) => state.documents);
const documentsUrl = useUserStore((state) => state.documentsUrl);
const userId = useUserStore((state) => state.userId) ?? "";
const isOpen = useUserStore((state) => state.isDocumentsDialogOpen)
const verificationStatus = useUserStore((state) => state.verificationStatus)
const documents = useUserStore((state) => state.documents)
const documentsUrl = useUserStore((state) => state.documentsUrl)
const userId = useUserStore((state) => state.userId) ?? ""
const sendUploadedDocuments = async () => {
if (
documents["ИНН"].file &&
const sendUploadedDocuments = async () => {
if (
documents["ИНН"].file &&
documents["Устав"].file &&
documents["Свидетельство о регистрации НКО"].file
&& !documentsUrl["ИНН"] && !documentsUrl["Устав"] && !documentsUrl["Свидетельство о регистрации НКО"]
) {
closeDocumentsDialog();
//Пользователь заполнил все поля и на беке пусто
const inn = await readFile(documents["ИНН"].file, "binary");
const rule = await readFile(documents["Устав"].file, "binary");
const certificate = await readFile(
documents["Свидетельство о регистрации НКО"].file,
"binary"
);
) {
closeDocumentsDialog()
//Пользователь заполнил все поля и на беке пусто
const inn = await readFile(documents["ИНН"].file, "binary")
const rule = await readFile(documents["Устав"].file, "binary")
const certificate = await readFile(
documents["Свидетельство о регистрации НКО"].file,
"binary"
)
const [_, sendDocumentsError] = await sendDocuments({
status: "nko",
inn,
rule,
certificate,
});
const [_, sendDocumentsError] = await sendDocuments({
status: "nko",
inn,
rule,
certificate,
})
if (sendDocumentsError) {
enqueueSnackbar(sendDocumentsError);
if (sendDocumentsError) {
enqueueSnackbar(sendDocumentsError)
return;
}
if (_ === "OK") {
enqueueSnackbar("Информация доставлена")
}
return
}
if (_ === "OK") {
enqueueSnackbar("Информация доставлена")
}
setDocument("ИНН", null);
setDocument("Устав", null);
setDocument("Свидетельство о регистрации НКО", null);
setDocument("ИНН", null)
setDocument("Устав", null)
setDocument("Свидетельство о регистрации НКО", null)
await verify(userId);
} else { //Пользователь заполнил не все, или на беке что-то есть
if ((documents["ИНН"].file || documents["Устав"].file || documents["Свидетельство о регистрации НКО"].file) && (documentsUrl["ИНН"] || documentsUrl["Устав"] || documentsUrl["Свидетельство о регистрации НКО"])) { //минимум 1 поле заполнено на фронте и минимум 1 поле на беке записано
closeDocumentsDialog();
const inn = documents["ИНН"].file
? await readFile(documents["ИНН"].file, "binary")
: undefined;
const rule = documents["Устав"].file
? await readFile(documents["Устав"].file, "binary")
: undefined;
const certificate = documents["Свидетельство о регистрации НКО"].file
? await readFile(
documents["Свидетельство о регистрации НКО"].file,
"binary"
)
: undefined;
await verify(userId)
} else { //Пользователь заполнил не все, или на беке что-то есть
if ((documents["ИНН"].file || documents["Устав"].file || documents["Свидетельство о регистрации НКО"].file) && (documentsUrl["ИНН"] || documentsUrl["Устав"] || documentsUrl["Свидетельство о регистрации НКО"])) { //минимум 1 поле заполнено на фронте и минимум 1 поле на беке записано
closeDocumentsDialog()
const inn = documents["ИНН"].file
? await readFile(documents["ИНН"].file, "binary")
: undefined
const rule = documents["Устав"].file
? await readFile(documents["Устав"].file, "binary")
: undefined
const certificate = documents["Свидетельство о регистрации НКО"].file
? await readFile(
documents["Свидетельство о регистрации НКО"].file,
"binary"
)
: undefined
const [_, updateDocumentsError] = await updateDocuments(
deleteEmptyKeys({
status: "org",
inn,
rule,
certificate,
})
);
const [_, updateDocumentsError] = await updateDocuments(
deleteEmptyKeys({
status: "org",
inn,
rule,
certificate,
})
)
if (updateDocumentsError) {
enqueueSnackbar(updateDocumentsError);
if (updateDocumentsError) {
enqueueSnackbar(updateDocumentsError)
return;
}
if (_ === "OK") {
enqueueSnackbar("Информация доставлена")
}
return
}
if (_ === "OK") {
enqueueSnackbar("Информация доставлена")
}
setDocument("ИНН", null);
setDocument("Устав", null);
setDocument("Свидетельство о регистрации НКО", null);
setDocument("ИНН", null)
setDocument("Устав", null)
setDocument("Свидетельство о регистрации НКО", null)
await verify(userId);
}
}
await verify(userId)
}
}
};
}
const disbutt = () => {
if (documents["ИНН"].file && documents["Устав"].file && documents["Свидетельство о регистрации НКО"].file && !documentsUrl["ИНН"] && !documentsUrl["Устав"] && !documentsUrl["Свидетельство о регистрации НКО"]) { //post
//все поля заполнены и на беке пусто
return false
} else {//patch
if (documents["ИНН"].file || documents["Устав"].file || documents["Свидетельство о регистрации НКО"].file ) {
//минимум одно поле заполнено
return false
}
return true
}
}
const disbutt = () => {
if (documents["ИНН"].file && documents["Устав"].file && documents["Свидетельство о регистрации НКО"].file && !documentsUrl["ИНН"] && !documentsUrl["Устав"] && !documentsUrl["Свидетельство о регистрации НКО"]) { //post
//все поля заполнены и на беке пусто
return false
} else {//patch
if (documents["ИНН"].file || documents["Устав"].file || documents["Свидетельство о регистрации НКО"].file ) {
//минимум одно поле заполнено
return false
}
return true
}
}
const documentElements =
const documentElements =
verificationStatus === VerificationStatus.VERIFICATED ? (
<>
<DocumentItem
text="1. Свидетельство о регистрации НКО"
documentUrl={documentsUrl["Свидетельство о регистрации НКО"]}
/>
<DocumentItem
text="2. Скан ИНН организации НКО (выписка из ЕГЮРЛ)"
documentUrl={documentsUrl["ИНН"]}
/>
<DocumentItem
text="3. Устав организации"
documentUrl={documentsUrl["Устав"]}
/>
</>
<>
<DocumentItem
text="1. Свидетельство о регистрации НКО"
documentUrl={documentsUrl["Свидетельство о регистрации НКО"]}
/>
<DocumentItem
text="2. Скан ИНН организации НКО (выписка из ЕГЮРЛ)"
documentUrl={documentsUrl["ИНН"]}
/>
<DocumentItem
text="3. Устав организации"
documentUrl={documentsUrl["Устав"]}
/>
</>
) : (
<>
<DocumentUploadItem
text="1. Свидетельство о регистрации НКО"
accept="application/pdf"
document={documents["Свидетельство о регистрации НКО"]}
documentUrl={documentsUrl["Свидетельство о регистрации НКО"]}
onFileChange={(e) =>
setDocument(
"Свидетельство о регистрации НКО",
e.target?.files?.[0] || null
)
}
/>
<DocumentUploadItem
text="2. Скан ИНН организации НКО (выписка из ЕГЮРЛ)"
accept="application/pdf"
document={documents["ИНН"]}
documentUrl={documentsUrl["ИНН"]}
onFileChange={(e) => setDocument("ИНН", e.target?.files?.[0] || null)}
/>
<DocumentUploadItem
text="3. Устав организации"
accept="application/pdf"
document={documents["Устав"]}
documentUrl={documentsUrl["Устав"]}
onFileChange={(e) =>
setDocument("Устав", e.target?.files?.[0] || null)
}
/>
</>
);
<>
<DocumentUploadItem
text="1. Свидетельство о регистрации НКО"
accept="application/pdf"
document={documents["Свидетельство о регистрации НКО"]}
documentUrl={documentsUrl["Свидетельство о регистрации НКО"]}
onFileChange={(e) =>
setDocument(
"Свидетельство о регистрации НКО",
e.target?.files?.[0] || null
)
}
/>
<DocumentUploadItem
text="2. Скан ИНН организации НКО (выписка из ЕГЮРЛ)"
accept="application/pdf"
document={documents["ИНН"]}
documentUrl={documentsUrl["ИНН"]}
onFileChange={(e) => setDocument("ИНН", e.target?.files?.[0] || null)}
/>
<DocumentUploadItem
text="3. Устав организации"
accept="application/pdf"
document={documents["Устав"]}
documentUrl={documentsUrl["Устав"]}
onFileChange={(e) =>
setDocument("Устав", e.target?.files?.[0] || null)
}
/>
</>
)
return (
<Dialog
open={isOpen}
onClose={closeDocumentsDialog}
PaperProps={{
sx: {
width: "600px",
maxWidth: "600px",
backgroundColor: "white",
position: "relative",
display: "flex",
flexDirection: "column",
gap: "20px",
borderRadius: "12px",
boxShadow: "none",
overflowY: "hidden",
},
}}
slotProps={{
backdrop: { style: { backgroundColor: "rgb(0 0 0 / 0.7)" } },
}}
>
<IconButton
onClick={closeDocumentsDialog}
sx={{
position: "absolute",
right: "7px",
top: "7px",
}}
>
<CloseSmallIcon />
</IconButton>
<Box sx={{ p: "40px", overflowY: "scroll" }}>
<Typography variant="h5" lineHeight="100%">
{verificationStatus === VerificationStatus.VERIFICATED
? "Ваши документы"
: "Загрузите документы"}
</Typography>
<Typography
sx={{
fontWeight: 400,
fontSize: "16px",
lineHeight: "100%",
mt: "12px",
}}
>
return (
<Dialog
open={isOpen}
onClose={closeDocumentsDialog}
PaperProps={{
sx: {
width: "600px",
maxWidth: "600px",
backgroundColor: "white",
position: "relative",
display: "flex",
flexDirection: "column",
gap: "20px",
borderRadius: "12px",
boxShadow: "none",
overflowY: "hidden",
},
}}
slotProps={{
backdrop: { style: { backgroundColor: "rgb(0 0 0 / 0.7)" } },
}}
>
<IconButton
onClick={closeDocumentsDialog}
sx={{
position: "absolute",
right: "7px",
top: "7px",
}}
>
<CloseSmallIcon />
</IconButton>
<Box sx={{ p: "40px", overflowY: "scroll" }}>
<Typography variant="h5" lineHeight="100%">
{verificationStatus === VerificationStatus.VERIFICATED
? "Ваши документы"
: "Загрузите документы"}
</Typography>
<Typography
sx={{
fontWeight: 400,
fontSize: "16px",
lineHeight: "100%",
mt: "12px",
}}
>
для верификации НКО в формате PDF
</Typography>
{Boolean(!documentsUrl["ИНН"] && !documentsUrl["Устав"] && !documentsUrl["Свидетельство о регистрации НКО"]) && <Typography
sx={{
fontWeight: 400,
fontSize: "16px",
lineHeight: "100%",
mt: "12px",
color: theme.palette.purple.main
}}
>
</Typography>
{Boolean(!documentsUrl["ИНН"] && !documentsUrl["Устав"] && !documentsUrl["Свидетельство о регистрации НКО"]) && <Typography
sx={{
fontWeight: 400,
fontSize: "16px",
lineHeight: "100%",
mt: "12px",
color: theme.palette.purple.main
}}
>
Пожалуйста, заполните все поля!
</Typography>}
<Box sx={dialogContainerStyle}>
<Box
sx={{
maxHeight: "280px",
mt: "30px",
display: "flex",
flexDirection: "column",
gap: "25px",
}}
>
{documentElements}
</Box>
</Box>
</Box>
{verificationStatus === VerificationStatus.NOT_VERIFICATED && (
<Button
sx={{ position: "absolute", bottom: "20px", right: "20px" }}
onClick={sendUploadedDocuments}
variant="pena-contained-dark"
disabled={disbutt()}
>
</Typography>}
<Box sx={dialogContainerStyle}>
<Box
sx={{
maxHeight: "280px",
mt: "30px",
display: "flex",
flexDirection: "column",
gap: "25px",
}}
>
{documentElements}
</Box>
</Box>
</Box>
{verificationStatus === VerificationStatus.NOT_VERIFICATED && (
<Button
sx={{ position: "absolute", bottom: "20px", right: "20px" }}
onClick={sendUploadedDocuments}
variant="pena-contained-dark"
disabled={disbutt()}
>
Отправить
</Button>
)}
<></>
</Dialog>
);
</Button>
)}
<></>
</Dialog>
)
}

@ -1,121 +1,121 @@
import { Box, useMediaQuery, useTheme } from "@mui/material";
import InputTextfield from "@components/InputTextfield";
import PasswordInput from "@components/passwordInput";
import { setSettingsField, useUserStore } from "@root/stores/user";
import { Box, useMediaQuery, useTheme } from "@mui/material"
import InputTextfield from "@components/InputTextfield"
import PasswordInput from "@components/passwordInput"
import { setSettingsField, useUserStore } from "@root/stores/user"
export default function UserFields () {
const theme = useTheme();
const upSm = useMediaQuery(theme.breakpoints.up("sm"));
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const theme = useTheme()
const upSm = useMediaQuery(theme.breakpoints.up("sm"))
const upMd = useMediaQuery(theme.breakpoints.up("md"))
const fields = useUserStore((state) => state.settingsFields);
const fields = useUserStore((state) => state.settingsFields)
console.log("fields")
console.log("fields")
const textFieldProps = {
gap: upMd ? "16px" : "10px",
color: "#F2F3F7",
bold: true,
};
const textFieldProps = {
gap: upMd ? "16px" : "10px",
color: "#F2F3F7",
bold: true,
}
return(
<Box
sx={{
display: "grid",
gridAutoFlow: upSm ? "column" : "row",
gridTemplateRows: "repeat(4, auto)",
gridAutoColumns: "1fr",
rowGap: "15px",
columnGap: "31px",
flexGrow: 1,
}}
>
<InputTextfield
TextfieldProps={{
placeholder: "Имя",
value: fields.firstname.value || "",
helperText: fields.firstname.touched && fields.firstname.error,
error: fields.firstname.touched && Boolean(fields.firstname.error),
}}
onChange={(e) => setSettingsField("firstname", e.target.value)}
id="firstname"
label="Имя"
{...textFieldProps}
/>
<InputTextfield
TextfieldProps={{
placeholder: "Фамилия",
value: fields.secondname.value || "",
helperText: fields.secondname.touched && fields.secondname.error,
error: fields.secondname.touched && Boolean(fields.secondname.error),
}}
onChange={(e) => setSettingsField("secondname", e.target.value)}
id="secondname"
label="Фамилия"
{...textFieldProps}
/>
<InputTextfield
TextfieldProps={{
placeholder: "Отчество",
value: fields.middlename.value || "",
helperText: fields.middlename.touched && fields.middlename.error,
error: fields.middlename.touched && Boolean(fields.middlename.error),
}}
onChange={(e) => setSettingsField("middlename", e.target.value)}
id="middlename"
label="Отчество"
{...textFieldProps}
/>
<InputTextfield
TextfieldProps={{
placeholder: "ООО Фирма",
value: fields.orgname.value || "",
helperText: fields.orgname.touched && fields.orgname.error,
error: fields.orgname.touched && Boolean(fields.orgname.error),
}}
onChange={(e) => setSettingsField("orgname", e.target.value)}
id="orgname"
label="Название компании"
{...textFieldProps}
/>
<InputTextfield
TextfieldProps={{
placeholder: "username@penahaub.com",
value: fields.email.value || "",
helperText: fields.email.touched && fields.email.error,
error: fields.email.touched && Boolean(fields.email.error),
}}
onChange={(e) => setSettingsField("email", e.target.value)}
id="email"
label="E-mail"
{...textFieldProps}
/>
<InputTextfield
TextfieldProps={{
placeholder: "+7 900 000 00 00",
value: fields.phoneNumber.value || "",
helperText: fields.phoneNumber.touched && fields.phoneNumber.error,
error: fields.phoneNumber.touched && Boolean(fields.phoneNumber.error),
}}
onChange={(e) => setSettingsField("phoneNumber", e.target.value)}
id="phoneNumber"
label="Телефон"
{...textFieldProps}
/>
<PasswordInput
TextfieldProps={{
placeholder: "Не менее 8 символов",
value: fields.password.value || "",
helperText: fields.password.touched && fields.password.error,
error: fields.password.touched && Boolean(fields.password.error),
autoComplete: "new-password",
}}
onChange={(e) => setSettingsField("password", e.target.value)}
id="password"
label="Пароль"
{...textFieldProps}
/>
</Box>
)
return(
<Box
sx={{
display: "grid",
gridAutoFlow: upSm ? "column" : "row",
gridTemplateRows: "repeat(4, auto)",
gridAutoColumns: "1fr",
rowGap: "15px",
columnGap: "31px",
flexGrow: 1,
}}
>
<InputTextfield
TextfieldProps={{
placeholder: "Имя",
value: fields.firstname.value || "",
helperText: fields.firstname.touched && fields.firstname.error,
error: fields.firstname.touched && Boolean(fields.firstname.error),
}}
onChange={(e) => setSettingsField("firstname", e.target.value)}
id="firstname"
label="Имя"
{...textFieldProps}
/>
<InputTextfield
TextfieldProps={{
placeholder: "Фамилия",
value: fields.secondname.value || "",
helperText: fields.secondname.touched && fields.secondname.error,
error: fields.secondname.touched && Boolean(fields.secondname.error),
}}
onChange={(e) => setSettingsField("secondname", e.target.value)}
id="secondname"
label="Фамилия"
{...textFieldProps}
/>
<InputTextfield
TextfieldProps={{
placeholder: "Отчество",
value: fields.middlename.value || "",
helperText: fields.middlename.touched && fields.middlename.error,
error: fields.middlename.touched && Boolean(fields.middlename.error),
}}
onChange={(e) => setSettingsField("middlename", e.target.value)}
id="middlename"
label="Отчество"
{...textFieldProps}
/>
<InputTextfield
TextfieldProps={{
placeholder: "ООО Фирма",
value: fields.orgname.value || "",
helperText: fields.orgname.touched && fields.orgname.error,
error: fields.orgname.touched && Boolean(fields.orgname.error),
}}
onChange={(e) => setSettingsField("orgname", e.target.value)}
id="orgname"
label="Название компании"
{...textFieldProps}
/>
<InputTextfield
TextfieldProps={{
placeholder: "username@penahaub.com",
value: fields.email.value || "",
helperText: fields.email.touched && fields.email.error,
error: fields.email.touched && Boolean(fields.email.error),
}}
onChange={(e) => setSettingsField("email", e.target.value)}
id="email"
label="E-mail"
{...textFieldProps}
/>
<InputTextfield
TextfieldProps={{
placeholder: "+7 900 000 00 00",
value: fields.phoneNumber.value || "",
helperText: fields.phoneNumber.touched && fields.phoneNumber.error,
error: fields.phoneNumber.touched && Boolean(fields.phoneNumber.error),
}}
onChange={(e) => setSettingsField("phoneNumber", e.target.value)}
id="phoneNumber"
label="Телефон"
{...textFieldProps}
/>
<PasswordInput
TextfieldProps={{
placeholder: "Не менее 8 символов",
value: fields.password.value || "",
helperText: fields.password.touched && fields.password.error,
error: fields.password.touched && Boolean(fields.password.error),
autoComplete: "new-password",
}}
onChange={(e) => setSettingsField("password", e.target.value)}
id="password"
label="Пароль"
{...textFieldProps}
/>
</Box>
)
}

@ -1,68 +1,68 @@
import { devlog } from "@frontend/kitui";
import { devlog } from "@frontend/kitui"
import { verification } from "@root/api/verification";
import { verification } from "@root/api/verification"
import {
setVerificationStatus,
setVerificationType,
setComment,
setDocumentUrl,
} from "@root/stores/user";
import { VerificationStatus } from "@root/model/account";
setVerificationStatus,
setVerificationType,
setComment,
setDocumentUrl,
} from "@root/stores/user"
import { VerificationStatus } from "@root/model/account"
import { DOCUMENT_TYPE_MAP } from "@root/constants/documentTypeMap";
import { DOCUMENT_TYPE_MAP } from "@root/constants/documentTypeMap"
import type { Verification } from "@root/model/auth";
import type { Verification } from "@root/model/auth"
const updateVerificationStatus = (verification: Verification) => {
if (!verification) {
setVerificationStatus(VerificationStatus.NOT_VERIFICATED);
if (!verification) {
setVerificationStatus(VerificationStatus.NOT_VERIFICATED)
return;
}
return
}
if (verification.accepted) {
setVerificationStatus(VerificationStatus.VERIFICATED);
if (verification.accepted) {
setVerificationStatus(VerificationStatus.VERIFICATED)
return;
}
return
}
if (
(!verification.accepted && !verification.files?.length) ||
if (
(!verification.accepted && !verification.files?.length) ||
(!verification.accepted && !verification.comment)
) {
setVerificationStatus(VerificationStatus.NOT_VERIFICATED);
) {
setVerificationStatus(VerificationStatus.NOT_VERIFICATED)
return;
}
return
}
if (verification.files?.length && !verification.comment) {
setVerificationStatus(VerificationStatus.WAITING);
if (verification.files?.length && !verification.comment) {
setVerificationStatus(VerificationStatus.WAITING)
return;
}
return
}
setVerificationStatus(VerificationStatus.NOT_VERIFICATED);
};
setVerificationStatus(VerificationStatus.NOT_VERIFICATED)
}
export const verify = async (id: string) => {
const [verificationResult, verificationError] = await verification(id);
const [verificationResult, verificationError] = await verification(id)
if (verificationError) {
setVerificationStatus(VerificationStatus.NOT_VERIFICATED);
if (verificationError) {
setVerificationStatus(VerificationStatus.NOT_VERIFICATED)
devlog("Error fetching user", verificationError);
devlog("Error fetching user", verificationError)
return;
}
return
}
if (verificationResult) {
updateVerificationStatus(verificationResult);
setVerificationType(verificationResult.status);
setComment(verificationResult.comment);
verificationResult.files.forEach((file) =>
setDocumentUrl(DOCUMENT_TYPE_MAP[file.name], file.url)
);
if (verificationResult) {
updateVerificationStatus(verificationResult)
setVerificationType(verificationResult.status)
setComment(verificationResult.comment)
verificationResult.files.forEach((file) =>
setDocumentUrl(DOCUMENT_TYPE_MAP[file.name], file.url)
)
devlog("User", verificationResult);
}
};
devlog("User", verificationResult)
}
}

@ -1,80 +1,80 @@
import { Box, IconButton, Typography, useMediaQuery, useTheme } from "@mui/material";
import SectionWrapper from "@components/SectionWrapper";
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import TotalPrice from "@components/TotalPrice";
import CustomWrapper from "./CustomWrapper";
import { useCart } from "@root/utils/hooks/useCart";
import { useLocation } from "react-router-dom";
import { usePrevLocation } from "@root/utils/hooks/handleCustomBackNavigation";
import { handleComponentError } from "@root/utils/handleComponentError";
import { withErrorBoundary } from "react-error-boundary";
import { Box, IconButton, Typography, useMediaQuery, useTheme } from "@mui/material"
import SectionWrapper from "@components/SectionWrapper"
import ArrowBackIcon from "@mui/icons-material/ArrowBack"
import TotalPrice from "@components/TotalPrice"
import CustomWrapper from "./CustomWrapper"
import { useCart } from "@root/utils/hooks/useCart"
import { useLocation } from "react-router-dom"
import { usePrevLocation } from "@root/utils/hooks/handleCustomBackNavigation"
import { handleComponentError } from "@root/utils/handleComponentError"
import { withErrorBoundary } from "react-error-boundary"
function Cart() {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const isMobile = useMediaQuery(theme.breakpoints.down(550));
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const cart = useCart();
const location = useLocation();
const theme = useTheme()
const upMd = useMediaQuery(theme.breakpoints.up("md"))
const isMobile = useMediaQuery(theme.breakpoints.down(550))
const isTablet = useMediaQuery(theme.breakpoints.down(1000))
const cart = useCart()
const location = useLocation()
const totalPriceBeforeDiscounts = cart.priceBeforeDiscounts;
const totalPriceAfterDiscounts = cart.priceAfterDiscounts;
const totalPriceBeforeDiscounts = cart.priceBeforeDiscounts
const totalPriceAfterDiscounts = cart.priceAfterDiscounts
const handleCustomBackNavigation = usePrevLocation(location);
const handleCustomBackNavigation = usePrevLocation(location)
return (
<SectionWrapper
maxWidth="lg"
sx={{
mt: upMd ? "25px" : "20px",
px: isTablet ? (upMd ? "40px" : "18px") : "20px",
mb: upMd ? "70px" : "37px",
}}
>
<Box
sx={{
mt: "20px",
mb: upMd ? "40px" : "20px",
display: "flex",
alignItems: "center",
gap: "10px",
}}
>
{isMobile && (
<IconButton onClick={handleCustomBackNavigation} sx={{ p: 0, height: "28px", width: "28px", color: "black" }}>
<ArrowBackIcon />
</IconButton>
)}
<Typography
sx={{
fontSize: isMobile ? "24px" : "36px",
fontWeight: "500",
}}
>
return (
<SectionWrapper
maxWidth="lg"
sx={{
mt: upMd ? "25px" : "20px",
px: isTablet ? (upMd ? "40px" : "18px") : "20px",
mb: upMd ? "70px" : "37px",
}}
>
<Box
sx={{
mt: "20px",
mb: upMd ? "40px" : "20px",
display: "flex",
alignItems: "center",
gap: "10px",
}}
>
{isMobile && (
<IconButton onClick={handleCustomBackNavigation} sx={{ p: 0, height: "28px", width: "28px", color: "black" }}>
<ArrowBackIcon />
</IconButton>
)}
<Typography
sx={{
fontSize: isMobile ? "24px" : "36px",
fontWeight: "500",
}}
>
Корзина
</Typography>
</Box>
<Box
sx={{
mt: upMd ? "27px" : "10px",
}}
>
{cart.services.map((serviceData, index) => (
<CustomWrapper
key={serviceData.serviceKey}
serviceData={serviceData}
first={index === 0}
last={index === cart.services.length - 1}
/>
))}
</Box>
<TotalPrice priceBeforeDiscounts={totalPriceBeforeDiscounts} priceAfterDiscounts={totalPriceAfterDiscounts} />
</SectionWrapper>
);
</Typography>
</Box>
<Box
sx={{
mt: upMd ? "27px" : "10px",
}}
>
{cart.services.map((serviceData, index) => (
<CustomWrapper
key={serviceData.serviceKey}
serviceData={serviceData}
first={index === 0}
last={index === cart.services.length - 1}
/>
))}
</Box>
<TotalPrice priceBeforeDiscounts={totalPriceBeforeDiscounts} priceAfterDiscounts={totalPriceAfterDiscounts} />
</SectionWrapper>
)
}
export default withErrorBoundary(Cart, {
fallback: <Typography mt="8px" textAlign="center">Ошибка при отображении корзины</Typography>,
onError: handleComponentError,
fallback: <Typography mt="8px" textAlign="center">Ошибка при отображении корзины</Typography>,
onError: handleComponentError,
})

@ -1,21 +1,21 @@
import { useState } from "react";
import { Box, SvgIcon, Typography, useMediaQuery, useTheme } from "@mui/material";
import ExpandIcon from "@components/icons/ExpandIcon";
import ClearIcon from "@mui/icons-material/Clear";
import { currencyFormatter } from "@root/utils/currencyFormatter";
import { removeTariffFromCart } from "@root/stores/user";
import { enqueueSnackbar } from "notistack";
import { CloseButton, ServiceCartData, getMessageFromFetchError } from "@frontend/kitui";
import { useState } from "react"
import { Box, SvgIcon, Typography, useMediaQuery, useTheme } from "@mui/material"
import ExpandIcon from "@components/icons/ExpandIcon"
import ClearIcon from "@mui/icons-material/Clear"
import { currencyFormatter } from "@root/utils/currencyFormatter"
import { removeTariffFromCart } from "@root/stores/user"
import { enqueueSnackbar } from "notistack"
import { CloseButton, ServiceCartData, getMessageFromFetchError } from "@frontend/kitui"
import type { MouseEvent } from "react";
import CustomTariffAccordion from "@root/components/CustomTariffAccordion";
import type { MouseEvent } from "react"
import CustomTariffAccordion from "@root/components/CustomTariffAccordion"
const name: Record<string, string> = {
templategen: "Шаблонизатор",
squiz: "Опросник",
reducer: "Сокращатель ссылок",
custom: "Кастомные тарифы",
};
templategen: "Шаблонизатор",
squiz: "Опросник",
reducer: "Сокращатель ссылок",
custom: "Кастомные тарифы",
}
interface Props {
serviceData: ServiceCartData;
@ -24,177 +24,177 @@ interface Props {
}
export default function CustomWrapper({ serviceData, last, first }: Props) {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const upSm = useMediaQuery(theme.breakpoints.up("sm"));
const [isExpanded, setIsExpanded] = useState<boolean>(false);
const theme = useTheme()
const upMd = useMediaQuery(theme.breakpoints.up("md"))
const upSm = useMediaQuery(theme.breakpoints.up("sm"))
const [isExpanded, setIsExpanded] = useState<boolean>(false)
function handleItemDeleteClick(tariffId: string) {
removeTariffFromCart(tariffId)
.then(() => {
enqueueSnackbar("Тариф удален");
})
.catch((error) => {
const message = getMessageFromFetchError(error);
if (message) enqueueSnackbar(message);
});
}
function handleItemDeleteClick(tariffId: string) {
removeTariffFromCart(tariffId)
.then(() => {
enqueueSnackbar("Тариф удален")
})
.catch((error) => {
const message = getMessageFromFetchError(error)
if (message) enqueueSnackbar(message)
})
}
const deleteService = async (event: MouseEvent<HTMLButtonElement>) => {
event.stopPropagation();
const deleteService = async (event: MouseEvent<HTMLButtonElement>) => {
event.stopPropagation()
setIsExpanded(false);
setIsExpanded(false)
for (const { id } of serviceData.tariffs) {
try {
await removeTariffFromCart(id);
} catch {}
}
for (const { id } of serviceData.tariffs) {
try {
await removeTariffFromCart(id)
} catch {}
}
enqueueSnackbar("Тарифы удалены");
};
enqueueSnackbar("Тарифы удалены")
}
return (
<Box
sx={{
borderRadius: "12px",
}}
>
<Box
sx={{
backgroundColor: "white",
borderTopLeftRadius: first ? "12px" : "0",
borderTopRightRadius: first ? "12px" : "0",
borderBottomLeftRadius: last ? "12px" : "0",
borderBottomRightRadius: last ? "12px" : "0",
borderBottom: `1px solid ${theme.palette.gray.main}`,
return (
<Box
sx={{
borderRadius: "12px",
}}
>
<Box
sx={{
backgroundColor: "white",
borderTopLeftRadius: first ? "12px" : "0",
borderTopRightRadius: first ? "12px" : "0",
borderBottomLeftRadius: last ? "12px" : "0",
borderBottomRightRadius: last ? "12px" : "0",
borderBottom: `1px solid ${theme.palette.gray.main}`,
...(last && { borderBottom: "none" }),
}}
>
<Box
onClick={() => setIsExpanded((prev) => !prev)}
sx={{
height: "72px",
px: "20px",
display: "flex",
gap: "15px",
alignItems: "center",
justifyContent: "space-between",
cursor: "pointer",
userSelect: "none",
}}
>
<Box
sx={{
width: "50px",
height: "100%",
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<ExpandIcon isExpanded={isExpanded} />
</Box>
<Typography
sx={{
width: "100%",
fontSize: upMd ? "20px" : "16px",
lineHeight: upMd ? undefined : "19px",
fontWeight: 500,
color: theme.palette.text.secondary,
px: 0,
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
{name[serviceData.serviceKey]}
</Typography>
<Box
sx={{
display: "flex",
justifyContent: "flex-end",
height: "100%",
alignItems: "center",
gap: upSm ? "111px" : "17px",
}}
>
<Typography
sx={{
color: theme.palette.gray.dark,
fontSize: upSm ? "20px" : "16px",
fontWeight: 500,
}}
>
{currencyFormatter.format(serviceData.price / 100)}
</Typography>
</Box>
<CloseButton style={{ height: "22 px", width: "22px" }} onClick={deleteService} />
</Box>
{isExpanded &&
...(last && { borderBottom: "none" }),
}}
>
<Box
onClick={() => setIsExpanded((prev) => !prev)}
sx={{
height: "72px",
px: "20px",
display: "flex",
gap: "15px",
alignItems: "center",
justifyContent: "space-between",
cursor: "pointer",
userSelect: "none",
}}
>
<Box
sx={{
width: "50px",
height: "100%",
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<ExpandIcon isExpanded={isExpanded} />
</Box>
<Typography
sx={{
width: "100%",
fontSize: upMd ? "20px" : "16px",
lineHeight: upMd ? undefined : "19px",
fontWeight: 500,
color: theme.palette.text.secondary,
px: 0,
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
{name[serviceData.serviceKey]}
</Typography>
<Box
sx={{
display: "flex",
justifyContent: "flex-end",
height: "100%",
alignItems: "center",
gap: upSm ? "111px" : "17px",
}}
>
<Typography
sx={{
color: theme.palette.gray.dark,
fontSize: upSm ? "20px" : "16px",
fontWeight: 500,
}}
>
{currencyFormatter.format(serviceData.price / 100)}
</Typography>
</Box>
<CloseButton style={{ height: "22 px", width: "22px" }} onClick={deleteService} />
</Box>
{isExpanded &&
serviceData.tariffs.map((tariff) => {
const privilege = tariff.privileges[0];
const privilege = tariff.privileges[0]
return tariff.privileges.length > 1 ? (
<CustomTariffAccordion key={tariff.id} tariffCartData={tariff} />
) : (
<Box
key={tariff.id + privilege.privilegeId}
sx={{
px: "20px",
py: upMd ? "25px" : undefined,
pt: upMd ? undefined : "15px",
pb: upMd ? undefined : "25px",
backgroundColor: "#F1F2F6",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
gap: "15px",
}}
>
<Typography
sx={{
fontSize: upMd ? undefined : "16px",
lineHeight: upMd ? undefined : "19px",
color: theme.palette.gray.dark,
width: "100%",
}}
>
{privilege.description}
</Typography>
<Box
sx={{
width: upSm ? "140px" : "123px",
marginRight: upSm ? "65px" : 0,
}}
>
<Typography
sx={{
color: theme.palette.gray.dark,
fontSize: upSm ? "20px" : "16px",
fontWeight: 500,
}}
>
{currencyFormatter.format(tariff.price / 100)}
</Typography>
</Box>
<Box
sx={{
cursor: "pointer",
width: "35px",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
onClick={() => handleItemDeleteClick(tariff.id)}
>
<SvgIcon component={ClearIcon} sx={{ fill: theme.palette.purple.main }} />
</Box>
</Box>
);
return tariff.privileges.length > 1 ? (
<CustomTariffAccordion key={tariff.id} tariffCartData={tariff} />
) : (
<Box
key={tariff.id + privilege.privilegeId}
sx={{
px: "20px",
py: upMd ? "25px" : undefined,
pt: upMd ? undefined : "15px",
pb: upMd ? undefined : "25px",
backgroundColor: "#F1F2F6",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
gap: "15px",
}}
>
<Typography
sx={{
fontSize: upMd ? undefined : "16px",
lineHeight: upMd ? undefined : "19px",
color: theme.palette.gray.dark,
width: "100%",
}}
>
{privilege.description}
</Typography>
<Box
sx={{
width: upSm ? "140px" : "123px",
marginRight: upSm ? "65px" : 0,
}}
>
<Typography
sx={{
color: theme.palette.gray.dark,
fontSize: upSm ? "20px" : "16px",
fontWeight: 500,
}}
>
{currencyFormatter.format(tariff.price / 100)}
</Typography>
</Box>
<Box
sx={{
cursor: "pointer",
width: "35px",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
onClick={() => handleItemDeleteClick(tariff.id)}
>
<SvgIcon component={ClearIcon} sx={{ fill: theme.palette.purple.main }} />
</Box>
</Box>
)
})}
</Box>
</Box>
);
</Box>
</Box>
)
}

@ -1,24 +1,24 @@
import { Box } from "@mui/material";
import { Box } from "@mui/material"
import CustomAccordion from "@components/CustomAccordion";
import { cardShadow } from "@root/utils/theme";
import CustomAccordion from "@components/CustomAccordion"
import { cardShadow } from "@root/utils/theme"
interface Props {
content: [string, string][];
}
export default function AccordionWrapper({ content }: Props) {
return (
<Box
sx={{
overflow: "hidden",
borderRadius: "12px",
boxShadow: cardShadow,
}}
>
{content.map((accordionItem, index) => (
<CustomAccordion key={index} header={accordionItem[0]} text={accordionItem[1]} />
))}
</Box>
);
return (
<Box
sx={{
overflow: "hidden",
borderRadius: "12px",
boxShadow: cardShadow,
}}
>
{content.map((accordionItem, index) => (
<CustomAccordion key={index} header={accordionItem[0]} text={accordionItem[1]} />
))}
</Box>
)
}

@ -1,170 +1,170 @@
import { IconButton, useMediaQuery, useTheme } from "@mui/material";
import { Select } from "@root/components/Select";
import { Box } from "@mui/material";
import { Typography } from "@mui/material";
import { useState } from "react";
import SectionWrapper from "../../components/SectionWrapper";
import AccordionWrapper from "./AccordionWrapper";
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import { Tabs } from "@root/components/Tabs";
import { useHistoryTracker } from "@root/utils/hooks/useHistoryTracker";
import { IconButton, useMediaQuery, useTheme } from "@mui/material"
import { Select } from "@root/components/Select"
import { Box } from "@mui/material"
import { Typography } from "@mui/material"
import { useState } from "react"
import SectionWrapper from "../../components/SectionWrapper"
import AccordionWrapper from "./AccordionWrapper"
import ArrowBackIcon from "@mui/icons-material/ArrowBack"
import { Tabs } from "@root/components/Tabs"
import { useHistoryTracker } from "@root/utils/hooks/useHistoryTracker"
const subPages = ["Pena hub", "Шаблоны", "Опросы", "Ссылки", "Финансовые", "Юридические", "Юридические лица"];
const subPages = ["Pena hub", "Шаблоны", "Опросы", "Ссылки", "Финансовые", "Юридические", "Юридические лица"]
export default function Faq() {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const isMobile = useMediaQuery(theme.breakpoints.down(550));
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const [tabIndex, setTabIndex] = useState<number>(0);
const [selectedItem, setSelectedItem] = useState<number>(0);
const theme = useTheme()
const upMd = useMediaQuery(theme.breakpoints.up("md"))
const isMobile = useMediaQuery(theme.breakpoints.down(550))
const isTablet = useMediaQuery(theme.breakpoints.down(1000))
const [tabIndex, setTabIndex] = useState<number>(0)
const [selectedItem, setSelectedItem] = useState<number>(0)
const handleCustomBackNavigation = useHistoryTracker();
const handleCustomBackNavigation = useHistoryTracker()
return (
<SectionWrapper
maxWidth="lg"
sx={{
mt: upMd ? "25px" : "20px",
px: isTablet ? (isMobile ? "18px" : "40px") : "20px",
mb: upMd ? "70px" : "37px",
}}
>
<Box
sx={{
mt: "20px",
mb: isTablet ? "38px" : "20px",
display: "flex",
alignItems: "center",
gap: "10px",
}}
>
{isMobile && (
<IconButton onClick={handleCustomBackNavigation} sx={{ p: 0, height: "28px", width: "28px", color: "black" }}>
<ArrowBackIcon />
</IconButton>
)}
<Typography
sx={{
fontSize: isMobile ? "24px" : "36px",
fontWeight: "500",
}}
>
return (
<SectionWrapper
maxWidth="lg"
sx={{
mt: upMd ? "25px" : "20px",
px: isTablet ? (isMobile ? "18px" : "40px") : "20px",
mb: upMd ? "70px" : "37px",
}}
>
<Box
sx={{
mt: "20px",
mb: isTablet ? "38px" : "20px",
display: "flex",
alignItems: "center",
gap: "10px",
}}
>
{isMobile && (
<IconButton onClick={handleCustomBackNavigation} sx={{ p: 0, height: "28px", width: "28px", color: "black" }}>
<ArrowBackIcon />
</IconButton>
)}
<Typography
sx={{
fontSize: isMobile ? "24px" : "36px",
fontWeight: "500",
}}
>
Вопросы и ответы
</Typography>
</Box>
<Box sx={{ marginBottom: isMobile ? "20px" : "0px" }}>
{isMobile ? (
<Select items={subPages} selectedItem={selectedItem} setSelectedItem={setSelectedItem} />
) : (
<Tabs items={subPages} selectedItem={tabIndex} setSelectedItem={setTabIndex} />
)}
</Box>
</Typography>
</Box>
<Box sx={{ marginBottom: isMobile ? "20px" : "0px" }}>
{isMobile ? (
<Select items={subPages} selectedItem={selectedItem} setSelectedItem={setSelectedItem} />
) : (
<Tabs items={subPages} selectedItem={tabIndex} setSelectedItem={setTabIndex} />
)}
</Box>
<TabPanel value={tabIndex} index={0} mt={upMd ? "42px" : "10px"}>
<AccordionWrapper
content={[
[
"Как зарегистрироваться?",
"Выберите план выше, чтобы начать. Мы попросим вас сообщить несколько основных деталей. Тогда ты в деле.",
],
[
"Какие функции я могу использовать бесплатно?",
"Выберите план выше, чтобы начать. Мы попросим вас сообщить несколько основных деталей. Тогда ты в деле.",
],
[
"Есть ли ограничение на количество ответов, которые я могу собрать?",
"Выберите план выше, чтобы начать. Мы попросим вас сообщить несколько основных деталей. Тогда ты в деле.",
],
[
"Нужно ли мне уметь кодировать?",
"Выберите план выше, чтобы начать. Мы попросим вас сообщить несколько основных деталей. Тогда ты в деле.",
],
[
"Работают ли шрифты на всех устройствах?",
"Выберите план выше, чтобы начать. Мы попросим вас сообщить несколько основных деталей. Тогда ты в деле.",
],
[
"Что я могу сделать со своими данными после их сбора?",
"Выберите план выше, чтобы начать. Мы попросим вас сообщить несколько основных деталей. Тогда ты в деле.",
],
]}
/>
</TabPanel>
<TabPanel value={tabIndex} index={1} mt={upMd ? "27px" : "10px"}>
<AccordionWrapper
content={[
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
]}
/>
</TabPanel>
<TabPanel value={tabIndex} index={2} mt={upMd ? "27px" : "10px"}>
<AccordionWrapper
content={[
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
]}
/>
</TabPanel>
<TabPanel value={tabIndex} index={3} mt={upMd ? "27px" : "10px"}>
<AccordionWrapper
content={[
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
]}
/>
</TabPanel>
<TabPanel value={tabIndex} index={4} mt={upMd ? "27px" : "10px"}>
<AccordionWrapper
content={[
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
]}
/>
</TabPanel>
<TabPanel value={tabIndex} index={5} mt={upMd ? "27px" : "10px"}>
<AccordionWrapper
content={[
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
]}
/>
</TabPanel>
<TabPanel value={tabIndex} index={6} mt={upMd ? "27px" : "10px"}>
<AccordionWrapper
content={[
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
]}
/>
</TabPanel>
</SectionWrapper>
);
<TabPanel value={tabIndex} index={0} mt={upMd ? "42px" : "10px"}>
<AccordionWrapper
content={[
[
"Как зарегистрироваться?",
"Выберите план выше, чтобы начать. Мы попросим вас сообщить несколько основных деталей. Тогда ты в деле.",
],
[
"Какие функции я могу использовать бесплатно?",
"Выберите план выше, чтобы начать. Мы попросим вас сообщить несколько основных деталей. Тогда ты в деле.",
],
[
"Есть ли ограничение на количество ответов, которые я могу собрать?",
"Выберите план выше, чтобы начать. Мы попросим вас сообщить несколько основных деталей. Тогда ты в деле.",
],
[
"Нужно ли мне уметь кодировать?",
"Выберите план выше, чтобы начать. Мы попросим вас сообщить несколько основных деталей. Тогда ты в деле.",
],
[
"Работают ли шрифты на всех устройствах?",
"Выберите план выше, чтобы начать. Мы попросим вас сообщить несколько основных деталей. Тогда ты в деле.",
],
[
"Что я могу сделать со своими данными после их сбора?",
"Выберите план выше, чтобы начать. Мы попросим вас сообщить несколько основных деталей. Тогда ты в деле.",
],
]}
/>
</TabPanel>
<TabPanel value={tabIndex} index={1} mt={upMd ? "27px" : "10px"}>
<AccordionWrapper
content={[
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
]}
/>
</TabPanel>
<TabPanel value={tabIndex} index={2} mt={upMd ? "27px" : "10px"}>
<AccordionWrapper
content={[
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
]}
/>
</TabPanel>
<TabPanel value={tabIndex} index={3} mt={upMd ? "27px" : "10px"}>
<AccordionWrapper
content={[
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
]}
/>
</TabPanel>
<TabPanel value={tabIndex} index={4} mt={upMd ? "27px" : "10px"}>
<AccordionWrapper
content={[
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
]}
/>
</TabPanel>
<TabPanel value={tabIndex} index={5} mt={upMd ? "27px" : "10px"}>
<AccordionWrapper
content={[
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
]}
/>
</TabPanel>
<TabPanel value={tabIndex} index={6} mt={upMd ? "27px" : "10px"}>
<AccordionWrapper
content={[
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
["Placeholder", "Placeholder"],
]}
/>
</TabPanel>
</SectionWrapper>
)
}
interface TabPanelProps {
@ -175,9 +175,9 @@ interface TabPanelProps {
}
function TabPanel({ index, value, children, mt }: TabPanelProps) {
return (
<Box hidden={index !== value} sx={{ mt }}>
{children}
</Box>
);
return (
<Box hidden={index !== value} sx={{ mt }}>
{children}
</Box>
)
}

@ -1,10 +1,10 @@
import { Box, IconButton, Typography, useMediaQuery, useTheme } from "@mui/material";
import CustomAccordion from "@components/CustomAccordion";
import { Box, IconButton, Typography, useMediaQuery, useTheme } from "@mui/material"
import CustomAccordion from "@components/CustomAccordion"
import File from "@components/icons/File"
import { getDeclension } from "@utils/declension"
import { enqueueSnackbar } from "notistack";
import { addTariffToCart } from "@root/stores/user";
import { getMessageFromFetchError } from "@frontend/kitui";
import { enqueueSnackbar } from "notistack"
import { addTariffToCart } from "@root/stores/user"
import { getMessageFromFetchError } from "@frontend/kitui"
export type History = {
title: string;
@ -25,216 +25,216 @@ interface AccordionWrapperProps {
}
export default function AccordionWrapper({ content, last, first, createdAt }: AccordionWrapperProps) {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const upSm = useMediaQuery(theme.breakpoints.up("sm"));
const isTablet = useMediaQuery(theme.breakpoints.down(900));
const isMobile = useMediaQuery(theme.breakpoints.down(560));
const theme = useTheme()
const upMd = useMediaQuery(theme.breakpoints.up("md"))
const upSm = useMediaQuery(theme.breakpoints.up("sm"))
const isTablet = useMediaQuery(theme.breakpoints.down(900))
const isMobile = useMediaQuery(theme.breakpoints.down(560))
const valuesByKey: any = {};
content[0].Value[0].forEach((item) => {
valuesByKey[item.Key] = item.Value;
});
console.log(content)
console.log(content[0])
console.log(content[0].Value)
console.log(valuesByKey)
const extractDateFromString = (tariffName: string) => {
const dateMatch = tariffName.match(/\d{4}-\d{2}-\d{2}/);
return dateMatch ? dateMatch[0] : null;
};
const valuesByKey: any = {}
content[0].Value[0].forEach((item) => {
valuesByKey[item.Key] = item.Value
})
console.log(content)
console.log(content[0])
console.log(content[0].Value)
console.log(valuesByKey)
const extractDateFromString = (tariffName: string) => {
const dateMatch = tariffName.match(/\d{4}-\d{2}-\d{2}/)
return dateMatch ? dateMatch[0] : null
}
async function handleTariffItemClick(tariffId: string) {
const { patchCartError } = await addTariffToCart(tariffId)
if (patchCartError) {
enqueueSnackbar(patchCartError);
} else {
enqueueSnackbar("Тариф добавлен в корзину");
}
}
async function handleTariffItemClick(tariffId: string) {
const { patchCartError } = await addTariffToCart(tariffId)
if (patchCartError) {
enqueueSnackbar(patchCartError)
} else {
enqueueSnackbar("Тариф добавлен в корзину")
}
}
return (
<Box
sx={{
borderRadius: "12px",
}}
>
<CustomAccordion
last={last}
first={first}
divide
text={valuesByKey.privileges.map((e:KeyValue[]) => (
<Typography
key={valuesByKey.id}
>
{e[1].Value} - {e[5].Value} {getDeclension(Number(e[5].Value), e[7].Value.toString())}
</Typography>)
)}
header={
<>
<Box
sx={{
width: "100%",
height: upMd ? "72px" : undefined,
padding: "20px 20px 20px 0",
display: "flex",
justifyContent: "space-between",
cursor: "pointer",
userSelect: "none",
gap: "20px",
alignItems: upSm ? "center" : undefined,
flexDirection: upSm ? undefined : "column",
}}
>
<Box
sx={{
display: "flex",
alignItems: upSm ? "center" : undefined,
justifyContent: "space-between",
flexDirection: upSm ? undefined : "column",
gap: upMd ? "51px" : "10px",
}}
>
<Typography
sx={{
width: "110px",
fontSize: upMd ? "20px" : "18px",
lineHeight: upMd ? undefined : "19px",
fontWeight: 500,
color: valuesByKey.expired ? theme.palette.text.disabled : theme.palette.text.secondary,
px: 0,
whiteSpace: "nowrap",
}}
>
{createdAt}
</Typography>
return (
<Box
sx={{
borderRadius: "12px",
}}
>
<CustomAccordion
last={last}
first={first}
divide
text={valuesByKey.privileges.map((e:KeyValue[]) => (
<Typography
key={valuesByKey.id}
>
{e[1].Value} - {e[5].Value} {getDeclension(Number(e[5].Value), e[7].Value.toString())}
</Typography>)
)}
header={
<>
<Box
sx={{
width: "100%",
height: upMd ? "72px" : undefined,
padding: "20px 20px 20px 0",
display: "flex",
justifyContent: "space-between",
cursor: "pointer",
userSelect: "none",
gap: "20px",
alignItems: upSm ? "center" : undefined,
flexDirection: upSm ? undefined : "column",
}}
>
<Box
sx={{
display: "flex",
alignItems: upSm ? "center" : undefined,
justifyContent: "space-between",
flexDirection: upSm ? undefined : "column",
gap: upMd ? "51px" : "10px",
}}
>
<Typography
sx={{
width: "110px",
fontSize: upMd ? "20px" : "18px",
lineHeight: upMd ? undefined : "19px",
fontWeight: 500,
color: valuesByKey.expired ? theme.palette.text.disabled : theme.palette.text.secondary,
px: 0,
whiteSpace: "nowrap",
}}
>
{createdAt}
</Typography>
<Typography
title={valuesByKey.iscustom ? "Кастомный тариф" : valuesByKey.name}
sx={{
fontSize: upMd ? "18px" : "16px",
lineHeight: upMd ? undefined : "19px",
fontWeight: 500,
color: valuesByKey.expired ? theme.palette.text.disabled : theme.palette.gray.dark,
px: 0,
width: '200px',
maxWidth: '200px',
overflow: 'hidden',
textOverflow: 'ellipsis'
}}
>
{valuesByKey.iscustom ? "Кастомный тариф" : valuesByKey.name}
</Typography>
</Box>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
flexFlow: "1",
flexBasis: "60%",
}}
>
<Box display="flex" width="100%" justifyContent="space-between">
<Typography
sx={{
display: upMd ? undefined : "none",
fontSize: upMd ? "18px" : "16px",
lineHeight: upMd ? undefined : "19px",
fontWeight: 400,
color: valuesByKey.expired ? theme.palette.text.disabled : theme.palette.gray.dark,
px: 0,
}}
title={`>Способ оплаты: ${valuesByKey.payMethod}</Typography>}`}
>
{valuesByKey.payMethod && <Typography
sx={{
maxWidth: '300px',
width: '300px',
overflow: 'hidden',
textOverflow: 'ellipsis'
}}
>Способ оплаты: {valuesByKey.payMethod}</Typography>}
</Typography>
<Box
sx={{
display: "flex",
height: "100%",
alignItems: "center",
gap: upSm ? "111px" : "17px",
width: "100%",
maxWidth: isTablet ? null : "160px",
}}
>
<Typography
sx={{
marginLeft: isTablet ? (isMobile ? null : "auto") : null,
color: valuesByKey.expired ? theme.palette.text.disabled : theme.palette.gray.dark,
fontSize: upSm ? "20px" : "16px",
fontWeight: 500,
textAlign: "left",
}}
>
{Number(content[1].Value) / 100 ? Number(content[1].Value) / 100 : "nodata"} руб.
</Typography>
</Box>
</Box>
{!isMobile &&
<Typography
title={valuesByKey.iscustom ? "Кастомный тариф" : valuesByKey.name}
sx={{
fontSize: upMd ? "18px" : "16px",
lineHeight: upMd ? undefined : "19px",
fontWeight: 500,
color: valuesByKey.expired ? theme.palette.text.disabled : theme.palette.gray.dark,
px: 0,
width: "200px",
maxWidth: "200px",
overflow: "hidden",
textOverflow: "ellipsis"
}}
>
{valuesByKey.iscustom ? "Кастомный тариф" : valuesByKey.name}
</Typography>
</Box>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
flexFlow: "1",
flexBasis: "60%",
}}
>
<Box display="flex" width="100%" justifyContent="space-between">
<Typography
sx={{
display: upMd ? undefined : "none",
fontSize: upMd ? "18px" : "16px",
lineHeight: upMd ? undefined : "19px",
fontWeight: 400,
color: valuesByKey.expired ? theme.palette.text.disabled : theme.palette.gray.dark,
px: 0,
}}
title={`>Способ оплаты: ${valuesByKey.payMethod}</Typography>}`}
>
{valuesByKey.payMethod && <Typography
sx={{
maxWidth: "300px",
width: "300px",
overflow: "hidden",
textOverflow: "ellipsis"
}}
>Способ оплаты: {valuesByKey.payMethod}</Typography>}
</Typography>
<Box
sx={{
display: "flex",
height: "100%",
alignItems: "center",
gap: upSm ? "111px" : "17px",
width: "100%",
maxWidth: isTablet ? null : "160px",
}}
>
<Typography
sx={{
marginLeft: isTablet ? (isMobile ? null : "auto") : null,
color: valuesByKey.expired ? theme.palette.text.disabled : theme.palette.gray.dark,
fontSize: upSm ? "20px" : "16px",
fontWeight: 500,
textAlign: "left",
}}
>
{Number(content[1].Value) / 100 ? Number(content[1].Value) / 100 : "nodata"} руб.
</Typography>
</Box>
</Box>
{!isMobile &&
<IconButton
title="Добавить в корзину тариф"
onClick={(e) => {
e.stopPropagation()
handleTariffItemClick(valuesByKey.id)
}}
sx={{
ml: "20px",
bgcolor:"#EEE4FC",
stroke: "#7E2AEA",
borderRadius: 2,
"&:hover": {
bgcolor:"#7E2AEA",
stroke: "white",
},
"&:active": {
bgcolor:"black",
stroke: "white",
}
}}
title="Добавить в корзину тариф"
onClick={(e) => {
e.stopPropagation()
handleTariffItemClick(valuesByKey.id)
}}
sx={{
ml: "20px",
bgcolor:"#EEE4FC",
stroke: "#7E2AEA",
borderRadius: 2,
"&:hover": {
bgcolor:"#7E2AEA",
stroke: "white",
},
"&:active": {
bgcolor:"black",
stroke: "white",
}
}}
>
<File></File>
<File></File>
</IconButton>
}
</Box>
</Box>
{isMobile &&
}
</Box>
</Box>
{isMobile &&
<IconButton
title="Добавить в корзину тариф"
onClick={(e) => {
e.stopPropagation()
handleTariffItemClick(valuesByKey.id)
}}
sx={{
mr: "10px",
bgcolor:"#EEE4FC",
stroke: "#7E2AEA",
borderRadius: 2,
"&:hover": {
bgcolor:"#7E2AEA",
stroke: "white",
},
"&:active": {
bgcolor:"black",
stroke: "white",
}
}}
title="Добавить в корзину тариф"
onClick={(e) => {
e.stopPropagation()
handleTariffItemClick(valuesByKey.id)
}}
sx={{
mr: "10px",
bgcolor:"#EEE4FC",
stroke: "#7E2AEA",
borderRadius: 2,
"&:hover": {
bgcolor:"#7E2AEA",
stroke: "white",
},
"&:active": {
bgcolor:"black",
stroke: "white",
}
}}
>
<File></File>
<File></File>
</IconButton>
}
</>
}
/>
</Box>
);
}
</>
}
/>
</Box>
)
}

@ -1,129 +1,129 @@
import type { History } from "./AccordionWrapper";
import type { History } from "./AccordionWrapper"
const PAYMENT_HISTORY: History[] = [
{
date: "28.05.2022",
title: "Шаблонизатор",
payMethod: "QIWI Кошелек",
info: "3 190 руб.",
description: "Дата действия приобретенной лицензии (в формате дд.мм.гггг-дд.мм.гггг) Или же объем",
},
{
date: "28.05.2022",
title: "Сокращатель ссылок",
payMethod: "Юмани",
info: "2 190 руб.",
description: "Дата действия приобретенной лицензии (в формате дд.мм.гггг-дд.мм.гггг) Или же объем",
},
{
date: "28.05.2022",
title: "Шаблонизатор",
payMethod: "QIWI Кошелек",
info: "1 190 руб.",
description: "Дата действия приобретенной лицензии (в формате дд.мм.гггг-дд.мм.гггг) Или же объем",
},
{
date: "08.04.2022",
title: "Шаблонизатор",
payMethod: "QIWI Кошелек",
info: "3 190 руб.",
description: "Дата действия приобретенной лицензии (в формате дд.мм.гггг-дд.мм.гггг) Или же объем",
},
{
date: "28.05.2022",
title: "Сокращатель ссылок",
payMethod: "Юмани",
info: "5 190 руб.",
description: "Дата действия приобретенной лицензии (в формате дд.мм.гггг-дд.мм.гггг) Или же объем",
},
{
date: "18.03.2022",
title: "Шаблонизатор",
payMethod: "Юмани",
info: "6 190 руб.",
description: "Дата действия приобретенной лицензии (в формате дд.мм.гггг-дд.мм.гггг) Или же объем",
},
];
{
date: "28.05.2022",
title: "Шаблонизатор",
payMethod: "QIWI Кошелек",
info: "3 190 руб.",
description: "Дата действия приобретенной лицензии (в формате дд.мм.гггг-дд.мм.гггг) Или же объем",
},
{
date: "28.05.2022",
title: "Сокращатель ссылок",
payMethod: "Юмани",
info: "2 190 руб.",
description: "Дата действия приобретенной лицензии (в формате дд.мм.гггг-дд.мм.гггг) Или же объем",
},
{
date: "28.05.2022",
title: "Шаблонизатор",
payMethod: "QIWI Кошелек",
info: "1 190 руб.",
description: "Дата действия приобретенной лицензии (в формате дд.мм.гггг-дд.мм.гггг) Или же объем",
},
{
date: "08.04.2022",
title: "Шаблонизатор",
payMethod: "QIWI Кошелек",
info: "3 190 руб.",
description: "Дата действия приобретенной лицензии (в формате дд.мм.гггг-дд.мм.гггг) Или же объем",
},
{
date: "28.05.2022",
title: "Сокращатель ссылок",
payMethod: "Юмани",
info: "5 190 руб.",
description: "Дата действия приобретенной лицензии (в формате дд.мм.гггг-дд.мм.гггг) Или же объем",
},
{
date: "18.03.2022",
title: "Шаблонизатор",
payMethod: "Юмани",
info: "6 190 руб.",
description: "Дата действия приобретенной лицензии (в формате дд.мм.гггг-дд.мм.гггг) Или же объем",
},
]
const PURCHASED_TARIFFS_HISTORY: History[] = [
{
date: "28.05.2022",
title: "Шаблонизатор",
info: "5 000 шаблонов",
description: "Тариф на время/ объем/ кастомный или другая информация",
},
{
date: "27.05.2022",
title: "Опросник",
info: "9 месяцев 1 000 шаблонов",
description: "Тариф на время/ объем/ кастомный или другая информация",
},
{
date: "20.05.2022",
title: "Шаблонизатор",
info: "Безлимит",
description: "Тариф на время/ объем/ кастомный или другая информация",
},
{
date: "08.04.2022",
title: "Опросник",
info: "10 000 шаблонов",
description: "Тариф на время/ объем/ кастомный или другая информация",
},
{
date: "28.05.2022",
title: "Сокращатель ссылок",
info: "3 дня",
description: "Тариф на время/ объем/ кастомный или другая информация",
},
{
date: "18.03.2022",
title: "Шаблонизатор",
info: "9 месяцев 1 000 шаблонов",
description: "Тариф на время/ объем/ кастомный или другая информация",
},
];
{
date: "28.05.2022",
title: "Шаблонизатор",
info: "5 000 шаблонов",
description: "Тариф на время/ объем/ кастомный или другая информация",
},
{
date: "27.05.2022",
title: "Опросник",
info: "9 месяцев 1 000 шаблонов",
description: "Тариф на время/ объем/ кастомный или другая информация",
},
{
date: "20.05.2022",
title: "Шаблонизатор",
info: "Безлимит",
description: "Тариф на время/ объем/ кастомный или другая информация",
},
{
date: "08.04.2022",
title: "Опросник",
info: "10 000 шаблонов",
description: "Тариф на время/ объем/ кастомный или другая информация",
},
{
date: "28.05.2022",
title: "Сокращатель ссылок",
info: "3 дня",
description: "Тариф на время/ объем/ кастомный или другая информация",
},
{
date: "18.03.2022",
title: "Шаблонизатор",
info: "9 месяцев 1 000 шаблонов",
description: "Тариф на время/ объем/ кастомный или другая информация",
},
]
const FINISHED_TARIFFS_HISTORY: History[] = [
{
date: "28.05.2022",
title: "Шаблонизатор",
info: "5 000 шаблонов",
description: "Тариф на время/ объем/ кастомный или другая информация",
},
{
date: "28.05.2022",
title: "Сокращатель ссылок",
info: "10 месяцев",
description: "Тариф на время/ объем/ кастомный или другая информация",
},
{
date: "20.05.2022",
title: "Шаблонизатор",
info: "Безлимит",
description: "Тариф на время/ объем/ кастомный или другая информация",
},
{
date: "08.04.2022",
title: "Опросник",
info: "5 000 шаблонов",
expired: true,
description: "Тариф на время/ объем/ кастомный или другая информация",
},
{
date: "01.03.2022",
title: "Шаблонизатор",
info: "3 дня",
expired: true,
description: "Тариф на время/ объем/ кастомный или другая информация",
},
{
date: "19.02.2022",
title: "Сокращатель ссылок",
info: "9 месяцев 1 000 шаблонов",
expired: true,
description: "Тариф на время/ объем/ кастомный или другая информация",
},
];
{
date: "28.05.2022",
title: "Шаблонизатор",
info: "5 000 шаблонов",
description: "Тариф на время/ объем/ кастомный или другая информация",
},
{
date: "28.05.2022",
title: "Сокращатель ссылок",
info: "10 месяцев",
description: "Тариф на время/ объем/ кастомный или другая информация",
},
{
date: "20.05.2022",
title: "Шаблонизатор",
info: "Безлимит",
description: "Тариф на время/ объем/ кастомный или другая информация",
},
{
date: "08.04.2022",
title: "Опросник",
info: "5 000 шаблонов",
expired: true,
description: "Тариф на время/ объем/ кастомный или другая информация",
},
{
date: "01.03.2022",
title: "Шаблонизатор",
info: "3 дня",
expired: true,
description: "Тариф на время/ объем/ кастомный или другая информация",
},
{
date: "19.02.2022",
title: "Сокращатель ссылок",
info: "9 месяцев 1 000 шаблонов",
expired: true,
description: "Тариф на время/ объем/ кастомный или другая информация",
},
]
export const HISTORY: History[][] = [PAYMENT_HISTORY, PURCHASED_TARIFFS_HISTORY, FINISHED_TARIFFS_HISTORY];
export const HISTORY: History[][] = [PAYMENT_HISTORY, PURCHASED_TARIFFS_HISTORY, FINISHED_TARIFFS_HISTORY]

@ -1,98 +1,98 @@
import { useEffect, useState } from "react";
import { Box, IconButton, Typography, useMediaQuery, useTheme } from "@mui/material";
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import { useEffect, useState } from "react"
import { Box, IconButton, Typography, useMediaQuery, useTheme } from "@mui/material"
import ArrowBackIcon from "@mui/icons-material/ArrowBack"
import SectionWrapper from "@root/components/SectionWrapper";
import { Select } from "@root/components/Select";
import { Tabs } from "@root/components/Tabs";
import SectionWrapper from "@root/components/SectionWrapper"
import { Select } from "@root/components/Select"
import { Tabs } from "@root/components/Tabs"
import AccordionWrapper from "./AccordionWrapper";
import { HISTORY } from "./historyMocks";
import { useHistoryTracker } from "@root/utils/hooks/useHistoryTracker";
import { useHistoryData } from "@root/utils/hooks/useHistoryData";
import { isArray } from "cypress/types/lodash";
import { ErrorBoundary } from "react-error-boundary";
import { handleComponentError } from "@root/utils/handleComponentError";
import AccordionWrapper from "./AccordionWrapper"
import { HISTORY } from "./historyMocks"
import { useHistoryTracker } from "@root/utils/hooks/useHistoryTracker"
import { useHistoryData } from "@root/utils/hooks/useHistoryData"
import { isArray } from "cypress/types/lodash"
import { ErrorBoundary } from "react-error-boundary"
import { handleComponentError } from "@root/utils/handleComponentError"
const subPages = ["Платежи", "Покупки тарифов", "Окончания тарифов"];
const subPages = ["Платежи", "Покупки тарифов", "Окончания тарифов"]
export default function History() {
const [selectedItem, setSelectedItem] = useState<number>(0);
const [selectedItem, setSelectedItem] = useState<number>(0)
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const isMobile = useMediaQuery(theme.breakpoints.down(600));
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const { historyData, error } = useHistoryData();
const theme = useTheme()
const upMd = useMediaQuery(theme.breakpoints.up("md"))
const isMobile = useMediaQuery(theme.breakpoints.down(600))
const isTablet = useMediaQuery(theme.breakpoints.down(1000))
const { historyData, error } = useHistoryData()
const handleCustomBackNavigation = useHistoryTracker();
const handleCustomBackNavigation = useHistoryTracker()
const extractDateFromString = (tariffName: string) => {
const dateMatch = tariffName.match(/\d{4}-\d{2}-\d{2}/);
return dateMatch ? dateMatch[0] : "";
};
const extractDateFromString = (tariffName: string) => {
const dateMatch = tariffName.match(/\d{4}-\d{2}-\d{2}/)
return dateMatch ? dateMatch[0] : ""
}
return (
<SectionWrapper
maxWidth="lg"
sx={{
mt: upMd ? "25px" : "20px",
mb: upMd ? "70px" : "37px",
px: isTablet ? (isTablet ? "18px" : "40px") : "20px",
}}
>
<Box
sx={{
mt: "20px",
mb: isTablet ? "38px" : "20px",
display: "flex",
alignItems: "center",
gap: "10px",
}}
>
{isMobile && (
<IconButton onClick={handleCustomBackNavigation} sx={{ p: 0, height: "28px", width: "28px", color: "black" }}>
<ArrowBackIcon />
</IconButton>
)}
<Typography
sx={{
fontSize: isMobile ? "24px" : "36px",
fontWeight: "500",
}}
>
return (
<SectionWrapper
maxWidth="lg"
sx={{
mt: upMd ? "25px" : "20px",
mb: upMd ? "70px" : "37px",
px: isTablet ? (isTablet ? "18px" : "40px") : "20px",
}}
>
<Box
sx={{
mt: "20px",
mb: isTablet ? "38px" : "20px",
display: "flex",
alignItems: "center",
gap: "10px",
}}
>
{isMobile && (
<IconButton onClick={handleCustomBackNavigation} sx={{ p: 0, height: "28px", width: "28px", color: "black" }}>
<ArrowBackIcon />
</IconButton>
)}
<Typography
sx={{
fontSize: isMobile ? "24px" : "36px",
fontWeight: "500",
}}
>
История
</Typography>
</Box>
{isMobile ? (
<Select items={subPages} selectedItem={selectedItem} setSelectedItem={setSelectedItem} />
) : (
<Tabs items={subPages} selectedItem={selectedItem} setSelectedItem={setSelectedItem} />
)}
<ErrorBoundary
fallback={
<Typography mt="8px">Ошибка загрузки истории</Typography>
}
onError={handleComponentError}
>
{historyData?.records
.filter((e) => {
e.createdAt = extractDateFromString(e.createdAt)
return(!e.isDeleted && e.key === "payCart" && Array.isArray(e.rawDetails[0].Value)
)})
.map(( e, index) => {
return (
<Box key={index} sx={{ mt: index === 0 ? "27px" : "0px" }}>
<AccordionWrapper
first={index === 0}
last={index === historyData?.records.length - 1}
content={e.rawDetails}
key={e.id}
createdAt={e.createdAt}
/>
</Box>
)})}
</ErrorBoundary>
</SectionWrapper>
);
</Typography>
</Box>
{isMobile ? (
<Select items={subPages} selectedItem={selectedItem} setSelectedItem={setSelectedItem} />
) : (
<Tabs items={subPages} selectedItem={selectedItem} setSelectedItem={setSelectedItem} />
)}
<ErrorBoundary
fallback={
<Typography mt="8px">Ошибка загрузки истории</Typography>
}
onError={handleComponentError}
>
{historyData?.records
.filter((e) => {
e.createdAt = extractDateFromString(e.createdAt)
return(!e.isDeleted && e.key === "payCart" && Array.isArray(e.rawDetails[0].Value)
)})
.map(( e, index) => {
return (
<Box key={index} sx={{ mt: index === 0 ? "27px" : "0px" }}>
<AccordionWrapper
first={index === 0}
last={index === historyData?.records.length - 1}
content={e.rawDetails}
key={e.id}
createdAt={e.createdAt}
/>
</Box>
)})}
</ErrorBoundary>
</SectionWrapper>
)
}

@ -1,4 +1,4 @@
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material"
interface Props {
bigText: string;
@ -7,28 +7,28 @@ interface Props {
}
export default function Infographics({ bigText, text, flex }: Props) {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const theme = useTheme()
const upMd = useMediaQuery(theme.breakpoints.up("md"))
return (
<Box
sx={{
flex,
maxWidth: upMd ? undefined : "120px",
}}
>
<Typography
variant="infographic"
color={theme.palette.purple.main}
sx={{
whiteSpace: "nowrap",
}}
>
{bigText}
</Typography>
<Typography variant={upMd ? "body1" : "body2"} sx={{ maxWidth: upMd ? "11em" : "5em", fontWeight: 500 }}>
{text}
</Typography>
</Box>
);
return (
<Box
sx={{
flex,
maxWidth: upMd ? undefined : "120px",
}}
>
<Typography
variant="infographic"
color={theme.palette.purple.main}
sx={{
whiteSpace: "nowrap",
}}
>
{bigText}
</Typography>
<Typography variant={upMd ? "body1" : "body2"} sx={{ maxWidth: upMd ? "11em" : "5em", fontWeight: 500 }}>
{text}
</Typography>
</Box>
)
}

@ -1,37 +1,37 @@
import { Box, CssBaseline, ThemeProvider, useTheme, useMediaQuery } from "@mui/material";
import Section1 from "./Section1";
import Section2 from "./Section2";
import Section3 from "./Section3";
import Section4 from "./Section4";
import Section5 from "./Section5";
import FloatingSupportChat from "@root/components/FloatingSupportChat/FloatingSupportChat";
import Footer from "@root/components/Footer";
import Navbar from "@root/components/NavbarLanding/Navbar";
import { theme } from "@root/utils/theme";
import { Box, CssBaseline, ThemeProvider, useTheme, useMediaQuery } from "@mui/material"
import Section1 from "./Section1"
import Section2 from "./Section2"
import Section3 from "./Section3"
import Section4 from "./Section4"
import Section5 from "./Section5"
import FloatingSupportChat from "@root/components/FloatingSupportChat/FloatingSupportChat"
import Footer from "@root/components/Footer"
import Navbar from "@root/components/NavbarLanding/Navbar"
import { theme } from "@root/utils/theme"
export default function Landing() {
const muiTheme = useTheme();
const isTablet = useMediaQuery(muiTheme.breakpoints.down(900));
const muiTheme = useTheme()
const isTablet = useMediaQuery(muiTheme.breakpoints.down(900))
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<Box
sx={{
position: "relative",
paddingTop: isTablet ? "51px" : "80px",
color: "white",
}}
>
<Navbar isLoggedIn={false} />
<Section1 />
<Section2 />
<Section3 />
<Section4 />
<Section5 />
<Footer />
<FloatingSupportChat />
</Box>
</ThemeProvider>
);
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<Box
sx={{
position: "relative",
paddingTop: isTablet ? "51px" : "80px",
color: "white",
}}
>
<Navbar isLoggedIn={false} />
<Section1 />
<Section2 />
<Section3 />
<Section4 />
<Section5 />
<Footer />
<FloatingSupportChat />
</Box>
</ThemeProvider>
)
}

Some files were not shown because too many files have changed in this diff Show More