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 = { module.exports = {
presets: [ presets: [
["@babel/preset-env", { targets: { node: "current" } }], ["@babel/preset-env", { targets: { node: "current" } }],
"@babel/preset-typescript", "@babel/preset-typescript",
], ],
}; }

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

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

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

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

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

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

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

@ -1,53 +1,53 @@
import OneIcons from "../assets/Icons/tariffs-volume/OneIcons.svg"; import OneIcons from "../assets/Icons/tariffs-volume/OneIcons.svg"
import TwoIcons from "../assets/Icons/tariffs-volume/TwoIcons.svg"; import TwoIcons from "../assets/Icons/tariffs-volume/TwoIcons.svg"
import ThreeIcons from "../assets/Icons/tariffs-volume/ThreeIcons.svg"; import ThreeIcons from "../assets/Icons/tariffs-volume/ThreeIcons.svg"
import FourIcons from "../assets/Icons/tariffs-volume/FourIcons.svg"; import FourIcons from "../assets/Icons/tariffs-volume/FourIcons.svg"
import FiveIcons from "../assets/Icons/tariffs-volume/FiveIcons.svg"; import FiveIcons from "../assets/Icons/tariffs-volume/FiveIcons.svg"
export const showCaseVolume = [ export const showCaseVolume = [
{ {
name: "Безлимит", name: "Безлимит",
desc: "Текст-заполнитель — это текст, который имеет", desc: "Текст-заполнитель — это текст, который имеет",
style: { icon: OneIcons, bgcolor: "#FEDFD0" }, style: { icon: OneIcons, bgcolor: "#FEDFD0" },
id: "id1", id: "id1",
privelegeid: "1", privelegeid: "1",
amount: 10, amount: 10,
price: 100, price: 100,
}, },
{ {
name: "1 месяц", name: "1 месяц",
desc: "Текст-заполнитель — это текст, который имеет", desc: "Текст-заполнитель — это текст, который имеет",
style: { icon: TwoIcons, bgcolor: "#FEDFD0" }, style: { icon: TwoIcons, bgcolor: "#FEDFD0" },
id: "id2", id: "id2",
privelegeid: "2", privelegeid: "2",
amount: 10, amount: 10,
price: 1000, price: 1000,
}, },
{ {
name: "3 месяц", name: "3 месяц",
desc: "Текст-заполнитель — это текст, который имеет", desc: "Текст-заполнитель — это текст, который имеет",
style: { icon: ThreeIcons, bgcolor: "#FEDFD0" }, style: { icon: ThreeIcons, bgcolor: "#FEDFD0" },
id: "id3", id: "id3",
privelegeid: "3", privelegeid: "3",
amount: 10, amount: 10,
price: 1000, price: 1000,
}, },
{ {
name: "6 месяц", name: "6 месяц",
desc: "Текст-заполнитель — это текст, который имеет", desc: "Текст-заполнитель — это текст, который имеет",
style: { icon: FourIcons, bgcolor: "#FEDFD0" }, style: { icon: FourIcons, bgcolor: "#FEDFD0" },
id: "id4", id: "id4",
privelegeid: "4", privelegeid: "4",
amount: 10, amount: 10,
price: 1000, price: 1000,
}, },
{ {
name: "9 месяц", name: "9 месяц",
desc: "Текст-заполнитель — это текст, который имеет", desc: "Текст-заполнитель — это текст, который имеет",
style: { icon: FiveIcons, bgcolor: "#FEDFD0" }, style: { icon: FiveIcons, bgcolor: "#FEDFD0" },
id: "id5", id: "id5",
privelegeid: "5", privelegeid: "5",
amount: 10, amount: 10,
price: 1000, 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 { import type {
LoginRequest, LoginRequest,
LoginResponse, LoginResponse,
RegisterRequest, RegisterRequest,
RegisterResponse, RegisterResponse,
} from "@frontend/kitui"; } from "@frontend/kitui"
const apiUrl = const apiUrl =
process.env.NODE_ENV === "production" process.env.NODE_ENV === "production"
? "/auth" ? "/auth"
: "https://hub.pena.digital/auth"; : "https://hub.pena.digital/auth"
export async function register( export async function register(
login: string, login: string,
password: string, password: string,
phoneNumber: string phoneNumber: string
): Promise<[RegisterResponse | null, string?]> { ): Promise<[RegisterResponse | null, string?]> {
try { try {
const registerResponse = await makeRequest< const registerResponse = await makeRequest<
RegisterRequest, RegisterRequest,
RegisterResponse RegisterResponse
>({ >({
url: apiUrl + "/register", url: apiUrl + "/register",
body: { login, password, phoneNumber }, body: { login, password, phoneNumber },
useToken: false, useToken: false,
withCredentials: true, withCredentials: true,
}); })
return [registerResponse]; return [registerResponse]
} catch (nativeError) { } catch (nativeError) {
const [error] = parseAxiosError(nativeError); const [error] = parseAxiosError(nativeError)
return [null, `Не удалось зарегестрировать аккаунт. ${error}`]; return [null, `Не удалось зарегестрировать аккаунт. ${error}`]
} }
} }
export async function login( export async function login(
login: string, login: string,
password: string password: string
): Promise<[LoginResponse | null, string?]> { ): Promise<[LoginResponse | null, string?]> {
try { try {
const loginResponse = await makeRequest<LoginRequest, LoginResponse>({ const loginResponse = await makeRequest<LoginRequest, LoginResponse>({
url: apiUrl + "/login", url: apiUrl + "/login",
body: { login, password }, body: { login, password },
useToken: false, useToken: false,
withCredentials: true, withCredentials: true,
}); })
return [loginResponse]; return [loginResponse]
} catch (nativeError) { } catch (nativeError) {
const [error] = parseAxiosError(nativeError); const [error] = parseAxiosError(nativeError)
return [null, `Не удалось войти. ${error}`]; return [null, `Не удалось войти. ${error}`]
} }
} }
export async function logout(): Promise<[unknown, string?]> { export async function logout(): Promise<[unknown, string?]> {
try { try {
const logoutResponse = await makeRequest<never, void>({ const logoutResponse = await makeRequest<never, void>({
url: apiUrl + "/logout", url: apiUrl + "/logout",
method: "POST", method: "POST",
useToken: true, useToken: true,
withCredentials: true, withCredentials: true,
}); })
return [logoutResponse]; return [logoutResponse]
} catch (nativeError) { } catch (nativeError) {
const [error] = parseAxiosError(nativeError); const [error] = parseAxiosError(nativeError)
return [null, `Не удалось выйти. ${error}`]; return [null, `Не удалось выйти. ${error}`]
} }
} }

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

@ -1,5 +1,5 @@
import { Tariff, makeRequest } from "@frontend/kitui"; import { Tariff, makeRequest } from "@frontend/kitui"
import { parseAxiosError } from "@root/utils/parse-error"; import { parseAxiosError } from "@root/utils/parse-error"
export interface GetHistoryResponse { export interface GetHistoryResponse {
totalPages: number; totalPages: number;
@ -21,17 +21,17 @@ type RawDetails = { Key: string; Value: KeyValue[][] }
type KeyValue = { Key: string; Value: string | number }; type KeyValue = { Key: string; Value: string | number };
export async function getHistory(): Promise<[GetHistoryResponse | null, string?]> { export async function getHistory(): Promise<[GetHistoryResponse | null, string?]> {
try { try {
const historyResponse = await makeRequest<never, GetHistoryResponse>({ const historyResponse = await makeRequest<never, GetHistoryResponse>({
url: "https://hub.pena.digital/customer/history?page=1&limit=100&type=payCart", url: "https://hub.pena.digital/customer/history?page=1&limit=100&type=payCart",
method: "get", method: "get",
useToken: true, useToken: true,
}); })
return [historyResponse]; return [historyResponse]
} catch (nativeError) { } catch (nativeError) {
const [error] = parseAxiosError(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?]> { export async function getDiscounts(signal: AbortSignal | undefined): Promise<[GetDiscountsResponse | null, string?]> {
try { try {
const discountsResponse = await makeRequest<never, GetDiscountsResponse>({ const discountsResponse = await makeRequest<never, GetDiscountsResponse>({
url: apiUrl + "/discounts", url: apiUrl + "/discounts",
method: "get", method: "get",
useToken: true, useToken: true,
signal, signal,
}); })
return [discountsResponse]; return [discountsResponse]
} catch (nativeError) { } catch (nativeError) {
const [error] = parseAxiosError(nativeError); const [error] = parseAxiosError(nativeError)
return [null, `Ошибка получения списка скидок. ${error}`]; return [null, `Ошибка получения списка скидок. ${error}`]
} }
} }

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

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

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

@ -1,48 +1,48 @@
import { makeRequest } from "@frontend/kitui"; import { makeRequest } from "@frontend/kitui"
import { SendPaymentRequest, SendPaymentResponse } from "@root/model/wallet"; import { SendPaymentRequest, SendPaymentResponse } from "@root/model/wallet"
import { parseAxiosError } from "@root/utils/parse-error"; import { parseAxiosError } from "@root/utils/parse-error"
const apiUrl = const apiUrl =
process.env.NODE_ENV === "production" process.env.NODE_ENV === "production"
? "/customer" ? "/customer"
: "https://hub.pena.digital/customer"; : "https://hub.pena.digital/customer"
const testPaymentBody: SendPaymentRequest = { const testPaymentBody: SendPaymentRequest = {
type: "bankCard", type: "bankCard",
amount: 15020, amount: 15020,
currency: "RUB", currency: "RUB",
bankCard: { bankCard: {
number: "RUB", number: "RUB",
expiryYear: "2021", expiryYear: "2021",
expiryMonth: "05", expiryMonth: "05",
csc: "05", csc: "05",
cardholder: "IVAN IVANOV", cardholder: "IVAN IVANOV",
}, },
phoneNumber: "79000000000", phoneNumber: "79000000000",
login: "login_test", login: "login_test",
returnUrl: window.location.origin + "/wallet", returnUrl: window.location.origin + "/wallet",
}; }
export async function sendPayment( export async function sendPayment(
body: SendPaymentRequest = testPaymentBody body: SendPaymentRequest = testPaymentBody
): Promise<[SendPaymentResponse | null, string?]> { ): Promise<[SendPaymentResponse | null, string?]> {
try { try {
const sendPaymentResponse = await makeRequest< const sendPaymentResponse = await makeRequest<
SendPaymentRequest, SendPaymentRequest,
SendPaymentResponse SendPaymentResponse
>({ >({
url: apiUrl + "/wallet", url: apiUrl + "/wallet",
contentType: true, contentType: true,
method: "POST", method: "POST",
useToken: true, useToken: true,
withCredentials: false, withCredentials: false,
body, body,
}); })
return [sendPaymentResponse]; return [sendPaymentResponse]
} catch (nativeError) { } catch (nativeError) {
const [error] = parseAxiosError(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 { Box, SxProps, Theme, Typography, useMediaQuery, useTheme } from "@mui/material"
import { PenaLink } from "@frontend/kitui"; import { PenaLink } from "@frontend/kitui"
import { Link as RouterLink } from "react-router-dom"; import { Link as RouterLink } from "react-router-dom"
interface Props { interface Props {
image?: string; image?: string;
@ -12,21 +12,21 @@ interface Props {
} }
export default function CardWithLink({ image, headerText, text, linkHref, isHighlighted = false, sx }: Props) { export default function CardWithLink({ image, headerText, text, linkHref, isHighlighted = false, sx }: Props) {
const theme = useTheme(); const theme = useTheme()
return ( return (
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
flexGrow: 1, flexGrow: 1,
alignItems: "start", alignItems: "start",
p: "20px", p: "20px",
maxWidth: "360px", maxWidth: "360px",
backgroundColor: isHighlighted ? theme.palette.purple.main : "#434657", backgroundColor: isHighlighted ? theme.palette.purple.main : "#434657",
borderRadius: "12px", borderRadius: "12px",
color: "white", color: "white",
boxShadow: ` boxShadow: `
0px 100px 309px rgba(37, 39, 52, 0.24), 0px 100px 309px rgba(37, 39, 52, 0.24),
0px 41.7776px 129.093px rgba(37, 39, 52, 0.172525), 0px 41.7776px 129.093px rgba(37, 39, 52, 0.172525),
0px 22.3363px 69.0192px rgba(37, 39, 52, 0.143066), 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 6.6501px 20.5488px rgba(37, 39, 52, 0.0969343),
0px 2.76726px 8.55082px rgba(37, 39, 52, 0.0674749) 0px 2.76726px 8.55082px rgba(37, 39, 52, 0.0674749)
`, `,
...sx, ...sx,
}} }}
> >
{image && ( {image && (
<img <img
src={image} src={image}
alt="" alt=""
style={{ style={{
objectFit: "contain", objectFit: "contain",
width: "100%", width: "100%",
display: "block", display: "block",
marginTop: "calc(-18px - 11%)", marginTop: "calc(-18px - 11%)",
pointerEvents: "none", pointerEvents: "none",
}} }}
/> />
)} )}
<Typography variant="h5">{headerText}</Typography> <Typography variant="h5">{headerText}</Typography>
<Typography mt="20px" mb="29px"> <Typography mt="20px" mb="29px">
{text} {text}
</Typography> </Typography>
<PenaLink <PenaLink
component={RouterLink} component={RouterLink}
to={linkHref} to={linkHref}
sx={{ sx={{
color: "white", color: "white",
textDecoration: "underline", textDecoration: "underline",
mt: "auto", mt: "auto",
mb: "15px", mb: "15px",
}} }}
> >
Подробнее Подробнее
</PenaLink> </PenaLink>
</Box> </Box>
); )
} }

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

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

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

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

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

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

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

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

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

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

@ -1,4 +1,4 @@
import { Box } from "@mui/material"; import { Box } from "@mui/material"
interface Props { interface Props {
@ -6,21 +6,21 @@ interface Props {
} }
export default function CircleDoubleDown({ isUp = false }: Props) { export default function CircleDoubleDown({ isUp = false }: Props) {
return ( return (
<Box sx={{ <Box sx={{
width: "32px", width: "32px",
height: "32px", height: "32px",
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
flexShrink: 0, flexShrink: 0,
transform: isUp ? "scale(1, -1)" : undefined, 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"> <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="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 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" /> <path d="M12.9004 14L16.9004 10L20.9004 14" stroke="#252734" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg> </svg>
</Box> </Box>
); )
} }

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

@ -1,22 +1,22 @@
import { Box } from "@mui/material"; import { Box } from "@mui/material"
export default function UserCircleIcon() { export default function UserCircleIcon() {
return ( return (
<Box sx={{ <Box sx={{
width: "32px", width: "32px",
height: "32px", height: "32px",
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
flexShrink: 0, flexShrink: 0,
}}> }}>
<svg width="32" height="33" viewBox="0 0 32 33" fill="none" xmlns="http://www.w3.org/2000/svg"> <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 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="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" /> <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> </svg>
</Box> </Box>
); )
} }

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

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

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

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

@ -1,42 +1,42 @@
import { Avatar, Box, IconButton, SxProps, Theme, Typography, useTheme } from "@mui/material"; import { Avatar, Box, IconButton, SxProps, Theme, Typography, useTheme } from "@mui/material"
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom"
interface Props { interface Props {
sx?: SxProps<Theme>; sx?: SxProps<Theme>;
} }
export default function CustomAvatar({ sx }: Props) { export default function CustomAvatar({ sx }: Props) {
const theme = useTheme(); const theme = useTheme()
const navigate = useNavigate() const navigate = useNavigate()
return ( return (
<IconButton <IconButton
onClick={() => navigate("/settings")} onClick={() => navigate("/settings")}
sx={{ sx={{
ml: "27px", ml: "27px",
height: "36px", width: "36px", height: "36px", width: "36px",
...sx, ...sx,
}} }}
> >
<Avatar sx={{ <Avatar sx={{
backgroundColor: theme.palette.orange.main, backgroundColor: theme.palette.orange.main,
}}> }}>
<Typography <Typography
sx={{ sx={{
fontWeight: 500, fontWeight: 500,
fontSize: "14px", fontSize: "14px",
lineHeight: "20px", lineHeight: "20px",
zIndex: 1, zIndex: 1,
}} }}
>AA</Typography> >AA</Typography>
<Box sx={{ position: "absolute" }}> <Box sx={{ position: "absolute" }}>
<svg xmlns="http://www.w3.org/2000/svg" width="37" height="36" viewBox="0 0 37 36" fill="none"> <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" /> <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="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" /> <circle cx="25.1065" cy="28.2781" r="1.26958" transform="rotate(-32.339 25.1065 28.2781)" fill="#FC712F" />
</svg> </svg>
</Box> </Box>
</Avatar> </Avatar>
</IconButton> </IconButton>
); )
} }

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

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

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

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

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

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

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

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

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

@ -1,5 +1,5 @@
import { Box, SxProps, Theme } from "@mui/material"; import { Box, SxProps, Theme } from "@mui/material"
import { ReactElement } from "react"; import { ReactElement } from "react"
interface Props { interface Props {
@ -10,86 +10,86 @@ interface Props {
} }
export default function NumberIcon({ number, backgroundColor = "rgb(0 0 0 / 0)", color, sx }: 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 firstDigit = Math.floor(number / 10)
const secondDigit = number % 10; const secondDigit = number % 10
const firstDigitTranslateX = 6; const firstDigitTranslateX = 6
const secondDigitTranslateX = number < 10 const secondDigitTranslateX = number < 10
? 9 ? 9
: number < 20 : number < 20
? 11 ? 11
: 12; : 12
const firstDigitElement = digitSvgs[firstDigit](firstDigitTranslateX); const firstDigitElement = digitSvgs[firstDigit](firstDigitTranslateX)
const secondDigitElement = digitSvgs[secondDigit](secondDigitTranslateX); const secondDigitElement = digitSvgs[secondDigit](secondDigitTranslateX)
return ( return (
<Box sx={{ <Box sx={{
backgroundColor, backgroundColor,
color, color,
width: "36px", width: "36px",
height: "36px", height: "36px",
borderRadius: "6px", borderRadius: "6px",
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
flexShrink: 0, flexShrink: 0,
...sx, ...sx,
}}> }}>
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="26" height="26" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
{circleSvg} {circleSvg}
{number > 9 && firstDigitElement} {number > 9 && firstDigitElement}
{secondDigitElement} {secondDigitElement}
</svg> </svg>
</Box> </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> = { const digitSvgs: Record<number, (translateX: number) => ReactElement> = {
0: (translateX: number) => ( 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" /> <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) => ( 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" /> <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) => ( 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" /> <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) => ( 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" /> <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) => ( 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="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" /> <path transform={`translate(${translateX} 7)`} d="M4.5 3.89038V8.89058" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</> </>
), ),
5: (translateX: number) => ( 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" /> <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) => ( 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="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" /> <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) => ( 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" /> <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) => ( 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="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" /> <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) => ( 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="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" /> <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 { useState } from "react"
import { InputAdornment, TextField, Typography, useTheme } from "@mui/material"; import { InputAdornment, TextField, Typography, useTheme } from "@mui/material"
import type { ChangeEvent } from "react"; import type { ChangeEvent } from "react"
interface Props { interface Props {
id: string; id: string;
@ -11,92 +11,92 @@ interface Props {
} }
export default function NumberInputWithUnitAdornment({ id, value, adornmentText, onChange }: Props) { export default function NumberInputWithUnitAdornment({ id, value, adornmentText, onChange }: Props) {
const theme = useTheme(); const theme = useTheme()
const [changed, setChanged] = useState<boolean>(false); const [changed, setChanged] = useState<boolean>(false)
return ( return (
<TextField <TextField
type="number" type="number"
size="small" size="small"
placeholder="Введите вручную" placeholder="Введите вручную"
id={id} id={id}
value={changed ? (value !== 0 ? value : "") : ""} value={changed ? (value !== 0 ? value : "") : ""}
onChange={({ target }: ChangeEvent<HTMLInputElement>) => { onChange={({ target }: ChangeEvent<HTMLInputElement>) => {
if (!changed) { if (!changed) {
setChanged(true); setChanged(true)
} }
if (Number(target.value) > 999999) { if (Number(target.value) > 999999) {
target.value = "999999"; target.value = "999999"
} }
const newNumber = parseInt(target.value); const newNumber = parseInt(target.value)
if (!isFinite(newNumber) || newNumber < 0) { if (!isFinite(newNumber) || newNumber < 0) {
onChange(0); onChange(0)
return; return
} }
onChange(newNumber); onChange(newNumber)
}} }}
sx={{ sx={{
maxWidth: "200px", maxWidth: "200px",
minWidth: "200px", minWidth: "200px",
".MuiInputBase-root": { ".MuiInputBase-root": {
display: "flex", display: "flex",
pr: 0, pr: 0,
height: "48px", height: "48px",
borderRadius: "8px", borderRadius: "8px",
backgroundColor: "#F2F3F7", backgroundColor: "#F2F3F7",
fieldset: { fieldset: {
border: "1px solid" + theme.palette.gray.main, border: "1px solid" + theme.palette.gray.main,
}, },
"&.Mui-focused fieldset": { "&.Mui-focused fieldset": {
borderColor: theme.palette.purple.main, borderColor: theme.palette.purple.main,
}, },
input: { input: {
height: "31px", height: "31px",
borderRight: !changed ? "none" : "1px solid #9A9AAF", borderRight: !changed ? "none" : "1px solid #9A9AAF",
}, },
"&.Mui-focused input": { "&.Mui-focused input": {
borderRight: "1px solid #9A9AAF", borderRight: "1px solid #9A9AAF",
}, },
"&:not(.Mui-focused) .MuiInputAdornment-root": { "&:not(.Mui-focused) .MuiInputAdornment-root": {
display: !changed ? "none" : undefined, display: !changed ? "none" : undefined,
}, },
"&.Mui-focused ::-webkit-input-placeholder": { "&.Mui-focused ::-webkit-input-placeholder": {
color: "transparent", color: "transparent",
}, },
// Hiding arrows // Hiding arrows
"input::-webkit-outer-spin-button, input::-webkit-inner-spin-button": { "input::-webkit-outer-spin-button, input::-webkit-inner-spin-button": {
WebkitAppearance: "none", WebkitAppearance: "none",
margin: 0, margin: 0,
}, },
"input[type = number]": { "input[type = number]": {
MozAppearance: "textfield", MozAppearance: "textfield",
}, },
}, },
}} }}
InputProps={{ InputProps={{
endAdornment: ( endAdornment: (
<InputAdornment <InputAdornment
position="end" position="end"
sx={{ sx={{
userSelect: "none", userSelect: "none",
pointerEvents: "none", pointerEvents: "none",
pl: "2px", pl: "2px",
pr: "13px", pr: "13px",
}} }}
> >
<Typography variant="body2" color="#4D4D4D"> <Typography variant="body2" color="#4D4D4D">
{adornmentText} {adornmentText}
</Typography> </Typography>
</InputAdornment> </InputAdornment>
), ),
}} }}
/> />
); )
} }

@ -1,4 +1,4 @@
import { useTheme } from "@mui/material"; import { useTheme } from "@mui/material"
interface Props { interface Props {
@ -7,29 +7,29 @@ interface Props {
} }
export default function PenaLogo({ width, color }: Props) { export default function PenaLogo({ width, color }: Props) {
const theme = useTheme(); const theme = useTheme()
return ( return (
<svg style={{ minWidth: width }} width={width} viewBox="0 0 180 70" fill="none" xmlns="http://www.w3.org/2000/svg"> <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)"> <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} /> <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="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} /> <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="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="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="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} /> <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}> <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="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="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} /> <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>
</g> </g>
<defs> <defs>
<clipPath id="clip0_122_333"> <clipPath id="clip0_122_333">
<rect width="179.509" height="69.4872" fill="white" /> <rect width="179.509" height="69.4872" fill="white" />
</clipPath> </clipPath>
</defs> </defs>
</svg> </svg>
); )
} }

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

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

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

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

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

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

@ -1,4 +1,4 @@
import { Box } from "@mui/material"; import { Box } from "@mui/material"
interface Props { interface Props {
color: string; color: string;
@ -6,63 +6,63 @@ interface Props {
} }
export default function CalendarIcon({ color, bgcolor }: Props) { export default function CalendarIcon({ color, bgcolor }: Props) {
return ( return (
<Box <Box
sx={{ sx={{
bgcolor, bgcolor,
height: "36px", height: "36px",
width: "36px", width: "36px",
minWidth: "36px", minWidth: "36px",
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
borderRadius: "6px", borderRadius: "6px",
}} }}
> >
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path <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" 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} stroke={color}
strokeWidth="1.5" strokeWidth="1.5"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
/> />
<path <path
d="M16.502 2.25455V5.25455" d="M16.502 2.25455V5.25455"
stroke={color} stroke={color}
strokeWidth="1.5" strokeWidth="1.5"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
/> />
<path <path
d="M7.50195 2.25455V5.25455" d="M7.50195 2.25455V5.25455"
stroke={color} stroke={color}
strokeWidth="1.5" strokeWidth="1.5"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
/> />
<path <path
d="M3.75195 8.25455H20.252" d="M3.75195 8.25455H20.252"
stroke={color} stroke={color}
strokeWidth="1.5" strokeWidth="1.5"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
/> />
<path <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" 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} stroke={color}
strokeWidth="1.5" strokeWidth="1.5"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
/> />
<path <path
d="M13.502 13.1295L15.002 12.0045V16.8795" d="M13.502 13.1295L15.002 12.0045V16.8795"
stroke={color} stroke={color}
strokeWidth="1.5" strokeWidth="1.5"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
/> />
</svg> </svg>
</Box> </Box>
); )
} }

@ -1,23 +1,23 @@
import { Box } from "@mui/material"; import { Box } from "@mui/material"
export default function CloseIcon() { export default function CloseIcon() {
return ( return (
<Box <Box
sx={{ sx={{
width: "30px", width: "30px",
height: "30px", height: "30px",
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
flexShrink: 0, flexShrink: 0,
"&:hover path": { "&:hover path": {
stroke: "#7E2AEA", stroke: "#7E2AEA",
}, },
}} }}
> >
<svg width="26" height="26" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg"> <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" /> <path d="M1 1L25 25M1 25L25 1" stroke="black" />
</svg> </svg>
</Box> </Box>
); )
} }

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

@ -1,4 +1,4 @@
import { Box } from "@mui/material"; import { Box } from "@mui/material"
interface Props { interface Props {
color: string; color: string;
@ -6,55 +6,55 @@ interface Props {
} }
export default function CustomIcon({ color, bgcolor }: Props) { export default function CustomIcon({ color, bgcolor }: Props) {
return ( return (
<Box <Box
sx={{ sx={{
bgcolor, bgcolor,
height: "36px", height: "36px",
width: "36px", width: "36px",
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
borderRadius: "6px", borderRadius: "6px",
}} }}
> >
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path <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" 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} stroke={color}
strokeWidth="1.5" strokeWidth="1.5"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
/> />
<path <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" 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} stroke={color}
strokeWidth="1.5" strokeWidth="1.5"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
/> />
<path <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" 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} stroke={color}
strokeWidth="1.5" strokeWidth="1.5"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
/> />
<path <path
d="M16.877 14.2545V19.5045" d="M16.877 14.2545V19.5045"
stroke={color} stroke={color}
strokeWidth="1.5" strokeWidth="1.5"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
/> />
<path <path
d="M19.502 16.8795H14.252" d="M19.502 16.8795H14.252"
stroke={color} stroke={color}
strokeWidth="1.5" strokeWidth="1.5"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
/> />
</svg> </svg>
</Box> </Box>
); )
} }

@ -1,4 +1,4 @@
import { useTheme , Box} from "@mui/material"; import { useTheme , Box} from "@mui/material"
interface Props { interface Props {
@ -6,21 +6,21 @@ interface Props {
} }
export default function ExpandIcon({ isExpanded }: Props) { export default function ExpandIcon({ isExpanded }: Props) {
const theme = useTheme(); const theme = useTheme()
return ( return (
<Box sx={{ <Box sx={{
width: "33px", width: "33px",
height: "33px", height: "33px",
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
flexShrink: 0, 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"> <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="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" /> <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> </svg>
</Box> </Box>
) )
} }

@ -1,21 +1,21 @@
import { Box } from "@mui/material"; import { Box } from "@mui/material"
export default function EyeIcon() { export default function EyeIcon() {
return ( return (
<Box sx={{ <Box sx={{
width: "24px", width: "24px",
height: "24px", height: "24px",
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
flexShrink: 0, flexShrink: 0,
}}> }}>
<svg width="22" height="15" viewBox="0 0 22 15" fill="none" xmlns="http://www.w3.org/2000/svg"> <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" /> <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" /> <circle cx="10.9495" cy="7.50033" r="3.58333" stroke="#7E2AEA" strokeWidth="1.5" />
</svg> </svg>
</Box> </Box>
); )
} }

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

@ -1,14 +1,14 @@
import { useTheme } from "@mui/material"; import { useTheme } from "@mui/material"
export default function LogoutIcon() { export default function LogoutIcon() {
const theme = useTheme(); const theme = useTheme()
return ( return (
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"> <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="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="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" /> <path d="M16.7384 9L6.70996 9" stroke={theme.palette.gray.main} strokeWidth="1.5" strokeLinecap="round" />
</svg> </svg>
); )
} }

@ -1,4 +1,4 @@
import { Box } from "@mui/material"; import { Box } from "@mui/material"
interface Props { interface Props {
@ -7,18 +7,18 @@ interface Props {
export default function PaperClipIcon({ color = "#7E2AEA" }: Props) { export default function PaperClipIcon({ color = "#7E2AEA" }: Props) {
return ( return (
<Box sx={{ <Box sx={{
width: "24px", width: "24px",
height: "24px", height: "24px",
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
flexShrink: 0, flexShrink: 0,
}}> }}>
<svg width="17" height="19" viewBox="0 0 17 19" fill="none" xmlns="http://www.w3.org/2000/svg"> <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" /> <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> </svg>
</Box> </Box>
); )
} }

@ -1,4 +1,4 @@
import { Box } from "@mui/material"; import { Box } from "@mui/material"
interface Props { interface Props {
@ -8,25 +8,25 @@ interface Props {
export default function PieChartIcon({ color, bgcolor }: Props) { export default function PieChartIcon({ color, bgcolor }: Props) {
return ( return (
<Box <Box
sx={{ sx={{
bgcolor, bgcolor,
height: "36px", height: "36px",
width: "36px", width: "36px",
minWidth: "36px", minWidth: "36px",
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
borderRadius: "6px", borderRadius: "6px",
}} }}
> >
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none"> <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="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="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="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" /> <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> </svg>
</Box> </Box>
); )
} }

@ -1,4 +1,4 @@
import { CSSProperties } from "react"; import { CSSProperties } from "react"
interface Props { interface Props {
@ -7,11 +7,11 @@ interface Props {
export default function SendIcon({ style }: Props) { export default function SendIcon({ style }: Props) {
return ( return (
<svg xmlns="http://www.w3.org/2000/svg" width="45" height="45" viewBox="0 0 45 45" fill="none" style={style}> <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" /> <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="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" /> <path d="M18.1943 22.9248H24.9868" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg> </svg>
); )
} }

@ -1,22 +1,22 @@
import { Box } from "@mui/material"; import { Box } from "@mui/material"
export default function SendIcon() { export default function SendIcon() {
return ( return (
<Box sx={{ <Box sx={{
width: "24px", width: "24px",
height: "24px", height: "24px",
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
flexShrink: 0, flexShrink: 0,
}}> }}>
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg"> <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="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="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" /> <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> </svg>
</Box> </Box>
); )
} }

@ -1,4 +1,4 @@
import { Box } from "@mui/material"; import { Box } from "@mui/material"
interface Props { interface Props {
color: string; color: string;
@ -6,32 +6,32 @@ interface Props {
} }
export default function SendIcon({ color, bgcolor }: Props) { export default function SendIcon({ color, bgcolor }: Props) {
return ( return (
<Box <Box
sx={{ sx={{
bgcolor, bgcolor,
borderRadius: "6px", borderRadius: "6px",
height: "36px", height: "36px",
width: "36px", width: "36px",
display: "flex", display: "flex",
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
ml: "8px", ml: "8px",
}} }}
> >
<svg <svg
width="22" width="22"
height="19" height="19"
viewBox="0 0 22 19" viewBox="0 0 22 19"
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<path <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" 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} stroke={color}
strokeWidth="1.5" strokeWidth="1.5"
/> />
</svg> </svg>
</Box> </Box>
); )
} }

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

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

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

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

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

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

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

@ -1,4 +1,4 @@
import { Discount } from "@frontend/kitui"; import { Discount } from "@frontend/kitui"
export interface GetDiscountsResponse { 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[]>; export type ServiceKeyToPrivilegesMap = Record<string, Privilege[]>;

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

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

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

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

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

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

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

@ -1,121 +1,121 @@
import { Box, useMediaQuery, useTheme } from "@mui/material"; import { Box, useMediaQuery, useTheme } from "@mui/material"
import InputTextfield from "@components/InputTextfield"; import InputTextfield from "@components/InputTextfield"
import PasswordInput from "@components/passwordInput"; import PasswordInput from "@components/passwordInput"
import { setSettingsField, useUserStore } from "@root/stores/user"; import { setSettingsField, useUserStore } from "@root/stores/user"
export default function UserFields () { export default function UserFields () {
const theme = useTheme(); const theme = useTheme()
const upSm = useMediaQuery(theme.breakpoints.up("sm")); const upSm = useMediaQuery(theme.breakpoints.up("sm"))
const upMd = useMediaQuery(theme.breakpoints.up("md")); 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 = { const textFieldProps = {
gap: upMd ? "16px" : "10px", gap: upMd ? "16px" : "10px",
color: "#F2F3F7", color: "#F2F3F7",
bold: true, bold: true,
}; }
return( return(
<Box <Box
sx={{ sx={{
display: "grid", display: "grid",
gridAutoFlow: upSm ? "column" : "row", gridAutoFlow: upSm ? "column" : "row",
gridTemplateRows: "repeat(4, auto)", gridTemplateRows: "repeat(4, auto)",
gridAutoColumns: "1fr", gridAutoColumns: "1fr",
rowGap: "15px", rowGap: "15px",
columnGap: "31px", columnGap: "31px",
flexGrow: 1, flexGrow: 1,
}} }}
> >
<InputTextfield <InputTextfield
TextfieldProps={{ TextfieldProps={{
placeholder: "Имя", placeholder: "Имя",
value: fields.firstname.value || "", value: fields.firstname.value || "",
helperText: fields.firstname.touched && fields.firstname.error, helperText: fields.firstname.touched && fields.firstname.error,
error: fields.firstname.touched && Boolean(fields.firstname.error), error: fields.firstname.touched && Boolean(fields.firstname.error),
}} }}
onChange={(e) => setSettingsField("firstname", e.target.value)} onChange={(e) => setSettingsField("firstname", e.target.value)}
id="firstname" id="firstname"
label="Имя" label="Имя"
{...textFieldProps} {...textFieldProps}
/> />
<InputTextfield <InputTextfield
TextfieldProps={{ TextfieldProps={{
placeholder: "Фамилия", placeholder: "Фамилия",
value: fields.secondname.value || "", value: fields.secondname.value || "",
helperText: fields.secondname.touched && fields.secondname.error, helperText: fields.secondname.touched && fields.secondname.error,
error: fields.secondname.touched && Boolean(fields.secondname.error), error: fields.secondname.touched && Boolean(fields.secondname.error),
}} }}
onChange={(e) => setSettingsField("secondname", e.target.value)} onChange={(e) => setSettingsField("secondname", e.target.value)}
id="secondname" id="secondname"
label="Фамилия" label="Фамилия"
{...textFieldProps} {...textFieldProps}
/> />
<InputTextfield <InputTextfield
TextfieldProps={{ TextfieldProps={{
placeholder: "Отчество", placeholder: "Отчество",
value: fields.middlename.value || "", value: fields.middlename.value || "",
helperText: fields.middlename.touched && fields.middlename.error, helperText: fields.middlename.touched && fields.middlename.error,
error: fields.middlename.touched && Boolean(fields.middlename.error), error: fields.middlename.touched && Boolean(fields.middlename.error),
}} }}
onChange={(e) => setSettingsField("middlename", e.target.value)} onChange={(e) => setSettingsField("middlename", e.target.value)}
id="middlename" id="middlename"
label="Отчество" label="Отчество"
{...textFieldProps} {...textFieldProps}
/> />
<InputTextfield <InputTextfield
TextfieldProps={{ TextfieldProps={{
placeholder: "ООО Фирма", placeholder: "ООО Фирма",
value: fields.orgname.value || "", value: fields.orgname.value || "",
helperText: fields.orgname.touched && fields.orgname.error, helperText: fields.orgname.touched && fields.orgname.error,
error: fields.orgname.touched && Boolean(fields.orgname.error), error: fields.orgname.touched && Boolean(fields.orgname.error),
}} }}
onChange={(e) => setSettingsField("orgname", e.target.value)} onChange={(e) => setSettingsField("orgname", e.target.value)}
id="orgname" id="orgname"
label="Название компании" label="Название компании"
{...textFieldProps} {...textFieldProps}
/> />
<InputTextfield <InputTextfield
TextfieldProps={{ TextfieldProps={{
placeholder: "username@penahaub.com", placeholder: "username@penahaub.com",
value: fields.email.value || "", value: fields.email.value || "",
helperText: fields.email.touched && fields.email.error, helperText: fields.email.touched && fields.email.error,
error: fields.email.touched && Boolean(fields.email.error), error: fields.email.touched && Boolean(fields.email.error),
}} }}
onChange={(e) => setSettingsField("email", e.target.value)} onChange={(e) => setSettingsField("email", e.target.value)}
id="email" id="email"
label="E-mail" label="E-mail"
{...textFieldProps} {...textFieldProps}
/> />
<InputTextfield <InputTextfield
TextfieldProps={{ TextfieldProps={{
placeholder: "+7 900 000 00 00", placeholder: "+7 900 000 00 00",
value: fields.phoneNumber.value || "", value: fields.phoneNumber.value || "",
helperText: fields.phoneNumber.touched && fields.phoneNumber.error, helperText: fields.phoneNumber.touched && fields.phoneNumber.error,
error: fields.phoneNumber.touched && Boolean(fields.phoneNumber.error), error: fields.phoneNumber.touched && Boolean(fields.phoneNumber.error),
}} }}
onChange={(e) => setSettingsField("phoneNumber", e.target.value)} onChange={(e) => setSettingsField("phoneNumber", e.target.value)}
id="phoneNumber" id="phoneNumber"
label="Телефон" label="Телефон"
{...textFieldProps} {...textFieldProps}
/> />
<PasswordInput <PasswordInput
TextfieldProps={{ TextfieldProps={{
placeholder: "Не менее 8 символов", placeholder: "Не менее 8 символов",
value: fields.password.value || "", value: fields.password.value || "",
helperText: fields.password.touched && fields.password.error, helperText: fields.password.touched && fields.password.error,
error: fields.password.touched && Boolean(fields.password.error), error: fields.password.touched && Boolean(fields.password.error),
autoComplete: "new-password", autoComplete: "new-password",
}} }}
onChange={(e) => setSettingsField("password", e.target.value)} onChange={(e) => setSettingsField("password", e.target.value)}
id="password" id="password"
label="Пароль" label="Пароль"
{...textFieldProps} {...textFieldProps}
/> />
</Box> </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 { import {
setVerificationStatus, setVerificationStatus,
setVerificationType, setVerificationType,
setComment, setComment,
setDocumentUrl, setDocumentUrl,
} from "@root/stores/user"; } from "@root/stores/user"
import { VerificationStatus } from "@root/model/account"; 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) => { const updateVerificationStatus = (verification: Verification) => {
if (!verification) { if (!verification) {
setVerificationStatus(VerificationStatus.NOT_VERIFICATED); setVerificationStatus(VerificationStatus.NOT_VERIFICATED)
return; return
} }
if (verification.accepted) { if (verification.accepted) {
setVerificationStatus(VerificationStatus.VERIFICATED); setVerificationStatus(VerificationStatus.VERIFICATED)
return; return
} }
if ( if (
(!verification.accepted && !verification.files?.length) || (!verification.accepted && !verification.files?.length) ||
(!verification.accepted && !verification.comment) (!verification.accepted && !verification.comment)
) { ) {
setVerificationStatus(VerificationStatus.NOT_VERIFICATED); setVerificationStatus(VerificationStatus.NOT_VERIFICATED)
return; return
} }
if (verification.files?.length && !verification.comment) { if (verification.files?.length && !verification.comment) {
setVerificationStatus(VerificationStatus.WAITING); setVerificationStatus(VerificationStatus.WAITING)
return; return
} }
setVerificationStatus(VerificationStatus.NOT_VERIFICATED); setVerificationStatus(VerificationStatus.NOT_VERIFICATED)
}; }
export const verify = async (id: string) => { export const verify = async (id: string) => {
const [verificationResult, verificationError] = await verification(id); const [verificationResult, verificationError] = await verification(id)
if (verificationError) { if (verificationError) {
setVerificationStatus(VerificationStatus.NOT_VERIFICATED); setVerificationStatus(VerificationStatus.NOT_VERIFICATED)
devlog("Error fetching user", verificationError); devlog("Error fetching user", verificationError)
return; return
} }
if (verificationResult) { if (verificationResult) {
updateVerificationStatus(verificationResult); updateVerificationStatus(verificationResult)
setVerificationType(verificationResult.status); setVerificationType(verificationResult.status)
setComment(verificationResult.comment); setComment(verificationResult.comment)
verificationResult.files.forEach((file) => verificationResult.files.forEach((file) =>
setDocumentUrl(DOCUMENT_TYPE_MAP[file.name], file.url) 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 { Box, IconButton, Typography, useMediaQuery, useTheme } from "@mui/material"
import SectionWrapper from "@components/SectionWrapper"; import SectionWrapper from "@components/SectionWrapper"
import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import ArrowBackIcon from "@mui/icons-material/ArrowBack"
import TotalPrice from "@components/TotalPrice"; import TotalPrice from "@components/TotalPrice"
import CustomWrapper from "./CustomWrapper"; import CustomWrapper from "./CustomWrapper"
import { useCart } from "@root/utils/hooks/useCart"; import { useCart } from "@root/utils/hooks/useCart"
import { useLocation } from "react-router-dom"; import { useLocation } from "react-router-dom"
import { usePrevLocation } from "@root/utils/hooks/handleCustomBackNavigation"; import { usePrevLocation } from "@root/utils/hooks/handleCustomBackNavigation"
import { handleComponentError } from "@root/utils/handleComponentError"; import { handleComponentError } from "@root/utils/handleComponentError"
import { withErrorBoundary } from "react-error-boundary"; import { withErrorBoundary } from "react-error-boundary"
function Cart() { function Cart() {
const theme = useTheme(); const theme = useTheme()
const upMd = useMediaQuery(theme.breakpoints.up("md")); const upMd = useMediaQuery(theme.breakpoints.up("md"))
const isMobile = useMediaQuery(theme.breakpoints.down(550)); const isMobile = useMediaQuery(theme.breakpoints.down(550))
const isTablet = useMediaQuery(theme.breakpoints.down(1000)); const isTablet = useMediaQuery(theme.breakpoints.down(1000))
const cart = useCart(); const cart = useCart()
const location = useLocation(); const location = useLocation()
const totalPriceBeforeDiscounts = cart.priceBeforeDiscounts; const totalPriceBeforeDiscounts = cart.priceBeforeDiscounts
const totalPriceAfterDiscounts = cart.priceAfterDiscounts; const totalPriceAfterDiscounts = cart.priceAfterDiscounts
const handleCustomBackNavigation = usePrevLocation(location); const handleCustomBackNavigation = usePrevLocation(location)
return ( return (
<SectionWrapper <SectionWrapper
maxWidth="lg" maxWidth="lg"
sx={{ sx={{
mt: upMd ? "25px" : "20px", mt: upMd ? "25px" : "20px",
px: isTablet ? (upMd ? "40px" : "18px") : "20px", px: isTablet ? (upMd ? "40px" : "18px") : "20px",
mb: upMd ? "70px" : "37px", mb: upMd ? "70px" : "37px",
}} }}
> >
<Box <Box
sx={{ sx={{
mt: "20px", mt: "20px",
mb: upMd ? "40px" : "20px", mb: upMd ? "40px" : "20px",
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
gap: "10px", gap: "10px",
}} }}
> >
{isMobile && ( {isMobile && (
<IconButton onClick={handleCustomBackNavigation} sx={{ p: 0, height: "28px", width: "28px", color: "black" }}> <IconButton onClick={handleCustomBackNavigation} sx={{ p: 0, height: "28px", width: "28px", color: "black" }}>
<ArrowBackIcon /> <ArrowBackIcon />
</IconButton> </IconButton>
)} )}
<Typography <Typography
sx={{ sx={{
fontSize: isMobile ? "24px" : "36px", fontSize: isMobile ? "24px" : "36px",
fontWeight: "500", fontWeight: "500",
}} }}
> >
Корзина Корзина
</Typography> </Typography>
</Box> </Box>
<Box <Box
sx={{ sx={{
mt: upMd ? "27px" : "10px", mt: upMd ? "27px" : "10px",
}} }}
> >
{cart.services.map((serviceData, index) => ( {cart.services.map((serviceData, index) => (
<CustomWrapper <CustomWrapper
key={serviceData.serviceKey} key={serviceData.serviceKey}
serviceData={serviceData} serviceData={serviceData}
first={index === 0} first={index === 0}
last={index === cart.services.length - 1} last={index === cart.services.length - 1}
/> />
))} ))}
</Box> </Box>
<TotalPrice priceBeforeDiscounts={totalPriceBeforeDiscounts} priceAfterDiscounts={totalPriceAfterDiscounts} /> <TotalPrice priceBeforeDiscounts={totalPriceBeforeDiscounts} priceAfterDiscounts={totalPriceAfterDiscounts} />
</SectionWrapper> </SectionWrapper>
); )
} }
export default withErrorBoundary(Cart, { export default withErrorBoundary(Cart, {
fallback: <Typography mt="8px" textAlign="center">Ошибка при отображении корзины</Typography>, fallback: <Typography mt="8px" textAlign="center">Ошибка при отображении корзины</Typography>,
onError: handleComponentError, onError: handleComponentError,
}) })

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

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

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

@ -1,129 +1,129 @@
import type { History } from "./AccordionWrapper"; import type { History } from "./AccordionWrapper"
const PAYMENT_HISTORY: History[] = [ const PAYMENT_HISTORY: History[] = [
{ {
date: "28.05.2022", date: "28.05.2022",
title: "Шаблонизатор", title: "Шаблонизатор",
payMethod: "QIWI Кошелек", payMethod: "QIWI Кошелек",
info: "3 190 руб.", info: "3 190 руб.",
description: "Дата действия приобретенной лицензии (в формате дд.мм.гггг-дд.мм.гггг) Или же объем", description: "Дата действия приобретенной лицензии (в формате дд.мм.гггг-дд.мм.гггг) Или же объем",
}, },
{ {
date: "28.05.2022", date: "28.05.2022",
title: "Сокращатель ссылок", title: "Сокращатель ссылок",
payMethod: "Юмани", payMethod: "Юмани",
info: "2 190 руб.", info: "2 190 руб.",
description: "Дата действия приобретенной лицензии (в формате дд.мм.гггг-дд.мм.гггг) Или же объем", description: "Дата действия приобретенной лицензии (в формате дд.мм.гггг-дд.мм.гггг) Или же объем",
}, },
{ {
date: "28.05.2022", date: "28.05.2022",
title: "Шаблонизатор", title: "Шаблонизатор",
payMethod: "QIWI Кошелек", payMethod: "QIWI Кошелек",
info: "1 190 руб.", info: "1 190 руб.",
description: "Дата действия приобретенной лицензии (в формате дд.мм.гггг-дд.мм.гггг) Или же объем", description: "Дата действия приобретенной лицензии (в формате дд.мм.гггг-дд.мм.гггг) Или же объем",
}, },
{ {
date: "08.04.2022", date: "08.04.2022",
title: "Шаблонизатор", title: "Шаблонизатор",
payMethod: "QIWI Кошелек", payMethod: "QIWI Кошелек",
info: "3 190 руб.", info: "3 190 руб.",
description: "Дата действия приобретенной лицензии (в формате дд.мм.гггг-дд.мм.гггг) Или же объем", description: "Дата действия приобретенной лицензии (в формате дд.мм.гггг-дд.мм.гггг) Или же объем",
}, },
{ {
date: "28.05.2022", date: "28.05.2022",
title: "Сокращатель ссылок", title: "Сокращатель ссылок",
payMethod: "Юмани", payMethod: "Юмани",
info: "5 190 руб.", info: "5 190 руб.",
description: "Дата действия приобретенной лицензии (в формате дд.мм.гггг-дд.мм.гггг) Или же объем", description: "Дата действия приобретенной лицензии (в формате дд.мм.гггг-дд.мм.гггг) Или же объем",
}, },
{ {
date: "18.03.2022", date: "18.03.2022",
title: "Шаблонизатор", title: "Шаблонизатор",
payMethod: "Юмани", payMethod: "Юмани",
info: "6 190 руб.", info: "6 190 руб.",
description: "Дата действия приобретенной лицензии (в формате дд.мм.гггг-дд.мм.гггг) Или же объем", description: "Дата действия приобретенной лицензии (в формате дд.мм.гггг-дд.мм.гггг) Или же объем",
}, },
]; ]
const PURCHASED_TARIFFS_HISTORY: History[] = [ const PURCHASED_TARIFFS_HISTORY: History[] = [
{ {
date: "28.05.2022", date: "28.05.2022",
title: "Шаблонизатор", title: "Шаблонизатор",
info: "5 000 шаблонов", info: "5 000 шаблонов",
description: "Тариф на время/ объем/ кастомный или другая информация", description: "Тариф на время/ объем/ кастомный или другая информация",
}, },
{ {
date: "27.05.2022", date: "27.05.2022",
title: "Опросник", title: "Опросник",
info: "9 месяцев 1 000 шаблонов", info: "9 месяцев 1 000 шаблонов",
description: "Тариф на время/ объем/ кастомный или другая информация", description: "Тариф на время/ объем/ кастомный или другая информация",
}, },
{ {
date: "20.05.2022", date: "20.05.2022",
title: "Шаблонизатор", title: "Шаблонизатор",
info: "Безлимит", info: "Безлимит",
description: "Тариф на время/ объем/ кастомный или другая информация", description: "Тариф на время/ объем/ кастомный или другая информация",
}, },
{ {
date: "08.04.2022", date: "08.04.2022",
title: "Опросник", title: "Опросник",
info: "10 000 шаблонов", info: "10 000 шаблонов",
description: "Тариф на время/ объем/ кастомный или другая информация", description: "Тариф на время/ объем/ кастомный или другая информация",
}, },
{ {
date: "28.05.2022", date: "28.05.2022",
title: "Сокращатель ссылок", title: "Сокращатель ссылок",
info: "3 дня", info: "3 дня",
description: "Тариф на время/ объем/ кастомный или другая информация", description: "Тариф на время/ объем/ кастомный или другая информация",
}, },
{ {
date: "18.03.2022", date: "18.03.2022",
title: "Шаблонизатор", title: "Шаблонизатор",
info: "9 месяцев 1 000 шаблонов", info: "9 месяцев 1 000 шаблонов",
description: "Тариф на время/ объем/ кастомный или другая информация", description: "Тариф на время/ объем/ кастомный или другая информация",
}, },
]; ]
const FINISHED_TARIFFS_HISTORY: History[] = [ const FINISHED_TARIFFS_HISTORY: History[] = [
{ {
date: "28.05.2022", date: "28.05.2022",
title: "Шаблонизатор", title: "Шаблонизатор",
info: "5 000 шаблонов", info: "5 000 шаблонов",
description: "Тариф на время/ объем/ кастомный или другая информация", description: "Тариф на время/ объем/ кастомный или другая информация",
}, },
{ {
date: "28.05.2022", date: "28.05.2022",
title: "Сокращатель ссылок", title: "Сокращатель ссылок",
info: "10 месяцев", info: "10 месяцев",
description: "Тариф на время/ объем/ кастомный или другая информация", description: "Тариф на время/ объем/ кастомный или другая информация",
}, },
{ {
date: "20.05.2022", date: "20.05.2022",
title: "Шаблонизатор", title: "Шаблонизатор",
info: "Безлимит", info: "Безлимит",
description: "Тариф на время/ объем/ кастомный или другая информация", description: "Тариф на время/ объем/ кастомный или другая информация",
}, },
{ {
date: "08.04.2022", date: "08.04.2022",
title: "Опросник", title: "Опросник",
info: "5 000 шаблонов", info: "5 000 шаблонов",
expired: true, expired: true,
description: "Тариф на время/ объем/ кастомный или другая информация", description: "Тариф на время/ объем/ кастомный или другая информация",
}, },
{ {
date: "01.03.2022", date: "01.03.2022",
title: "Шаблонизатор", title: "Шаблонизатор",
info: "3 дня", info: "3 дня",
expired: true, expired: true,
description: "Тариф на время/ объем/ кастомный или другая информация", description: "Тариф на время/ объем/ кастомный или другая информация",
}, },
{ {
date: "19.02.2022", date: "19.02.2022",
title: "Сокращатель ссылок", title: "Сокращатель ссылок",
info: "9 месяцев 1 000 шаблонов", info: "9 месяцев 1 000 шаблонов",
expired: true, expired: true,
description: "Тариф на время/ объем/ кастомный или другая информация", 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 { useEffect, useState } from "react"
import { Box, IconButton, Typography, useMediaQuery, useTheme } from "@mui/material"; import { Box, IconButton, Typography, useMediaQuery, useTheme } from "@mui/material"
import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import ArrowBackIcon from "@mui/icons-material/ArrowBack"
import SectionWrapper from "@root/components/SectionWrapper"; import SectionWrapper from "@root/components/SectionWrapper"
import { Select } from "@root/components/Select"; import { Select } from "@root/components/Select"
import { Tabs } from "@root/components/Tabs"; import { Tabs } from "@root/components/Tabs"
import AccordionWrapper from "./AccordionWrapper"; import AccordionWrapper from "./AccordionWrapper"
import { HISTORY } from "./historyMocks"; import { HISTORY } from "./historyMocks"
import { useHistoryTracker } from "@root/utils/hooks/useHistoryTracker"; import { useHistoryTracker } from "@root/utils/hooks/useHistoryTracker"
import { useHistoryData } from "@root/utils/hooks/useHistoryData"; import { useHistoryData } from "@root/utils/hooks/useHistoryData"
import { isArray } from "cypress/types/lodash"; import { isArray } from "cypress/types/lodash"
import { ErrorBoundary } from "react-error-boundary"; import { ErrorBoundary } from "react-error-boundary"
import { handleComponentError } from "@root/utils/handleComponentError"; import { handleComponentError } from "@root/utils/handleComponentError"
const subPages = ["Платежи", "Покупки тарифов", "Окончания тарифов"]; const subPages = ["Платежи", "Покупки тарифов", "Окончания тарифов"]
export default function History() { export default function History() {
const [selectedItem, setSelectedItem] = useState<number>(0); const [selectedItem, setSelectedItem] = useState<number>(0)
const theme = useTheme(); const theme = useTheme()
const upMd = useMediaQuery(theme.breakpoints.up("md")); const upMd = useMediaQuery(theme.breakpoints.up("md"))
const isMobile = useMediaQuery(theme.breakpoints.down(600)); const isMobile = useMediaQuery(theme.breakpoints.down(600))
const isTablet = useMediaQuery(theme.breakpoints.down(1000)); const isTablet = useMediaQuery(theme.breakpoints.down(1000))
const { historyData, error } = useHistoryData(); const { historyData, error } = useHistoryData()
const handleCustomBackNavigation = useHistoryTracker(); const handleCustomBackNavigation = useHistoryTracker()
const extractDateFromString = (tariffName: string) => { const extractDateFromString = (tariffName: string) => {
const dateMatch = tariffName.match(/\d{4}-\d{2}-\d{2}/); const dateMatch = tariffName.match(/\d{4}-\d{2}-\d{2}/)
return dateMatch ? dateMatch[0] : ""; return dateMatch ? dateMatch[0] : ""
}; }
return ( return (
<SectionWrapper <SectionWrapper
maxWidth="lg" maxWidth="lg"
sx={{ sx={{
mt: upMd ? "25px" : "20px", mt: upMd ? "25px" : "20px",
mb: upMd ? "70px" : "37px", mb: upMd ? "70px" : "37px",
px: isTablet ? (isTablet ? "18px" : "40px") : "20px", px: isTablet ? (isTablet ? "18px" : "40px") : "20px",
}} }}
> >
<Box <Box
sx={{ sx={{
mt: "20px", mt: "20px",
mb: isTablet ? "38px" : "20px", mb: isTablet ? "38px" : "20px",
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
gap: "10px", gap: "10px",
}} }}
> >
{isMobile && ( {isMobile && (
<IconButton onClick={handleCustomBackNavigation} sx={{ p: 0, height: "28px", width: "28px", color: "black" }}> <IconButton onClick={handleCustomBackNavigation} sx={{ p: 0, height: "28px", width: "28px", color: "black" }}>
<ArrowBackIcon /> <ArrowBackIcon />
</IconButton> </IconButton>
)} )}
<Typography <Typography
sx={{ sx={{
fontSize: isMobile ? "24px" : "36px", fontSize: isMobile ? "24px" : "36px",
fontWeight: "500", fontWeight: "500",
}} }}
> >
История История
</Typography> </Typography>
</Box> </Box>
{isMobile ? ( {isMobile ? (
<Select items={subPages} selectedItem={selectedItem} setSelectedItem={setSelectedItem} /> <Select items={subPages} selectedItem={selectedItem} setSelectedItem={setSelectedItem} />
) : ( ) : (
<Tabs items={subPages} selectedItem={selectedItem} setSelectedItem={setSelectedItem} /> <Tabs items={subPages} selectedItem={selectedItem} setSelectedItem={setSelectedItem} />
)} )}
<ErrorBoundary <ErrorBoundary
fallback={ fallback={
<Typography mt="8px">Ошибка загрузки истории</Typography> <Typography mt="8px">Ошибка загрузки истории</Typography>
} }
onError={handleComponentError} onError={handleComponentError}
> >
{historyData?.records {historyData?.records
.filter((e) => { .filter((e) => {
e.createdAt = extractDateFromString(e.createdAt) e.createdAt = extractDateFromString(e.createdAt)
return(!e.isDeleted && e.key === "payCart" && Array.isArray(e.rawDetails[0].Value) return(!e.isDeleted && e.key === "payCart" && Array.isArray(e.rawDetails[0].Value)
)}) )})
.map(( e, index) => { .map(( e, index) => {
return ( return (
<Box key={index} sx={{ mt: index === 0 ? "27px" : "0px" }}> <Box key={index} sx={{ mt: index === 0 ? "27px" : "0px" }}>
<AccordionWrapper <AccordionWrapper
first={index === 0} first={index === 0}
last={index === historyData?.records.length - 1} last={index === historyData?.records.length - 1}
content={e.rawDetails} content={e.rawDetails}
key={e.id} key={e.id}
createdAt={e.createdAt} createdAt={e.createdAt}
/> />
</Box> </Box>
)})} )})}
</ErrorBoundary> </ErrorBoundary>
</SectionWrapper> </SectionWrapper>
); )
} }

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

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

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