Compare commits

..

1 Commits

Author SHA1 Message Date
ea6297e5e2 feat: deploy to prod 2024-01-15 19:26:22 +03:00
786 changed files with 23850 additions and 61292 deletions

@ -1,2 +0,0 @@
1.0.1 Страница заявок корректно отображает мультиответ
1.0.0 Добавлены фичи "мультиответ", "перенос строки в своём ответе", "свой ответ", "плейсхолдер своего ответа"

2
.env

@ -1,2 +0,0 @@
TSC_COMPILE_ON_ERROR=true
ESLINT_NO_DEV_ERRORS=true

@ -1 +0,0 @@
REACT_APP_DOMAIN="https://squiz.pena.digital"

@ -1 +0,0 @@
REACT_APP_DOMAIN=""

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

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

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

2
.gitignore vendored

@ -14,7 +14,9 @@
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*

53
.gitlab-ci.yml Normal file

@ -0,0 +1,53 @@
include:
- project: "devops/pena-continuous-integration"
file: "/templates/docker/build-template.gitlab-ci.yml"
- project: "devops/pena-continuous-integration"
file: "/templates/docker/clean-template.gitlab-ci.yml"
- project: "devops/pena-continuous-integration"
file: "/templates/docker/deploy-template.gitlab-ci.yml"
stages:
- clean
- build
- deploy
clear-old-images:
tags:
- frontbuild
extends: .clean_template
variables:
STAGING_BRANCH: "without-buttons"
PRODUCTION_BRANCH: "main"
image:
name: docker/compose:1.28.0
entrypoint: [""]
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker system prune -af
build-app:
tags:
- frontbuild
extends: .build_template
variables:
BRANCH: "without-buttons"
rules:
- if: "$CI_COMMIT_BRANCH == $BRANCH"
when: on_success
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker build -t $CI_REGISTRY_IMAGE/main:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID --build-arg GITLAB_TOKEN=$GITLAB_TOKEN $CI_PROJECT_DIR
- docker push $CI_REGISTRY_IMAGE/main:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID
deploy-to-staging:
tags:
- front
- prod
extends: .deploy_template
variables:
BRANCH: "without-buttons"
rules:
- if: "$CI_COMMIT_BRANCH == $BRANCH"
script:
- docker login -u $REGISTRY_USER -p $REGISTRY_TOKEN $CI_REGISTRY
- docker-compose -f deployments/main/docker-compose.yaml up -d

1
.npmrc

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

@ -1,4 +0,0 @@
{
"ExpandedNodes": [""],
"PreviewInSolutionExplorer": false
}

Binary file not shown.

Binary file not shown.

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

1
.yarnrc Normal file

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

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

@ -0,0 +1,14 @@
FROM node:20.10-alpine3.18 as build
RUN apk update && rm -rf /var/cache/apk/*
WORKDIR /usr/app
COPY . .
RUN yarn install --ignore-scripts --non-interactive --frozen-lockfile && yarn cache clean
RUN yarn build
FROM nginx:latest as result
WORKDIR /usr/share/nginx/html
COPY --from=build /usr/app/build/ /usr/share/nginx/html
COPY hub.conf /etc/nginx/conf.d/default.conf

@ -1,577 +0,0 @@
<!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>

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

@ -3,23 +3,8 @@ import { defineConfig } from "cypress";
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
viewportWidth: 1280,
viewportHeight: 720,
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',
},
viewportWidth: 1440,
viewportHeight: 900,
supportFile: false,
},
});

@ -1,75 +0,0 @@
/// <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');
});
});
});

@ -61,6 +61,8 @@ describe("Тестирование полей главной страницы",
cy.wait(500);
// cy.get('[data-cy="create-question"]').click();
cy.get(`[data-cy="select-questiontype-images"]`).click();
cy.get(
'[data-cy="checkbox-optional-question"] > .PrivateSwitchBase-input',
@ -113,8 +115,8 @@ describe("Тестирование полей главной страницы",
cy.visit(linkText);
});
cy.origin("https://s.hbpn.link", () => {
// <команды, направленные на https://s.hbpn.link, идут здесь>
cy.origin("https://hbpn.link", () => {
// <команды, направленные на https://hbpn.link, идут здесь>
cy.contains("p", "Заголовок заголовка").should("exist");
cy.wait(100);
cy.contains("p", "У нас тут какой-то текст").should("exist");

@ -22,6 +22,7 @@ describe("Тестирование полей вопросов", () => {
cy.get('[data-cy="setup-questions"]').click();
cy.wait(500);
// cy.get('[data-cy="create-question"]').click();
cy.get(`[data-cy="select-questiontype-images"]`).click();

@ -1,141 +0,0 @@
import "cypress-file-upload";
describe("Тест на появление страницы Результатов", () => {
beforeEach(() => {
cy.visit("http://localhost:3000");
cy.wait(1000);
cy.contains("Регистрация / Войти").click();
const login = "valid_user@exammple.com";
const password = "valid_password";
cy.get("#email").type(login);
cy.get("#password").type(password);
cy.get('button[type="submit"]').click();
});
it("Тест ", () => {
//создаём вопрос и выбираем стартовую страницу
cy.get('[data-cy="create-quiz"]').click();
cy.wait(1000);
cy.get('button[data-cy="create-quiz-card"]').eq(0).click();
cy.wait(1000);
cy.get('button[data-cy="select-quiz-layout-standard"]').click();
cy.get('input[type="checkbox"]').click();
cy.get('[data-cy="setup-questions"]').click();
cy.wait(500);
cy.get(`[data-cy="select-questiontype-images"]`).click();
cy.get(
'[data-cy="checkbox-optional-question"] > .PrivateSwitchBase-input',
).click();
cy.get("#questionTitle").type("Вопрос");
cy.get('[data-cy="quiz-variant-question-answer"]')
.eq(0)
.type("1")
.type("{enter}");
cy.get('[data-cy="quiz-variant-question-answer"]')
.eq(1)
.should("have.value", "")
.type("2");
cy.get('[data-cy="next-step"]').click();
//проверяем все поля на пустоту страницы результата
cy.get(`[data-cy="expand-question"]`).eq(0).click();
cy.wait(200);
cy.get("#after-the-contact-form").click();
cy.get("#heading-result").should("have.value", "");
cy.get("#headline-is-bolder").should("have.value", "");
cy.get("#heading-description").should("have.value", "");
cy.get(`[data-cy="add-button"]`).click();
cy.wait(200);
cy.get("#button-text-result").should("have.value", "");
cy.get("#link-page-result").should("have.value", "");
//перемещаемся на странице тестового просмотра
cy.visit("http://localhost:3000/view");
cy.get(".MuiFormControlLabel-label").contains("1").click();
cy.get("#buttonFurther").click();
cy.wait(1000);
cy.get("#name-input").type("Name");
cy.get("#email-input").type("valid_user@exammple.com");
cy.get("#phone-input").type("+2682585");
cy.get('input[type="checkbox"]').click();
cy.wait(200);
cy.get("#get-results").click();
//проверяем что страница результатов не отобразилась
cy.wait(2000);
cy.get("#find-out-more-button").should("not.exist");
//поселе чего заполняем все поля в результате
cy.visit("http://localhost:3000/edit");
cy.get("#heading-result").type("Результат");
cy.get("#headline-is-bolder").type("Заголовок пожирнее");
cy.get("#heading-description").type("Описание обычного заголовка");
cy.get(`[data-cy="add-button"]`).click();
cy.wait(200);
cy.get("#button-text-result").type("Узнать подробнее");
//переходим на страницу тестового просмотра
cy.visit("http://localhost:3000/view");
cy.get(".MuiFormControlLabel-label").contains("1").click();
cy.get("#buttonFurther").click();
cy.wait(1000);
cy.get("#name-input").type("Name");
cy.get("#email-input").type("valid_user@exammple.com");
cy.get("#phone-input").type("+2682585");
cy.get('input[type="checkbox"]').click();
cy.wait(200);
cy.get("#get-results").click();
cy.wait(2000);
cy.contains("p", "Заголовок пожирнее").should("exist");
//Переключаем показывания результата до формы контактов
cy.visit("http://localhost:3000/edit");
cy.get(`[data-cy="expand-question"]`).eq(0).click();
cy.wait(200);
cy.get("#before-contact-form").click();
cy.wait(2000);
cy.visit("http://localhost:3000/view");
cy.get(".MuiFormControlLabel-label").contains("1").click();
cy.get("#buttonFurther").click();
cy.wait(2000);
cy.contains("p", "Заголовок пожирнее").should("exist");
//тоже самое тольок теперь очищаем поля формы результатов
cy.visit("http://localhost:3000/edit");
cy.get("#heading-result").clear();
cy.get("#headline-is-bolder").clear();
cy.get("#heading-description").clear();
cy.get("#button-text-result").clear();
cy.visit("http://localhost:3000/view");
cy.get(".MuiFormControlLabel-label").contains("1").click();
cy.get("#buttonFurther").click();
cy.wait(2000);
cy.get("#find-out-more-button").should("not.exist");
});
});

@ -1,28 +0,0 @@
/// <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 });
});

@ -1,13 +0,0 @@
// 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,7 @@ services:
squiz:
container_name: squiz
restart: unless-stopped
image: gitea.pena/squiz/frontpanel/main:$GITHUB_RUN_NUMBER
image: $CI_REGISTRY_IMAGE/main:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID
hostname: squiz
tty: true

@ -2,6 +2,12 @@ services:
squiz:
container_name: squiz
restart: unless-stopped
image: gitea.pena/squiz/frontpanel/staging:$GITHUB_RUN_NUMBER
image: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID
networks:
- marketplace_penahub_frontend
hostname: squiz
tty: true
networks:
marketplace_penahub_frontend:
external: true

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

20860
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -6,11 +6,9 @@
"@craco/craco": "^7.0.0",
"@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5",
"@frontend/kitui": "^1.0.108",
"@frontend/squzanswerer": "^1.0.57",
"@frontend/kitui": "^1.0.55",
"@mui/icons-material": "^5.10.14",
"@mui/material": "^5.10.14",
"@mui/x-charts": "^6.19.5",
"@mui/x-date-pickers": "^6.16.1",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^13.0.0",
@ -19,27 +17,25 @@
"@types/file-saver": "^2.0.5",
"@types/jest": "^27.0.1",
"@types/node": "^16.7.13",
"@types/react": "^18.2.55",
"@types/react": "^18.0.0",
"@types/react-dnd": "^3.0.2",
"@types/react-dom": "^18.2.19",
"@types/react-slick": "^0.23.13",
"@types/react-dom": "^18.0.0",
"axios": "^1.5.1",
"country-flag-emoji-polyfill": "^0.1.8",
"cypress-file-upload": "^5.0.8",
"cytoscape": "^3.26.0",
"cytoscape-popper": "^2.0.0",
"date-fns": "^3.0.6",
"emoji-mart": "^5.6.0",
"dayjs": "^1.11.10",
"emoji-mart": "^5.5.2",
"file-saver": "^2.0.5",
"formik": "^2.4.5",
"html-to-image": "^1.11.11",
"immer": "^10.0.3",
"jszip": "^3.10.1",
"moment": "^2.30.1",
"nanoid": "^5.0.3",
"notistack": "^3.0.1",
"react": "^18.2.0",
"react-beautiful-dnd": "^13.1.1",
"react-colorful": "^5.6.1",
"react-cytoscapejs": "^2.0.0",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
@ -48,15 +44,10 @@
"react-error-boundary": "^4.0.11",
"react-image-crop": "^10.1.5",
"react-image-file-resizer": "^0.4.8",
"react-lazily": "^0.9.2",
"react-rnd": "^10.4.1",
"react-router-dom": "^6.6.2",
"react-scripts": "5.0.1",
"react-slick": "^0.29.0",
"slick-carousel": "^1.8.1",
"swiper": "^11.1.4",
"swr": "^2.2.4",
"transliteration": "^2.3.5",
"typescript": "^5.2.2",
"use-debounce": "^9.0.4",
"web-vitals": "^2.1.0",
@ -68,10 +59,9 @@
"build": "craco build",
"test": "craco test",
"eject": "craco eject",
"code:format": "prettier --write --ignore-unknown",
"prepare": "husky install",
"cypress:open": "cypress open",
"cypress:run": "cypress run"
"code:format": "prettier ./src --write --ignore-unknown",
"prepare": "husky install"
},
"browserslist": {
"production": [
@ -86,30 +76,18 @@
]
},
"devDependencies": {
"@emoji-mart/data": "^1.2.1",
"@emoji-mart/data": "^1.1.2",
"@emoji-mart/react": "^1.1.1",
"@types/cytoscape-popper": "^2.0.4",
"@types/react-beautiful-dnd": "^13.1.4",
"@types/react-cytoscapejs": "^1.2.4",
"craco-alias": "^3.0.1",
"cypress": "^13.6.1",
"husky": "^8.0.3",
"lint-staged": "^15.2.0",
"prettier": "^3.1.1"
},
"lint-staged": {
"**/*": "yarn code:format"
},
"prettier": {
"semi": true,
"trailingComma": "es5",
"singleQuote": false,
"printWidth": 120,
"tabWidth": 2,
"useTabs": false,
"endOfLine": "auto",
"bracketSpacing": true,
"arrowParens": "always",
"jsxSingleQuote": false,
"singleAttributePerLine": true
}
}

12
prettierrc Normal file

@ -0,0 +1,12 @@
{
"semi": true,
"trailingComma": "es5",
"singleQuote": false,
"printWidth": 120,
"tabWidth": 2,
"useTabs": false,
"endOfLine": "auto",
"bracketSpacing": true,
"arrowParens": "always",
"jsxSingleQuote": false
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
public/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

9
public/browserconfig.xml Normal file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/mstile-150x150.png"/>
<TileColor>#da532c</TileColor>
</tile>
</msapplication>
</browserconfig>

BIN
public/favicon-16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
public/favicon-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

@ -1,71 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="43.585045"
height="39.566273"
viewBox="0 0 63.268614 57.700814"
fill="none"
version="1.1"
id="svg8"
sodipodi:docname="favicon.svg"
inkscape:export-filename="favicon.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview8"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showguides="true" />
<g
clip-path="url(#clip0_316_1239)"
id="g8"
transform="translate(-1.6513393,-3.2563442)">
<g
id="g9"
transform="translate(0.27803262,-0.00871564)">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M 25.9138,3.31956 C 18.594,2.47167 13.5439,10.3345 8.84663,16.0182 4.72431,21.0062 1.6549,26.6402 1.29838,33.1042 0.919075,39.9813 2.16658,47.1435 6.85174,52.1873 11.6777,57.3827 18.9068,60.6653 25.9138,59.604 32.3391,58.6308 35.1822,51.5749 39.9658,47.1716 45.16,42.3905 54.837,40.1668 54.7027,33.1042 54.5683,26.0308 44.3552,24.6463 39.441,19.5621 34.3509,14.2959 33.1853,4.16185 25.9138,3.31956 Z"
fill="#7e2aea"
id="path1" />
<circle
cx="44.125999"
cy="56.918098"
r="4.0390601"
fill="#7e2aea"
id="circle1" />
<circle
cx="40.086498"
cy="12.1038"
r="1.53869"
fill="#7e2aea"
id="circle2" />
<path
d="m 64.699,31.4509 c -0.4487,-4.3618 -2.5007,-8.4017 -5.7585,-11.3366 -3.2577,-2.9349 -7.4891,-4.5558 -11.8739,-4.5485 -0.6225,3e-4 -1.2446,0.0329 -1.8638,0.0976 -4.3599,0.4578 -8.3958,2.5137 -11.3293,5.7713 -2.9336,3.2577 -4.557,7.4861 -4.5571,11.8699 v 0 25.3412 h 7.6024 v -10.77 c 2.9724,2.0679 6.5079,3.1735 10.1288,3.1676 0.6226,-2e-4 1.2447,-0.0327 1.8639,-0.0975 2.3167,-0.2435 4.5629,-0.9409 6.6101,-2.0525 2.0472,-1.1116 3.8555,-2.6155 5.3215,-4.4259 1.466,-1.8104 2.5611,-3.8918 3.2227,-6.1254 0.6616,-2.2336 0.8767,-4.5757 0.6332,-6.8924 z m -9.764,8.2359 c -0.8351,1.0374 -1.8677,1.8988 -3.038,2.5343 -1.1704,0.6355 -2.4552,1.0325 -3.78,1.168 -0.3553,0.0369 -0.7122,0.0555 -1.0694,0.0558 -2.2991,-0.0021 -4.5293,-0.7858 -6.3243,-2.2224 -1.7951,-1.4366 -3.0484,-3.4408 -3.5544,-5.6836 -0.5059,-2.2428 -0.2343,-4.5909 0.7702,-6.6591 1.0045,-2.0681 2.6822,-3.7333 4.7578,-4.7222 2.0756,-0.9889 4.4257,-1.2429 6.6647,-0.7202 2.2389,0.5228 4.2336,1.7911 5.6567,3.5969 1.4231,1.8058 2.19,4.0417 2.1749,6.3408 -0.0151,2.2991 -0.8114,4.5248 -2.2582,6.3117 z"
fill="#000000"
id="path2" />
</g>
</g>
<defs
id="defs8">
<clipPath
id="clip0_316_1239">
<rect
width="179.509"
height="69.487198"
fill="#ffffff"
id="rect8"
x="0"
y="0" />
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

@ -2,192 +2,19 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Pena Quiz</title>
<meta
name="description"
content="Веб-сервис с инструментами для повышения эффективности маркетологов."
/>
<meta
name="keywords"
content=" Экосистема маркетинговых инструментов,
Инструменты для социальных исследований,
Малый бизнес,
Маркетинговые инструменты,
Социальные исследования,
Бизнес-инструменты,
Исследование рынка,
Аналитика бизнеса,
Онлайн-маркетинг,
Исследования рынка,
Продвижение бизнеса,
Реклама и маркетинг,
Управление проектами,
Автоматизация процессов,
Оптимизация бюджета,
Планирование стратегии,
Оценка эффективности,
Анализ данных,
Улучшение результатов,
Увеличение прибыли,
Повышение конкурентоспособности "
/>
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" sizes="any" />
<!-- 32×32 -->
<link rel="icon" href="%PUBLIC_URL%/favicon.svg" type="image/svg+xml" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/favicon.png" />
<!-- 180×180 -->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta http-equiv="Pragma" content="no-cache" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Rubik:wght@400;500;600&display=swap"
rel="stylesheet"
/>
<!-- Yandex.Metrika counter -->
<script type="text/javascript">
(function (m, e, t, r, i, k, a) {
m[i] =
m[i] ||
function () {
(m[i].a = m[i].a || []).push(arguments);
};
m[i].l = 1 * new Date();
for (var j = 0; j < document.scripts.length; j++) {
if (document.scripts[j].src === r) {
return;
}
}
(k = e.createElement(t)),
(a = e.getElementsByTagName(t)[0]),
(k.async = 1),
(k.src = r),
a.parentNode.insertBefore(k, a);
})(
window,
document,
"script",
"https://mc.yandex.ru/metrika/tag.js",
"ym",
);
const domain = location.hostname;
if (domain === "quiz.pena.digital") {
ym(96979576, "init", {
clickmap: true,
trackLinks: true,
accurateTrackBounce: true,
webvisor: true,
});
// <!-- Top.Mail.Ru counter -->
var _tmr = window._tmr || (window._tmr = []);
_tmr.push({
id: "3513005",
type: "pageView",
start: new Date().getTime(),
});
(function (d, w, id) {
if (d.getElementById(id)) return;
var ts = d.createElement("script");
ts.type = "text/javascript";
ts.async = true;
ts.id = id;
ts.src = "https://top-fwz1.mail.ru/js/code.js";
var f = function () {
var s = d.getElementsByTagName("script")[0];
s.parentNode.insertBefore(ts, s);
};
if (w.opera == "[object Opera]") {
d.addEventListener("DOMContentLoaded", f, false);
} else {
f();
}
})(document, window, "tmr-code");
// <!-- /Top.Mail.Ru counter -->
}
if (domain === "squiz.pena.digital") {
ym(96979625, "init", {
clickmap: true,
trackLinks: true,
accurateTrackBounce: true,
webvisor: true,
});
}
if (domain === "penaquiz.online" || domain === "penaquiz.ru") {
ym(97241101, "init", {
clickmap: true,
trackLinks: true,
accurateTrackBounce: true,
webvisor: true,
});
}
</script>
<noscript>
<div>
<img
src="https://mc.yandex.ru/watch/96979576"
style="position: absolute; left: -9999px"
alt=""
/>
</div>
<div>
<img
src="https://mc.yandex.ru/watch/96979625"
style="position: absolute; left: -9999px"
alt=""
/>
</div>
<div>
<img
src="https://mc.yandex.ru/watch/97241101"
style="position: absolute; left: -9999px"
alt=""
/>
</div>
<div>
<img
src="https://top-fwz1.mail.ru/counter?id=3513005;js=na"
style="position: absolute; left: -9999px"
alt="Top.Mail.Ru"
/>
</div>
</noscript>
<!-- /Yandex.Metrika counter -->
<script>
let params = new URLSearchParams(document.location.search);
console.log(params.get("debug"))
if (params.get("debug")) {
console.log(
"mhgfhdhfjhffhfhjfghjgf"
)
let scriptTag = document.createElement('script');
scriptTag.setAttribute('src', "https://markknol.github.io/console-log-viewer/console-log-viewer.js");
scriptTag.setAttribute('async', '');
document.getElementsByTagName("head")[0].append(scriptTag);
}
</script>
<script>
//Ждём время отображения сайта и оповещаем при выходе за рамки лимита.
const timeout = 25;
window.LoadingObserver = true;
setTimeout(() => {
if (window.LoadingObserver) fetch({
url: "https://squiz.pena.digital/heruvym/v1.0.0/create",
method: "POST",
body: JSON.stringify({ Title: "Error", Message: `Реакт приложение не отобразилось за ${timeout} секунд`, system: true })
});
}, timeout * 1000)
</script>
<title>Pena Quiz</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>

23
public/manifest.json Normal file → Executable file

@ -1,10 +1,23 @@
{
"short_name": "PenaHub",
"name": "Pena Hub",
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{ "src": "/favicon192.png", "type": "image/png", "sizes": "192x192" },
{ "src": "/favicon512.png", "type": "image/png", "sizes": "512x512" }
],
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "android-chrome-192x192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "android-chrome-512x512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",

BIN
public/mstile-150x150.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

@ -0,0 +1,48 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="576.000000pt" height="576.000000pt" viewBox="0 0 576.000000 576.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.14, written by Peter Selinger 2001-2017
</metadata>
<g transform="translate(0.000000,576.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M1980 5475 c-206 -46 -387 -146 -578 -320 -83 -75 -199 -195 -272
-281 -48 -56 -136 -162 -155 -186 -39 -50 -211 -264 -215 -268 -4 -4 -155
-192 -188 -235 -19 -25 -131 -185 -164 -235 -46 -70 -131 -221 -161 -285 -37
-77 -75 -163 -85 -190 -6 -16 -17 -43 -23 -60 -7 -16 -13 -32 -14 -35 -2 -11
-27 -86 -32 -100 -3 -8 -8 -24 -10 -34 -2 -10 -12 -53 -23 -95 -10 -42 -21
-94 -25 -116 -3 -23 -8 -52 -10 -65 -28 -137 -27 -633 0 -798 3 -15 7 -43 10
-62 3 -19 8 -53 11 -75 7 -44 62 -265 76 -305 3 -8 16 -44 28 -80 107 -313
338 -619 641 -850 71 -55 243 -170 253 -170 3 0 28 -14 56 -30 28 -17 57 -30
65 -30 8 0 15 -4 15 -10 0 -5 9 -10 20 -10 11 0 20 -4 20 -9 0 -5 10 -11 23
-14 12 -3 65 -22 117 -42 136 -53 311 -95 465 -112 98 -11 321 -5 411 11 94
16 212 54 270 87 20 11 36 14 39 9 7 -12 684 -14 686 -2 1 4 2 169 2 368 l2
361 32 40 c87 106 95 114 117 107 12 -4 23 -10 26 -14 6 -8 147 -68 230 -98
323 -115 754 -107 1080 20 102 40 294 138 310 159 3 3 15 12 28 18 25 13 25
14 118 90 120 97 289 289 335 380 8 14 16 28 19 31 25 25 117 225 148 320 25
78 49 173 57 220 2 19 9 62 14 95 9 58 7 350 -3 407 -2 16 -7 47 -11 70 -21
133 -75 291 -151 443 -114 230 -275 416 -509 590 -84 62 -342 193 -359 182 -1
-1 -9 3 -17 10 -8 6 -41 19 -74 28 -185 51 -307 68 -485 67 -64 0 -131 -3
-150 -5 -19 -3 -57 -9 -85 -12 -39 -6 -225 -51 -280 -69 -43 -13 -187 -78
-231 -102 l-48 -27 -22 26 c-46 54 -154 244 -312 547 -94 181 -107 205 -114
208 -5 2 -8 8 -8 13 0 10 -79 140 -90 149 -4 3 -11 12 -16 21 -35 62 -167 197
-241 247 -72 48 -156 87 -208 98 -11 2 -31 7 -45 10 -55 14 -246 13 -310 -1z
m2295 -1799 c49 -5 125 -22 155 -34 8 -3 17 -6 20 -7 40 -14 157 -69 184 -87
202 -132 334 -305 395 -518 12 -41 26 -86 30 -100 8 -28 7 -276 -2 -325 -31
-171 -136 -372 -254 -488 -118 -116 -282 -209 -428 -243 -117 -27 -345 -37
-345 -15 0 5 44 35 98 66 96 58 322 203 332 214 3 3 28 24 55 45 75 59 176
165 220 231 40 61 88 174 100 235 9 46 10 187 2 225 -16 76 -20 90 -39 134
-42 94 -123 196 -223 281 -36 31 -184 134 -205 143 -8 4 -28 16 -45 26 -32 21
-153 88 -275 154 -41 22 -77 42 -79 44 -9 7 6 12 54 18 28 4 51 8 52 9 4 3
152 -3 198 -8z"/>
<path d="M3470 4818 c-28 -10 -80 -62 -81 -81 0 -7 -3 -24 -5 -39 -7 -40 19
-91 59 -122 28 -22 45 -27 79 -25 80 5 129 51 135 128 5 52 -18 98 -61 126
-30 20 -91 26 -126 13z"/>
<path d="M3759 967 c-149 -63 -230 -177 -232 -327 -2 -96 2 -114 36 -180 138
-265 520 -262 650 4 34 69 48 162 34 226 -28 127 -97 216 -207 267 -81 38
-204 43 -281 10z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

19
public/site.webmanifest Normal file

@ -0,0 +1,19 @@
{
"name": "",
"short_name": "",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

@ -1,43 +1,104 @@
import { clearAuthToken, getMessageFromFetchError, UserAccount, useUserFetcher } from "@frontend/kitui";
import type { OriginalUserAccount } from "@root/user";
import { clearUserData, setCustomerAccount, setUser, setUserAccount, useUserStore } from "@root/user";
import ContactFormModal from "@ui_kit/ContactForm";
import FloatingSupportChat from "@ui_kit/FloatingSupportChat";
import PrivateRoute from "@ui_kit/PrivateRoute";
import { useAfterPay } from "@utils/hooks/useAutoPay";
import { useUserAccountFetcher } from "@utils/hooks/useUserAccountFetcher";
import { enqueueSnackbar } from "notistack";
import type { SuspenseProps } from "react";
import { lazy, Suspense } from "react";
import { lazily } from "react-lazily";
import { Navigate, Route, Routes, useLocation, useNavigate } from "react-router-dom";
import { useAmoAccount } from "./api/integration";
import ListPageDummy from "./components/Dummys/pageDummys/listPageDummy";
import "./index.css";
import OutdatedLink from "./pages/auth/OutdatedLink";
import RecoverPassword from "./pages/auth/RecoverPassword";
import { Restore } from "./pages/auth/Restore";
import ImageCrop from "@ui_kit/Modal/ImageCrop";
import dayjs from "dayjs";
import "dayjs/locale/ru";
import SigninDialog from "./pages/auth/Signin";
import SignupDialog from "./pages/auth/Signup";
import { InfoPrivilege } from "./pages/InfoPrivilege";
import AmoTokenExpiredDialog from "./pages/IntegrationsPage/IntegrationsModal/Amo/AmoTokenExpiredDialog";
import { ViewPage } from "./pages/ViewPublicationPage";
import { DesignPage } from "./pages/DesignPage/DesignPage";
import {
Route,
Routes,
useLocation,
useNavigate,
Navigate,
} from "react-router-dom";
import "./index.css";
import ContactFormPage from "./pages/ContactFormPage/ContactFormPage";
import InstallQuiz from "./pages/InstallQuiz/InstallQuiz";
import Landing from "./pages/Landing/Landing";
import QuestionsPage from "./pages/Questions/QuestionsPage";
import { Result } from "./pages/ResultPage/Result";
import { ResultSettings } from "./pages/ResultPage/ResultSettings";
import MyQuizzesFull from "./pages/createQuize/MyQuizzesFull";
import Main from "./pages/main";
import { ErrorBoundary } from "react-error-boundary";
import EditPage from "./pages/startPage/EditPage";
import { Tariffs } from "./pages/Tariffs/Tariffs";
import {
clearAuthToken,
getMessageFromFetchError,
useUserFetcher,
UserAccount,
makeRequest,
devlog,
createUserAccount,
} from "@frontend/kitui";
import {
clearUserData,
setUser,
setUserAccount,
useUserStore,
} from "@root/user";
import { enqueueSnackbar } from "notistack";
import PrivateRoute from "@ui_kit/PrivateRoute";
const MyQuizzesFull = lazy(() => import("./pages/createQuize/MyQuizzesFull"));
const QuizGallery = lazy(() => import("./pages/createQuize/QuizGallery"));
const ViewPage = lazy(() => import("./pages/ViewPublicationPage"));
const Analytics = lazy(() => import("./pages/Analytics/Analytics"));
const EditPage = lazy(() => import("./pages/startPage/EditPage"));
const { Tariffs } = lazily(() => import("./pages/Tariffs/Tariffs"));
const { DesignPage } = lazily(() => import("./pages/DesignPage/DesignPage"));
const { IntegrationsPage } = lazily(() => import("./pages/IntegrationsPage/IntegrationsPage"));
const { QuizAnswersPage } = lazily(() => import("./pages/QuizAnswersPage/QuizAnswersPage"));
const ChatImageNewWindow = lazy(() => import("@ui_kit/FloatingSupportChat/ChatImageNewWindow"));
const PersonalizationAI = lazy(() => import("./pages/PersonalizationAI/PersonalizationAI"));
let params = new URLSearchParams(document.location.search);
const isTest = Boolean(params.get("test"))
import { Restore } from "./pages/startPage/Restore";
import { isAxiosError } from "axios";
import { useEffect, useLayoutEffect, useRef } from "react";
export function useUserAccountFetcher({
onError,
onNewUserAccount,
url,
userId,
}: {
url: string;
userId: string | null;
onNewUserAccount: (response: UserAccount) => void;
onError?: (error: any) => void;
}) {
const onNewUserAccountRef = useRef(onNewUserAccount);
const onErrorRef = useRef(onError);
useLayoutEffect(() => {
onNewUserAccountRef.current = onNewUserAccount;
onErrorRef.current = onError;
}, [onError, onNewUserAccount]);
useEffect(() => {
if (!userId) return;
const controller = new AbortController();
makeRequest<never, UserAccount>({
url,
contentType: true,
method: "GET",
useToken: true,
withCredentials: false,
signal: controller.signal,
})
.then((result) => {
devlog("User account", result);
onNewUserAccountRef.current(result);
})
.catch((error) => {
devlog("Error fetching user account", error);
if (isAxiosError(error) && error.response?.status === 404) {
createUserAccount(controller.signal, url.replace("get", "create"))
.then((result) => {
devlog("Created user account", result);
onNewUserAccountRef.current(result);
})
.catch((error) => {
devlog("Error creating user account", error);
onErrorRef.current?.(error);
});
} else {
onErrorRef.current?.(error);
}
});
return () => controller.abort();
}, [url, userId]);
}
dayjs.locale("ru");
const routeslink = [
{
@ -54,34 +115,15 @@ const routeslink = [
sidebar: true,
footer: true,
},
{
path: "/integrations",
page: IntegrationsPage,
header: true,
sidebar: true,
footer: true,
},
{
path: "/personalization-ai",
page: PersonalizationAI,
header: true,
sidebar: true,
},
] as const;
const LazyLoading = ({ children, fallback }: SuspenseProps) => (
<Suspense fallback={fallback ?? <></>}>{children}</Suspense>
);
export default function App() {
window.LoadingObserver = false;
const userId = useUserStore((state) => state.userId);
const location = useLocation();
const navigate = useNavigate();
const { data: amoAccount } = useAmoAccount();
useUserFetcher({
url: `${process.env.REACT_APP_DOMAIN}/user/${userId}`,
url: `https://hub.pena.digital/user/${userId}`,
userId,
onNewUser: setUser,
onError: (error) => {
@ -94,23 +136,8 @@ export default function App() {
},
});
useUserAccountFetcher<UserAccount>({
url: `${process.env.REACT_APP_DOMAIN}/customer/v1.0.1/account`,
userId,
onNewUserAccount: setCustomerAccount,
onError: (error) => {
const errorMessage = getMessageFromFetchError(error);
if (errorMessage) {
enqueueSnackbar(errorMessage);
clearUserData();
clearAuthToken();
navigate("/signin");
}
},
});
useUserAccountFetcher<OriginalUserAccount>({
url: `${process.env.REACT_APP_DOMAIN}/squiz/account/get`,
useUserAccountFetcher({
url: "https://squiz.pena.digital/squiz/account/get",
userId,
onNewUserAccount: setUserAccount,
onError: (error) => {
@ -124,8 +151,6 @@ export default function App() {
},
});
useAfterPay();
if (location.state?.redirectTo)
return (
<Navigate
@ -137,148 +162,53 @@ export default function App() {
return (
<>
{amoAccount && <AmoTokenExpiredDialog isAmoTokenExpired={amoAccount.stale} />}
<ContactFormModal />
{!isTest && <FloatingSupportChat />}
{location.state?.backgroundLocation && (
<Routes>
<Route
path="/signin"
element={<SigninDialog />}
/>
<Route
path="/signup"
element={<SignupDialog />}
/>
<Route
path="/recover"
element={<Restore />}
/>
<Route
path="/changepwd"
element={<RecoverPassword />}
/>
<Route
path="/changepwd/expired"
element={<OutdatedLink />}
/>
<Route path="/signin" element={<SigninDialog />} />
<Route path="/signup" element={<SignupDialog />} />
<Route path="/restore" element={<Restore />} />
</Routes>
)}
<Routes location={location.state?.backgroundLocation || location}>
<Route
path="/"
element={<Landing />}
/>
<Route
<Route path="/" element={<Landing />} />
{/* <Route
path="/signin"
element={
<Navigate
to="/"
replace
state={{ redirectTo: "/signin" }}
/>
<Navigate to="/" replace state={{ redirectTo: "/signin" }} />
}
/>
<Route
path="/signup"
element={
<Navigate
to="/"
replace
state={{ redirectTo: "/signup" }}
/>
<Navigate to="/" replace state={{ redirectTo: "/signup" }} />
}
/>
<Route
path="/recover"
path="/restore"
element={
<Navigate
to="/"
replace
state={{ redirectTo: "/recover" }}
/>
<Navigate to="/" replace state={{ redirectTo: "/restore" }} />
}
/>
<Route
path="/changepwd"
element={
<Navigate
to="/"
replace
state={{
redirectTo: window.location.pathname + window.location.search,
}}
/>
}
/>
<Route
path="/changepwd/expired"
element={
<Navigate
to="/"
replace
state={{ redirectTo: "/changepwd/expired" }}
/>
}
/>
<Route
path="/gallery"
element={<LazyLoading children={<QuizGallery />} />}
/>
<Route
path="/list"
element={
<LazyLoading
children={<MyQuizzesFull />}
fallback={<ListPageDummy />}
/>
}
/>
<Route
path={"/view/:quizId"}
element={<LazyLoading children={<ViewPage />} />}
/>
<Route
path={"/tariffs"}
element={<LazyLoading children={<Tariffs />} />}
/>
<Route
path={"/analytics"}
element={<LazyLoading children={<Analytics />} />}
/>
<Route
path={"/results/:quizId"}
element={<LazyLoading children={<QuizAnswersPage />} />}
/>
<Route
path={"/qaz"}
element={<LazyLoading children={<InfoPrivilege />} />}
/>
<Route
path={"/image/:srcImage"}
element={<ChatImageNewWindow />}
/>
<Route path="/list" element={<MyQuizzesFull />} />
<Route path={"/view"} element={<ViewPage />} />
<Route path={"/tariffs"} element={<Tariffs />} />
<Route element={<PrivateRoute />}>
{routeslink.map((e, i) => (
<Route
key={i}
path={e.path}
element={
<LazyLoading
children={
<Main
Page={e.page}
header={e.header}
sidebar={e.sidebar}
footer={e.footer}
/>
}
<Main
Page={e.page}
header={e.header}
sidebar={e.sidebar}
footer={e.footer}
/>
}
/>
))}
</Route>
</Route> */}
</Routes>
</>
);

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

@ -1,6 +1,4 @@
import { makeRequest } from "@api/makeRequest";
import { parseAxiosError } from "@utils/parse-error";
import { makeRequest } from "@frontend/kitui";
import type {
LoginRequest,
@ -8,24 +6,24 @@ import type {
RegisterRequest,
RegisterResponse,
} from "@frontend/kitui";
import { parseAxiosError } from "../utils/parse-error";
type RecoverResponse = {
message: string;
};
const apiUrl =
process.env.NODE_ENV === "production"
? "/auth"
: "https://squiz.pena.digital/auth";
const API_URL = `${process.env.REACT_APP_DOMAIN}/auth`;
export const register = async (
export async function register(
login: string,
password: string,
phoneNumber: string,
): Promise<[RegisterResponse | null, string?]> => {
): Promise<[RegisterResponse | null, string?]> {
try {
const registerResponse = await makeRequest<
RegisterRequest,
RegisterResponse
>({
url: `${API_URL}/register`,
url: apiUrl + "/register",
body: { login, password, phoneNumber },
useToken: false,
withCredentials: true,
@ -34,19 +32,18 @@ export const register = async (
return [registerResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
console.error(error)
return [null, `Не удалось зарегестрировать аккаунт. ${error}`];
}
};
}
export const login = async (
export async function login(
login: string,
password: string,
): Promise<[LoginResponse | null, string?]> => {
): Promise<[LoginResponse | null, string?]> {
try {
const loginResponse = await makeRequest<LoginRequest, LoginResponse>({
url: `${API_URL}/login`,
url: apiUrl + "/login",
body: { login, password },
useToken: false,
withCredentials: true,
@ -58,13 +55,13 @@ export const login = async (
return [null, `Не удалось войти. ${error}`];
}
};
}
export const logout = async (): Promise<[void | null, string?]> => {
export async function logout(): Promise<[unknown, string?]> {
try {
const logoutResponse = await makeRequest<never, void>({
url: apiUrl + "/logout",
method: "POST",
url: `${API_URL}/logout`,
useToken: true,
withCredentials: true,
});
@ -75,30 +72,4 @@ export const logout = async (): Promise<[void | null, string?]> => {
return [null, `Не удалось выйти. ${error}`];
}
};
export const recover = async (
email: string,
): Promise<[RecoverResponse | null, string?]> => {
try {
const formData = new FormData();
formData.append("email", email);
formData.append(
"RedirectionURL",
`${process.env.REACT_APP_DOMAIN}/changepwd`,
);
const recoverResponse = await makeRequest<FormData, RecoverResponse>({
url: `${process.env.REACT_APP_DOMAIN}/codeword/recover`,
body: formData,
useToken: false,
withCredentials: true,
});
return [recoverResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось восстановить пароль. ${error}`];
}
};
}

@ -1,63 +0,0 @@
import { makeRequest } from "@api/makeRequest";
import { parseAxiosError } from "@utils/parse-error";
import type { UserAccount } from "@frontend/kitui";
const API_URL = `${process.env.REACT_APP_DOMAIN}/customer/v1.0.1/cart`;
const payCart = async (): Promise<[UserAccount | null, string?]> => {
try {
const payCartResponse = await makeRequest<never, UserAccount>({
method: "POST",
url: `${API_URL}/pay`,
useToken: true,
});
return [payCartResponse];
} catch (nativeError) {
const error = parseAxiosError(nativeError);
return [null, `Не удалось оплатить товар из корзины. ${error}`];
}
};
const addCartItem = async (
id: string,
): Promise<[UserAccount | null, string?]> => {
try {
const addedItem = await makeRequest<never, UserAccount>({
method: "PATCH",
url: `${API_URL}?id=${id}`,
});
return [addedItem];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось добавить товар в корзину. ${error}`];
}
};
const deleteCartItem = async (
id: string,
): Promise<[UserAccount | null, string?]> => {
try {
const deletedItem = await makeRequest<never, UserAccount>({
method: "DELETE",
url: `${API_URL}?id=${id}`,
});
return [deletedItem];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось удалить товар из корзины. ${error}`];
}
};
export const cartApi = {
pay: payCart,
add: addCartItem,
delete: deleteCartItem,
};

@ -1,31 +1,19 @@
import { makeRequest } from "@api/makeRequest";
import axios from "axios";
import { parseAxiosError } from "@utils/parse-error";
const domen =
window.location.hostname === "localhost"
? "squiz.pena.digital"
: window.location.hostname;
const API_URL = `${process.env.REACT_APP_DOMAIN}/feedback`;
type SendContactFormBody = {
export function sendContactFormRequest(body: {
contact: string;
whoami: string;
};
export const sendContactFormRequest = async (
body: SendContactFormBody,
): Promise<[unknown | null, string?, number?]> => {
try {
const sendContactFormResponse = await makeRequest<
SendContactFormBody,
unknown
>({
method: "POST",
url: `${API_URL}/callme`,
body,
});
return [sendContactFormResponse];
} catch (nativeError) {
const [error, status] = parseAxiosError(nativeError);
return [null, `Не удалось отправить контакты. ${error}`, status];
}
};
}) {
return axios(`https://${domen}/feedback/callme`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
data: body,
});
}

@ -1,24 +0,0 @@
import { makeRequest } from "@frontend/kitui";
import { parseAxiosError } from "@utils/parse-error";
import type { Discount } from "@model/discounts";
const API_URL = `${process.env.REACT_APP_DOMAIN}/price/discount`;
export const getDiscounts = async (
userId: string,
): Promise<[Discount[] | null, string?]> => {
try {
const { Discounts } = await makeRequest<never, { Discounts: Discount[] }>({
method: "GET",
url: `${API_URL}/user/${userId}`,
});
return [Discounts];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось получить скидки. ${error}`];
}
};

@ -1,380 +0,0 @@
import { QuestionKeys } from "@/pages/IntegrationsPage/IntegrationsModal/Amo/types";
import { makeRequest } from "@api/makeRequest";
import { useToken } from "@frontend/kitui";
import { parseAxiosError } from "@utils/parse-error";
import useSWR from "swr";
export type PaginationRequest = {
page: number;
size: number;
};
const API_URL = `${process.env.REACT_APP_DOMAIN}/squiz/amocrm`;
// получение информации об аккаунте
export type AccountResponse = {
id: number;
accountID: string;
amoID: number;
name: string;
deleted: boolean;
createdAt: string;
subdomain: string;
country: string;
driveURL: string;
stale: boolean;
};
export const getAccount = async (): Promise<[AccountResponse | null, string?]> => {
try {
const response = await makeRequest<void, AccountResponse>({
method: "GET",
url: `${API_URL}/account`,
useToken: true,
});
return [response];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, ""];
// return [null, `Не удалось получить информацию об аккаунте. ${error}`];
}
};
export function useAmoAccount() {
const token = useToken();
return useSWR(token ? "amoAccount" : null, () =>
makeRequest<void, AccountResponse>({
method: "GET",
url: `${API_URL}/account`,
useToken: true,
})
);
}
// подключить Amo
export const connectAmo = async (): Promise<[string | null, string?]> => {
try {
const response = await makeRequest<void, { link: string }>({
method: "POST",
url: `${API_URL}/account`,
useToken: true,
withCredentials: true,
});
return [response.link];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось подключить аккаунт. ${error}`];
}
};
// получение токена
export type TokenPair = {
accessToken: string;
refreshToken: string;
};
export const getTokens = async (): Promise<[TokenPair | null, string?]> => {
try {
const response = await makeRequest<void, TokenPair>({
method: "GET",
url: `${API_URL}/webhook/create`,
useToken: true,
});
return [response];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Failed to get tokens. ${error}`];
}
};
//получение списка тегов
export type Tag = {
ID: number;
AmoID: number;
AccountID: number;
Entity: string;
Name: string;
Color: string;
Deleted: boolean;
CreatedAt: number;
};
export type TagsResponse = {
count: number;
items: Tag[];
};
export const getTags = async ({ page, size }: PaginationRequest): Promise<[TagsResponse | null, string?]> => {
try {
const tagsResponse = await makeRequest<PaginationRequest, TagsResponse>({
method: "GET",
url: `${API_URL}/tags?page=${page}&size=${size}`,
});
return [tagsResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось получить список тегов. ${error}`];
}
};
//получение списка пользователей
export type User = {
id: number;
amoID: number;
name: string;
email: string;
role: number;
group: number;
deleted: boolean;
createdAt: string;
amoUserID: number;
// Subdomain: string;
// AccountID: string;
};
export type UsersResponse = {
count: number;
items: User[];
};
export const getUsers = async ({ page, size }: PaginationRequest): Promise<[UsersResponse | null, string?]> => {
try {
const usersResponse = await makeRequest<PaginationRequest, UsersResponse>({
method: "GET",
url: `${API_URL}/users?page=${page}&size=${size}`,
});
return [usersResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось получить список пользователей. ${error}`];
}
};
//получение списка шагов
export type Step = {
ID: number;
AmoID: number;
PipelineID: number;
AccountID: number;
Name: string;
Color: string;
Deleted: boolean;
CreatedAt: number;
};
export type StepsResponse = {
count: number;
items: Step[];
};
export const getSteps = async ({
page,
size,
pipelineId,
}: PaginationRequest & { pipelineId: number }): Promise<[StepsResponse | null, string?]> => {
try {
const stepsResponse = await makeRequest<PaginationRequest & { pipelineId: number }, StepsResponse>({
method: "GET",
url: `${API_URL}/steps?page=${page}&size=${size}&pipelineID=${pipelineId}`,
});
return [stepsResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось получить список шагов. ${error}`];
}
};
//получение списка воронок
export type Pipeline = {
ID: number;
AmoID: number;
AccountID: number;
Name: string;
IsArchive: boolean;
Deleted: boolean;
CreatedAt: number;
};
export type PipelinesResponse = {
count: number;
items: Pipeline[];
};
export const getPipelines = async ({ page, size }: PaginationRequest): Promise<[PipelinesResponse | null, string?]> => {
try {
const pipelinesResponse = await makeRequest<PaginationRequest, PipelinesResponse>({
method: "GET",
url: `${API_URL}/pipelines?page=${page}&size=${size}`,
});
return [pipelinesResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось получить список воронок. ${error}`];
}
};
//получение настроек интеграции
export type QuestionID = Record<string, number>;
export type IntegrationRules = {
PipelineID: number;
StepID: number;
PerformerID?: number;
FieldsRule: FieldsRule;
TagsToAdd: {
Lead: number[] | null;
Contact: number[] | null;
Company: number[] | null;
Customer: number[] | null;
};
};
export type FieldsRule = Record<Partial<QuestionKeys>, null | [{ QuestionID: QuestionID }]>;
export const getIntegrationRules = async (quizID: string): Promise<[IntegrationRules | null, string?]> => {
try {
const settingsResponse = await makeRequest<void, IntegrationRules>({
method: "GET",
url: `${API_URL}/rules/${quizID}`,
});
return [settingsResponse || null];
} catch (nativeError) {
if (nativeError.response.status === 404) return [null, "first"];
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось получить настройки интеграции. ${error}`];
}
};
//обновление настроек интеграции
export type IntegrationRulesUpdate = {
PerformerID: number;
PipelineID: number;
StepID: number;
Utms: number[];
FieldsRule: {
Lead: { QuestionID: number }[];
Contact: { ContactRuleMap: string }[];
Company: { QuestionID: number }[];
Customer: { QuestionID: number }[];
};
};
export const setIntegrationRules = async (
quizID: string,
settings: IntegrationRulesUpdate
): Promise<[string | null, string?]> => {
try {
const updateResponse = await makeRequest<IntegrationRulesUpdate, string>({
method: "POST",
url: `${API_URL}/rules/${quizID}`,
body: settings,
});
return [updateResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Failed to update integration settings. ${error}`];
}
};
export const updateIntegrationRules = async (
quizID: string,
settings: IntegrationRulesUpdate
): Promise<[string | null, string?]> => {
try {
const updateResponse = await makeRequest<IntegrationRulesUpdate, string>({
method: "PATCH",
url: `${API_URL}/rules/${quizID}`,
body: settings,
});
return [updateResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Failed to update integration settings. ${error}`];
}
};
// Получение кастомных полей
export type CustomField = {
ID: number;
AmoID: number;
Code: string;
AccountID: number;
Name: string;
EntityType: string;
Type: string;
Deleted: boolean;
CreatedAt: number;
};
export type Field = {
ID: number;
AmoID: number;
Code: string;
AccountID: number;
Name: string;
Entity: string;
Type: string;
Deleted: boolean;
CreatedAt: number;
};
export type CustomFieldsResponse = {
count: number;
items: CustomField[];
};
export type FieldsResponse = {
count: number;
items: Field[];
};
export const getCustomFields = async (
pagination: PaginationRequest
): Promise<[CustomFieldsResponse | null, string?]> => {
try {
const customFieldsResponse = await makeRequest<PaginationRequest, CustomFieldsResponse>({
method: "GET",
url: `${API_URL}/fields?page=${pagination.page}&size=${pagination.size}`,
});
return [customFieldsResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось получить список кастомных полей. ${error}`];
}
};
//Отвязать аккаунт амо от публикации
export const removeAmoAccount = async (): Promise<[void | null, string?]> => {
try {
await makeRequest<void>({
method: "DELETE",
url: `${API_URL}/account`,
});
return [null, ""];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось отвязать аккаунт. ${error}`];
}
};
export const getFields = async (pagination: PaginationRequest): Promise<[FieldsResponse | null, string?]> => {
try {
const fieldsResponse = await makeRequest<PaginationRequest, FieldsResponse>({
method: "GET",
url: `${API_URL}/fields?page=${pagination.page}&size=${pagination.size}`,
});
return [fieldsResponse, ""];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось получить список полей. ${error}`];
}
};

@ -1,55 +0,0 @@
import * as KIT from "@frontend/kitui";
import { Method, ResponseType, AxiosError } from "axios";
import { redirect } from "react-router-dom";
import { clearAuthToken } from "@frontend/kitui";
import { cleanAuthTicketData } from "@root/ticket";
import { clearUserData } from "@root/user";
import { clearQuizData } from "@root/quizes/store";
import type { AxiosResponse } from "axios";
import { selectSendingMethod } from "@/ui_kit/FloatingSupportChat/utils";
interface MakeRequest {
method?: Method | undefined;
url: string;
body?: unknown;
useToken?: boolean | undefined;
contentType?: boolean | undefined;
responseType?: ResponseType | undefined;
signal?: AbortSignal | undefined;
withCredentials?: boolean | undefined;
}
type ExtendedAxiosResponse = AxiosResponse & { message: string };
export const makeRequest = async <TRequest = unknown, TResponse = unknown>(
data: MakeRequest,
): Promise<TResponse> => {
try {
const response = await KIT.makeRequest<unknown, TResponse>(data);
return response;
} catch (nativeError) {
const error = nativeError as AxiosError;
// if (window.location.hostname !== 'localhost') selectSendingMethod({
// messageField: `status: ${error.response?.status}. Message ${(error.response?.data as ExtendedAxiosResponse)?.message}`,
// isSnackbar: false,
// systemError: true
// });
if (
error.response?.status === 400 &&
(error.response?.data as ExtendedAxiosResponse)?.message ===
"refreshToken is empty"
) {
cleanAuthTicketData();
clearAuthToken();
clearUserData();
clearQuizData();
redirect("/");
}
throw nativeError;
}
};

@ -1,30 +0,0 @@
import { makeRequest } from "@api/makeRequest";
import { parseAxiosError } from "@utils/parse-error";
type ActivatePromocodeRequest = { codeword: string } | { fastLink: string };
type ActivatePromocodeResponse = { greetings: string };
const API_URL = `${process.env.REACT_APP_DOMAIN}/codeword/promocode`;
export const activatePromocode = async (
promocode: string,
): Promise<[string | null, string?]> => {
try {
const response = await makeRequest<
ActivatePromocodeRequest,
ActivatePromocodeResponse
>({
method: "POST",
url: `${API_URL}/activate`,
body: { codeword: promocode },
contentType: true,
});
return [response.greetings];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Ошибка при активации промокода. ${error}`];
}
};

@ -1,155 +1,85 @@
import { makeRequest } from "@api/makeRequest";
import { replaceSpacesToEmptyLines } from "@utils/replaceSpacesToEmptyLines";
import { parseAxiosError } from "@utils/parse-error";
import type { CreateQuestionRequest } from "model/question/create";
import type { RawQuestion } from "model/question/question";
import type {
import { makeRequest } from "@frontend/kitui";
import { CreateQuestionRequest } from "model/question/create";
import { RawQuestion } from "model/question/question";
import {
GetQuestionListRequest,
GetQuestionListResponse,
} from "@model/question/getList";
import type {
import {
EditQuestionRequest,
EditQuestionResponse,
} from "@model/question/edit";
import type {
import {
DeleteQuestionRequest,
DeleteQuestionResponse,
} from "@model/question/delete";
import type {
import {
CopyQuestionRequest,
CopyQuestionResponse,
} from "@model/question/copy";
import { replaceSpacesToEmptyLines } from "../utils/replaceSpacesToEmptyLines";
const API_URL = `${process.env.REACT_APP_DOMAIN}/squiz`;
const baseUrl =
process.env.NODE_ENV === "production"
? "/squiz"
: "https://squiz.pena.digital/squiz";
export const createQuestion = async (
body: CreateQuestionRequest,
): Promise<[RawQuestion | null, string?]> => {
try {
const createdQuestion = await makeRequest<
CreateQuestionRequest,
RawQuestion
>({
method: "POST",
url: `${API_URL}/question/create`,
body,
});
function createQuestion(body: CreateQuestionRequest) {
return makeRequest<CreateQuestionRequest, RawQuestion>({
url: `${baseUrl}/question/create`,
body,
method: "POST",
});
}
return [createdQuestion];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
async function getQuestionList(body?: Partial<GetQuestionListRequest>) {
if (!body?.quiz_id) return null;
return [null, `Не удалось создать вопрос. ${error}`];
}
};
const response = await makeRequest<
GetQuestionListRequest,
GetQuestionListResponse
>({
url: `${baseUrl}/question/getList`,
body: { ...defaultGetQuestionListBody, ...body },
method: "POST",
});
const clearArrayFromEmptySpaceBlaBlaValue = response.items?.map(
(question) => {
let data = question;
for (let key in question) {
if (question[key] === " ") data[key] = "";
}
return data;
},
);
const getQuestionList = async (
body?: Partial<GetQuestionListRequest>,
): Promise<[RawQuestion[] | null, string?]> => {
try {
if (!body?.quiz_id) return [null, "Квиз не найден"];
return replaceSpacesToEmptyLines(clearArrayFromEmptySpaceBlaBlaValue);
}
const response = await makeRequest<
GetQuestionListRequest,
GetQuestionListResponse
>({
method: "POST",
url: `${API_URL}/question/getList`,
body: { ...defaultGetQuestionListBody, ...body },
});
function editQuestion(body: EditQuestionRequest, signal?: AbortSignal) {
return makeRequest<EditQuestionRequest, EditQuestionResponse>({
url: `${baseUrl}/question/edit`,
body,
method: "PATCH",
signal,
});
}
const clearArrayFromEmptySpaceBlaBlaValue = response.items?.map(
(question) => {
let data = question;
function copyQuestion(questionId: number, quizId: number) {
return makeRequest<CopyQuestionRequest, CopyQuestionResponse>({
url: `${baseUrl}/question/copy`,
body: { id: questionId, quiz_id: quizId },
method: "POST",
});
}
for (let key in question) {
if (question[key as keyof RawQuestion] === " ") {
//@ts-ignore
data[key] = "";
}
}
return data;
},
);
return [
replaceSpacesToEmptyLines(clearArrayFromEmptySpaceBlaBlaValue) ?? null,
];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось получить список вопросов. ${error}`];
}
};
export const editQuestion = async (
body: EditQuestionRequest,
signal?: AbortSignal,
): Promise<[EditQuestionResponse | null, string?]> => {
try {
const editedQuestion = await makeRequest<
EditQuestionRequest,
EditQuestionResponse
>({
method: "PATCH",
url: `${API_URL}/question/edit`,
body,
signal,
});
return [editedQuestion];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось изменить вопрос. ${error}`];
}
};
export const copyQuestion = async (
questionId: number,
quizId: number,
): Promise<[CopyQuestionResponse | null, string?]> => {
try {
const copiedQuestion = await makeRequest<
CopyQuestionRequest,
CopyQuestionResponse
>({
method: "POST",
url: `${API_URL}/question/copy`,
body: { id: questionId, quiz_id: quizId },
});
return [copiedQuestion];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось скопировать вопрос. ${error}`];
}
};
export const deleteQuestion = async (
id: number,
): Promise<[DeleteQuestionResponse | null, string?]> => {
try {
const deletedQuestion = await makeRequest<
DeleteQuestionRequest,
DeleteQuestionResponse
>({
url: `${API_URL}/question/delete`,
body: { id },
method: "DELETE",
});
return [deletedQuestion];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось удалить вопрос. ${error}`];
}
};
function deleteQuestion(id: number) {
return makeRequest<DeleteQuestionRequest, DeleteQuestionResponse>({
url: `${baseUrl}/question/delete`,
body: { id },
method: "DELETE",
});
}
export const questionApi = {
create: createQuestion,

@ -1,197 +1,85 @@
import { makeRequest } from "@api/makeRequest";
import { makeRequest } from "@frontend/kitui";
import { defaultQuizConfig } from "@model/quizSettings";
import { CopyQuizRequest, CopyQuizResponse } from "model/quiz/copy";
import { CreateQuizRequest } from "model/quiz/create";
import { DeleteQuizRequest, DeleteQuizResponse } from "model/quiz/delete";
import { EditQuizRequest, EditQuizResponse } from "model/quiz/edit";
import { GetQuizRequest, GetQuizResponse } from "model/quiz/get";
import { GetQuizListRequest, GetQuizListResponse } from "model/quiz/getList";
import { RawQuiz } from "model/quiz/quiz";
import { parseAxiosError } from "@utils/parse-error";
const baseUrl =
process.env.NODE_ENV === "production"
? "/squiz"
: "https://squiz.pena.digital/squiz";
const imagesUrl =
process.env.NODE_ENV === "production"
? "/squizstorer"
: "https://squiz.pena.digital/squizstorer";
import type { RawQuiz } from "model/quiz/quiz";
import type { CopyQuizRequest, CopyQuizResponse } from "model/quiz/copy";
import type { CreateQuizRequest } from "model/quiz/create";
import type { DeleteQuizRequest, DeleteQuizResponse } from "model/quiz/delete";
import type { EditQuizRequest, EditQuizResponse } from "model/quiz/edit";
import type { GetQuizRequest, GetQuizResponse } from "model/quiz/get";
import { transliterate } from 'transliteration';
import type {
GetQuizListRequest,
GetQuizListResponse,
} from "model/quiz/getList";
function createQuiz(body?: Partial<CreateQuizRequest>) {
return makeRequest<CreateQuizRequest, RawQuiz>({
url: `${baseUrl}/quiz/create`,
body: { ...defaultCreateQuizBody, ...body },
method: "POST",
});
}
type AddedQuizImagesResponse = {
[key: string]: string;
};
async function getQuizList(body?: Partial<GetQuizListRequest>) {
const response = await makeRequest<GetQuizListRequest, GetQuizListResponse>({
url: `${baseUrl}/quiz/getList`,
body: { ...defaultGetQuizListBody, ...body },
method: "POST",
});
const API_URL = `${process.env.REACT_APP_DOMAIN}/squiz`;
const IMAGES_URL = `${process.env.REACT_APP_DOMAIN}/squizstorer/v1.0.0`;
return response.items;
}
export const createQuiz = async (
body?: Partial<CreateQuizRequest>,
): Promise<[RawQuiz | null, string?]> => {
try {
const createdQuiz = await makeRequest<CreateQuizRequest, RawQuiz>({
method: "POST",
url: `${API_URL}/quiz/create`,
body: { ...defaultCreateQuizBody, ...body },
});
function getQuiz(body?: Partial<GetQuizRequest>) {
return makeRequest<GetQuizRequest, GetQuizResponse>({
url: `${baseUrl}/quiz/get`,
body: { ...defaultGetQuizBody, ...body },
method: "GET",
});
}
return [createdQuiz];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
async function editQuiz(body: EditQuizRequest, signal?: AbortSignal) {
return makeRequest<EditQuizRequest, EditQuizResponse>({
url: `${baseUrl}/quiz/edit`,
body,
method: "PATCH",
signal,
});
}
return [null, `Не удалось создать квиз. ${error}`];
}
};
function copyQuiz(id: number) {
return makeRequest<CopyQuizRequest, CopyQuizResponse>({
url: `${baseUrl}/quiz/copy`,
body: { id },
method: "POST",
});
}
export const getQuizList = async (
body?: Partial<CreateQuizRequest>,
): Promise<[RawQuiz[] | null, string?]> => {
try {
const { items } = await makeRequest<
GetQuizListRequest,
GetQuizListResponse
>({
method: "POST",
url: `${API_URL}/quiz/getList`,
body: { ...defaultGetQuizListBody, ...body },
});
function deleteQuiz(id: number) {
return makeRequest<DeleteQuizRequest, DeleteQuizResponse>({
url: `${baseUrl}/quiz/delete`,
body: { id },
method: "DELETE",
});
}
return [items];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
function addQuizImages(quizId: number, image: Blob) {
const formData = new FormData();
return [null, `Не удалось получить список квизов. ${error}`];
}
};
formData.append("quiz", quizId.toString());
formData.append("image", image);
export const getQuiz = async (
body?: Partial<GetQuizRequest>,
): Promise<[GetQuizResponse | null, string?]> => {
try {
const quiz = await makeRequest<GetQuizRequest, GetQuizResponse>({
method: "GET",
url: `${API_URL}/quiz/get`,
body: { ...defaultGetQuizBody, ...body },
});
return [quiz];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось получить квиз. ${error}`];
}
};
export const editQuiz = async (
body: EditQuizRequest,
signal?: AbortSignal,
): Promise<[EditQuizResponse | null, string?]> => {
try {
const editedQuiz = await makeRequest<EditQuizRequest, EditQuizResponse>({
method: "PATCH",
url: `${API_URL}/quiz/edit`,
body,
signal,
});
return [editedQuiz];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось изменить квиз. ${error}`];
}
};
export const copyQuiz = async (
id: number,
): Promise<[EditQuizResponse | null, string?]> => {
try {
const copiedQuiz = await makeRequest<CopyQuizRequest, CopyQuizResponse>({
method: "POST",
url: `${API_URL}/quiz/copy`,
body: { id },
});
return [copiedQuiz];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось скопировать квиз. ${error}`];
}
};
export const deleteQuiz = async (
id: number,
): Promise<[DeleteQuizResponse | null, string?]> => {
try {
const deletedQuiz = await makeRequest<
DeleteQuizRequest,
DeleteQuizResponse
>({
method: "DELETE",
url: `${API_URL}/quiz/delete`,
body: { id },
});
return [deletedQuiz];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось удалить квиз. ${error}`];
}
};
export const addQuizImages = async (
quizId: number,
image: Blob,
): Promise<[AddedQuizImagesResponse | null, string?]> => {
try {
const formData = new FormData();
const name = image?.name ? transliterate(image?.name.replace(/\s/g, '_')) : "blob"
//Замена всех побелов на _
const renamedImage = new File([image], name)
formData.append("quiz", quizId.toString());
formData.append("image", renamedImage);
const addedQuizImages = await makeRequest<
FormData,
AddedQuizImagesResponse
>({
url: `${IMAGES_URL}/quiz/putImages`,
body: formData,
method: "PUT",
});
return [addedQuizImages];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось добавить изображение. ${error}`];
}
};
export const copyQuizTemplate = async (
qid: string,
): Promise<[number | null, string?]> => {
try {
const { id } = await makeRequest<{ Qid: string }, { id: number }>({
method: "POST",
url: `${API_URL}/quiz/template`,
body: { Qid: qid },
});
if (!id) {
return [null, `Не удалось скопировать шаблон квиза.`];
}
return [id];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось скопировать шаблон квиза. ${error}`];
}
};
return makeRequest<FormData, { [key: string]: string }>({
url: `${imagesUrl}/quiz/putImages`,
body: formData,
method: "PUT",
});
}
export const quizApi = {
create: createQuiz,
@ -201,7 +89,6 @@ export const quizApi = {
copy: copyQuiz,
delete: deleteQuiz,
addImages: addQuizImages,
copyTemplate: copyQuizTemplate,
};
const defaultCreateQuizBody: CreateQuizRequest = {

56
src/api/quizRelase.ts Normal file

@ -0,0 +1,56 @@
import { makeRequest } from "@frontend/kitui";
import { CreateQuestionRequest } from "model/question/create";
import { RawQuestion } from "model/question/question";
import {
GetQuestionListRequest,
GetQuestionListResponse,
} from "@model/question/getList";
import {
EditQuestionRequest,
EditQuestionResponse,
} from "@model/question/edit";
import {
DeleteQuestionRequest,
DeleteQuestionResponse,
} from "@model/question/delete";
import {
CopyQuestionRequest,
CopyQuestionResponse,
} from "@model/question/copy";
const baseUrl =
process.env.NODE_ENV === "production"
? "/squiz"
: "https://squiz.pena.digital";
function get(quizId: string) {
return makeRequest<any>({
url: `${baseUrl}/question/copy`,
body: { id: questionId, quiz_id: quizId },
method: "POST",
});
}
function quizRelase(quizId: string, status: "start" | "stop") {
return makeRequest<any>({
url: `https://squiz.pena.digital/answer/quiz/get`,
body: {
quiz_id: quizId,
limit: 100,
page: 0,
need_config: true,
},
method: "POST",
});
}
export const relaseApi = {
relase: quizRelase,
get: quizRelase,
};
const defaultGetQuestionListBody: GetQuestionListRequest = {
limit: 100,
offset: 0,
type: "",
};

@ -1,149 +0,0 @@
import { makeRequest } from "@api/makeRequest";
import { parseAxiosError } from "@utils/parse-error";
import type { RawResult } from "@model/result/result";
interface IResultListBody {
to: number;
from: string;
new: boolean;
page: number;
limit: number;
}
export interface IAnswerResult {
Browser: string;
CreatedAt: string;
Deleted: boolean;
Device: string;
DeviceType: string;
Email: string;
Fingerprint: string;
IP: string;
Id: number;
OS: string;
QuizId: number;
Result: boolean;
Session: string;
Start: boolean;
content: string;
new: boolean;
question_id: number;
}
type ResultFilter = {
from?: string;
new?: boolean;
to?: string;
};
type ObsolescenceRequest = {
answers: number[];
};
const API_URL = `${process.env.REACT_APP_DOMAIN}/squiz`;
const getResultList = async (
quizId: number,
page: number,
body: ResultFilter,
): Promise<[RawResult | null, string?]> => {
try {
const resultList = await makeRequest<IResultListBody, RawResult>({
method: "POST",
url: `${API_URL}/results/getResults/${quizId}`,
body: { page: page, limit: 10, ...body },
});
return [resultList];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось получить результат. ${error}`];
}
};
const deleteResult = async (
resultId: number,
): Promise<[string | null, string?]> => {
try {
const deletedResult = await makeRequest<void, string>({
method: "DELETE",
url: `${API_URL}/results/delete/${resultId}`,
body: {},
});
return [deletedResult];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось удалить результат. ${error}`];
}
};
const obsolescenceResult = async (
idResultArray: number[],
): Promise<[null, string?]> => {
try {
const obsolescencedResult = await makeRequest<ObsolescenceRequest, null>({
method: "PATCH",
url: `${API_URL}/result/seen`,
body: { answers: idResultArray },
});
return [obsolescencedResult];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось изменить результат. ${error}`];
}
};
const getAnswerResultList = async (
resultId: number,
): Promise<[IAnswerResult[] | null, string?]> => {
try {
const answerResultList = await makeRequest<never, IAnswerResult[]>({
method: "GET",
url: `${API_URL}/result/${resultId}`,
});
return [answerResultList];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось получить список результатов. ${error}`];
}
};
const AnswerResultListEx = async (
quizId: number,
body: ResultFilter,
): Promise<[Blob | null, string?]> => {
try {
const answerResultListEx = await makeRequest<ResultFilter, Blob>({
method: "POST",
url: `${API_URL}/results/${quizId}/export`,
body,
responseType: "blob",
});
return [answerResultListEx];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [
null,
`Не удалось получить список устаревших результатов. ${error}`,
];
}
};
export const resultApi = {
getList: getResultList,
delete: deleteResult,
getAnswerList: getAnswerResultList,
export: AnswerResultListEx,
obsolescence: obsolescenceResult,
};

@ -1,114 +0,0 @@
import { makeRequest } from "@api/makeRequest";
import { parseAxiosError } from "@utils/parse-error";
export type DevicesResponse = {
Device: Record<string, number>;
OS: Record<string, number>;
Browser: Record<string, number>;
};
export type GeneralResponse = {
Open: Record<string, number>;
Result: Record<string, number>;
AvTime: Record<string, number>;
Conversion: Record<string, number>;
};
export type QuestionsResponse = {
Funnel: number[];
FunnelData: number[];
Results: Record<string, number>;
Questions: Record<string, Record<string, number>>;
};
export type GraphicsResponse = unknown;
type TRequest = {
to: number;
from: number;
};
const API_URL = `${process.env.REACT_APP_DOMAIN}/squiz/statistic`;
export const getDevices = async (
quizId: string,
to: number,
from: number,
): Promise<[DevicesResponse | null, string?]> => {
try {
const devicesResponse = await makeRequest<TRequest, DevicesResponse>({
method: "POST",
url: `${API_URL}/${quizId}/devices`,
withCredentials: true,
body: { to, from },
});
return [devicesResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось получить статистику о девайсах. ${error}`];
}
};
export const getGeneral = async (
quizId: string,
to: number,
from: number,
): Promise<[GeneralResponse | null, string?]> => {
try {
const generalResponse = await makeRequest<TRequest, GeneralResponse>({
method: "POST",
url: `${API_URL}/${quizId}/general`,
withCredentials: true,
body: { to, from },
});
return [generalResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось получить ключевые метрики. ${error}`];
}
};
export const getQuestions = async (
quizId: string,
to: number,
from: number,
): Promise<[QuestionsResponse | null, string?]> => {
try {
const questionsResponse = await makeRequest<TRequest, QuestionsResponse>({
method: "POST",
url: `${API_URL}/${quizId}/questions`,
withCredentials: true,
body: { to, from },
});
return [questionsResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось получить статистику по результатам. ${error}`];
}
};
export const getGraphics = async (
quizId: string,
to: number,
from: number,
): Promise<[GraphicsResponse | null, string?]> => {
try {
const questionsResponse = await makeRequest<TRequest, QuestionsResponse>({
method: "get",
url: `${API_URL}s/${quizId}/pipelines?from=${from}&to=${to}`,
withCredentials: true,
});
return [questionsResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось получить статистику. ${error}`];
}
};

@ -1,45 +0,0 @@
import { makeRequest } from "@api/makeRequest";
import { parseAxiosError } from "@utils/parse-error";
import type { GetTariffsResponse } from "@frontend/kitui";
const API_URL = `${process.env.REACT_APP_DOMAIN}/strator/tariff`;
export const getTariffs = async (
page: number = 1,
): Promise<[GetTariffsResponse | null, string?]> => {
try {
const tariffs = await makeRequest<never, GetTariffsResponse>({
method: "GET",
url: `${API_URL}/getList?page=${page}&limit=100`,
});
return [tariffs];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Ошибка при получении списка тарифов. ${error}`];
}
};
import axios from "axios";
const apiUrl = process.env.REACT_APP_DOMAIN + "/requestquiz";
export async function sendContactFormRequest(body: {
contact: string;
whoami: string;
}) {
try {
const a = await axios(apiUrl + "/callme", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
data: body,
});
return [a];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Ошибка при отправке запроса. ${error}`];
}
}

@ -1,9 +0,0 @@
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'
});
};

@ -1,102 +0,0 @@
import { createTicket as createTicketRequest } from "@frontend/kitui";
import { makeRequest } from "@api/makeRequest";
import { parseAxiosError } from "@utils/parse-error";
import type {
SendTicketMessageRequest,
CreateTicketResponse,
} from "@frontend/kitui";
type SendFileResponse = {
message: string;
};
const API_URL = `${process.env.REACT_APP_DOMAIN}/heruvym/v1.0.0`;
export const sendTicketMessage = async (
ticketId: string,
message: string,
systemError: boolean
): Promise<[null, string?]> => {
try {
const sendTicketMessageResponse = await makeRequest<
SendTicketMessageRequest,
null
>({
url: `${API_URL}/send`,
method: "POST",
useToken: true,
body: { ticket: ticketId, message: message, lang: "ru", files: [], system: systemError },
});
return [sendTicketMessageResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось отправить сообщение. ${error}`];
}
};
export const shownMessage = async (id: string): Promise<[null, string?]> => {
try {
const shownMessageResponse = await makeRequest<{ id: string }, null>({
url: `${API_URL}/shown`,
method: "POST",
useToken: true,
body: { id },
});
return [shownMessageResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось прочесть сообщение. ${error}`];
}
};
export const sendFile = async (
ticketId: string,
file: File,
): Promise<[SendFileResponse | null, string?]> => {
try {
const body = new FormData();
body.append(file.name, file);
body.append("ticket", ticketId);
const sendResponse = await makeRequest<FormData, SendFileResponse>({
method: "POST",
url: `${API_URL}/sendFiles`,
body,
});
return [sendResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось отправить файл. ${error}`];
}
};
export const createTicket = async (
message: string,
useToken: boolean,
systemError: boolean
): Promise<[CreateTicketResponse | null, string?]> => {
try {
const createdTicket = await createTicketRequest({
url: `${process.env.REACT_APP_DOMAIN}/heruvym/v1.0.0/create`,
body: { Title: "Unauth title", Message: message, system: systemError },
useToken,
});
return [createdTicket];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось создать тикет. ${error}`];
}
};

@ -1,66 +0,0 @@
import { makeRequest } from "@api/makeRequest";
import { parseAxiosError } from "@utils/parse-error";
import type { UserAccount } from "@frontend/kitui";
import type { OriginalUserAccount } from "@root/user";
type RecoverUserRequest = {
password: string;
};
export const getUser = async (): Promise<[UserAccount | null, string?]> => {
try {
const user = await makeRequest<never, UserAccount>({
method: "GET",
url: `${process.env.REACT_APP_DOMAIN}/customer/v1.0.1/account`,
});
return [user];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось получить пользователя. ${error}`];
}
};
export const getAccount = async (): Promise<
[OriginalUserAccount | null, string?]
> => {
try {
const controller = new AbortController();
const account = await makeRequest<never, OriginalUserAccount>({
url: `${process.env.REACT_APP_DOMAIN}/squiz/account/get`,
contentType: true,
method: "GET",
useToken: true,
withCredentials: false,
signal: controller.signal,
});
return [account];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось получить данные аккаунта. ${error}`];
}
};
export const recoverUser = async (
password: string,
): Promise<[unknown | null, string?]> => {
try {
const recoverResponse = await makeRequest<RecoverUserRequest, unknown>({
url: `${process.env.REACT_APP_DOMAIN}/user`,
method: "PATCH",
body: { password },
});
return [recoverResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось восстановить пароль. ${error}`];
}
};

BIN
src/assets/AutoOpen.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
src/assets/BannerImg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

BIN
src/assets/Bunner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
src/assets/InBodySite.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

@ -141,6 +141,7 @@ export default function Notebook({ color }: Props) {
</g>
<mask
id="mask0_3_590"
// style="mask-type:alpha"
maskUnits="userSpaceOnUse"
x="26"
y="10"

BIN
src/assets/OnButton.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
src/assets/VidjetImg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

BIN
src/assets/Widget.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

File diff suppressed because one or more lines are too long

@ -1,5 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 930 B

@ -1,12 +0,0 @@
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;

@ -1,26 +0,0 @@
import { Box } from "@mui/material";
interface Props {
color?: string;
height?: string;
width?: string;
}
export default function AmoTrash({ color, height, width }: Props) {
return (
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.4994 6H4.5" stroke="#FC2012" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.5 18.7492V8.74609H18.5V18.7492C18.5 20.1299 17.3807 21.2492 16 21.2492H8C6.61929 21.2492 5.5 20.1299 5.5 18.7492Z" fill="#FC2012" stroke="#F02B2B"/>
<path d="M15.75 6V4.5C15.75 4.10218 15.592 3.72064 15.3107 3.43934C15.0294 3.15804 14.6478 3 14.25 3H9.75C9.35218 3 8.97064 3.15804 8.68934 3.43934C8.40804 3.72064 8.25 4.10218 8.25 4.5V6" stroke="#FC2012" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</Box>
);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

@ -1,16 +0,0 @@
<svg
width="30"
height="30"
viewBox="0 0 30 30"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect width="30" height="30" rx="6" fill="#FEDFD0" />
<path
d="M18.7891 11.2006L14.526 15.4943M10.3154 15.2084L14.526 19.2993L22.7891 10.7006M7.21053 15.4144L11 19.0962"
stroke="#FC712F"
stroke-width="1.5"
strokeLinecap="round"
stroke-linejoin="round"
/>
</svg>

Before

Width:  |  Height:  |  Size: 394 B

@ -1,3 +0,0 @@
<svg width="7" height="12" viewBox="0 0 7 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 1.5L1 6L6 10.5" stroke="white" stroke-width="1.5" strokeLinecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 211 B

@ -1,23 +0,0 @@
<svg
width="30"
height="30"
viewBox="0 0 30 30"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect width="30" height="30" rx="6" fill="#FEDFD0" />
<path
d="M11.5 10.5L15.5 15L11.5 19.5"
stroke="#FC712F"
stroke-width="1.5"
strokeLinecap="round"
stroke-linejoin="round"
/>
<path
d="M15.5 10.5L19.5 15L15.5 19.5"
stroke="#FC712F"
stroke-width="1.5"
strokeLinecap="round"
stroke-linejoin="round"
/>
</svg>

Before

Width:  |  Height:  |  Size: 466 B

@ -1,3 +0,0 @@
<svg width="14" height="7" viewBox="0 0 14 7" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 1L7 6L13 1" stroke="#7E2AEA" stroke-width="1.5" strokeLinecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 209 B

@ -1,4 +0,0 @@
<svg width="26" height="26" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.8955 10.1257H22.7705V5.25073" stroke="#7E2AEA" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M19.3174 19.3172C18.0677 20.5679 16.4752 21.4198 14.7413 21.7652C13.0074 22.1107 11.21 21.9341 9.57649 21.2579C7.94295 20.5816 6.54667 19.4361 5.56428 17.9662C4.5819 16.4962 4.05754 14.768 4.05754 13C4.05754 11.232 4.5819 9.50376 5.56428 8.03384C6.54667 6.56391 7.94295 5.41838 9.57649 4.74213C11.21 4.06589 13.0074 3.88932 14.7413 4.23477C16.4752 4.58022 18.0677 5.43216 19.3174 6.68282L22.7705 10.1258" stroke="#7E2AEA" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 730 B

@ -1,3 +0,0 @@
<svg width="7" height="12" viewBox="0 0 7 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 1.5L6 6L1 10.5" stroke="white" stroke-width="1.5" strokeLinecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 211 B

@ -1,35 +1,43 @@
import { Box, SxProps, Theme } from "@mui/material";
import { Box, useTheme } from "@mui/material";
export default function ChartIcon() {
const theme = useTheme();
export default function ChartLineUp(sx: SxProps<Theme>) {
return (
<Box sx={sx}>
<Box
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M21 19.5H3V4.5"
stroke="#7E2AEA"
stroke-width="1.5"
stroke={theme.palette.brightPurple.main}
strokeWidth="1.5"
strokeLinecap="round"
stroke-linejoin="round"
strokeLinejoin="round"
/>
<path
d="M19.5 6L12 13.5L9 10.5L3 16.5"
stroke="#7E2AEA"
stroke-width="1.5"
stroke={theme.palette.brightPurple.main}
strokeWidth="1.5"
strokeLinecap="round"
stroke-linejoin="round"
strokeLinejoin="round"
/>
<path
d="M19.5 9.75V6H15.75"
stroke="#7E2AEA"
stroke-width="1.5"
stroke={theme.palette.brightPurple.main}
strokeWidth="1.5"
strokeLinecap="round"
stroke-linejoin="round"
strokeLinejoin="round"
/>
</svg>
</Box>

@ -3,13 +3,11 @@ import { Box, useTheme } from "@mui/material";
interface CheckboxIconProps {
checked?: boolean;
color?: string;
isRounded?: boolean;
}
export default function CheckboxIcon({
checked = false,
color = "#7E2AEA",
isRounded,
}: CheckboxIconProps) {
const theme = useTheme();
@ -18,7 +16,7 @@ export default function CheckboxIcon({
sx={{
height: "24px",
width: "24px",
borderRadius: isRounded ? "50%" : "6px",
borderRadius: "6px",
display: "flex",
justifyContent: "center",
alignItems: "center",

@ -1,14 +0,0 @@
export const ClockWiseIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
>
<path
d="M10.4115 22.35L11.1615 22.35L11.1615 20.85L10.4115 20.85L10.4115 22.35ZM19.2553 12L19.2553 12.75L20.7553 12.75L20.7553 12L19.2553 12ZM20.0053 13.6L19.4748 14.1302L20.0053 14.661L20.5358 14.1302L20.0053 13.6ZM10.4115 20.85C5.52885 20.85 1.56777 16.887 1.56777 12L0.0677734 12C0.0677731 17.7146 4.6995 22.35 10.4115 22.35L10.4115 20.85ZM1.56777 12C1.56777 7.11295 5.52885 3.15 10.4115 3.15L10.4115 1.65C4.6995 1.65 0.0677736 6.28545 0.0677734 12L1.56777 12ZM10.4115 3.15C15.2942 3.15 19.2553 7.11295 19.2553 12L20.7553 12C20.7553 6.28545 16.1235 1.65 10.4115 1.65L10.4115 3.15ZM22.6726 9.86985L19.4748 13.0698L20.5358 14.1302L23.7336 10.9302L22.6726 9.86985ZM20.5358 13.0698L17.3377 9.86983L16.2767 10.9302L19.4748 14.1302L20.5358 13.0698Z"
fill="#7E2AEA"
/>
</svg>
);

@ -8,6 +8,8 @@ export default function AddressIcon({ color }: Props) {
return (
<Box
sx={{
// height: "38px",
// width: "45px",
display: "flex",
alignItems: "center",
justifyContent: "center",

@ -8,6 +8,8 @@ export default function EmailIcon({ color }: Props) {
return (
<Box
sx={{
// height: "38px",
// width: "45px",
display: "flex",
alignItems: "center",
justifyContent: "center",

@ -8,6 +8,8 @@ export default function NameIcon({ color }: Props) {
return (
<Box
sx={{
// height: "38px",
// width: "45px",
display: "flex",
alignItems: "center",
justifyContent: "center",

@ -8,6 +8,8 @@ export default function PhoneIcon({ color }: Props) {
return (
<Box
sx={{
// height: "38px",
// width: "45px",
display: "flex",
alignItems: "center",
justifyContent: "center",

@ -8,6 +8,8 @@ export default function TextIcon({ color }: Props) {
return (
<Box
sx={{
// height: "38px",
// width: "45px",
display: "flex",
alignItems: "center",
justifyContent: "center",

@ -8,6 +8,8 @@ export default function SupplementIcon({ color }: Props) {
return (
<Box
sx={{
// height: "38px",
// width: "45px",
display: "flex",
alignItems: "center",
justifyContent: "center",

@ -4,15 +4,9 @@ interface Props {
color?: string;
bgcolor?: string;
marL?: string;
width?: string;
}
export default function CopyIcon({
color,
bgcolor,
marL,
width = "36px",
}: Props) {
export default function CopyIcon({ color, bgcolor, marL }: Props) {
const theme = useTheme();
return (
@ -20,8 +14,8 @@ export default function CopyIcon({
sx={{
bgcolor,
borderRadius: "6px",
height: width,
width: width,
height: "36px",
width: "36px",
display: "flex",
justifyContent: "center",
alignItems: "center",

@ -1,36 +0,0 @@
import { Box } from "@mui/material";
interface Props {
color: string;
height: string;
width: string;
}
export default function EditPencil({ color, height, width }: Props) {
return (
<Box
sx={{
height,
width,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<svg
width="19"
height="18"
viewBox="0 0 19 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M10.5137 0.80552C11.5869 -0.269234 13.3274 -0.269892 14.4013 0.804049L16.8932 3.2959C17.958 4.36068 17.969 6.08444 16.918 7.16281L7.68486 16.6361C6.97933 17.3599 6.01167 17.7681 5.00124 17.7681L2.24909 17.768C0.969844 17.7679 -0.0517699 16.7015 0.00203171 15.4224L0.120186 12.6134C0.159684 11.6744 0.549963 10.7844 1.2138 10.1195L10.5137 0.80552ZM13.3415 1.86551C12.8533 1.37736 12.0622 1.37766 11.5744 1.86618L9.9113 3.53178L14.1911 7.81157L15.8446 6.11505C16.3224 5.62488 16.3173 4.84136 15.8333 4.35737L13.3415 1.86551ZM2.27446 11.1802L8.85145 4.59325L13.144 8.88585L6.61148 15.5883C6.18816 16.0226 5.60756 16.2675 5.0013 16.2675L2.24916 16.2674C1.82274 16.2674 1.4822 15.9119 1.50014 15.4856L1.61829 12.6765C1.64199 12.1131 1.87616 11.5791 2.27446 11.1802ZM17.5148 17.6948C17.9289 17.6948 18.2645 17.3589 18.2645 16.9445C18.2645 16.5301 17.9289 16.1942 17.5148 16.1942H11.3931C10.9791 16.1942 10.6434 16.5301 10.6434 16.9445C10.6434 17.3589 10.9791 17.6948 11.3931 17.6948H17.5148Z"
fill="#7E2AEA"
/>
</svg>
</Box>
);
}

@ -6,7 +6,6 @@ type InfoProps = {
sx?: SxProps;
onClick?: any;
className?: string;
color?: string;
};
export default function Info({
@ -15,7 +14,6 @@ export default function Info({
sx,
onClick,
className,
color = "#7e2aea",
}: InfoProps) {
return (
<IconButton sx={sx} className={className} onClick={onClick}>
@ -28,21 +26,21 @@ export default function Info({
>
<path
d="M10 19C14.9706 19 19 14.9706 19 10C19 5.02944 14.9706 1 10 1C5.02944 1 1 5.02944 1 10C1 14.9706 5.02944 19 10 19Z"
stroke={color}
stroke="#7E2AEA"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M9.25 9.25H10V14.5H10.75"
stroke={color}
stroke="#7E2AEA"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M9.8125 7C10.4338 7 10.9375 6.49632 10.9375 5.875C10.9375 5.25368 10.4338 4.75 9.8125 4.75C9.19118 4.75 8.6875 5.25368 8.6875 5.875C8.6875 6.49632 9.19118 7 9.8125 7Z"
fill={color}
fill="#7E2AEA"
/>
</svg>
</IconButton>

@ -3,11 +3,10 @@ import { Box, useTheme } from "@mui/material";
import type { SxProps } from "@mui/material";
type InfoIconProps = {
color?: string;
sx?: SxProps;
};
export default function InfoIcon({ sx, color = "#7e2aea" }: InfoIconProps) {
export default function InfoIcon({ sx }: InfoIconProps) {
const theme = useTheme();
return (
@ -30,21 +29,21 @@ export default function InfoIcon({ sx, color = "#7e2aea" }: InfoIconProps) {
>
<path
d="M12 21C16.9706 21 21 16.9706 21 12C21 7.02944 16.9706 3 12 3C7.02944 3 3 7.02944 3 12C3 16.9706 7.02944 21 12 21Z"
stroke={color}
stroke={theme.palette.brightPurple.main}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M11.25 11.25H12V16.5H12.75"
stroke={color}
stroke={theme.palette.brightPurple.main}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M11.8125 9C12.4338 9 12.9375 8.49632 12.9375 7.875C12.9375 7.25368 12.4338 6.75 11.8125 6.75C11.1912 6.75 10.6875 7.25368 10.6875 7.875C10.6875 8.49632 11.1912 9 11.8125 9Z"
fill={color}
fill={theme.palette.brightPurple.main}
/>
</svg>
</Box>

@ -12,23 +12,23 @@ export const LinkSimple: FC<SVGProps<SVGSVGElement>> = (props) => (
<path
d="M8.82031 15.1781L15.1766 8.8125"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M13.5949 16.7719L10.9418 19.425C10.5238 19.843 10.0276 20.1745 9.48151 20.4007C8.93541 20.6269 8.35009 20.7434 7.75899 20.7434C6.5652 20.7434 5.42031 20.2691 4.57618 19.425C3.73204 18.5809 3.25781 17.436 3.25781 16.2422C3.25781 15.0484 3.73204 13.9035 4.57618 13.0594L7.2293 10.4062"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M16.7719 13.5937L19.425 10.9406C20.2691 10.0965 20.7434 8.95159 20.7434 7.7578C20.7434 6.56401 20.2691 5.41912 19.425 4.57499C18.5809 3.73085 17.436 3.25662 16.2422 3.25662C15.0484 3.25662 13.9035 3.73085 13.0594 4.57499L10.4062 7.22811"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
);

@ -1,22 +0,0 @@
import { useLocation } from "react-router-dom";
import { Box, SxProps, Theme } from "@mui/material";
interface Props {
sx?: SxProps<Theme>;
}
export default function NotebookWithPencil({ sx }: Props) {
return (
<Box
sx={{
height: "24px",
...sx
}}
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.88766 6.5124H15.3537M5.88766 10.4075H12.0172M5.88766 14.398H9.18792M18.988 4.41492V3.69678C18.988 3.14449 18.5403 2.69678 17.988 2.69678H3.19922C2.64693 2.69678 2.19922 3.14449 2.19922 3.69678V19.7038C2.19922 20.5407 3.16577 21.0074 3.8211 20.4869L6.25047 18.5577C6.42733 18.4173 6.64652 18.3408 6.87236 18.3408H17.988C18.5403 18.3408 18.988 17.8931 18.988 17.3408V15.2147" stroke="#7E2AEA" stroke-width="1.5" stroke-linecap="round" />
<path d="M20.383 7.29034L20.9133 6.76001L20.383 6.22968L19.8527 6.76001L20.383 7.29034ZM15.9919 11.6815L15.4615 11.1511L14.9312 11.6815L15.4615 12.2118L15.9919 11.6815ZM16.7185 12.4081L16.1882 12.9384L16.7185 13.4688L17.2489 12.9384L16.7185 12.4081ZM21.1096 8.01698L21.64 8.54731L22.1703 8.01698L21.64 7.48665L21.1096 8.01698ZM14.4001 13.2732L14.9304 12.7429L14.4001 12.2126L13.8698 12.7429L14.4001 13.2732ZM14.0817 13.5917L13.5514 13.0613C13.5011 13.1115 13.4583 13.1686 13.4242 13.2309L14.0817 13.5917ZM13.1982 15.2018L12.5407 14.841C12.3803 15.1333 12.4321 15.4964 12.6678 15.7321C12.9036 15.9679 13.2667 16.0197 13.559 15.8593L13.1982 15.2018ZM14.8083 14.3183L15.1691 14.9758C15.2314 14.9417 15.2885 14.8988 15.3387 14.8486L14.8083 14.3183ZM15.1267 13.9999L15.6571 14.5302L16.1874 13.9999L15.6571 13.4696L15.1267 13.9999ZM21.9075 5.76585L21.3772 5.23552L20.8468 5.76585L21.3772 6.29618L21.9075 5.76585ZM22.2812 5.39209L22.8116 4.86176C22.5187 4.56887 22.0438 4.56887 21.7509 4.86176L22.2812 5.39209ZM23.0079 6.11874L23.5382 6.64907C23.8311 6.35617 23.8311 5.8813 23.5382 5.58841L23.0079 6.11874ZM22.6341 6.4925L22.1038 7.02283L22.6341 7.55316L23.1645 7.02283L22.6341 6.4925ZM19.8527 6.76001L15.4615 11.1511L16.5222 12.2118L20.9133 7.82067L19.8527 6.76001ZM15.4615 12.2118L16.1882 12.9384L17.2489 11.8778L16.5222 11.1511L15.4615 12.2118ZM17.2489 12.9384L21.64 8.54731L20.5793 7.48665L16.1882 11.8778L17.2489 12.9384ZM21.64 7.48665L20.9133 6.76001L19.8527 7.82067L20.5793 8.54731L21.64 7.48665ZM13.8698 12.7429L13.5514 13.0613L14.612 14.122L14.9304 13.8036L13.8698 12.7429ZM13.4242 13.2309L12.5407 14.841L13.8557 15.5626L14.7392 13.9524L13.4242 13.2309ZM13.559 15.8593L15.1691 14.9758L14.4475 13.6608L12.8374 14.5443L13.559 15.8593ZM15.3387 14.8486L15.6571 14.5302L14.5964 13.4696L14.278 13.788L15.3387 14.8486ZM15.6571 13.4696L14.9304 12.7429L13.8698 13.8036L14.5964 14.5302L15.6571 13.4696ZM22.4378 6.29618L22.8116 5.92242L21.7509 4.86176L21.3772 5.23552L22.4378 6.29618ZM21.7509 5.92242L22.4776 6.64907L23.5382 5.58841L22.8116 4.86176L21.7509 5.92242ZM22.4776 5.58841L22.1038 5.96217L23.1645 7.02283L23.5382 6.64907L22.4776 5.58841ZM23.1645 5.96217L22.4378 5.23552L21.3772 6.29618L22.1038 7.02283L23.1645 5.96217Z" fill="#7E2AEA" />
</svg>
</Box>
);
}

@ -0,0 +1,41 @@
import { Box } from "@mui/material";
interface Props {
color: string;
}
export default function NumberThree({ color }: Props) {
return (
<Box
sx={{
height: "30px",
width: "30px",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<svg
width="24"
height="25"
viewBox="0 0 24 25"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 21.1875C16.9706 21.1875 21 17.1581 21 12.1875C21 7.21694 16.9706 3.1875 12 3.1875C7.02944 3.1875 3 7.21694 3 12.1875C3 17.1581 7.02944 21.1875 12 21.1875Z"
stroke={color}
strokeWidth="1.5"
strokeMiterlimit="10"
/>
<path
d="M9.75 8.0625H14.25L11.625 11.8125C12.0567 11.8125 12.4817 11.919 12.8624 12.1225C13.243 12.326 13.5677 12.6203 13.8075 12.9792C14.0473 13.3381 14.1949 13.7507 14.2372 14.1803C14.2795 14.6099 14.2152 15.0433 14.05 15.4421C13.8848 15.8409 13.6238 16.1928 13.2901 16.4666C12.9564 16.7405 12.5603 16.9278 12.137 17.0121C11.7136 17.0963 11.276 17.0748 10.8629 16.9495C10.4498 16.8242 10.074 16.599 9.76875 16.2937"
stroke={color}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</Box>
);
}

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