Merge branch 'staging'
This commit is contained in:
commit
89b55d4d3e
@ -17,7 +17,7 @@ jobs:
|
|||||||
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||||
DeployService:
|
DeployService:
|
||||||
runs-on: [frontprod]
|
runs-on: [frontprod]
|
||||||
needs: CreateImage
|
# needs: CreateImage
|
||||||
uses: https://gitea.pena/PenaDevops/actions.git/.gitea/workflows/deploy.yml@v1.1.4-p7
|
uses: https://gitea.pena/PenaDevops/actions.git/.gitea/workflows/deploy.yml@v1.1.4-p7
|
||||||
with:
|
with:
|
||||||
runner: hubprod
|
runner: hubprod
|
||||||
|
@ -8,10 +8,10 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
CreateImage:
|
CreateImage:
|
||||||
runs-on: [hubstaging]
|
runs-on: [skeris]
|
||||||
uses: http://gitea.pena/PenaDevops/actions.git/.gitea/workflows/build-image.yml@v1.1.6-p
|
uses: http://gitea.pena/PenaDevops/actions.git/.gitea/workflows/build-image.yml@v1.1.6-p
|
||||||
with:
|
with:
|
||||||
runner: hubstaging
|
runner: skeris
|
||||||
secrets:
|
secrets:
|
||||||
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
||||||
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
FROM gitea.pena/penadevops/container-images/node:v20.14.0 as build
|
FROM gitea.pena/penadevops/container-images/node:main as build
|
||||||
|
|
||||||
WORKDIR /usr/app
|
WORKDIR /usr/app
|
||||||
COPY . .
|
COPY . .
|
||||||
|
577
api-docs.html
Normal file
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',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
75
cypress/e2e/personalization.cy.ts
Normal file
75
cypress/e2e/personalization.cy.ts
Normal file
@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
28
cypress/support/commands.ts
Normal file
28
cypress/support/commands.ts
Normal file
@ -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
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>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,6 @@ services:
|
|||||||
squiz:
|
squiz:
|
||||||
container_name: squiz
|
container_name: squiz
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
image: gitea.pena/squiz/frontpanel/main:$GITHUB_RUN_NUMBER
|
image: gitea.pena/squiz/frontpanel/main:1018
|
||||||
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
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'
|
||||||
|
}
|
||||||
|
};
|
1121
package-lock.json
generated
1121
package-lock.json
generated
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.88",
|
"@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",
|
||||||
@ -69,7 +69,9 @@
|
|||||||
"test": "craco test",
|
"test": "craco test",
|
||||||
"eject": "craco eject",
|
"eject": "craco eject",
|
||||||
"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": [
|
||||||
|
@ -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
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}`];
|
||||||
|
}
|
||||||
|
};
|
@ -148,7 +148,7 @@ 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());
|
||||||
|
@ -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
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'
|
||||||
|
});
|
||||||
|
};
|
5
src/assets/icons/AiPersonalizationIcon.svg
Normal file
5
src/assets/icons/AiPersonalizationIcon.svg
Normal file
@ -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 |
12
src/assets/icons/AiPersonalizationIcon.tsx
Normal file
12
src/assets/icons/AiPersonalizationIcon.tsx
Normal file
@ -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;
|
@ -83,11 +83,11 @@ const GeneralItem = ({
|
|||||||
xAxis={[
|
xAxis={[
|
||||||
{
|
{
|
||||||
data: statiscticsResult ? days : Object.keys(general),
|
data: statiscticsResult ? days : Object.keys(general),
|
||||||
valueFormatter: (value) =>
|
valueFormatter: (value) => {
|
||||||
moment.unix(Number(value)).format("DD/MM/YYYY HH") +
|
const timestamp = Number(value);
|
||||||
statiscticsResult
|
if (isNaN(timestamp)) return 'Invalid Date';
|
||||||
? ""
|
return moment.unix(timestamp).format(statiscticsResult ? "DD/MM/YYYY" : "DD/MM/YYYY HH") + (statiscticsResult ? "" : "ч");
|
||||||
: "ч",
|
},
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
series={[
|
series={[
|
||||||
|
@ -3,8 +3,10 @@ import { decrementCurrentStep } from "@root/quizes/actions";
|
|||||||
import ArrowLeft from "@/assets/icons/questionsPage/arrowLeft";
|
import ArrowLeft from "@/assets/icons/questionsPage/arrowLeft";
|
||||||
import QuizInstallationCard from "./QuizInstallationCard/QuizInstallationCard";
|
import QuizInstallationCard from "./QuizInstallationCard/QuizInstallationCard";
|
||||||
import QuizLinkCard from "./QuizLinkCard";
|
import QuizLinkCard from "./QuizLinkCard";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
export default function InstallQuiz() {
|
export default function InstallQuiz() {
|
||||||
|
const navigate = useNavigate();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box
|
<Box
|
||||||
@ -33,6 +35,11 @@ export default function InstallQuiz() {
|
|||||||
>
|
>
|
||||||
<ArrowLeft />
|
<ArrowLeft />
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
sx={{ padding: "10px 20px", borderRadius: "8px" }}
|
||||||
|
onClick={() => navigate("/list")}
|
||||||
|
>На главную</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
|
181
src/pages/PersonalizationAI/AgeInputWithSelect.tsx
Normal file
181
src/pages/PersonalizationAI/AgeInputWithSelect.tsx
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
import React, { useState, useRef } from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
InputBase,
|
||||||
|
IconButton,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
Paper,
|
||||||
|
Popper,
|
||||||
|
Grow,
|
||||||
|
ClickAwayListener,
|
||||||
|
MenuList,
|
||||||
|
useTheme,
|
||||||
|
FormHelperText
|
||||||
|
} from '@mui/material';
|
||||||
|
import ArrowDownIcon from "@/assets/icons/ArrowDownIcon";
|
||||||
|
|
||||||
|
interface AgeInputWithSelectProps {
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
onErrorChange?: (isError: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AgeInputWithSelect = ({ value, onChange, onErrorChange }: AgeInputWithSelectProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const anchorRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [errorType, setErrorType] = useState<'format' | 'range' | false>(false);
|
||||||
|
|
||||||
|
// Валидация: только число или диапазон число-число, и диапазон 0-150
|
||||||
|
const validate = (val: string) => {
|
||||||
|
if (!val) return false;
|
||||||
|
// Число (только положительное)
|
||||||
|
if (/^-?\d+$/.test(val)) {
|
||||||
|
const num = Number(val);
|
||||||
|
if (num < 0 || num > 150) return 'range';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Диапазон (только положительные числа)
|
||||||
|
const rangeMatch = val.match(/^(-?\d+)-(-?\d+)$/);
|
||||||
|
if (rangeMatch) {
|
||||||
|
const left = Number(rangeMatch[1]);
|
||||||
|
const right = Number(rangeMatch[2]);
|
||||||
|
if (left < 0 || left > 150 || right < 0 || right > 150 || left > right) return 'range';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return 'format';
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const filtered = e.target.value.replace(/[^\d-]/g, '');
|
||||||
|
onChange(filtered);
|
||||||
|
const err = validate(filtered);
|
||||||
|
setErrorType(err);
|
||||||
|
if (onErrorChange) onErrorChange(!!err);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputBlur = (e: React.FocusEvent<HTMLInputElement>) => {
|
||||||
|
const trimmed = e.target.value.replace(/\s+/g, '');
|
||||||
|
onChange(trimmed);
|
||||||
|
const err = validate(trimmed);
|
||||||
|
setErrorType(err);
|
||||||
|
if (onErrorChange) onErrorChange(!!err);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectItemClick = (selectedValue: string) => {
|
||||||
|
onChange(selectedValue);
|
||||||
|
setErrorType(false);
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggle = () => {
|
||||||
|
setOpen((prevOpen) => !prevOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = (event: Event) => {
|
||||||
|
if (anchorRef.current && anchorRef.current.contains(event.target as HTMLElement)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
ref={anchorRef}
|
||||||
|
sx={{
|
||||||
|
position: 'relative',
|
||||||
|
mt: "17px",
|
||||||
|
height: "48px",
|
||||||
|
maxWidth: "420px",
|
||||||
|
width: "100%",
|
||||||
|
borderRadius: "8px",
|
||||||
|
border: "1px solid #9A9AAF",
|
||||||
|
'&:hover': {
|
||||||
|
borderColor: '#B0B0B0',
|
||||||
|
},
|
||||||
|
'&:focus-within': {
|
||||||
|
borderColor: '#7E2AEA',
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<InputBase
|
||||||
|
value={value}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
onBlur={handleInputBlur}
|
||||||
|
fullWidth
|
||||||
|
placeholder="Введите возраст"
|
||||||
|
inputProps={{ inputMode: 'numeric', pattern: '[0-9-]*' }}
|
||||||
|
sx={{
|
||||||
|
height: "100%",
|
||||||
|
padding: "10px 20px",
|
||||||
|
'& input': {
|
||||||
|
height: "100%",
|
||||||
|
width: "100%",
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{errorType === 'format' && (
|
||||||
|
<FormHelperText error sx={{ position: 'absolute', left: 0, top: '100%', mt: '2px', ml: '10px' }}>
|
||||||
|
можно только число или диапазон
|
||||||
|
</FormHelperText>
|
||||||
|
)}
|
||||||
|
{errorType === 'range' && (
|
||||||
|
<FormHelperText error sx={{ position: 'absolute', left: 0, top: '100%', mt: '2px', ml: '10px' }}>
|
||||||
|
таких возрастов нет
|
||||||
|
</FormHelperText>
|
||||||
|
)}
|
||||||
|
<IconButton
|
||||||
|
onClick={handleToggle}
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
right: 0,
|
||||||
|
top: '50%',
|
||||||
|
transform: `translateY(-50%) rotate(${open ? 180 : 0}deg)`,
|
||||||
|
cursor: 'pointer',
|
||||||
|
color: theme.palette.brightPurple.main,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
transition: 'transform 0.2s',
|
||||||
|
padding: '8px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ArrowDownIcon style={{ width: "18px", height: "18px" }} />
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
<Popper
|
||||||
|
open={open}
|
||||||
|
anchorEl={anchorRef.current}
|
||||||
|
role={undefined}
|
||||||
|
placement="bottom-end"
|
||||||
|
transition
|
||||||
|
disablePortal
|
||||||
|
sx={{ zIndex: 1300 }}
|
||||||
|
>
|
||||||
|
{({ TransitionProps, placement }) => (
|
||||||
|
<Grow
|
||||||
|
{...TransitionProps}
|
||||||
|
style={{
|
||||||
|
transformOrigin: placement === 'bottom' ? 'center top' : 'center bottom',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Paper elevation={8}>
|
||||||
|
<ClickAwayListener onClickAway={handleClose}>
|
||||||
|
<MenuList autoFocusItem={open} id="menu-list-grow">
|
||||||
|
<MenuItem onClick={() => handleSelectItemClick('')}>Выберите возраст</MenuItem>
|
||||||
|
<MenuItem onClick={() => handleSelectItemClick('18-24')}>18-24</MenuItem>
|
||||||
|
<MenuItem onClick={() => handleSelectItemClick('25-34')}>25-34</MenuItem>
|
||||||
|
<MenuItem onClick={() => handleSelectItemClick('35-44')}>35-44</MenuItem>
|
||||||
|
<MenuItem onClick={() => handleSelectItemClick('45-54')}>45-54</MenuItem>
|
||||||
|
<MenuItem onClick={() => handleSelectItemClick('55+')}>55+</MenuItem>
|
||||||
|
</MenuList>
|
||||||
|
</ClickAwayListener>
|
||||||
|
</Paper>
|
||||||
|
</Grow>
|
||||||
|
)}
|
||||||
|
</Popper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AgeInputWithSelect;
|
98
src/pages/PersonalizationAI/AuditoryLink.tsx
Normal file
98
src/pages/PersonalizationAI/AuditoryLink.tsx
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import { AuditoryItem } from "@/api/auditory";
|
||||||
|
import Trash from "@/assets/icons/trash";
|
||||||
|
import { useCurrentQuiz } from "@/stores/quizes/hooks";
|
||||||
|
import { InfoPopover } from "@/ui_kit/InfoPopover";
|
||||||
|
import TooltipClickInfo from "@/ui_kit/Toolbars/TooltipClickInfo";
|
||||||
|
import { useDomainDefine } from "@/utils/hooks/useDomainDefine";
|
||||||
|
import { IconButton, ListItem, Typography, useTheme } from "@mui/material";
|
||||||
|
import { CopyButton } from "./CopyButton";
|
||||||
|
|
||||||
|
interface AuditoryLinkProps {
|
||||||
|
item: AuditoryItem;
|
||||||
|
index: number;
|
||||||
|
onDelete: (id: number) => void;
|
||||||
|
utmParams: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AuditoryLink = ({ utmParams, item, index, onDelete }: AuditoryLinkProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const quiz = useCurrentQuiz();
|
||||||
|
const { isTestServer } = useDomainDefine();
|
||||||
|
|
||||||
|
const handleCopy = (text: string) => {
|
||||||
|
navigator.clipboard.writeText(text);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = () => {
|
||||||
|
onDelete(item.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const linkText = `${isTestServer ? "https://s.hbpn.link/" : "https://hbpn.link/"}${quiz?.qid}?_paud=${item.id}${utmParams}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListItem
|
||||||
|
key={index}
|
||||||
|
disablePadding
|
||||||
|
sx={{
|
||||||
|
bgcolor: "#F2F3F7",
|
||||||
|
borderRadius: "10px",
|
||||||
|
p: "13px 14px 13px 20px",
|
||||||
|
mb: "8px",
|
||||||
|
maxWidth: "756px",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
transition: 'background 0.2s, border 0.2s',
|
||||||
|
'& .MuiListItemSecondaryAction-root': {
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '12px',
|
||||||
|
width: "70px",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
secondaryAction={
|
||||||
|
<>
|
||||||
|
<IconButton
|
||||||
|
className="delete_aud"
|
||||||
|
edge="end"
|
||||||
|
aria-label="delete"
|
||||||
|
sx={{ color: theme.palette.brightPurple.main, p: 0, width: 18, height: 18 }}
|
||||||
|
onClick={handleDelete}
|
||||||
|
>
|
||||||
|
<Trash sx={{
|
||||||
|
"& path": {
|
||||||
|
stroke: theme.palette.brightPurple.main,
|
||||||
|
}
|
||||||
|
}} />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton edge="end" aria-label="info" sx={{ color: theme.palette.brightPurple.main, p: 0, width: 18, height: 18 }}>
|
||||||
|
<TooltipClickInfo title={`Пол: ${item.sex === 0 ? "женский" : item.sex === 1 ? "мужской" : "оба"} \n Возраст: ${item.age}`} />
|
||||||
|
</IconButton>
|
||||||
|
<CopyButton
|
||||||
|
created_at={item.created_at}
|
||||||
|
onCopy={handleCopy}
|
||||||
|
text={linkText}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
className="link"
|
||||||
|
sx={{
|
||||||
|
color: 'black',
|
||||||
|
fontWeight: 400,
|
||||||
|
fontSize: "16px",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
overflow: "hidden",
|
||||||
|
width: "calc(100% - 80px)",
|
||||||
|
userSelect: "none",
|
||||||
|
WebkitUserSelect: "none",
|
||||||
|
MozUserSelect: "none",
|
||||||
|
msUserSelect: "none"
|
||||||
|
}}>
|
||||||
|
{linkText}
|
||||||
|
</Typography>
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
};
|
49
src/pages/PersonalizationAI/AuditoryList.tsx
Normal file
49
src/pages/PersonalizationAI/AuditoryList.tsx
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { auditoryGet, AuditoryResponse, AuditoryItem } from "@/api/auditory";
|
||||||
|
import ArrowDownIcon from "@/assets/icons/ArrowDownIcon";
|
||||||
|
import { useCurrentQuiz } from "@/stores/quizes/hooks";
|
||||||
|
import { useDomainDefine } from "@/utils/hooks/useDomainDefine";
|
||||||
|
import { Box, Collapse, IconButton, List, Typography, useTheme } from "@mui/material";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { AuditoryLink } from "./AuditoryLink";
|
||||||
|
|
||||||
|
export const AuditoryList = ({utmParams, auditory, onDelete}:{utmParams:string,auditory:AuditoryItem[], onDelete: (id: number) => void}) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const { isTestServer } = useDomainDefine();
|
||||||
|
const [linksOpen, setLinksOpen] = useState(true);
|
||||||
|
|
||||||
|
console.log("auditory-___---_auditory__---__-__auditory_------__---__-__---_------__---__-__---_------__---__-____--__")
|
||||||
|
console.log(auditory)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Box sx={{
|
||||||
|
maxWidth: "796px",
|
||||||
|
bgcolor: "#fff",
|
||||||
|
borderRadius: "12px",
|
||||||
|
p: "20px",
|
||||||
|
boxShadow: "0px 4px 32px 0px #7E2AEA14",
|
||||||
|
mt: "24px"
|
||||||
|
}}>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
|
<Typography sx={{ fontSize: "18px", fontWeight: 500, color: theme.palette.grey3.main }}>
|
||||||
|
Ваши сохраненные ссылки
|
||||||
|
</Typography>
|
||||||
|
<IconButton
|
||||||
|
sx={{ cursor: 'pointer', color: theme.palette.brightPurple.main, display: 'flex', alignItems: 'center', transition: 'transform 0.2s', transform: linksOpen ? 'rotate(0deg)' : 'rotate(180deg)' }}
|
||||||
|
onClick={() => setLinksOpen((prev) => !prev)}
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
|
<ArrowDownIcon style={{ width: "18px", height: "18px" }} />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
<Collapse in={linksOpen} timeout="auto" unmountOnExit sx={{ mt: "3px" }}>
|
||||||
|
<List sx={{ gap: '8px', p: 0, m: 0 }}>
|
||||||
|
{auditory.map((item, idx) => (
|
||||||
|
<AuditoryLink utmParams={utmParams} key={idx} item={item} index={idx} onDelete={onDelete} />
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</Collapse>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
161
src/pages/PersonalizationAI/CopyButton.tsx
Normal file
161
src/pages/PersonalizationAI/CopyButton.tsx
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
import { IconButton, Skeleton, useTheme, Tooltip, ClickAwayListener } from "@mui/material";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import CopyIcon from "@/assets/icons/CopyIcon";
|
||||||
|
import { useSnackbar } from "notistack";
|
||||||
|
|
||||||
|
interface CopyButtonProps {
|
||||||
|
created_at: number;
|
||||||
|
onCopy: (text: string) => void;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CopyButton = ({ created_at, onCopy, text }: CopyButtonProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const { enqueueSnackbar } = useSnackbar();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [timeLeft, setTimeLeft] = useState<string>("");
|
||||||
|
|
||||||
|
const getCreatedTime = (timestamp: number) => {
|
||||||
|
// Если timestamp в секундах (10 цифр)
|
||||||
|
if (timestamp.toString().length === 10) {
|
||||||
|
return new Date(timestamp * 1000).getTime();
|
||||||
|
}
|
||||||
|
// Если timestamp в миллисекундах (13 цифр)
|
||||||
|
return new Date(timestamp).getTime();
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTimeLeft = (milliseconds: number) => {
|
||||||
|
const minutes = Math.floor(milliseconds / (1000 * 60));
|
||||||
|
const seconds = Math.floor((milliseconds % (1000 * 60)) / 1000);
|
||||||
|
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(() => {
|
||||||
|
if (!created_at) return false;
|
||||||
|
const now = new Date().getTime();
|
||||||
|
const created = getCreatedTime(created_at);
|
||||||
|
const diffInMinutes = (now - created) / (1000 * 60);
|
||||||
|
return diffInMinutes < 3;
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!created_at) return;
|
||||||
|
|
||||||
|
const now = new Date().getTime();
|
||||||
|
const created = getCreatedTime(created_at);
|
||||||
|
const diffInMinutes = (now - created) / (1000 * 60);
|
||||||
|
|
||||||
|
if (now - created < 1000) {
|
||||||
|
setIsLoading(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (diffInMinutes < 3) {
|
||||||
|
const timeLeft = Math.ceil((3 - diffInMinutes) * 60 * 1000);
|
||||||
|
setTimeLeft(formatTimeLeft(timeLeft));
|
||||||
|
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
const currentTime = new Date().getTime();
|
||||||
|
const elapsed = currentTime - created;
|
||||||
|
const remaining = 3 * 60 * 1000 - elapsed;
|
||||||
|
|
||||||
|
if (remaining <= 0) {
|
||||||
|
setIsLoading(false);
|
||||||
|
clearInterval(timer);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeLeft(formatTimeLeft(remaining));
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
return () => clearInterval(timer);
|
||||||
|
}
|
||||||
|
}, [created_at]);
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
if (isLoading) return;
|
||||||
|
onCopy(text);
|
||||||
|
enqueueSnackbar("Ссылка успешно скопирована", { variant: "success" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTooltipClose = () => {
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTooltipOpen = () => {
|
||||||
|
setOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<ClickAwayListener onClickAway={handleTooltipClose}>
|
||||||
|
<div>
|
||||||
|
<Tooltip
|
||||||
|
PopperProps={{
|
||||||
|
disablePortal: true,
|
||||||
|
sx: {
|
||||||
|
"& .MuiTooltip-tooltip": {
|
||||||
|
minWidth: "175px",
|
||||||
|
maxWidth: "175px",
|
||||||
|
whiteSpace: "normal",
|
||||||
|
textAlign: "center"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placement="top"
|
||||||
|
onClose={handleTooltipClose}
|
||||||
|
open={open}
|
||||||
|
title={`Идёт процесс генерации вопросов, он будет закончен через ${timeLeft}`}
|
||||||
|
onMouseEnter={handleTooltipOpen}
|
||||||
|
onMouseLeave={handleTooltipClose}
|
||||||
|
sx={{
|
||||||
|
fontSize: "12px",
|
||||||
|
p: "10px"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Skeleton
|
||||||
|
variant="circular"
|
||||||
|
width={18}
|
||||||
|
height={18}
|
||||||
|
sx={{
|
||||||
|
bgcolor: theme.palette.grey[400],
|
||||||
|
marginRight: "-2px"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</ClickAwayListener>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
edge="end"
|
||||||
|
aria-label="copy"
|
||||||
|
sx={{
|
||||||
|
color: theme.palette.brightPurple.main,
|
||||||
|
p: 0,
|
||||||
|
width: 18,
|
||||||
|
height: 18,
|
||||||
|
marginRight: "-2px",
|
||||||
|
position: "relative",
|
||||||
|
"&:hover": {
|
||||||
|
"&::after": {
|
||||||
|
content: '""',
|
||||||
|
position: "absolute",
|
||||||
|
top: "-15px",
|
||||||
|
left: "-15px",
|
||||||
|
right: "-15px",
|
||||||
|
bottom: "-15px",
|
||||||
|
borderRadius: "50%",
|
||||||
|
backgroundColor: theme.palette.grey[500],
|
||||||
|
opacity: 0.1,
|
||||||
|
zIndex: -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
<CopyIcon color={theme.palette.brightPurple.main} />
|
||||||
|
</IconButton>
|
||||||
|
);
|
||||||
|
};
|
212
src/pages/PersonalizationAI/GenderAndAgeSelector.tsx
Normal file
212
src/pages/PersonalizationAI/GenderAndAgeSelector.tsx
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
import { Box, FormControl, FormLabel, Checkbox, FormControlLabel, useTheme, Button, useMediaQuery } from "@mui/material";
|
||||||
|
import CheckboxIcon from "@icons/Checkbox";
|
||||||
|
import AgeInputWithSelect from "./AgeInputWithSelect";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
|
interface GenderAndAgeSelectorProps {
|
||||||
|
gender: string;
|
||||||
|
age: string;
|
||||||
|
ageError: boolean;
|
||||||
|
onGenderChange: (gender: string) => void;
|
||||||
|
onAgeChange: (age: string) => void;
|
||||||
|
onAgeErrorChange: (error: boolean) => void;
|
||||||
|
onStartCreate: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function GenderAndAgeSelector({
|
||||||
|
gender,
|
||||||
|
age,
|
||||||
|
ageError,
|
||||||
|
onGenderChange,
|
||||||
|
onAgeChange,
|
||||||
|
onAgeErrorChange,
|
||||||
|
onStartCreate
|
||||||
|
}: GenderAndAgeSelectorProps) {
|
||||||
|
const theme = useTheme();
|
||||||
|
const isMobile = useMediaQuery(theme.breakpoints.down(845));
|
||||||
|
const [maleChecked, setMaleChecked] = useState(false);
|
||||||
|
const [femaleChecked, setFemaleChecked] = useState(false);
|
||||||
|
|
||||||
|
// Синхронизируем состояние чекбоксов с пропсом gender
|
||||||
|
useEffect(() => {
|
||||||
|
if (gender === '1') {
|
||||||
|
setMaleChecked(true);
|
||||||
|
setFemaleChecked(false);
|
||||||
|
} else if (gender === '0') {
|
||||||
|
setMaleChecked(false);
|
||||||
|
setFemaleChecked(true);
|
||||||
|
} else if (gender === '2') {
|
||||||
|
setMaleChecked(true);
|
||||||
|
setFemaleChecked(true);
|
||||||
|
} else {
|
||||||
|
setMaleChecked(false);
|
||||||
|
setFemaleChecked(false);
|
||||||
|
}
|
||||||
|
}, [gender]);
|
||||||
|
|
||||||
|
const handleGenderChange = (type: 'male' | 'female', checked: boolean) => {
|
||||||
|
if (type === 'male') {
|
||||||
|
setMaleChecked(checked);
|
||||||
|
} else {
|
||||||
|
setFemaleChecked(checked);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем значение gender в родительском компоненте
|
||||||
|
if (type === 'male' && checked && !femaleChecked) {
|
||||||
|
onGenderChange('1'); // Только мужской
|
||||||
|
} else if (type === 'female' && checked && !maleChecked) {
|
||||||
|
onGenderChange('0'); // Только женский
|
||||||
|
} else if (type === 'male' && checked && femaleChecked) {
|
||||||
|
onGenderChange('2'); // Оба пола
|
||||||
|
} else if (type === 'female' && checked && maleChecked) {
|
||||||
|
onGenderChange('2'); // Оба пола
|
||||||
|
} else if (type === 'male' && !checked && femaleChecked) {
|
||||||
|
onGenderChange('0'); // Только женский
|
||||||
|
} else if (type === 'female' && !checked && maleChecked) {
|
||||||
|
onGenderChange('1'); // Только мужской
|
||||||
|
} else {
|
||||||
|
onGenderChange(''); // Ничего не выбрано
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: 'flex', gap: 4, alignItems: "end", flexWrap: "wrap" }}>
|
||||||
|
<Box sx={{ display: "inline-flex", flexWrap: isMobile ? "wrap" : "initial" }}>
|
||||||
|
<FormControl component="fieldset" variant="standard">
|
||||||
|
<Box sx={{ display: 'flex', alignItems: "end", gap: '4px' }}>
|
||||||
|
<FormLabel
|
||||||
|
sx={{
|
||||||
|
'&.Mui-focused': {
|
||||||
|
color: '#4D4D4D',
|
||||||
|
},
|
||||||
|
color: '#4D4D4D',
|
||||||
|
fontSize: '18px',
|
||||||
|
fontWeight: 500,
|
||||||
|
lineHeight: 1,
|
||||||
|
letterSpacing: 0,
|
||||||
|
mr: '3px',
|
||||||
|
}}
|
||||||
|
component="legend">Пол</FormLabel>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "155px",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
mt: "20px",
|
||||||
|
ml: "-9px",
|
||||||
|
display: 'flex',
|
||||||
|
gap: '10px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormControlLabel
|
||||||
|
sx={{
|
||||||
|
padding: 0,
|
||||||
|
'& .MuiTouchRipple-root': {
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
overflow: 'visible',
|
||||||
|
},
|
||||||
|
'& .MuiSvgIcon-root': {
|
||||||
|
position: 'absolute',
|
||||||
|
left: '50%',
|
||||||
|
top: '50%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
},
|
||||||
|
'& .MuiFormControlLabel-label': {
|
||||||
|
fontSize: '18px',
|
||||||
|
fontWeight: 400,
|
||||||
|
fontFamily: 'Rubik',
|
||||||
|
lineHeight: 1,
|
||||||
|
letterSpacing: 0,
|
||||||
|
color: '#4D4D4D',
|
||||||
|
ml: "6px"
|
||||||
|
},
|
||||||
|
m: 0,
|
||||||
|
}}
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
icon={<CheckboxIcon />}
|
||||||
|
checkedIcon={<CheckboxIcon checked />}
|
||||||
|
checked={maleChecked}
|
||||||
|
onChange={(e) => handleGenderChange('male', e.target.checked)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="М"
|
||||||
|
/>
|
||||||
|
<FormControlLabel
|
||||||
|
sx={{
|
||||||
|
padding: 0,
|
||||||
|
'& .MuiTouchRipple-root': {
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
overflow: 'visible',
|
||||||
|
},
|
||||||
|
'& .MuiSvgIcon-root': {
|
||||||
|
position: 'absolute',
|
||||||
|
left: '50%',
|
||||||
|
top: '50%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
},
|
||||||
|
'& .MuiFormControlLabel-label': {
|
||||||
|
fontSize: '18px',
|
||||||
|
fontWeight: 400,
|
||||||
|
fontFamily: 'Rubik',
|
||||||
|
lineHeight: 1,
|
||||||
|
letterSpacing: 0,
|
||||||
|
color: '#4D4D4D',
|
||||||
|
},
|
||||||
|
m: 0,
|
||||||
|
}}
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
icon={<CheckboxIcon />}
|
||||||
|
checkedIcon={<CheckboxIcon checked />}
|
||||||
|
checked={femaleChecked}
|
||||||
|
onChange={(e) => handleGenderChange('female', e.target.checked)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Ж"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl sx={{ maxWidth: "420px", width: "100%", marginLeft: isMobile ? "0" : "120px", minWidth: "265px" }} variant="filled">
|
||||||
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'end',
|
||||||
|
gap: '4px'
|
||||||
|
}}>
|
||||||
|
<FormLabel sx={{
|
||||||
|
color: '#4D4D4D',
|
||||||
|
fontSize: "18px",
|
||||||
|
fontWeight: 500,
|
||||||
|
lineHeight: "100%",
|
||||||
|
'&.Mui-focused': {
|
||||||
|
color: '#4D4D4D',
|
||||||
|
},
|
||||||
|
}}>Возраст</FormLabel>
|
||||||
|
</Box>
|
||||||
|
<AgeInputWithSelect value={age} onChange={onAgeChange} onErrorChange={onAgeErrorChange} />
|
||||||
|
</FormControl>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={onStartCreate}
|
||||||
|
variant="contained"
|
||||||
|
disabled={!gender || !age || ageError}
|
||||||
|
sx={{
|
||||||
|
bgcolor: theme.palette.brightPurple.main,
|
||||||
|
borderRadius: "8px",
|
||||||
|
width: "130px",
|
||||||
|
height: "48px",
|
||||||
|
boxShadow: "none",
|
||||||
|
textTransform: "none",
|
||||||
|
fontSize: "18px",
|
||||||
|
'&:hover': { bgcolor: theme.palette.brightPurple.main },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Ок
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
54
src/pages/PersonalizationAI/GenderSelector.tsx
Normal file
54
src/pages/PersonalizationAI/GenderSelector.tsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Box, Typography, useTheme } from '@mui/material';
|
||||||
|
import { useMediaQuery } from '@mui/material';
|
||||||
|
import { InfoPopover } from './InfoPopover';
|
||||||
|
import { GenderButton } from './GenderButton';
|
||||||
|
|
||||||
|
interface GenderSelectorProps {
|
||||||
|
value: string[];
|
||||||
|
onChange: (value: string[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GenderSelector: React.FC<GenderSelectorProps> = ({ value, onChange }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||||
|
|
||||||
|
const handleGenderClick = (gender: string) => {
|
||||||
|
if (value.includes(gender)) {
|
||||||
|
onChange(value.filter(g => g !== gender));
|
||||||
|
} else {
|
||||||
|
onChange([...value, gender]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: isMobile ? '14px' : '16px',
|
||||||
|
fontWeight: 500,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Пол
|
||||||
|
</Typography>
|
||||||
|
<InfoPopover />
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ display: 'flex', gap: '8px' }}>
|
||||||
|
<GenderButton
|
||||||
|
selected={value.includes('male')}
|
||||||
|
onClick={() => handleGenderClick('male')}
|
||||||
|
icon="male"
|
||||||
|
label="Мужской"
|
||||||
|
/>
|
||||||
|
<GenderButton
|
||||||
|
selected={value.includes('female')}
|
||||||
|
onClick={() => handleGenderClick('female')}
|
||||||
|
icon="female"
|
||||||
|
label="Женский"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
54
src/pages/PersonalizationAI/PayModal.tsx
Normal file
54
src/pages/PersonalizationAI/PayModal.tsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { Box, Button, Modal, Typography } from "@mui/material"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onCreate: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const PayModal = ({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
onCreate
|
||||||
|
}: Props) => {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
onClose={onClose}
|
||||||
|
aria-labelledby="modal-modal-title"
|
||||||
|
aria-describedby="modal-modal-description"
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: "absolute" as "absolute",
|
||||||
|
top: "50%",
|
||||||
|
left: "50%",
|
||||||
|
transform: "translate(-50%, -50%)",
|
||||||
|
maxWidth: "550px",
|
||||||
|
width: "100%",
|
||||||
|
bgcolor: "background.paper",
|
||||||
|
borderRadius: "12px",
|
||||||
|
boxShadow: 24,
|
||||||
|
p: "30px",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 2
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography sx={{ width: "100%", textAlign: "center", mb: "25px" }}>
|
||||||
|
Данная услуга предоставляется за 500 рублей/опрос. Готовы оплатить?
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||||
|
<Button sx={{width: "100px"}} variant="outlined" onClick={onClose}>
|
||||||
|
Нет
|
||||||
|
</Button>
|
||||||
|
<Button sx={{width: "100px"}} variant="contained" onClick={onCreate}>
|
||||||
|
Да
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
392
src/pages/PersonalizationAI/PersonalizationAI.tsx
Normal file
392
src/pages/PersonalizationAI/PersonalizationAI.tsx
Normal file
@ -0,0 +1,392 @@
|
|||||||
|
import { Box, Container, Typography, TextField, Button, List, ListItem, IconButton, Modal } from "@mui/material";
|
||||||
|
import { InfoPopover } from '@ui_kit/InfoPopover';
|
||||||
|
import GenderAndAgeSelector from "./GenderAndAgeSelector";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import CustomTextField from "@ui_kit/CustomTextField";
|
||||||
|
import { useTheme } from "@mui/material";
|
||||||
|
import { AuditoryItem, auditoryAdd, auditoryDelete, auditoryGet } from "@/api/auditory";
|
||||||
|
import { useCurrentQuiz } from "@/stores/quizes/hooks";
|
||||||
|
import { AuditoryList } from "./AuditoryList";
|
||||||
|
import { useSnackbar } from "notistack";
|
||||||
|
import { PayModal } from "./PayModal";
|
||||||
|
import { useUserStore } from "@/stores/user";
|
||||||
|
import { cartApi } from "@/api/cart";
|
||||||
|
import { outCart } from "../Tariffs/Tariffs";
|
||||||
|
import { inCart } from "../Tariffs/Tariffs";
|
||||||
|
import { isTestServer } from "@/utils/hooks/useDomainDefine";
|
||||||
|
import { useToken } from "@frontend/kitui";
|
||||||
|
import { useSWRConfig } from "swr";
|
||||||
|
import { makeRequest } from "@api/makeRequest";
|
||||||
|
import { setUserAccount, setCustomerAccount } from "@/stores/user";
|
||||||
|
import { quizApi } from "@api/quiz";
|
||||||
|
import { setQuizes } from "@root/quizes/actions";
|
||||||
|
import TooltipClickInfo from "@/ui_kit/Toolbars/TooltipClickInfo";
|
||||||
|
|
||||||
|
const tariff = isTestServer ? "6844b8858258f5cc35791ef7" : "6851db40acfb4d3e5fcd9b19";
|
||||||
|
export default function PersonalizationAI() {
|
||||||
|
const theme = useTheme();
|
||||||
|
const [auditory, setAuditory] = useState<AuditoryItem[]>([]);
|
||||||
|
const [deleteModal, setDeleteModal] = useState<number>(0);
|
||||||
|
const [link, setLink] = useState<string>('');
|
||||||
|
const [utmParams, setUtmParams] = useState<string>('');
|
||||||
|
const quiz = useCurrentQuiz();
|
||||||
|
const { enqueueSnackbar } = useSnackbar();
|
||||||
|
const privilegesOfUser = useUserStore((state) => state.userAccount?.privileges);
|
||||||
|
const user = useUserStore((state) => state.customerAccount);
|
||||||
|
const token = useToken();
|
||||||
|
const userId = useUserStore((state) => state.userId);
|
||||||
|
|
||||||
|
const [gender, setGender] = useState<string>('');
|
||||||
|
const [age, setAge] = useState<string>('');
|
||||||
|
const [ageError, setAgeError] = useState(false);
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
// Обновляем данные пользователя через SWR
|
||||||
|
const { mutate } = useSWRConfig();
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
setGender('');
|
||||||
|
setAge('');
|
||||||
|
setAgeError(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const createNewLink = async () => {
|
||||||
|
if (!quiz?.backendId) {
|
||||||
|
enqueueSnackbar('Ошибка: не выбран квиз', { variant: 'error' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [result, error] = await auditoryAdd({
|
||||||
|
quizId: quiz.backendId,
|
||||||
|
body: {
|
||||||
|
sex: parseInt(gender),
|
||||||
|
age
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
enqueueSnackbar('Не удалось добавить ссылку', { variant: 'error' });
|
||||||
|
return [, error];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
handleAdd({
|
||||||
|
id: result.ID,
|
||||||
|
quiz_id: quiz.backendId,
|
||||||
|
sex: parseInt(gender),
|
||||||
|
age,
|
||||||
|
deleted: false,
|
||||||
|
created_at: Date.now()
|
||||||
|
});
|
||||||
|
enqueueSnackbar('Ссылка успешно добавлена', { variant: 'success' });
|
||||||
|
resetForm();
|
||||||
|
setIsModalOpen(false);
|
||||||
|
|
||||||
|
// Обновляем данные пользователя после успешного создания ссылки
|
||||||
|
try {
|
||||||
|
const [userAccountResult, customerAccountResult] = await Promise.all([
|
||||||
|
makeRequest({
|
||||||
|
url: `${process.env.REACT_APP_DOMAIN}/squiz/account/get`,
|
||||||
|
method: "GET",
|
||||||
|
useToken: true,
|
||||||
|
withCredentials: false,
|
||||||
|
}).catch(error => {
|
||||||
|
console.log(error)
|
||||||
|
enqueueSnackbar("Ошибка при обновлении данных пользователя", { variant: "error" });
|
||||||
|
return null;
|
||||||
|
}),
|
||||||
|
makeRequest({
|
||||||
|
url: `${process.env.REACT_APP_DOMAIN}/customer/v1.0.1/account`,
|
||||||
|
method: "GET",
|
||||||
|
useToken: true,
|
||||||
|
withCredentials: false,
|
||||||
|
}).catch(error => {
|
||||||
|
console.log(error)
|
||||||
|
enqueueSnackbar("Ошибка при обновлении данных клиента", { variant: "error" });
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (userAccountResult) {
|
||||||
|
setUserAccount(userAccountResult);
|
||||||
|
}
|
||||||
|
if (customerAccountResult) {
|
||||||
|
setCustomerAccount(customerAccountResult);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
enqueueSnackbar("Ошибка при обновлении данных", { variant: "error" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
enqueueSnackbar('Произошла ошибка при добавлении', { variant: 'error' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
if (quiz?.backendId) {
|
||||||
|
const [result, error] = await auditoryGet({ quizId: quiz.backendId });
|
||||||
|
console.log("result-___---_------__---__-__---_------__---__-__---_------__---__-__---_------__---__-____--__")
|
||||||
|
console.log(result)
|
||||||
|
if (result) {
|
||||||
|
setAuditory(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [quiz]);
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
// 1. Закрываем модалку
|
||||||
|
setDeleteModal(0);
|
||||||
|
|
||||||
|
// 2. Находим индекс объекта в стейте
|
||||||
|
const indexToDelete = auditory.findIndex(item => item.id === deleteModal);
|
||||||
|
if (indexToDelete === -1) return;
|
||||||
|
|
||||||
|
// 3. Сохраняем удаляемый объект
|
||||||
|
const deletedItem = auditory[indexToDelete];
|
||||||
|
|
||||||
|
// 4. Меняем стейт, вырезая объект
|
||||||
|
setAuditory(prev => prev.filter(item => item.id !== deleteModal));
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 5. Вызываем функцию удаления
|
||||||
|
const [result, error] = await auditoryDelete({
|
||||||
|
quizId: quiz?.backendId,
|
||||||
|
auditoryId: deleteModal
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
// 6. Если удалить не удалось - показываем снекбар и возвращаем ссылку
|
||||||
|
enqueueSnackbar('Не удалось удалить ссылку', { variant: 'error' });
|
||||||
|
setAuditory(prev => {
|
||||||
|
const newArray = [...prev];
|
||||||
|
newArray.splice(indexToDelete, 0, deletedItem);
|
||||||
|
return newArray;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Обработка ошибки сети или других ошибок
|
||||||
|
enqueueSnackbar('Произошла ошибка при удалении', { variant: 'error' });
|
||||||
|
setAuditory(prev => {
|
||||||
|
const newArray = [...prev];
|
||||||
|
newArray.splice(indexToDelete, 0, deletedItem);
|
||||||
|
return newArray;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAdd = (item: AuditoryItem) => {
|
||||||
|
setAuditory(old => ([...old, item]));
|
||||||
|
// Очищаем форму после успешного добавления
|
||||||
|
setGender('');
|
||||||
|
setAge('');
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLinkChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const newLink = e.target.value;
|
||||||
|
setLink(newLink);
|
||||||
|
|
||||||
|
// Регулярное выражение для поиска параметров URL
|
||||||
|
const paramRegex = /[?&]([^=&]+)=([^&]*)/g;
|
||||||
|
const params: Record<string, string> = {};
|
||||||
|
let match;
|
||||||
|
|
||||||
|
// Ищем все параметры в строке
|
||||||
|
while ((match = paramRegex.exec(newLink)) !== null) {
|
||||||
|
const key = decodeURIComponent(match[1]);
|
||||||
|
const value = decodeURIComponent(match[2]);
|
||||||
|
params[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Преобразуем объект параметров в строку URL
|
||||||
|
const paramString = Object.entries(params)
|
||||||
|
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
|
||||||
|
.join('&');
|
||||||
|
|
||||||
|
setUtmParams(paramString ? `&${paramString}` : "");
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("______----giga_chat-----__--_---_--_----__--__-__--_--__--__--_---_______-quiz")
|
||||||
|
console.log(quiz?.giga_chat)
|
||||||
|
const startCreate = async () => {
|
||||||
|
if (quiz?.giga_chat) {
|
||||||
|
createNewLink();
|
||||||
|
} else {
|
||||||
|
setIsModalOpen(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const tryBuy = async ({ id, price }: { id: string; price: number }) => {
|
||||||
|
//Если в корзине что-то было - выкладываем содержимое и запоминаем чо там лежало
|
||||||
|
if (user?.cart?.length > 0) {
|
||||||
|
outCart(user.cart);
|
||||||
|
}
|
||||||
|
//Добавляем желаемый тариф в корзину
|
||||||
|
const [_, addError] = await cartApi.add(tariff);
|
||||||
|
|
||||||
|
if (addError) {
|
||||||
|
//Развращаем товары в корзину
|
||||||
|
inCart();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Если нам хватает денежек - покупаем тариф
|
||||||
|
const [data, payError] = await cartApi.pay();
|
||||||
|
|
||||||
|
if (payError || !data) {
|
||||||
|
//если денег не хватило
|
||||||
|
if (payError?.includes("insufficient funds") || payError?.includes("Payment Required")) {
|
||||||
|
var link = document.createElement("a");
|
||||||
|
link.href = `https://${isTestServer ? "s" : ""}hub.pena.digital/quizpayment?action=squizpay&dif=50000&data=${token}&userid=${userId}&from=AI&wayback=ai_${quiz?.backendId}`;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//другая ошибка
|
||||||
|
enqueueSnackbar("Произошла ошибка. Попробуйте позже");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Развращаем товары в корзину
|
||||||
|
inCart();
|
||||||
|
|
||||||
|
//Показываем сообщение об успешной покупке
|
||||||
|
enqueueSnackbar("Тариф успешно приобретен", { variant: "success" });
|
||||||
|
|
||||||
|
|
||||||
|
// Создаем новую ссылку после обновления данных
|
||||||
|
await createNewLink();
|
||||||
|
|
||||||
|
|
||||||
|
// Обновляем данные квиза после успешной оплаты
|
||||||
|
console.log("Обновляем данные квиза после оплаты");
|
||||||
|
const [quizes, quizesError] = await quizApi.getList();
|
||||||
|
console.log("Получены данные квизов:", quizes);
|
||||||
|
if (!quizesError) {
|
||||||
|
setQuizes(quizes);
|
||||||
|
console.log("Данные квизов обновлены в сторе");
|
||||||
|
} else {
|
||||||
|
console.error("Ошибка при получении данных квизов:", quizesError);
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Container id="PersonalizationAI" maxWidth={false} sx={{ minHeight: "100%", p: "20px", height: " calc(100vh - 80px)", overflow: "auto", pt: "55px" }}>
|
||||||
|
<Typography variant="h5" color={theme.palette.grey3.main} fontWeight={700} sx={{ fontSize: 24, letterSpacing: "-0.2px" }}>
|
||||||
|
Персонализация вопросов с помощью AI
|
||||||
|
</Typography>
|
||||||
|
<Typography sx={{
|
||||||
|
color: theme.palette.grey3.main, fontSize: "18px", maxWidth: 796, m: 0,
|
||||||
|
mt: "19px",
|
||||||
|
letterSpacing: "0.009px",
|
||||||
|
wordSpacing: "0.1px",
|
||||||
|
lineHeight: "21.4px"
|
||||||
|
}}>
|
||||||
|
Данный раздел позволяет вам создавать персонализированный опрос под каждую целевую аудиторию отдельно, наш AI перефразирует ваши вопросы согласно настройкам.
|
||||||
|
Для этого нужно выбрать пол и возраст вашей аудитории и получите персональную ссылку с нужными настройками в списке ниже.
|
||||||
|
|
||||||
|
Так же вы можете обогатить свою ссылку UTM метками в поле "вставьте свою ссылку"и этим метки применятся ко всем вашим ссылкам.
|
||||||
|
|
||||||
|
ВАЖНО: если ваши вопросы уже подходят целевой аудитории, то персонализированы они скорее всего не будут. {/* Первый белый блок */}
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{
|
||||||
|
bgcolor: "#fff",
|
||||||
|
borderRadius: "12px",
|
||||||
|
mt: "40px",
|
||||||
|
p: "20px 20px 30px",
|
||||||
|
boxShadow: "none",
|
||||||
|
maxWidth: "796px"
|
||||||
|
}}>
|
||||||
|
<GenderAndAgeSelector
|
||||||
|
gender={gender}
|
||||||
|
age={age}
|
||||||
|
ageError={ageError}
|
||||||
|
onGenderChange={setGender}
|
||||||
|
onAgeChange={setAge}
|
||||||
|
onAgeErrorChange={setAgeError}
|
||||||
|
onStartCreate={startCreate}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Ссылка */}
|
||||||
|
<Box sx={{ mt: "34px" }}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||||
|
<Typography sx={{ color: theme.palette.grey3.main, fontSize: "18px", fontWeight: 500 }}>Ссылка</Typography>
|
||||||
|
<TooltipClickInfo title={`Данное поле создано для обогащения utm метками вашей ссылки. Нужно скопировать ссылку вашего квиза, задать настройки ца, вставить ссылку в поле, прописать метки(советуем использовать динамические), и нажать "ок" выше поля. Метки будут применены ко всем ссылкам с персонализацией в рамках данного квиза.`}/>
|
||||||
|
{/* <InfoPopover >
|
||||||
|
<Typography sx={{maxWidth: "300px"}} >
|
||||||
|
Данное поле создано для обогащения utm метками вашей ссылки. Нужно скопировать ссылку вашего квиза, задать настройки ца, вставить ссылку в поле, прописать метки(советуем использовать динамические), и нажать "ок" выше поля. Метки будут применены ко всем ссылкам с персонализацией в рамках данного квиза.
|
||||||
|
</Typography>
|
||||||
|
</InfoPopover> */}
|
||||||
|
</Box>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "14px",
|
||||||
|
lineHeight: "100%",
|
||||||
|
letterSpacing: "0 %",
|
||||||
|
color: theme.palette.grey2.main,
|
||||||
|
mt: "16px"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Вставьте ссылку со всеми utm-метками
|
||||||
|
</Typography>
|
||||||
|
<CustomTextField
|
||||||
|
placeholder="linkexample.com"
|
||||||
|
maxLength={500}
|
||||||
|
value={link}
|
||||||
|
onChange={handleLinkChange}
|
||||||
|
sxForm={{
|
||||||
|
maxWidth: "615px",
|
||||||
|
width: "100%",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<AuditoryList utmParams={utmParams} onDelete={setDeleteModal} auditory={auditory} />
|
||||||
|
|
||||||
|
</Container>
|
||||||
|
<Modal
|
||||||
|
open={Boolean(deleteModal)}
|
||||||
|
onClose={() => setDeleteModal(0)}
|
||||||
|
aria-labelledby="modal-modal-title"
|
||||||
|
aria-describedby="modal-modal-description"
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: "absolute" as "absolute",
|
||||||
|
top: "50%",
|
||||||
|
left: "50%",
|
||||||
|
transform: "translate(-50%, -50%)",
|
||||||
|
maxWidth: "620px",
|
||||||
|
width: "100%",
|
||||||
|
bgcolor: "background.paper",
|
||||||
|
borderRadius: "12px",
|
||||||
|
|
||||||
|
boxShadow: 24,
|
||||||
|
p: "20px",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography sx={{ width: "100%", textAlign: "center", mb: "25px" }}>Уверены, что хотите удалить ссылку?</Typography>
|
||||||
|
<Button sx={{ mb: "20px" }} id="delete_OK" onClick={handleDelete}>Удалить</Button>
|
||||||
|
<Button variant="contained" onClick={() => setDeleteModal(0)} >Отмена</Button>
|
||||||
|
</Box>
|
||||||
|
</Modal>
|
||||||
|
<PayModal
|
||||||
|
open={isModalOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setIsModalOpen(false);
|
||||||
|
}}
|
||||||
|
onCreate={tryBuy}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -2,7 +2,11 @@ import { Box } from "@mui/material";
|
|||||||
import { reorderQuestionVariants } from "@root/questions/actions";
|
import { reorderQuestionVariants } from "@root/questions/actions";
|
||||||
import { type ReactNode } from "react";
|
import { type ReactNode } from "react";
|
||||||
import type { DropResult } from "react-beautiful-dnd";
|
import type { DropResult } from "react-beautiful-dnd";
|
||||||
import { DragDropContext, Droppable } from "react-beautiful-dnd";
|
import { DragDropContext as DragDropContextOriginal } from "react-beautiful-dnd";
|
||||||
|
import { StrictModeDroppable } from "./StrictModeDroppable";
|
||||||
|
|
||||||
|
// Исправляем типизацию для DragDropContext
|
||||||
|
const DragDropContext = DragDropContextOriginal as any;
|
||||||
|
|
||||||
type AnswerDraggableListProps = {
|
type AnswerDraggableListProps = {
|
||||||
questionId: string;
|
questionId: string;
|
||||||
@ -14,21 +18,36 @@ export const AnswerDraggableList = ({
|
|||||||
variants,
|
variants,
|
||||||
}: AnswerDraggableListProps) => {
|
}: AnswerDraggableListProps) => {
|
||||||
const onDragEnd = ({ destination, source }: DropResult) => {
|
const onDragEnd = ({ destination, source }: DropResult) => {
|
||||||
if (destination) {
|
// Проверяем наличие необходимых данных
|
||||||
|
if (!destination || !source) return;
|
||||||
|
|
||||||
|
// Проверяем, что индексы действительно изменились
|
||||||
|
if (destination.index === source.index) return;
|
||||||
|
|
||||||
|
// Проверяем валидность индексов
|
||||||
|
if (source.index < 0 || destination.index < 0) return;
|
||||||
|
|
||||||
|
try {
|
||||||
reorderQuestionVariants(questionId, source.index, destination.index);
|
reorderQuestionVariants(questionId, source.index, destination.index);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error reordering variants:', error);
|
||||||
|
// Здесь можно добавить уведомление пользователю об ошибке
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DragDropContext onDragEnd={onDragEnd}>
|
<DragDropContext onDragEnd={onDragEnd}>
|
||||||
<Droppable droppableId="droppable-answer-list">
|
<StrictModeDroppable droppableId="droppable-answer-list">
|
||||||
{(provided) => (
|
{(provided) => (
|
||||||
<Box ref={provided.innerRef} {...provided.droppableProps}>
|
<Box
|
||||||
|
ref={provided.innerRef}
|
||||||
|
{...provided.droppableProps}
|
||||||
|
>
|
||||||
{variants}
|
{variants}
|
||||||
{provided.placeholder}
|
{provided.placeholder}
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Droppable>
|
</StrictModeDroppable>
|
||||||
</DragDropContext>
|
</DragDropContext>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -3,6 +3,7 @@ import { ResultSettings } from "./ResultSettings";
|
|||||||
import {
|
import {
|
||||||
decrementCurrentStep,
|
decrementCurrentStep,
|
||||||
incrementCurrentStep,
|
incrementCurrentStep,
|
||||||
|
setCurrentStep,
|
||||||
} from "@root/quizes/actions";
|
} from "@root/quizes/actions";
|
||||||
import { Box, Button } from "@mui/material";
|
import { Box, Button } from "@mui/material";
|
||||||
import ArrowLeft from "@icons/questionsPage/arrowLeft";
|
import ArrowLeft from "@icons/questionsPage/arrowLeft";
|
||||||
|
@ -1,10 +1,6 @@
|
|||||||
import { logout } from "@api/auth";
|
|
||||||
import { activatePromocode } from "@api/promocode";
|
import { activatePromocode } from "@api/promocode";
|
||||||
import type { Tariff } from "@frontend/kitui";
|
|
||||||
import { useToken } from "@frontend/kitui";
|
import { useToken } from "@frontend/kitui";
|
||||||
import { makeRequest } from "@api/makeRequest";
|
|
||||||
import ArrowLeft from "@icons/questionsPage/arrowLeft";
|
import ArrowLeft from "@icons/questionsPage/arrowLeft";
|
||||||
import type { GetTariffsResponse } from "@model/tariff";
|
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
@ -12,15 +8,11 @@ import {
|
|||||||
IconButton,
|
IconButton,
|
||||||
Modal,
|
Modal,
|
||||||
Paper,
|
Paper,
|
||||||
Select,
|
|
||||||
Typography,
|
Typography,
|
||||||
useMediaQuery,
|
useMediaQuery,
|
||||||
useTheme,
|
useTheme,
|
||||||
MenuItem,
|
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { clearQuizData } from "@root/quizes/store";
|
import { useUserStore } from "@root/user";
|
||||||
import { cleanAuthTicketData } from "@root/ticket";
|
|
||||||
import { clearUserData, useUserStore } from "@root/user";
|
|
||||||
import { LogoutButton } from "@ui_kit/LogoutButton";
|
import { LogoutButton } from "@ui_kit/LogoutButton";
|
||||||
import { useDomainDefine } from "@utils/hooks/useDomainDefine";
|
import { useDomainDefine } from "@utils/hooks/useDomainDefine";
|
||||||
import { enqueueSnackbar } from "notistack";
|
import { enqueueSnackbar } from "notistack";
|
||||||
@ -34,16 +26,14 @@ import { createTariffElements } from "./tariffsUtils/createTariffElements";
|
|||||||
import { currencyFormatter } from "./tariffsUtils/currencyFormatter";
|
import { currencyFormatter } from "./tariffsUtils/currencyFormatter";
|
||||||
import { useWallet, setCash } from "@root/cash";
|
import { useWallet, setCash } from "@root/cash";
|
||||||
import { handleLogoutClick } from "@utils/HandleLogoutClick";
|
import { handleLogoutClick } from "@utils/HandleLogoutClick";
|
||||||
import { getDiscounts } from "@api/discounts";
|
|
||||||
import { cartApi } from "@api/cart";
|
import { cartApi } from "@api/cart";
|
||||||
import { getUser } from "@api/user";
|
|
||||||
import { getTariffs } from "@api/tariff";
|
|
||||||
|
|
||||||
import type { Discount } from "@model/discounts";
|
|
||||||
import { Other } from "./pages/Other";
|
import { Other } from "./pages/Other";
|
||||||
import { ModalRequestCreate } from "./ModalRequestCreate";
|
import { ModalRequestCreate } from "./ModalRequestCreate";
|
||||||
import { cancelCC, useCC } from "@/stores/cc";
|
import { cancelCC, useCC } from "@/stores/cc";
|
||||||
import { NavSelect } from "./NavSelect";
|
import { NavSelect } from "./NavSelect";
|
||||||
|
import { useTariffs } from '@utils/hooks/useTariffs';
|
||||||
|
import { useDiscounts } from '@utils/hooks/useDiscounts';
|
||||||
|
|
||||||
const StepperText: Record<string, string> = {
|
const StepperText: Record<string, string> = {
|
||||||
day: "Тарифы на время",
|
day: "Тарифы на время",
|
||||||
@ -59,9 +49,11 @@ function TariffPage() {
|
|||||||
const isMobile = useMediaQuery(theme.breakpoints.down(600));
|
const isMobile = useMediaQuery(theme.breakpoints.down(600));
|
||||||
const userId = useUserStore((state) => state.userId);
|
const userId = useUserStore((state) => state.userId);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [tariffs, setTariffs] = useState<Tariff[]>([]);
|
const user = useUserStore((state) => state.customerAccount);
|
||||||
const [user, setUser] = useState();
|
const a = useUserStore((state) => state.customerAccount); //c wallet
|
||||||
const [discounts, setDiscounts] = useState<Discount[]>([]);
|
console.log("________________34563875693785692576_____________USERRRRRRR")
|
||||||
|
console.log(a)
|
||||||
|
const { data: discounts } = useDiscounts(userId);
|
||||||
const [isRequestCreate, setIsRequestCreate] = useState(false);
|
const [isRequestCreate, setIsRequestCreate] = useState(false);
|
||||||
const [openModal, setOpenModal] = useState({});
|
const [openModal, setOpenModal] = useState({});
|
||||||
const { cashString, cashCop, cashRub } = useWallet();
|
const { cashString, cashCop, cashRub } = useWallet();
|
||||||
@ -70,56 +62,20 @@ function TariffPage() {
|
|||||||
const [promocodeField, setPromocodeField] = useState<string>("");
|
const [promocodeField, setPromocodeField] = useState<string>("");
|
||||||
const cc = useCC(store => store.cc)
|
const cc = useCC(store => store.cc)
|
||||||
|
|
||||||
const getTariffsList = async (): Promise<Tariff[]> => {
|
|
||||||
const tariffsList: Tariff[] = [];
|
|
||||||
let page = 2
|
|
||||||
const [tariffsResponse, tariffsResponseError] = await getTariffs(page - 1);
|
|
||||||
console.log(tariffsResponse)
|
|
||||||
if (tariffsResponseError || !tariffsResponse) {
|
|
||||||
return tariffsList;
|
|
||||||
}
|
|
||||||
|
|
||||||
tariffsList.push(...tariffsResponse.tariffs);
|
const { data: tariffs, error: tariffsError, isLoading: tariffsLoading } = useTariffs();
|
||||||
|
|
||||||
for (page; page <= tariffsResponse.totalPages; page += 1) {
|
console.log("________34563875693785692576_____ TARIFFS")
|
||||||
const [tariffsResult] = await getTariffs(page);
|
console.log(tariffs)
|
||||||
|
|
||||||
if (tariffsResult) {
|
|
||||||
tariffsList.push(...tariffsResult.tariffs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return tariffsList;
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const get = async () => {
|
if (a) {
|
||||||
const [user, userError] = await getUser();
|
|
||||||
|
|
||||||
if (userError) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tariffsList = await getTariffsList();
|
|
||||||
|
|
||||||
if (userId) {
|
|
||||||
const [discounts] = await getDiscounts(userId);
|
|
||||||
|
|
||||||
if (discounts?.length) {
|
|
||||||
setDiscounts(discounts);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setUser(user);
|
|
||||||
setTariffs(tariffsList);
|
|
||||||
|
|
||||||
let cs = currencyFormatter.format(Number(user.wallet.cash) / 100);
|
let cs = currencyFormatter.format(Number(user.wallet.cash) / 100);
|
||||||
let cc = Number(user.wallet.cash);
|
let cc = Number(user.wallet.cash);
|
||||||
let cr = Number(user.wallet.cash) / 100;
|
let cr = Number(user.wallet.cash) / 100;
|
||||||
setCash(cs, cc, cr);
|
setCash(cs, cc, cr);
|
||||||
};
|
}
|
||||||
get();
|
}, [a]);
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (cc) {
|
if (cc) {
|
||||||
@ -203,32 +159,17 @@ console.log(tariffsResponse)
|
|||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
enqueueSnackbar(error);
|
enqueueSnackbar(error);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
enqueueSnackbar(greetings);
|
enqueueSnackbar(greetings);
|
||||||
|
|
||||||
if (!userId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [discounts, discountsError] = await getDiscounts(userId);
|
|
||||||
|
|
||||||
if (discountsError) {
|
|
||||||
throw new Error(discountsError);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (discounts?.length) {
|
|
||||||
setDiscounts(discounts);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const startRequestCreate = () => {
|
const startRequestCreate = () => {
|
||||||
setIsRequestCreate(true)
|
setIsRequestCreate(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!a) return null;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Container
|
<Container
|
||||||
@ -346,7 +287,7 @@ console.log(tariffsResponse)
|
|||||||
selectedItem={selectedItem}
|
selectedItem={selectedItem}
|
||||||
content={[
|
content={[
|
||||||
{
|
{
|
||||||
title: `Убрать логотип “PenaQuiz”`,
|
title: `Убрать логотип "PenaQuiz"`,
|
||||||
onClick: () => setSelectedItem("hide")
|
onClick: () => setSelectedItem("hide")
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -446,7 +387,7 @@ export const inCart = () => {
|
|||||||
localStorage.setItem("saveCart", "[]");
|
localStorage.setItem("saveCart", "[]");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const outCart = (cart: string[]) => {
|
export const outCart = (cart: string[]) => {
|
||||||
//Сделаем муторно и подольше, зато при прерывании сессии данные потеряются минимально
|
//Сделаем муторно и подольше, зато при прерывании сессии данные потеряются минимально
|
||||||
if (cart.length > 0) {
|
if (cart.length > 0) {
|
||||||
cart.forEach(async (id: string) => {
|
cart.forEach(async (id: string) => {
|
||||||
|
@ -19,6 +19,8 @@ export const createTariffElements = (
|
|||||||
) => {
|
) => {
|
||||||
console.log("start work createTariffElements")
|
console.log("start work createTariffElements")
|
||||||
console.log("filteredTariffs ", filteredTariffs)
|
console.log("filteredTariffs ", filteredTariffs)
|
||||||
|
console.log("user ", user)
|
||||||
|
console.log("user.isUserNko, ", user.isUserNko)
|
||||||
const tariffElements = filteredTariffs
|
const tariffElements = filteredTariffs
|
||||||
.filter((tariff) => tariff.privileges.length > 0)
|
.filter((tariff) => tariff.privileges.length > 0)
|
||||||
.map((tariff, index) => {
|
.map((tariff, index) => {
|
||||||
@ -27,7 +29,7 @@ export const createTariffElements = (
|
|||||||
discounts,
|
discounts,
|
||||||
user.wallet.spent,
|
user.wallet.spent,
|
||||||
[],
|
[],
|
||||||
user.isUserNko,
|
user.status === "nko",
|
||||||
user.userId,
|
user.userId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -11,58 +11,59 @@ import AutoIcon8 from "@/assets/quiz-templates/auto/auto-8.jpg";
|
|||||||
import AutoIcon9 from "@/assets/quiz-templates/auto/auto-9.jpg";
|
import AutoIcon9 from "@/assets/quiz-templates/auto/auto-9.jpg";
|
||||||
import AutoIcon10 from "@/assets/quiz-templates/auto/auto-10.jpg";
|
import AutoIcon10 from "@/assets/quiz-templates/auto/auto-10.jpg";
|
||||||
|
|
||||||
|
import { isTestServer } from "@/utils/hooks/useDomainDefine";
|
||||||
export const AUTO_TEMPLATES: Category = {
|
export const AUTO_TEMPLATES: Category = {
|
||||||
categoryType: "Auto",
|
categoryType: "Auto",
|
||||||
category: "Авто",
|
category: "Авто",
|
||||||
templates: [
|
templates: [
|
||||||
{
|
{
|
||||||
quizId: "eb885519-d9c2-41a5-a69c-6105d2bd9bef",
|
quizId: isTestServer ? "b1b0ed51-e2de-4b48-a8ca-d55e42b290ca" : "eb885519-d9c2-41a5-a69c-6105d2bd9bef",
|
||||||
title: "Узнайте, что у вас с машиной",
|
title: "Узнайте, что у вас с машиной",
|
||||||
picture: AutoIcon1,
|
picture: AutoIcon1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "68f080e2-ae70-4a1a-be09-05c3decea592",
|
quizId: isTestServer ? "037f6f16-58e9-4854-a3fd-ccbdaa2ef901" : "68f080e2-ae70-4a1a-be09-05c3decea592",
|
||||||
title: "Узнай стоимость и сроки выкупа своего автомобиля",
|
title: "Узнай стоимость и сроки выкупа своего автомобиля",
|
||||||
picture: AutoIcon2,
|
picture: AutoIcon2,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "446a5e79-8f10-4fb0-aa0f-165e3fbd8d36",
|
quizId: isTestServer ? "f5eadfa3-9cfc-4429-9854-380f5240fbbe" : "446a5e79-8f10-4fb0-aa0f-165e3fbd8d36",
|
||||||
title: "Автошкола «Руль в Руки»",
|
title: "Автошкола «Руль в Руки»",
|
||||||
picture: AutoIcon3,
|
picture: AutoIcon3,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "f30c7d80-852e-405d-8308-a124636b5ffa",
|
quizId: isTestServer ? "1f4d6841-9ee6-43ba-9d3d-929bbf2a5252" : "f30c7d80-852e-405d-8308-a124636b5ffa",
|
||||||
title: "Узнайте, в какой компании выгодней КАСКО и ОСАГО",
|
title: "Узнайте, в какой компании выгодней КАСКО и ОСАГО",
|
||||||
picture: AutoIcon4,
|
picture: AutoIcon4,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "e200c96f-9c7a-4859-9bd2-65e42a6450b3",
|
quizId: isTestServer ? "dc5d523f-3922-4407-883c-22fc07f440d6" : "e200c96f-9c7a-4859-9bd2-65e42a6450b3",
|
||||||
title:
|
title:
|
||||||
"Пройди тест, чтобы рассчитать стоимость необходимых детейлинг услуг",
|
"Пройди тест, чтобы рассчитать стоимость необходимых детейлинг услуг",
|
||||||
picture: AutoIcon5,
|
picture: AutoIcon5,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "824c4553-ecb5-43e8-9b62-efc4844b01a8",
|
quizId: isTestServer ? "1e3dd6a6-34fb-44fc-9583-fd85de35b553" : "824c4553-ecb5-43e8-9b62-efc4844b01a8",
|
||||||
title: "Онлайн-калькулятор шиномонтажных услуг",
|
title: "Онлайн-калькулятор шиномонтажных услуг",
|
||||||
picture: AutoIcon6,
|
picture: AutoIcon6,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "42423a16-1159-4c5c-bb45-4e9940ab6098",
|
quizId: isTestServer ? "15a14d9a-7afc-44f8-b162-eddcc327911a" : "42423a16-1159-4c5c-bb45-4e9940ab6098",
|
||||||
title: "Калькулятор расчёта стоимости тонировки автомобиля",
|
title: "Калькулятор расчёта стоимости тонировки автомобиля",
|
||||||
picture: AutoIcon7,
|
picture: AutoIcon7,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "a0dfe680-30ff-4cac-91a5-28eb79889b68",
|
quizId: isTestServer ? "5c65c221-ac6d-4544-9f93-222b5790310b" : "a0dfe680-30ff-4cac-91a5-28eb79889b68",
|
||||||
title: "Рассчитайте стоимость проката премиум-автомобиля за 3 минуты",
|
title: "Рассчитайте стоимость проката премиум-автомобиля за 3 минуты",
|
||||||
picture: AutoIcon8,
|
picture: AutoIcon8,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "18145500-1fdd-4814-9607-8775fb1a5ea7",
|
quizId: isTestServer ? "bec775f0-2e0a-47b2-8f74-4ab03ee0b29e" : "18145500-1fdd-4814-9607-8775fb1a5ea7",
|
||||||
title: "Безопасное автокресло для вашего ребенка",
|
title: "Безопасное автокресло для вашего ребенка",
|
||||||
picture: AutoIcon9,
|
picture: AutoIcon9,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "63aa090c-8943-4a50-a10a-be394e75188b",
|
quizId: isTestServer ? "81a7b7e5-3045-4a5a-a850-b05d53d4bc82" : "63aa090c-8943-4a50-a10a-be394e75188b",
|
||||||
title: "Подберём для вас премиум-автомобиль для проката",
|
title: "Подберём для вас премиум-автомобиль для проката",
|
||||||
picture: AutoIcon10,
|
picture: AutoIcon10,
|
||||||
},
|
},
|
||||||
|
@ -11,59 +11,60 @@ import EductionIcon8 from "@/assets/quiz-templates/education/education-8.jpg";
|
|||||||
import EductionIcon9 from "@/assets/quiz-templates/education/education-9.jpg";
|
import EductionIcon9 from "@/assets/quiz-templates/education/education-9.jpg";
|
||||||
import EductionIcon10 from "@/assets/quiz-templates/education/education-10.jpg";
|
import EductionIcon10 from "@/assets/quiz-templates/education/education-10.jpg";
|
||||||
|
|
||||||
|
import { isTestServer } from "@/utils/hooks/useDomainDefine";
|
||||||
export const EDUCATION_TEMPLATES: Category = {
|
export const EDUCATION_TEMPLATES: Category = {
|
||||||
categoryType: "Education",
|
categoryType: "Education",
|
||||||
category: "Образование",
|
category: "Образование",
|
||||||
templates: [
|
templates: [
|
||||||
{
|
{
|
||||||
quizId: "27c10a81-f629-4af4-bdd0-2eb6c9cf10a8",
|
quizId: isTestServer ? "845cb5eb-bca8-495d-826f-e7b52a271b41" : "27c10a81-f629-4af4-bdd0-2eb6c9cf10a8",
|
||||||
title: "Получите приглашение на занятие по программированию для ребёнка",
|
title: "Получите приглашение на занятие по программированию для ребёнка",
|
||||||
picture: EductionIcon1,
|
picture: EductionIcon1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "bf9aaa3b-5d2d-4f82-9d5e-74862d73d10e",
|
quizId: isTestServer ? "33042986-9ff3-408e-898b-13b53319cb08" : "bf9aaa3b-5d2d-4f82-9d5e-74862d73d10e",
|
||||||
title: "Научим играть любимую песню на фортепиано за 7 занятий",
|
title: "Научим играть любимую песню на фортепиано за 7 занятий",
|
||||||
picture: EductionIcon2,
|
picture: EductionIcon2,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "e2ed3948-6da2-48f4-86c7-42118b5abf85",
|
quizId: isTestServer ? "e7751cf8-467e-40e8-bd4d-0935ccab934b" : "e2ed3948-6da2-48f4-86c7-42118b5abf85",
|
||||||
title: "Подбери репетитора для своего ребёнка со скидкой в 20%",
|
title: "Подбери репетитора для своего ребёнка со скидкой в 20%",
|
||||||
picture: EductionIcon3,
|
picture: EductionIcon3,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "076d3d12-c8f0-442a-b918-7f6085daa3ec",
|
quizId: isTestServer ? "a9e40faa-4cd5-495e-8812-acc0dde2dee2" : "076d3d12-c8f0-442a-b918-7f6085daa3ec",
|
||||||
title: "Обратная связь о вебинаре",
|
title: "Обратная связь о вебинаре",
|
||||||
picture: EductionIcon4,
|
picture: EductionIcon4,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "9914fe9c-19b4-47b1-aef8-a3c8e44f4c4c",
|
quizId: isTestServer ? "ab3fb1bc-afc8-4cf1-b77a-3fcff361d5be" : "9914fe9c-19b4-47b1-aef8-a3c8e44f4c4c",
|
||||||
title: "Хотите выучить английский?",
|
title: "Хотите выучить английский?",
|
||||||
picture: EductionIcon5,
|
picture: EductionIcon5,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "ec9c252e-ea2c-489a-809d-27522b7c1972",
|
quizId: isTestServer ? "a2900f7b-cf24-4a9b-b5da-f7f99dbd8a1b" : "ec9c252e-ea2c-489a-809d-27522b7c1972",
|
||||||
title:
|
title:
|
||||||
"Ответьте на 4 вопроса и узнайте, куда записать ребенка чтобы развивать его таланты",
|
"Ответьте на 4 вопроса и узнайте, куда записать ребенка чтобы развивать его таланты",
|
||||||
picture: EductionIcon6,
|
picture: EductionIcon6,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "45acb5b0-1dca-45fe-aaa0-88895bd5b237",
|
quizId: isTestServer ? "" : "45acb5b0-1dca-45fe-aaa0-88895bd5b237",
|
||||||
title: "Поделитесь мнением о конференции",
|
title: "Поделитесь мнением о конференции",
|
||||||
picture: EductionIcon7,
|
picture: EductionIcon7,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "a9f17936-30c8-41ff-84d4-668840e02b56",
|
quizId: isTestServer ? "" : "a9f17936-30c8-41ff-84d4-668840e02b56",
|
||||||
title: "Научитесь красиво петь и управлять своим голосом",
|
title: "Научитесь красиво петь и управлять своим голосом",
|
||||||
picture: EductionIcon8,
|
picture: EductionIcon8,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "51c4d927-4d27-405d-ab7e-6c2707418017",
|
quizId: isTestServer ? "" : "51c4d927-4d27-405d-ab7e-6c2707418017",
|
||||||
title: "Узнайте, подойдёт ли вам профессия «Разработчик Phyton»?",
|
title: "Узнайте, подойдёт ли вам профессия «Разработчик Phyton»?",
|
||||||
categoryDescription: "(С ветвлением)",
|
categoryDescription: "(С ветвлением)",
|
||||||
picture: EductionIcon9,
|
picture: EductionIcon9,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "6063ee99-3188-43aa-89bc-895d90b08628",
|
quizId: isTestServer ? "e6dc608c-055a-44bd-ba2e-6cb185b378fe" : "6063ee99-3188-43aa-89bc-895d90b08628",
|
||||||
title: "Проверьте своё знание английского языка",
|
title: "Проверьте своё знание английского языка",
|
||||||
categoryDescription: "(С ветвлением)",
|
categoryDescription: "(С ветвлением)",
|
||||||
picture: EductionIcon10,
|
picture: EductionIcon10,
|
||||||
|
@ -21,125 +21,126 @@ import HealthIcon18 from "@/assets/quiz-templates/health/health-18.jpg";
|
|||||||
import HealthIcon19 from "@/assets/quiz-templates/health/health-19.jpg";
|
import HealthIcon19 from "@/assets/quiz-templates/health/health-19.jpg";
|
||||||
import HealthIcon20 from "@/assets/quiz-templates/health/health-20.jpg";
|
import HealthIcon20 from "@/assets/quiz-templates/health/health-20.jpg";
|
||||||
|
|
||||||
|
import { isTestServer } from "@/utils/hooks/useDomainDefine";
|
||||||
export const HEALTH_TEMPLATES: Category = {
|
export const HEALTH_TEMPLATES: Category = {
|
||||||
categoryType: "Health",
|
categoryType: "Health",
|
||||||
category: "Здоровье и уход",
|
category: "Здоровье и уход",
|
||||||
templates: [
|
templates: [
|
||||||
{
|
{
|
||||||
quizId: "294c9c27-a189-4aa1-b792-a4d4612c99bf",
|
quizId: isTestServer ? "1927cf61-d80c-431c-8a04-4abca7c84b1e" : "294c9c27-a189-4aa1-b792-a4d4612c99bf",
|
||||||
title: "Узнайте, сколько будет стоить ваш маникюр",
|
title: "Узнайте, сколько будет стоить ваш маникюр",
|
||||||
categoryDescription: "Косметология",
|
categoryDescription: "Косметология",
|
||||||
picture: HealthIcon1,
|
picture: HealthIcon1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "89fc7b57-9a13-4889-9e70-9d08714085f5",
|
quizId: isTestServer ? "cdb28a49-4bd4-411f-be8c-bc4bcdd577ab" : "89fc7b57-9a13-4889-9e70-9d08714085f5",
|
||||||
title: "Узнайте стоимость услуг косметолога в Казани",
|
title: "Узнайте стоимость услуг косметолога в Казани",
|
||||||
categoryDescription: "Косметология",
|
categoryDescription: "Косметология",
|
||||||
picture: HealthIcon2,
|
picture: HealthIcon2,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "425c75c7-9412-485e-930f-3ae65f517fab",
|
quizId: isTestServer ? "6e6e8039-6d5e-4bc2-983a-f0e39f4b91c8" : "425c75c7-9412-485e-930f-3ae65f517fab",
|
||||||
title:
|
title:
|
||||||
"Узнайте, как правильно ухаживать за вашим типом кожи в домашних условиях",
|
"Узнайте, как правильно ухаживать за вашим типом кожи в домашних условиях",
|
||||||
categoryDescription: "Косметология",
|
categoryDescription: "Косметология",
|
||||||
picture: HealthIcon3,
|
picture: HealthIcon3,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "99461154-6296-4c8c-930d-2b1809f221cd",
|
quizId: isTestServer ? "1dcec3e5-5bfc-481a-bf80-5a1ca8941e89" : "99461154-6296-4c8c-930d-2b1809f221cd",
|
||||||
title: "Какая косметологическая процедура вам нужна?",
|
title: "Какая косметологическая процедура вам нужна?",
|
||||||
categoryDescription: "Косметология",
|
categoryDescription: "Косметология",
|
||||||
picture: HealthIcon4,
|
picture: HealthIcon4,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "cbf6a8d4-538a-4edf-9477-062a15361b04",
|
quizId: isTestServer ? "3cf82c7a-44c9-49d0-bbeb-97a84f6ebe8f" : "cbf6a8d4-538a-4edf-9477-062a15361b04",
|
||||||
title: "5 вопросов до улыбки вашей мечты",
|
title: "5 вопросов до улыбки вашей мечты",
|
||||||
categoryDescription: "Стоматология",
|
categoryDescription: "Стоматология",
|
||||||
picture: HealthIcon5,
|
picture: HealthIcon5,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "017d9d5c-57a8-4eca-95c1-11db847a0e18",
|
quizId: isTestServer ? "3520c146-3cd9-43c6-9ef2-42571ff06a3e" : "017d9d5c-57a8-4eca-95c1-11db847a0e18",
|
||||||
title:
|
title:
|
||||||
"Пройдите небольшой опрос, и узнайте, какая процедура у стоматолога вам нужна",
|
"Пройдите небольшой опрос, и узнайте, какая процедура у стоматолога вам нужна",
|
||||||
categoryDescription: "Стоматология",
|
categoryDescription: "Стоматология",
|
||||||
picture: HealthIcon6,
|
picture: HealthIcon6,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "162cb4f1-ab0a-49c4-b773-16932700f871",
|
quizId: isTestServer ? "aaa50e95-cd8c-4458-b82e-0139174d85ee" : "162cb4f1-ab0a-49c4-b773-16932700f871",
|
||||||
title: "Какой врач мне нужен?",
|
title: "Какой врач мне нужен?",
|
||||||
picture: HealthIcon7,
|
picture: HealthIcon7,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "c851276b-505d-492b-9acb-5cd85e6fe3a7",
|
quizId: isTestServer ? "d81b56a0-0961-41ca-8816-cc391bf75efb" : "c851276b-505d-492b-9acb-5cd85e6fe3a7",
|
||||||
title: "Психологическая помощь",
|
title: "Психологическая помощь",
|
||||||
categoryDescription: "Психолог",
|
categoryDescription: "Психолог",
|
||||||
picture: HealthIcon8,
|
picture: HealthIcon8,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "2fa1d438-72ac-49b2-95b6-73a8c9d8347a",
|
quizId: isTestServer ? "f4a0e414-b739-4a2e-8001-3de6eb1304c3" : "2fa1d438-72ac-49b2-95b6-73a8c9d8347a",
|
||||||
title: "Ищешь психолога?",
|
title: "Ищешь психолога?",
|
||||||
categoryDescription: "Психолог",
|
categoryDescription: "Психолог",
|
||||||
picture: HealthIcon9,
|
picture: HealthIcon9,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "b0b30965-ec43-4718-8a1f-2ae35f932a61",
|
quizId: isTestServer ? "f10774b0-23f6-4525-a2fd-b3ffd9d59cce" : "b0b30965-ec43-4718-8a1f-2ae35f932a61",
|
||||||
title: "Подбор медицинского центра для лечебного массажа",
|
title: "Подбор медицинского центра для лечебного массажа",
|
||||||
categoryDescription: "Массаж",
|
categoryDescription: "Массаж",
|
||||||
picture: HealthIcon10,
|
picture: HealthIcon10,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "722aff37-d247-4341-9908-412e41f9d7cd",
|
quizId: isTestServer ? "418d735e-8134-4742-963b-8fdf392aebd3" : "722aff37-d247-4341-9908-412e41f9d7cd",
|
||||||
title: "Исследование рынка мобильных приложений для здоровья",
|
title: "Исследование рынка мобильных приложений для здоровья",
|
||||||
picture: HealthIcon11,
|
picture: HealthIcon11,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "f0d800bc-2df0-42a6-8457-5c7759021854",
|
quizId: isTestServer ? "63552fb8-1586-4f14-a7c7-b75736294a87" : "f0d800bc-2df0-42a6-8457-5c7759021854",
|
||||||
title: "Выполним стрижки и окрашивания любой сложности",
|
title: "Выполним стрижки и окрашивания любой сложности",
|
||||||
categoryDescription: "Косметология",
|
categoryDescription: "Косметология",
|
||||||
picture: HealthIcon12,
|
picture: HealthIcon12,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "f88e2eb6-66e6-41ba-9d3d-1d7fe69d30d8",
|
quizId: isTestServer ? "4f4c6b83-a73c-4dbe-8776-ab93a073503d" : "f88e2eb6-66e6-41ba-9d3d-1d7fe69d30d8",
|
||||||
title: "Массажный салон «Промято» в Ярославле",
|
title: "Массажный салон «Промято» в Ярославле",
|
||||||
categoryDescription: "Массаж",
|
categoryDescription: "Массаж",
|
||||||
picture: HealthIcon13,
|
picture: HealthIcon13,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "9b2d47e8-d45f-48b7-a7fd-1c9c35edab17",
|
quizId: isTestServer ? "d47812bc-b7ac-4325-9eb1-496f1e60ab2c" : "9b2d47e8-d45f-48b7-a7fd-1c9c35edab17",
|
||||||
title: "Подбери себе направление в йоге",
|
title: "Подбери себе направление в йоге",
|
||||||
categoryDescription: "Йога",
|
categoryDescription: "Йога",
|
||||||
picture: HealthIcon14,
|
picture: HealthIcon14,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "8f6a1b3f-27fc-4e1c-a117-f67867e5df65",
|
quizId: isTestServer ? "bcd65cdd-07e7-480f-b305-596b815d1bb9" : "8f6a1b3f-27fc-4e1c-a117-f67867e5df65",
|
||||||
title: "Подберите за 2 минуты рацион готового питания",
|
title: "Подберите за 2 минуты рацион готового питания",
|
||||||
categoryDescription: "Питание",
|
categoryDescription: "Питание",
|
||||||
picture: HealthIcon15,
|
picture: HealthIcon15,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "73ff039f-3e93-4412-80ab-749f54c9bafa",
|
quizId: isTestServer ? "07f118c8-84fb-473a-9c83-357246fecaf1" : "73ff039f-3e93-4412-80ab-749f54c9bafa",
|
||||||
title: "Рассчитайте стоимость установки грудных имплантов",
|
title: "Рассчитайте стоимость установки грудных имплантов",
|
||||||
picture: HealthIcon16,
|
picture: HealthIcon16,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "2b4be94e-3505-41ae-85bb-c6c4a4d1bcd4",
|
quizId: isTestServer ? "ecba00e5-3990-4501-8b7a-fbee50383625" : "2b4be94e-3505-41ae-85bb-c6c4a4d1bcd4",
|
||||||
title: "Не знаете, как выбрать очки? Подберите оправу под свои параметры",
|
title: "Не знаете, как выбрать очки? Подберите оправу под свои параметры",
|
||||||
categoryDescription: "Зрение",
|
categoryDescription: "Зрение",
|
||||||
picture: HealthIcon17,
|
picture: HealthIcon17,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "28b133a5-0e6a-46b9-bd6b-81a44b808341",
|
quizId: isTestServer ? "1315b676-2abb-49e6-a959-89e2963bbe53" : "28b133a5-0e6a-46b9-bd6b-81a44b808341",
|
||||||
title: "Санаторий в Подмосковье для пожилых людей",
|
title: "Санаторий в Подмосковье для пожилых людей",
|
||||||
categoryDescription: "Санаторий",
|
categoryDescription: "Санаторий",
|
||||||
picture: HealthIcon18,
|
picture: HealthIcon18,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "88a8e952-1475-4052-b99a-bbb7eb31249c",
|
quizId: isTestServer ? "d439fa6a-e13b-4a38-9bea-9414ee82c9fd" : "88a8e952-1475-4052-b99a-bbb7eb31249c",
|
||||||
title: "Свежие блюда своими руками. 15 минут и готово",
|
title: "Свежие блюда своими руками. 15 минут и готово",
|
||||||
categoryDescription: "Питание",
|
categoryDescription: "Питание",
|
||||||
picture: HealthIcon19,
|
picture: HealthIcon19,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "6baf144a-7401-442a-a513-6bc5aa3f1a6a",
|
quizId: isTestServer ? "de88083b-f02d-4a53-8afa-2c646d6aa588" : "6baf144a-7401-442a-a513-6bc5aa3f1a6a",
|
||||||
title: "Рассчитайте стоимость отдыха в лучшей бане Москвы",
|
title: "Рассчитайте стоимость отдыха в лучшей бане Москвы",
|
||||||
picture: HealthIcon20,
|
picture: HealthIcon20,
|
||||||
},
|
},
|
||||||
|
@ -11,58 +11,59 @@ import ProductionIcon8 from "@/assets/quiz-templates/production/production-8.jpg
|
|||||||
import ProductionIcon9 from "@/assets/quiz-templates/production/production-9.jpg";
|
import ProductionIcon9 from "@/assets/quiz-templates/production/production-9.jpg";
|
||||||
import ProductionIcon10 from "@/assets/quiz-templates/production/production-10.jpg";
|
import ProductionIcon10 from "@/assets/quiz-templates/production/production-10.jpg";
|
||||||
|
|
||||||
|
import { isTestServer } from "@/utils/hooks/useDomainDefine";
|
||||||
export const PRODUCTION_TEMPLATES: Category = {
|
export const PRODUCTION_TEMPLATES: Category = {
|
||||||
categoryType: "Production",
|
categoryType: "Production",
|
||||||
category: "Производство",
|
category: "Производство",
|
||||||
templates: [
|
templates: [
|
||||||
{
|
{
|
||||||
quizId: "14859665-e8ea-4e4a-b381-af88179f8ba3",
|
quizId: isTestServer ? "2745eb4a-e592-4319-9e6c-4e3f7b9503d2" : "14859665-e8ea-4e4a-b381-af88179f8ba3",
|
||||||
title: "Рассчитайте стоимость постельного белья",
|
title: "Рассчитайте стоимость постельного белья",
|
||||||
picture: ProductionIcon1,
|
picture: ProductionIcon1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "39cb17b6-10df-4107-abb8-6726d4845cbf",
|
quizId: isTestServer ? "1f18bf94-24c7-4f08-8362-e7efd2923359" : "39cb17b6-10df-4107-abb8-6726d4845cbf",
|
||||||
title: "Ответьте на 4 вопроса и подберите межкомнатную дверь",
|
title: "Ответьте на 4 вопроса и подберите межкомнатную дверь",
|
||||||
picture: ProductionIcon2,
|
picture: ProductionIcon2,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "21b125ed-0213-4a3c-bd30-1a75b3953f4a",
|
quizId: isTestServer ? "75a52c54-9ebf-4785-bd11-8c432125005a" : "21b125ed-0213-4a3c-bd30-1a75b3953f4a",
|
||||||
title: "Узнай стоимость производства и монтажа металлических ворот",
|
title: "Узнай стоимость производства и монтажа металлических ворот",
|
||||||
picture: ProductionIcon3,
|
picture: ProductionIcon3,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "ed1a01f4-9497-4a79-adac-8f4fbf7f26f5",
|
quizId: isTestServer ? "2bc49b0b-e356-43bb-ac4e-37d135c48b2d" : "ed1a01f4-9497-4a79-adac-8f4fbf7f26f5",
|
||||||
title: "Заполните анкету, чтобы заказать изготовление ювелирного изделия",
|
title: "Заполните анкету, чтобы заказать изготовление ювелирного изделия",
|
||||||
picture: ProductionIcon4,
|
picture: ProductionIcon4,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "c94834f8-dd3a-43a0-8d40-6ebae4f475ed",
|
quizId: isTestServer ? "38880384-214d-4ac5-95f7-8ceb2f6060b5" : "c94834f8-dd3a-43a0-8d40-6ebae4f475ed",
|
||||||
title: "Идеальный пол для любого помещения",
|
title: "Идеальный пол для любого помещения",
|
||||||
picture: ProductionIcon5,
|
picture: ProductionIcon5,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "35ccb5b5-f4d2-4bbc-b172-5984356e7cfb",
|
quizId: isTestServer ? "d8189d8a-eb1d-41d7-9aee-62db91bd0ee0" : "35ccb5b5-f4d2-4bbc-b172-5984356e7cfb",
|
||||||
title: "Рассчитайте стоимость изготовления зеркала",
|
title: "Рассчитайте стоимость изготовления зеркала",
|
||||||
picture: ProductionIcon6,
|
picture: ProductionIcon6,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "e89d3758-2cfb-4566-9eb2-733c1c11ea03",
|
quizId: isTestServer ? "a1c7240c-af97-4405-9724-f02155e140df" : "e89d3758-2cfb-4566-9eb2-733c1c11ea03",
|
||||||
title: "Подбери лучшие кеды",
|
title: "Подбери лучшие кеды",
|
||||||
picture: ProductionIcon7,
|
picture: ProductionIcon7,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "26f00205-8373-4d00-bd93-7ced6cd0f509",
|
quizId: isTestServer ? "2031fe03-4f3e-4144-8cbf-26715b54d973" : "26f00205-8373-4d00-bd93-7ced6cd0f509",
|
||||||
title: "Идеальная кровать для вашего ребенка",
|
title: "Идеальная кровать для вашего ребенка",
|
||||||
picture: ProductionIcon8,
|
picture: ProductionIcon8,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "4cc7cacf-30a9-4571-9319-dd186b915624",
|
quizId: isTestServer ? "46824071-75e7-4988-b157-960471ad7234" : "4cc7cacf-30a9-4571-9319-dd186b915624",
|
||||||
title:
|
title:
|
||||||
"Рассчитайте стоимость кухни ручной работы из Италии с доставкой в Россию",
|
"Рассчитайте стоимость кухни ручной работы из Италии с доставкой в Россию",
|
||||||
picture: ProductionIcon9,
|
picture: ProductionIcon9,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "0d839f24-53e8-4dbd-9d9b-c57ac8e53a9c",
|
quizId: isTestServer ? "df3aff58-afb1-4876-a97a-2d792373894e" : "0d839f24-53e8-4dbd-9d9b-c57ac8e53a9c",
|
||||||
title: "Узнайте примерную стоимость индивидуального пошива одежды",
|
title: "Узнайте примерную стоимость индивидуального пошива одежды",
|
||||||
picture: ProductionIcon10,
|
picture: ProductionIcon10,
|
||||||
},
|
},
|
||||||
|
@ -11,62 +11,63 @@ import RealEstateIcon8 from "@/assets/quiz-templates/real-estate/real-estate-8.j
|
|||||||
import RealEstateIcon9 from "@/assets/quiz-templates/real-estate/real-estate-9.jpg";
|
import RealEstateIcon9 from "@/assets/quiz-templates/real-estate/real-estate-9.jpg";
|
||||||
import RealEstateIcon10 from "@/assets/quiz-templates/real-estate/real-estate-10.jpg";
|
import RealEstateIcon10 from "@/assets/quiz-templates/real-estate/real-estate-10.jpg";
|
||||||
|
|
||||||
|
import { isTestServer } from "@/utils/hooks/useDomainDefine";
|
||||||
export const REAL_ESTATE_TEMPLATES: Category = {
|
export const REAL_ESTATE_TEMPLATES: Category = {
|
||||||
categoryType: "RealEstate",
|
categoryType: "RealEstate",
|
||||||
category: "Недвижимость",
|
category: "Недвижимость",
|
||||||
templates: [
|
templates: [
|
||||||
{
|
{
|
||||||
quizId: "d3930e95-ae95-4e2f-b9f9-79b929c2e1e6",
|
quizId: isTestServer ? "af9ed905-3947-4396-a9d9-a3b233451349" : "d3930e95-ae95-4e2f-b9f9-79b929c2e1e6",
|
||||||
title: "Рассчитайте стоимость каркасного дома своей мечты",
|
title: "Рассчитайте стоимость каркасного дома своей мечты",
|
||||||
categoryDescription: "Строительство и ремонт",
|
categoryDescription: "Строительство и ремонт",
|
||||||
picture: RealEstateIcon1,
|
picture: RealEstateIcon1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "4e488b9b-d273-4f1c-b729-991fcbc006cd",
|
quizId: isTestServer ? "67b2c10a-a1f8-4eee-82a6-3aa7cd938566" : "4e488b9b-d273-4f1c-b729-991fcbc006cd",
|
||||||
title: "Краткосрочная аренда коммерческих помещений",
|
title: "Краткосрочная аренда коммерческих помещений",
|
||||||
categoryDescription: "Аренда",
|
categoryDescription: "Аренда",
|
||||||
picture: RealEstateIcon2,
|
picture: RealEstateIcon2,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "84605c72-ce1d-49fb-a40e-7ed2ab96ac7d",
|
quizId: isTestServer ? "eacc428a-b724-4c26-9573-a179f23aef81" : "84605c72-ce1d-49fb-a40e-7ed2ab96ac7d",
|
||||||
title: "Подберем новостройку под ваши критерии",
|
title: "Подберем новостройку под ваши критерии",
|
||||||
picture: RealEstateIcon3,
|
picture: RealEstateIcon3,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "ab701ab8-b8ad-4f45-a1ef-f0ab5357a587",
|
quizId: isTestServer ? "b77623d9-b73e-4eab-9137-6cd3e3722bfa" : "ab701ab8-b8ad-4f45-a1ef-f0ab5357a587",
|
||||||
title: "15 лучших предложений от застройщиков в Москве",
|
title: "15 лучших предложений от застройщиков в Москве",
|
||||||
picture: RealEstateIcon4,
|
picture: RealEstateIcon4,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "a5998d6c-c055-4702-bfc7-e1185fffa6c6",
|
quizId: isTestServer ? "8ebca441-50f2-4ed8-a648-fd35a86976e9" : "a5998d6c-c055-4702-bfc7-e1185fffa6c6",
|
||||||
title: "Подберем идеальное жильё в Риме",
|
title: "Подберем идеальное жильё в Риме",
|
||||||
picture: RealEstateIcon5,
|
picture: RealEstateIcon5,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "bfbf97f2-3eba-4386-a794-4fa8f5825ac1",
|
quizId: isTestServer ? "1df0c5b6-5d15-427f-8972-651b0a8e67d7" : "bfbf97f2-3eba-4386-a794-4fa8f5825ac1",
|
||||||
title: "Подбери уютный коттедж для отдыха в Подмосковье за 1 минуту",
|
title: "Подбери уютный коттедж для отдыха в Подмосковье за 1 минуту",
|
||||||
picture: RealEstateIcon6,
|
picture: RealEstateIcon6,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "1b6ce902-0568-43c2-90a1-55dec710cb4f",
|
quizId: isTestServer ? "2857158a-18e0-4de7-bf0f-f1915ad95db1" : "1b6ce902-0568-43c2-90a1-55dec710cb4f",
|
||||||
title: "Среди сотен новостроек подберём для вас самые подходящие",
|
title: "Среди сотен новостроек подберём для вас самые подходящие",
|
||||||
picture: RealEstateIcon7,
|
picture: RealEstateIcon7,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "0dfa128f-8c2b-4519-8cf4-05f9171979e1",
|
quizId: isTestServer ? "df32e587-660c-4248-8fd3-c6e9b1752ae0" : "0dfa128f-8c2b-4519-8cf4-05f9171979e1",
|
||||||
title: "Рассчитайте стоимость бронирования клуба для мероприятий",
|
title: "Рассчитайте стоимость бронирования клуба для мероприятий",
|
||||||
categoryDescription: "Aренда",
|
categoryDescription: "Aренда",
|
||||||
picture: RealEstateIcon8,
|
picture: RealEstateIcon8,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "8c4c8e3d-19cb-4c55-8952-558b877245bd",
|
quizId: isTestServer ? "9151a489-0d31-4442-a868-db0779322697" : "8c4c8e3d-19cb-4c55-8952-558b877245bd",
|
||||||
title:
|
title:
|
||||||
"Запишитесь на консультацию и получите каталог объектов в перспективных районах Дубая",
|
"Запишитесь на консультацию и получите каталог объектов в перспективных районах Дубая",
|
||||||
categoryDescription: "Услуги риелтора",
|
categoryDescription: "Услуги риелтора",
|
||||||
picture: RealEstateIcon9,
|
picture: RealEstateIcon9,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "36ebbe5d-4d85-453d-b5d2-51cdf7f95327",
|
quizId: isTestServer ? "6bcee3c8-951f-4745-8858-2b2cd6f6d282" : "36ebbe5d-4d85-453d-b5d2-51cdf7f95327",
|
||||||
title:
|
title:
|
||||||
"Строим дома за 90 дней вместе со всеми коммуникациями и электричеством",
|
"Строим дома за 90 дней вместе со всеми коммуникациями и электричеством",
|
||||||
categoryDescription: "Строительство и ремонт",
|
categoryDescription: "Строительство и ремонт",
|
||||||
|
@ -11,59 +11,60 @@ import RepairIcon8 from "@/assets/quiz-templates/repair/repair-8.jpg";
|
|||||||
import RepairIcon9 from "@/assets/quiz-templates/repair/repair-9.jpg";
|
import RepairIcon9 from "@/assets/quiz-templates/repair/repair-9.jpg";
|
||||||
import RepairIcon10 from "@/assets/quiz-templates/repair/repair-10.jpg";
|
import RepairIcon10 from "@/assets/quiz-templates/repair/repair-10.jpg";
|
||||||
|
|
||||||
|
import { isTestServer } from "@/utils/hooks/useDomainDefine";
|
||||||
export const REPAIR_TEMPLATES: Category = {
|
export const REPAIR_TEMPLATES: Category = {
|
||||||
categoryType: "Repair",
|
categoryType: "Repair",
|
||||||
category: "Ремонт",
|
category: "Ремонт",
|
||||||
templates: [
|
templates: [
|
||||||
{
|
{
|
||||||
quizId: "556760d9-652b-4ff1-91d5-3dc629650882",
|
quizId: isTestServer ? "d15ca3f0-8d59-4ac3-b168-6ac3246a22bb" : "556760d9-652b-4ff1-91d5-3dc629650882",
|
||||||
title: "Капитальный ремонт квартир с фиксированной ценой",
|
title: "Капитальный ремонт квартир с фиксированной ценой",
|
||||||
picture: RepairIcon1,
|
picture: RepairIcon1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "8f034581-71fb-467e-82dd-a415d4b8d73c",
|
quizId: isTestServer ? "62295ce9-58ad-42c5-9827-e3b180c8c4f7" : "8f034581-71fb-467e-82dd-a415d4b8d73c",
|
||||||
title: "Натяжные потолки с гарантией 25 лет",
|
title: "Натяжные потолки с гарантией 25 лет",
|
||||||
picture: RepairIcon2,
|
picture: RepairIcon2,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "fcb8c47b-f409-400c-b3d5-66657755f885",
|
quizId: isTestServer ? "d46febb0-3e79-4b83-8d7f-d104991a9359" : "fcb8c47b-f409-400c-b3d5-66657755f885",
|
||||||
title: "Рассчитайте стоимость пластиковых окон",
|
title: "Рассчитайте стоимость пластиковых окон",
|
||||||
picture: RepairIcon3,
|
picture: RepairIcon3,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "7544a8d3-ff03-491d-9189-1433fe307ad0",
|
quizId: isTestServer ? "d473b5a6-4d70-49d3-bacb-35ce21cd88fe" : "7544a8d3-ff03-491d-9189-1433fe307ad0",
|
||||||
title: "Рассчитайте стоимость установки тёплого пола",
|
title: "Рассчитайте стоимость установки тёплого пола",
|
||||||
picture: RepairIcon4,
|
picture: RepairIcon4,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "dcf8bd1d-4c3f-4d1a-9efa-3d25991068f9",
|
quizId: isTestServer ? "a1bba994-a733-4817-a9bb-c64a68d670c8" : "dcf8bd1d-4c3f-4d1a-9efa-3d25991068f9",
|
||||||
title:
|
title:
|
||||||
"Рассчитайте стоимость лестницы под ключ по вашим параметрам всего за одну минуту",
|
"Рассчитайте стоимость лестницы под ключ по вашим параметрам всего за одну минуту",
|
||||||
picture: RepairIcon5,
|
picture: RepairIcon5,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "2a921839-e5c8-45aa-afca-703d0dad8fad",
|
quizId: isTestServer ? "ad2fd24a-c28d-469c-95ea-78626fc51719" : "2a921839-e5c8-45aa-afca-703d0dad8fad",
|
||||||
title:
|
title:
|
||||||
"Ответьте на 5 вопросов и рассчитайте стоимость вентиляции с монтажом под объект",
|
"Ответьте на 5 вопросов и рассчитайте стоимость вентиляции с монтажом под объект",
|
||||||
picture: RepairIcon6,
|
picture: RepairIcon6,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "ed13de01-f803-456a-b237-3644c808a0a1",
|
quizId: isTestServer ? "831303b3-aa9d-4115-936e-c46b899dd9b0" : "ed13de01-f803-456a-b237-3644c808a0a1",
|
||||||
title: "Узнайте стоимость освещения вашего объекта",
|
title: "Узнайте стоимость освещения вашего объекта",
|
||||||
picture: RepairIcon7,
|
picture: RepairIcon7,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "8d05e910-df1f-4ad3-9679-c0c3f7b7e575",
|
quizId: isTestServer ? "48c57689-3999-4f52-90ef-b26232fc400d" : "8d05e910-df1f-4ad3-9679-c0c3f7b7e575",
|
||||||
title: "Узнайте стоимость кухни на заказ",
|
title: "Узнайте стоимость кухни на заказ",
|
||||||
picture: RepairIcon8,
|
picture: RepairIcon8,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "9cabba56-2861-40dc-8f33-800745c3c949",
|
quizId: isTestServer ? "b61ca69f-d0e1-4754-ab91-2ec56fa45ca3" : "9cabba56-2861-40dc-8f33-800745c3c949",
|
||||||
title: "Узнай стоимость дизайна интерьера под ключ",
|
title: "Узнай стоимость дизайна интерьера под ключ",
|
||||||
picture: RepairIcon9,
|
picture: RepairIcon9,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "1c0eb1ad-ed3e-43f9-bcba-f094d13fef5b",
|
quizId: isTestServer ? "bc6aad09-b0e0-419c-87d8-6dd9770cc12e" : "1c0eb1ad-ed3e-43f9-bcba-f094d13fef5b",
|
||||||
title:
|
title:
|
||||||
"Требуется штукатурка? Узнайте примерную стоимость работ и материалов.",
|
"Требуется штукатурка? Узнайте примерную стоимость работ и материалов.",
|
||||||
picture: RepairIcon10,
|
picture: RepairIcon10,
|
||||||
|
@ -11,57 +11,58 @@ import ResearchIcon8 from "@/assets/quiz-templates/research/research-8.jpg";
|
|||||||
import ResearchIcon9 from "@/assets/quiz-templates/research/research-9.jpg";
|
import ResearchIcon9 from "@/assets/quiz-templates/research/research-9.jpg";
|
||||||
import ResearchIcon10 from "@/assets/quiz-templates/research/research-10.jpg";
|
import ResearchIcon10 from "@/assets/quiz-templates/research/research-10.jpg";
|
||||||
|
|
||||||
|
import { isTestServer } from "@/utils/hooks/useDomainDefine";
|
||||||
export const RESEARCH_TEMPLATES: Category = {
|
export const RESEARCH_TEMPLATES: Category = {
|
||||||
categoryType: "Research",
|
categoryType: "Research",
|
||||||
category: "Исследовательские",
|
category: "Исследовательские",
|
||||||
templates: [
|
templates: [
|
||||||
{
|
{
|
||||||
quizId: "1b356222-e762-4f3d-87e5-4c3d6c0a9467",
|
quizId: isTestServer ? "14f05b7d-abd1-4069-83ef-52e5fb748592" : "1b356222-e762-4f3d-87e5-4c3d6c0a9467",
|
||||||
title: "Общественные настроения. Социальное самочувствие граждан",
|
title: "Общественные настроения. Социальное самочувствие граждан",
|
||||||
picture: ResearchIcon1,
|
picture: ResearchIcon1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "7e901bea-6774-48b7-b31f-b62fd21ac88f",
|
quizId: isTestServer ? "b06f995a-7b35-493d-9614-a57cbbeae619" : "7e901bea-6774-48b7-b31f-b62fd21ac88f",
|
||||||
title: "Социальные институты и проблемы общества",
|
title: "Социальные институты и проблемы общества",
|
||||||
picture: ResearchIcon2,
|
picture: ResearchIcon2,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "2570ccef-563c-4d8e-a052-d6ad142fb789",
|
quizId: isTestServer ? "47a72329-e3fe-4164-94d2-87ec8471de39" : "2570ccef-563c-4d8e-a052-d6ad142fb789",
|
||||||
title: "Уровень жизни населения",
|
title: "Уровень жизни населения",
|
||||||
picture: ResearchIcon3,
|
picture: ResearchIcon3,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "b9394ed2-25e0-4e55-9d2a-9577856e903d",
|
quizId: isTestServer ? "54b0eccc-2ba2-4f48-8947-98449478a56f" : "b9394ed2-25e0-4e55-9d2a-9577856e903d",
|
||||||
title: "Проблемы семьи и семейные отношения",
|
title: "Проблемы семьи и семейные отношения",
|
||||||
picture: ResearchIcon4,
|
picture: ResearchIcon4,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "922088b6-9e02-4a0f-b6af-a7150781d4eb",
|
quizId: isTestServer ? "e7c67cc4-6e80-4c78-81d0-62669274fd3e" : "922088b6-9e02-4a0f-b6af-a7150781d4eb",
|
||||||
title: "Здоровье и здравоохранение",
|
title: "Здоровье и здравоохранение",
|
||||||
picture: ResearchIcon5,
|
picture: ResearchIcon5,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "528ef773-2da5-4988-b687-b393d687ed00",
|
quizId: isTestServer ? "8c155f0c-9025-4779-be2c-17119a49fd40" : "528ef773-2da5-4988-b687-b393d687ed00",
|
||||||
title: "Религия и Церковь",
|
title: "Религия и Церковь",
|
||||||
picture: ResearchIcon6,
|
picture: ResearchIcon6,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "8887c07c-831f-40c6-9bf7-951ab09546da",
|
quizId: isTestServer ? "afa35959-9df4-4ff8-9b3c-9a6ede888931" : "8887c07c-831f-40c6-9bf7-951ab09546da",
|
||||||
title: "Трудоустройство молодежи",
|
title: "Трудоустройство молодежи",
|
||||||
picture: ResearchIcon7,
|
picture: ResearchIcon7,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "850fde64-0462-40f7-992e-44fd0177e3b7",
|
quizId: isTestServer ? "75a252ec-b9e6-4764-acef-5d490a170e0b" : "850fde64-0462-40f7-992e-44fd0177e3b7",
|
||||||
title: "Культура и ценности",
|
title: "Культура и ценности",
|
||||||
picture: ResearchIcon8,
|
picture: ResearchIcon8,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "2c6ba86a-6c86-47b2-b71c-3c4ebaf29fbb",
|
quizId: isTestServer ? "18ca6609-5390-45ea-81e0-b6d3d26277bf" : "2c6ba86a-6c86-47b2-b71c-3c4ebaf29fbb",
|
||||||
title: "Наука и технологии",
|
title: "Наука и технологии",
|
||||||
picture: ResearchIcon9,
|
picture: ResearchIcon9,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "7ccd26ff-ccf5-4d6c-a148-1612a970211e",
|
quizId: isTestServer ? "cf3e02e5-b5d7-43d2-9ea3-888112387695" : "7ccd26ff-ccf5-4d6c-a148-1612a970211e",
|
||||||
title: "Бизнес и предпринимательство",
|
title: "Бизнес и предпринимательство",
|
||||||
picture: ResearchIcon10,
|
picture: ResearchIcon10,
|
||||||
},
|
},
|
||||||
|
@ -11,67 +11,68 @@ import ServiceIcon10 from "@/assets/quiz-templates/services/service-10.jpg";
|
|||||||
import ServiceIcon11 from "@/assets/quiz-templates/services/service-11.jpg";
|
import ServiceIcon11 from "@/assets/quiz-templates/services/service-11.jpg";
|
||||||
|
|
||||||
import type { Category } from "../Template";
|
import type { Category } from "../Template";
|
||||||
|
import { isTestServer } from "@/utils/hooks/useDomainDefine";
|
||||||
|
|
||||||
export const SERVICE_TEMPLATES: Category = {
|
export const SERVICE_TEMPLATES: Category = {
|
||||||
categoryType: "Services",
|
categoryType: "Services",
|
||||||
category: "Услуги",
|
category: "Услуги",
|
||||||
templates: [
|
templates: [
|
||||||
{
|
{
|
||||||
quizId: "a3490800-1ad3-4944-bb9c-32189d36b75c",
|
quizId: isTestServer ? "a055f7c1-34eb-4969-bf6a-1c701e1217b1" : "a3490800-1ad3-4944-bb9c-32189d36b75c",
|
||||||
title:
|
title:
|
||||||
"Ответьте на 3 вопроса и узнайте, паспорт какой европейской страны вам подойдёт",
|
"Ответьте на 3 вопроса и узнайте, паспорт какой европейской страны вам подойдёт",
|
||||||
picture: ServiceIcon1,
|
picture: ServiceIcon1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "785fed83-6608-4029-ae22-6a26ce621e5f",
|
quizId: isTestServer ? "9fd1eff3-e342-4ae8-8e2a-2929c83af9cd" : "785fed83-6608-4029-ae22-6a26ce621e5f",
|
||||||
title:
|
title:
|
||||||
"Ответьте на 7 вопросов, чтобы получить коммерческое предложение от маркетолога",
|
"Ответьте на 7 вопросов, чтобы получить коммерческое предложение от маркетолога",
|
||||||
picture: ServiceIcon2,
|
picture: ServiceIcon2,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "dfa3733f-66ce-4335-b83a-2c6511cbd1ce",
|
quizId: isTestServer ? "baf9468d-31b8-422f-9baa-efe2b00ac2af" : "dfa3733f-66ce-4335-b83a-2c6511cbd1ce",
|
||||||
title:
|
title:
|
||||||
"Ответьте на пару вопросов, чтобы найти свой индивидуальный стиль одежды",
|
"Ответьте на пару вопросов, чтобы найти свой индивидуальный стиль одежды",
|
||||||
picture: ServiceIcon3,
|
picture: ServiceIcon3,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "8bf582a9-0a66-4f7b-bc0f-3c2f656c7449",
|
quizId: isTestServer ? "934e9f8f-ab9c-443b-8693-ce132baae9d1" : "8bf582a9-0a66-4f7b-bc0f-3c2f656c7449",
|
||||||
title: "Обменяйте рубли на валюту с комиссией 0%",
|
title: "Обменяйте рубли на валюту с комиссией 0%",
|
||||||
picture: ServiceIcon4,
|
picture: ServiceIcon4,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "206ba071-afe9-4ee0-a722-a24a4f592679",
|
quizId: isTestServer ? "16cdb992-4338-417b-a8f5-0684c062e2cb" : "206ba071-afe9-4ee0-a722-a24a4f592679",
|
||||||
title: "Рассчитайте стоимость уборки вашей квартиры",
|
title: "Рассчитайте стоимость уборки вашей квартиры",
|
||||||
picture: ServiceIcon5,
|
picture: ServiceIcon5,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "6938ff93-52eb-4296-86bf-fe5aa3fddabf",
|
quizId: isTestServer ? "c3c5bf13-8498-4c35-9b58-1b9d6114fc47" : "6938ff93-52eb-4296-86bf-fe5aa3fddabf",
|
||||||
title: "Забронируйте номер в зоогостинице для своего любимого питомца",
|
title: "Забронируйте номер в зоогостинице для своего любимого питомца",
|
||||||
picture: ServiceIcon6,
|
picture: ServiceIcon6,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "5262bc69-1ea0-446c-a16f-e929b6190e6d",
|
quizId: isTestServer ? "" : "5262bc69-1ea0-446c-a16f-e929b6190e6d",
|
||||||
title: "Организуем перевозку под ключ",
|
title: "Организуем перевозку под ключ",
|
||||||
picture: ServiceIcon7,
|
picture: ServiceIcon7,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "9f8015f7-07fc-4acb-92dd-6e00505884cc",
|
quizId: isTestServer ? "37bc6554-1634-4b0c-8d95-abf589c8f56d" : "9f8015f7-07fc-4acb-92dd-6e00505884cc",
|
||||||
title: "Рассчитайте стоимость ремонта пластиковых окон за 3 минуты",
|
title: "Рассчитайте стоимость ремонта пластиковых окон за 3 минуты",
|
||||||
picture: ServiceIcon8,
|
picture: ServiceIcon8,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "259749bf-a54f-4a8e-ab5a-4cd0862d7504",
|
quizId: isTestServer ? "5ff47f56-2f50-42c8-af1e-936c8f63aca2" : "259749bf-a54f-4a8e-ab5a-4cd0862d7504",
|
||||||
title: "Поможем подобрать эскиз для татуировки",
|
title: "Поможем подобрать эскиз для татуировки",
|
||||||
picture: ServiceIcon9,
|
picture: ServiceIcon9,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "e107c0cd-4fa1-4a8f-938a-10a329b6528d",
|
quizId: isTestServer ? "4fc3dd99-b818-40ac-81d4-75150308608e" : "e107c0cd-4fa1-4a8f-938a-10a329b6528d",
|
||||||
title: "Подбери себе лучшего юриста за 30 секунд",
|
title: "Подбери себе лучшего юриста за 30 секунд",
|
||||||
categoryDescription: "Юр услуги",
|
categoryDescription: "Юр услуги",
|
||||||
picture: ServiceIcon10,
|
picture: ServiceIcon10,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "ce7903b1-3dfb-4a28-a2a4-0b41af447ae1",
|
quizId: isTestServer ? "ad4adfed-8e82-4d77-8675-6c024ab492ad" : "ce7903b1-3dfb-4a28-a2a4-0b41af447ae1",
|
||||||
title: "Рассчитайте размер ипотечного кредитования, ответив на 4 вопроса",
|
title: "Рассчитайте размер ипотечного кредитования, ответив на 4 вопроса",
|
||||||
categoryDescription: "Юр услуги",
|
categoryDescription: "Юр услуги",
|
||||||
picture: ServiceIcon11,
|
picture: ServiceIcon11,
|
||||||
|
@ -10,58 +10,59 @@ import TourismIcon9 from "@/assets/quiz-templates/tourism/tourism-9.jpg";
|
|||||||
import TourismIcon10 from "@/assets/quiz-templates/tourism/tourism-10.jpg";
|
import TourismIcon10 from "@/assets/quiz-templates/tourism/tourism-10.jpg";
|
||||||
|
|
||||||
import type { Category } from "../Template";
|
import type { Category } from "../Template";
|
||||||
|
import { isTestServer } from "@/utils/hooks/useDomainDefine";
|
||||||
|
|
||||||
export const TOURISM_TEMPLATES: Category = {
|
export const TOURISM_TEMPLATES: Category = {
|
||||||
categoryType: "Tourism",
|
categoryType: "Tourism",
|
||||||
category: "Туризм",
|
category: "Туризм",
|
||||||
templates: [
|
templates: [
|
||||||
{
|
{
|
||||||
quizId: "f7a2b3b8-2548-47d8-afb3-f2c69a3a0a81",
|
quizId: isTestServer ? "0d42bc16-c927-4d49-8764-deb4e4f14c4f" : "f7a2b3b8-2548-47d8-afb3-f2c69a3a0a81",
|
||||||
title: "Подбор туристической страховки",
|
title: "Подбор туристической страховки",
|
||||||
picture: TourismIcon1,
|
picture: TourismIcon1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "e0927ded-5c4c-4d45-a5ba-c2e938362ffa",
|
quizId: isTestServer ? "897e3908-28e3-494e-a0be-0e3a0264b946" : "e0927ded-5c4c-4d45-a5ba-c2e938362ffa",
|
||||||
title: "Оцените свои шансы на получение визы в США",
|
title: "Оцените свои шансы на получение визы в США",
|
||||||
picture: TourismIcon2,
|
picture: TourismIcon2,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "23af97f4-0b8f-4d8b-8099-66ebef409ce1",
|
quizId: isTestServer ? "c4eec832-3e34-4486-b4db-e32812f924ea" : "23af97f4-0b8f-4d8b-8099-66ebef409ce1",
|
||||||
title: "Персональный тур с лучшими местами в Германии",
|
title: "Персональный тур с лучшими местами в Германии",
|
||||||
picture: TourismIcon3,
|
picture: TourismIcon3,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "ca3bd705-7d41-4ff1-ae4c-0b2d4a8faa30",
|
quizId: isTestServer ? "67fbd981-e77d-4f43-8e37-9b2e94dc4afa" : "ca3bd705-7d41-4ff1-ae4c-0b2d4a8faa30",
|
||||||
title: "Подберём лучший вариант тура под ваши критерии",
|
title: "Подберём лучший вариант тура под ваши критерии",
|
||||||
picture: TourismIcon4,
|
picture: TourismIcon4,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "5c2effd9-fe6a-40e6-9752-3f61dc20d6fa",
|
quizId: isTestServer ? "dd965dcf-aeca-4c39-8ee9-0dde372204b6" : "5c2effd9-fe6a-40e6-9752-3f61dc20d6fa",
|
||||||
title: "Выберем самый подходящий для вас тур в Грузию",
|
title: "Выберем самый подходящий для вас тур в Грузию",
|
||||||
picture: TourismIcon5,
|
picture: TourismIcon5,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "b559a764-6f55-4dc2-a9c4-aecd8b96003c",
|
quizId: isTestServer ? "e8d98b38-fbda-486e-b69a-b036f74703c8" : "b559a764-6f55-4dc2-a9c4-aecd8b96003c",
|
||||||
title: "Бонжур, Сена! Подберём для вас тур по Франции",
|
title: "Бонжур, Сена! Подберём для вас тур по Франции",
|
||||||
picture: TourismIcon6,
|
picture: TourismIcon6,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "e33bf54b-9ad5-4cb9-b552-77148264d6af",
|
quizId: isTestServer ? "52018c8a-53e7-4d76-8fc3-4e3bd414ee3e" : "e33bf54b-9ad5-4cb9-b552-77148264d6af",
|
||||||
title: "Персональный тур в Египет с лучшими местами страны",
|
title: "Персональный тур в Египет с лучшими местами страны",
|
||||||
picture: TourismIcon7,
|
picture: TourismIcon7,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "c5815b1d-4991-4df2-ae14-8713d7f313b9",
|
quizId: isTestServer ? "740b5ee8-0ab4-441f-8f75-e22b3a61ab36" : "c5815b1d-4991-4df2-ae14-8713d7f313b9",
|
||||||
title: "Тур по местам России",
|
title: "Тур по местам России",
|
||||||
picture: TourismIcon8,
|
picture: TourismIcon8,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "a0a4dce8-43bb-4978-a802-96d384465df4",
|
quizId: isTestServer ? "5e0d7397-6e05-4f81-b24f-a625c2da12d3" : "a0a4dce8-43bb-4978-a802-96d384465df4",
|
||||||
title: "Подберём для вас тур с самыми красивыми местами мира",
|
title: "Подберём для вас тур с самыми красивыми местами мира",
|
||||||
picture: TourismIcon9,
|
picture: TourismIcon9,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
quizId: "0749abc5-a352-41b9-85c3-db7541326f23",
|
quizId: isTestServer ? "65fdf9a0-d30d-4728-abe3-b9a64c965269" : "0749abc5-a352-41b9-85c3-db7541326f23",
|
||||||
title: "Выберем лучшие туристические места для вас",
|
title: "Выберем лучшие туристические места для вас",
|
||||||
picture: TourismIcon10,
|
picture: TourismIcon10,
|
||||||
},
|
},
|
||||||
|
59
src/ui_kit/InfoPopover.tsx
Normal file
59
src/ui_kit/InfoPopover.tsx
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import { useState, MouseEvent, ReactNode } from "react";
|
||||||
|
import Info from "@icons/Info";
|
||||||
|
|
||||||
|
import { Paper, Popover, SxProps, Typography } from "@mui/material";
|
||||||
|
|
||||||
|
export const InfoPopover = ({
|
||||||
|
blink = false,
|
||||||
|
sx,
|
||||||
|
children = "подсказка"
|
||||||
|
}: {
|
||||||
|
blink?: boolean,
|
||||||
|
sx?: SxProps,
|
||||||
|
children?: ReactNode
|
||||||
|
}) => {
|
||||||
|
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
|
||||||
|
|
||||||
|
const handleClick = (event: MouseEvent<HTMLButtonElement>) => {
|
||||||
|
setAnchorEl(event.currentTarget);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setAnchorEl(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const open = Boolean(anchorEl);
|
||||||
|
const id = open ? "simple-popover" : undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Info
|
||||||
|
className={blink ? "blink" : ""}
|
||||||
|
onClick={handleClick}
|
||||||
|
sx={{p:0, height: "20px", ...sx}}
|
||||||
|
/>
|
||||||
|
<Popover
|
||||||
|
id={id}
|
||||||
|
open={open}
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
onClose={handleClose}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: "bottom",
|
||||||
|
horizontal: "left",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Paper
|
||||||
|
sx={{
|
||||||
|
p: "20px",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
flexDirection: "column",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Paper>
|
||||||
|
</Popover>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -31,6 +31,7 @@ export default function MenuItem({
|
|||||||
px: 0,
|
px: 0,
|
||||||
pt: "5px",
|
pt: "5px",
|
||||||
pb: "3px",
|
pb: "3px",
|
||||||
|
whiteSpace: "break-spaces"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ListItemButton
|
<ListItemButton
|
||||||
|
@ -4,7 +4,7 @@ import PencilCircleIcon from "@icons/PencilCircleIcon";
|
|||||||
import PuzzlePieceIcon from "@icons/PuzzlePieceIcon";
|
import PuzzlePieceIcon from "@icons/PuzzlePieceIcon";
|
||||||
import TagIcon from "@icons/TagIcon";
|
import TagIcon from "@icons/TagIcon";
|
||||||
import { quizSetupSteps } from "@model/quizSettings";
|
import { quizSetupSteps } from "@model/quizSettings";
|
||||||
import { Box, IconButton, List, Typography, useTheme } from "@mui/material";
|
import { Box, IconButton, List, Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||||
import { useQuizStore } from "@root/quizes/store";
|
import { useQuizStore } from "@root/quizes/store";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import MenuItem from "../MenuItem";
|
import MenuItem from "../MenuItem";
|
||||||
@ -12,6 +12,7 @@ import { useCurrentQuiz } from "@root/quizes/hooks";
|
|||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
import { setCurrentStep } from "@root/quizes/actions";
|
import { setCurrentStep } from "@root/quizes/actions";
|
||||||
import { setTryShowAmoTokenExpiredDialog, updateNextStep } from "@root/uiTools/actions";
|
import { setTryShowAmoTokenExpiredDialog, updateNextStep } from "@root/uiTools/actions";
|
||||||
|
import AiPersonalizationIcon from "../../assets/icons/AiPersonalizationIcon";
|
||||||
|
|
||||||
const quizSettingsMenuItems = [
|
const quizSettingsMenuItems = [
|
||||||
[TagIcon, "Дополнения"],
|
[TagIcon, "Дополнения"],
|
||||||
@ -31,6 +32,7 @@ export default function Sidebar({ changePage, disableCollapse }: SidebarProps) {
|
|||||||
const currentStep = useQuizStore((state) => state.currentStep);
|
const currentStep = useQuizStore((state) => state.currentStep);
|
||||||
const quiz = useCurrentQuiz();
|
const quiz = useCurrentQuiz();
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
|
const isMobile = useMediaQuery(theme.breakpoints.down(650));
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const changeMenuItem = (index: number) => {
|
const changeMenuItem = (index: number) => {
|
||||||
@ -47,17 +49,18 @@ export default function Sidebar({ changePage, disableCollapse }: SidebarProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
|
id="Sidebar"
|
||||||
sx={{
|
sx={{
|
||||||
backgroundColor: theme.palette.lightPurple.main,
|
backgroundColor: theme.palette.lightPurple.main,
|
||||||
minWidth: isMenuCollapsed ? "80px" : "230px",
|
minWidth: isMenuCollapsed ? "80px" : "230px",
|
||||||
width: isMenuCollapsed ? "80px" : "230px",
|
width: isMenuCollapsed ? "80px" : "230px",
|
||||||
height: "calc(100vh - 80px)",
|
height: isMobile ? "100%" : "calc(100vh - 80px)",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
py: "19px",
|
py: "19px",
|
||||||
transitionProperty: "width, min-width",
|
transitionProperty: "width, min-width",
|
||||||
transitionDuration: "200ms",
|
transitionDuration: "200ms",
|
||||||
overflow: "hidden",
|
overflow: "auto",
|
||||||
whiteSpace: "nowrap",
|
whiteSpace: "nowrap",
|
||||||
boxSizing: "border-box",
|
boxSizing: "border-box",
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
@ -71,6 +74,7 @@ export default function Sidebar({ changePage, disableCollapse }: SidebarProps) {
|
|||||||
mb: isMenuCollapsed ? "5px" : undefined,
|
mb: isMenuCollapsed ? "5px" : undefined,
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: isMenuCollapsed ? "center" : undefined,
|
justifyContent: isMenuCollapsed ? "center" : undefined,
|
||||||
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!isMenuCollapsed && (
|
{!isMenuCollapsed && (
|
||||||
@ -99,7 +103,7 @@ export default function Sidebar({ changePage, disableCollapse }: SidebarProps) {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<List disablePadding>
|
<List disablePadding id="momobibilele">
|
||||||
{quizSetupSteps.map((menuItem, index) => {
|
{quizSetupSteps.map((menuItem, index) => {
|
||||||
const Icon = menuItem.sidebarIcon;
|
const Icon = menuItem.sidebarIcon;
|
||||||
|
|
||||||
@ -168,6 +172,20 @@ export default function Sidebar({ changePage, disableCollapse }: SidebarProps) {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
navigate("/personalization-ai");
|
||||||
|
setCurrentStep(17);
|
||||||
|
setTryShowAmoTokenExpiredDialog(true);
|
||||||
|
}}
|
||||||
|
text={"Персонализация вопросов с помощью AI"}
|
||||||
|
isCollapsed={isMenuCollapsed}
|
||||||
|
isActive={pathname.startsWith("/personalization-ai")}
|
||||||
|
disabled={pathname.startsWith("/personalization-ai") ? false : quiz === undefined ? true : quiz?.config.type === null}
|
||||||
|
icon={
|
||||||
|
<AiPersonalizationIcon />
|
||||||
|
}
|
||||||
|
/>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate("/integrations");
|
navigate("/integrations");
|
||||||
|
@ -22,7 +22,7 @@ export const SidebarModal = ({
|
|||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
sx={{
|
sx={{
|
||||||
outline: "none",
|
outline: "none",
|
||||||
overflow: "hidden",
|
overflow: "auto",
|
||||||
maxWidth: "230px",
|
maxWidth: "230px",
|
||||||
maxHeight: "400px",
|
maxHeight: "400px",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
|
@ -12,6 +12,7 @@ export default function TooltipClickInfo({ title }: { title: string }) {
|
|||||||
const handleTooltipOpen = () => {
|
const handleTooltipOpen = () => {
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ClickAwayListener onClickAway={handleTooltipClose}>
|
<ClickAwayListener onClickAway={handleTooltipClose}>
|
||||||
@ -19,14 +20,21 @@ export default function TooltipClickInfo({ title }: { title: string }) {
|
|||||||
<Tooltip
|
<Tooltip
|
||||||
PopperProps={{
|
PopperProps={{
|
||||||
disablePortal: true,
|
disablePortal: true,
|
||||||
|
sx: {
|
||||||
|
"& .MuiTooltip-tooltip": {
|
||||||
|
fontSize: "14px",
|
||||||
|
padding: "12px",
|
||||||
|
maxWidth: "400px",
|
||||||
|
whiteSpace: "pre-line"
|
||||||
|
}
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
placement="top"
|
placement="top"
|
||||||
onClose={handleTooltipClose}
|
onClose={handleTooltipClose}
|
||||||
open={open}
|
open={open}
|
||||||
disableFocusListener
|
|
||||||
disableHoverListener
|
|
||||||
disableTouchListener
|
|
||||||
title={title}
|
title={title}
|
||||||
|
onMouseEnter={handleTooltipOpen}
|
||||||
|
onMouseLeave={handleTooltipClose}
|
||||||
>
|
>
|
||||||
<IconButton onClick={handleTooltipOpen}>
|
<IconButton onClick={handleTooltipOpen}>
|
||||||
<InfoIcon />
|
<InfoIcon />
|
||||||
|
@ -6,6 +6,7 @@ import { useEffect } from "react";
|
|||||||
import { redirect, useNavigate, useSearchParams } from "react-router-dom";
|
import { redirect, useNavigate, useSearchParams } from "react-router-dom";
|
||||||
import { calcTimeOfReadyPayCart, cancelPayCartProcess, startPayCartProcess, useNotEnoughMoneyAmount } from "@/stores/notEnoughMoneyAmount";
|
import { calcTimeOfReadyPayCart, cancelPayCartProcess, startPayCartProcess, useNotEnoughMoneyAmount } from "@/stores/notEnoughMoneyAmount";
|
||||||
import { startCC } from "@/stores/cc";
|
import { startCC } from "@/stores/cc";
|
||||||
|
import { setEditQuizId, setCurrentStep } from "@root/quizes/actions";
|
||||||
|
|
||||||
export const useAfterPay = () => {
|
export const useAfterPay = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -17,7 +18,19 @@ export const useAfterPay = () => {
|
|||||||
const purpose = searchParams.get("purpose");
|
const purpose = searchParams.get("purpose");
|
||||||
const paymentUserId = searchParams.get("userid");
|
const paymentUserId = searchParams.get("userid");
|
||||||
const currentCC = searchParams.get("cc");
|
const currentCC = searchParams.get("cc");
|
||||||
|
const wayback = searchParams.get("wayback");
|
||||||
|
|
||||||
|
// Обработка wayback параметра
|
||||||
|
useEffect(() => {
|
||||||
|
if (wayback) {
|
||||||
|
const quizId = wayback.split("_")[1];
|
||||||
|
if (quizId) {
|
||||||
|
setEditQuizId(Number(quizId));
|
||||||
|
setCurrentStep(17); // Шаг для персонализации AI
|
||||||
|
navigate("/personalization-ai");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [wayback, navigate]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
//Звёзды сошлись, будем оплачивать корзину
|
//Звёзды сошлись, будем оплачивать корзину
|
||||||
@ -25,7 +38,7 @@ export const useAfterPay = () => {
|
|||||||
|
|
||||||
if (purpose === "paycart") {
|
if (purpose === "paycart") {
|
||||||
setSearchParams({}, { replace: true });
|
setSearchParams({}, { replace: true });
|
||||||
if (currentCC) { startCC() }
|
if (currentCC) startCC()
|
||||||
(async () => {
|
(async () => {
|
||||||
|
|
||||||
//Проверяем можем ли мы оплатить корзину здесь и сейчас
|
//Проверяем можем ли мы оплатить корзину здесь и сейчас
|
||||||
|
10
src/utils/hooks/useDiscounts.ts
Normal file
10
src/utils/hooks/useDiscounts.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import useSWR from 'swr';
|
||||||
|
import { getDiscounts } from '@api/discounts';
|
||||||
|
import type { Discount } from '@model/discounts';
|
||||||
|
|
||||||
|
export const useDiscounts = (userId: string | null) => {
|
||||||
|
return useSWR<Discount[]>(
|
||||||
|
userId ? `discounts/${userId}` : null,
|
||||||
|
() => getDiscounts(userId!).then(([data]) => data)
|
||||||
|
);
|
||||||
|
};
|
@ -11,3 +11,6 @@ export function useDomainDefine(): { isTestServer: boolean } {
|
|||||||
|
|
||||||
return { isTestServer };
|
return { isTestServer };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const host = window.location.hostname;
|
||||||
|
export const isTestServer = host.includes("s");
|
19
src/utils/hooks/useTariffs.ts
Normal file
19
src/utils/hooks/useTariffs.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import useSWR from 'swr';
|
||||||
|
import { getTariffs } from '@/api/tariff';
|
||||||
|
import type { GetTariffsResponse, Tariff } from '@frontend/kitui';
|
||||||
|
|
||||||
|
export const useTariffs = () => {
|
||||||
|
const { data, error, isLoading } = useSWR<Tariff[]>('tariffs', async () => {
|
||||||
|
const [response] = await getTariffs();
|
||||||
|
if (response?.tariffs) {
|
||||||
|
return response.tariffs;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
error,
|
||||||
|
isLoading
|
||||||
|
};
|
||||||
|
};
|
7
src/utils/hooks/useUser.ts
Normal file
7
src/utils/hooks/useUser.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import useSWR from 'swr';
|
||||||
|
import { getUser } from '@api/user';
|
||||||
|
import type { User } from '@frontend/kitui';
|
||||||
|
|
||||||
|
export const useUser = () => {
|
||||||
|
return useSWR<User>('user', getUser);
|
||||||
|
};
|
@ -1,9 +1,7 @@
|
|||||||
import { useEffect, useLayoutEffect, useRef } from "react";
|
import { useEffect, useLayoutEffect, useRef } from "react";
|
||||||
import { createUserAccount, devlog } from "@frontend/kitui";
|
import { createUserAccount, devlog } from "@frontend/kitui";
|
||||||
import { isAxiosError } from "axios";
|
import { isAxiosError } from "axios";
|
||||||
|
|
||||||
import { makeRequest } from "@api/makeRequest";
|
import { makeRequest } from "@api/makeRequest";
|
||||||
|
|
||||||
import type { UserAccount } from "@frontend/kitui";
|
import type { UserAccount } from "@frontend/kitui";
|
||||||
import { setUserAccount } from "@/stores/user";
|
import { setUserAccount } from "@/stores/user";
|
||||||
|
|
||||||
@ -20,10 +18,12 @@ export const useUserAccountFetcher = <T = UserAccount>({
|
|||||||
}) => {
|
}) => {
|
||||||
const onNewUserAccountRef = useRef(onNewUserAccount);
|
const onNewUserAccountRef = useRef(onNewUserAccount);
|
||||||
const onErrorRef = useRef(onError);
|
const onErrorRef = useRef(onError);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
onNewUserAccountRef.current = onNewUserAccount;
|
onNewUserAccountRef.current = onNewUserAccount;
|
||||||
onErrorRef.current = onError;
|
onErrorRef.current = onError;
|
||||||
}, [onError, onNewUserAccount]);
|
}, [onError, onNewUserAccount]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!userId) return;
|
if (!userId) return;
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
|
Loading…
Reference in New Issue
Block a user