Compare commits

...

80 Commits
dev ... main

Author SHA1 Message Date
be993e2206 Merge branch 'staging'
All checks were successful
Deploy / CreateImage (push) Successful in 4m44s
Deploy / DeployService (push) Successful in 22s
2025-06-25 14:12:27 +03:00
48ae1359ac возможность убрать смайлик
All checks were successful
Deploy / CreateImage (push) Successful in 10m41s
Deploy / DeployService (push) Successful in 26s
2025-06-23 15:30:06 +03:00
7686c9e326 текст в АИ раздельный
Some checks failed
Deploy / CreateImage (push) Failing after 53s
Deploy / DeployService (push) Has been skipped
2025-06-23 08:27:39 +03:00
2d087ae113 fix опечаток в AI
All checks were successful
Deploy / CreateImage (push) Successful in 2m45s
Deploy / DeployService (push) Successful in 22s
2025-06-23 08:27:13 +03:00
43839c5879 debug: return right pipeline logic
All checks were successful
Deploy / CreateImage (push) Successful in 9m43s
Deploy / DeployService (push) Successful in 22s
2025-06-22 22:11:24 +03:00
effc4cbea9 текст в АИ раздельный
Some checks failed
Deploy / DeployService (push) Successful in 21s
Deploy / CreateImage (push) Failing after 1m5s
2025-06-22 16:03:26 +03:00
89b55d4d3e Merge branch 'staging'
Some checks failed
Deploy / DeployService (push) Successful in 22s
Deploy / CreateImage (push) Has been cancelled
2025-06-18 01:37:52 +03:00
25deb4a0f4 есть id тарифа для прода
Some checks failed
Deploy / CreateImage (push) Failing after 11m59s
Deploy / DeployService (push) Has been cancelled
2025-06-18 00:23:50 +03:00
5ad3a810ef описания
All checks were successful
Deploy / CreateImage (push) Successful in 4m58s
Deploy / DeployService (push) Successful in 25s
2025-06-16 08:24:10 +03:00
5ff3d7ae16 3 min
All checks were successful
Deploy / CreateImage (push) Successful in 4m32s
Deploy / DeployService (push) Successful in 25s
2025-06-15 20:43:39 +03:00
8604daf6a4 тултип для показа таймера
All checks were successful
Deploy / CreateImage (push) Successful in 3m21s
Deploy / DeployService (push) Successful in 25s
2025-06-15 17:20:13 +03:00
a7c78e57e7 новая логика оплаты
All checks were successful
Deploy / CreateImage (push) Successful in 6m41s
Deploy / DeployService (push) Successful in 27s
2025-06-10 23:22:18 +03:00
a8d6db9f2d реактивное отображение скелетонов вместо копирования 2025-06-10 23:04:01 +03:00
c396752ce6 --
All checks were successful
Deploy / CreateImage (push) Successful in 3m21s
Deploy / DeployService (push) Successful in 27s
2025-06-09 23:55:24 +03:00
929e6047cf fix checkbox
All checks were successful
Deploy / CreateImage (push) Successful in 7m11s
Deploy / DeployService (push) Successful in 28s
2025-06-09 18:22:41 +03:00
6465652f82 перевод на аи
All checks were successful
Deploy / CreateImage (push) Successful in 11m2s
Deploy / DeployService (push) Successful in 27s
2025-06-09 17:26:19 +03:00
9bb69f1ab2 обноление данных о юзере после покупки 2025-06-09 16:47:26 +03:00
ac7c001738 оплата через хаб
All checks were successful
Deploy / CreateImage (push) Successful in 7m43s
Deploy / DeployService (push) Successful in 24s
2025-06-08 23:23:24 +03:00
eb5f8168c8 осталось добавить пополнение счёта 2025-06-08 23:08:47 +03:00
918b6bf145 возможность выбрать и корректно отправить пол 2025-06-08 22:56:22 +03:00
61c0357adc покупка при ок
Some checks failed
Deploy / CreateImage (push) Failing after 1m27s
Deploy / DeployService (push) Has been skipped
2025-06-08 20:52:55 +03:00
5ea6d6638a вынесены запросы тарифов и привилегий в отдельный хук 2025-06-08 10:17:50 +03:00
87d4c9d53d не копировать для новых ссылок (без возможности теста пока)
All checks were successful
Deploy / CreateImage (push) Successful in 3m14s
Deploy / DeployService (push) Successful in 24s
2025-06-07 06:24:08 +03:00
98bf92466b инфо открывается при наведении
Some checks failed
Deploy / CreateImage (push) Failing after 43s
Deploy / DeployService (push) Has been skipped
2025-06-06 16:25:06 +03:00
595f4b041b Можно выбрать оба пола
Some checks are pending
Deploy / CreateImage (push) Waiting to run
Deploy / DeployService (push) Blocked by required conditions
2025-06-06 16:06:50 +03:00
2efdd0b422 теперь до выбора типа не показывается null в меню
Some checks are pending
Deploy / CreateImage (push) Waiting to run
Deploy / DeployService (push) Blocked by required conditions
2025-06-06 14:53:57 +03:00
a83214acd4 теперь до выбора типа не показывается null в меню
Some checks failed
Deploy / CreateImage (push) Failing after 1m9s
Deploy / DeployService (push) Has been skipped
2025-06-06 14:53:15 +03:00
f0a977031d дизабл кнопки при некорректных условиях
All checks were successful
Deploy / CreateImage (push) Successful in 5m0s
Deploy / DeployService (push) Successful in 27s
2025-06-03 21:50:07 +03:00
f5a4bdc36f +qid 2025-06-03 21:10:14 +03:00
8255fe6908 корректные проверки селекта возраста 2025-06-03 21:07:37 +03:00
2540dd8079 селект+штпут 2025-06-03 20:27:14 +03:00
ff2569dea8 fix: base image change
All checks were successful
Deploy / CreateImage (push) Successful in 6m15s
Deploy / DeployService (push) Successful in 26s
2025-06-03 00:06:05 +03:00
dd2a96f948 ссылки с utm метками
Some checks failed
Deploy / CreateImage (push) Failing after 3m32s
Deploy / DeployService (push) Has been skipped
2025-06-02 21:22:55 +03:00
7f024bcf78 ссылки не переполняют 2025-06-02 21:19:54 +03:00
cc6d78935c селектор для мобилки 2025-06-02 18:53:46 +03:00
14bfc6750f добавление utm меток
Some checks failed
Deploy / CreateImage (push) Failing after 1m2s
Deploy / DeployService (push) Has been skipped
2025-06-02 01:48:35 +03:00
65db81af2d меню в хедере скроллится, меню исправлено 2025-06-02 01:32:35 +03:00
fbb6025512 добавить итем 2025-06-02 01:18:12 +03:00
1e8a50077b реактивное удаление 2025-06-02 00:36:55 +03:00
e7121cb06a удаление без реактивности, выведен корректный список линков 2025-06-02 00:27:22 +03:00
c2d79c04cc api файл и отображение списка 2025-06-01 17:05:41 +03:00
6273e62e66 на главную из установки квиза
All checks were successful
Deploy / CreateImage (push) Successful in 8m0s
Deploy / DeployService (push) Successful in 23s
2025-05-30 00:37:58 +03:00
0e0376686d fix
All checks were successful
Deploy / CreateImage (push) Successful in 5m7s
Deploy / DeployService (push) Successful in 22s
2025-05-29 17:37:43 +03:00
350b28cb6b remove развёрнутый ответ
All checks were successful
Deploy / CreateImage (push) Successful in 7m17s
Deploy / DeployService (push) Successful in 22s
2025-05-29 08:42:03 +03:00
ab1e221ada detailedAnswer multi
All checks were successful
Deploy / CreateImage (push) Successful in 7m28s
Deploy / DeployService (push) Successful in 22s
2025-05-28 10:19:25 +03:00
262337272f first AI page 2025-05-25 19:36:56 +03:00
e2e391325b nko
All checks were successful
Deploy / CreateImage (push) Successful in 3m31s
Deploy / DeployService (push) Successful in 23s
2025-05-22 23:10:15 +03:00
aa917fe6ba fix dragdrop
All checks were successful
Deploy / CreateImage (push) Successful in 8m9s
Deploy / DeployService (push) Successful in 24s
2025-05-22 00:37:21 +03:00
c6cb130d5b даты на general аналитике
All checks were successful
Deploy / CreateImage (push) Successful in 6m45s
Deploy / DeployService (push) Successful in 23s
2025-05-20 16:22:17 +03:00
4eda624080 update kit
All checks were successful
Deploy / CreateImage (push) Successful in 11m21s
Deploy / DeployService (push) Successful in 23s
2025-05-18 15:54:58 +03:00
22b61bed44 delete env dev
All checks were successful
Deploy / CreateImage (push) Successful in 8m39s
Deploy / DeployService (push) Successful in 23s
2025-05-18 02:27:52 +03:00
1b32719ef7 delete env dev
All checks were successful
Deploy / CreateImage (push) Successful in 9m11s
Deploy / DeployService (push) Successful in 22s
2025-05-18 02:02:32 +03:00
b9154061ec fix deployment
All checks were successful
Deploy / CreateImage (push) Successful in 3m58s
Deploy / DeployService (push) Successful in 25s
2025-05-17 14:52:50 +03:00
93af4a0707 staging template
Some checks failed
Deploy / CreateImage (push) Successful in 3m31s
Deploy / DeployService (push) Failing after 23s
2025-05-17 02:08:08 +03:00
39b0e04293 fix deploy staging
Some checks failed
Deploy / CreateImage (push) Failing after 1m19s
Deploy / DeployService (push) Has been skipped
2025-05-14 00:07:58 +03:00
1e657569a4 fix design
Some checks failed
Deploy / CreateImage (push) Has been cancelled
Deploy / DeployService (push) Has been cancelled
2025-04-22 18:47:47 +03:00
62762c955c add ai
All checks were successful
Deploy / CreateImage (push) Successful in 3m29s
Deploy / DeployService (push) Successful in 21s
2025-04-15 23:16:01 +03:00
9336508260 fix parse-error
All checks were successful
Deploy / CreateImage (push) Successful in 3m41s
Deploy / DeployService (push) Successful in 22s
2025-04-09 16:35:40 +03:00
cf1fce88cb cut send system true
All checks were successful
Deploy / CreateImage (push) Successful in 5m28s
Deploy / DeployService (push) Successful in 22s
2025-04-03 22:22:15 +03:00
459553056d useAmoAccount слушает токен 2025-04-03 22:20:02 +03:00
8e6f756f78 Merge branch 'labmain'
All checks were successful
Deploy / CreateImage (push) Successful in 4m43s
Deploy / DeployService (push) Successful in 22s
2025-04-02 07:59:33 +03:00
f4ea6b15a7 skeleton in edit page 2025-04-02 05:26:55 +03:00
fff9560f9c Merge branch 'main' of penahub.gitlab.yandexcloud.net:frontend/squiz 2025-04-02 01:32:45 +03:00
0cdc7797f8 новый порядок для анкеты в перетаскивании вопросов 2025-04-02 01:32:10 +03:00
5bba710b91 -- 2025-04-01 14:52:46 +03:00
277a8c3076 Merge branch 'staging' into 'main'
Staging

See merge request frontend/squiz!393
2025-03-31 23:44:42 +03:00
de8f66a837 автозакрытие модалки при загрузке видео 2025-03-31 21:13:19 +03:00
62a9237925 запрещено начинать диапазон с 0 2025-03-31 20:48:35 +03:00
b3537381b7 костыль для эмоджи 2025-03-31 20:19:33 +03:00
2f843b4c96 расширение лимитов инпутов 2025-03-31 19:28:55 +03:00
3d35ef033a запрещено слать системные ошибки на локалхост, урезканы картинки для шаблонов 2025-03-31 18:43:34 +03:00
99e394390c fix возможность добавить видео к вопросам 2025-03-31 16:20:50 +03:00
32e153518c Merge branch 'main' into dev 2025-03-31 16:07:15 +03:00
ff57344ee7 some edit parseerror crutch design theme 2025-03-31 15:50:55 +03:00
1ec8f34238 measure ram utilisation on staging hub
Some checks failed
Deploy / CreateImage (push) Failing after 1m14s
Deploy / DeployService (push) Has been skipped
2025-03-31 15:02:12 +03:00
28a1ee950e crutch design theme
Some checks failed
Deploy / CreateImage (push) Failing after 1m18s
Deploy / DeployService (push) Has been skipped
2025-03-28 02:05:42 +03:00
f5cf23fdef ci prod
All checks were successful
Deploy / CreateImage (push) Successful in 8m54s
Deploy / DeployService (push) Successful in 24s
2025-03-09 02:27:22 +03:00
05ffc45c18 -- 2025-02-19 21:11:59 +03:00
75c7fb55e7 update frontend package 2025-02-19 18:54:10 +03:00
fa1a907222 update kit 2025-01-24 13:04:25 +03:00
195 changed files with 35289 additions and 12440 deletions

@ -0,0 +1,26 @@
name: Deploy
run-name: ${{ gitea.actor }} build image and push to container registry
on:
push:
branches:
- 'main'
jobs:
CreateImage:
runs-on: [skeris]
uses: https://gitea.pena/PenaDevops/actions.git/.gitea/workflows/build-image.yml@v1.1.6-p
with:
runner: skeris
secrets:
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
DeployService:
runs-on: [frontprod]
needs: CreateImage
uses: https://gitea.pena/PenaDevops/actions.git/.gitea/workflows/deploy.yml@v1.1.4-p7
with:
runner: hubprod
actionid: ${{ gitea.run_id }}

@ -0,0 +1,26 @@
name: Deploy
run-name: ${{ gitea.actor }} build image and push to container registry
on:
push:
branches:
- 'staging'
jobs:
CreateImage:
runs-on: [skeris]
uses: http://gitea.pena/PenaDevops/actions.git/.gitea/workflows/build-image.yml@v1.1.6-p
with:
runner: skeris
secrets:
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
DeployService:
runs-on: [frontstaging]
needs: CreateImage
uses: http://gitea.pena/PenaDevops/actions.git/.gitea/workflows/deploy.yml@v1.1.4-p7
with:
runner: frontstaging
actionid: ${{ gitea.run_id }}

14
.gitea/workflows/lint.yml Normal file

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

@ -1,38 +0,0 @@
include:
- project: "devops/pena-continuous-integration"
file: "/templates/docker/build-template.gitlab-ci.yml"
- project: "devops/pena-continuous-integration"
file: "/templates/docker/deploy-template.gitlab-ci.yml"
- project: "devops/pena-continuous-integration"
file: "/templates/docker/service-discovery.gitlab-ci.yml"
stages:
- build
- deploy
- service-discovery
build-app:
extends: .build_template
tags:
- frontbuild
variables:
DOCKER_BUILD_PATH: "./Dockerfile"
STAGING_BRANCH: "staging"
PRODUCTION_BRANCH: "main"
deploy-to-staging:
extends: .deploy_template
rules:
- if: "$CI_COMMIT_BRANCH == $STAGING_BRANCH"
tags:
- front
- staging
deploy-to-prod:
extends: .deploy_template
rules:
- if: "$CI_COMMIT_BRANCH == $PRODUCTION_BRANCH"
tags:
- front
- prod
service-discovery:
extends: .sd_artefacts_template

1
.npmrc Normal file

@ -0,0 +1 @@
@frontend:registry=http://gitea.pena/api/packages/skeris/npm/

5
.vscode/extensions.json vendored Normal file

@ -0,0 +1,5 @@
{
"recommendations": [
"godrix.svgr-preview"
]
}

@ -1 +0,0 @@
"@frontend:registry" "https://penahub.gitlab.yandexcloud.net/api/v4/packages/npm/"

13
Containerfile Normal file

@ -0,0 +1,13 @@
FROM gitea.pena/penadevops/container-images/node:main as build
WORKDIR /usr/app
COPY . .
RUN npm install --force && yarn cache clean
RUN psstat.sh "npm run build"
FROM gitea.pena/penadevops/container-images/nginx:main as result
WORKDIR /usr/share/nginx/html
COPY --from=build /usr/app/build/ /usr/share/nginx/html
COPY hub.conf /etc/nginx/conf.d/default.conf

@ -1,19 +0,0 @@
FROM penahub.gitlab.yandexcloud.net:5050/devops/dockerhub-backup/node as build
RUN apk update && rm -rf /var/cache/apk/*
WORKDIR /usr/app
COPY . .
RUN npm config set -- //penahub.gitlab.yandexcloud.net/api/v4/packages/npm/:_authToken=glpat-JL_7wSM1QpW7xGfd-oWX
RUN npm config set -- //penahub.gitlab.yandexcloud.net/api/v4/projects/43/packages/npm/:_authToken=glpat-JL_7wSM1QpW7xGfd-oWX
RUN npm config set @frontend:registry https://penahub.gitlab.yandexcloud.net/api/v4/packages/npm/
RUN yarn config set '//penahub.gitlab.yandexcloud.net/api/v4/packages/npm/:_authToken' "glpat-JL_7wSM1QpW7xGfd-oWX"
RUN yarn config set '//penahub.gitlab.yandexcloud.net/api/v4/projects/:_authToken' "glpat-JL_7wSM1QpW7xGfd-oWX"
RUN yarn install --ignore-scripts --non-interactive && yarn cache clean
RUN yarn build
FROM penahub.gitlab.yandexcloud.net:5050/devops/dockerhub-backup/nginx as result
WORKDIR /usr/share/nginx/html
COPY --from=build /usr/app/build/ /usr/share/nginx/html
COPY hub.conf /etc/nginx/conf.d/default.conf

577
api-docs.html Normal file

@ -0,0 +1,577 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>QUIZ Service API Documentation</title>
<style>
:root {
--primary-color: #7E2AEA;
--text-color: #333;
--bg-color: #fff;
--border-color: #e0e0e0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
line-height: 1.6;
color: var(--text-color);
margin: 0;
padding: 0;
background: var(--bg-color);
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
header {
background: var(--primary-color);
color: white;
padding: 2rem 0;
margin-bottom: 2rem;
}
h1, h2, h3 {
color: var(--primary-color);
}
.endpoint {
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
background: #fafafa;
}
.method {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
color: white;
font-weight: bold;
margin-right: 10px;
}
.get { background: #61affe; }
.post { background: #49cc90; }
.put { background: #fca130; }
.delete { background: #f93e3e; }
.patch { background: #50e3c2; }
.schema {
background: #f8f9fa;
padding: 15px;
border-radius: 4px;
margin: 10px 0;
}
.nav {
position: sticky;
top: 0;
background: white;
padding: 1rem;
border-bottom: 1px solid var(--border-color);
}
.nav a {
color: var(--primary-color);
text-decoration: none;
margin-right: 20px;
}
.nav a:hover {
text-decoration: underline;
}
code {
background: #f1f1f1;
padding: 2px 4px;
border-radius: 4px;
font-family: 'Courier New', Courier, monospace;
}
.response {
margin-top: 10px;
padding: 10px;
background: #f8f9fa;
border-left: 4px solid var(--primary-color);
}
.components {
margin: 2rem 0;
padding: 1rem;
background: #f8f9fa;
border-radius: 8px;
}
.security {
background: #fff3cd;
padding: 1rem;
border-radius: 4px;
margin: 1rem 0;
}
.parameter {
margin: 0.5rem 0;
padding: 0.5rem;
background: #f8f9fa;
border-radius: 4px;
}
.parameter.required {
border-left: 4px solid #dc3545;
}
.enum-values {
color: #666;
font-style: italic;
}
</style>
</head>
<body>
<header>
<div class="container">
<h1>QUIZ Service API Documentation</h1>
<p>Version 1.0.0</p>
</div>
</header>
<nav class="nav">
<div class="container">
<a href="#components">Components</a>
<a href="#quiz">Quiz Endpoints</a>
<a href="#question">Question Endpoints</a>
<a href="#results">Results Endpoints</a>
<a href="#statistics">Statistics Endpoints</a>
<a href="#account">Account Endpoints</a>
</div>
</nav>
<main class="container">
<section id="components">
<h2>Components</h2>
<div class="components">
<h3>Quiz Model</h3>
<div class="schema">
<pre><code>{
"id": integer, // Id of created quiz
"qid": string, // string id for customers
"deleted": boolean, // true if quiz deleted
"archived": boolean, // true if quiz archived
"fingerprinting": boolean, // set true for save deviceId
"repeatable": boolean, // set true for allow user to repeat quiz
"note_prevented": boolean, // set true for save statistic of incomplete quiz passing
"mail_notifications": boolean, // set true for mail notification for each quiz passing
"unique_answers": boolean, // set true for save statistics only for unique quiz passing
"name": string, // name of quiz. max 280 length
"description": string, // description of quiz
"config": string, // config of quiz. serialized json for rules of quiz flow
"status": string, // status of quiz. allow only '', 'draft', 'template', 'stop', 'start'
"limit": integer, // limit is count of max quiz passing
"due_to": integer, // last time when quiz is valid. timestamp in seconds
"time_of_passing": integer, // seconds to pass quiz
"pausable": boolean, // true if it is allowed for pause quiz
"version": integer, // version of quiz
"version_comment": string, // version comment to version of quiz
"parent_ids": integer[], // array of previous versions of quiz
"created_at": string, // time of creating
"updated_at": string, // time of last updating
"question_cnt": integer, // count of questions
"passed_count": integer, // count passings
"average_time": integer, // average time of passing
"super": boolean, // set true if squiz realize group functionality
"group_id": integer // group of new quiz
}</code></pre>
</div>
</div>
<div class="components">
<h3>Question Model</h3>
<div class="schema">
<pre><code>{
"id": integer, // Id of created question
"quiz_id": integer, // relation to quiz
"title": string, // title of question. max 512 length
"description": string, // description of question
"type": string, // status of question. allow only text, select, file, variant, images, varimg, emoji, date, number, page, rating
"required": boolean, // user must pass this question
"deleted": boolean, // true if question is deleted
"page": integer, // page if question
"content": string, // serialized json of created question
"version": integer, // version of quiz
"parent_ids": integer[], // array of previous versions of quiz
"created_at": string, // time of creating
"updated_at": string // time of last updating
}</code></pre>
</div>
</div>
<div class="components">
<h3>Answer Model</h3>
<div class="schema">
<pre><code>{
"Id": integer, // id ответа
"Content": string, // контент ответа
"QuestionId": integer, // id вопроса к которому ответ
"QuizId": integer, // id опроса к которому ответ
"Fingerprint": string, // fingerprint
"Session": string, // сессия
"Result": boolean, // true or false?
"CreatedAt": string, // таймшап когда ответ создан
"New": boolean, // новый ответ?
"Deleted": boolean // удален?
}</code></pre>
</div>
</div>
<div class="components">
<h3>LeadTarget Model</h3>
<div class="schema">
<pre><code>{
"ID": integer, // primary key
"AccountID": string, // account identifier
"Type": string, // type of target (mail, telegram, whatsapp)
"QuizID": integer, // ID of the quiz
"Target": string, // target address
"InviteLink": string, // invitation link
"Deleted": boolean, // is deleted
"CreatedAt": string // creation timestamp
}</code></pre>
</div>
</div>
<div class="components">
<h3>TgAccount Model</h3>
<div class="schema">
<pre><code>{
"ID": integer, // primary key
"ApiID": integer, // Telegram API ID
"ApiHash": string, // Telegram API Hash
"PhoneNumber": string, // phone number
"Password": string, // account password
"Status": string, // account status (active, inactive, ban)
"Deleted": boolean, // is deleted
"CreatedAt": string // creation timestamp
}</code></pre>
</div>
</div>
</section>
<section id="quiz">
<h2>Quiz Endpoints</h2>
<div class="endpoint">
<h3>Create Quiz</h3>
<span class="method post">POST</span>
<code>/quiz/create</code>
<p>Create a new quiz with specified parameters.</p>
<div class="security">
<h4>Security</h4>
<p>This endpoint requires authentication.</p>
</div>
<h4>Request Body:</h4>
<div class="schema">
<pre><code>{
"fingerprinting": boolean, // set true for save deviceId
"repeatable": boolean, // set true for allow user to repeat quiz
"note_prevented": boolean, // set true for save statistic of incomplete quiz passing
"mail_notifications": boolean, // set true for mail notification for each quiz passing
"unique_answers": boolean, // set true for save statistics only for unique quiz passing
"name": string, // name of quiz. max 280 length
"description": string, // description of quiz
"config": string, // config of quiz. serialized json for rules of quiz flow
"status": string, // status of quiz. allow only '', 'draft', 'template', 'stop', 'start'
"limit": integer, // limit is count of max quiz passing
"due_to": integer, // last time when quiz is valid. timestamp in seconds
"time_of_passing": integer, // seconds to pass quiz
"pausable": boolean, // true if it is allowed for pause quiz
"question_cnt": integer, // count of questions
"super": boolean, // set true if squiz realize group functionality
"group_id": integer // group of new quiz
}</code></pre>
</div>
<h4>Responses:</h4>
<div class="response">
<h5>201 Created</h5>
<p>Quiz successfully created. Returns the created quiz object.</p>
<div class="schema">
<pre><code>{
"id": integer,
"qid": string,
"name": string,
"description": string,
// ... other quiz properties
}</code></pre>
</div>
</div>
<div class="response">
<h5>422 Unprocessable Entity</h5>
<p>Name field should have less than 280 characters.</p>
</div>
<div class="response">
<h5>406 Not Acceptable</h5>
<p>Status on creating must be only draft, template, stop, start or due to time must be lesser than now.</p>
<div class="enum-values">
Allowed status values: '', 'draft', 'template', 'stop', 'start'
</div>
</div>
<div class="response">
<h5>409 Conflict</h5>
<p>You can pause quiz only if it has deadline for passing.</p>
</div>
<div class="response">
<h5>500 Internal Server Error</h5>
<p>If you get any content string send it to developer.</p>
</div>
</div>
<div class="endpoint">
<h3>Get Quiz List</h3>
<span class="method post">POST</span>
<code>/quiz/getList</code>
<p>Get paginated list of quizzes with filtering options.</p>
<h4>Request Body:</h4>
<div class="schema">
<pre><code>{
"limit": integer,
"offset": integer,
"from": integer,
"to": integer,
"search": string,
"status": string,
"deleted": boolean,
"archived": boolean,
"super": boolean,
"group_id": integer
}</code></pre>
</div>
<h4>Responses:</h4>
<div class="response">
<h5>200 OK</h5>
<p>Returns list of quizzes with total count.</p>
<div class="schema">
<pre><code>{
"count": integer,
"items": [
{
"id": integer,
"qid": string,
// ... other quiz properties
}
]
}</code></pre>
</div>
</div>
<div class="response">
<h5>406 Not Acceptable</h5>
<p>Inappropriate status, allowed only '', 'stop', 'start', 'draft', 'template', 'timeout', 'offlimit'.</p>
</div>
<div class="response">
<h5>500 Internal Server Error</h5>
<p>If you get any content string send it to developer.</p>
</div>
</div>
<!-- Add more quiz endpoints -->
</section>
<section id="question">
<h2>Question Endpoints</h2>
<div class="endpoint">
<h3>Create Question</h3>
<span class="method post">POST</span>
<code>/question/create</code>
<p>Create a new question for a quiz.</p>
</div>
<!-- Add more question endpoints -->
</section>
<section id="results">
<h2>Results Endpoints</h2>
<div class="endpoint">
<h3>Get Quiz Results</h3>
<span class="method post">POST</span>
<code>/results/getResults/{quizId}</code>
<p>Get list of quiz results with pagination.</p>
<h4>Path Parameters:</h4>
<div class="parameter">
<code>quizId</code> - ID of the quiz to get results for
</div>
<h4>Request Body:</h4>
<div class="schema">
<pre><code>{
"to": integer, // таймштамп времени, до которого выбирать статистику. если 0 или не передано - этого ограничения нет
"from": integer, // таймштамп времени, после которого выбирать статистику. если 0 или не передано - этого ограничения нет
"new": boolean, // флаг, по которому вернутся только новые результаты, ещё не просмотренные пользователем
"page": integer, // номер страницы для пагинации
"limit": integer // размер страницы для пагинации
}</code></pre>
</div>
<h4>Responses:</h4>
<div class="response">
<h5>200 OK</h5>
<p>Returns paginated list of results.</p>
<div class="schema">
<pre><code>{
"total_count": integer, // общее количество элементов удволетворяющее фильтру
"results": [
{
"content": string, // содержимое ответа
"id": integer, // айдишник ответа
"new": boolean, // статус, был ли просмотрен ответ
"created_at": string // время создания этого результата
}
]
}</code></pre>
</div>
</div>
</div>
<div class="endpoint">
<h3>Export Results</h3>
<span class="method post">POST</span>
<code>/results/{quizID}/export</code>
<p>Export quiz results to CSV format.</p>
<h4>Path Parameters:</h4>
<div class="parameter required">
<code>quizID</code> - ID of the quiz to export results from
</div>
<h4>Request Body:</h4>
<div class="schema">
<pre><code>{
"to": string, // Дата окончания диапазона времени для экспорта результатов
"from": string, // Дата начала временного диапазона для экспорта результатов
"new": boolean // Экспортировать ли только новые результаты?
}</code></pre>
</div>
<h4>Responses:</h4>
<div class="response">
<h5>200 OK</h5>
<p>Returns CSV file with quiz results.</p>
<div class="schema">
<pre><code>Content-Type: text/csv</code></pre>
</div>
</div>
</div>
</section>
<section id="telegram">
<h2>Telegram Endpoints</h2>
<div class="endpoint">
<h3>Create Telegram Account</h3>
<span class="method post">POST</span>
<code>/telegram/create</code>
<p>Authorize server in Telegram account.</p>
<h4>Request Body:</h4>
<div class="schema">
<pre><code>{
"ApiID": integer, // Telegram API ID
"ApiHash": string, // Telegram API Hash
"PhoneNumber": string, // Phone number
"Password": string // Account password
}</code></pre>
</div>
<h4>Responses:</h4>
<div class="response">
<h5>200 OK</h5>
<p>Returns signature for code verification.</p>
<div class="schema">
<pre><code>{
"signature": string // Session identifier for code verification
}</code></pre>
</div>
</div>
<div class="response">
<h5>409 Conflict</h5>
<p>Account already exists and is active.</p>
</div>
</div>
</section>
<section id="audience">
<h2>Audience Endpoints</h2>
<div class="endpoint">
<h3>Create Quiz Audience</h3>
<span class="method post">POST</span>
<code>/quiz/{quizID}/auditory</code>
<p>Create audience for a quiz.</p>
<h4>Path Parameters:</h4>
<div class="parameter required">
<code>quizID</code> - ID of the quiz
</div>
<h4>Responses:</h4>
<div class="response">
<h5>200 OK</h5>
<p>Returns ID of created audience.</p>
<div class="schema">
<pre><code>{
"id": integer // ID of created auditory
}</code></pre>
</div>
</div>
</div>
</section>
<section id="statistics">
<h2>Statistics Endpoints</h2>
<div class="endpoint">
<h3>Get Question Statistics</h3>
<span class="method post">POST</span>
<code>/statistic/{quizID}/questions</code>
<p>Get statistics for specific questions in a quiz.</p>
</div>
<!-- Add more statistics endpoints -->
</section>
<section id="account">
<h2>Account Endpoints</h2>
<div class="endpoint">
<h3>Add Lead Target</h3>
<span class="method post">POST</span>
<code>/account/leadtarget</code>
<p>Add a target destination for lead notifications.</p>
</div>
<!-- Add more account endpoints -->
</section>
</main>
<footer class="container">
<p>© 2024 QUIZ Service API Documentation</p>
</footer>
</body>
</html>

@ -3,8 +3,23 @@ import { defineConfig } from "cypress";
export default defineConfig({ export default defineConfig({
e2e: { e2e: {
baseUrl: 'http://localhost:3000', baseUrl: 'http://localhost:3000',
viewportWidth: 1440, viewportWidth: 1280,
viewportHeight: 900, viewportHeight: 720,
supportFile: false, video: true,
screenshotOnRunFailure: true,
supportFile: 'cypress/support/e2e.ts',
defaultCommandTimeout: 10000,
pageLoadTimeout: 30000,
requestTimeout: 10000,
responseTimeout: 30000,
setupNodeEvents(on, config) {
// implement node event listeners here
},
},
component: {
devServer: {
framework: 'react',
bundler: 'vite',
},
}, },
}); });

@ -0,0 +1,75 @@
/// <reference types="cypress" />
describe('Personalization Flow', () => {
beforeEach(() => {
// Логинимся перед каждым тестом
cy.login();
});
it('should complete personalization flow and open link in new tab', () => {
// Ищем нужный квиз и нажимаем редактировать
cy.contains('Сочетание перестановки и размещения')
.parent()
.parent()
.contains('Редактировать')
.click();
// Переходим на вкладку персонализации
cy.contains('Персонализация').click();
// Ждем загрузки данных
cy.get('.MuiSkeleton-root', { timeout: 30000 }).should('not.exist');
cy.wait(6000);
// Удаляем все существующие ссылки
cy.get('body').then(($body) => {
if ($body.find('.delete_aud').length > 0) {
// Пока есть кнопки удаления - удаляем ссылки
const deleteLinks = () => {
// Находим первую кнопку удаления и кликаем по ней
cy.get('.delete_aud').first().click();
// Подтверждаем удаление
cy.get('#delete_OK').click();
// Проверяем, остались ли еще кнопки удаления
cy.get('body').then(($body) => {
if ($body.find('.delete_aud').length > 0) {
deleteLinks();
}
});
};
deleteLinks();
}
});
// Выбираем пол (М)
cy.contains('М').click();
// Генерируем случайный возраст от 1 до 99
const randomAge = Math.floor(Math.random() * 99) + 1;
// Вводим возраст
cy.get('input[placeholder="Введите возраст"]')
.type(randomAge.toString())
.should('have.value', randomAge.toString());
// Нажимаем кнопку Ок
cy.contains('Ок').click();
// Ждем появления ссылки и получаем её текст
cy.get('.link', { timeout: 30000 })
.should('be.visible')
.invoke('text')
.then((text) => {
// Исправляем домен в ссылке
const url = new URL(text);
url.hostname = 's.hbpn.link';
const correctUrl = url.toString();
// Переходим на страницу по исправленной ссылке
cy.visit(correctUrl);
// Проверяем содержимое страницы
cy.contains('Сочетание перестановки и размещения').should('exist');
});
});
});

@ -0,0 +1,28 @@
/// <reference types="cypress" />
declare global {
namespace Cypress {
interface Chainable {
login(): Chainable<void>
}
}
}
Cypress.Commands.add('login', () => {
// Пробуем перейти на страницу входа
cy.visit('/signin', {
timeout: 10000, // Увеличиваем таймаут до 10 секунд
failOnStatusCode: false // Не падаем при ошибках статуса
});
// Проверяем, что мы на странице входа
cy.url().should('include', '/signin');
// Вводим данные для входа
cy.get('#email', { timeout: 10000 }).should('be.visible').type('test@test.ru');
cy.get('#password', { timeout: 10000 }).should('be.visible').type('testtest');
cy.get('button[type="submit"]', { timeout: 10000 }).should('be.visible').click();
// Ждем успешного входа
cy.url().should('not.include', '/signin', { timeout: 10000 });
});

13
cypress/support/e2e.ts Normal file

@ -0,0 +1,13 @@
// Import commands.js using ES2015 syntax:
import './commands';
// Alternatively you can use CommonJS syntax:
// require('./commands')
declare global {
namespace Cypress {
interface Chainable {
login(): Chainable<void>
}
}
}

@ -1,8 +1,7 @@
version: "3"
services: services:
squiz: squiz:
container_name: squiz container_name: squiz
restart: unless-stopped restart: unless-stopped
image: $CI_REGISTRY_IMAGE/main:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID image: gitea.pena/squiz/frontpanel/main:$GITHUB_RUN_NUMBER
hostname: squiz hostname: squiz
tty: true tty: true

@ -1,15 +1,7 @@
version: "3"
services: services:
squiz: squiz:
container_name: squiz container_name: squiz
restart: unless-stopped restart: unless-stopped
image: $CI_REGISTRY_IMAGE/staging:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID image: gitea.pena/squiz/frontpanel/staging:$GITHUB_RUN_NUMBER
networks:
- marketplace_penahub_frontend
labels:
com.pena.domains: squiz.pena.digital
hostname: squiz hostname: squiz
tty: true tty: true
networks:
marketplace_penahub_frontend:
external: true

15
jest.config.js Normal file

@ -0,0 +1,15 @@
module.exports = {
transformIgnorePatterns: [
'/node_modules/(?!(@frontend/kitui|@frontend/squzanswerer)/)'
],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
'^@utils/(.*)$': '<rootDir>/src/utils/$1',
'^@assets/(.*)$': '<rootDir>/src/assets/$1'
},
setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
testEnvironment: 'jsdom',
transform: {
'^.+\\.(ts|tsx)$': 'ts-jest'
}
};

20860
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

@ -6,7 +6,7 @@
"@craco/craco": "^7.0.0", "@craco/craco": "^7.0.0",
"@emotion/react": "^11.10.5", "@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5", "@emotion/styled": "^11.10.5",
"@frontend/kitui": "^1.0.86", "@frontend/kitui": "^1.0.108",
"@frontend/squzanswerer": "^1.0.57", "@frontend/squzanswerer": "^1.0.57",
"@mui/icons-material": "^5.10.14", "@mui/icons-material": "^5.10.14",
"@mui/material": "^5.10.14", "@mui/material": "^5.10.14",
@ -25,7 +25,6 @@
"@types/react-slick": "^0.23.13", "@types/react-slick": "^0.23.13",
"axios": "^1.5.1", "axios": "^1.5.1",
"country-flag-emoji-polyfill": "^0.1.8", "country-flag-emoji-polyfill": "^0.1.8",
"cypress-file-upload": "^5.0.8",
"cytoscape": "^3.26.0", "cytoscape": "^3.26.0",
"cytoscape-popper": "^2.0.0", "cytoscape-popper": "^2.0.0",
"date-fns": "^3.0.6", "date-fns": "^3.0.6",
@ -69,9 +68,10 @@
"build": "craco build", "build": "craco build",
"test": "craco test", "test": "craco test",
"eject": "craco eject", "eject": "craco eject",
"cypress:open": "cypress open",
"code:format": "prettier --write --ignore-unknown", "code:format": "prettier --write --ignore-unknown",
"prepare": "husky install" "prepare": "husky install",
"cypress:open": "cypress open",
"cypress:run": "cypress run"
}, },
"browserslist": { "browserslist": {
"production": [ "production": [
@ -86,13 +86,12 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"@emoji-mart/data": "^1.1.2", "@emoji-mart/data": "^1.2.1",
"@emoji-mart/react": "^1.1.1", "@emoji-mart/react": "^1.1.1",
"@types/cytoscape-popper": "^2.0.4", "@types/cytoscape-popper": "^2.0.4",
"@types/react-beautiful-dnd": "^13.1.4", "@types/react-beautiful-dnd": "^13.1.4",
"@types/react-cytoscapejs": "^1.2.4", "@types/react-cytoscapejs": "^1.2.4",
"craco-alias": "^3.0.1", "craco-alias": "^3.0.1",
"cypress": "^13.6.1",
"husky": "^8.0.3", "husky": "^8.0.3",
"lint-staged": "^15.2.0", "lint-staged": "^15.2.0",
"prettier": "^3.1.1" "prettier": "^3.1.1"

@ -35,6 +35,7 @@ const { DesignPage } = lazily(() => import("./pages/DesignPage/DesignPage"));
const { IntegrationsPage } = lazily(() => import("./pages/IntegrationsPage/IntegrationsPage")); const { IntegrationsPage } = lazily(() => import("./pages/IntegrationsPage/IntegrationsPage"));
const { QuizAnswersPage } = lazily(() => import("./pages/QuizAnswersPage/QuizAnswersPage")); const { QuizAnswersPage } = lazily(() => import("./pages/QuizAnswersPage/QuizAnswersPage"));
const ChatImageNewWindow = lazy(() => import("@ui_kit/FloatingSupportChat/ChatImageNewWindow")); const ChatImageNewWindow = lazy(() => import("@ui_kit/FloatingSupportChat/ChatImageNewWindow"));
const PersonalizationAI = lazy(() => import("./pages/PersonalizationAI/PersonalizationAI"));
let params = new URLSearchParams(document.location.search); let params = new URLSearchParams(document.location.search);
const isTest = Boolean(params.get("test")) const isTest = Boolean(params.get("test"))
@ -60,6 +61,12 @@ const routeslink = [
sidebar: true, sidebar: true,
footer: true, footer: true,
}, },
{
path: "/personalization-ai",
page: PersonalizationAI,
header: true,
sidebar: true,
},
] as const; ] as const;
const LazyLoading = ({ children, fallback }: SuspenseProps) => ( const LazyLoading = ({ children, fallback }: SuspenseProps) => (

108
src/api/auditory.ts Normal file

@ -0,0 +1,108 @@
import { makeRequest } from "@frontend/kitui";
import { parseAxiosError } from "@utils/parse-error";
const API_URL = `${process.env.REACT_APP_DOMAIN}/squiz`;
// Types
export interface AuditoryItem {
id: number;
quiz_id: number;
sex: number; // 0 - женский, 1 - мужской, 2 - оба
age: string;
deleted: boolean;
created_at: number;
}
export interface AuditoryResponse {
ID: number;
quiz_id: number;
sex: number;
age: string;
deleted: boolean;
created_at: number;
}
// Request Types
export interface AuditoryGetRequest {
quizId: number;
}
export interface AuditoryDeleteRequest {
id: number;
}
export interface AuditoryAddRequest {
sex: number;
age: string;
}
// Parameters
export interface AuditoryGetParams {
quizId: number;
}
export interface AuditoryDeleteParams {
quizId: number;
auditoryId: number;
}
export interface AuditoryAddParams {
quizId: number;
body: AuditoryAddRequest;
}
// API calls
export const auditoryGet = async ({ quizId }: AuditoryGetParams): Promise<[AuditoryItem[] | null, string?]> => {
if (!quizId) {
return [null, "Quiz ID is required"];
}
try {
const response = await makeRequest<AuditoryGetRequest, AuditoryItem[]>({
url: `${API_URL}/quiz/${quizId}/auditory`,
method: "GET",
});
return [response];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось получить аудиторию. ${error}`];
}
};
export const auditoryDelete = async ({ quizId, auditoryId }: AuditoryDeleteParams): Promise<[AuditoryResponse | null, string?]> => {
if (!quizId || !auditoryId) {
return [null, "Quiz ID and Auditory ID are required"];
}
try {
const response = await makeRequest<AuditoryDeleteRequest, AuditoryResponse>({
url: `${API_URL}/quiz/${quizId}/auditory/${auditoryId}`,
method: "DELETE",
});
return [response];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось удалить аудиторию. ${error}`];
}
};
export const auditoryAdd = async ({ quizId, body }: AuditoryAddParams): Promise<[AuditoryResponse | null, string?]> => {
if (!quizId) {
return [null, "Quiz ID is required"];
}
try {
const response = await makeRequest<AuditoryAddRequest, AuditoryResponse>({
url: `${API_URL}/quiz/${quizId}/auditory`,
body,
method: "POST",
});
return [response];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось добавить аудиторию. ${error}`];
}
};

@ -1,5 +1,6 @@
import { QuestionKeys } from "@/pages/IntegrationsPage/IntegrationsModal/Amo/types"; import { QuestionKeys } from "@/pages/IntegrationsPage/IntegrationsModal/Amo/types";
import { makeRequest } from "@api/makeRequest"; import { makeRequest } from "@api/makeRequest";
import { useToken } from "@frontend/kitui";
import { parseAxiosError } from "@utils/parse-error"; import { parseAxiosError } from "@utils/parse-error";
import useSWR from "swr"; import useSWR from "swr";
@ -41,7 +42,9 @@ export const getAccount = async (): Promise<[AccountResponse | null, string?]> =
}; };
export function useAmoAccount() { export function useAmoAccount() {
return useSWR("amoAccount", () => const token = useToken();
return useSWR(token ? "amoAccount" : null, () =>
makeRequest<void, AccountResponse>({ makeRequest<void, AccountResponse>({
method: "GET", method: "GET",
url: `${API_URL}/account`, url: `${API_URL}/account`,
@ -363,9 +366,9 @@ export const removeAmoAccount = async (): Promise<[void | null, string?]> => {
}; };
export const getFields = async ( pagination: PaginationRequest ): Promise<[FieldsResponse | null, string?]> => { export const getFields = async (pagination: PaginationRequest): Promise<[FieldsResponse | null, string?]> => {
try { try {
const fieldsResponse = await makeRequest<PaginationRequest, FieldsResponse>({ const fieldsResponse = await makeRequest<PaginationRequest, FieldsResponse>({
method: "GET", method: "GET",
url: `${API_URL}/fields?page=${pagination.page}&size=${pagination.size}`, url: `${API_URL}/fields?page=${pagination.page}&size=${pagination.size}`,
}); });

@ -33,11 +33,11 @@ export const makeRequest = async <TRequest = unknown, TResponse = unknown>(
} catch (nativeError) { } catch (nativeError) {
const error = nativeError as AxiosError; const error = nativeError as AxiosError;
selectSendingMethod({ // if (window.location.hostname !== 'localhost') selectSendingMethod({
messageField: `status: ${error.response?.status}. Message ${(error.response?.data as ExtendedAxiosResponse)?.message}`, // messageField: `status: ${error.response?.status}. Message ${(error.response?.data as ExtendedAxiosResponse)?.message}`,
isSnackbar: false, // isSnackbar: false,
systemError: true // systemError: true
}); // });
if ( if (
error.response?.status === 400 && error.response?.status === 400 &&
(error.response?.data as ExtendedAxiosResponse)?.message === (error.response?.data as ExtendedAxiosResponse)?.message ===

@ -148,8 +148,8 @@ export const addQuizImages = async (
const name = image?.name ? transliterate(image?.name.replace(/\s/g, '_')) : "blob" const name = image?.name ? transliterate(image?.name.replace(/\s/g, '_')) : "blob"
//Замена всех побелов на _ //Замена всех побелов на _
const renamedImage = new File([image], name) const renamedImage = new File([image], name)
formData.append("quiz", quizId.toString()); formData.append("quiz", quizId.toString());
formData.append("image", renamedImage); formData.append("image", renamedImage);

@ -1,13 +1,11 @@
import { makeRequest } from "@api/makeRequest"; import { makeRequest } from "@api/makeRequest";
import { parseAxiosError } from "@utils/parse-error"; import { parseAxiosError } from "@utils/parse-error";
import type { GetTariffsResponse } from "@frontend/kitui"; import type { GetTariffsResponse } from "@frontend/kitui";
const API_URL = `${process.env.REACT_APP_DOMAIN}/strator/tariff`; const API_URL = `${process.env.REACT_APP_DOMAIN}/strator/tariff`;
export const getTariffs = async ( export const getTariffs = async (
page: number, page: number = 1,
): Promise<[GetTariffsResponse | null, string?]> => { ): Promise<[GetTariffsResponse | null, string?]> => {
try { try {
const tariffs = await makeRequest<never, GetTariffsResponse>({ const tariffs = await makeRequest<never, GetTariffsResponse>({
@ -17,7 +15,6 @@ export const getTariffs = async (
return [tariffs]; return [tariffs];
} catch (nativeError) { } catch (nativeError) {
const [error] = parseAxiosError(nativeError); const [error] = parseAxiosError(nativeError);
return [null, `Ошибка при получении списка тарифов. ${error}`]; return [null, `Ошибка при получении списка тарифов. ${error}`];
} }
}; };

9
src/api/tariffs.ts Normal file

@ -0,0 +1,9 @@
import { makeRequest } from '@utils/makeRequest';
import type { GetTariffsResponse } from '@/model/tariff';
export const getTariffs = async (): Promise<[GetTariffsResponse | null, string?]> => {
return makeRequest<GetTariffsResponse>({
url: `${process.env.REACT_APP_DOMAIN}/tariffs`,
method: 'GET'
});
};

@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 15L19.6668 16.2577C20.1354 17.1416 20.8584 17.8646 21.7423 18.3332L23 19L21.7423 19.6668C20.8584 20.1354 20.1354 20.8584 19.6668 21.7423L19 23L18.3332 21.7423C17.8646 20.8584 17.1416 20.1354 16.2577 19.6668L15 19L16.2577 18.3332C17.1416 17.8646 17.8646 17.1416 18.3332 16.2577L19 15Z" stroke="#7E2AEA" stroke-width="1.5" stroke-linejoin="round"/>
<path d="M20 11V7C20 4.23858 17.7614 2 15 2H7C4.23858 2 2 4.23858 2 7V15C2 17.7614 4.23858 20 7 20H11" stroke="#7E2AEA" stroke-width="1.5" stroke-linecap="round"/>
<path d="M7.5 14.5V9.25C7.5 8.78587 7.68437 8.34075 8.01256 8.01256C8.34075 7.68437 8.78587 7.5 9.25 7.5C9.71413 7.5 10.1592 7.68437 10.4874 8.01256C10.8156 8.34075 11 8.78587 11 9.25V14.5M7.5 11.875H11M14.5 7.5V14.5" stroke="#7E2AEA" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 930 B

@ -0,0 +1,12 @@
import * as React from "react";
import { SvgIcon, SvgIconProps } from "@mui/material";
const AiPersonalizationIcon = (props: SvgIconProps) => (
<SvgIcon {...props} viewBox="0 0 24 24" fill="none">
<path d="M19 15L19.6668 16.2577C20.1354 17.1416 20.8584 17.8646 21.7423 18.3332L23 19L21.7423 19.6668C20.8584 20.1354 20.1354 20.8584 19.6668 21.7423L19 23L18.3332 21.7423C17.8646 20.8584 17.1416 20.1354 16.2577 19.6668L15 19L16.2577 18.3332C17.1416 17.8646 17.8646 17.1416 18.3332 16.2577L19 15Z" stroke="#7E2AEA" strokeWidth="1.5" strokeLinejoin="round"/>
<path d="M20 11V7C20 4.23858 17.7614 2 15 2H7C4.23858 2 2 4.23858 2 7V15C2 17.7614 4.23858 20 7 20H11" stroke="#7E2AEA" strokeWidth="1.5" strokeLinecap="round"/>
<path d="M7.5 14.5V9.25C7.5 8.78587 7.68437 8.34075 8.01256 8.01256C8.34075 7.68437 8.78587 7.5 9.25 7.5C9.71413 7.5 10.1592 7.68437 10.4874 8.01256C10.8156 8.34075 11 8.78587 11 9.25V14.5M7.5 11.875H11M14.5 7.5V14.5" stroke="#7E2AEA" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
</SvgIcon>
);
export default AiPersonalizationIcon;

@ -0,0 +1,18 @@
import { SvgIcon, SxProps } from "@mui/material";
export default function SmallAddPluse({ sx }: { sx: SxProps }) {
return (
<SvgIcon
sx={{
width: "11px",
height: "11px",
...sx
}}
>
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.518 9.612C4.398 9.612 4.296 9.576 4.212 9.504C4.14 9.42 4.104 9.318 4.104 9.198V5.454H0.414C0.294 5.454 0.192 5.418 0.108 5.346C0.036 5.262 0 5.16 0 5.04V4.464C0 4.344 0.036 4.248 0.108 4.176C0.192 4.092 0.294 4.05 0.414 4.05H4.104V0.414C4.104 0.294 4.14 0.198 4.212 0.126C4.296 0.0420001 4.398 0 4.518 0H5.148C5.268 0 5.364 0.0420001 5.436 0.126C5.52 0.198 5.562 0.294 5.562 0.414V4.05H9.27C9.39 4.05 9.486 4.092 9.558 4.176C9.642 4.248 9.684 4.344 9.684 4.464V5.04C9.684 5.16 9.642 5.262 9.558 5.346C9.486 5.418 9.39 5.454 9.27 5.454H5.562V9.198C5.562 9.318 5.52 9.42 5.436 9.504C5.364 9.576 5.268 9.612 5.148 9.612H4.518Z" fill="white" />
</svg>
</SvgIcon>
);
}

@ -1,6 +1,6 @@
import { Box } from "@mui/material"; import { Box, type SxProps} from "@mui/material";
export default function Plus() { export default function Plus(sx:SxProps) {
return ( return (
<Box <Box
sx={{ sx={{
@ -9,6 +9,7 @@ export default function Plus() {
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
...sx
}} }}
> >
<svg <svg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 861 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 514 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 642 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 667 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 600 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 618 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 264 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 600 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 645 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 847 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 746 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 431 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 752 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 424 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 584 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 205 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 517 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 209 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 719 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 870 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 516 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 522 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 310 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 572 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 758 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1013 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 436 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 571 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 669 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 425 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 865 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 378 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 568 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 550 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 512 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 410 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 351 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 658 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 242 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 297 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 698 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 356 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 262 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 788 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 533 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 656 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 352 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 972 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 488 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 592 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 637 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 771 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 447 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 518 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 580 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 826 KiB

After

Width:  |  Height:  |  Size: 18 KiB

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