Compare commits
150 Commits
Author | SHA1 | Date | |
---|---|---|---|
125ce3351b | |||
96ab34c3a0 | |||
bcadda8861 | |||
25829f5a16 | |||
2fd0d63a1c | |||
97b62fb5f8 | |||
8356f4ad3d | |||
f3d0f30a5e | |||
8d82a92707 | |||
b1366656a6 | |||
a1bf5b2b6d | |||
46f0b0aae5 | |||
0c8cacbaed | |||
656e8f8371 | |||
1de2f0f29c | |||
a8b26f46e3 | |||
b7929258f9 | |||
69efd8aa83 | |||
3a9fbb7189 | |||
bffd0f3f55 | |||
371fc689e7 | |||
6ce7e962ab | |||
206820ab6f | |||
e9cce5ee3d | |||
fb91267cf6 | |||
1227f4fecb | |||
9c3ddb1b06 | |||
80d5799571 | |||
f812320698 | |||
e4ed71bd9a | |||
b67a1abf10 | |||
ebaaf06166 | |||
b4258a0d46 | |||
3ef7ad7791 | |||
6142f5acdd | |||
3fc33544f2 | |||
3f0431437d | |||
978c4d66eb | |||
3d0850ebab | |||
98b2a6c1d1 | |||
047c21bd06 | |||
2e849a8d11 | |||
25e80c6196 | |||
fd445ce49d | |||
6b256f37b9 | |||
0ca1fcf98c | |||
aacb44fd00 | |||
f3290036d1 | |||
08a3edb3e3 | |||
230451ff78 | |||
be6f3cdce6 | |||
![]() |
79adf3ca86 | ||
![]() |
761a2d7cea | ||
![]() |
2db710de79 | ||
![]() |
21a3323af9 | ||
![]() |
341bee7ea9 | ||
![]() |
a3a934e404 | ||
![]() |
3a8b8b1b42 | ||
![]() |
5f17e1c730 | ||
![]() |
e3281be590 | ||
![]() |
897316cbc6 | ||
![]() |
e0a88efeaf | ||
![]() |
b3729eb344 | ||
![]() |
ced4f8c613 | ||
![]() |
b24edcad37 | ||
![]() |
3280b82e7c | ||
5327da77ea | |||
![]() |
3aff04bf4a | ||
![]() |
4a21fecdac | ||
![]() |
349cd20480 | ||
8730135e3a | |||
![]() |
9b333f5126 | ||
![]() |
664a0cfb31 | ||
![]() |
cdf7bc4ed4 | ||
![]() |
7af34954c9 | ||
![]() |
3d80cf1ea9 | ||
![]() |
f574b4822b | ||
![]() |
a9c3a7bb31 | ||
![]() |
56afbbefe2 | ||
![]() |
8b469c1b2d | ||
![]() |
b2e9eeff44 | ||
![]() |
f16a659a61 | ||
![]() |
eb7814f3e3 | ||
![]() |
7f2ab8c5a5 | ||
![]() |
31058d1604 | ||
![]() |
77a8ff5cc0 | ||
![]() |
64028a50e2 | ||
![]() |
a72b951202 | ||
![]() |
23d41d5ff0 | ||
![]() |
732004c45d | ||
![]() |
b3d04f4f61 | ||
![]() |
30c5bcc6bd | ||
![]() |
5e4262ef49 | ||
![]() |
b24265246f | ||
![]() |
eaa062302e | ||
![]() |
e6ae21082d | ||
![]() |
b97c70a5f2 | ||
![]() |
49efa9c3b2 | ||
![]() |
77a9346b5a | ||
![]() |
0fadd8fc76 | ||
![]() |
f3b5c1862a | ||
![]() |
06f55de0c3 | ||
![]() |
6b71828653 | ||
![]() |
5231e805bc | ||
![]() |
d215cdc007 | ||
![]() |
9d155a083a | ||
![]() |
9988be9ea3 | ||
![]() |
0aafce2c5d | ||
![]() |
9cd8d50475 | ||
![]() |
e59386d60a | ||
![]() |
e13590d055 | ||
![]() |
2018fa85dd | ||
![]() |
0524a39318 | ||
![]() |
25b24fbeb0 | ||
![]() |
211acfbd98 | ||
![]() |
c15d17e77d | ||
![]() |
84c7efe6b5 | ||
![]() |
96edf676f4 | ||
![]() |
45b532415b | ||
![]() |
264b982ba5 | ||
![]() |
40c598ce9b | ||
![]() |
19cfdbd74e | ||
![]() |
551ada4f5b | ||
![]() |
d5f35ed623 | ||
![]() |
8433c7adcd | ||
![]() |
bdeb1d9765 | ||
![]() |
6b39f34884 | ||
![]() |
460a9f28d1 | ||
![]() |
fb9f17798c | ||
![]() |
7151dc25aa | ||
![]() |
197b9c247a | ||
![]() |
dd62433c6b | ||
![]() |
51a471e8dc | ||
![]() |
4956543b7e | ||
![]() |
b0f2e94ebf | ||
![]() |
4c96954764 | ||
![]() |
f6bc1dbad3 | ||
![]() |
7063bafb28 | ||
![]() |
c1aae18bf8 | ||
![]() |
7c7edd9fe9 | ||
![]() |
e74c7b0fd3 | ||
![]() |
20687d8551 | ||
![]() |
6b2803864e | ||
![]() |
02184d3084 | ||
![]() |
2b8978567b | ||
![]() |
9e28031d9b | ||
![]() |
faa2cdb62a | ||
![]() |
8909a32576 | ||
![]() |
c59be041e7 | ||
![]() |
a9ca5294dd |
25
.eslintrc.cjs
Normal file
25
.eslintrc.cjs
Normal file
@ -0,0 +1,25 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: { browser: true, es2020: true },
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
],
|
||||
ignorePatterns: ['dist', '.eslintrc.cjs', "vite.config.ts"],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['react-refresh'],
|
||||
rules: {
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-unused-vars": "warn",
|
||||
"no-restricted-exports": ["error", {
|
||||
"restrictDefaultExports": {
|
||||
direct: true,
|
||||
}
|
||||
}]
|
||||
},
|
||||
};
|
33
.gitea/workflows/deploy.yml
Normal file
33
.gitea/workflows/deploy.yml
Normal file
@ -0,0 +1,33 @@
|
||||
name: CreateVersion
|
||||
run-name: ${{ gitea.actor }} build image and push to container registry
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
- 'staging'
|
||||
- 'dev'
|
||||
|
||||
jobs:
|
||||
BumpVersion:
|
||||
runs-on: ["skeris"]
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: http://gitea.pena:3000/PenaDevops/actions.git/checkout@v1
|
||||
- name: Publish
|
||||
run: |
|
||||
git config --global user.email "kotilion.95@gmail.com"
|
||||
git config --global user.name "skeris"
|
||||
npm config set @frontend:registry=http://gitea.pena/api/packages/skeris/npm/
|
||||
npm config set registry=https://registry.npmjs.org/
|
||||
npm config set -- '//gitea.pena/api/packages/skeris/npm/:_authToken' "1856e802057f59193ca6fdb4068cbea44982bcc2"
|
||||
if [ "${{ github.ref }}" == "refs/heads/main" ]; then
|
||||
npm version major
|
||||
elif [ "${{ github.ref }}" == "refs/heads/staging" ]; then
|
||||
npm version minor
|
||||
else
|
||||
npm version patch
|
||||
fi
|
||||
npm install --force
|
||||
git push --follow-tags
|
||||
npm publish
|
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
4
.npmrc
4
.npmrc
@ -1,4 +0,0 @@
|
||||
@frontend:kitui=https://penahub.gitlab.yandexcloud.net/api/v4/packages/npm/
|
||||
'//penahub.gitlab.yandexcloud.net/api/v4/packages/npm/:_authToken'="${GITLAB_AUTH_TOKEN}"
|
||||
'//penahub.gitlab.yandexcloud.net/api/v4/projects/21/packages/npm/:_authToken'="${GITLAB_AUTH_TOKEN}"
|
||||
|
3
.yarnrc
Normal file
3
.yarnrc
Normal file
@ -0,0 +1,3 @@
|
||||
"@frontend:registry" "http://gitea.pena/api/packages/skeris/npm/"
|
||||
# Для всех остальных - стандартный npmjs
|
||||
"registry" "https://registry.npmjs.org/"
|
19
README.md
Normal file
19
README.md
Normal file
@ -0,0 +1,19 @@
|
||||
## Перед использованием и публикацией
|
||||
```bash
|
||||
# заменить TOKEN на актуальный токен
|
||||
npm config set //penahub.gitlab.yandexcloud.net/api/v4/packages/npm/:_authToken=TOKEN
|
||||
npm config set //penahub.gitlab.yandexcloud.net/api/v4/projects/21/packages/npm/:_authToken=TOKEN
|
||||
```
|
||||
## Публикация
|
||||
```bash
|
||||
yarn publish
|
||||
```
|
||||
## Использование
|
||||
Добавить в корень проекта файл .yarnrc с содержимым
|
||||
```
|
||||
"@frontend:registry" "https://penahub.gitlab.yandexcloud.net/api/v4/packages/npm/"
|
||||
```
|
||||
Установка
|
||||
```bash
|
||||
yarn add @frontend/kitui
|
||||
```
|
17
index.html
Normal file
17
index.html
Normal file
@ -0,0 +1,17 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React + TS</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Rubik:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
27
lib/api/account.ts
Normal file
27
lib/api/account.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { UserAccount, UserName } from "../model/account";
|
||||
import { makeRequest } from "./makeRequest";
|
||||
|
||||
|
||||
const apiUrl = process.env.REACT_APP_DOMAIN + "/customer";
|
||||
|
||||
export function patchUserAccount(user: UserName, version:string | undefined) {
|
||||
return makeRequest<UserName, UserAccount>({
|
||||
url: `${apiUrl + (version ? `/${version}` : "")}/account`,
|
||||
contentType: true,
|
||||
method: "PATCH",
|
||||
useToken: true,
|
||||
withCredentials: false,
|
||||
body: user,
|
||||
});
|
||||
}
|
||||
|
||||
export function createUserAccount(signal: AbortSignal, url: string | undefined, version: string) {
|
||||
return makeRequest<never, UserAccount>({
|
||||
url: url || `${apiUrl + (version.length > 0 ? `/${version}` : "")}/account`,
|
||||
contentType: true,
|
||||
method: "POST",
|
||||
useToken: true,
|
||||
withCredentials: false,
|
||||
signal,
|
||||
});
|
||||
}
|
4
lib/api/index.ts
Normal file
4
lib/api/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from "./account";
|
||||
export * from "./makeRequest";
|
||||
export * from "./tariff";
|
||||
export * from "./tickets";
|
76
lib/api/makeRequest.ts
Normal file
76
lib/api/makeRequest.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import axios, { AxiosResponse, Method, ResponseType } from "axios";
|
||||
import { getAuthToken, setAuthToken } from "../stores/auth";
|
||||
|
||||
|
||||
export async function makeRequest<TRequest = unknown, TResponse = unknown>({
|
||||
method = "post",
|
||||
url,
|
||||
body,
|
||||
useToken = true,
|
||||
contentType = false,
|
||||
responseType = "json",
|
||||
signal,
|
||||
withCredentials,
|
||||
}: {
|
||||
method?: Method;
|
||||
url: string;
|
||||
body?: TRequest;
|
||||
/** Send access token */
|
||||
useToken?: boolean;
|
||||
contentType?: boolean;
|
||||
responseType?: ResponseType;
|
||||
signal?: AbortSignal;
|
||||
/** Send refresh token */
|
||||
withCredentials?: boolean;
|
||||
}): Promise<TResponse> {
|
||||
const headers: Record<string, string> = {};
|
||||
if (useToken) headers["Authorization"] = getAuthToken() ? `Bearer ${getAuthToken()}` : "";
|
||||
if (contentType) headers["Content-Type"] = "application/json";
|
||||
|
||||
try {
|
||||
const response = await axios<TRequest, AxiosResponse<TResponse & { accessToken?: string; }>>({
|
||||
url,
|
||||
method,
|
||||
headers,
|
||||
data: body,
|
||||
signal,
|
||||
responseType,
|
||||
withCredentials,
|
||||
});
|
||||
|
||||
if (response.data?.accessToken) {
|
||||
setAuthToken(response.data.accessToken);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error) && error.response?.status === 401 && !withCredentials) {
|
||||
const refreshResponse = await refresh(getAuthToken());
|
||||
if (refreshResponse.data?.accessToken) setAuthToken(refreshResponse.data.accessToken);
|
||||
|
||||
headers["Authorization"] = refreshResponse.data.accessToken;
|
||||
const response = await axios.request<TRequest, AxiosResponse<TResponse>>({
|
||||
url,
|
||||
method,
|
||||
headers,
|
||||
data: body,
|
||||
signal,
|
||||
});
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function refresh(token: string) {
|
||||
|
||||
return axios<never, AxiosResponse<{ accessToken: string; }>>(process.env.REACT_APP_DOMAIN + "/auth/refresh", {
|
||||
headers: {
|
||||
"Authorization": `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: "post"
|
||||
});
|
||||
}
|
11
lib/api/tariff.ts
Normal file
11
lib/api/tariff.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { Tariff } from "../model/tariff";
|
||||
import { makeRequest } from "./makeRequest";
|
||||
|
||||
|
||||
export function getTariffById(tariffId:string){
|
||||
return makeRequest<never, Tariff>({
|
||||
url: process.env.REACT_APP_DOMAIN + `/strator/tariff/${tariffId}`,
|
||||
method: "get",
|
||||
useToken: true,
|
||||
});
|
||||
}
|
100
lib/api/tickets.ts
Normal file
100
lib/api/tickets.ts
Normal file
@ -0,0 +1,100 @@
|
||||
import { makeRequest } from "./makeRequest";
|
||||
import type {
|
||||
SendTicketMessageRequest,
|
||||
CreateTicketResponse,
|
||||
SendFileResponse,
|
||||
CreateTicketRequest
|
||||
} from "../model/ticket";
|
||||
|
||||
|
||||
const apiUrl = `${process.env.REACT_APP_DOMAIN}/heruvym/v1.0.0`;
|
||||
|
||||
export const sendTicketMessage = async ({
|
||||
ticketId,
|
||||
message,
|
||||
systemError
|
||||
}: {
|
||||
ticketId: string,
|
||||
message: string,
|
||||
systemError: boolean
|
||||
}): Promise<[null, unknown?]> => {
|
||||
try {
|
||||
const sendTicketMessageResponse = await makeRequest<
|
||||
SendTicketMessageRequest,
|
||||
null
|
||||
>({
|
||||
url: `${apiUrl}/send`,
|
||||
method: "POST",
|
||||
useToken: true,
|
||||
body: { ticket: ticketId, message: message, lang: "ru", files: [], System: systemError },
|
||||
|
||||
});
|
||||
|
||||
return [sendTicketMessageResponse];
|
||||
} catch (nativeError) {
|
||||
return [null, nativeError];
|
||||
}
|
||||
};
|
||||
|
||||
export const shownMessage = async (id: string): Promise<[null, unknown?]> => {
|
||||
try {
|
||||
const shownMessageResponse = await makeRequest<{ id: string }, null>({
|
||||
url: `${apiUrl}/shown`,
|
||||
method: "POST",
|
||||
useToken: true,
|
||||
body: { id },
|
||||
});
|
||||
|
||||
return [shownMessageResponse];
|
||||
} catch (nativeError) {
|
||||
|
||||
return [null, nativeError];
|
||||
}
|
||||
};
|
||||
|
||||
export const sendFile = async ({
|
||||
ticketId,
|
||||
file,
|
||||
}: {
|
||||
ticketId: string,
|
||||
file: File,
|
||||
}): Promise<[SendFileResponse | null, unknown?]> => {
|
||||
try {
|
||||
const body = new FormData();
|
||||
|
||||
body.append(file.name, file);
|
||||
body.append("ticket", ticketId);
|
||||
|
||||
const sendResponse = await makeRequest<FormData, SendFileResponse>({
|
||||
method: "POST",
|
||||
url: `${apiUrl}/sendFiles`,
|
||||
body,
|
||||
});
|
||||
|
||||
return [sendResponse];
|
||||
} catch (nativeError) {
|
||||
return [null, nativeError];
|
||||
}
|
||||
};
|
||||
|
||||
export const createTicket = async ({
|
||||
message,
|
||||
useToken,
|
||||
systemError
|
||||
}: {
|
||||
message: string,
|
||||
useToken: boolean,
|
||||
systemError: boolean
|
||||
}): Promise<[CreateTicketResponse | null, unknown?]> => {
|
||||
try {
|
||||
const createdTicket = await makeRequest<CreateTicketRequest, CreateTicketResponse>({
|
||||
url: `${process.env.REACT_APP_DOMAIN}/heruvym/v1.0.0/create`,
|
||||
body: { Title: "Unauth title", Message: message, System: systemError },
|
||||
useToken,
|
||||
});
|
||||
|
||||
return [createdTicket];
|
||||
} catch (nativeError) {
|
||||
return [null, nativeError];
|
||||
}
|
||||
};
|
59
lib/components/AvatarButton.tsx
Normal file
59
lib/components/AvatarButton.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import { Avatar, IconButton, IconButtonProps, Typography, useTheme } from "@mui/material";
|
||||
import { deepmerge } from "@mui/utils";
|
||||
import { ForwardRefExoticComponent, RefAttributes } from "react";
|
||||
import { LinkProps } from "react-router-dom";
|
||||
|
||||
|
||||
type Props = IconButtonProps & {
|
||||
component?: ForwardRefExoticComponent<LinkProps & RefAttributes<HTMLAnchorElement>>;
|
||||
to?: string;
|
||||
};
|
||||
|
||||
export function AvatarButton(props: Props) {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
{...deepmerge({
|
||||
sx: {
|
||||
height: 36,
|
||||
width: 36,
|
||||
p: 0,
|
||||
"&:hover .MuiAvatar-root": {
|
||||
border: `2px solid ${theme.palette.gray.main}`,
|
||||
},
|
||||
"&:active .MuiAvatar-root": {
|
||||
backgroundColor: theme.palette.purple.main,
|
||||
color: theme.palette.purple.main,
|
||||
border: "1px solid black",
|
||||
},
|
||||
}
|
||||
}, props)}
|
||||
>
|
||||
<Avatar sx={{
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
backgroundColor: theme.palette.orange.main,
|
||||
color: theme.palette.orange.light,
|
||||
transition: "all 100ms",
|
||||
}}>
|
||||
<Typography sx={{
|
||||
fontWeight: 500,
|
||||
fontSize: "14px",
|
||||
lineHeight: "20px",
|
||||
zIndex: 1,
|
||||
textTransform: "uppercase",
|
||||
position: "absolute",
|
||||
color: "white",
|
||||
}}>
|
||||
{props.children ?? "AA"}
|
||||
</Typography>
|
||||
<svg width="100%" height="100%" viewBox="0 0 36 37" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M15.348 16.146c.1-5.981 6.823-10.034 12.62-11.479 6.055-1.508 13.264-.719 17.31 4.023 3.673 4.303.83 10.565-.085 16.16-.678 4.152-1.209 8.41-4.383 11.171-3.418 2.973-8.742 6.062-12.43 3.452-3.663-2.593 1.412-8.88-.78-12.8-2.764-4.95-12.347-4.85-12.252-10.527Z" fill="currentColor" />
|
||||
<circle cx="28.052" cy="-3.333" r="5.519" transform="rotate(-32.339 28.052 -3.333)" fill="currentColor" />
|
||||
<circle cx="24.363" cy="29.03" r="1.27" transform="rotate(-32.339 24.363 29.03)" fill="currentColor" />
|
||||
</svg>
|
||||
</Avatar>
|
||||
</IconButton>
|
||||
);
|
||||
}
|
31
lib/components/BurgerButton.tsx
Normal file
31
lib/components/BurgerButton.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import { IconButton, IconButtonProps, useTheme } from "@mui/material";
|
||||
import { deepmerge } from "@mui/utils";
|
||||
|
||||
|
||||
export function BurgerButton(props: IconButtonProps) {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
{...deepmerge({
|
||||
sx: {
|
||||
height: 30,
|
||||
width: 30,
|
||||
p: 0,
|
||||
color: "black",
|
||||
"&:hover": {
|
||||
color: theme.palette.purple.main,
|
||||
backgroundColor: "rgb(0 0 0 / 0)",
|
||||
},
|
||||
"&:active": {
|
||||
color: theme.palette.purple.main,
|
||||
},
|
||||
},
|
||||
}, props)}
|
||||
>
|
||||
<svg width="30" height="31" viewBox="0 0 30 31" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M28 8.005H3M28 16.005H3M28 24.005H3" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
||||
</svg>
|
||||
</IconButton>
|
||||
);
|
||||
}
|
31
lib/components/CloseButton.tsx
Normal file
31
lib/components/CloseButton.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import { IconButton, IconButtonProps, useTheme } from "@mui/material";
|
||||
import { deepmerge } from "@mui/utils";
|
||||
|
||||
|
||||
export function CloseButton(props: IconButtonProps) {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
{...deepmerge({
|
||||
sx: {
|
||||
height: 30,
|
||||
width: 30,
|
||||
p: 0,
|
||||
color: "black",
|
||||
"&:hover": {
|
||||
color: theme.palette.orange.main,
|
||||
backgroundColor: "rgb(0 0 0 / 0)",
|
||||
},
|
||||
"&:active": {
|
||||
color: theme.palette.orange.main,
|
||||
},
|
||||
},
|
||||
}, props)}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 30 31" fill="none">
|
||||
<path d="m3 3.605 24 24m-24 0 24-24" stroke="currentColor" />
|
||||
</svg>
|
||||
</IconButton>
|
||||
);
|
||||
}
|
31
lib/components/CloseButtonSmall.tsx
Normal file
31
lib/components/CloseButtonSmall.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import { IconButton, IconButtonProps, useTheme } from "@mui/material";
|
||||
import { deepmerge } from "@mui/utils";
|
||||
|
||||
|
||||
export function CloseButtonSmall(props: IconButtonProps) {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
{...deepmerge({
|
||||
sx: {
|
||||
height: 12,
|
||||
width: 12,
|
||||
p: 0,
|
||||
color: theme.palette.purple.main,
|
||||
"&:hover": {
|
||||
color: theme.palette.orange.main,
|
||||
backgroundColor: "rgb(0 0 0 / 0)",
|
||||
},
|
||||
"&:active": {
|
||||
color: theme.palette.orange.main,
|
||||
},
|
||||
},
|
||||
}, props)}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 14 14" fill="none">
|
||||
<path d="m13 1.176-12 12M13 13.176l-12-12" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</IconButton>
|
||||
);
|
||||
}
|
39
lib/components/LogoutButton.tsx
Normal file
39
lib/components/LogoutButton.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import { IconButton, IconButtonProps, useTheme } from "@mui/material";
|
||||
import { deepmerge } from "@mui/utils";
|
||||
|
||||
|
||||
export function LogoutButton(props: IconButtonProps) {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
{...deepmerge({
|
||||
sx: {
|
||||
height: 36,
|
||||
width: 36,
|
||||
p: 0,
|
||||
borderRadius: "6px",
|
||||
backgroundColor: theme.palette.background.default,
|
||||
color: theme.palette.gray.main,
|
||||
"&:hover": {
|
||||
color: theme.palette.background.default,
|
||||
backgroundColor: theme.palette.gray.main,
|
||||
},
|
||||
"&:active": {
|
||||
backgroundColor: theme.palette.purple.main,
|
||||
color: "white",
|
||||
},
|
||||
}
|
||||
}, props)}
|
||||
>
|
||||
<svg width="100%" height="100%" viewBox="0 0 36 37" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M22.559 22.052v2.95a2 2 0 0 1-2 2h-6.865a2 2 0 0 1-2-2v-12.5a2 2 0 0 1 2-2h6.865a2 2 0 0 1 2 2v2.95M25.067 21.227l1.786-1.763a1 1 0 0 0 0-1.423l-1.786-1.764M26.737 18.752H16.71"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
</IconButton>
|
||||
);
|
||||
}
|
34
lib/components/PenaLink.tsx
Normal file
34
lib/components/PenaLink.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import { Link, LinkProps, LinkTypeMap, Typography, useTheme } from "@mui/material";
|
||||
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
|
||||
import { deepmerge } from "@mui/utils";
|
||||
import { OverridableComponent } from "@mui/material/OverridableComponent";
|
||||
|
||||
|
||||
export const PenaLink: OverridableComponent<LinkTypeMap<object, "a">> = (props: LinkProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Link
|
||||
{...deepmerge({
|
||||
sx: {
|
||||
display: "flex",
|
||||
gap: "3px",
|
||||
textUnderlinePosition: "under",
|
||||
color: theme.palette.purple.light,
|
||||
textDecorationColor: theme.palette.purple.main,
|
||||
textUnderlineOffset: "2px",
|
||||
"&:hover": {
|
||||
textDecorationColor: theme.palette.purple.light,
|
||||
},
|
||||
"&:active": {
|
||||
color: "white",
|
||||
textDecorationColor: "white",
|
||||
},
|
||||
}
|
||||
}, props)}
|
||||
>
|
||||
<Typography variant="body2">{props.children}</Typography>
|
||||
<ArrowForwardIcon sx={{ height: "20px", width: "20px" }} />
|
||||
</Link>
|
||||
);
|
||||
};
|
120
lib/components/PenaTextField.tsx
Normal file
120
lib/components/PenaTextField.tsx
Normal file
@ -0,0 +1,120 @@
|
||||
import { FormControl, InputLabel, SxProps, TextField, TextFieldProps, Theme, useMediaQuery, useTheme } from "@mui/material";
|
||||
|
||||
|
||||
interface Props {
|
||||
id?: string;
|
||||
label?: string;
|
||||
labelSx?: SxProps<Theme>;
|
||||
bold?: boolean;
|
||||
gap?: string;
|
||||
backgroundColor?: string;
|
||||
FormControlSx?: SxProps<Theme>;
|
||||
TextFieldSx?: SxProps<Theme>;
|
||||
placeholder?: TextFieldProps["placeholder"];
|
||||
value?: TextFieldProps["value"];
|
||||
helperText?: TextFieldProps["helperText"];
|
||||
error?: TextFieldProps["error"];
|
||||
type?: TextFieldProps["type"];
|
||||
onBlur?: TextFieldProps["onBlur"];
|
||||
onChange?: TextFieldProps["onChange"];
|
||||
fullWidth?: boolean;
|
||||
}
|
||||
|
||||
export function PenaTextField({
|
||||
id = "pena-textfield",
|
||||
label,
|
||||
labelSx,
|
||||
bold = false,
|
||||
gap = "10px",
|
||||
onChange,
|
||||
error,
|
||||
helperText,
|
||||
onBlur,
|
||||
placeholder,
|
||||
type,
|
||||
value,
|
||||
backgroundColor,
|
||||
FormControlSx,
|
||||
TextFieldSx,
|
||||
fullWidth = true,
|
||||
}: Props) {
|
||||
const theme = useTheme();
|
||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||
|
||||
const labelFont = upMd
|
||||
? bold
|
||||
? theme.typography.p1
|
||||
: { ...theme.typography.body1, fontWeight: 500 }
|
||||
: theme.typography.body2;
|
||||
|
||||
const placeholderFont = upMd ? undefined : { fontWeight: 400, fontSize: "16px", lineHeight: "19px" };
|
||||
|
||||
return (
|
||||
<FormControl
|
||||
fullWidth={fullWidth}
|
||||
variant="standard"
|
||||
sx={{
|
||||
gap,
|
||||
...FormControlSx,
|
||||
}}
|
||||
>
|
||||
{label && (
|
||||
<InputLabel
|
||||
shrink
|
||||
htmlFor={id}
|
||||
sx={{
|
||||
position: "inherit",
|
||||
color: "black",
|
||||
transform: "none",
|
||||
...labelFont,
|
||||
...labelSx,
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</InputLabel>
|
||||
)}
|
||||
<TextField
|
||||
fullWidth
|
||||
id={id}
|
||||
error={error}
|
||||
helperText={helperText}
|
||||
onBlur={onBlur}
|
||||
placeholder={placeholder}
|
||||
type={type}
|
||||
value={value}
|
||||
sx={{
|
||||
"& .MuiInputBase-root": {
|
||||
height: "48px",
|
||||
borderRadius: "8px",
|
||||
"& fieldset": {
|
||||
border: `1px solid ${theme.palette.gray.main}`,
|
||||
},
|
||||
"&:hover fieldset": {
|
||||
border: `1px solid ${theme.palette.gray.dark}`,
|
||||
},
|
||||
"&.Mui-focused fieldset": {
|
||||
border: `2px solid ${theme.palette.purple.main}`,
|
||||
},
|
||||
},
|
||||
"& .MuiFormHelperText-root.MuiFormHelperText-contained.MuiFormHelperText-filled.Mui-error": {
|
||||
position: "absolute",
|
||||
top: "45px",
|
||||
},
|
||||
...TextFieldSx,
|
||||
}}
|
||||
inputProps={{
|
||||
sx: {
|
||||
boxSizing: "border-box",
|
||||
backgroundColor: backgroundColor ?? theme.palette.background.default,
|
||||
borderRadius: "8px",
|
||||
height: "48px",
|
||||
py: 0,
|
||||
color: theme.palette.gray.dark,
|
||||
...placeholderFont,
|
||||
},
|
||||
}}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}
|
41
lib/components/WalletButton.tsx
Normal file
41
lib/components/WalletButton.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import { IconButton, IconButtonProps, useTheme } from "@mui/material";
|
||||
import { ForwardRefExoticComponent, RefAttributes } from "react";
|
||||
import { LinkProps } from "react-router-dom";
|
||||
import { deepmerge } from "@mui/utils";
|
||||
|
||||
|
||||
type Props = IconButtonProps & {
|
||||
component?: ForwardRefExoticComponent<LinkProps & RefAttributes<HTMLAnchorElement>>;
|
||||
to?: string;
|
||||
};
|
||||
|
||||
export function WalletButton(props: Props) {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
{...deepmerge({
|
||||
sx: {
|
||||
height: 36,
|
||||
width: 36,
|
||||
p: 0,
|
||||
borderRadius: "6px",
|
||||
backgroundColor: theme.palette.background.default,
|
||||
color: theme.palette.gray.main,
|
||||
"&:hover": {
|
||||
color: theme.palette.background.default,
|
||||
backgroundColor: theme.palette.gray.main,
|
||||
},
|
||||
"&:active": {
|
||||
backgroundColor: theme.palette.purple.main,
|
||||
color: "white",
|
||||
},
|
||||
},
|
||||
}, props)}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 36 37" fill="none">
|
||||
<path d="M26.571 16.051v-5c0-.789-.64-1.428-1.428-1.428H9.429c-.79 0-1.429.64-1.429 1.428v14.286c0 .789.64 1.428 1.429 1.428h15.714c.789 0 1.428-.64 1.428-1.428v-5m1.33-5h-7.044a2.857 2.857 0 0 0 0 5.714h7.044a.099.099 0 0 0 .099-.099v-5.516a.1.1 0 0 0-.099-.1Z" stroke="currentColor" strokeWidth="1.5" />
|
||||
</svg>
|
||||
</IconButton>
|
||||
);
|
||||
}
|
9
lib/components/index.ts
Normal file
9
lib/components/index.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export * from "./AvatarButton";
|
||||
export * from "./BurgerButton";
|
||||
export * from "./CloseButton";
|
||||
export * from "./CloseButtonSmall";
|
||||
export * from "./LogoutButton";
|
||||
export * from "./PenaLink";
|
||||
export * from "./PenaTextField";
|
||||
export * from "./theme";
|
||||
export * from "./WalletButton";
|
612
lib/components/theme.ts
Normal file
612
lib/components/theme.ts
Normal file
@ -0,0 +1,612 @@
|
||||
import { createTheme } from "@mui/material";
|
||||
|
||||
|
||||
export const penaMuiTheme = createTheme({
|
||||
breakpoints: {
|
||||
values: {
|
||||
xs: 300,
|
||||
sm: 560,
|
||||
md: 900,
|
||||
lg: 1200,
|
||||
xl: 1536,
|
||||
},
|
||||
},
|
||||
palette: {
|
||||
mode: "light",
|
||||
primary: {
|
||||
main: "#000000",
|
||||
},
|
||||
secondary: {
|
||||
main: "#252734",
|
||||
},
|
||||
text: {
|
||||
primary: "#000000",
|
||||
secondary: "#7E2AEA",
|
||||
},
|
||||
background: {
|
||||
default: "#F2F3F7",
|
||||
},
|
||||
purple: {
|
||||
main: "#7E2AEA",
|
||||
dark: "#581CA7",
|
||||
light: "#944FEE",
|
||||
},
|
||||
bg: {
|
||||
main: "#333647",
|
||||
dark: "#252734",
|
||||
},
|
||||
gray: {
|
||||
main: "#9A9AAF",
|
||||
dark: "#4D4D4D",
|
||||
},
|
||||
orange: {
|
||||
main: "#FB5607",
|
||||
light: "#FC712F",
|
||||
},
|
||||
},
|
||||
components: {
|
||||
MuiButton: {
|
||||
variants: [
|
||||
{
|
||||
props: { variant: "pena-contained-dark" },
|
||||
style: ({ theme }) => theme.unstable_sx({
|
||||
minWidth: "180px",
|
||||
py: "9px",
|
||||
px: "43px",
|
||||
borderRadius: "8px",
|
||||
boxShadow: "none",
|
||||
fontSize: "18px",
|
||||
lineHeight: "24px",
|
||||
fontWeight: 400,
|
||||
textTransform: "none",
|
||||
color: "white",
|
||||
backgroundColor: theme.palette.purple.main,
|
||||
border: `1px solid ${theme.palette.purple.main}`,
|
||||
"&:hover": {
|
||||
color: "white",
|
||||
backgroundColor: theme.palette.purple.light,
|
||||
border: `1px solid ${theme.palette.purple.light}`,
|
||||
},
|
||||
"&:active": {
|
||||
color: theme.palette.purple.main,
|
||||
backgroundColor: "white",
|
||||
border: `1px solid ${theme.palette.purple.main}`,
|
||||
}
|
||||
}),
|
||||
},
|
||||
{
|
||||
props: { variant: "pena-outlined-dark" },
|
||||
style: ({ theme }) => theme.unstable_sx({
|
||||
minWidth: "180px",
|
||||
py: "9px",
|
||||
px: "43px",
|
||||
borderRadius: "8px",
|
||||
boxShadow: "none",
|
||||
fontSize: "18px",
|
||||
lineHeight: "24px",
|
||||
fontWeight: 400,
|
||||
textTransform: "none",
|
||||
color: "white",
|
||||
backgroundColor: "rgb(0 0 0 / 0)",
|
||||
border: `1px solid white`,
|
||||
"&:hover": {
|
||||
color: "white",
|
||||
backgroundColor: theme.palette.bg.dark,
|
||||
border: `1px solid white`,
|
||||
},
|
||||
"&:active": {
|
||||
color: "white",
|
||||
backgroundColor: theme.palette.purple.main,
|
||||
border: `1px solid ${theme.palette.purple.main}`,
|
||||
}
|
||||
}),
|
||||
},
|
||||
{
|
||||
props: { variant: "pena-contained-light" },
|
||||
style: ({ theme }) => theme.unstable_sx({
|
||||
minWidth: "180px",
|
||||
py: "9px",
|
||||
px: "43px",
|
||||
borderRadius: "8px",
|
||||
boxShadow: "none",
|
||||
fontSize: "18px",
|
||||
lineHeight: "24px",
|
||||
fontWeight: 400,
|
||||
textTransform: "none",
|
||||
color: theme.palette.bg.dark,
|
||||
backgroundColor: "white",
|
||||
border: `1px solid white`,
|
||||
"&:hover": {
|
||||
color: theme.palette.bg.dark,
|
||||
backgroundColor: theme.palette.background.default,
|
||||
border: `1px solid ${theme.palette.background.default}`,
|
||||
},
|
||||
"&:active": {
|
||||
color: "white",
|
||||
backgroundColor: "black",
|
||||
border: `1px solid black`,
|
||||
}
|
||||
}),
|
||||
},
|
||||
{
|
||||
props: { variant: "pena-outlined-light" },
|
||||
style: ({ theme }) => theme.unstable_sx({
|
||||
minWidth: "180px",
|
||||
py: "9px",
|
||||
px: "43px",
|
||||
borderRadius: "8px",
|
||||
boxShadow: "none",
|
||||
fontSize: "18px",
|
||||
lineHeight: "24px",
|
||||
fontWeight: 400,
|
||||
textTransform: "none",
|
||||
color: "white",
|
||||
backgroundColor: "rgb(0 0 0 / 0)",
|
||||
border: `1px solid white`,
|
||||
"&:hover": {
|
||||
color: "white",
|
||||
backgroundColor: "#581CA7",
|
||||
border: `1px solid white`,
|
||||
},
|
||||
"&:active": {
|
||||
color: "white",
|
||||
backgroundColor: "black",
|
||||
border: `1px solid black`,
|
||||
}
|
||||
}),
|
||||
},
|
||||
{
|
||||
props: { variant: "pena-outlined-purple" },
|
||||
style: ({ theme }) => theme.unstable_sx({
|
||||
minWidth: "180px",
|
||||
py: "9px",
|
||||
px: "43px",
|
||||
borderRadius: "8px",
|
||||
boxShadow: "none",
|
||||
fontSize: "18px",
|
||||
lineHeight: "24px",
|
||||
fontWeight: 400,
|
||||
textTransform: "none",
|
||||
color: theme.palette.purple.main,
|
||||
backgroundColor: "rgb(0 0 0 / 0)",
|
||||
border: `1px solid ${theme.palette.purple.main}`,
|
||||
"&:hover": {
|
||||
color: theme.palette.purple.main,
|
||||
backgroundColor: theme.palette.background.default,
|
||||
border: `1px solid ${theme.palette.purple.main}`,
|
||||
},
|
||||
"&:active": {
|
||||
color: "white",
|
||||
backgroundColor: theme.palette.purple.dark,
|
||||
border: `1px solid ${theme.palette.purple.dark}`,
|
||||
}
|
||||
}),
|
||||
},
|
||||
{
|
||||
props: { variant: "pena-navitem-dark" },
|
||||
style: ({ theme }) => theme.unstable_sx({
|
||||
p: 0,
|
||||
boxShadow: "none",
|
||||
fontSize: "16px",
|
||||
lineHeight: "20px",
|
||||
fontWeight: 500,
|
||||
textTransform: "none",
|
||||
whiteSpace: "nowrap",
|
||||
color: "white",
|
||||
backgroundColor: "rgb(0 0 0 / 0)",
|
||||
"&:hover": {
|
||||
color: theme.palette.purple.light,
|
||||
backgroundColor: "rgb(0 0 0 / 0)",
|
||||
},
|
||||
"&:active": {
|
||||
color: theme.palette.purple.light,
|
||||
backgroundColor: "rgb(0 0 0 / 0)",
|
||||
}
|
||||
}),
|
||||
},
|
||||
{
|
||||
props: { variant: "pena-navitem-light" },
|
||||
style: ({ theme }) => theme.unstable_sx({
|
||||
p: 0,
|
||||
boxShadow: "none",
|
||||
fontSize: "16px",
|
||||
lineHeight: "20px",
|
||||
fontWeight: 500,
|
||||
textTransform: "none",
|
||||
whiteSpace: "nowrap",
|
||||
color: "black",
|
||||
backgroundColor: "rgb(0 0 0 / 0)",
|
||||
"&:hover": {
|
||||
color: theme.palette.purple.main,
|
||||
backgroundColor: "rgb(0 0 0 / 0)",
|
||||
},
|
||||
"&:active": {
|
||||
color: theme.palette.purple.main,
|
||||
backgroundColor: "rgb(0 0 0 / 0)",
|
||||
}
|
||||
}),
|
||||
},
|
||||
{
|
||||
props: { variant: "pena-contained-white1" },
|
||||
style: ({ theme }) => theme.unstable_sx({
|
||||
minWidth: "180px",
|
||||
py: "9px",
|
||||
px: "43px",
|
||||
borderRadius: "8px",
|
||||
boxShadow: "none",
|
||||
fontSize: "18px",
|
||||
lineHeight: "24px",
|
||||
fontWeight: 400,
|
||||
textTransform: "none",
|
||||
color: theme.palette.purple.main,
|
||||
backgroundColor: theme.palette.background.default,
|
||||
border: `1px solid ${theme.palette.gray.main}`,
|
||||
"&:hover": {
|
||||
color: "white",
|
||||
backgroundColor: theme.palette.purple.main,
|
||||
border: `1px solid ${theme.palette.purple.main}`,
|
||||
},
|
||||
"&:active": {
|
||||
color: "white",
|
||||
backgroundColor: "black",
|
||||
border: `1px solid black`,
|
||||
}
|
||||
}),
|
||||
},
|
||||
{
|
||||
props: { variant: "pena-contained-white2" },
|
||||
style: ({ theme }) => theme.unstable_sx({
|
||||
minWidth: "180px",
|
||||
py: "9px",
|
||||
px: "43px",
|
||||
borderRadius: "8px",
|
||||
boxShadow: "none",
|
||||
fontSize: "18px",
|
||||
lineHeight: "24px",
|
||||
fontWeight: 400,
|
||||
textTransform: "none",
|
||||
color: "black",
|
||||
backgroundColor: theme.palette.background.default,
|
||||
border: `1px solid ${theme.palette.background.default}`,
|
||||
"&:hover": {
|
||||
color: "white",
|
||||
backgroundColor: theme.palette.purple.light,
|
||||
border: `1px solid white`,
|
||||
},
|
||||
"&:active": {
|
||||
color: theme.palette.purple.main,
|
||||
backgroundColor: "white",
|
||||
border: `1px solid ${theme.palette.purple.light}`,
|
||||
}
|
||||
}),
|
||||
},
|
||||
{
|
||||
props: {
|
||||
variant: "pena-text",
|
||||
},
|
||||
style: ({ theme }) => ({
|
||||
color: theme.palette.purple.main,
|
||||
padding: 0,
|
||||
textTransform: "none",
|
||||
textDecoration: "underline",
|
||||
textUnderlineOffset: "7px",
|
||||
fontSize: "16px",
|
||||
fontWeight: 500,
|
||||
lineHeight: "20px",
|
||||
}),
|
||||
},
|
||||
],
|
||||
defaultProps: {
|
||||
disableTouchRipple: true,
|
||||
},
|
||||
},
|
||||
MuiIconButton: {
|
||||
defaultProps: {
|
||||
disableTouchRipple: true,
|
||||
},
|
||||
},
|
||||
MuiTypography: {
|
||||
defaultProps: {
|
||||
variantMapping: {
|
||||
p1: "p",
|
||||
t1: "p",
|
||||
"pena-h1": "h1",
|
||||
"pena-card-header1": "h5",
|
||||
}
|
||||
},
|
||||
},
|
||||
MuiAlert: {
|
||||
styleOverrides: {
|
||||
filledError: {
|
||||
backgroundColor: "#FB5607",
|
||||
},
|
||||
root: {
|
||||
borderRadius: "8px",
|
||||
}
|
||||
}
|
||||
},
|
||||
MuiPagination: {
|
||||
variants: [
|
||||
{
|
||||
props: { variant: "pena-pagination" },
|
||||
style: {
|
||||
marginRight: "-15px",
|
||||
marginLeft: "-15px",
|
||||
"& .MuiPaginationItem-root": {
|
||||
height: "30px",
|
||||
width: "30px",
|
||||
minWidth: "30px",
|
||||
marginLeft: "5px",
|
||||
marginRight: "5px",
|
||||
backgroundColor: "white",
|
||||
color: "black",
|
||||
fontSize: "16px",
|
||||
lineHeight: "20px",
|
||||
fontWeight: 400,
|
||||
borderRadius: "5px",
|
||||
"&.Mui-selected": {
|
||||
backgroundColor: "white",
|
||||
color: "#7E2AEA",
|
||||
fontWeight: 500,
|
||||
},
|
||||
"&:hover": {
|
||||
backgroundColor: "#ffffff55",
|
||||
},
|
||||
"&:active": {
|
||||
backgroundColor: "#7F2CEA",
|
||||
color: "white",
|
||||
},
|
||||
boxShadow: `
|
||||
0px 77.2727px 238.773px rgba(210, 208, 225, 0.24),
|
||||
0px 32.2827px 99.7535px rgba(210, 208, 225, 0.172525),
|
||||
0px 17.2599px 53.333px rgba(210, 208, 225, 0.143066),
|
||||
0px 9.67574px 29.8981px rgba(210, 208, 225, 0.12),
|
||||
0px 5.13872px 15.8786px rgba(210, 208, 225, 0.0969343),
|
||||
0px 2.13833px 6.60745px rgba(210, 208, 225, 0.0674749)
|
||||
`,
|
||||
},
|
||||
"& .MuiPaginationItem-previousNext": {
|
||||
backgroundColor: "#7E2AEA",
|
||||
color: "white",
|
||||
marginLeft: "15px",
|
||||
marginRight: "15px",
|
||||
"&:hover": {
|
||||
backgroundColor: "#995DED",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
],
|
||||
},
|
||||
MuiSwitch: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
color: "#7E2AEA",
|
||||
height: "50px",
|
||||
width: "69px",
|
||||
"& .MuiSwitch-switchBase.Mui-checked+.MuiSwitch-track": {
|
||||
backgroundColor: "#7E2AEA",
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
track: {
|
||||
height: "12px",
|
||||
alignSelf: "center",
|
||||
backgroundColor: "#00000000",
|
||||
opacity: 1,
|
||||
border: "1px solid #9A9AAF",
|
||||
},
|
||||
thumb: {
|
||||
height: "32px",
|
||||
width: "32px",
|
||||
border: `6px solid #7E2AEA`,
|
||||
backgroundColor: "white",
|
||||
boxShadow: `0px 0px 0px 3px white,
|
||||
0px 4px 4px 3px #C3C8DD
|
||||
`,
|
||||
"&:focus, &:hover, &.Mui-active, &.Mui-focusVisible": {
|
||||
boxShadow: `0px 0px 0px 3px white,
|
||||
0px 4px 4px 3px #C3C8DD
|
||||
`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
typography: palette => ({
|
||||
h5: {
|
||||
fontSize: "24px",
|
||||
lineHeight: "28.44px",
|
||||
fontWeight: 500,
|
||||
},
|
||||
button: {
|
||||
fontSize: "18px",
|
||||
lineHeight: "24px",
|
||||
fontWeight: 400,
|
||||
textTransform: "none",
|
||||
},
|
||||
body1: {
|
||||
fontSize: "18px",
|
||||
lineHeight: "21.33px",
|
||||
fontWeight: 400,
|
||||
},
|
||||
body2: {
|
||||
fontSize: "16px",
|
||||
lineHeight: "20px",
|
||||
fontWeight: 500,
|
||||
},
|
||||
p1: {
|
||||
fontSize: "20px",
|
||||
lineHeight: "24px",
|
||||
fontWeight: 500,
|
||||
},
|
||||
price: {
|
||||
fontWeight: 500,
|
||||
fontSize: "20px",
|
||||
lineHeight: "24px",
|
||||
color: palette.gray.dark,
|
||||
},
|
||||
oldPrice: {
|
||||
fontWeight: 400,
|
||||
fontSize: "18px",
|
||||
lineHeight: "21px",
|
||||
textDecorationLine: "line-through",
|
||||
color: palette.orange.main,
|
||||
},
|
||||
t1: {
|
||||
display: "block",
|
||||
fontWeight: 400,
|
||||
fontSize: "18px",
|
||||
lineHeight: "21.33px",
|
||||
},
|
||||
fontFamily: [
|
||||
"Rubik",
|
||||
"-apple-system",
|
||||
"BlinkMacSystemFont",
|
||||
'"Segoe UI"',
|
||||
'"Helvetica Neue"',
|
||||
"Arial",
|
||||
"sans-serif",
|
||||
'"Apple Color Emoji"',
|
||||
'"Segoe UI Emoji"',
|
||||
'"Segoe UI Symbol"',
|
||||
].join(","),
|
||||
}),
|
||||
});
|
||||
|
||||
penaMuiTheme.typography["pena-h1"] = {
|
||||
fontSize: "70px",
|
||||
fontWeight: 500,
|
||||
lineHeight: "100%",
|
||||
[penaMuiTheme.breakpoints.down("md")]: {
|
||||
fontSize: "36px",
|
||||
lineHeight: "100%",
|
||||
},
|
||||
};
|
||||
|
||||
penaMuiTheme.typography["pena-h3"] = {
|
||||
color: "#000000",
|
||||
fontWeight: 500,
|
||||
fontSize: "36px",
|
||||
lineHeight: "100%",
|
||||
[penaMuiTheme.breakpoints.down("md")]: {
|
||||
fontSize: "30px",
|
||||
lineHeight: "100%",
|
||||
},
|
||||
};
|
||||
|
||||
penaMuiTheme.typography["pena-card-header1"] = {
|
||||
fontWeight: 500,
|
||||
fontSize: "24px",
|
||||
lineHeight: "100%",
|
||||
[penaMuiTheme.breakpoints.down("md")]: {
|
||||
fontSize: "21px",
|
||||
lineHeight: "100%",
|
||||
},
|
||||
};
|
||||
|
||||
penaMuiTheme.typography.h2 = {
|
||||
fontSize: "70px",
|
||||
lineHeight: "70px",
|
||||
fontWeight: 500,
|
||||
[penaMuiTheme.breakpoints.down("md")]: {
|
||||
fontSize: "42px",
|
||||
lineHeight: "50px",
|
||||
}
|
||||
};
|
||||
|
||||
penaMuiTheme.typography.h4 = {
|
||||
fontSize: "36px",
|
||||
lineHeight: "42.66px",
|
||||
fontWeight: 500,
|
||||
[penaMuiTheme.breakpoints.down("md")]: {
|
||||
fontSize: "24px",
|
||||
lineHeight: "28.44px",
|
||||
}
|
||||
};
|
||||
|
||||
penaMuiTheme.typography.infographic = {
|
||||
fontSize: "80px",
|
||||
lineHeight: "94.8px",
|
||||
fontWeight: 400,
|
||||
[penaMuiTheme.breakpoints.down("md")]: {
|
||||
fontSize: "50px",
|
||||
lineHeight: "59px",
|
||||
fontWeight: 400,
|
||||
}
|
||||
};
|
||||
|
||||
declare module '@mui/material/Button' {
|
||||
interface ButtonPropsVariantOverrides {
|
||||
"pena-contained-light": true;
|
||||
"pena-outlined-light": true;
|
||||
"pena-contained-dark": true;
|
||||
"pena-outlined-dark": true;
|
||||
"pena-outlined-purple": true;
|
||||
"pena-navitem-light": true;
|
||||
"pena-navitem-dark": true;
|
||||
"pena-contained-white1": true;
|
||||
"pena-contained-white2": true;
|
||||
"pena-text": true;
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@mui/material/Pagination' {
|
||||
interface PaginationPropsVariantOverrides {
|
||||
"pena-pagination": true;
|
||||
}
|
||||
}
|
||||
|
||||
declare module "@mui/material/styles" {
|
||||
interface Palette {
|
||||
purple: Palette["primary"],
|
||||
bg: Palette["primary"],
|
||||
gray: Palette["primary"],
|
||||
orange: Palette["primary"],
|
||||
}
|
||||
interface PaletteOptions {
|
||||
purple?: PaletteOptions["primary"],
|
||||
bg?: PaletteOptions["primary"],
|
||||
gray?: PaletteOptions["primary"],
|
||||
orange?: PaletteOptions["primary"],
|
||||
}
|
||||
interface TypographyVariants {
|
||||
infographic: React.CSSProperties;
|
||||
p1: React.CSSProperties;
|
||||
price: React.CSSProperties;
|
||||
oldPrice: React.CSSProperties;
|
||||
t1: React.CSSProperties;
|
||||
"pena-card-header1": React.CSSProperties;
|
||||
"pena-h1": React.CSSProperties;
|
||||
"pena-h3": React.CSSProperties;
|
||||
}
|
||||
interface TypographyVariantsOptions {
|
||||
infographic?: React.CSSProperties;
|
||||
p1?: React.CSSProperties;
|
||||
price?: React.CSSProperties;
|
||||
oldPrice?: React.CSSProperties;
|
||||
t1?: React.CSSProperties;
|
||||
"pena-card-header1"?: React.CSSProperties;
|
||||
"pena-h1"?: React.CSSProperties;
|
||||
"pena-h3"?: React.CSSProperties;
|
||||
}
|
||||
}
|
||||
|
||||
declare module "@mui/material/Typography" {
|
||||
interface TypographyPropsVariantOverrides {
|
||||
infographic: true;
|
||||
p1: true;
|
||||
price: true;
|
||||
oldPrice: true;
|
||||
t1: true;
|
||||
"pena-card-header1": true;
|
||||
"pena-h1": true;
|
||||
"pena-h3": true;
|
||||
}
|
||||
}
|
||||
|
||||
declare module "@mui/material/Switch" {
|
||||
interface SwitchPropsVariantOverrides {
|
||||
"pena-switch": true;
|
||||
}
|
||||
}
|
1
lib/decorators/index.ts
Normal file
1
lib/decorators/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./throttle";
|
29
lib/decorators/throttle.ts
Normal file
29
lib/decorators/throttle.ts
Normal file
@ -0,0 +1,29 @@
|
||||
export type ThrottledFunction<T extends (...args: any) => any> = (...args: Parameters<T>) => void;
|
||||
|
||||
export function throttle<T extends (...args: any) => any>(func: T, ms: number): ThrottledFunction<T> {
|
||||
let isThrottled = false;
|
||||
let savedArgs: Parameters<T> | null;
|
||||
let savedThis: any;
|
||||
|
||||
function wrapper(this: any, ...args: Parameters<T>) {
|
||||
if (isThrottled) {
|
||||
savedArgs = args;
|
||||
savedThis = this;
|
||||
return;
|
||||
}
|
||||
|
||||
func.apply(this, args);
|
||||
|
||||
isThrottled = true;
|
||||
|
||||
setTimeout(function () {
|
||||
isThrottled = false;
|
||||
if (savedArgs) {
|
||||
wrapper.apply(savedThis, savedArgs);
|
||||
savedArgs = savedThis = null;
|
||||
}
|
||||
}, ms);
|
||||
}
|
||||
|
||||
return wrapper;
|
||||
}
|
7
lib/env.d.ts
vendored
Normal file
7
lib/env.d.ts
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
export declare global {
|
||||
namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
readonly NODE_ENV: 'development' | 'production' | 'test';
|
||||
}
|
||||
}
|
||||
}
|
12
lib/hooks/index.ts
Normal file
12
lib/hooks/index.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export * from "./useAllTariffsFetcher";
|
||||
export * from "./useDebounce";
|
||||
export * from "./useEventListener";
|
||||
export * from "./usePrivilegeFetcher";
|
||||
export * from "./useSSESubscription";
|
||||
export * from "./usePaginatedTariffsFetcher";
|
||||
export * from "./useThrottle";
|
||||
export * from "./useTicketMessages";
|
||||
export * from "./useTickets";
|
||||
export * from "./useToken";
|
||||
export * from "./useUserAccountFetcher";
|
||||
export * from "./useUserFetcher";
|
60
lib/hooks/useAllTariffsFetcher.ts
Normal file
60
lib/hooks/useAllTariffsFetcher.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { useRef, useLayoutEffect, useEffect } from "react";
|
||||
import { GetTariffsResponse, Tariff } from "../model/tariff";
|
||||
import { makeRequest } from "../api/makeRequest";
|
||||
|
||||
|
||||
export function useAllTariffsFetcher({
|
||||
enabled = true,
|
||||
baseUrl = process.env.REACT_APP_DOMAIN + "/strator/tariff",
|
||||
onSuccess,
|
||||
onError,
|
||||
}: {
|
||||
enabled?: boolean;
|
||||
baseUrl?: string;
|
||||
onSuccess: (response: Tariff[]) => void;
|
||||
onError?: (error: Error) => void;
|
||||
}) {
|
||||
const onNewTariffsRef = useRef(onSuccess);
|
||||
const onErrorRef = useRef(onError);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
onNewTariffsRef.current = onSuccess;
|
||||
onErrorRef.current = onError;
|
||||
}, [onError, onSuccess]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
|
||||
const controller = new AbortController();
|
||||
|
||||
async function getPaginatedTariffs() {
|
||||
let apiPage = 1;
|
||||
const tariffsPerPage = 100;
|
||||
let isDone = false;
|
||||
|
||||
while (!isDone) {
|
||||
try {
|
||||
const result = await makeRequest<never, GetTariffsResponse>({
|
||||
url: baseUrl + `?page=${apiPage}&limit=${tariffsPerPage}`,
|
||||
method: "get",
|
||||
useToken: true,
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (result) onNewTariffsRef.current(result.tariffs);
|
||||
if (result.totalPages < apiPage) {
|
||||
apiPage++;
|
||||
} else {
|
||||
isDone = true;
|
||||
}
|
||||
} catch (error) {
|
||||
onErrorRef.current?.(error as Error);
|
||||
isDone = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getPaginatedTariffs();
|
||||
|
||||
return () => controller.abort();
|
||||
}, [baseUrl, enabled]);
|
||||
}
|
15
lib/hooks/useDebounce.ts
Normal file
15
lib/hooks/useDebounce.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
|
||||
export function useDebounce<T>(value: T, delay: number) {
|
||||
const [debouncedValue, setDebouncedValue] = useState<T>(value);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedValue(value);
|
||||
}, delay);
|
||||
return () => clearTimeout(handler);
|
||||
}, [value, delay]);
|
||||
|
||||
return debouncedValue;
|
||||
}
|
80
lib/hooks/useEventListener.ts
Normal file
80
lib/hooks/useEventListener.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import { useEffect, useRef, RefObject, useLayoutEffect } from "react";
|
||||
|
||||
// https://usehooks-ts.com/react-hook/use-event-listener
|
||||
|
||||
// MediaQueryList Event based useEventListener interface
|
||||
export function useEventListener<K extends keyof MediaQueryListEventMap>(
|
||||
eventName: K,
|
||||
handler: (event: MediaQueryListEventMap[K]) => void,
|
||||
element: RefObject<MediaQueryList>,
|
||||
options?: boolean | AddEventListenerOptions,
|
||||
): void;
|
||||
|
||||
// Window Event based useEventListener interface
|
||||
export function useEventListener<K extends keyof WindowEventMap>(
|
||||
eventName: K,
|
||||
handler: (event: WindowEventMap[K]) => void,
|
||||
element?: undefined,
|
||||
options?: boolean | AddEventListenerOptions,
|
||||
): void;
|
||||
|
||||
// Element Event based useEventListener interface
|
||||
export function useEventListener<
|
||||
K extends keyof HTMLElementEventMap,
|
||||
T extends HTMLElement = HTMLDivElement,
|
||||
>(
|
||||
eventName: K,
|
||||
handler: (event: HTMLElementEventMap[K]) => void,
|
||||
element: RefObject<T>,
|
||||
options?: boolean | AddEventListenerOptions,
|
||||
): void;
|
||||
|
||||
// Document Event based useEventListener interface
|
||||
export function useEventListener<K extends keyof DocumentEventMap>(
|
||||
eventName: K,
|
||||
handler: (event: DocumentEventMap[K]) => void,
|
||||
element: RefObject<Document>,
|
||||
options?: boolean | AddEventListenerOptions,
|
||||
): void;
|
||||
|
||||
export function useEventListener<
|
||||
KW extends keyof WindowEventMap,
|
||||
KH extends keyof HTMLElementEventMap,
|
||||
KM extends keyof MediaQueryListEventMap,
|
||||
T extends HTMLElement | MediaQueryList | void = void,
|
||||
>(
|
||||
eventName: KW | KH | KM,
|
||||
handler: (
|
||||
event:
|
||||
| WindowEventMap[KW]
|
||||
| HTMLElementEventMap[KH]
|
||||
| MediaQueryListEventMap[KM]
|
||||
| Event,
|
||||
) => void,
|
||||
element?: RefObject<T>,
|
||||
options?: boolean | AddEventListenerOptions,
|
||||
) {
|
||||
// Create a ref that stores handler
|
||||
const savedHandler = useRef(handler);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
savedHandler.current = handler;
|
||||
}, [handler]);
|
||||
|
||||
useEffect(() => {
|
||||
// Define the listening target
|
||||
const targetElement: T | Window = element?.current ?? window;
|
||||
|
||||
if (!(targetElement && targetElement.addEventListener)) return;
|
||||
|
||||
// Create event listener that calls handler export function stored in ref
|
||||
const listener: typeof handler = event => savedHandler.current(event);
|
||||
|
||||
targetElement.addEventListener(eventName, listener, options);
|
||||
|
||||
// Remove event listener on cleanup
|
||||
return () => {
|
||||
targetElement.removeEventListener(eventName, listener, options);
|
||||
};
|
||||
}, [eventName, element, options]);
|
||||
}
|
51
lib/hooks/usePaginatedTariffsFetcher.ts
Normal file
51
lib/hooks/usePaginatedTariffsFetcher.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { useEffect, useLayoutEffect, useRef } from "react";
|
||||
import { makeRequest } from "../api";
|
||||
import { Tariff, GetTariffsResponse } from "../model/tariff";
|
||||
import { devlog } from "../utils";
|
||||
import { FetchState } from "../model/fetchState";
|
||||
|
||||
|
||||
export function usePaginatedTariffsFetcher({ enabled = true, url, tariffsPerPage, apiPage, onSuccess, onError, onFetchStateChange }: {
|
||||
enabled?: boolean;
|
||||
url: string;
|
||||
tariffsPerPage: number;
|
||||
apiPage: number;
|
||||
onSuccess: (response: Tariff[]) => void;
|
||||
onError?: (error: Error) => void;
|
||||
onFetchStateChange?: (state: FetchState) => void;
|
||||
}) {
|
||||
const onNewTariffsRef = useRef(onSuccess);
|
||||
const onErrorRef = useRef(onError);
|
||||
const onFetchStateChangeRef = useRef(onFetchStateChange);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
onNewTariffsRef.current = onSuccess;
|
||||
onErrorRef.current = onError;
|
||||
onFetchStateChangeRef.current = onFetchStateChange;
|
||||
}, [onSuccess, onError, onFetchStateChange]);
|
||||
|
||||
useEffect(function fetchTickets() {
|
||||
if (!enabled) return;
|
||||
|
||||
const controller = new AbortController();
|
||||
|
||||
onFetchStateChangeRef.current?.("fetching");
|
||||
makeRequest<never, GetTariffsResponse>({
|
||||
url,
|
||||
method: "get",
|
||||
useToken: true,
|
||||
signal: controller.signal,
|
||||
}).then((result) => {
|
||||
devlog("GetTariffsResponse", result);
|
||||
if (result.tariffs.length > 0) {
|
||||
onNewTariffsRef.current(result.tariffs);
|
||||
onFetchStateChangeRef.current?.("idle");
|
||||
} else onFetchStateChangeRef.current?.("all fetched");
|
||||
}).catch(error => {
|
||||
devlog("Error fetching tariffs", error);
|
||||
onErrorRef.current?.(error);
|
||||
});
|
||||
|
||||
return () => controller.abort();
|
||||
}, [apiPage, enabled, tariffsPerPage, url]);
|
||||
}
|
39
lib/hooks/usePrivilegeFetcher.ts
Normal file
39
lib/hooks/usePrivilegeFetcher.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { useEffect, useLayoutEffect, useRef } from "react";
|
||||
import { makeRequest } from "../api";
|
||||
import { Privilege } from "../model";
|
||||
|
||||
|
||||
export function usePrivilegeFetcher({
|
||||
onSuccess,
|
||||
url = process.env.REACT_APP_DOMAIN + "/strator/privilege",
|
||||
onError,
|
||||
}: {
|
||||
onSuccess: (response: Privilege[]) => void;
|
||||
url?: string;
|
||||
onError?: (error: Error) => void;
|
||||
}) {
|
||||
const onSuccessRef = useRef(onSuccess);
|
||||
const onErrorRef = useRef(onError);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
onSuccessRef.current = onSuccess;
|
||||
onErrorRef.current = onError;
|
||||
}, [onSuccess, onError]);
|
||||
|
||||
useEffect(function fetchTickets() {
|
||||
const controller = new AbortController();
|
||||
|
||||
makeRequest<never, Privilege[]>({
|
||||
url,
|
||||
method: "get",
|
||||
useToken: true,
|
||||
signal: controller.signal,
|
||||
}).then((result) => {
|
||||
onSuccessRef.current(result);
|
||||
}).catch(error => {
|
||||
onErrorRef.current?.(error);
|
||||
});
|
||||
|
||||
return () => controller.abort();
|
||||
}, [url]);
|
||||
}
|
50
lib/hooks/useSSESubscription.ts
Normal file
50
lib/hooks/useSSESubscription.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { useEffect, useLayoutEffect, useRef } from "react";
|
||||
import ReconnectingEventSource from "reconnecting-eventsource";
|
||||
import { devlog } from "../utils";
|
||||
|
||||
|
||||
export function useSSESubscription<T>({ enabled = true, url, onNewData, onDisconnect, marker = "" }: {
|
||||
enabled?: boolean;
|
||||
url: string;
|
||||
onNewData: (data: T[]) => void;
|
||||
onDisconnect?: () => void;
|
||||
marker?: string;
|
||||
}) {
|
||||
const onNewDataRef = useRef(onNewData);
|
||||
const onDisconnectRef = useRef(onDisconnect);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
onNewDataRef.current = onNewData;
|
||||
onDisconnectRef.current = onDisconnect;
|
||||
}, [onNewData, onDisconnect]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
|
||||
const eventSource = new ReconnectingEventSource(url);
|
||||
|
||||
eventSource.addEventListener("open", () => devlog(`EventSource connected with ${url}`));
|
||||
eventSource.addEventListener("close", () => devlog(`EventSource closed with ${url}`));
|
||||
eventSource.addEventListener("message", event => {
|
||||
try {
|
||||
console.log("EVENT")
|
||||
console.log(event)
|
||||
const newData = JSON.parse(event.data) as T;
|
||||
if (!(typeof newData === "object" && newData !== null && "event" in newData && newData.event === "ping")) {
|
||||
devlog(`new SSE: ${marker}`, newData);
|
||||
onNewDataRef.current([newData]);
|
||||
}
|
||||
} catch (error) {
|
||||
devlog(`SSE parsing error: ${marker}`, event.data, error);
|
||||
}
|
||||
});
|
||||
eventSource.addEventListener("error", event => {
|
||||
devlog("SSE Error:", event);
|
||||
});
|
||||
|
||||
return () => {
|
||||
eventSource.close();
|
||||
onDisconnectRef.current?.();
|
||||
};
|
||||
}, [enabled, marker, url]);
|
||||
}
|
22
lib/hooks/useThrottle.ts
Normal file
22
lib/hooks/useThrottle.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
|
||||
|
||||
export function useThrottle<T>(value: T, delay: number) {
|
||||
const [throttledValue, setThrottledValue] = useState<T>(value);
|
||||
const time = useRef<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
const now = Date.now();
|
||||
if (now > time.current + delay) {
|
||||
time.current = now;
|
||||
setThrottledValue(value);
|
||||
} else {
|
||||
const handler = setTimeout(() => {
|
||||
setThrottledValue(value);
|
||||
}, delay);
|
||||
return () => clearTimeout(handler);
|
||||
}
|
||||
}, [value, delay]);
|
||||
|
||||
return throttledValue;
|
||||
}
|
58
lib/hooks/useTicketMessages.ts
Normal file
58
lib/hooks/useTicketMessages.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { useEffect, useRef, useLayoutEffect } from "react";
|
||||
import { TicketMessage, GetMessagesRequest, GetMessagesResponse } from "../model";
|
||||
import { devlog } from "../utils";
|
||||
import { makeRequest } from "../api";
|
||||
import { FetchState } from "../model/fetchState";
|
||||
|
||||
|
||||
export function useTicketMessages({ url, messageApiPage, messagesPerPage, ticketId, isUnauth = false, onSuccess, onError, onFetchStateChange }: {
|
||||
url: string;
|
||||
ticketId: string | undefined;
|
||||
messagesPerPage: number;
|
||||
messageApiPage: number;
|
||||
isUnauth?: boolean;
|
||||
onSuccess: (messages: TicketMessage[]) => void;
|
||||
onError?: (error: Error) => void;
|
||||
onFetchStateChange?: (state: FetchState) => void;
|
||||
}) {
|
||||
const onNewMessagesRef = useRef(onSuccess);
|
||||
const onErrorRef = useRef(onError);
|
||||
const onFetchStateChangeRef = useRef(onFetchStateChange);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
onNewMessagesRef.current = onSuccess;
|
||||
onErrorRef.current = onError;
|
||||
onFetchStateChangeRef.current = onFetchStateChange;
|
||||
}, [onSuccess, onError, onFetchStateChange]);
|
||||
|
||||
useEffect(function fetchTicketMessages() {
|
||||
if (!ticketId) return;
|
||||
|
||||
const controller = new AbortController();
|
||||
|
||||
onFetchStateChangeRef.current?.("fetching");
|
||||
makeRequest<GetMessagesRequest, GetMessagesResponse>({
|
||||
url,
|
||||
method: "POST",
|
||||
useToken: !isUnauth,
|
||||
body: {
|
||||
amt: messagesPerPage,
|
||||
page: messageApiPage,
|
||||
ticket: ticketId,
|
||||
},
|
||||
signal: controller.signal,
|
||||
withCredentials: isUnauth,
|
||||
}).then(result => {
|
||||
devlog("GetMessagesResponse", result);
|
||||
if (result?.length > 0) {
|
||||
onNewMessagesRef.current(result);
|
||||
onFetchStateChangeRef.current?.("idle");
|
||||
} else onFetchStateChangeRef.current?.("all fetched");
|
||||
}).catch(error => {
|
||||
devlog("Error fetching messages", error);
|
||||
onErrorRef.current?.(error);
|
||||
});
|
||||
|
||||
return () => controller.abort();
|
||||
}, [isUnauth, messageApiPage, messagesPerPage, ticketId, url]);
|
||||
}
|
56
lib/hooks/useTickets.ts
Normal file
56
lib/hooks/useTickets.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { useEffect, useRef, useLayoutEffect } from "react";
|
||||
import { GetTicketsResponse, GetTicketsRequest } from "../model";
|
||||
import { devlog } from "../utils";
|
||||
import { makeRequest } from "../api";
|
||||
import { FetchState } from "../model/fetchState";
|
||||
|
||||
|
||||
export function useTicketsFetcher({ enabled = true, url, ticketsPerPage, ticketApiPage, onSuccess, onError, onFetchStateChange }: {
|
||||
enabled?: boolean;
|
||||
url: string;
|
||||
ticketsPerPage: number;
|
||||
ticketApiPage: number;
|
||||
onSuccess: (response: GetTicketsResponse) => void;
|
||||
onError?: (error: Error) => void;
|
||||
onFetchStateChange?: (state: FetchState) => void;
|
||||
}) {
|
||||
const onNewTicketsRef = useRef(onSuccess);
|
||||
const onErrorRef = useRef(onError);
|
||||
const onFetchStateChangeRef = useRef(onFetchStateChange);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
onNewTicketsRef.current = onSuccess;
|
||||
onErrorRef.current = onError;
|
||||
onFetchStateChangeRef.current = onFetchStateChange;
|
||||
}, [onSuccess, onError, onFetchStateChange]);
|
||||
|
||||
useEffect(function fetchTickets() {
|
||||
if (!enabled) return;
|
||||
|
||||
const controller = new AbortController();
|
||||
|
||||
onFetchStateChangeRef.current?.("fetching");
|
||||
makeRequest<GetTicketsRequest, GetTicketsResponse>({
|
||||
url,
|
||||
method: "POST",
|
||||
useToken: true,
|
||||
body: {
|
||||
amt: ticketsPerPage,
|
||||
page: ticketApiPage,
|
||||
status: "open",
|
||||
},
|
||||
signal: controller.signal,
|
||||
}).then((result) => {
|
||||
devlog("GetTicketsResponse", result);
|
||||
if (result.data) {
|
||||
onNewTicketsRef.current(result);
|
||||
onFetchStateChangeRef.current?.("idle");
|
||||
} else onFetchStateChangeRef.current?.("all fetched");
|
||||
}).catch(error => {
|
||||
devlog("Error fetching tickets", error);
|
||||
onErrorRef.current?.(error);
|
||||
});
|
||||
|
||||
return () => controller.abort();
|
||||
}, [enabled, ticketApiPage, ticketsPerPage, url]);
|
||||
}
|
6
lib/hooks/useToken.ts
Normal file
6
lib/hooks/useToken.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { useAuthStore } from "../stores/auth";
|
||||
|
||||
|
||||
export function useToken() {
|
||||
return useAuthStore(state => state.token);
|
||||
}
|
55
lib/hooks/useUserAccountFetcher.ts
Normal file
55
lib/hooks/useUserAccountFetcher.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { isAxiosError } from "axios";
|
||||
import { useEffect, useLayoutEffect, useRef } from "react";
|
||||
import { UserAccount } from "../model/account";
|
||||
import { makeRequest } from "../api/makeRequest";
|
||||
import { devlog } from "../utils/devlog";
|
||||
import { createUserAccount } from "../api/account";
|
||||
|
||||
|
||||
export function useUserAccountFetcher<T = UserAccount>({ onError, onNewUserAccount, versionOfCustomer="v1.0.0", url, userId }: {
|
||||
url: string;
|
||||
userId: string | null;
|
||||
versionOfCustomer?: string;
|
||||
onNewUserAccount: (response: T) => void;
|
||||
onError?: (error: any) => void;
|
||||
}) {
|
||||
const onNewUserAccountRef = useRef(onNewUserAccount);
|
||||
const onErrorRef = useRef(onError);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
onNewUserAccountRef.current = onNewUserAccount;
|
||||
onErrorRef.current = onError;
|
||||
}, [onError, onNewUserAccount]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!userId) return;
|
||||
|
||||
const controller = new AbortController();
|
||||
makeRequest<never, T>({
|
||||
url,
|
||||
contentType: true,
|
||||
method: "GET",
|
||||
useToken: true,
|
||||
withCredentials: false,
|
||||
signal: controller.signal,
|
||||
}).then(result => {
|
||||
devlog("User account", result);
|
||||
onNewUserAccountRef.current(result);
|
||||
}).catch(error => {
|
||||
devlog("Error fetching user account", error);
|
||||
if (isAxiosError(error) && error.response?.status === 404) {
|
||||
createUserAccount(controller.signal, url, versionOfCustomer).then(result => {
|
||||
devlog("Created user account", result);
|
||||
onNewUserAccountRef.current(result as T);
|
||||
}).catch(error => {
|
||||
devlog("Error creating user account", error);
|
||||
onErrorRef.current?.(error);
|
||||
});
|
||||
} else {
|
||||
onErrorRef.current?.(error);
|
||||
}
|
||||
});
|
||||
|
||||
return () => controller.abort();
|
||||
}, [url, userId]);
|
||||
}
|
43
lib/hooks/useUserFetcher.ts
Normal file
43
lib/hooks/useUserFetcher.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { useEffect, useLayoutEffect, useRef } from "react";
|
||||
import { User } from "../model/user";
|
||||
import { devlog } from "../utils/devlog";
|
||||
import { makeRequest } from "../api/makeRequest";
|
||||
|
||||
|
||||
export function useUserFetcher({ onError, onNewUser, url, userId }: {
|
||||
url: string;
|
||||
userId: string | null;
|
||||
onNewUser: (response: User) => void;
|
||||
onError?: (error: any) => void;
|
||||
}) {
|
||||
const onNewUserRef = useRef(onNewUser);
|
||||
const onErrorRef = useRef(onError);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
onNewUserRef.current = onNewUser;
|
||||
onErrorRef.current = onError;
|
||||
}, [onError, onNewUser]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!userId) return;
|
||||
|
||||
const controller = new AbortController();
|
||||
|
||||
makeRequest<never, User>({
|
||||
url,
|
||||
contentType: true,
|
||||
method: "GET",
|
||||
useToken: true,
|
||||
withCredentials: false,
|
||||
signal: controller.signal,
|
||||
}).then(result => {
|
||||
devlog("User", result);
|
||||
onNewUserRef.current(result);
|
||||
}).catch(error => {
|
||||
devlog("Error fetching user", error);
|
||||
onErrorRef.current?.(error);
|
||||
});
|
||||
|
||||
return () => controller.abort();
|
||||
}, [url, userId]);
|
||||
}
|
8
lib/index.ts
Normal file
8
lib/index.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export * from "./api";
|
||||
export * from "./components";
|
||||
export * from "./decorators";
|
||||
export * from "./hooks";
|
||||
export * from "./model";
|
||||
export * from "./stores";
|
||||
export * from "./utils";
|
||||
export type * from "./model";
|
25
lib/model/account.ts
Normal file
25
lib/model/account.ts
Normal file
@ -0,0 +1,25 @@
|
||||
export interface UserAccount {
|
||||
_id: string;
|
||||
userId: string;
|
||||
name: UserName;
|
||||
cart: string[];
|
||||
wallet: {
|
||||
currency: string;
|
||||
cash: number;
|
||||
purchasesAmount: number;
|
||||
spent: number;
|
||||
money: number;
|
||||
};
|
||||
status: "no" | "nko" | "org";
|
||||
isDeleted: false;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
deletedAt: string;
|
||||
}
|
||||
|
||||
export interface UserName {
|
||||
firstname?: string;
|
||||
secondname?: string;
|
||||
middlename?: string;
|
||||
orgname?: string;
|
||||
}
|
21
lib/model/auth.ts
Normal file
21
lib/model/auth.ts
Normal file
@ -0,0 +1,21 @@
|
||||
export interface RegisterRequest {
|
||||
login: string;
|
||||
password: string;
|
||||
phoneNumber: string;
|
||||
}
|
||||
|
||||
export interface RegisterResponse {
|
||||
accessToken: string;
|
||||
login: string;
|
||||
email: string;
|
||||
phoneNumber: string;
|
||||
refreshToken: string;
|
||||
_id: string;
|
||||
}
|
||||
|
||||
export interface LoginRequest {
|
||||
login: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export type LoginResponse = RegisterResponse;
|
32
lib/model/cart.ts
Normal file
32
lib/model/cart.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { Discount } from "./discount";
|
||||
|
||||
|
||||
export type PrivilegeCartData = {
|
||||
serviceKey: string;
|
||||
privilegeId: string;
|
||||
description: string;
|
||||
price: number;
|
||||
amount: number;
|
||||
appliedDiscounts: Set<Discount>;
|
||||
};
|
||||
|
||||
export type TariffCartData = {
|
||||
name: string;
|
||||
id: string;
|
||||
price: number;
|
||||
isCustom: boolean;
|
||||
privileges: PrivilegeCartData[];
|
||||
};
|
||||
|
||||
export type ServiceCartData = {
|
||||
serviceKey: string;
|
||||
tariffs: TariffCartData[];
|
||||
price: number;
|
||||
};
|
||||
|
||||
export type CartData = {
|
||||
services: ServiceCartData[];
|
||||
priceBeforeDiscounts: number;
|
||||
priceAfterDiscounts: number;
|
||||
allAppliedDiscounts: Set<Discount>;
|
||||
};
|
12
lib/model/customTariffs.ts
Normal file
12
lib/model/customTariffs.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { CustomPrivilegeWithAmount } from "./privilege";
|
||||
|
||||
|
||||
type ServiceKey = string;
|
||||
|
||||
export type PrivilegeWithoutPrice = Omit<CustomPrivilegeWithAmount, "price">;
|
||||
|
||||
export type CustomTariffUserValues = Record<string, number>;
|
||||
|
||||
export type CustomTariffUserValuesMap = Record<ServiceKey, CustomTariffUserValues>;
|
||||
|
||||
export type ServiceKeyToPriceMap = Record<ServiceKey, number>;
|
40
lib/model/discount.ts
Normal file
40
lib/model/discount.ts
Normal file
@ -0,0 +1,40 @@
|
||||
export interface Discount {
|
||||
ID: string;
|
||||
Name: string;
|
||||
Layer: number;
|
||||
Description: string;
|
||||
Condition: {
|
||||
Period?: {
|
||||
From: string;
|
||||
To: string;
|
||||
};
|
||||
User: string;
|
||||
UserType?: string;
|
||||
Coupon: string;
|
||||
PurchasesAmount?: string;
|
||||
CartPurchasesAmount?: string;
|
||||
Product?: string;
|
||||
Term?: string;
|
||||
Usage?: string;
|
||||
PriceFrom?: string;
|
||||
Group?: string;
|
||||
};
|
||||
Target: {
|
||||
Products: {
|
||||
ID: string;
|
||||
Factor: number;
|
||||
Overhelm: boolean;
|
||||
}[];
|
||||
Factor: number;
|
||||
TargetScope: string;
|
||||
TargetGroup: string;
|
||||
Overhelm: boolean;
|
||||
};
|
||||
Audit: {
|
||||
UpdatedAt: string;
|
||||
CreatedAt: string;
|
||||
DeletedAt?: string;
|
||||
Deleted: boolean;
|
||||
};
|
||||
Deprecated: boolean;
|
||||
}
|
1
lib/model/fetchState.ts
Normal file
1
lib/model/fetchState.ts
Normal file
@ -0,0 +1 @@
|
||||
export type FetchState = "fetching" | "idle" | "all fetched";
|
10
lib/model/index.ts
Normal file
10
lib/model/index.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export type * from "./account";
|
||||
export type * from "./auth";
|
||||
export type * from "./cart";
|
||||
export type * from "./customTariffs";
|
||||
export type * from "./discount";
|
||||
export type * from "./fetchState";
|
||||
export type * from "./privilege";
|
||||
export type * from "./tariff";
|
||||
export type * from "./ticket";
|
||||
export type * from "./user";
|
30
lib/model/privilege.ts
Normal file
30
lib/model/privilege.ts
Normal file
@ -0,0 +1,30 @@
|
||||
export interface Privilege {
|
||||
name: string;
|
||||
privilegeId: string;
|
||||
serviceKey: string;
|
||||
description: string;
|
||||
type: "day" | "count";
|
||||
value: PrivilegeValueType;
|
||||
price: number;
|
||||
amount: number;
|
||||
}
|
||||
|
||||
export interface CustomPrivilege {
|
||||
_id: string;
|
||||
name: string;
|
||||
privilegeId: string;
|
||||
serviceKey: string;
|
||||
description: string;
|
||||
type: "day" | "count";
|
||||
value: PrivilegeValueType;
|
||||
price: number;
|
||||
updatedAt?: string;
|
||||
isDeleted?: boolean;
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
export type PrivilegeMap = Record<string, CustomPrivilege[]>;
|
||||
|
||||
export type PrivilegeValueType = "шаблон" | "день" | "МБ" | "заявка";
|
||||
|
||||
export type CustomPrivilegeWithAmount = CustomPrivilege & { amount: number; };
|
21
lib/model/tariff.ts
Normal file
21
lib/model/tariff.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { Privilege } from "./privilege";
|
||||
|
||||
|
||||
export interface GetTariffsResponse {
|
||||
totalPages: number;
|
||||
tariffs: Tariff[];
|
||||
}
|
||||
|
||||
export interface Tariff {
|
||||
_id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
order?: number;
|
||||
price?: number;
|
||||
isCustom: boolean;
|
||||
privileges: Privilege[];
|
||||
isDeleted: boolean;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
deletedAt?: string;
|
||||
}
|
74
lib/model/ticket.ts
Normal file
74
lib/model/ticket.ts
Normal file
@ -0,0 +1,74 @@
|
||||
|
||||
|
||||
export interface CreateTicketRequest {
|
||||
Title: string;
|
||||
Message: string;
|
||||
System: boolean
|
||||
}
|
||||
|
||||
export interface CreateTicketResponse {
|
||||
Ticket: string;
|
||||
sess: string;
|
||||
}
|
||||
|
||||
export interface SendTicketMessageRequest {
|
||||
message: string;
|
||||
ticket: string;
|
||||
lang: string;
|
||||
files: string[];
|
||||
System?: boolean;
|
||||
}
|
||||
|
||||
export type TicketStatus = "open";
|
||||
|
||||
export interface GetTicketsRequest {
|
||||
amt: number;
|
||||
/** Пагинация начинается с индекса 0 */
|
||||
page: number;
|
||||
srch?: string;
|
||||
status?: TicketStatus;
|
||||
}
|
||||
|
||||
export interface GetTicketsResponse {
|
||||
count: number;
|
||||
data: Ticket[] | null;
|
||||
}
|
||||
|
||||
export interface Ticket {
|
||||
id: string;
|
||||
user: string;
|
||||
sess: string;
|
||||
ans: string;
|
||||
state: string;
|
||||
top_message: TicketMessage;
|
||||
title: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
rate: number;
|
||||
origin: string;
|
||||
}
|
||||
|
||||
export interface TicketMessage {
|
||||
id: string;
|
||||
ticket_id: string;
|
||||
user_id: string,
|
||||
session_id: string;
|
||||
message: string;
|
||||
files: string[],
|
||||
shown: { [key: string]: number; },
|
||||
request_screenshot: string,
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface GetMessagesRequest {
|
||||
amt: number;
|
||||
page: number;
|
||||
srch?: string;
|
||||
ticket: string;
|
||||
}
|
||||
|
||||
export type GetMessagesResponse = TicketMessage[];
|
||||
|
||||
export type SendFileResponse = {
|
||||
message: string;
|
||||
};
|
10
lib/model/user.ts
Normal file
10
lib/model/user.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export interface User {
|
||||
_id: string;
|
||||
login: string;
|
||||
email: string;
|
||||
phoneNumber: string;
|
||||
isDeleted: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
deletedAt?: string;
|
||||
}
|
24
lib/stores/auth.ts
Normal file
24
lib/stores/auth.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
|
||||
|
||||
interface AuthStore {
|
||||
token: string;
|
||||
}
|
||||
|
||||
export const useAuthStore = create<AuthStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
token: "",
|
||||
}),
|
||||
{
|
||||
name: "token",
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export const getAuthToken = () => useAuthStore.getState().token;
|
||||
|
||||
export const setAuthToken = (token: string) => useAuthStore.setState({ token });
|
||||
|
||||
export const clearAuthToken = () => useAuthStore.setState({ token: "" });
|
1
lib/stores/index.ts
Normal file
1
lib/stores/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./auth";
|
30
lib/utils/backendMessageHandler.ts
Normal file
30
lib/utils/backendMessageHandler.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { isAxiosError } from "axios";
|
||||
|
||||
|
||||
const translateMessage: Record<string, string> = {
|
||||
"user not found": "Пользователь не найден",
|
||||
"invalid password": "Неправильный пароль",
|
||||
"field <password> is empty": "Поле \"Пароль\" не заполнено",
|
||||
"field <login> is empty": "Поле \"Логин\" не заполнено",
|
||||
"field <email> is empty": "Поле \"E-mail\" не заполнено",
|
||||
"field <phoneNumber> is empty": "Поле \"Номер телефона\" не заполнено",
|
||||
"user with this email or login is exist": "Пользователь уже существует",
|
||||
};
|
||||
|
||||
export function getMessageFromFetchError(error: any, defaultMessage = "Что-то пошло не так. Повторите попытку позже"): string | null {
|
||||
const rawMessage = error.response?.data?.message;
|
||||
|
||||
const translatedMessage = translateMessage[rawMessage];
|
||||
if (translatedMessage) return translatedMessage;
|
||||
|
||||
if (isAxiosError(error)) {
|
||||
switch (error.code) {
|
||||
case "ERR_NETWORK": return "Ошибка сети";
|
||||
case "ERR_CANCELED": return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === "development") return rawMessage ?? error.message ?? defaultMessage;
|
||||
|
||||
return defaultMessage;
|
||||
}
|
21
lib/utils/cart/calcCart.test.ts
Normal file
21
lib/utils/cart/calcCart.test.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { expect, test, describe } from "vitest";
|
||||
import { calcCart } from "./calcCart";
|
||||
import { testDiscounts } from "./mockData/discounts";
|
||||
import { cartTestResults } from "./mockData/results";
|
||||
import { testTariffs } from "./mockData/tariffs";
|
||||
|
||||
|
||||
describe("Cart calculation", () => {
|
||||
for (let i = 0; i < cartTestResults.length; i++) {
|
||||
test(`Cart calculation №${i}`, () => {
|
||||
const usedTariffsMask = cartTestResults[i][1];
|
||||
const isNkoApplied = Boolean(usedTariffsMask.pop());
|
||||
|
||||
const tariffs = testTariffs.filter((_, index) => (usedTariffsMask[index] === 1));
|
||||
|
||||
const cart = calcCart(tariffs, testDiscounts, 0, "someuserid", isNkoApplied);
|
||||
|
||||
expect(cart.priceAfterDiscounts).toBeCloseTo(cartTestResults[i][0]);
|
||||
});
|
||||
}
|
||||
});
|
192
lib/utils/cart/calcCart.ts
Normal file
192
lib/utils/cart/calcCart.ts
Normal file
@ -0,0 +1,192 @@
|
||||
import { CartData, PrivilegeCartData, TariffCartData } from "../../model/cart";
|
||||
import { Discount } from "../../model/discount";
|
||||
import { Tariff } from "../../model/tariff";
|
||||
import { findCartDiscount, findDiscountFactor, findLoyaltyDiscount, findNkoDiscount, findPrivilegeDiscount, findServiceDiscount } from "./utils";
|
||||
|
||||
|
||||
export function calcCart(tariffs: Tariff[], discounts: Discount[], purchasesAmount: number, userId: string, isUserNko?: boolean): CartData {
|
||||
const cartData: CartData = {
|
||||
services: [],
|
||||
priceBeforeDiscounts: 0,
|
||||
priceAfterDiscounts: 0,
|
||||
allAppliedDiscounts: new Set(),
|
||||
};
|
||||
const privilegeAmountById = new Map<string, number>();
|
||||
const servicePriceByKey = new Map<string, number>();
|
||||
|
||||
tariffs.forEach(tariff => {
|
||||
if (tariff.privileges === undefined) return;
|
||||
if (
|
||||
(tariff.price || 0) > 0
|
||||
&& tariff.privileges.length !== 1
|
||||
) throw new Error("Price is defined for tariff with several privileges");
|
||||
|
||||
let serviceData = cartData.services.find(service => (service.serviceKey === "custom" && tariff.isCustom));
|
||||
if (!serviceData && !tariff.isCustom) serviceData = cartData.services.find(service => service.serviceKey === tariff.privileges[0]?.serviceKey);
|
||||
|
||||
if (!serviceData) {
|
||||
serviceData = {
|
||||
serviceKey: tariff.isCustom ? "custom" : tariff.privileges[0]?.serviceKey,
|
||||
tariffs: [],
|
||||
price: 0,
|
||||
};
|
||||
cartData.services.push(serviceData);
|
||||
}
|
||||
|
||||
const tariffCartData: TariffCartData = {
|
||||
price: tariff.price ?? 0,
|
||||
isCustom: tariff.isCustom,
|
||||
privileges: [],
|
||||
id: tariff._id,
|
||||
name: tariff.name,
|
||||
};
|
||||
serviceData.tariffs.push(tariffCartData);
|
||||
|
||||
tariff.privileges.forEach(privilege => {
|
||||
let privilegePrice = privilege.amount * privilege.price;
|
||||
if (!tariff.price) tariffCartData.price += privilegePrice;
|
||||
else privilegePrice = tariff.price;
|
||||
|
||||
const privilegeCartData: PrivilegeCartData = {
|
||||
serviceKey: privilege.serviceKey,
|
||||
privilegeId: privilege.privilegeId,
|
||||
description: privilege.description,
|
||||
price: privilegePrice,
|
||||
amount: privilege.amount,
|
||||
appliedDiscounts: new Set(),
|
||||
};
|
||||
|
||||
privilegeAmountById.set(
|
||||
privilege.privilegeId,
|
||||
privilege.amount + (privilegeAmountById.get(privilege.privilegeId) ?? 0)
|
||||
);
|
||||
servicePriceByKey.set(
|
||||
privilege.serviceKey,
|
||||
privilegePrice + (servicePriceByKey.get(privilege.serviceKey) ?? 0)
|
||||
);
|
||||
|
||||
tariffCartData.privileges.push(privilegeCartData);
|
||||
});
|
||||
|
||||
serviceData.price += tariffCartData.price;
|
||||
});
|
||||
|
||||
cartData.priceBeforeDiscounts = Array.from(servicePriceByKey.values()).reduce((a, b) => a + b, 0);
|
||||
cartData.priceAfterDiscounts = cartData.priceBeforeDiscounts;
|
||||
|
||||
const nkoDiscount = findNkoDiscount(discounts);
|
||||
if (isUserNko && nkoDiscount) {
|
||||
cartData.allAppliedDiscounts.add(nkoDiscount);
|
||||
|
||||
cartData.services.forEach(service => {
|
||||
service.tariffs.forEach(tariff => {
|
||||
tariff.privileges.forEach(privilege => {
|
||||
privilege.appliedDiscounts.add(nkoDiscount);
|
||||
|
||||
const discountAmount = privilege.price * (1 - findDiscountFactor(nkoDiscount));
|
||||
|
||||
privilege.price -= discountAmount;
|
||||
tariff.price -= discountAmount;
|
||||
service.price -= discountAmount;
|
||||
cartData.priceAfterDiscounts -= discountAmount;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return cartData;
|
||||
}
|
||||
|
||||
cartData.services.forEach(service => {
|
||||
service.tariffs.forEach(tariff => {
|
||||
tariff.privileges.forEach(privilege => {
|
||||
const privilegeTotalAmount = privilegeAmountById.get(privilege.privilegeId) ?? 0;
|
||||
|
||||
const discount = findPrivilegeDiscount(privilege.privilegeId, privilegeTotalAmount, discounts, userId);
|
||||
if (!discount) return;
|
||||
|
||||
cartData.allAppliedDiscounts.add(discount);
|
||||
privilege.appliedDiscounts.add(discount);
|
||||
|
||||
const discountAmount = privilege.price * (1 - findDiscountFactor(discount));
|
||||
|
||||
privilege.price -= discountAmount;
|
||||
tariff.price -= discountAmount;
|
||||
service.price -= discountAmount;
|
||||
cartData.priceAfterDiscounts -= discountAmount;
|
||||
|
||||
const serviceTotalPrice = servicePriceByKey.get(privilege.serviceKey);
|
||||
if (!serviceTotalPrice) throw new Error(`Service key ${privilege.serviceKey} not found in servicePriceByKey`);
|
||||
|
||||
servicePriceByKey.set(privilege.serviceKey, serviceTotalPrice - discountAmount);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
cartData.services.forEach(service => {
|
||||
service.tariffs.map(tariff => {
|
||||
tariff.privileges.forEach(privilege => {
|
||||
const serviceTotalPrice = servicePriceByKey.get(privilege.serviceKey);
|
||||
if (!serviceTotalPrice) throw new Error(`Service key ${privilege.serviceKey} not found in servicePriceByKey`);
|
||||
|
||||
const discount = findServiceDiscount(privilege.serviceKey, serviceTotalPrice, discounts, userId);
|
||||
if (!discount) return;
|
||||
|
||||
cartData.allAppliedDiscounts.add(discount);
|
||||
privilege.appliedDiscounts.add(discount);
|
||||
|
||||
const discountAmount = privilege.price * (1 - findDiscountFactor(discount));
|
||||
|
||||
privilege.price -= discountAmount;
|
||||
tariff.price -= discountAmount;
|
||||
service.price -= discountAmount;
|
||||
cartData.priceAfterDiscounts -= discountAmount;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const userDiscount = discounts.find(discount => discount.Condition.User === userId);
|
||||
|
||||
const cartDiscount = findCartDiscount(cartData.priceAfterDiscounts, discounts);
|
||||
if (cartDiscount) {
|
||||
cartData.services.forEach(service => {
|
||||
if (service.serviceKey === userDiscount?.Condition.Group) return;
|
||||
|
||||
service.tariffs.forEach(tariff => {
|
||||
tariff.privileges.forEach(privilege => {
|
||||
cartData.allAppliedDiscounts.add(cartDiscount);
|
||||
privilege.appliedDiscounts.add(cartDiscount);
|
||||
|
||||
const discountAmount = privilege.price * (1 - findDiscountFactor(cartDiscount));
|
||||
|
||||
privilege.price -= discountAmount;
|
||||
tariff.price -= discountAmount;
|
||||
service.price -= discountAmount;
|
||||
cartData.priceAfterDiscounts -= discountAmount;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const loyalDiscount = findLoyaltyDiscount(purchasesAmount, discounts);
|
||||
if (loyalDiscount) {
|
||||
cartData.services.forEach(service => {
|
||||
if (service.serviceKey === userDiscount?.Condition.Group) return;
|
||||
|
||||
service.tariffs.forEach(tariff => {
|
||||
tariff.privileges.forEach(privilege => {
|
||||
cartData.allAppliedDiscounts.add(loyalDiscount);
|
||||
privilege.appliedDiscounts.add(loyalDiscount);
|
||||
|
||||
const discountAmount = privilege.price * (1 - findDiscountFactor(loyalDiscount));
|
||||
|
||||
privilege.price -= discountAmount;
|
||||
tariff.price -= discountAmount;
|
||||
service.price -= discountAmount;
|
||||
cartData.priceAfterDiscounts -= discountAmount;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return cartData;
|
||||
}
|
52
lib/utils/cart/calcCustomTariffPrice.ts
Normal file
52
lib/utils/cart/calcCustomTariffPrice.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { CustomTariffUserValues } from "../../model/customTariffs";
|
||||
import { Discount } from "../../model/discount";
|
||||
import { CustomPrivilegeWithAmount } from "../../model/privilege";
|
||||
import { Tariff } from "../../model/tariff";
|
||||
import { calcCart } from "./calcCart";
|
||||
|
||||
|
||||
export function calcCustomTariffPrice(
|
||||
customTariffUserValues: CustomTariffUserValues,
|
||||
servicePrivileges: CustomPrivilegeWithAmount[],
|
||||
cartTariffs: Tariff[],
|
||||
discounts: Discount[],
|
||||
purchasesAmount: number,
|
||||
isUserNko: boolean,
|
||||
userId: string,
|
||||
) {
|
||||
const privileges = new Array<CustomPrivilegeWithAmount>();
|
||||
|
||||
const priceBeforeDiscounts = servicePrivileges.reduce((price, privilege) => {
|
||||
const amount = customTariffUserValues?.[privilege._id] ?? 0;
|
||||
return price + privilege.price * amount;
|
||||
}, 0);
|
||||
|
||||
Object.keys(customTariffUserValues).forEach(privilegeId => {
|
||||
const pwa = servicePrivileges.find(p => p._id === privilegeId);
|
||||
if (!pwa) return;
|
||||
if (customTariffUserValues[privilegeId] > 0) privileges.push({
|
||||
...pwa,
|
||||
amount: customTariffUserValues[privilegeId]
|
||||
});
|
||||
});
|
||||
|
||||
const customTariff: Tariff = {
|
||||
_id: crypto.randomUUID(),
|
||||
name: "",
|
||||
price: 0,
|
||||
description: "",
|
||||
isCustom: true,
|
||||
isDeleted: false,
|
||||
privileges: privileges,
|
||||
};
|
||||
|
||||
const cart = calcCart([...cartTariffs, customTariff], discounts, purchasesAmount, userId, isUserNko);
|
||||
|
||||
const customService = cart.services.flatMap(service => service.tariffs).find(tariff => tariff.id === customTariff._id);
|
||||
if (!customService) throw new Error("Custom service not found in cart");
|
||||
|
||||
return {
|
||||
priceBeforeDiscounts,
|
||||
priceAfterDiscounts: customService.price,
|
||||
};
|
||||
}
|
31
lib/utils/cart/calcTariffPrice.ts
Normal file
31
lib/utils/cart/calcTariffPrice.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { Discount } from "../../model/discount";
|
||||
import { Tariff } from "../../model/tariff";
|
||||
import { calcCart } from "./calcCart";
|
||||
|
||||
|
||||
export function calcTariffPrice(
|
||||
targetTariff: Tariff,
|
||||
discounts: Discount[],
|
||||
purchasesAmount: number,
|
||||
currentTariffs: Tariff[],
|
||||
isUserNko: boolean,
|
||||
userId: string,
|
||||
): {
|
||||
priceBeforeDiscounts: number;
|
||||
priceAfterDiscounts: number;
|
||||
} {
|
||||
const priceBeforeDiscounts = targetTariff.price || targetTariff.privileges.reduce(
|
||||
(sum, privilege) => sum + privilege.amount * privilege.price,
|
||||
0
|
||||
);
|
||||
|
||||
const cart = calcCart([...currentTariffs, targetTariff], discounts, purchasesAmount, userId, isUserNko);
|
||||
|
||||
const tariffCartData = cart.services.flatMap(service => service.tariffs).find(tariff => tariff.id === targetTariff._id);
|
||||
if (!tariffCartData) throw new Error(`Target tariff ${targetTariff._id} not found in cart`);
|
||||
|
||||
return {
|
||||
priceBeforeDiscounts,
|
||||
priceAfterDiscounts: tariffCartData.price,
|
||||
};
|
||||
}
|
4
lib/utils/cart/index.ts
Normal file
4
lib/utils/cart/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from "./calcCart";
|
||||
export * from "./calcCustomTariffPrice";
|
||||
export * from "./calcTariffPrice";
|
||||
export * from "./utils";
|
901
lib/utils/cart/mockData/discounts.ts
Normal file
901
lib/utils/cart/mockData/discounts.ts
Normal file
@ -0,0 +1,901 @@
|
||||
import { Discount } from "../../../model/discount";
|
||||
|
||||
|
||||
export const testDiscounts: Discount[] = [
|
||||
{
|
||||
"ID": "6521d98b166f36879928ebbf",
|
||||
"Name": "NKO",
|
||||
"Layer": 4,
|
||||
"Description": "скидка ветеранам НКО",
|
||||
"Condition": {
|
||||
"Period": {
|
||||
"From": "2023-10-07T21:00:45.829Z",
|
||||
"To": "2023-11-06T21:00:45.829Z"
|
||||
},
|
||||
"User": "",
|
||||
"UserType": "nko",
|
||||
"Coupon": "",
|
||||
"PurchasesAmount": "0",
|
||||
"CartPurchasesAmount": "0",
|
||||
"Product": "",
|
||||
"Term": "0",
|
||||
"Usage": "0",
|
||||
"PriceFrom": "0",
|
||||
"Group": ""
|
||||
},
|
||||
"Target": {
|
||||
"Products": [],
|
||||
"Factor": 0.4,
|
||||
"TargetScope": "Sum",
|
||||
"TargetGroup": "",
|
||||
"Overhelm": true
|
||||
},
|
||||
"Audit": {
|
||||
"UpdatedAt": "2023-11-10T23:20:32.619Z",
|
||||
"CreatedAt": "2023-09-16T20:10:26.048Z",
|
||||
"Deleted": false
|
||||
},
|
||||
"Deprecated": false
|
||||
},
|
||||
{
|
||||
"ID": "657b9b73153787e41052c25b",
|
||||
"Name": "1000 шаблонов",
|
||||
"Layer": 1,
|
||||
"Description": "Тариф на 1000 шаблонов",
|
||||
"Condition": {
|
||||
"Period": {
|
||||
"From": "2023-12-15T00:18:57.999Z",
|
||||
"To": "2024-01-14T00:18:58Z"
|
||||
},
|
||||
"User": "",
|
||||
"UserType": "",
|
||||
"Coupon": "",
|
||||
"PurchasesAmount": "0",
|
||||
"CartPurchasesAmount": "0",
|
||||
"Product": "templateCnt",
|
||||
"Term": "1000",
|
||||
"Usage": "0",
|
||||
"PriceFrom": "0",
|
||||
"Group": ""
|
||||
},
|
||||
"Target": {
|
||||
"Products": [
|
||||
{
|
||||
"ID": "templateCnt",
|
||||
"Factor": 0.7,
|
||||
"Overhelm": false
|
||||
}
|
||||
],
|
||||
"Factor": 0.7,
|
||||
"TargetScope": "Sum",
|
||||
"TargetGroup": "",
|
||||
"Overhelm": false
|
||||
},
|
||||
"Audit": {
|
||||
"UpdatedAt": "2023-12-15T00:18:59.138Z",
|
||||
"CreatedAt": "2023-12-15T00:18:59.138Z",
|
||||
"Deleted": false
|
||||
},
|
||||
"Deprecated": false
|
||||
},
|
||||
{
|
||||
"ID": "657b9cb0153787e41052c25c",
|
||||
"Name": "десять тысяч шаблонов",
|
||||
"Layer": 1,
|
||||
"Description": "Тариф 10 000",
|
||||
"Condition": {
|
||||
"Period": {
|
||||
"From": "2023-12-15T00:24:15.555Z",
|
||||
"To": "2024-01-14T00:24:15.555Z"
|
||||
},
|
||||
"User": "",
|
||||
"UserType": "",
|
||||
"Coupon": "",
|
||||
"PurchasesAmount": "0",
|
||||
"CartPurchasesAmount": "0",
|
||||
"Product": "templateCnt",
|
||||
"Term": "10000",
|
||||
"Usage": "0",
|
||||
"PriceFrom": "0",
|
||||
"Group": ""
|
||||
},
|
||||
"Target": {
|
||||
"Products": [
|
||||
{
|
||||
"ID": "templateCnt",
|
||||
"Factor": 0.5,
|
||||
"Overhelm": false
|
||||
}
|
||||
],
|
||||
"Factor": 0.5,
|
||||
"TargetScope": "Sum",
|
||||
"TargetGroup": "",
|
||||
"Overhelm": false
|
||||
},
|
||||
"Audit": {
|
||||
"UpdatedAt": "2023-12-15T00:24:16.562Z",
|
||||
"CreatedAt": "2023-12-15T00:24:16.562Z",
|
||||
"Deleted": false
|
||||
},
|
||||
"Deprecated": false
|
||||
},
|
||||
{
|
||||
"ID": "657b9e67153787e41052c25d",
|
||||
"Name": "3 месяца",
|
||||
"Layer": 1,
|
||||
"Description": "Тариф 3 месяца",
|
||||
"Condition": {
|
||||
"Period": {
|
||||
"From": "2023-12-15T00:31:34.807Z",
|
||||
"To": "2024-01-14T00:31:34.807Z"
|
||||
},
|
||||
"User": "",
|
||||
"UserType": "",
|
||||
"Coupon": "",
|
||||
"PurchasesAmount": "0",
|
||||
"CartPurchasesAmount": "0",
|
||||
"Product": "templateUnlimTime",
|
||||
"Term": "90",
|
||||
"Usage": "0",
|
||||
"PriceFrom": "0",
|
||||
"Group": ""
|
||||
},
|
||||
"Target": {
|
||||
"Products": [
|
||||
{
|
||||
"ID": "templateUnlimTime",
|
||||
"Factor": 0.8,
|
||||
"Overhelm": false
|
||||
}
|
||||
],
|
||||
"Factor": 0.8,
|
||||
"TargetScope": "Sum",
|
||||
"TargetGroup": "",
|
||||
"Overhelm": false
|
||||
},
|
||||
"Audit": {
|
||||
"UpdatedAt": "2023-12-15T00:31:35.601Z",
|
||||
"CreatedAt": "2023-12-15T00:31:35.601Z",
|
||||
"Deleted": false
|
||||
},
|
||||
"Deprecated": false
|
||||
},
|
||||
{
|
||||
"ID": "657b9e8a153787e41052c25e",
|
||||
"Name": "год",
|
||||
"Layer": 1,
|
||||
"Description": "Тариф год",
|
||||
"Condition": {
|
||||
"Period": {
|
||||
"From": "2023-12-15T00:32:09.329Z",
|
||||
"To": "2024-01-14T00:32:09.329Z"
|
||||
},
|
||||
"User": "",
|
||||
"UserType": "",
|
||||
"Coupon": "",
|
||||
"PurchasesAmount": "0",
|
||||
"CartPurchasesAmount": "0",
|
||||
"Product": "templateUnlimTime",
|
||||
"Term": "365",
|
||||
"Usage": "0",
|
||||
"PriceFrom": "0",
|
||||
"Group": ""
|
||||
},
|
||||
"Target": {
|
||||
"Products": [
|
||||
{
|
||||
"ID": "templateUnlimTime",
|
||||
"Factor": 0.65,
|
||||
"Overhelm": false
|
||||
}
|
||||
],
|
||||
"Factor": 0.65,
|
||||
"TargetScope": "Sum",
|
||||
"TargetGroup": "",
|
||||
"Overhelm": false
|
||||
},
|
||||
"Audit": {
|
||||
"UpdatedAt": "2023-12-15T00:32:10.123Z",
|
||||
"CreatedAt": "2023-12-15T00:32:10.123Z",
|
||||
"Deleted": false
|
||||
},
|
||||
"Deprecated": false
|
||||
},
|
||||
{
|
||||
"ID": "657b9eb4153787e41052c25f",
|
||||
"Name": "3 года",
|
||||
"Layer": 1,
|
||||
"Description": "Тариф 3 года",
|
||||
"Condition": {
|
||||
"Period": {
|
||||
"From": "2023-12-15T00:32:51.379Z",
|
||||
"To": "2024-01-14T00:32:51.379Z"
|
||||
},
|
||||
"User": "",
|
||||
"UserType": "",
|
||||
"Coupon": "",
|
||||
"PurchasesAmount": "0",
|
||||
"CartPurchasesAmount": "0",
|
||||
"Product": "templateUnlimTime",
|
||||
"Term": "1095",
|
||||
"Usage": "0",
|
||||
"PriceFrom": "0",
|
||||
"Group": ""
|
||||
},
|
||||
"Target": {
|
||||
"Products": [
|
||||
{
|
||||
"ID": "templateUnlimTime",
|
||||
"Factor": 0.5,
|
||||
"Overhelm": false
|
||||
}
|
||||
],
|
||||
"Factor": 0.5,
|
||||
"TargetScope": "Sum",
|
||||
"TargetGroup": "",
|
||||
"Overhelm": false
|
||||
},
|
||||
"Audit": {
|
||||
"UpdatedAt": "2023-12-15T00:32:52.174Z",
|
||||
"CreatedAt": "2023-12-15T00:32:52.174Z",
|
||||
"Deleted": false
|
||||
},
|
||||
"Deprecated": false
|
||||
},
|
||||
{
|
||||
"ID": "657f5028153787e41052c266",
|
||||
"Name": "10т.р",
|
||||
"Layer": 4,
|
||||
"Description": "купил больше чем на 10 тыров",
|
||||
"Condition": {
|
||||
"Period": {
|
||||
"From": "2023-12-17T19:48:47.466Z",
|
||||
"To": "2024-01-16T19:48:47.466Z"
|
||||
},
|
||||
"User": "",
|
||||
"UserType": "",
|
||||
"Coupon": "",
|
||||
"PurchasesAmount": "10000",
|
||||
"CartPurchasesAmount": "0",
|
||||
"Product": "",
|
||||
"Term": "0",
|
||||
"Usage": "0",
|
||||
"PriceFrom": "0",
|
||||
"Group": ""
|
||||
},
|
||||
"Target": {
|
||||
"Products": [
|
||||
{
|
||||
"ID": "",
|
||||
"Factor": 0,
|
||||
"Overhelm": false
|
||||
}
|
||||
],
|
||||
"Factor": 0.98,
|
||||
"TargetScope": "Sum",
|
||||
"TargetGroup": "",
|
||||
"Overhelm": false
|
||||
},
|
||||
"Audit": {
|
||||
"UpdatedAt": "2023-12-17T19:48:46.072Z",
|
||||
"CreatedAt": "2023-12-17T19:46:48.854Z",
|
||||
"Deleted": false
|
||||
},
|
||||
"Deprecated": false
|
||||
},
|
||||
{
|
||||
"ID": "657f50c3153787e41052c268",
|
||||
"Name": "1т.р",
|
||||
"Layer": 4,
|
||||
"Description": "купил больше чем на 1 тыр",
|
||||
"Condition": {
|
||||
"Period": {
|
||||
"From": "2023-12-17T19:49:24.782Z",
|
||||
"To": "2024-01-16T19:49:24.782Z"
|
||||
},
|
||||
"User": "",
|
||||
"UserType": "",
|
||||
"Coupon": "",
|
||||
"PurchasesAmount": "100000",
|
||||
"CartPurchasesAmount": "0",
|
||||
"Product": "",
|
||||
"Term": "0",
|
||||
"Usage": "0",
|
||||
"PriceFrom": "0",
|
||||
"Group": ""
|
||||
},
|
||||
"Target": {
|
||||
"Products": [
|
||||
{
|
||||
"ID": "",
|
||||
"Factor": 0,
|
||||
"Overhelm": false
|
||||
}
|
||||
],
|
||||
"Factor": 0.99,
|
||||
"TargetScope": "Sum",
|
||||
"TargetGroup": "",
|
||||
"Overhelm": false
|
||||
},
|
||||
"Audit": {
|
||||
"UpdatedAt": "2023-12-17T19:49:23.384Z",
|
||||
"CreatedAt": "2023-12-17T19:49:23.384Z",
|
||||
"Deleted": false
|
||||
},
|
||||
"Deprecated": false
|
||||
},
|
||||
{
|
||||
"ID": "657f50e4153787e41052c269",
|
||||
"Name": "100т.р",
|
||||
"Layer": 4,
|
||||
"Description": "купил больше чем на 100 тыров",
|
||||
"Condition": {
|
||||
"Period": {
|
||||
"From": "2023-12-17T19:49:57.462Z",
|
||||
"To": "2024-01-16T19:49:57.462Z"
|
||||
},
|
||||
"User": "",
|
||||
"UserType": "",
|
||||
"Coupon": "",
|
||||
"PurchasesAmount": "10000000",
|
||||
"CartPurchasesAmount": "0",
|
||||
"Product": "",
|
||||
"Term": "0",
|
||||
"Usage": "0",
|
||||
"PriceFrom": "0",
|
||||
"Group": ""
|
||||
},
|
||||
"Target": {
|
||||
"Products": [
|
||||
{
|
||||
"ID": "",
|
||||
"Factor": 0,
|
||||
"Overhelm": false
|
||||
}
|
||||
],
|
||||
"Factor": 0.95,
|
||||
"TargetScope": "Sum",
|
||||
"TargetGroup": "",
|
||||
"Overhelm": false
|
||||
},
|
||||
"Audit": {
|
||||
"UpdatedAt": "2023-12-17T19:49:56.066Z",
|
||||
"CreatedAt": "2023-12-17T19:49:56.066Z",
|
||||
"Deleted": false
|
||||
},
|
||||
"Deprecated": false
|
||||
},
|
||||
{
|
||||
"ID": "657f511b153787e41052c26a",
|
||||
"Name": "1 т.р",
|
||||
"Layer": 3,
|
||||
"Description": "Больще 1т.р",
|
||||
"Condition": {
|
||||
"Period": {
|
||||
"From": "2023-12-17T19:50:52.764Z",
|
||||
"To": "2024-01-16T19:50:52.764Z"
|
||||
},
|
||||
"User": "",
|
||||
"UserType": "",
|
||||
"Coupon": "",
|
||||
"PurchasesAmount": "0",
|
||||
"CartPurchasesAmount": "100000",
|
||||
"Product": "",
|
||||
"Term": "0",
|
||||
"Usage": "0",
|
||||
"PriceFrom": "0",
|
||||
"Group": ""
|
||||
},
|
||||
"Target": {
|
||||
"Products": [
|
||||
{
|
||||
"ID": "",
|
||||
"Factor": 0,
|
||||
"Overhelm": false
|
||||
}
|
||||
],
|
||||
"Factor": 0.95,
|
||||
"TargetScope": "Sum",
|
||||
"TargetGroup": "",
|
||||
"Overhelm": false
|
||||
},
|
||||
"Audit": {
|
||||
"UpdatedAt": "2023-12-17T19:50:51.408Z",
|
||||
"CreatedAt": "2023-12-17T19:50:51.408Z",
|
||||
"Deleted": false
|
||||
},
|
||||
"Deprecated": false
|
||||
},
|
||||
{
|
||||
"ID": "657f512d153787e41052c26b",
|
||||
"Name": "5 т.р",
|
||||
"Layer": 3,
|
||||
"Description": "Больще 5т.р",
|
||||
"Condition": {
|
||||
"Period": {
|
||||
"From": "2023-12-17T19:51:11.104Z",
|
||||
"To": "2024-01-16T19:51:11.104Z"
|
||||
},
|
||||
"User": "",
|
||||
"UserType": "",
|
||||
"Coupon": "",
|
||||
"PurchasesAmount": "0",
|
||||
"CartPurchasesAmount": "500000",
|
||||
"Product": "",
|
||||
"Term": "0",
|
||||
"Usage": "0",
|
||||
"PriceFrom": "0",
|
||||
"Group": ""
|
||||
},
|
||||
"Target": {
|
||||
"Products": [
|
||||
{
|
||||
"ID": "",
|
||||
"Factor": 0,
|
||||
"Overhelm": false
|
||||
}
|
||||
],
|
||||
"Factor": 0.93,
|
||||
"TargetScope": "Sum",
|
||||
"TargetGroup": "",
|
||||
"Overhelm": false
|
||||
},
|
||||
"Audit": {
|
||||
"UpdatedAt": "2023-12-17T19:51:09.707Z",
|
||||
"CreatedAt": "2023-12-17T19:51:09.707Z",
|
||||
"Deleted": false
|
||||
},
|
||||
"Deprecated": false
|
||||
},
|
||||
{
|
||||
"ID": "657f5144153787e41052c26c",
|
||||
"Name": "10 т.р",
|
||||
"Layer": 3,
|
||||
"Description": "Больше 10т.р",
|
||||
"Condition": {
|
||||
"Period": {
|
||||
"From": "2023-12-17T19:51:33.502Z",
|
||||
"To": "2024-01-16T19:51:33.502Z"
|
||||
},
|
||||
"User": "",
|
||||
"UserType": "",
|
||||
"Coupon": "",
|
||||
"PurchasesAmount": "0",
|
||||
"CartPurchasesAmount": "1000000",
|
||||
"Product": "",
|
||||
"Term": "0",
|
||||
"Usage": "0",
|
||||
"PriceFrom": "0",
|
||||
"Group": ""
|
||||
},
|
||||
"Target": {
|
||||
"Products": [
|
||||
{
|
||||
"ID": "",
|
||||
"Factor": 0,
|
||||
"Overhelm": false
|
||||
}
|
||||
],
|
||||
"Factor": 0.91,
|
||||
"TargetScope": "Sum",
|
||||
"TargetGroup": "",
|
||||
"Overhelm": false
|
||||
},
|
||||
"Audit": {
|
||||
"UpdatedAt": "2023-12-17T19:51:32.105Z",
|
||||
"CreatedAt": "2023-12-17T19:51:32.105Z",
|
||||
"Deleted": false
|
||||
},
|
||||
"Deprecated": false
|
||||
},
|
||||
{
|
||||
"ID": "657f515a153787e41052c26d",
|
||||
"Name": "50 т.р",
|
||||
"Layer": 3,
|
||||
"Description": "Больше 50т.р",
|
||||
"Condition": {
|
||||
"Period": {
|
||||
"From": "2023-12-17T19:51:56.316Z",
|
||||
"To": "2024-01-16T19:51:56.316Z"
|
||||
},
|
||||
"User": "",
|
||||
"UserType": "",
|
||||
"Coupon": "",
|
||||
"PurchasesAmount": "0",
|
||||
"CartPurchasesAmount": "5000000",
|
||||
"Product": "",
|
||||
"Term": "0",
|
||||
"Usage": "0",
|
||||
"PriceFrom": "0",
|
||||
"Group": ""
|
||||
},
|
||||
"Target": {
|
||||
"Products": [
|
||||
{
|
||||
"ID": "",
|
||||
"Factor": 0,
|
||||
"Overhelm": false
|
||||
}
|
||||
],
|
||||
"Factor": 0.89,
|
||||
"TargetScope": "Sum",
|
||||
"TargetGroup": "",
|
||||
"Overhelm": false
|
||||
},
|
||||
"Audit": {
|
||||
"UpdatedAt": "2023-12-17T19:51:54.919Z",
|
||||
"CreatedAt": "2023-12-17T19:51:54.919Z",
|
||||
"Deleted": false
|
||||
},
|
||||
"Deprecated": false
|
||||
},
|
||||
{
|
||||
"ID": "65872fb4153787e41052c26e",
|
||||
"Name": "Лямчик",
|
||||
"Layer": 4,
|
||||
"Description": "Купил больше чем на миллиона",
|
||||
"Condition": {
|
||||
"Period": {
|
||||
"From": "2023-12-23T19:06:27.521Z",
|
||||
"To": "2024-01-22T19:06:27.521Z"
|
||||
},
|
||||
"User": "",
|
||||
"UserType": "",
|
||||
"Coupon": "",
|
||||
"PurchasesAmount": "100000000",
|
||||
"CartPurchasesAmount": "0",
|
||||
"Product": "",
|
||||
"Term": "0",
|
||||
"Usage": "0",
|
||||
"PriceFrom": "0",
|
||||
"Group": ""
|
||||
},
|
||||
"Target": {
|
||||
"Products": [
|
||||
{
|
||||
"ID": "",
|
||||
"Factor": 0,
|
||||
"Overhelm": false
|
||||
}
|
||||
],
|
||||
"Factor": 0.9,
|
||||
"TargetScope": "Sum",
|
||||
"TargetGroup": "",
|
||||
"Overhelm": false
|
||||
},
|
||||
"Audit": {
|
||||
"UpdatedAt": "2023-12-23T19:06:28.253Z",
|
||||
"CreatedAt": "2023-12-23T19:06:28.253Z",
|
||||
"Deleted": false
|
||||
},
|
||||
"Deprecated": false
|
||||
},
|
||||
{
|
||||
"ID": "6588c6e9153787e41052c26f",
|
||||
"Name": "больше 5т.р",
|
||||
"Layer": 2,
|
||||
"Description": "Шаблонизатор:Больше 5т.р",
|
||||
"Condition": {
|
||||
"Period": {
|
||||
"From": "2023-12-25T00:03:53.024Z",
|
||||
"To": "2024-01-24T00:03:53.024Z"
|
||||
},
|
||||
"User": "",
|
||||
"UserType": "",
|
||||
"Coupon": "",
|
||||
"PurchasesAmount": "0",
|
||||
"CartPurchasesAmount": "0",
|
||||
"Product": "",
|
||||
"Term": "0",
|
||||
"Usage": "0",
|
||||
"PriceFrom": "500000",
|
||||
"Group": "templategen"
|
||||
},
|
||||
"Target": {
|
||||
"Products": [
|
||||
{
|
||||
"ID": "",
|
||||
"Factor": 0,
|
||||
"Overhelm": false
|
||||
}
|
||||
],
|
||||
"Factor": 0.98,
|
||||
"TargetScope": "Sum",
|
||||
"TargetGroup": "templategen",
|
||||
"Overhelm": false
|
||||
},
|
||||
"Audit": {
|
||||
"UpdatedAt": "2023-12-25T00:03:53.269Z",
|
||||
"CreatedAt": "2023-12-25T00:03:53.269Z",
|
||||
"Deleted": false
|
||||
},
|
||||
"Deprecated": false
|
||||
},
|
||||
{
|
||||
"ID": "6588c70c153787e41052c270",
|
||||
"Name": "больше 10 т.р",
|
||||
"Layer": 2,
|
||||
"Description": "Шаблонизатор:Больше 10 т.р",
|
||||
"Condition": {
|
||||
"Period": {
|
||||
"From": "2023-12-25T00:04:28.279Z",
|
||||
"To": "2024-01-24T00:04:28.279Z"
|
||||
},
|
||||
"User": "",
|
||||
"UserType": "",
|
||||
"Coupon": "",
|
||||
"PurchasesAmount": "0",
|
||||
"CartPurchasesAmount": "0",
|
||||
"Product": "",
|
||||
"Term": "0",
|
||||
"Usage": "0",
|
||||
"PriceFrom": "1000000",
|
||||
"Group": "templategen"
|
||||
},
|
||||
"Target": {
|
||||
"Products": [
|
||||
{
|
||||
"ID": "",
|
||||
"Factor": 0,
|
||||
"Overhelm": false
|
||||
}
|
||||
],
|
||||
"Factor": 0.97,
|
||||
"TargetScope": "Sum",
|
||||
"TargetGroup": "templategen",
|
||||
"Overhelm": false
|
||||
},
|
||||
"Audit": {
|
||||
"UpdatedAt": "2023-12-25T00:04:28.442Z",
|
||||
"CreatedAt": "2023-12-25T00:04:28.442Z",
|
||||
"Deleted": false
|
||||
},
|
||||
"Deprecated": false
|
||||
},
|
||||
{
|
||||
"ID": "6588c724153787e41052c271",
|
||||
"Name": "больше 100 т.р",
|
||||
"Layer": 2,
|
||||
"Description": "Шаблонизатор:Больше 100 т.р",
|
||||
"Condition": {
|
||||
"Period": {
|
||||
"From": "2023-12-25T00:04:52.461Z",
|
||||
"To": "2024-01-24T00:04:52.461Z"
|
||||
},
|
||||
"User": "",
|
||||
"UserType": "",
|
||||
"Coupon": "",
|
||||
"PurchasesAmount": "0",
|
||||
"CartPurchasesAmount": "0",
|
||||
"Product": "",
|
||||
"Term": "0",
|
||||
"Usage": "0",
|
||||
"PriceFrom": "10000000",
|
||||
"Group": "templategen"
|
||||
},
|
||||
"Target": {
|
||||
"Products": [
|
||||
{
|
||||
"ID": "",
|
||||
"Factor": 0,
|
||||
"Overhelm": false
|
||||
}
|
||||
],
|
||||
"Factor": 0.95,
|
||||
"TargetScope": "Sum",
|
||||
"TargetGroup": "templategen",
|
||||
"Overhelm": false
|
||||
},
|
||||
"Audit": {
|
||||
"UpdatedAt": "2023-12-25T00:04:52.625Z",
|
||||
"CreatedAt": "2023-12-25T00:04:52.625Z",
|
||||
"Deleted": false
|
||||
},
|
||||
"Deprecated": false
|
||||
},
|
||||
{
|
||||
"ID": "65a49c215389294d1c348511",
|
||||
"Name": "1000 заявок",
|
||||
"Layer": 1,
|
||||
"Description": "Полное прохождение 1000 опросов респондентом",
|
||||
"Condition": {
|
||||
"Period": {
|
||||
"From": "2024-01-15T02:44:49.156Z",
|
||||
"To": "2024-02-14T02:44:49.156Z"
|
||||
},
|
||||
"User": "",
|
||||
"UserType": "",
|
||||
"Coupon": "",
|
||||
"PurchasesAmount": "0",
|
||||
"CartPurchasesAmount": "0",
|
||||
"Product": "quizCnt",
|
||||
"Term": "1000",
|
||||
"Usage": "0",
|
||||
"PriceFrom": "0",
|
||||
"Group": ""
|
||||
},
|
||||
"Target": {
|
||||
"Products": [
|
||||
{
|
||||
"ID": "quizCnt",
|
||||
"Factor": 0.7,
|
||||
"Overhelm": false
|
||||
}
|
||||
],
|
||||
"Factor": 0.7,
|
||||
"TargetScope": "Sum",
|
||||
"TargetGroup": "",
|
||||
"Overhelm": false
|
||||
},
|
||||
"Audit": {
|
||||
"UpdatedAt": "2024-01-15T02:44:48.995Z",
|
||||
"CreatedAt": "2024-01-15T02:44:48.995Z",
|
||||
"Deleted": false
|
||||
},
|
||||
"Deprecated": false
|
||||
},
|
||||
{
|
||||
"ID": "65a49c4f5389294d1c348512",
|
||||
"Name": "10000 заявок",
|
||||
"Layer": 1,
|
||||
"Description": "Полное прохождение 10000 опросов респондентом",
|
||||
"Condition": {
|
||||
"Period": {
|
||||
"From": "2024-01-15T02:45:37.236Z",
|
||||
"To": "2024-02-14T02:45:37.236Z"
|
||||
},
|
||||
"User": "",
|
||||
"UserType": "",
|
||||
"Coupon": "",
|
||||
"PurchasesAmount": "0",
|
||||
"CartPurchasesAmount": "0",
|
||||
"Product": "quizCnt",
|
||||
"Term": "10000",
|
||||
"Usage": "0",
|
||||
"PriceFrom": "0",
|
||||
"Group": ""
|
||||
},
|
||||
"Target": {
|
||||
"Products": [
|
||||
{
|
||||
"ID": "quizCnt",
|
||||
"Factor": 0.5,
|
||||
"Overhelm": false
|
||||
}
|
||||
],
|
||||
"Factor": 0.5,
|
||||
"TargetScope": "Sum",
|
||||
"TargetGroup": "",
|
||||
"Overhelm": false
|
||||
},
|
||||
"Audit": {
|
||||
"UpdatedAt": "2024-01-15T02:45:35.984Z",
|
||||
"CreatedAt": "2024-01-15T02:45:35.984Z",
|
||||
"Deleted": false
|
||||
},
|
||||
"Deprecated": false
|
||||
},
|
||||
{
|
||||
"ID": "65a49d1a5389294d1c348513",
|
||||
"Name": "3 месяца",
|
||||
"Layer": 1,
|
||||
"Description": "3 Месяца безлимита",
|
||||
"Condition": {
|
||||
"Period": {
|
||||
"From": "2024-01-15T02:48:59.617Z",
|
||||
"To": "2024-02-14T02:48:59.617Z"
|
||||
},
|
||||
"User": "",
|
||||
"UserType": "",
|
||||
"Coupon": "",
|
||||
"PurchasesAmount": "0",
|
||||
"CartPurchasesAmount": "0",
|
||||
"Product": "quizUnlimTime",
|
||||
"Term": "90",
|
||||
"Usage": "0",
|
||||
"PriceFrom": "0",
|
||||
"Group": ""
|
||||
},
|
||||
"Target": {
|
||||
"Products": [
|
||||
{
|
||||
"ID": "quizUnlimTime",
|
||||
"Factor": 0.8,
|
||||
"Overhelm": false
|
||||
}
|
||||
],
|
||||
"Factor": 0.8,
|
||||
"TargetScope": "Sum",
|
||||
"TargetGroup": "",
|
||||
"Overhelm": false
|
||||
},
|
||||
"Audit": {
|
||||
"UpdatedAt": "2024-01-15T02:48:58.560Z",
|
||||
"CreatedAt": "2024-01-15T02:48:58.560Z",
|
||||
"Deleted": false
|
||||
},
|
||||
"Deprecated": false
|
||||
},
|
||||
{
|
||||
"ID": "65a49d335389294d1c348514",
|
||||
"Name": "Год",
|
||||
"Layer": 1,
|
||||
"Description": "Год безлимита",
|
||||
"Condition": {
|
||||
"Period": {
|
||||
"From": "2024-01-15T02:49:24.266Z",
|
||||
"To": "2024-02-14T02:49:24.266Z"
|
||||
},
|
||||
"User": "",
|
||||
"UserType": "",
|
||||
"Coupon": "",
|
||||
"PurchasesAmount": "0",
|
||||
"CartPurchasesAmount": "0",
|
||||
"Product": "quizUnlimTime",
|
||||
"Term": "365",
|
||||
"Usage": "0",
|
||||
"PriceFrom": "0",
|
||||
"Group": ""
|
||||
},
|
||||
"Target": {
|
||||
"Products": [
|
||||
{
|
||||
"ID": "quizUnlimTime",
|
||||
"Factor": 0.65,
|
||||
"Overhelm": false
|
||||
}
|
||||
],
|
||||
"Factor": 0.65,
|
||||
"TargetScope": "Sum",
|
||||
"TargetGroup": "",
|
||||
"Overhelm": false
|
||||
},
|
||||
"Audit": {
|
||||
"UpdatedAt": "2024-01-15T02:49:23.024Z",
|
||||
"CreatedAt": "2024-01-15T02:49:23.024Z",
|
||||
"Deleted": false
|
||||
},
|
||||
"Deprecated": false
|
||||
},
|
||||
{
|
||||
"ID": "65a49d4f5389294d1c348515",
|
||||
"Name": "3 Года",
|
||||
"Layer": 1,
|
||||
"Description": "3 Года безлимита",
|
||||
"Condition": {
|
||||
"Period": {
|
||||
"From": "2024-01-15T02:49:52.264Z",
|
||||
"To": "2024-02-14T02:49:52.264Z"
|
||||
},
|
||||
"User": "",
|
||||
"UserType": "",
|
||||
"Coupon": "",
|
||||
"PurchasesAmount": "0",
|
||||
"CartPurchasesAmount": "0",
|
||||
"Product": "quizUnlimTime",
|
||||
"Term": "1095",
|
||||
"Usage": "0",
|
||||
"PriceFrom": "0",
|
||||
"Group": ""
|
||||
},
|
||||
"Target": {
|
||||
"Products": [
|
||||
{
|
||||
"ID": "quizUnlimTime",
|
||||
"Factor": 0.5,
|
||||
"Overhelm": false
|
||||
}
|
||||
],
|
||||
"Factor": 0.5,
|
||||
"TargetScope": "Sum",
|
||||
"TargetGroup": "",
|
||||
"Overhelm": false
|
||||
},
|
||||
"Audit": {
|
||||
"UpdatedAt": "2024-01-15T02:49:51.024Z",
|
||||
"CreatedAt": "2024-01-15T02:49:51.024Z",
|
||||
"Deleted": false
|
||||
},
|
||||
"Deprecated": false
|
||||
}
|
||||
];
|
84
lib/utils/cart/mockData/results.ts
Normal file
84
lib/utils/cart/mockData/results.ts
Normal file
@ -0,0 +1,84 @@
|
||||
type CartTestResult = [
|
||||
/** Cart price */
|
||||
number,
|
||||
/** Used tariff mask, last number shows if nko applied */
|
||||
number[],
|
||||
];
|
||||
|
||||
export const cartTestResults: CartTestResult[] = [
|
||||
[750184.5, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0]],
|
||||
[680, [0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]],
|
||||
[232560, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0]],
|
||||
[910227.5, [0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0]],
|
||||
[1274000, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0]],
|
||||
[95000, [0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
|
||||
[465000, [0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0]],
|
||||
[1700, [0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
|
||||
[465093, [0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0]],
|
||||
[910273, [0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0]],
|
||||
[910000, [0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0]],
|
||||
[848285.55, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
|
||||
[911207.5700000001, [0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0]],
|
||||
[383158.75, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
|
||||
[1101077.25, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0]],
|
||||
[602756.25, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0]],
|
||||
[465069.75, [0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
|
||||
[910887.25, [0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0]],
|
||||
[910250.25, [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0]],
|
||||
[116280, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
|
||||
[864016.5, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0]],
|
||||
[348840, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0]],
|
||||
[910591.5, [0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0]],
|
||||
[465186, [0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0]],
|
||||
[465000, [0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0]],
|
||||
[912912, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0]],
|
||||
[910000, [0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0]],
|
||||
[465569.82499999995, [0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
|
||||
[683432.355, [0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0]],
|
||||
[465068.35500000004, [0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0]],
|
||||
[910039.5850000001, [0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0]],
|
||||
[910089.635, [0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0]],
|
||||
[637980, [0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
|
||||
[865644, [0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0]],
|
||||
[910078.26, [1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0]],
|
||||
[910076.4400000001, [0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0]],
|
||||
[973904.9775, [0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
|
||||
[1196672.9775, [0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0]],
|
||||
[749535.36, [0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
|
||||
[956184.3200000001, [0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0]],
|
||||
[910247.52, [0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0]],
|
||||
[903498.255, [0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
|
||||
[911006.915, [1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
|
||||
[1180018.385, [0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0]],
|
||||
[797926.05, [0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0]],
|
||||
[910668.85, [0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0]],
|
||||
[1035015.8, [0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0]],
|
||||
[536665.8, [0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0]],
|
||||
[910054.6, [0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0]],
|
||||
[910338.0650000001, [0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0]],
|
||||
[877021.155, [0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0]],
|
||||
[1037969.66, [0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0]],
|
||||
[1147125.98, [0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0]],
|
||||
[760745.5800000001, [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
|
||||
[967153.4600000001, [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0]],
|
||||
[910182, [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0]],
|
||||
[1092804.6675, [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
|
||||
[1315572.6675, [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0]],
|
||||
[1504998.2675, [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0]],
|
||||
[872300.9400000001, [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
|
||||
[1076309.78, [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0]],
|
||||
[910538.7200000001, [0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0]],
|
||||
[1265735.3800000001, [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0]],
|
||||
[902551.05, [0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
|
||||
[1105909.35, [0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0]],
|
||||
[910080.0800000001, [0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
|
||||
[1295334.95, [0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0]],
|
||||
[1354679.69, [0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
|
||||
[910176.54, [1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0]],
|
||||
[902190.2100000001, [0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
|
||||
[1105556.27, [0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0]],
|
||||
[910063.7000000001, [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
|
||||
[902280.42, [1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
|
||||
[1105644.54, [1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0]],
|
||||
[1295070.1400000001, [1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0]],
|
||||
];
|
812
lib/utils/cart/mockData/tariffs.ts
Normal file
812
lib/utils/cart/mockData/tariffs.ts
Normal file
@ -0,0 +1,812 @@
|
||||
import { Tariff } from "../../../model/tariff";
|
||||
|
||||
|
||||
export const testTariffs: Tariff[] = [
|
||||
{
|
||||
"_id": "64f06be63fae7d590bf6426c",
|
||||
"name": "Безлимит, Количество Шаблонов, 2023-08-31T10:31:02.472Z",
|
||||
"isCustom": true,
|
||||
"privileges": [
|
||||
{
|
||||
"name": "Безлимит",
|
||||
"privilegeId": "64e88d30c4c82e949d5c443d",
|
||||
"serviceKey": "templategen",
|
||||
"description": "Количество дней, в течении которых пользование сервисом безлимитно",
|
||||
"type": "day",
|
||||
"value": "день",
|
||||
"price": 10,
|
||||
"amount": 130
|
||||
},
|
||||
{
|
||||
"name": "Количество Шаблонов",
|
||||
"privilegeId": "64e88d30c4c82e949d5c443e",
|
||||
"serviceKey": "templategen",
|
||||
"description": "Количество шаблонов, которые может сделать пользователь сервиса",
|
||||
"type": "count",
|
||||
"value": "шаблон",
|
||||
"price": 5,
|
||||
"amount": 2100
|
||||
}
|
||||
],
|
||||
"isDeleted": false,
|
||||
"createdAt": "2023-08-31T10:31:02.570Z",
|
||||
"updatedAt": "2023-08-31T10:31:02.570Z"
|
||||
},
|
||||
{
|
||||
"_id": "64f06be93fae7d590bf64271",
|
||||
"name": "Безлимит, Количество Шаблонов, 2023-08-31T10:31:05.703Z",
|
||||
"isCustom": true,
|
||||
"privileges": [
|
||||
{
|
||||
"name": "Безлимит",
|
||||
"privilegeId": "64e88d30c4c82e949d5c443d",
|
||||
"serviceKey": "templategen",
|
||||
"description": "Количество дней, в течении которых пользование сервисом безлимитно",
|
||||
"type": "day",
|
||||
"value": "день",
|
||||
"price": 10,
|
||||
"amount": 130
|
||||
},
|
||||
{
|
||||
"name": "Количество Шаблонов",
|
||||
"privilegeId": "64e88d30c4c82e949d5c443e",
|
||||
"serviceKey": "templategen",
|
||||
"description": "Количество шаблонов, которые может сделать пользователь сервиса",
|
||||
"type": "count",
|
||||
"value": "шаблон",
|
||||
"price": 5,
|
||||
"amount": 2100
|
||||
}
|
||||
],
|
||||
"isDeleted": false,
|
||||
"createdAt": "2023-08-31T10:31:05.732Z",
|
||||
"updatedAt": "2023-08-31T10:31:05.732Z"
|
||||
},
|
||||
{
|
||||
"_id": "64f06be93fae7d590bf64276",
|
||||
"name": "Безлимит, Количество Шаблонов, 2023-08-31T10:31:05.874Z",
|
||||
"isCustom": true,
|
||||
"privileges": [
|
||||
{
|
||||
"name": "Безлимит",
|
||||
"privilegeId": "64e88d30c4c82e949d5c443d",
|
||||
"serviceKey": "templategen",
|
||||
"description": "Количество дней, в течении которых пользование сервисом безлимитно",
|
||||
"type": "day",
|
||||
"value": "день",
|
||||
"price": 10,
|
||||
"amount": 130
|
||||
},
|
||||
{
|
||||
"name": "Количество Шаблонов",
|
||||
"privilegeId": "64e88d30c4c82e949d5c443e",
|
||||
"serviceKey": "templategen",
|
||||
"description": "Количество шаблонов, которые может сделать пользователь сервиса",
|
||||
"type": "count",
|
||||
"value": "шаблон",
|
||||
"price": 5,
|
||||
"amount": 2100
|
||||
}
|
||||
],
|
||||
"isDeleted": false,
|
||||
"createdAt": "2023-08-31T10:31:05.884Z",
|
||||
"updatedAt": "2023-08-31T10:31:05.884Z"
|
||||
},
|
||||
{
|
||||
"_id": "64f06c103fae7d590bf6427b",
|
||||
"name": "Безлимит, Количество Шаблонов, 2023-08-31T10:31:44.015Z",
|
||||
"isCustom": true,
|
||||
"privileges": [
|
||||
{
|
||||
"name": "Безлимит",
|
||||
"privilegeId": "64e88d30c4c82e949d5c443d",
|
||||
"serviceKey": "templategen",
|
||||
"description": "Количество дней, в течении которых пользование сервисом безлимитно",
|
||||
"type": "day",
|
||||
"value": "день",
|
||||
"price": 10,
|
||||
"amount": 130
|
||||
},
|
||||
{
|
||||
"name": "Количество Шаблонов",
|
||||
"privilegeId": "64e88d30c4c82e949d5c443e",
|
||||
"serviceKey": "templategen",
|
||||
"description": "Количество шаблонов, которые может сделать пользователь сервиса",
|
||||
"type": "count",
|
||||
"value": "шаблон",
|
||||
"price": 5,
|
||||
"amount": 2100
|
||||
}
|
||||
],
|
||||
"isDeleted": false,
|
||||
"createdAt": "2023-08-31T10:31:44.198Z",
|
||||
"updatedAt": "2023-08-31T10:31:44.198Z"
|
||||
},
|
||||
{
|
||||
"_id": "64f06c123fae7d590bf64280",
|
||||
"name": "Безлимит, Количество Шаблонов, 2023-08-31T10:31:16.087Z",
|
||||
"isCustom": true,
|
||||
"privileges": [
|
||||
{
|
||||
"name": "Безлимит",
|
||||
"privilegeId": "64e88d30c4c82e949d5c443d",
|
||||
"serviceKey": "templategen",
|
||||
"description": "Количество дней, в течении которых пользование сервисом безлимитно",
|
||||
"type": "day",
|
||||
"value": "день",
|
||||
"price": 10,
|
||||
"amount": 130
|
||||
},
|
||||
{
|
||||
"name": "Количество Шаблонов",
|
||||
"privilegeId": "64e88d30c4c82e949d5c443e",
|
||||
"serviceKey": "templategen",
|
||||
"description": "Количество шаблонов, которые может сделать пользователь сервиса",
|
||||
"type": "count",
|
||||
"value": "шаблон",
|
||||
"price": 5,
|
||||
"amount": 2100
|
||||
}
|
||||
],
|
||||
"isDeleted": false,
|
||||
"createdAt": "2023-08-31T10:31:46.954Z",
|
||||
"updatedAt": "2023-08-31T10:31:46.954Z"
|
||||
},
|
||||
{
|
||||
"_id": "64f61a713fae7d590bf6494c",
|
||||
"name": "Размер Диска, Безлимит, 2023-09-04T17:57:05.862Z",
|
||||
"isCustom": true,
|
||||
"privileges": [
|
||||
{
|
||||
"name": "Размер Диска",
|
||||
"privilegeId": "64e88d30c4c82e949d5c443c",
|
||||
"serviceKey": "templategen",
|
||||
"description": "Обьём ПенаДиска для хранения шаблонов и результатов шаблонизации",
|
||||
"type": "count",
|
||||
"value": "МБ",
|
||||
"price": 555,
|
||||
"amount": 1500
|
||||
},
|
||||
{
|
||||
"name": "Безлимит",
|
||||
"privilegeId": "64e88d30c4c82e949d5c443d",
|
||||
"serviceKey": "templategen",
|
||||
"description": "Количество дней, в течении которых пользование сервисом безлимитно",
|
||||
"type": "day",
|
||||
"value": "день",
|
||||
"price": 10,
|
||||
"amount": 220
|
||||
}
|
||||
],
|
||||
"isDeleted": false,
|
||||
"createdAt": "2023-09-04T17:57:05.987Z",
|
||||
"updatedAt": "2023-09-04T17:57:05.987Z"
|
||||
},
|
||||
{
|
||||
"_id": "64ff6eb75913fc89c5667d85",
|
||||
"name": "Безлимит, 2023-09-11T19:47:03.383Z",
|
||||
"isCustom": true,
|
||||
"privileges": [
|
||||
{
|
||||
"name": "Безлимит",
|
||||
"privilegeId": "64e88d30c4c82e949d5c443d",
|
||||
"serviceKey": "templategen",
|
||||
"description": "Количество дней, в течении которых пользование сервисом безлимитно",
|
||||
"type": "day",
|
||||
"value": "день",
|
||||
"price": 10,
|
||||
"amount": 170
|
||||
}
|
||||
],
|
||||
"isDeleted": false,
|
||||
"createdAt": "2023-09-11T19:47:03.546Z",
|
||||
"updatedAt": "2023-09-11T19:47:03.546Z"
|
||||
},
|
||||
{
|
||||
"_id": "657b9b11215b615d2e35741f",
|
||||
"name": "100 шаблонов",
|
||||
"price": 0,
|
||||
"isCustom": false,
|
||||
"privileges": [
|
||||
{
|
||||
"name": "Количество Шаблонов",
|
||||
"privilegeId": "templateCnt",
|
||||
"serviceKey": "templategen",
|
||||
"description": "Количество шаблонов, которые может сделать пользователь сервиса",
|
||||
"type": "count",
|
||||
"value": "шаблон",
|
||||
"price": 1000,
|
||||
"amount": 100
|
||||
}
|
||||
],
|
||||
"isDeleted": false,
|
||||
"createdAt": "2023-12-15T00:17:21.104Z",
|
||||
"updatedAt": "2023-12-15T00:17:21.104Z"
|
||||
},
|
||||
{
|
||||
"_id": "657b9b1a215b615d2e357424",
|
||||
"name": "1000 шаблонов",
|
||||
"price": 0,
|
||||
"isCustom": false,
|
||||
"privileges": [
|
||||
{
|
||||
"name": "Количество Шаблонов",
|
||||
"privilegeId": "templateCnt",
|
||||
"serviceKey": "templategen",
|
||||
"description": "Количество шаблонов, которые может сделать пользователь сервиса",
|
||||
"type": "count",
|
||||
"value": "шаблон",
|
||||
"price": 1000,
|
||||
"amount": 1000
|
||||
}
|
||||
],
|
||||
"isDeleted": false,
|
||||
"createdAt": "2023-12-15T00:17:30.813Z",
|
||||
"updatedAt": "2023-12-15T00:17:30.813Z"
|
||||
},
|
||||
{
|
||||
"_id": "657b9b98215b615d2e35743a",
|
||||
"name": "10000 шаблонов",
|
||||
"price": 0,
|
||||
"isCustom": false,
|
||||
"privileges": [
|
||||
{
|
||||
"name": "Количество Шаблонов",
|
||||
"privilegeId": "templateCnt",
|
||||
"serviceKey": "templategen",
|
||||
"description": "Количество шаблонов, которые может сделать пользователь сервиса",
|
||||
"type": "count",
|
||||
"value": "шаблон",
|
||||
"price": 1000,
|
||||
"amount": 10000
|
||||
}
|
||||
],
|
||||
"isDeleted": false,
|
||||
"createdAt": "2023-12-15T00:19:36.848Z",
|
||||
"updatedAt": "2023-12-15T00:19:36.848Z"
|
||||
},
|
||||
{
|
||||
"_id": "657b9bd0215b615d2e357445",
|
||||
"name": "1 день",
|
||||
"price": 10000,
|
||||
"isCustom": false,
|
||||
"privileges": [
|
||||
{
|
||||
"name": "Безлимит",
|
||||
"privilegeId": "templateUnlimTime",
|
||||
"serviceKey": "templategen",
|
||||
"description": "Количество дней, в течении которых пользование сервисом безлимитно",
|
||||
"type": "day",
|
||||
"value": "день",
|
||||
"price": 1700,
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"isDeleted": false,
|
||||
"createdAt": "2023-12-15T00:20:32.586Z",
|
||||
"updatedAt": "2023-12-15T00:20:32.586Z"
|
||||
},
|
||||
{
|
||||
"_id": "657b9c15215b615d2e35745f",
|
||||
"name": "Месяц",
|
||||
"price": 0,
|
||||
"isCustom": false,
|
||||
"privileges": [
|
||||
{
|
||||
"name": "Безлимит",
|
||||
"privilegeId": "templateUnlimTime",
|
||||
"serviceKey": "templategen",
|
||||
"description": "Количество дней, в течении которых пользование сервисом безлимитно",
|
||||
"type": "day",
|
||||
"value": "день",
|
||||
"price": 1700,
|
||||
"amount": 30
|
||||
}
|
||||
],
|
||||
"isDeleted": false,
|
||||
"createdAt": "2023-12-15T00:21:41.878Z",
|
||||
"updatedAt": "2023-12-15T00:21:41.878Z"
|
||||
},
|
||||
{
|
||||
"_id": "657b9c25215b615d2e357464",
|
||||
"name": "3 месяца",
|
||||
"price": 0,
|
||||
"isCustom": false,
|
||||
"privileges": [
|
||||
{
|
||||
"name": "Безлимит",
|
||||
"privilegeId": "templateUnlimTime",
|
||||
"serviceKey": "templategen",
|
||||
"description": "Количество дней, в течении которых пользование сервисом безлимитно",
|
||||
"type": "day",
|
||||
"value": "день",
|
||||
"price": 1700,
|
||||
"amount": 90
|
||||
}
|
||||
],
|
||||
"isDeleted": false,
|
||||
"createdAt": "2023-12-15T00:21:57.114Z",
|
||||
"updatedAt": "2023-12-15T00:21:57.114Z"
|
||||
},
|
||||
{
|
||||
"_id": "657b9c4d215b615d2e357469",
|
||||
"name": "Год",
|
||||
"price": 0,
|
||||
"isCustom": false,
|
||||
"privileges": [
|
||||
{
|
||||
"name": "Безлимит",
|
||||
"privilegeId": "templateUnlimTime",
|
||||
"serviceKey": "templategen",
|
||||
"description": "Количество дней, в течении которых пользование сервисом безлимитно",
|
||||
"type": "day",
|
||||
"value": "день",
|
||||
"price": 1700,
|
||||
"amount": 365
|
||||
}
|
||||
],
|
||||
"isDeleted": false,
|
||||
"createdAt": "2023-12-15T00:22:37.456Z",
|
||||
"updatedAt": "2023-12-15T00:22:37.456Z"
|
||||
},
|
||||
{
|
||||
"_id": "657b9c59215b615d2e35746e",
|
||||
"name": "3 года",
|
||||
"price": 0,
|
||||
"isCustom": false,
|
||||
"privileges": [
|
||||
{
|
||||
"name": "Безлимит",
|
||||
"privilegeId": "templateUnlimTime",
|
||||
"serviceKey": "templategen",
|
||||
"description": "Количество дней, в течении которых пользование сервисом безлимитно",
|
||||
"type": "day",
|
||||
"value": "день",
|
||||
"price": 1700,
|
||||
"amount": 1095
|
||||
}
|
||||
],
|
||||
"isDeleted": false,
|
||||
"createdAt": "2023-12-15T00:22:49.492Z",
|
||||
"updatedAt": "2023-12-15T00:22:49.492Z"
|
||||
},
|
||||
{
|
||||
"_id": "65af14358507c326f5a2db91",
|
||||
"name": "Безлимит Опросов, Количество Заявок, 2024-01-23T01:19:49.676Z",
|
||||
"isCustom": true,
|
||||
"privileges": [
|
||||
{
|
||||
"name": "Безлимит Опросов",
|
||||
"privilegeId": "quizUnlimTime",
|
||||
"serviceKey": "squiz",
|
||||
"description": "Количество дней, в течении которых пользование сервисом безлимитно",
|
||||
"type": "day",
|
||||
"value": "день",
|
||||
"price": 3400,
|
||||
"amount": 30
|
||||
},
|
||||
{
|
||||
"name": "Количество Заявок",
|
||||
"privilegeId": "quizCnt",
|
||||
"serviceKey": "squiz",
|
||||
"description": "Количество полных прохождений опросов",
|
||||
"type": "count",
|
||||
"value": "заявка",
|
||||
"price": 2000,
|
||||
"amount": 100
|
||||
}
|
||||
],
|
||||
"isDeleted": false,
|
||||
"createdAt": "2024-01-23T01:19:49.897Z",
|
||||
"updatedAt": "2024-01-23T01:19:49.897Z"
|
||||
},
|
||||
{
|
||||
"_id": "65af1b978507c326f5a2dbaa",
|
||||
"name": "Безлимит Опросов, Количество Заявок, 2024-01-23T01:51:19.622Z",
|
||||
"isCustom": true,
|
||||
"privileges": [
|
||||
{
|
||||
"name": "Безлимит Опросов",
|
||||
"privilegeId": "quizUnlimTime",
|
||||
"serviceKey": "squiz",
|
||||
"description": "Количество дней, в течении которых пользование сервисом безлимитно",
|
||||
"type": "day",
|
||||
"value": "день",
|
||||
"price": 3400,
|
||||
"amount": 3
|
||||
},
|
||||
{
|
||||
"name": "Количество Заявок",
|
||||
"privilegeId": "quizCnt",
|
||||
"serviceKey": "squiz",
|
||||
"description": "Количество полных прохождений опросов",
|
||||
"type": "count",
|
||||
"value": "заявка",
|
||||
"price": 2000,
|
||||
"amount": 100
|
||||
}
|
||||
],
|
||||
"isDeleted": false,
|
||||
"createdAt": "2024-01-23T01:51:19.814Z",
|
||||
"updatedAt": "2024-01-23T01:51:19.814Z"
|
||||
},
|
||||
{
|
||||
"_id": "65afd0518507c326f5a2e59d",
|
||||
"name": "Безлимит Опросов, Количество Заявок, 2024-01-23T14:42:25.093Z",
|
||||
"isCustom": true,
|
||||
"privileges": [
|
||||
{
|
||||
"name": "Безлимит Опросов",
|
||||
"privilegeId": "quizUnlimTime",
|
||||
"serviceKey": "squiz",
|
||||
"description": "Количество дней, в течении которых пользование сервисом безлимитно",
|
||||
"type": "day",
|
||||
"value": "день",
|
||||
"price": 3400,
|
||||
"amount": 70
|
||||
},
|
||||
{
|
||||
"name": "Количество Заявок",
|
||||
"privilegeId": "quizCnt",
|
||||
"serviceKey": "squiz",
|
||||
"description": "Количество полных прохождений опросов",
|
||||
"type": "count",
|
||||
"value": "заявка",
|
||||
"price": 2000,
|
||||
"amount": 850
|
||||
}
|
||||
],
|
||||
"isDeleted": false,
|
||||
"createdAt": "2024-01-23T14:42:25.154Z",
|
||||
"updatedAt": "2024-01-23T14:42:25.154Z"
|
||||
},
|
||||
{
|
||||
"_id": "65afd05e8507c326f5a2e5a2",
|
||||
"name": "Безлимит Опросов, Количество Заявок, 2024-01-23T14:42:38.254Z",
|
||||
"isCustom": true,
|
||||
"privileges": [
|
||||
{
|
||||
"name": "Безлимит Опросов",
|
||||
"privilegeId": "quizUnlimTime",
|
||||
"serviceKey": "squiz",
|
||||
"description": "Количество дней, в течении которых пользование сервисом безлимитно",
|
||||
"type": "day",
|
||||
"value": "день",
|
||||
"price": 3400,
|
||||
"amount": 70
|
||||
},
|
||||
{
|
||||
"name": "Количество Заявок",
|
||||
"privilegeId": "quizCnt",
|
||||
"serviceKey": "squiz",
|
||||
"description": "Количество полных прохождений опросов",
|
||||
"type": "count",
|
||||
"value": "заявка",
|
||||
"price": 2000,
|
||||
"amount": 850
|
||||
}
|
||||
],
|
||||
"isDeleted": false,
|
||||
"createdAt": "2024-01-23T14:42:38.338Z",
|
||||
"updatedAt": "2024-01-23T14:42:38.339Z"
|
||||
},
|
||||
{
|
||||
"_id": "65afd0738507c326f5a2e5a7",
|
||||
"name": "Безлимит Опросов, Количество Заявок, 2024-01-23T14:42:58.966Z",
|
||||
"isCustom": true,
|
||||
"privileges": [
|
||||
{
|
||||
"name": "Безлимит Опросов",
|
||||
"privilegeId": "quizUnlimTime",
|
||||
"serviceKey": "squiz",
|
||||
"description": "Количество дней, в течении которых пользование сервисом безлимитно",
|
||||
"type": "day",
|
||||
"value": "день",
|
||||
"price": 3400,
|
||||
"amount": 70
|
||||
},
|
||||
{
|
||||
"name": "Количество Заявок",
|
||||
"privilegeId": "quizCnt",
|
||||
"serviceKey": "squiz",
|
||||
"description": "Количество полных прохождений опросов",
|
||||
"type": "count",
|
||||
"value": "заявка",
|
||||
"price": 2000,
|
||||
"amount": 850
|
||||
}
|
||||
],
|
||||
"isDeleted": false,
|
||||
"createdAt": "2024-01-23T14:42:59.050Z",
|
||||
"updatedAt": "2024-01-23T14:42:59.050Z"
|
||||
},
|
||||
{
|
||||
"_id": "65afd08a8507c326f5a2e5ac",
|
||||
"name": "Безлимит Опросов, Количество Заявок, 2024-01-23T14:43:22.214Z",
|
||||
"isCustom": true,
|
||||
"privileges": [
|
||||
{
|
||||
"name": "Безлимит Опросов",
|
||||
"privilegeId": "quizUnlimTime",
|
||||
"serviceKey": "squiz",
|
||||
"description": "Количество дней, в течении которых пользование сервисом безлимитно",
|
||||
"type": "day",
|
||||
"value": "день",
|
||||
"price": 3400,
|
||||
"amount": 30
|
||||
},
|
||||
{
|
||||
"name": "Количество Заявок",
|
||||
"privilegeId": "quizCnt",
|
||||
"serviceKey": "squiz",
|
||||
"description": "Количество полных прохождений опросов",
|
||||
"type": "count",
|
||||
"value": "заявка",
|
||||
"price": 2000,
|
||||
"amount": 100
|
||||
}
|
||||
],
|
||||
"isDeleted": false,
|
||||
"createdAt": "2024-01-23T14:43:22.284Z",
|
||||
"updatedAt": "2024-01-23T14:43:22.284Z"
|
||||
},
|
||||
{
|
||||
"_id": "65b2d740c644401f2ff3ad26",
|
||||
"name": "Безлимит Опросов, Количество Заявок, 2024-01-25T21:48:47.933Z",
|
||||
"isCustom": true,
|
||||
"privileges": [
|
||||
{
|
||||
"name": "Безлимит Опросов",
|
||||
"privilegeId": "quizUnlimTime",
|
||||
"serviceKey": "squiz",
|
||||
"description": "Количество дней, в течении которых пользование сервисом безлимитно",
|
||||
"type": "day",
|
||||
"value": "день",
|
||||
"price": 3400,
|
||||
"amount": 100
|
||||
},
|
||||
{
|
||||
"name": "Количество Заявок",
|
||||
"privilegeId": "quizCnt",
|
||||
"serviceKey": "squiz",
|
||||
"description": "Количество полных прохождений опросов",
|
||||
"type": "count",
|
||||
"value": "заявка",
|
||||
"price": 2000,
|
||||
"amount": 3480
|
||||
}
|
||||
],
|
||||
"isDeleted": false,
|
||||
"createdAt": "2024-01-25T21:48:48.015Z",
|
||||
"updatedAt": "2024-01-25T21:48:48.015Z"
|
||||
},
|
||||
{
|
||||
"_id": "657b9b06215b615d2e35741a",
|
||||
"name": "10 шаблонов",
|
||||
"price": 20000,
|
||||
"order": 1,
|
||||
"isCustom": false,
|
||||
"privileges": [
|
||||
{
|
||||
"name": "Количество Шаблонов",
|
||||
"privilegeId": "templateCnt",
|
||||
"serviceKey": "templategen",
|
||||
"description": "Количество шаблонов, которые может сделать пользователь сервиса",
|
||||
"type": "count",
|
||||
"value": "шаблон",
|
||||
"price": 1000,
|
||||
"amount": 0
|
||||
}
|
||||
],
|
||||
"isDeleted": false,
|
||||
"createdAt": "2024-01-14T19:22:07.206Z",
|
||||
"updatedAt": "2024-01-14T19:22:07.206Z"
|
||||
},
|
||||
{
|
||||
"_id": "65a493550089bcd87ba53d4b",
|
||||
"name": "10 заявок",
|
||||
"description": "Полное прохождение 10 опросов респондентом",
|
||||
"price": 0,
|
||||
"order": 1,
|
||||
"isCustom": false,
|
||||
"privileges": [
|
||||
{
|
||||
"name": "Количество Заявок",
|
||||
"privilegeId": "quizCnt",
|
||||
"serviceKey": "squiz",
|
||||
"description": "Количество полных прохождений опросов",
|
||||
"type": "count",
|
||||
"value": "заявка",
|
||||
"price": 2000,
|
||||
"amount": 10
|
||||
}
|
||||
],
|
||||
"isDeleted": false,
|
||||
"createdAt": "2024-01-15T02:07:17.403Z",
|
||||
"updatedAt": "2024-01-15T02:07:17.403Z"
|
||||
},
|
||||
{
|
||||
"_id": "65a493b60089bcd87ba53d5f",
|
||||
"name": "1 день",
|
||||
"description": "день безлимитного пользования сервисом",
|
||||
"price": 10000,
|
||||
"order": 1,
|
||||
"isCustom": false,
|
||||
"privileges": [
|
||||
{
|
||||
"name": "Безлимит Опросов",
|
||||
"privilegeId": "quizUnlimTime",
|
||||
"serviceKey": "squiz",
|
||||
"description": "Количество дней, в течении которых пользование сервисом безлимитно",
|
||||
"type": "day",
|
||||
"value": "день",
|
||||
"price": 3400,
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"isDeleted": false,
|
||||
"createdAt": "2024-01-15T02:08:54.275Z",
|
||||
"updatedAt": "2024-01-15T02:08:54.275Z"
|
||||
},
|
||||
{
|
||||
"_id": "65a493640089bcd87ba53d50",
|
||||
"name": "100 заявок",
|
||||
"description": "Полное прохождение 100 опросов респондентом",
|
||||
"price": 0,
|
||||
"order": 2,
|
||||
"isCustom": false,
|
||||
"privileges": [
|
||||
{
|
||||
"name": "Количество Заявок",
|
||||
"privilegeId": "quizCnt",
|
||||
"serviceKey": "squiz",
|
||||
"description": "Количество полных прохождений опросов",
|
||||
"type": "count",
|
||||
"value": "заявка",
|
||||
"price": 2000,
|
||||
"amount": 100
|
||||
}
|
||||
],
|
||||
"isDeleted": false,
|
||||
"createdAt": "2024-01-15T02:07:32.444Z",
|
||||
"updatedAt": "2024-01-15T02:07:32.444Z"
|
||||
},
|
||||
{
|
||||
"_id": "65a497890089bcd87ba53d64",
|
||||
"name": "Месяц",
|
||||
"description": "Месяц безлимитного пользования сервисом",
|
||||
"price": 0,
|
||||
"order": 2,
|
||||
"isCustom": false,
|
||||
"privileges": [
|
||||
{
|
||||
"name": "Безлимит Опросов",
|
||||
"privilegeId": "quizUnlimTime",
|
||||
"serviceKey": "squiz",
|
||||
"description": "Количество дней, в течении которых пользование сервисом безлимитно",
|
||||
"type": "day",
|
||||
"value": "день",
|
||||
"price": 3400,
|
||||
"amount": 30
|
||||
}
|
||||
],
|
||||
"isDeleted": false,
|
||||
"createdAt": "2024-01-15T02:25:13.366Z",
|
||||
"updatedAt": "2024-01-15T02:25:13.366Z"
|
||||
},
|
||||
{
|
||||
"_id": "65a493740089bcd87ba53d55",
|
||||
"name": "1000 заявок",
|
||||
"description": "Полное прохождение 1000 опросов респондентом",
|
||||
"price": 0,
|
||||
"order": 3,
|
||||
"isCustom": false,
|
||||
"privileges": [
|
||||
{
|
||||
"name": "Количество Заявок",
|
||||
"privilegeId": "quizCnt",
|
||||
"serviceKey": "squiz",
|
||||
"description": "Количество полных прохождений опросов",
|
||||
"type": "count",
|
||||
"value": "заявка",
|
||||
"price": 2000,
|
||||
"amount": 1000
|
||||
}
|
||||
],
|
||||
"isDeleted": false,
|
||||
"createdAt": "2024-01-15T02:07:48.095Z",
|
||||
"updatedAt": "2024-01-15T02:07:48.095Z"
|
||||
},
|
||||
{
|
||||
"_id": "65a4987e0089bcd87ba53d75",
|
||||
"name": "3 Месяца",
|
||||
"description": "3 Месяца безлимитного пользования сервисом",
|
||||
"price": 0,
|
||||
"order": 3,
|
||||
"isCustom": false,
|
||||
"privileges": [
|
||||
{
|
||||
"name": "Безлимит Опросов",
|
||||
"privilegeId": "quizUnlimTime",
|
||||
"serviceKey": "squiz",
|
||||
"description": "Количество дней, в течении которых пользование сервисом безлимитно",
|
||||
"type": "day",
|
||||
"value": "день",
|
||||
"price": 3400,
|
||||
"amount": 90
|
||||
}
|
||||
],
|
||||
"isDeleted": false,
|
||||
"createdAt": "2024-01-15T02:29:18.577Z",
|
||||
"updatedAt": "2024-01-15T02:29:18.577Z"
|
||||
},
|
||||
{
|
||||
"_id": "65a498cc0089bcd87ba53d92",
|
||||
"name": "Год",
|
||||
"description": "Год безлимитного пользования сервисом",
|
||||
"price": 0,
|
||||
"order": 3,
|
||||
"isCustom": false,
|
||||
"privileges": [
|
||||
{
|
||||
"name": "Безлимит Опросов",
|
||||
"privilegeId": "quizUnlimTime",
|
||||
"serviceKey": "squiz",
|
||||
"description": "Количество дней, в течении которых пользование сервисом безлимитно",
|
||||
"type": "day",
|
||||
"value": "день",
|
||||
"price": 3400,
|
||||
"amount": 365
|
||||
}
|
||||
],
|
||||
"isDeleted": false,
|
||||
"createdAt": "2024-01-15T02:30:36.131Z",
|
||||
"updatedAt": "2024-01-15T02:30:36.131Z"
|
||||
},
|
||||
{
|
||||
"_id": "65a493830089bcd87ba53d5a",
|
||||
"name": "10000 заявок",
|
||||
"description": "Полное прохождение 10000 опросов респондентом",
|
||||
"price": 0,
|
||||
"order": 4,
|
||||
"isCustom": false,
|
||||
"privileges": [
|
||||
{
|
||||
"name": "Количество Заявок",
|
||||
"privilegeId": "quizCnt",
|
||||
"serviceKey": "squiz",
|
||||
"description": "Количество полных прохождений опросов",
|
||||
"type": "count",
|
||||
"value": "заявка",
|
||||
"price": 2000,
|
||||
"amount": 10000
|
||||
}
|
||||
],
|
||||
"isDeleted": false,
|
||||
"createdAt": "2024-01-15T02:08:03.341Z",
|
||||
"updatedAt": "2024-01-15T02:08:03.341Z"
|
||||
},
|
||||
{
|
||||
"_id": "65a498f80089bcd87ba53d97",
|
||||
"name": "3 Года",
|
||||
"description": "3 Года безлимитного пользования сервисом",
|
||||
"price": 0,
|
||||
"order": 4,
|
||||
"isCustom": false,
|
||||
"privileges": [
|
||||
{
|
||||
"name": "Безлимит Опросов",
|
||||
"privilegeId": "quizUnlimTime",
|
||||
"serviceKey": "squiz",
|
||||
"description": "Количество дней, в течении которых пользование сервисом безлимитно",
|
||||
"type": "day",
|
||||
"value": "день",
|
||||
"price": 3400,
|
||||
"amount": 1095
|
||||
}
|
||||
],
|
||||
"isDeleted": false,
|
||||
"createdAt": "2024-01-15T02:31:20.448Z",
|
||||
"updatedAt": "2024-01-15T02:31:20.448Z"
|
||||
}
|
||||
];
|
127
lib/utils/cart/utils.ts
Normal file
127
lib/utils/cart/utils.ts
Normal file
@ -0,0 +1,127 @@
|
||||
import { Discount } from "../../model/discount";
|
||||
|
||||
|
||||
export function findDiscountFactor(discount: Discount | null | undefined): number {
|
||||
if (!discount) return 1;
|
||||
|
||||
if (discount.Layer === 1) return discount.Target.Products[0].Factor;
|
||||
|
||||
return discount.Target.Factor;
|
||||
}
|
||||
|
||||
export function findNkoDiscount(discounts: Discount[]): Discount | null {
|
||||
const applicableDiscounts = discounts.filter(discount => discount.Condition.UserType === "nko");
|
||||
|
||||
if (!applicableDiscounts.length) return null;
|
||||
|
||||
const maxValueDiscount = applicableDiscounts.reduce((prev, current) => {
|
||||
return Number(current.Condition.CartPurchasesAmount) > Number(prev.Condition.CartPurchasesAmount) ? current : prev;
|
||||
});
|
||||
|
||||
return maxValueDiscount;
|
||||
}
|
||||
|
||||
export function findPrivilegeDiscount(
|
||||
privilegeId: string,
|
||||
privilegeAmount: number,
|
||||
discounts: Discount[],
|
||||
userId: string,
|
||||
): Discount | null {
|
||||
const applicableDiscounts = discounts.filter(discount => {
|
||||
return (
|
||||
discount.Layer === 1
|
||||
&& privilegeId === discount.Condition.Product
|
||||
&& privilegeAmount >= Number(discount.Condition.Term)
|
||||
&& (discount.Condition.User === "" || discount.Condition.User === userId)
|
||||
);
|
||||
});
|
||||
|
||||
if (!applicableDiscounts.length) return null;
|
||||
|
||||
let maxValueDiscount: Discount = applicableDiscounts[0];
|
||||
for (const discount of applicableDiscounts) {
|
||||
if (discount.Condition.User !== "" && discount.Condition.User === userId) {
|
||||
maxValueDiscount = discount;
|
||||
break;
|
||||
}
|
||||
|
||||
if (Number(discount.Condition.Term) > Number(maxValueDiscount.Condition.Term)) {
|
||||
maxValueDiscount = discount;
|
||||
}
|
||||
}
|
||||
|
||||
return maxValueDiscount;
|
||||
}
|
||||
|
||||
export function findServiceDiscount(
|
||||
serviceKey: string,
|
||||
currentPrice: number,
|
||||
discounts: Discount[],
|
||||
userId: string,
|
||||
): Discount | null {
|
||||
const applicableDiscounts = discounts.filter(discount => {
|
||||
return (
|
||||
discount.Layer === 2
|
||||
&& serviceKey === discount.Condition.Group
|
||||
&& currentPrice >= Number(discount.Condition.PriceFrom)
|
||||
&& (discount.Condition.User === "" || discount.Condition.User === userId)
|
||||
);
|
||||
});
|
||||
|
||||
if (!applicableDiscounts.length) return null;
|
||||
|
||||
let maxValueDiscount: Discount = applicableDiscounts[0];
|
||||
for (const discount of applicableDiscounts) {
|
||||
if (discount.Condition.User !== "" && discount.Condition.User === userId) {
|
||||
maxValueDiscount = discount;
|
||||
break;
|
||||
}
|
||||
|
||||
if (Number(discount.Condition.PriceFrom) > Number(maxValueDiscount.Condition.PriceFrom)) {
|
||||
maxValueDiscount = discount;
|
||||
}
|
||||
}
|
||||
|
||||
return maxValueDiscount;
|
||||
}
|
||||
|
||||
export function findCartDiscount(
|
||||
cartPurchasesAmount: number,
|
||||
discounts: Discount[],
|
||||
): Discount | null {
|
||||
const applicableDiscounts = discounts.filter(discount => {
|
||||
return (
|
||||
discount.Layer === 3
|
||||
&& cartPurchasesAmount >= Number(discount.Condition.CartPurchasesAmount)
|
||||
);
|
||||
});
|
||||
|
||||
if (!applicableDiscounts.length) return null;
|
||||
|
||||
const maxValueDiscount = applicableDiscounts.reduce((prev, current) => {
|
||||
return Number(current.Condition.CartPurchasesAmount) > Number(prev.Condition.CartPurchasesAmount) ? current : prev;
|
||||
});
|
||||
|
||||
return maxValueDiscount;
|
||||
}
|
||||
|
||||
export function findLoyaltyDiscount(
|
||||
purchasesAmount: number,
|
||||
discounts: Discount[],
|
||||
): Discount | null {
|
||||
const applicableDiscounts = discounts.filter(discount => {
|
||||
return (
|
||||
discount.Layer === 4
|
||||
&& discount.Condition.UserType !== "nko"
|
||||
&& purchasesAmount >= Number(discount.Condition.PurchasesAmount)
|
||||
);
|
||||
});
|
||||
|
||||
if (!applicableDiscounts.length) return null;
|
||||
|
||||
const maxValueDiscount = applicableDiscounts.reduce((prev, current) => {
|
||||
return Number(current.Condition.PurchasesAmount) > Number(prev.Condition.PurchasesAmount) ? current : prev;
|
||||
});
|
||||
|
||||
return maxValueDiscount;
|
||||
}
|
3
lib/utils/devlog.ts
Normal file
3
lib/utils/devlog.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export const devlog: typeof console.log = (...args) => {
|
||||
if (process.env.NODE_ENV === "development") console.log(...args);
|
||||
};
|
5
lib/utils/getInitials.ts
Normal file
5
lib/utils/getInitials.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export function getInitials(firstname: string, secondname: string) {
|
||||
return firstname[0] && secondname[0]
|
||||
? firstname[0] + secondname[0]
|
||||
: "АА";
|
||||
}
|
4
lib/utils/index.ts
Normal file
4
lib/utils/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from "./backendMessageHandler";
|
||||
export * from "./cart";
|
||||
export * from "./devlog";
|
||||
export * from "./getInitials";
|
6488
package-lock.json
generated
Normal file
6488
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
70
package.json
70
package.json
@ -1,12 +1,72 @@
|
||||
{
|
||||
"name": "@frontend/kitui",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.109",
|
||||
"description": "test",
|
||||
"main": "index.js",
|
||||
"repository": "git@penahub.gitlab.yandexcloud.net:frontend/kitui.git",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"repository": "git@gitea.pena:PenaSide/UIKit.git",
|
||||
"author": "skeris <kotilion.95@gmail.com>",
|
||||
"license": "MIT",
|
||||
"publishConfig": {
|
||||
"registry": "https://penahub.gitlab.yandexcloud.net/api/v4/projects/21/packages/npm/"
|
||||
"type": "module",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview",
|
||||
"test:cart": "vitest ./lib/utils/cart",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"publishConfig": {
|
||||
"registry": "http://gitea.pena/api/packages/skeris/npm/"
|
||||
},
|
||||
"dependencies": {
|
||||
"immer": "^10.0.2",
|
||||
"reconnecting-eventsource": "^1.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@mui/icons-material": "^5.14.3",
|
||||
"@mui/material": "^5.14.4",
|
||||
"@types/node": "^20.5.0",
|
||||
"@types/react": "^18.2.15",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@types/react-syntax-highlighter": "^15.5.7",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"@vitejs/plugin-react": "^4.0.3",
|
||||
"axios": "^1.4.0",
|
||||
"eslint": "^8.45.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.3",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.15.0",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"typescript": "^5.0.2",
|
||||
"vite": "^4.4.5",
|
||||
"vite-plugin-dts": "^3.5.2",
|
||||
"vitest": "^1.4.0",
|
||||
"zustand": "^4.3.8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@mui/icons-material": "^5.14.3",
|
||||
"@mui/material": "^5.14.4",
|
||||
"axios": "^1.4.0",
|
||||
"react": "^18.2.0",
|
||||
"react-router-dom": "^6.15.0",
|
||||
"zustand": "^4.3.8"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||
}
|
||||
|
274
src/App.tsx
Normal file
274
src/App.tsx
Normal file
@ -0,0 +1,274 @@
|
||||
import { Box, Button, Container, Pagination, SxProps, Theme, Typography, useTheme } from "@mui/material";
|
||||
import { PenaLink } from "../lib/components/PenaLink";
|
||||
import { PrismLight as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||
import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism";
|
||||
import jsx from "react-syntax-highlighter/dist/esm/languages/prism/tsx";
|
||||
import { ReactNode } from "react";
|
||||
import { BurgerButton, CloseButton, CloseButtonSmall, PenaTextField, WalletButton } from "../lib";
|
||||
import { AvatarButton } from "../lib/components/AvatarButton";
|
||||
import { LogoutButton } from "../lib/components/LogoutButton";
|
||||
|
||||
|
||||
SyntaxHighlighter.registerLanguage("jsx", jsx);
|
||||
|
||||
export function App() {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
backgroundColor: theme.palette.bg.main,
|
||||
minHeight: "100dvh",
|
||||
width: "100%",
|
||||
}}>
|
||||
<Container sx={{
|
||||
py: 4,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 3,
|
||||
alignItems: "start",
|
||||
color: "white",
|
||||
}}>
|
||||
<Typography variant="h4">Components</Typography>
|
||||
<ComponentWithCode
|
||||
code={`<Button variant="pena-contained-dark">Подробнее</Button>`}
|
||||
element={<Button variant="pena-contained-dark">Подробнее</Button>}
|
||||
/>
|
||||
<ComponentWithCode
|
||||
code={`<Button variant="pena-contained-light">Подробнее</Button>`}
|
||||
element={<Button variant="pena-contained-light">Подробнее</Button>}
|
||||
/>
|
||||
<ComponentWithCode
|
||||
code={`<Button variant="pena-outlined-dark">Подробнее</Button>`}
|
||||
element={<Button variant="pena-outlined-dark">Подробнее</Button>}
|
||||
/>
|
||||
<ComponentWithCode
|
||||
code={`<Button variant="pena-outlined-light">Подробнее</Button>`}
|
||||
element={<Button variant="pena-outlined-light">Подробнее</Button>}
|
||||
/>
|
||||
<ComponentWithCode
|
||||
code={`<Button variant="pena-outlined-purple">Перейти</Button>`}
|
||||
element={<Button variant="pena-outlined-purple">Перейти</Button>}
|
||||
/>
|
||||
<ComponentWithCode
|
||||
code={`<Button variant="pena-contained-white1">Купить</Button>`}
|
||||
element={<Button variant="pena-contained-white1">Купить</Button>}
|
||||
/>
|
||||
<ComponentWithCode
|
||||
code={`<Button variant="pena-contained-white2">Купить</Button>`}
|
||||
element={<Button variant="pena-contained-white2">Выбрать</Button>}
|
||||
/>
|
||||
<ComponentWithCode
|
||||
code={`<Button variant="pena-navitem-dark">Подробнее</Button>`}
|
||||
element={<Button variant="pena-navitem-dark">Подробнее</Button>}
|
||||
/>
|
||||
<ComponentWithCode
|
||||
code={`<Button variant="pena-navitem-light">Подробнее</Button>`}
|
||||
element={<Button variant="pena-navitem-light">Подробнее</Button>}
|
||||
/>
|
||||
<ComponentWithCode
|
||||
code={`<Button variant="pena-text">Подробнее</Button>`}
|
||||
element={<Button variant="pena-text">Подробнее</Button>}
|
||||
/>
|
||||
<ComponentWithCode
|
||||
code={`<PenaLink>Подробнее</PenaLink>`}
|
||||
element={<PenaLink href="/">Подробнее</PenaLink>}
|
||||
/>
|
||||
<ComponentWithCode
|
||||
code={`<PenaTextField />`}
|
||||
element={<PenaTextField />}
|
||||
/>
|
||||
<ComponentWithCode
|
||||
code={`<AvatarButton>AB</AvatarButton>`}
|
||||
element={<AvatarButton>AB</AvatarButton>}
|
||||
/>
|
||||
<ComponentWithCode
|
||||
code={`<LogoutButton />`}
|
||||
element={<LogoutButton />}
|
||||
/>
|
||||
<ComponentWithCode
|
||||
code={`<WalletButton />`}
|
||||
element={<WalletButton />}
|
||||
/>
|
||||
<ComponentWithCode
|
||||
code={`<CloseButton />`}
|
||||
element={<CloseButton />}
|
||||
/>
|
||||
<ComponentWithCode
|
||||
code={`<CloseButtonSmall />`}
|
||||
element={<CloseButtonSmall />}
|
||||
/>
|
||||
<ComponentWithCode
|
||||
code={`<BurgerButton />`}
|
||||
element={<BurgerButton />}
|
||||
/>
|
||||
<ComponentWithCode
|
||||
sx={{
|
||||
p: 1,
|
||||
backgroundColor: theme.palette.background.default,
|
||||
}}
|
||||
code={`<Pagination variant="pena-pagination" />`}
|
||||
element={<Pagination
|
||||
variant="pena-pagination"
|
||||
count={10}
|
||||
page={1}
|
||||
/>}
|
||||
/>
|
||||
<ComponentWithCode
|
||||
code={`<Typography variant="infographic">Some text</Typography>`}
|
||||
element={<Typography variant="infographic">Lorem ipsum dolor sit amet</Typography>}
|
||||
/>
|
||||
<ComponentWithCode
|
||||
code={`<Typography variant="p1">Some text</Typography>`}
|
||||
element={<Typography variant="p1">Lorem ipsum dolor sit amet</Typography>}
|
||||
/>
|
||||
<ComponentWithCode
|
||||
code={`<Typography variant="price">Some text</Typography>`}
|
||||
element={<Typography variant="price">Lorem ipsum dolor sit amet</Typography>}
|
||||
/>
|
||||
<ComponentWithCode
|
||||
code={`<Typography variant="oldPrice">Some text</Typography>`}
|
||||
element={<Typography variant="oldPrice">Lorem ipsum dolor sit amet</Typography>}
|
||||
/>
|
||||
<ComponentWithCode
|
||||
code={`<Typography variant="t1">Some text</Typography>`}
|
||||
element={<Typography variant="t1">Lorem ipsum dolor sit amet</Typography>}
|
||||
/>
|
||||
<ComponentWithCode
|
||||
code={`<Typography variant="pena-card-header1">Some text</Typography>`}
|
||||
element={<Typography variant="pena-card-header1">Lorem ipsum dolor sit amet</Typography>}
|
||||
/>
|
||||
<ComponentWithCode
|
||||
code={`<Typography variant="pena-h1">Some text</Typography>`}
|
||||
element={<Typography variant="pena-h1">Lorem ipsum dolor sit amet</Typography>}
|
||||
/>
|
||||
<ComponentWithCode
|
||||
code={`<Typography variant="pena-h3">Some text</Typography>`}
|
||||
element={<Typography variant="pena-h3">Lorem ipsum dolor sit amet</Typography>}
|
||||
/>
|
||||
<Typography variant="h4">Colors</Typography>
|
||||
<Typography variant="body2">Click text to copy</Typography>
|
||||
<Box sx={{
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
gap: 1,
|
||||
}}>
|
||||
<ColorShowcase
|
||||
color={theme.palette.purple.light}
|
||||
text1="theme.palette.purple.light"
|
||||
text2="#944FEE"
|
||||
/>
|
||||
<ColorShowcase
|
||||
color={theme.palette.purple.main}
|
||||
text1="theme.palette.purple.main"
|
||||
text2="#7E2AEA"
|
||||
/>
|
||||
<ColorShowcase
|
||||
color={theme.palette.purple.dark}
|
||||
text1="theme.palette.purple.dark"
|
||||
text2="#581CA7"
|
||||
/>
|
||||
<ColorShowcase
|
||||
color={theme.palette.background.default}
|
||||
text1="theme.palette.background.default"
|
||||
text2="#F2F3F7"
|
||||
/>
|
||||
<ColorShowcase
|
||||
color={theme.palette.bg.main}
|
||||
text1="theme.palette.bg.main"
|
||||
text2="#333647"
|
||||
/>
|
||||
<ColorShowcase
|
||||
color={theme.palette.bg.dark}
|
||||
text1="theme.palette.bg.dark"
|
||||
text2="#252734"
|
||||
/>
|
||||
<ColorShowcase
|
||||
color={theme.palette.gray.main}
|
||||
text1="theme.palette.gray.main"
|
||||
text2="#9A9AAF"
|
||||
/>
|
||||
<ColorShowcase
|
||||
color={theme.palette.gray.dark}
|
||||
text1="theme.palette.gray.dark"
|
||||
text2="#4D4D4D"
|
||||
/>
|
||||
<ColorShowcase
|
||||
color={theme.palette.orange.main}
|
||||
text1="theme.palette.orange.main"
|
||||
text2="#FB5607"
|
||||
/>
|
||||
<ColorShowcase
|
||||
color={theme.palette.orange.light}
|
||||
text1="theme.palette.orange.light"
|
||||
text2="#FC712F"
|
||||
/>
|
||||
</Box>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function ComponentWithCode({ sx, code, element }: {
|
||||
sx?: SxProps<Theme>;
|
||||
code: string;
|
||||
element: ReactNode;
|
||||
}) {
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 1,
|
||||
alignItems: "start",
|
||||
...sx,
|
||||
}}>
|
||||
<SyntaxHighlighter
|
||||
language="jsx"
|
||||
style={oneDark}
|
||||
customStyle={{
|
||||
padding: "4px",
|
||||
fontSize: "16px",
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
{code}
|
||||
</SyntaxHighlighter>
|
||||
{element}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function ColorShowcase({ color, text1, text2 }: {
|
||||
text1: string;
|
||||
text2: string;
|
||||
color: string;
|
||||
}) {
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
backgroundColor: color,
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
maxWidth: "350px",
|
||||
p: 2,
|
||||
gap: 1,
|
||||
border: "1px solid white",
|
||||
overflow: "hidden",
|
||||
}}>
|
||||
<Typography
|
||||
onClick={() => navigator.clipboard.writeText(text1)}
|
||||
sx={{
|
||||
textShadow: "0 0 6px black",
|
||||
}}
|
||||
>{text1}</Typography>
|
||||
<Typography
|
||||
onClick={() => navigator.clipboard.writeText(text2)}
|
||||
sx={{
|
||||
textShadow: "0 0 6px black",
|
||||
}}
|
||||
>{text2}</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
15
src/main.tsx
Normal file
15
src/main.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { App } from "./App.tsx";
|
||||
import { CssBaseline, ThemeProvider } from "@mui/material";
|
||||
import { penaMuiTheme } from "../lib/index.ts";
|
||||
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<ThemeProvider theme={penaMuiTheme}>
|
||||
<CssBaseline />
|
||||
<App />
|
||||
</ThemeProvider>
|
||||
</React.StrictMode>,
|
||||
);
|
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
32
tsconfig.json
Normal file
32
tsconfig.json
Normal file
@ -0,0 +1,32 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": [
|
||||
"ES2020",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": [
|
||||
"src",
|
||||
"lib"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
}
|
||||
]
|
||||
}
|
10
tsconfig.node.json
Normal file
10
tsconfig.node.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
29
vite.config.ts
Normal file
29
vite.config.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import react from '@vitejs/plugin-react';
|
||||
import { resolve } from "path";
|
||||
import { defineConfig } from 'vite';
|
||||
import dts from 'vite-plugin-dts';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react(), dts({ include: ["lib"] })],
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(__dirname, "lib/index.ts"),
|
||||
formats: ["es"],
|
||||
fileName: "index"
|
||||
},
|
||||
copyPublicDir: false,
|
||||
rollupOptions: {
|
||||
external: [
|
||||
"@emotion/react",
|
||||
"@emotion/styled",
|
||||
"@mui/icons-material",
|
||||
"@mui/material",
|
||||
"axios",
|
||||
"react-router-dom",
|
||||
"react",
|
||||
"zustand",
|
||||
],
|
||||
},
|
||||
}
|
||||
});
|
Loading…
Reference in New Issue
Block a user