Compare commits
4 Commits
main
...
pickstagin
| Author | SHA1 | Date | |
|---|---|---|---|
| 841defad06 | |||
| fb236f55ee | |||
| e2198f4cfe | |||
| a0ba62d916 |
26
.gitea/workflows/deploy.yml
Normal file
@ -0,0 +1,26 @@
|
||||
name: Deploy
|
||||
run-name: ${{ gitea.actor }} build image and push to container registry
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
- 'staging'
|
||||
|
||||
jobs:
|
||||
DeployService:
|
||||
runs-on: [frontstaging]
|
||||
container:
|
||||
image: gitea.pena:3000/penadevops/container-images/node-compose:main
|
||||
env:
|
||||
GITHUB_RUN_NUMBER: "${{ inputs.actionid }}"
|
||||
volumes:
|
||||
- /run/user/1000/podman/podman.sock:/run/user/1000/podman/podman.sock
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: http://gitea.pena:3000/PenaDevops/actions.git/checkout@v1
|
||||
- run: printenv
|
||||
- run: GITHUB_RUN_NUMBER=${{ gitea.run_id }} compose -f deployments/${{ gitea.ref_name }}/docker-compose.yaml up -d
|
||||
# uses: http://gitea.pena/PenaDevops/actions.git/.gitea/workflows/deploy.yml@v1.1.6-p
|
||||
# with:
|
||||
# runner: frontstaging
|
||||
@ -1,37 +0,0 @@
|
||||
name: Deploy
|
||||
run-name: ${{ gitea.actor }} build image and push to container registry
|
||||
|
||||
on:
|
||||
registry_package:
|
||||
types: [published]
|
||||
|
||||
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 }}
|
||||
#
|
||||
DeployService:
|
||||
if: contains(github.event.package.name, 'main')
|
||||
runs-on: [frontprod]
|
||||
container:
|
||||
image: gitea.pena/penadevops/container-images/node-compose:main
|
||||
env:
|
||||
GITHUB_RUN_NUMBER: "${{ inputs.actionid }}"
|
||||
volumes:
|
||||
- /run/user/1000/podman/podman.sock:/run/user/1000/podman/podman.sock
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: http://gitea.pena/PenaDevops/actions.git/checkout@v1
|
||||
- run: compose -f deployments/main/docker-compose.yaml up -d
|
||||
@ -1,31 +0,0 @@
|
||||
name: Deploy
|
||||
run-name: ${{ gitea.actor }} build image and push to container registry
|
||||
|
||||
on:
|
||||
registry_package:
|
||||
types: [published]
|
||||
|
||||
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:
|
||||
if: contains(github.event.package.name, 'staging')
|
||||
runs-on: [frontstaging]
|
||||
container:
|
||||
image: gitea.pena:3000/penadevops/container-images/node-compose:main
|
||||
env:
|
||||
GITHUB_RUN_NUMBER: "${{ inputs.actionid }}"
|
||||
volumes:
|
||||
- /run/user/1000/docker/docker.sock:/run/user/1000/docker/docker.sock
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: http://gitea.pena:3000/PenaDevops/actions.git/checkout@v1
|
||||
- run: compose -f deployments/staging/docker-compose.yaml up -d
|
||||
|
||||
|
||||
@ -4,11 +4,11 @@ run-name: ${{ gitea.actor }} produce linting
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'dev'
|
||||
- 'sdev'
|
||||
|
||||
jobs:
|
||||
Lint:
|
||||
runs-on: [hubstaging]
|
||||
uses: http://gitea.pena/PenaDevops/actions.git/.gitea/workflows/lint.yml@v1.1.0
|
||||
uses: http://gitea.pena/PenaDevops/actions.git/.gitea/workflows/lint.yml@v1.1.2
|
||||
with:
|
||||
runner: hubstaging
|
||||
|
||||
38
.gitlab-ci.yml
Normal file
@ -0,0 +1,38 @@
|
||||
include:
|
||||
- project: "devops/pena-continuous-integration"
|
||||
file: "/templates/docker/build-template.gitlab-ci.yml"
|
||||
- project: "devops/pena-continuous-integration"
|
||||
file: "/templates/docker/deploy-template.gitlab-ci.yml"
|
||||
- project: "devops/pena-continuous-integration"
|
||||
file: "/templates/docker/service-discovery.gitlab-ci.yml"
|
||||
stages:
|
||||
- build
|
||||
- deploy
|
||||
- service-discovery
|
||||
build-app:
|
||||
extends: .build_template
|
||||
tags:
|
||||
- frontbuild
|
||||
variables:
|
||||
DOCKER_BUILD_PATH: "./Dockerfile"
|
||||
STAGING_BRANCH: "staging"
|
||||
PRODUCTION_BRANCH: "main"
|
||||
|
||||
deploy-to-staging:
|
||||
extends: .deploy_template
|
||||
rules:
|
||||
- if: "$CI_COMMIT_BRANCH == $STAGING_BRANCH"
|
||||
tags:
|
||||
- front
|
||||
- staging
|
||||
|
||||
deploy-to-prod:
|
||||
extends: .deploy_template
|
||||
rules:
|
||||
- if: "$CI_COMMIT_BRANCH == $PRODUCTION_BRANCH"
|
||||
tags:
|
||||
- front
|
||||
- prod
|
||||
|
||||
service-discovery:
|
||||
extends: .sd_artefacts_template
|
||||
19
.husky/pre-commit
Executable file → Normal file
@ -1,21 +1,4 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
# yarn lint-staged --allow-empty
|
||||
|
||||
if [ "$HUSKY_SKIP_CHANGELOG" != "1" ]; then # HUSKY_SKIP_CHANGELOG=1 git commit -m "--"
|
||||
|
||||
# записываем в changelog инфо о коммите
|
||||
bash .husky/scripts/update-changelog.sh
|
||||
|
||||
# Проверяем, изменился ли CHANGELOG.md
|
||||
if git diff --name-only | grep -q "CHANGELOG.md"; then
|
||||
# Добавляем CHANGELOG.md в staging, если он был изменен
|
||||
git add CHANGELOG.md
|
||||
echo "CHANGELOG.md updated and added to staging"
|
||||
else
|
||||
echo "CHANGELOG.md not modified"
|
||||
fi
|
||||
else
|
||||
echo "Skipping changelog update (HUSKY_SKIP_CHANGELOG=1)"
|
||||
fi
|
||||
yarn lint-staged --allow-empty
|
||||
|
||||
@ -1,64 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Проверяем, что мы в ветке staging
|
||||
CURRENT_BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null)
|
||||
|
||||
if [ "$CURRENT_BRANCH" != "staging" ]; then
|
||||
echo "Not in staging branch, skipping changelog update"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Получаем последний commit message
|
||||
COMMIT_MESSAGE=$(git log -1 --pretty=%B 2>/dev/null | head -1)
|
||||
|
||||
if [ -z "$COMMIT_MESSAGE" ]; then
|
||||
echo "No commit message found"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Получаем текущую дату
|
||||
CURRENT_DATE=$(date +%Y-%m-%d)
|
||||
CHANGELOG_FILE="CHANGELOG.md"
|
||||
|
||||
# Определяем новую версию
|
||||
if [ -f "$CHANGELOG_FILE" ]; then
|
||||
# Ищем последнюю версию в формате X.X.X (без v и ##)
|
||||
LAST_VERSION=$(grep -E '^[0-9]+\.[0-9]+\.[0-9]+' "$CHANGELOG_FILE" | head -1 | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+')
|
||||
|
||||
if [ -n "$LAST_VERSION" ]; then
|
||||
# Увеличиваем patch версию (третью цифру)
|
||||
IFS='.' read -r MAJOR MINOR PATCH <<< "$LAST_VERSION"
|
||||
NEW_VERSION="$MAJOR.$MINOR.$((PATCH + 1))"
|
||||
echo "Found last version: $LAST_VERSION, new version: $NEW_VERSION"
|
||||
else
|
||||
# Если не нашли версию, начинаем с 1.0.1
|
||||
NEW_VERSION="1.0.1"
|
||||
echo "No version found, starting from: $NEW_VERSION"
|
||||
fi
|
||||
else
|
||||
NEW_VERSION="1.0.0"
|
||||
echo "CHANGELOG.md not found, starting from: $NEW_VERSION"
|
||||
fi
|
||||
|
||||
# Создаем временный файл
|
||||
TEMP_FILE=$(mktemp)
|
||||
|
||||
# Добавляем новую запись БЕЗ ##, БЕЗ v, БЕЗ переноса строки
|
||||
echo "${NEW_VERSION} _ ${CURRENT_DATE} _ ${COMMIT_MESSAGE}" > "$TEMP_FILE"
|
||||
|
||||
# Добавляем существующее содержимое БЕЗ пустых строк между записями
|
||||
if [ -f "$CHANGELOG_FILE" ]; then
|
||||
# Убираем пустые строки между записями и добавляем содержимое
|
||||
awk 'NF' "$CHANGELOG_FILE" >> "$TEMP_FILE"
|
||||
else
|
||||
# Создаем базовую структуру для нового файла
|
||||
echo "# Changelog" >> "$TEMP_FILE"
|
||||
echo "" >> "$TEMP_FILE"
|
||||
echo "All notable changes to this project will be documented in this file." >> "$TEMP_FILE"
|
||||
echo "" >> "$TEMP_FILE"
|
||||
fi
|
||||
|
||||
# Заменяем оригинальный файл
|
||||
mv "$TEMP_FILE" "$CHANGELOG_FILE"
|
||||
|
||||
echo "CHANGELOG.md updated to version ${NEW_VERSION}"
|
||||
1
.npmrc
@ -1 +0,0 @@
|
||||
@frontend:registry=http://gitea.pena/api/packages/skeris/npm/
|
||||
5
.vscode/extensions.json
vendored
@ -1,5 +0,0 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"godrix.svgr-preview"
|
||||
]
|
||||
}
|
||||
1
.yarnrc
Normal file
@ -0,0 +1 @@
|
||||
"@frontend:registry" "http://gitea.pena/api/packages/skeris/npm/"
|
||||
12
CHANGELOG.md
@ -1,12 +1,2 @@
|
||||
1.0.11 _ 2025-10-06 _ Merge branch 'staging'
|
||||
1.0.10 _ 2025-10-05 _ utm
|
||||
1.0.9 _ 2025-10-05 _ utm
|
||||
1.0.8 _ 2025-10-05 _ замена инпут на текстареа
|
||||
1.0.7 _ 2025-10-05 _ замена инпут на текстареа
|
||||
1.0.6 _ 2025-09-19 _ логика включения таймера
|
||||
1.0.5 _ 2025-09-18 _ особые условия для вывода картинок
|
||||
1.0.4 _ 2025-09-14 _ особые условия для вывода картинок
|
||||
1.0.3 _ 2025-09-12 _ среднее время не учитывает нули
|
||||
1.0.2 _ 2025-09-07 _ добавлена автозапись в стейджинг
|
||||
1.0.1 Страница заявок корректно отображает мультиответ
|
||||
1.0.0 Добавлены фичи "мультиответ", "перенос строки в своём ответе", "свой ответ", "плейсхолдер своего ответа"
|
||||
1.0.0 Добавлены фичи "мультиответ", "перенос строки в своём ответе", "свой ответ", "плейсхолдер своего ответа"
|
||||
@ -1,11 +1,12 @@
|
||||
FROM gitea.pena/penadevops/container-images/node:main as build
|
||||
|
||||
RUN apk update && rm -rf /var/cache/apk/*
|
||||
WORKDIR /usr/app
|
||||
COPY . .
|
||||
|
||||
RUN yarn install --ignore-scripts --non-interactive && yarn cache clean
|
||||
RUN yarn build
|
||||
|
||||
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
|
||||
|
||||
577
api-docs.html
@ -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>
|
||||
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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>
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,8 @@
|
||||
version: "3"
|
||||
services:
|
||||
squiz:
|
||||
container_name: squiz
|
||||
restart: unless-stopped
|
||||
image: gitea.pena/squiz/frontpanel/main:latest
|
||||
image: $CI_REGISTRY_IMAGE/main:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID
|
||||
hostname: squiz
|
||||
tty: true
|
||||
pull_policy: always
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
version: "3"
|
||||
services:
|
||||
squiz:
|
||||
container_name: squiz
|
||||
restart: unless-stopped
|
||||
image: gitea.pena/squiz/frontpanel/staging:latest
|
||||
image: gitea.pena/squiz/frontpanel/staging:324
|
||||
labels:
|
||||
com.pena.domains: squiz.pena.digital
|
||||
hostname: squiz
|
||||
tty: true
|
||||
pull_policy: always
|
||||
|
||||
@ -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'
|
||||
}
|
||||
};
|
||||
21260
package-lock.json
generated
14
package.json
@ -6,8 +6,8 @@
|
||||
"@craco/craco": "^7.0.0",
|
||||
"@emotion/react": "^11.10.5",
|
||||
"@emotion/styled": "^11.10.5",
|
||||
"@frontend/kitui": "1.0.110",
|
||||
"@frontend/squzanswerer": "^1.0.57",
|
||||
"@frontend/kitui": "^1.0.87",
|
||||
"@frontend/squzanswerer": "^1.0.59",
|
||||
"@mui/icons-material": "^5.10.14",
|
||||
"@mui/material": "^5.10.14",
|
||||
"@mui/x-charts": "^6.19.5",
|
||||
@ -25,6 +25,7 @@
|
||||
"@types/react-slick": "^0.23.13",
|
||||
"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",
|
||||
@ -68,11 +69,9 @@
|
||||
"build": "craco build",
|
||||
"test": "craco test",
|
||||
"eject": "craco eject",
|
||||
"code:format": "prettier --write --ignore-unknown",
|
||||
"deploy": "docker login gitea.pena && docker build -t gitea.pena/squiz/frontpanel/$(git branch --show-current):latest . && docker push gitea.pena/squiz/frontpanel/$(git branch --show-current):latest",
|
||||
"prepare": "husky install",
|
||||
"cypress:open": "cypress open",
|
||||
"cypress:run": "cypress run"
|
||||
"code:format": "prettier --write --ignore-unknown",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
@ -87,12 +86,13 @@
|
||||
]
|
||||
},
|
||||
"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"
|
||||
|
||||
@ -165,7 +165,7 @@
|
||||
console.log(params.get("debug"))
|
||||
if (params.get("debug")) {
|
||||
console.log(
|
||||
"params.get(debug) is true"
|
||||
"mhgfhdhfjhffhfhjfghjgf"
|
||||
)
|
||||
let scriptTag = document.createElement('script');
|
||||
scriptTag.setAttribute('src', "https://markknol.github.io/console-log-viewer/console-log-viewer.js");
|
||||
|
||||
119
src/App.tsx
@ -1,4 +1,4 @@
|
||||
import { clearAuthToken, createMakeRequestConfig, getMessageFromFetchError, handleComponentError, UserAccount, useTicketsFetcher, useUserFetcher } from "@frontend/kitui";
|
||||
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";
|
||||
@ -8,7 +8,7 @@ import { useAfterPay } from "@utils/hooks/useAutoPay";
|
||||
import { useUserAccountFetcher } from "@utils/hooks/useUserAccountFetcher";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import type { SuspenseProps } from "react";
|
||||
import { lazy, Suspense, useEffect } 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";
|
||||
@ -23,11 +23,7 @@ import { InfoPrivilege } from "./pages/InfoPrivilege";
|
||||
import AmoTokenExpiredDialog from "./pages/IntegrationsPage/IntegrationsModal/Amo/AmoTokenExpiredDialog";
|
||||
import Landing from "./pages/Landing/Landing";
|
||||
import Main from "./pages/main";
|
||||
import Debug from "./pages/Debug";
|
||||
import { setTicketData, setTickets, useTicketStore } from "./stores/ticket";
|
||||
import { parseAxiosError } from "./utils/parse-error";
|
||||
import { ErrorBoundary } from "react-error-boundary";
|
||||
import { handleLogoutClick } from "./utils/HandleLogoutClick";
|
||||
|
||||
const MyQuizzesFull = lazy(() => import("./pages/createQuize/MyQuizzesFull"));
|
||||
const QuizGallery = lazy(() => import("./pages/createQuize/QuizGallery"));
|
||||
@ -39,16 +35,9 @@ 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"))
|
||||
|
||||
createMakeRequestConfig(
|
||||
handleLogoutClick,
|
||||
(error, info, getTickets) => handleComponentError(error, info, getTickets()),
|
||||
() => useTicketStore.getState().tickets
|
||||
);
|
||||
|
||||
const routeslink = [
|
||||
{
|
||||
path: "/edit",
|
||||
@ -71,28 +60,18 @@ const routeslink = [
|
||||
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>
|
||||
);
|
||||
|
||||
const ApologyPage = () => <div><p>Что-то пошло не так</p></div>
|
||||
|
||||
export default function App() {
|
||||
window.LoadingObserver = false;
|
||||
const userId = useUserStore((state) => state.userId);
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const { data: amoAccount } = useAmoAccount();
|
||||
const tickets = useTicketStore(store => store.tickets);
|
||||
|
||||
|
||||
useUserFetcher({
|
||||
url: `${process.env.REACT_APP_DOMAIN}/user/${userId}`,
|
||||
@ -111,11 +90,8 @@ export default function App() {
|
||||
useUserAccountFetcher<UserAccount>({
|
||||
url: `${process.env.REACT_APP_DOMAIN}/customer/v1.0.1/account`,
|
||||
userId,
|
||||
onNewUserAccount: (account) => {
|
||||
setCustomerAccount(account);
|
||||
},
|
||||
onNewUserAccount: setCustomerAccount,
|
||||
onError: (error) => {
|
||||
console.error("App: Error in customerAccount fetcher:", error);
|
||||
const errorMessage = getMessageFromFetchError(error);
|
||||
if (errorMessage) {
|
||||
enqueueSnackbar(errorMessage);
|
||||
@ -141,37 +117,6 @@ export default function App() {
|
||||
},
|
||||
});
|
||||
|
||||
useTicketsFetcher({
|
||||
url: `${process.env.REACT_APP_DOMAIN}/heruvym/v1.0.0/getTickets`,
|
||||
ticketsPerPage: 10,
|
||||
ticketApiPage: 0,
|
||||
onSuccess: (result) => {
|
||||
if (result.data?.length) {
|
||||
// Записываем все тикеты в стор
|
||||
setTickets(result.data);
|
||||
|
||||
const currentTicket = result.data.find(
|
||||
({ origin }) => !origin.includes("/support"),
|
||||
);
|
||||
|
||||
if (!currentTicket) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTicketData({
|
||||
ticketId: currentTicket.id,
|
||||
sessionId: currentTicket.sess,
|
||||
});
|
||||
}
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
const message = parseAxiosError(error);
|
||||
if (message) enqueueSnackbar(message);
|
||||
},
|
||||
onFetchStateChange: () => { },
|
||||
enabled: Boolean(userId),
|
||||
});
|
||||
|
||||
useAfterPay();
|
||||
|
||||
if (location.state?.redirectTo)
|
||||
@ -184,10 +129,7 @@ export default function App() {
|
||||
);
|
||||
|
||||
return (
|
||||
<ErrorBoundary
|
||||
FallbackComponent={ApologyPage}
|
||||
onError={(error, info) => handleComponentError(error, info, () => useTicketStore.getState().tickets)}
|
||||
>
|
||||
<>
|
||||
{amoAccount && <AmoTokenExpiredDialog isAmoTokenExpired={amoAccount.stale} />}
|
||||
|
||||
<ContactFormModal />
|
||||
@ -277,10 +219,31 @@ export default function App() {
|
||||
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 />} />}
|
||||
@ -289,10 +252,6 @@ export default function App() {
|
||||
path={"/image/:srcImage"}
|
||||
element={<ChatImageNewWindow />}
|
||||
/>
|
||||
<Route
|
||||
path={"/debug"}
|
||||
element={<div></div>}
|
||||
/>
|
||||
<Route element={<PrivateRoute />}>
|
||||
{routeslink.map((e, i) => (
|
||||
<Route
|
||||
@ -312,32 +271,8 @@ export default function App() {
|
||||
}
|
||||
/>
|
||||
))}
|
||||
<Route
|
||||
path="/list"
|
||||
element={
|
||||
<LazyLoading
|
||||
children={<MyQuizzesFull />}
|
||||
fallback={<ListPageDummy />}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={"/view/:quizId"}
|
||||
element={<LazyLoading children={<ViewPage />} />}
|
||||
/>
|
||||
<Route
|
||||
path={"/analytics"}
|
||||
element={<LazyLoading children={<Analytics />} />}
|
||||
/>
|
||||
<Route
|
||||
path={"/results/:quizId"}
|
||||
element={<LazyLoading children={<QuizAnswersPage />} />}
|
||||
/>
|
||||
</Route>
|
||||
</Routes>
|
||||
|
||||
{/* Компонент отладки ошибок - доступен по Ctrl+Shift+D */}
|
||||
<Debug />
|
||||
</ErrorBoundary>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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,4 +1,4 @@
|
||||
import { makeRequest } from "@frontend/kitui";
|
||||
import { makeRequest } from "@api/makeRequest";
|
||||
|
||||
import { parseAxiosError } from "@utils/parse-error";
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { makeRequest } from "@frontend/kitui";
|
||||
import { makeRequest } from "@api/makeRequest";
|
||||
|
||||
import { parseAxiosError } from "@utils/parse-error";
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { makeRequest } from "@frontend/kitui";
|
||||
import { makeRequest } from "@api/makeRequest";
|
||||
|
||||
import { parseAxiosError } from "@utils/parse-error";
|
||||
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { QuestionKeys } from "@/pages/IntegrationsPage/IntegrationsModal/Amo/types";
|
||||
import { makeRequest } from "@frontend/kitui";
|
||||
import { useToken } from "@frontend/kitui";
|
||||
import { makeRequest } from "@api/makeRequest";
|
||||
import { parseAxiosError } from "@utils/parse-error";
|
||||
import useSWR from "swr";
|
||||
|
||||
@ -42,9 +41,7 @@ export const getAccount = async (): Promise<[AccountResponse | null, string?]> =
|
||||
};
|
||||
|
||||
export function useAmoAccount() {
|
||||
const token = useToken();
|
||||
|
||||
return useSWR(token ? "amoAccount" : null, () =>
|
||||
return useSWR("amoAccount", () =>
|
||||
makeRequest<void, AccountResponse>({
|
||||
method: "GET",
|
||||
url: `${API_URL}/account`,
|
||||
@ -366,9 +363,9 @@ export const removeAmoAccount = async (): Promise<[void | null, string?]> => {
|
||||
};
|
||||
|
||||
|
||||
export const getFields = async (pagination: PaginationRequest): Promise<[FieldsResponse | null, string?]> => {
|
||||
export const getFields = async ( pagination: PaginationRequest ): Promise<[FieldsResponse | null, string?]> => {
|
||||
try {
|
||||
const fieldsResponse = await makeRequest<PaginationRequest, FieldsResponse>({
|
||||
const fieldsResponse = await makeRequest<PaginationRequest, FieldsResponse>({
|
||||
method: "GET",
|
||||
url: `${API_URL}/fields?page=${pagination.page}&size=${pagination.size}`,
|
||||
});
|
||||
|
||||
@ -1,101 +0,0 @@
|
||||
import { makeRequest } from "@frontend/kitui";
|
||||
import { parseAxiosError } from "@utils/parse-error";
|
||||
|
||||
export type LeadTargetType = "mail" | "telegram" | "whatsapp" | "webhook";
|
||||
|
||||
export interface LeadTargetModel {
|
||||
id: number;
|
||||
accountID: string;
|
||||
type: LeadTargetType;
|
||||
quizID: number;
|
||||
target: string; // содержит подстроку "zapier" или "postback"
|
||||
inviteLink?: string;
|
||||
deleted?: boolean;
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
const API_URL = `${process.env.REACT_APP_DOMAIN}/squiz`;
|
||||
|
||||
export const getLeadTargetsByQuiz = async (
|
||||
quizId: number,
|
||||
): Promise<[LeadTargetModel[] | null, string?]> => {
|
||||
try {
|
||||
const items = await makeRequest<unknown, LeadTargetModel[]>({
|
||||
method: "GET",
|
||||
url: `${API_URL}/account/leadtarget/${quizId}`,
|
||||
});
|
||||
|
||||
return [items];
|
||||
} catch (nativeError) {
|
||||
const [error] = parseAxiosError(nativeError);
|
||||
return [null, `Не удалось получить цели лида. ${error}`];
|
||||
}
|
||||
};
|
||||
|
||||
export const createLeadTarget = async (
|
||||
body: {
|
||||
type: LeadTargetType;
|
||||
quizID: number;
|
||||
target: string;
|
||||
name?: string;
|
||||
},
|
||||
): Promise<[LeadTargetModel | true | null, string?]> => {
|
||||
try {
|
||||
const response = await makeRequest<typeof body, LeadTargetModel | true>({
|
||||
method: "POST",
|
||||
url: `${API_URL}/account/leadtarget`,
|
||||
body,
|
||||
});
|
||||
|
||||
return [response];
|
||||
} catch (nativeError) {
|
||||
const [error] = parseAxiosError(nativeError);
|
||||
return [null, `Не удалось создать цель лида. ${error}`];
|
||||
}
|
||||
};
|
||||
|
||||
export const updateLeadTarget = async (
|
||||
body: {
|
||||
id: number;
|
||||
target: string;
|
||||
},
|
||||
): Promise<[LeadTargetModel | null, string?]> => {
|
||||
try {
|
||||
const updated = await makeRequest<typeof body, LeadTargetModel>({
|
||||
method: "PUT",
|
||||
url: `${API_URL}/account/leadtarget`,
|
||||
body,
|
||||
});
|
||||
|
||||
return [updated];
|
||||
} catch (nativeError) {
|
||||
const [error] = parseAxiosError(nativeError);
|
||||
return [null, `Не удалось обновить цель лида. ${error}`];
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteLeadTarget = async (
|
||||
id: number,
|
||||
): Promise<[true | null, string?]> => {
|
||||
try {
|
||||
await makeRequest<unknown, unknown>({
|
||||
method: "DELETE",
|
||||
url: `${API_URL}/account/leadtarget/${id}`,
|
||||
});
|
||||
|
||||
return [true];
|
||||
} catch (nativeError) {
|
||||
const [error] = parseAxiosError(nativeError);
|
||||
return [null, `Не удалось удалить цель лида. ${error}`];
|
||||
}
|
||||
};
|
||||
|
||||
export const leadTargetApi = {
|
||||
getByQuiz: getLeadTargetsByQuiz,
|
||||
create: createLeadTarget,
|
||||
update: updateLeadTarget,
|
||||
delete: deleteLeadTarget,
|
||||
};
|
||||
|
||||
|
||||
|
||||
55
src/api/makeRequest.ts
Normal file
@ -0,0 +1,55 @@
|
||||
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;
|
||||
|
||||
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,4 +1,4 @@
|
||||
import { makeRequest } from "@frontend/kitui";
|
||||
import { makeRequest } from "@api/makeRequest";
|
||||
|
||||
import { parseAxiosError } from "@utils/parse-error";
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { makeRequest } from "@frontend/kitui";
|
||||
import { makeRequest } from "@api/makeRequest";
|
||||
|
||||
import { replaceSpacesToEmptyLines } from "@utils/replaceSpacesToEmptyLines";
|
||||
import { parseAxiosError } from "@utils/parse-error";
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { makeRequest } from "@frontend/kitui";
|
||||
import { makeRequest } from "@api/makeRequest";
|
||||
import { defaultQuizConfig } from "@model/quizSettings";
|
||||
|
||||
import { parseAxiosError } from "@utils/parse-error";
|
||||
@ -148,8 +148,8 @@ export const addQuizImages = async (
|
||||
const name = image?.name ? transliterate(image?.name.replace(/\s/g, '_')) : "blob"
|
||||
|
||||
//Замена всех побелов на _
|
||||
const renamedImage = new File([image], name)
|
||||
|
||||
const renamedImage = new File([image], name)
|
||||
|
||||
|
||||
formData.append("quiz", quizId.toString());
|
||||
formData.append("image", renamedImage);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { makeRequest } from "@frontend/kitui";
|
||||
import { makeRequest } from "@api/makeRequest";
|
||||
|
||||
import { parseAxiosError } from "@utils/parse-error";
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { makeRequest } from "@frontend/kitui";
|
||||
import { makeRequest } from "@api/makeRequest";
|
||||
|
||||
import { parseAxiosError } from "@utils/parse-error";
|
||||
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
import { makeRequest } from "@frontend/kitui";
|
||||
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,
|
||||
page: number,
|
||||
): Promise<[GetTariffsResponse | null, string?]> => {
|
||||
try {
|
||||
const tariffs = await makeRequest<never, GetTariffsResponse>({
|
||||
@ -15,6 +17,7 @@ export const getTariffs = async (
|
||||
return [tariffs];
|
||||
} 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,6 +1,6 @@
|
||||
import { createTicket as createTicketRequest } from "@frontend/kitui";
|
||||
|
||||
import { makeRequest } from "@frontend/kitui";
|
||||
import { makeRequest } from "@api/makeRequest";
|
||||
|
||||
import { parseAxiosError } from "@utils/parse-error";
|
||||
|
||||
@ -15,7 +15,47 @@ type SendFileResponse = {
|
||||
|
||||
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,
|
||||
@ -49,7 +89,7 @@ export const createTicket = async (
|
||||
try {
|
||||
const createdTicket = await createTicketRequest({
|
||||
url: `${process.env.REACT_APP_DOMAIN}/heruvym/v1.0.0/create`,
|
||||
body: { Title: "Unauth title", Message: message, system: systemError },
|
||||
body: { Title: "Unauth title", Message: message, System: systemError },
|
||||
useToken,
|
||||
});
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { makeRequest } from "@frontend/kitui";
|
||||
import { makeRequest } from "@api/makeRequest";
|
||||
|
||||
import { parseAxiosError } from "@utils/parse-error";
|
||||
|
||||
|
||||
@ -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,13 +0,0 @@
|
||||
import { Box, SxProps } from "@mui/material";
|
||||
|
||||
export default function OrangeYoutube(sx: SxProps) {
|
||||
return (
|
||||
<Box
|
||||
sx={sx}
|
||||
>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19.9756 4.36328C21.0799 4.36346 21.9754 5.25898 21.9756 6.36328V17.6367C21.9754 18.741 21.0799 19.6365 19.9756 19.6367H4.02734C2.92289 19.6367 2.02754 18.7411 2.02734 17.6367V6.36328C2.02754 5.25887 2.92289 4.36328 4.02734 4.36328H19.9756ZM10.2227 8.18262C10.1533 8.17891 10.0838 8.19583 10.0225 8.23145C9.9614 8.26705 9.9107 8.32013 9.875 8.38477C9.83921 8.44959 9.81946 8.52397 9.81934 8.59961V15.4004C9.81946 15.476 9.83921 15.5504 9.875 15.6152C9.9107 15.6799 9.9614 15.7329 10.0225 15.7686C10.0838 15.8042 10.1533 15.8211 10.2227 15.8174C10.292 15.8137 10.3592 15.79 10.417 15.748L15.1025 12.3477C15.1552 12.3095 15.1986 12.258 15.2285 12.1973C15.2584 12.1366 15.2744 12.0689 15.2744 12C15.2744 11.9311 15.2584 11.8634 15.2285 11.8027C15.1986 11.742 15.1552 11.6905 15.1025 11.6523L10.417 8.25195C10.3592 8.21003 10.292 8.18633 10.2227 8.18262Z" fill="#FA590B" />
|
||||
</svg>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@ -1,18 +0,0 @@
|
||||
import { SvgIcon, SxProps } from "@mui/material";
|
||||
|
||||
export default function SmallAddPluse({ sx }: { sx: SxProps }) {
|
||||
return (
|
||||
<SvgIcon
|
||||
sx={{
|
||||
width: "11px",
|
||||
height: "11px",
|
||||
|
||||
...sx
|
||||
}}
|
||||
>
|
||||
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.518 9.612C4.398 9.612 4.296 9.576 4.212 9.504C4.14 9.42 4.104 9.318 4.104 9.198V5.454H0.414C0.294 5.454 0.192 5.418 0.108 5.346C0.036 5.262 0 5.16 0 5.04V4.464C0 4.344 0.036 4.248 0.108 4.176C0.192 4.092 0.294 4.05 0.414 4.05H4.104V0.414C4.104 0.294 4.14 0.198 4.212 0.126C4.296 0.0420001 4.398 0 4.518 0H5.148C5.268 0 5.364 0.0420001 5.436 0.126C5.52 0.198 5.562 0.294 5.562 0.414V4.05H9.27C9.39 4.05 9.486 4.092 9.558 4.176C9.642 4.248 9.684 4.344 9.684 4.464V5.04C9.684 5.16 9.642 5.262 9.558 5.346C9.486 5.418 9.39 5.454 9.27 5.454H5.562V9.198C5.562 9.318 5.52 9.42 5.436 9.504C5.364 9.576 5.268 9.612 5.148 9.612H4.518Z" fill="white" />
|
||||
</svg>
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
import { Box, SxProps } from "@mui/material";
|
||||
import PostbackDefault from "./Postback";
|
||||
import PostbackPC from "./PostbackPC";
|
||||
|
||||
export const PostbackLogo = (sx: SxProps) => (
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1, ...sx }}>
|
||||
<PostbackPC sx={{ width: "24px", height: "20px" }} />
|
||||
<PostbackDefault sx={{ width: "40px", height: "8px" }} />
|
||||
</Box>
|
||||
);
|
||||
@ -1,19 +0,0 @@
|
||||
import { Box, SxProps } from "@mui/material";
|
||||
|
||||
export default (sx: SxProps) => (
|
||||
<Box
|
||||
component="svg"
|
||||
width="40px"
|
||||
height="34px"
|
||||
viewBox="0 0 40 34"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
sx={{
|
||||
mr: "15px"
|
||||
}}
|
||||
>
|
||||
<path d="M37 0H3C1.34315 0 0 1.34314 0 3V20.967H40V3C40 1.34315 38.6569 0 37 0Z" fill="#9A9AAF" fillOpacity="0.2" />
|
||||
<path d="M0 24.1244V21.967H40V24.1244C40 25.7813 38.6569 27.1244 37 27.1244H25.5472L26.8066 33.5412H13.2534L14.3928 27.1244H3C1.34315 27.1244 0 25.7813 0 24.1244Z" fill="#9A9AAF" fillOpacity="0.2" />
|
||||
<path d="M12.9803 9.56785L16.8969 12.7545C17.2236 13.0203 17.7125 12.7878 17.7125 12.3666V10.6689C19.4084 10.3847 22.8834 10.4557 23.2157 13.0131C23.631 16.2098 20.9105 17.2115 20.1629 17.318C19.4153 17.4246 19.7476 18 19.9137 18C22.115 18 25.5 16.2098 25.5 12.8213C25.3505 7.77475 20.246 7.19508 17.7125 7.53607V6.04142C17.7125 5.62197 17.2271 5.38894 16.8998 5.65124L12.9832 8.78983C12.7346 8.98903 12.7332 9.3668 12.9803 9.56785Z" fill="#7E2AEA" fillOpacity="0.5" />
|
||||
</Box>
|
||||
);
|
||||
@ -1,15 +0,0 @@
|
||||
import { Box, SxProps } from "@mui/material";
|
||||
|
||||
export const ZapierLogo = (sx: SxProps) => (
|
||||
<Box
|
||||
component="img"
|
||||
src="/src/assets/icons/logo/zapier.png"
|
||||
alt="Zapier"
|
||||
sx={{
|
||||
width: "40px",
|
||||
height: "40px",
|
||||
objectFit: "contain",
|
||||
...sx,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
Before Width: | Height: | Size: 3.4 KiB |
@ -1,6 +1,6 @@
|
||||
import { Box, type SxProps} from "@mui/material";
|
||||
import { Box } from "@mui/material";
|
||||
|
||||
export default function Plus(sx:SxProps) {
|
||||
export default function Plus() {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
@ -9,7 +9,6 @@ export default function Plus(sx:SxProps) {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
...sx
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
|
||||
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 123 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 164 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 861 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 514 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 642 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 667 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 600 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 618 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 442 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 264 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 600 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 295 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 645 KiB |
|
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 847 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 746 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 431 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 752 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 424 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 584 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 205 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 517 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 442 KiB |
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 209 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 719 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 132 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 870 KiB |
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 516 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 522 KiB |
|
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 310 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 572 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 758 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 1013 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 436 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 571 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 669 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 425 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 865 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 378 KiB |
|
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 568 KiB |
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 550 KiB |
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 512 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 410 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 351 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 658 KiB |