Compare commits
1 Commits
f69d6ae96c
...
7d794b16a5
Author | SHA1 | Date | |
---|---|---|---|
Arsen Mirzaev Tatyano-Muradovich | 7d794b16a5 |
99
README.md
99
README.md
|
@ -1,3 +1,98 @@
|
|||
# Telegram-robot for registering for tasks
|
||||
# huesos
|
||||
|
||||
Synchronizes accounts with the site, displays a list of published applications with a selection by date, and also register to tasks
|
||||
Basis for developing chat-robots with "Web App" technology for Telegram
|
||||
|
||||
## Installation
|
||||
|
||||
### AnangoDB
|
||||
|
||||
1. Create a View in ArangoDB for the document "product"
|
||||
`
|
||||
"links": {
|
||||
"product": {
|
||||
"fields": {
|
||||
"description": {
|
||||
"analyzers": [
|
||||
"text_ru"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
2. Create a Graph with the specified values
|
||||
**Name:** hierarchy
|
||||
|
||||
**edgeDefinition:** entry
|
||||
**fromCollections:** part, product...
|
||||
**toCollections:** category, part...
|
||||
|
||||
3. Create indexes for the "product" collection
|
||||
**Type:** "Inverted Index"
|
||||
**Fields:** title.RU
|
||||
**Analyzer:** "text_ru"
|
||||
**Search field:** true
|
||||
**Name:** title_ru
|
||||
|
||||
4. Create a View with the specified values
|
||||
**Name:** products_search
|
||||
|
||||
**type:** search-alias (you can also use "arangosearch")
|
||||
**indexes:**
|
||||
`
|
||||
"indexes": [
|
||||
{
|
||||
"collection": "product",
|
||||
"index": "title_ru" (THIS IS AN EXAMPLE)
|
||||
}
|
||||
]
|
||||
`
|
||||
|
||||
### NGINX
|
||||
|
||||
1. Add this to a NGINX config: `try_files $uri $uri/ /index.php;`
|
||||
|
||||
2. Add this to a NGINX config
|
||||
`
|
||||
location /images {
|
||||
alias /PATH/TO/public/themes/default/images;
|
||||
}
|
||||
`
|
||||
|
||||
## Settings
|
||||
Settings of chat-robot and Web App
|
||||
|
||||
Make sure you have a "settings" collection (can be created automatically) and at least one document with the "status" parameter set to "active"
|
||||
`
|
||||
{
|
||||
"status": "active"
|
||||
}
|
||||
`
|
||||
|
||||
### language
|
||||
Language for system messages if user language could not be determined
|
||||
|
||||
**Value:** en
|
||||
|
||||
## Suspensions
|
||||
System of suspensions of chat-robot and Web App
|
||||
|
||||
Make sure you have a "suspension" collection (can be created automatically)
|
||||
`
|
||||
{
|
||||
"end": 1726068961,
|
||||
"targets": {
|
||||
"chat-robot": true,
|
||||
"web app": true
|
||||
}
|
||||
"access": {
|
||||
"tester": true,
|
||||
"developer": true
|
||||
},
|
||||
"description": {
|
||||
"ru": "Разрабатываю каталог, поиск и корзину",
|
||||
"en": "I am developing a catalog, search and cart"
|
||||
}
|
||||
}
|
||||
`
|
||||
|
|
|
@ -1,31 +1,39 @@
|
|||
{
|
||||
"name": "mirzaev/arming_bot",
|
||||
"type": "robot",
|
||||
"tags": [
|
||||
"description": "Chat-robot for tuning weapons",
|
||||
"homepage": "https://t.me/arming_bot",
|
||||
"type": "chat-robot",
|
||||
"keywords": [
|
||||
"telegram",
|
||||
"chat-robot",
|
||||
"military",
|
||||
"shop"
|
||||
],
|
||||
"require": {
|
||||
"triagens/arangodb": "^3.8",
|
||||
"mirzaev/arangodb": "^1.0",
|
||||
"badfarm/zanzara": "^0.9.1",
|
||||
"nyholm/psr7": "^1.8",
|
||||
"react/filesystem": "^0.1.2"
|
||||
},
|
||||
"license": "WTFPL",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"mirzaev\\arming_bot\\": "mirzaev/arming_bot/system/"
|
||||
}
|
||||
},
|
||||
"readme": "README.md",
|
||||
"license": "WTFPL",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Arsen Mirzaev Tatyano-Muradovich",
|
||||
"email": "arsen@mirzaev.sexy"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"triagens/arangodb": "^3.8",
|
||||
"mirzaev/minimal": "^2.2",
|
||||
"mirzaev/arangodb": "^1.3",
|
||||
"badfarm/zanzara": "^0.9.1",
|
||||
"nyholm/psr7": "^1.8",
|
||||
"react/filesystem": "^0.1.2",
|
||||
"twig/twig": "^3.10",
|
||||
"twig/extra-bundle": "^3.7",
|
||||
"twig/intl-extra": "^3.10",
|
||||
"phpoffice/phpspreadsheet": "^2.1"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"mirzaev\\arming_bot\\": "mirzaev/arming_bot/system/"
|
||||
}
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,280 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace mirzaev\arming_bot\controllers;
|
||||
|
||||
// Files of the project
|
||||
use mirzaev\arming_bot\controllers\core,
|
||||
mirzaev\arming_bot\models\categories,
|
||||
mirzaev\arming_bot\models\category,
|
||||
mirzaev\arming_bot\models\product;
|
||||
|
||||
/**
|
||||
* Controller of catalog
|
||||
*
|
||||
* @package mirzaev\arming_bot\controllers
|
||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||
*/
|
||||
final class catalog extends core
|
||||
{
|
||||
/**
|
||||
* Registry of errors
|
||||
*/
|
||||
protected array $errors = [
|
||||
'session' => [],
|
||||
'account' => [],
|
||||
'catalog' => []
|
||||
];
|
||||
|
||||
/**
|
||||
* Catalog
|
||||
*
|
||||
* @param array $parameters Parameters of the request (POST + GET)
|
||||
*/
|
||||
public function index(array $parameters = []): ?string
|
||||
{
|
||||
if (!empty($parameters['categories']) && $parameters['categories'] !== ['/']) {
|
||||
// Переданы категории ["category1", "category2", "category3"] (иерархия)
|
||||
|
||||
// Инициализация актуальной категории
|
||||
$category = end($parameters['categories']);
|
||||
|
||||
if ($model = categories::category($category)) {
|
||||
// Найдена модель обработки актуальной категории
|
||||
|
||||
if (method_exists($model, 'entries')) {
|
||||
// Найден метод поиска вхождений
|
||||
|
||||
// Поиск категорий или товаров входящих в актуальную категорию
|
||||
$entries = $model::entries(
|
||||
category: $model->getId(),
|
||||
filter: 'v.deleted != true && v.hidden != true',
|
||||
amount: 30,
|
||||
errors: $this->errors['catalog']
|
||||
);
|
||||
|
||||
// Объявление буферов категорий и товаров (важно - в единственном числе, по параметру из базы данных)
|
||||
$category = $product = [];
|
||||
|
||||
foreach ($entries as $entry) {
|
||||
// Перебор вхождений
|
||||
|
||||
// Запись массивов категорий и товаров ($category и $product) в буфер глобальной переменной шаблонизатора
|
||||
${$entry->_type}[] = $entry;
|
||||
}
|
||||
|
||||
// Запись категорий из буфера в глобальную переменную шаблонизатора
|
||||
$this->view->categories = $category;
|
||||
|
||||
// Запись товаров из буфера в глобальную переменную шаблонизатора
|
||||
$this->view->products = $product;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Не переданы категории
|
||||
|
||||
// Поиск категорий: "categories"
|
||||
// @todo сделать автоматический поиск "самой верхней" категории
|
||||
$this->view->categories = category::_read(
|
||||
filter: 'd.deleted != true && d.hidden != true',
|
||||
sort: 'd.position ASC, d.name ASC, d.created DESC',
|
||||
amount: 30,
|
||||
errors: $this->errors['catalog']
|
||||
);
|
||||
|
||||
// Search for products
|
||||
/* $this->view->products = product::read(
|
||||
filter: 'd.deleted != true && d.hidden != true',
|
||||
sort: 'd.promoting ASC, d.position ASC, d.created DESC',
|
||||
amount: 6,
|
||||
errors: $this->errors['catalog']
|
||||
); */
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
|
||||
// GET request
|
||||
|
||||
// Exit (success)
|
||||
return $this->view->render('catalog/page.html');
|
||||
} else if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
// POST request
|
||||
|
||||
// Initializing a response headers
|
||||
header('Content-Type: application/json');
|
||||
header('Content-Encoding: none');
|
||||
header('X-Accel-Buffering: no');
|
||||
|
||||
// Initializing of the output buffer
|
||||
ob_start();
|
||||
|
||||
// Generating the reponse
|
||||
echo json_encode(
|
||||
[
|
||||
'title' => $title ?? '',
|
||||
'html' => [
|
||||
'categories' => $this->view->render('catalog/elements/categories.html'),
|
||||
'products' => $this->view->render('catalog/elements/products/2columns.html')
|
||||
],
|
||||
'errors' => $this->errors
|
||||
]
|
||||
);
|
||||
|
||||
// Initializing a response headers
|
||||
header('Content-Length: ' . ob_get_length());
|
||||
|
||||
// Sending and deinitializing of the output buffer
|
||||
ob_end_flush();
|
||||
flush();
|
||||
|
||||
// Exit (success)
|
||||
return null;
|
||||
}
|
||||
|
||||
// Exit (fail)
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search
|
||||
*
|
||||
* @param array $parameters Parameters of the request (POST + GET)
|
||||
*/
|
||||
public function search(array $parameters = []): ?string
|
||||
{
|
||||
// Initializing of text fore search
|
||||
preg_match('/[\w\s]+/u', $parameters['text'] ?? '', $matches);
|
||||
$text = $matches[0] ?? null;
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
// POST request
|
||||
|
||||
// Search for products
|
||||
$this->view->products = isset($text) ? product::read(
|
||||
search: $text,
|
||||
filter: 'd.deleted != true && d.hidden != true',
|
||||
sort: 'd.position ASC, d.name ASC, d.created DESC',
|
||||
amount: 30,
|
||||
errors: $this->errors['catalog']
|
||||
) : [];
|
||||
|
||||
// Initializing a response headers
|
||||
header('Content-Type: application/json');
|
||||
header('Content-Encoding: none');
|
||||
header('X-Accel-Buffering: no');
|
||||
|
||||
// Initializing of the output buffer
|
||||
ob_start();
|
||||
|
||||
// Generating the reponse
|
||||
echo json_encode(
|
||||
[
|
||||
'title' => $title ?? '',
|
||||
'html' => [
|
||||
'products' => $this->view->render('catalog/elements/products.html')
|
||||
],
|
||||
'errors' => $this->errors
|
||||
]
|
||||
);
|
||||
|
||||
// Initializing a response headers
|
||||
header('Content-Length: ' . ob_get_length());
|
||||
|
||||
// Sending and deinitializing of the output buffer
|
||||
ob_end_flush();
|
||||
flush();
|
||||
|
||||
// Exit (success)
|
||||
return null;
|
||||
}
|
||||
|
||||
// Exit (fail)
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Product
|
||||
*
|
||||
* @param array $parameters Parameters of the request (POST + GET)
|
||||
*/
|
||||
public function product(array $parameters = []): ?string
|
||||
{
|
||||
// Initializing of text fore search
|
||||
preg_match('/[\d]+/', $parameters['id'] ?? '', $matches);
|
||||
$_key = $matches[0] ?? null;
|
||||
|
||||
if (!empty($_key)) {
|
||||
// Received id of prouct (_key)
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
// POST request
|
||||
|
||||
// Search for products
|
||||
$product = product::read(
|
||||
filter: "d._key == \"$_key\" && d.deleted != true && d.hidden != true",
|
||||
sort: 'd.created DESC',
|
||||
amount: 1,
|
||||
return: '{id: d._key, title: d.title.ru, description: d.description.ru, cost: d.cost, weight: d.weight, dimensions: d.dimensions, brand: d.brand.ru, compatibility: d.compatibility.ru}',
|
||||
errors: $this->errors['catalog']
|
||||
)[0]?->getAll();
|
||||
|
||||
if (!empty($product)) {
|
||||
// Found the product
|
||||
|
||||
// Initializing buffer of images
|
||||
$images = [];
|
||||
|
||||
foreach (
|
||||
glob(INDEX .
|
||||
DIRECTORY_SEPARATOR .
|
||||
'themes' .
|
||||
DIRECTORY_SEPARATOR .
|
||||
(THEME ?? 'default') .
|
||||
DIRECTORY_SEPARATOR .
|
||||
'images' .
|
||||
DIRECTORY_SEPARATOR .
|
||||
$_key .
|
||||
DIRECTORY_SEPARATOR .
|
||||
"*.{jpg,png,gif}", GLOB_BRACE) as $file
|
||||
) {
|
||||
// Iterate over images of the product
|
||||
|
||||
// Write to buffer of images
|
||||
$images[] = "/images/$_key/" . basename($file);
|
||||
}
|
||||
|
||||
$product = $product + ['images' => $images];
|
||||
}
|
||||
|
||||
// Initializing a response headers
|
||||
header('Content-Type: application/json');
|
||||
header('Content-Encoding: none');
|
||||
header('X-Accel-Buffering: no');
|
||||
|
||||
// Initializing of the output buffer
|
||||
ob_start();
|
||||
|
||||
// Generating the reponse
|
||||
echo json_encode(
|
||||
[
|
||||
'product' => $product,
|
||||
'errors' => $this->errors
|
||||
]
|
||||
);
|
||||
|
||||
// Initializing a response headers
|
||||
header('Content-Length: ' . ob_get_length());
|
||||
|
||||
// Sending and deinitializing of the output buffer
|
||||
ob_end_flush();
|
||||
flush();
|
||||
|
||||
// Exit (success)
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Exit (fail)
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,193 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace mirzaev\arming_bot\controllers;
|
||||
|
||||
// Files of the project
|
||||
use mirzaev\arming_bot\views\templater,
|
||||
mirzaev\arming_bot\models\core as models,
|
||||
mirzaev\arming_bot\models\account,
|
||||
mirzaev\arming_bot\models\session,
|
||||
mirzaev\arming_bot\models\settings,
|
||||
mirzaev\arming_bot\models\suspension;
|
||||
|
||||
// Library for ArangoDB
|
||||
use ArangoDBClient\Document as _document;
|
||||
|
||||
// Framework for PHP
|
||||
use mirzaev\minimal\controller;
|
||||
|
||||
/**
|
||||
* Core of controllers
|
||||
*
|
||||
* @package mirzaev\arming_bot\controllers
|
||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||
*/
|
||||
class core extends controller
|
||||
{
|
||||
/**
|
||||
* Postfix for name of controllers files
|
||||
*/
|
||||
final public const string POSTFIX = '';
|
||||
|
||||
/**
|
||||
* Instance of the settings
|
||||
*/
|
||||
public static settings $settings;
|
||||
|
||||
/**
|
||||
* Instance of a session
|
||||
*/
|
||||
protected readonly session $session;
|
||||
|
||||
/**
|
||||
* Instance of an account
|
||||
*/
|
||||
protected readonly ?account $account;
|
||||
|
||||
/**
|
||||
* Registry of errors
|
||||
*/
|
||||
protected array $errors = [
|
||||
'session' => [],
|
||||
'account' => []
|
||||
];
|
||||
|
||||
/**
|
||||
* Constructor of an instance
|
||||
*
|
||||
* @param bool $initialize Initialize a controller?
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(bool $initialize = true)
|
||||
{
|
||||
// Blocking requests from CloudFlare (better to write this blocking into nginx config file)
|
||||
if (isset($_SERVER['HTTP_USER_AGENT']) && $_SERVER['HTTP_USER_AGENT'] === 'nginx-ssl early hints') return;
|
||||
|
||||
// For the extends system
|
||||
parent::__construct($initialize);
|
||||
|
||||
if ($initialize) {
|
||||
// Initializing is requested
|
||||
|
||||
// Initializing of models core (connect to ArangoDB...)
|
||||
new models();
|
||||
|
||||
// Initializing of the date until which the session will be active
|
||||
$expires = strtotime('+1 week');
|
||||
|
||||
// Initializing of default value of hash of the session
|
||||
$_COOKIE["session"] ??= null;
|
||||
|
||||
// Initializing of a session
|
||||
$this->session = new session($_COOKIE["session"], $expires, $this->errors['session']);
|
||||
|
||||
// Handle a problems with initializing a session
|
||||
if (!empty($this->errors['session'])) exit(1);
|
||||
|
||||
// телеграм не сохраняет куки
|
||||
/* else if ($_COOKIE["session"] !== $this->session->hash) {
|
||||
// Hash of the session is changed (implies that the session has expired and recreated)
|
||||
|
||||
// Write a new hash of the session to cookies
|
||||
setcookie(
|
||||
'session',
|
||||
$this->session->hash,
|
||||
[
|
||||
'expires' => $expires,
|
||||
'path' => '/',
|
||||
'secure' => true,
|
||||
'httponly' => true,
|
||||
'samesite' => 'strict'
|
||||
]
|
||||
);
|
||||
} */
|
||||
|
||||
// Initializing of the account
|
||||
$this->account = $this->session->account($this->errors['account']);
|
||||
|
||||
// Initializing of the settings
|
||||
self::$settings = settings::active();
|
||||
|
||||
// Initializing of preprocessor of views
|
||||
$this->view = new templater($this->session, $this->account);
|
||||
|
||||
// @todo перенести в middleware
|
||||
|
||||
// Search for suspensions
|
||||
$suspension = suspension::search();
|
||||
|
||||
if ($suspension && $suspension->targets['web app']) {
|
||||
// Found a suspension
|
||||
|
||||
if ($this->account) {
|
||||
// Initialized account
|
||||
|
||||
foreach ($suspension->access as $type => $status) {
|
||||
// Перебор статусов доступа
|
||||
|
||||
if ($status && $this->account->{$type}) {
|
||||
// Authorized account
|
||||
|
||||
// Exit (success)
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
// Exit (success)
|
||||
goto suspension;
|
||||
} else {
|
||||
// Not initialized account
|
||||
|
||||
// Send the suspension page and exit (success)
|
||||
suspension:
|
||||
|
||||
// Write title of the page to templater global variables
|
||||
$this->view->title = match ($account?->language ?? self::$settings?->language) {
|
||||
'ru' => 'Приостановлено',
|
||||
'en' => 'Suspended',
|
||||
default => 'Suspended'
|
||||
};
|
||||
|
||||
// Write description of the suspension to templater global variables
|
||||
$this->view->description = $suspension->description[$account?->language ?? self::$settings?->language] ?? array_values($suspension->description)[0];
|
||||
|
||||
// Write message of remaining time of the suspension to templater global variables
|
||||
$this->view->remain = [
|
||||
'title' => match ($account?->language ?? self::$settings?->language) {
|
||||
'ru' => 'Осталось времени: ',
|
||||
'en' => 'Time remaining: ',
|
||||
default => 'Time remaining: '
|
||||
},
|
||||
'value' => $suspension?->message()
|
||||
];
|
||||
|
||||
// Send the suspension page
|
||||
echo $this->view->render('suspension/page.html');
|
||||
|
||||
// Exit (success)
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check of initialization
|
||||
*
|
||||
* Checks whether a property is initialized in a document instance from ArangoDB
|
||||
*
|
||||
* @param string $name Name of the property from ArangoDB
|
||||
*
|
||||
* @return bool The property is initialized?
|
||||
*/
|
||||
public function __isset(string $name): bool
|
||||
{
|
||||
// Check of initialization of the property and exit (success)
|
||||
return match ($name) {
|
||||
default => isset($this->{$name})
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace mirzaev\arming_bot\controllers;
|
||||
|
||||
// Files of the project
|
||||
use mirzaev\arming_bot\controllers\core,
|
||||
mirzaev\arming_bot\models\product;
|
||||
|
||||
/**
|
||||
* Index controller
|
||||
*
|
||||
* @package mirzaev\arming_bot\controllers
|
||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||
*/
|
||||
final class index extends core
|
||||
{
|
||||
/**
|
||||
* Render the main page
|
||||
*
|
||||
* @param array $parameters Parameters of the request (POST + GET)
|
||||
*/
|
||||
public function index(array $parameters = []): ?string
|
||||
{
|
||||
// Поиск товаров
|
||||
/* $this->view->products = product::read(); */
|
||||
|
||||
// Exit (success)
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'GET') return $this->view->render('catalog.html');
|
||||
|
||||
// Exit (fail)
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace mirzaev\arming_bot\controllers;
|
||||
|
||||
// Files of the project
|
||||
use mirzaev\arming_bot\controllers\core,
|
||||
mirzaev\arming_bot\models\account;
|
||||
|
||||
/**
|
||||
* Controller of session
|
||||
*
|
||||
* @package mirzaev\arming_bot\controllers
|
||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||
*/
|
||||
final class session extends core
|
||||
{
|
||||
/**
|
||||
* Registry of errors
|
||||
*/
|
||||
protected array $errors = [
|
||||
'session' => [],
|
||||
'account' => []
|
||||
];
|
||||
|
||||
/**
|
||||
* Connect session to the telegram account
|
||||
*
|
||||
* @param array $parameters Parameters of the request (POST + GET)
|
||||
*/
|
||||
public function telegram(array $parameters = []): ?string
|
||||
{
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
// POST request
|
||||
|
||||
if ($connected = isset($this->account)) {
|
||||
// Found the account
|
||||
|
||||
// Initializing language of the account
|
||||
$language = $this->account->language;
|
||||
} else {
|
||||
// Not found the account
|
||||
|
||||
if (count($parameters) > 1 && isset($parameters['hash'])) {
|
||||
|
||||
$buffer = $parameters;
|
||||
|
||||
unset($buffer['authentication'], $buffer['hash']);
|
||||
ksort($buffer);
|
||||
|
||||
$prepared = [];
|
||||
foreach ($buffer as $key => $value) {
|
||||
if (is_array($value)) {
|
||||
$prepared[] = $key . '=' . json_encode($value, JSON_UNESCAPED_UNICODE);
|
||||
} else {
|
||||
$prepared[] = $key . '=' . $value;
|
||||
}
|
||||
}
|
||||
|
||||
$key = hash_hmac('sha256', require(SETTINGS . DIRECTORY_SEPARATOR . 'key.php'), 'WebAppData', true);
|
||||
$hash = bin2hex(hash_hmac('sha256', implode(PHP_EOL, $prepared), $key, true));
|
||||
|
||||
if (hash_equals($hash, $parameters['hash'])) {
|
||||
// Data confirmed (according to telegram documentation)
|
||||
|
||||
if (time() - $parameters['auth_date'] < 86400) {
|
||||
// Authorization date less than 1 day ago
|
||||
|
||||
// Initializing data of the account
|
||||
$data = json_decode($parameters['user']);
|
||||
|
||||
// Initializing of the account
|
||||
$account = account::initialization(
|
||||
$data->id,
|
||||
[
|
||||
'id' => $data->id,
|
||||
'name' => [
|
||||
'first' => $data->first_name,
|
||||
'last' => $data->last_name
|
||||
],
|
||||
'domain' => $data->username,
|
||||
'language' => $data->language_code,
|
||||
'messages' => $data->allows_write_to_pm
|
||||
],
|
||||
$this->errors['account']
|
||||
);
|
||||
|
||||
if ($account instanceof account) {
|
||||
// Initialized the account
|
||||
|
||||
// Connecting the account to the session
|
||||
$connected = $this->session->connect($account, $this->errors['session']);
|
||||
|
||||
// Initializing language of the account
|
||||
$language = $account->language;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initializing a response headers
|
||||
header('Content-Type: application/json');
|
||||
header('Content-Encoding: none');
|
||||
header('X-Accel-Buffering: no');
|
||||
|
||||
// Initializing of the output buffer
|
||||
ob_start();
|
||||
|
||||
// Generating the reponse
|
||||
echo json_encode(
|
||||
[
|
||||
'connected' => (bool) $connected,
|
||||
'language' => $language ?? null,
|
||||
'errors' => $this->errors
|
||||
]
|
||||
);
|
||||
|
||||
// Initializing a response headers
|
||||
header('Content-Length: ' . ob_get_length());
|
||||
|
||||
// Sending and deinitializing of the output buffer
|
||||
ob_end_flush();
|
||||
flush();
|
||||
|
||||
// Exit (success)
|
||||
return null;
|
||||
}
|
||||
|
||||
// Exit (fail)
|
||||
return null;
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,124 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace mirzaev\arming_bot\models;
|
||||
|
||||
// Files of the project
|
||||
use mirzaev\arming_bot\models\core,
|
||||
mirzaev\arming_bot\models\traits\status,
|
||||
mirzaev\arming_bot\models\traits\document as arangodb_document_trait,
|
||||
mirzaev\arming_bot\models\interfaces\document as arangodb_document_interface;
|
||||
|
||||
// Framework for ArangoDB
|
||||
use mirzaev\arangodb\collection,
|
||||
mirzaev\arangodb\document;
|
||||
|
||||
// Library для ArangoDB
|
||||
use ArangoDBClient\Document as _document;
|
||||
|
||||
// Framework for Telegram
|
||||
use Zanzara\Telegram\Type\User as telegram;
|
||||
|
||||
// Built-in libraries
|
||||
use exception;
|
||||
|
||||
/**
|
||||
* Model of an account
|
||||
*
|
||||
* @package mirzaev\arming_bot\models
|
||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||
*/
|
||||
final class account extends core implements arangodb_document_interface
|
||||
{
|
||||
use status, arangodb_document_trait;
|
||||
|
||||
/**
|
||||
* Name of the collection in ArangoDB
|
||||
*/
|
||||
final public const string COLLECTION = 'account';
|
||||
|
||||
/**
|
||||
* Инициализация
|
||||
*
|
||||
* @param int $id Идентификатор Telegram
|
||||
* @param telegram|array|null $registration Данные для регистрация, если аккаунт не найден
|
||||
* @param array &$errors Registry of errors
|
||||
*
|
||||
* @return static|null Объект аккаунта, если найден
|
||||
*/
|
||||
public static function initialization(int $id, telegram|array|null $registration = null, array &$errors = []): static|null
|
||||
{
|
||||
try {
|
||||
if (collection::init(core::$arangodb->session, self::COLLECTION)) {
|
||||
if ($document = collection::search(core::$arangodb->session, sprintf("FOR d IN %s FILTER d.id == %u RETURN d", self::COLLECTION, $id))) {
|
||||
// Найден аккаунт
|
||||
|
||||
// Инициализация объекта аккаунта
|
||||
$account = new static;
|
||||
|
||||
// Запись инстанции документа в объект
|
||||
$account->document = $document;
|
||||
|
||||
// Возврат (успех)
|
||||
return $account;
|
||||
} else if ($registration) {
|
||||
// Не найден аккаунт и запрошена его регистрация
|
||||
|
||||
// Создание аккаунта
|
||||
document::write(
|
||||
core::$arangodb->session,
|
||||
self::COLLECTION,
|
||||
(is_array($registration)
|
||||
? $registration :
|
||||
[
|
||||
'id' => $registration->getId(),
|
||||
'name' => [
|
||||
'first' => $registration->getFirstName(),
|
||||
'last' => $registration->getLastName()
|
||||
],
|
||||
'domain' => $registration->getUsername(),
|
||||
'robot' => $registration->isBot(),
|
||||
'banned' => false,
|
||||
'tester' => false,
|
||||
'developer' => false,
|
||||
'access' => [
|
||||
'settings' => false
|
||||
],
|
||||
'menus' => [
|
||||
'attachments' => $registration->getAddedToAttachmentMenu()
|
||||
],
|
||||
'messages' => true,
|
||||
'groups' => [
|
||||
'join' => $registration->getCanJoinGroups(),
|
||||
'messages' => $registration->getCanReadAllGroupMessages()
|
||||
],
|
||||
'premium' => $registration->isPremium(),
|
||||
'language' => $registration->getLanguageCode(),
|
||||
'queries' => [
|
||||
'inline' => $registration->getSupportsInlineQueries()
|
||||
]
|
||||
]) + [
|
||||
'version' => ROBOT_VERSION,
|
||||
'active' => true
|
||||
]
|
||||
);
|
||||
|
||||
// Инициализация (без регистрации)
|
||||
return static::initialization($id, errors: $errors);
|
||||
} else throw new exception('Account not found');
|
||||
} else throw new exception('Failed to initialize document collection: ' . self::COLLECTION);
|
||||
} catch (exception $e) {
|
||||
// Write to the registry of errors
|
||||
$errors[] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
}
|
||||
|
||||
// Выход (провал)
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,294 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace mirzaev\arming_bot\models;
|
||||
|
||||
// Files of the project
|
||||
use mirzaev\arming_bot\models\core,
|
||||
mirzaev\arming_bot\models\traits\document as arangodb_document_trait,
|
||||
mirzaev\arming_bot\models\interfaces\document as arangodb_document_interface;
|
||||
|
||||
// Framework for ArangoDB
|
||||
use mirzaev\arangodb\collection,
|
||||
mirzaev\arangodb\document;
|
||||
|
||||
// Library для ArangoDB
|
||||
use ArangoDBClient\Document as _document;
|
||||
|
||||
// Built-in libraries
|
||||
use exception;
|
||||
|
||||
/**
|
||||
* Model of a category
|
||||
*
|
||||
* @package mirzaev\arming_bot\models
|
||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||
*/
|
||||
class categories extends core implements arangodb_document_interface
|
||||
{
|
||||
use arangodb_document_trait;
|
||||
|
||||
/**
|
||||
* Name of the collection in ArangoDB
|
||||
*/
|
||||
public const string COLLECTION = 'THIS_COLLECTION_SHOULD_NOT_EXIST';
|
||||
|
||||
/**
|
||||
* Запись категории
|
||||
*
|
||||
* @param string $name Название
|
||||
* @param arrau $labels Ярлыки для отображения в интерфейсе ['EN' => "Label"]
|
||||
* @param array $hierarchy Иерархия вложенности (от родителей к потомкам: [ model, model ... ])
|
||||
* @param array &$errors Registry of errors
|
||||
*
|
||||
* @return string|null Идентификатор (_id) документа, если создан
|
||||
*/
|
||||
public static function write(
|
||||
string $name,
|
||||
array $labels = ['RU' => 'Без названия'],
|
||||
array $hierarchy = [],
|
||||
array &$errors = []
|
||||
): string|null {
|
||||
try {
|
||||
if (collection::init(core::$arangodb->session, static::COLLECTION)) {
|
||||
// Инициализирована коллекция
|
||||
|
||||
// Создание категории
|
||||
$category = document::write(
|
||||
core::$arangodb->session,
|
||||
static::COLLECTION,
|
||||
[
|
||||
'name' => $name,
|
||||
'labels' => $labels,
|
||||
'version' => ROBOT_VERSION
|
||||
]
|
||||
);
|
||||
|
||||
if ($category) {
|
||||
// Создана категория
|
||||
|
||||
/* if (collection::init(core::$arangodb->session, 'entry', true)) {
|
||||
// Инициализирована коллекция
|
||||
|
||||
foreach ($hierarchy as $model) {
|
||||
// Перебор иерархической структуры категорий
|
||||
|
||||
// Инициализация вложенной категории (следующей в массиве)
|
||||
$next = current($hierarchy);
|
||||
|
||||
// Поиск ребра описывающего иерархическую связь
|
||||
|
||||
document::write(core::$arangodb->session, 'entry');
|
||||
}
|
||||
} else throw new exception('Failed to initialize document collection: ' . static::COLLECTION); */
|
||||
}
|
||||
} else throw new exception('Failed to initialize edge collection: entry');
|
||||
} catch (exception $e) {
|
||||
// Write to the registry of errors
|
||||
$errors[] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
}
|
||||
|
||||
// Выход (провал)
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Поиск вхождений (подкатегории или товары)
|
||||
*
|
||||
* Находит вхождения через ребро entry
|
||||
* Генерирует _type со значениями "category" и "product"
|
||||
* относительно того есть ли у документа ещё вложения (у product вложений быть не может)
|
||||
* Объединяет возвращаемые объекты документа с переменной _type
|
||||
*
|
||||
* @param string|null $category Category identifier (_id)
|
||||
* @param string|null $filter Expression for filtering (AQL)
|
||||
* @param string|null $sort Expression for sorting (AQL)
|
||||
* @param int $page Страница
|
||||
* @param int $amount Количество товаров на странице
|
||||
* @param array &$errors Registry of errors
|
||||
*
|
||||
* @return array Массив с найденными вхождениями (может быть пустым)
|
||||
*/
|
||||
public static function entries(
|
||||
?string $category = null,
|
||||
?string $filter = null,
|
||||
?string $sort = 'v.promotion DESC, v.position ASC, v.created DESC',
|
||||
int $page = 1,
|
||||
int $amount = 100,
|
||||
array &$errors = []
|
||||
): array {
|
||||
try {
|
||||
if (collection::init(core::$arangodb->session, static::COLLECTION)) {
|
||||
// Инициализирована коллекция
|
||||
|
||||
if ($documents = collection::search(
|
||||
core::$arangodb->session,
|
||||
sprintf(
|
||||
<<<AQL
|
||||
FOR v IN 1..1 INBOUND %s GRAPH hierarchy
|
||||
%s
|
||||
%s
|
||||
LIMIT %u, %u
|
||||
LET _type = (FOR v2 IN INBOUND v._id GRAPH hierarchy RETURN v2)[0] ? "category" : "product"
|
||||
RETURN MERGE(v, {_type})
|
||||
AQL,
|
||||
empty($category) ? '(FOR d IN ' . ${static::COLLECTION} . 'LIMIT 1 RETURN d._id)[0]' : "\"$category\"",
|
||||
empty($filter) ? '' : "FILTER $filter",
|
||||
empty($sort) ? '' : "SORT $sort",
|
||||
--$page <= 0 ? $page = 0 : $page * $amount,
|
||||
$amount,
|
||||
)
|
||||
)) {
|
||||
// Найдены вхождения
|
||||
|
||||
// Возврат (успех)
|
||||
return is_array($documents) ? $documents : [$documents];
|
||||
} else return [];
|
||||
} else throw new exception('Failed to initialize document collection: ' . static::COLLECTION);
|
||||
} catch (exception $e) {
|
||||
// Write to the registry of errors
|
||||
$errors[] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
}
|
||||
|
||||
// Выход (провал)
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Поиск категорий и товаров
|
||||
*
|
||||
* Ищет категории и товары по коллекции рёбер entry из _from и _to
|
||||
*
|
||||
* @param array &$errors Registry of errors
|
||||
*
|
||||
* @return array Массив с уникализированными найденными коллекциями (может быть пустым)
|
||||
*/
|
||||
protected static function collections(
|
||||
array &$errors = []
|
||||
): array {
|
||||
try {
|
||||
if (collection::init(core::$arangodb->session, $collection = 'entry')) {
|
||||
// Инициализирована коллекция
|
||||
|
||||
if ($names = collection::search(
|
||||
core::$arangodb->session,
|
||||
sprintf(
|
||||
<<<AQL
|
||||
FOR e IN %s
|
||||
COLLECT AGGREGATE
|
||||
from_category = UNIQUE(PARSE_IDENTIFIER(e._from).collection),
|
||||
to_category = UNIQUE(PARSE_IDENTIFIER(e._to).collection)
|
||||
RETURN UNIQUE(UNION(from_category, to_category))
|
||||
AQL,
|
||||
$collection
|
||||
)
|
||||
)) {
|
||||
// Найдены коллекции
|
||||
|
||||
// Возврат (успех)
|
||||
return $names->getAll();
|
||||
} else return [];
|
||||
} else throw new exception('Failed to initialize edge collection: entry');
|
||||
} catch (exception $e) {
|
||||
// Write to the registry of errors
|
||||
$errors[] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
}
|
||||
|
||||
// Выход (провал)
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Поиск категории по названияю
|
||||
*
|
||||
* Перебирает все коллекции из self::collections() и ищет в них соответствие с параметром name
|
||||
* ВНИМАНИЕ: НЕЛЬЗЯ ДОПУСКАТЬ ОДИНАКОВЫЕ НАЗВАНИЯ СРЕДИ РАЗНЫХ КАТЕГОРИЙ
|
||||
*
|
||||
* @param string $name Name of a collection
|
||||
* @param array &$errors Registry of errors
|
||||
*
|
||||
* @return categories|null Модель имплементирующая документ с категорией, если была найдена
|
||||
*/
|
||||
public static function category(
|
||||
string $name,
|
||||
array &$errors = []
|
||||
): ?categories {
|
||||
try {
|
||||
// Инициалиация списка коллекций
|
||||
$collections = self::collections($errors);
|
||||
|
||||
if (count($collections) > 0) {
|
||||
// Найдены коллекции
|
||||
|
||||
// Инициализация буфера части запроса со списком коллекций для аргументов UNION()
|
||||
$union = [];
|
||||
foreach ($collections as $collection) $union[] = "FOR d IN $collection RETURN d";
|
||||
unset($collection);
|
||||
|
||||
if ($document = collection::search(
|
||||
core::$arangodb->session,
|
||||
sprintf(
|
||||
<<<AQL
|
||||
FOR u IN UNION(%s)
|
||||
FILTER u.name == "%s"
|
||||
SORT u.created ASC
|
||||
LIMIT 1
|
||||
RETURN u
|
||||
AQL,
|
||||
implode(', ', $union),
|
||||
$name
|
||||
)
|
||||
)) {
|
||||
// Найдена категория
|
||||
|
||||
// Инициализация названия коллекции
|
||||
$collection = explode('/', $document->getId())[0];
|
||||
|
||||
if (class_exists($model = "mirzaev\\arming_bot\\models\\$collection")) {
|
||||
// Найдена модель имплементирующая документы из этой коллекции
|
||||
|
||||
// Инициализация объекта модели
|
||||
$object = new $model;
|
||||
|
||||
if ($object instanceof categories) {
|
||||
// Объект является инстанцией категории (то есть не товара)
|
||||
|
||||
// Запись инстанции документа в объект модели
|
||||
$object->document = $document;
|
||||
|
||||
// Возврат (успех)
|
||||
return $object;
|
||||
}
|
||||
}
|
||||
} else return null;
|
||||
}
|
||||
} catch (exception $e) {
|
||||
// Write to the registry of errors
|
||||
$errors[] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
}
|
||||
|
||||
// Выход (провал)
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace mirzaev\arming_bot\models;
|
||||
|
||||
// Files of the project
|
||||
use mirzaev\arming_bot\models\categories;
|
||||
|
||||
/**
|
||||
* Model of a category
|
||||
*
|
||||
* @package mirzaev\arming_bot\models
|
||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||
*/
|
||||
final class category extends categories
|
||||
{
|
||||
/**
|
||||
* Name of the collection in ArangoDB
|
||||
*/
|
||||
final public const string COLLECTION = 'category';
|
||||
}
|
|
@ -0,0 +1,602 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace mirzaev\arming_bot\models;
|
||||
|
||||
// Files of the project
|
||||
use mirzaev\arming_bot\models\core,
|
||||
mirzaev\arming_bot\controllers\core as controller,
|
||||
mirzaev\arming_bot\models\account;
|
||||
|
||||
// Фреймворк Telegram
|
||||
use Zanzara\Zanzara,
|
||||
Zanzara\Context,
|
||||
Zanzara\Telegram\Type\Input\InputFile,
|
||||
Zanzara\Telegram\Type\File\Document as telegram_document,
|
||||
Zanzara\Telegram\Type\File\File,
|
||||
Zanzara\Middleware\MiddlewareNode as Node;
|
||||
|
||||
// Library для ArangoDB
|
||||
use ArangoDBClient\Document as _document;
|
||||
|
||||
/**
|
||||
* Model of a chat
|
||||
*
|
||||
* @package mirzaev\arming_bot\models
|
||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||
*/
|
||||
final class chat extends core
|
||||
{
|
||||
/**
|
||||
* Экранирование символов для Markdown
|
||||
*
|
||||
* @param string $text Текст для экранирования
|
||||
* @param array $exception Символы которые будут исключены из списка для экранирования
|
||||
*
|
||||
* @return string Экранированный текст
|
||||
*/
|
||||
public static function unmarkdown(string $text, array $exceptions = []): string
|
||||
{
|
||||
// Инициализация реестра символом для конвертации
|
||||
$from = array_diff(
|
||||
[
|
||||
'#',
|
||||
'*',
|
||||
'_',
|
||||
'=',
|
||||
'.',
|
||||
'[',
|
||||
']',
|
||||
'(',
|
||||
')',
|
||||
'-',
|
||||
'>',
|
||||
'<',
|
||||
'!',
|
||||
'`'
|
||||
],
|
||||
$exceptions
|
||||
);
|
||||
|
||||
// Инициализация реестра целей для конвертации
|
||||
$to = [];
|
||||
foreach ($from as $symbol) $to[] = "\\$symbol";
|
||||
|
||||
// Конвертация и выход (успех)
|
||||
return str_replace($from, $to, $text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Инициализация запчасти
|
||||
*
|
||||
* Проверяет существование запчасти
|
||||
*
|
||||
* @param string $spare Запчасть
|
||||
*
|
||||
* @return string|bool Запчасть, если найдена, иначе false
|
||||
*/
|
||||
public static function spares(string $spare): string|bool
|
||||
{
|
||||
// Поиск запчастей и выход (успех)
|
||||
return match (mb_strtolower($spare)) {
|
||||
'цевьё' => 'Цевьё',
|
||||
default => false
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Главное меню
|
||||
*
|
||||
* Команда: /start
|
||||
*
|
||||
* @param Context $ctx
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function menu(Context $ctx): void
|
||||
{
|
||||
// Инициализация клавиатуры
|
||||
$keyboard = [
|
||||
[
|
||||
['text' => '🛒 Каталог', 'web_app' => ['url' => 'https://arming.dev.mirzaev.sexy']]
|
||||
],
|
||||
[
|
||||
['text' => '🏛️ О компании'],
|
||||
['text' => '💬 Контакты']
|
||||
],
|
||||
[
|
||||
['text' => '🎯 Сообщество']
|
||||
]
|
||||
];
|
||||
|
||||
if ($ctx->get('account')?->access['settings']) $keyboard[] = [['text' => '⚙️ Настройки']];
|
||||
|
||||
// Отправка сообщения
|
||||
$ctx->sendMessage(
|
||||
static::unmarkdown(<<<TXT
|
||||
Это сообщение будет отображаться (оно должно быть обязательно) при вызове главного меню командой /start (создаёт кнопки меню снизу)
|
||||
TXT),
|
||||
[
|
||||
'reply_markup' => [
|
||||
'keyboard' => $keyboard,
|
||||
'resize_keyboard' => true
|
||||
],
|
||||
'disable_notification' => true
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Начало работы с чат-роботом
|
||||
*
|
||||
* Команда: /start
|
||||
*
|
||||
* @param Context $ctx
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function start(Context $ctx): void
|
||||
{
|
||||
// Главное меню
|
||||
static::menu($ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Контакты
|
||||
*
|
||||
* Команда: /contacts
|
||||
*
|
||||
* @param Context $ctx
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function contacts(Context $ctx): void
|
||||
{
|
||||
// Отправка сообщения
|
||||
$ctx->sendMessage(static::unmarkdown(<<<TXT
|
||||
Здесь придумать текст для раздела "Контакты"
|
||||
TXT), [
|
||||
'reply_markup' => [
|
||||
'inline_keyboard' => [
|
||||
[
|
||||
['text' => '⚡ Связь с менеджером', 'url' => 'https://t.me/iarming'],
|
||||
],
|
||||
[
|
||||
['text' => '📨 Почта', 'callback_data' => 'mail']
|
||||
],
|
||||
[
|
||||
['text' => '🪖 Сайт', 'url' => 'https://arming.ru'],
|
||||
['text' => '🛒 Wildberries', 'url' => 'https://arming.ru']
|
||||
]
|
||||
]
|
||||
],
|
||||
'link_preview_options' => [
|
||||
'is_disabled' => true
|
||||
],
|
||||
'disable_notification' => true
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Почта
|
||||
*
|
||||
* @param Context $ctx
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function _mail(Context $ctx): void
|
||||
{
|
||||
// Отправка сообщения
|
||||
$ctx->sendMessage(static::unmarkdown(<<<TXT
|
||||
[info@arming.ru](mailto::info@arming.ru)
|
||||
TXT, ['[', ']', '(', ')']), [
|
||||
'link_preview_options' => [
|
||||
'is_disabled' => true
|
||||
],
|
||||
'disable_notification' => true
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Компания
|
||||
*
|
||||
* Команда: /company
|
||||
*
|
||||
* @param Context $ctx
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function company(Context $ctx): void
|
||||
{
|
||||
// Отправка сообщения
|
||||
$ctx->sendMessage(
|
||||
static::unmarkdown(<<<TXT
|
||||
Здесь придумать текст для раздела "Компания"
|
||||
TXT),
|
||||
/* [
|
||||
'reply_markup' => [
|
||||
'inline_keyboard' => [
|
||||
[
|
||||
['text' => '⚡ Связь с менеджером', 'url' => 'https://git.mirzaev.sexy/mirzaev/mashtrash'],
|
||||
['text' => '📨 Почта', 'text' => ''],
|
||||
],
|
||||
[
|
||||
['text' => '🪖 Сайт', 'url' => '']
|
||||
['text' => '🛒 Wildberries', 'url' => '']
|
||||
]
|
||||
]
|
||||
],
|
||||
'link_preview_options' => [
|
||||
'is_disabled' => true
|
||||
]
|
||||
] */
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Сообщество
|
||||
*
|
||||
* Команда: /community
|
||||
*
|
||||
* @param Context $ctx
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function community(Context $ctx): void
|
||||
{
|
||||
// Отправка сообщения
|
||||
$ctx->sendMessage(static::unmarkdown(<<<TXT
|
||||
Здесь придумать текст для раздела "Сообщество"
|
||||
TXT), [
|
||||
'reply_markup' => [
|
||||
'inline_keyboard' => [
|
||||
[
|
||||
['text' => '💬 Основной чат', 'url' => 'https://t.me/arming_zone'],
|
||||
]
|
||||
]
|
||||
],
|
||||
'link_preview_options' => [
|
||||
'is_disabled' => true
|
||||
],
|
||||
'disable_notification' => true
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Настройки (доступ только авторизованным)
|
||||
*
|
||||
* Команда: /settings
|
||||
*
|
||||
* @param Context $ctx
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function settings(Context $ctx): void
|
||||
{
|
||||
if ($ctx->get('account')?->access['settings']) {
|
||||
// Авторизован доступ к настройкам
|
||||
|
||||
// Отправка сообщения
|
||||
$ctx->sendMessage(
|
||||
static::unmarkdown(<<<TXT
|
||||
Панель управления чат-роботом ARMING
|
||||
TXT),
|
||||
[
|
||||
'reply_markup' => [
|
||||
'inline_keyboard' => [
|
||||
[
|
||||
['text' => '📦 Импорт товаров', 'callback_data' => 'import_request'],
|
||||
]
|
||||
]
|
||||
],
|
||||
'link_preview_options' => [
|
||||
'is_disabled' => true
|
||||
],
|
||||
'disable_notification' => true
|
||||
]
|
||||
);
|
||||
} else {
|
||||
// Не авторизован доступ к настройкам
|
||||
|
||||
// Отправка сообщения
|
||||
$ctx->sendMessage('⛔ *Нет доступа*');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Запросить файл для импорта товаров (доступ только авторизованным)
|
||||
*
|
||||
* @param Context $ctx
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function import_request(Context $ctx): void
|
||||
{
|
||||
if ($ctx->get('account')?->access['settings']) {
|
||||
// Авторизован доступ к настройкам
|
||||
|
||||
// Отправка сообщения
|
||||
$ctx->sendMessage(static::unmarkdown('Отправьте документ в формате xlsx со списком товаров'))
|
||||
->then(function ($message) use ($ctx) {
|
||||
// Отправка файла
|
||||
$ctx->sendDocument(new InputFile(CATALOG_EXAMPLE), ['disable_notification' => true]);
|
||||
|
||||
// Импорт файла
|
||||
$ctx->nextStep("import");
|
||||
});
|
||||
} else {
|
||||
// Не авторизован доступ к настройкам
|
||||
|
||||
// Отправка сообщения
|
||||
$ctx->sendMessage('⛔ *Нет доступа*');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Импорт товаров (доступ только авторизованным)
|
||||
*
|
||||
* @param Context $ctx
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function import(Context $ctx): void
|
||||
{
|
||||
if ($ctx->get('account')?->access['settings']) {
|
||||
// Авторизован доступ к настройкам
|
||||
|
||||
// Инициализация документа
|
||||
$document = $ctx->getMessage()?->getDocument();
|
||||
|
||||
if ($document instanceof telegram_document) {
|
||||
// Инициализирован документ
|
||||
|
||||
// Инициализация файла
|
||||
$ctx->getFile($document->getFileId())->then(function ($file) use ($ctx) {
|
||||
|
||||
if ($file->getFileSize() <= 50000000) {
|
||||
// Не превышает 50 мегабайт (50000000 байт) размер файла
|
||||
|
||||
if ($file->getFilePath()['extension'] === 'xlsx') {
|
||||
// Имеет расширение xlsx файл
|
||||
|
||||
// Сохранение файла
|
||||
file_put_contents(STORAGE . DIRECTORY_SEPARATOR . 'import.xlsx', file_get_contents('https://api.telegram.org/file/bot' . KEY . '/' . $file->getFilePath()));
|
||||
|
||||
// Инициализация счётчика загруженных товаров
|
||||
$loaded = $created = $updated = $deleted = $old = $new = 0;
|
||||
|
||||
|
||||
|
||||
// Отправка сообщения
|
||||
$ctx->sendMessage(<<<TXT
|
||||
*Загружено для обработки:* $loaded
|
||||
|
||||
*Добавлено:* $created
|
||||
*Обновлено:* $updated
|
||||
*Удалено:* $deleted
|
||||
|
||||
*Опубликовано в магазине:* $old \-\> *$new*
|
||||
TXT)
|
||||
->then(function ($message) use ($ctx) {
|
||||
// Завершение диалога
|
||||
$ctx->endConversation();
|
||||
});
|
||||
} else {
|
||||
// Не имеет расширение xlsx файл
|
||||
|
||||
// Отправка сообщения
|
||||
$ctx->sendMessage(static::unmarkdown('Файл должен иметь расширение xlsx'));
|
||||
}
|
||||
} else {
|
||||
// Превышает 50 мегабайт (50000000 байт) размер файла
|
||||
|
||||
// Отправка сообщения
|
||||
$ctx->sendMessage(static::unmarkdown('Размер файла не должен превышать 50 мегабайт'));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Не инициализирован документ
|
||||
|
||||
// Отправка сообщения
|
||||
$ctx->sendMessage(static::unmarkdown('Отправьте документ в формате xlsx со списком товаров'));
|
||||
}
|
||||
} else {
|
||||
// Не авторизован доступ к настройкам
|
||||
|
||||
// Отправка сообщения
|
||||
$ctx->sendMessage('⛔ *Нет доступа*');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Инициализация аккаунта (middleware)
|
||||
*
|
||||
* @param Context $ctx
|
||||
* @param Node $next
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function account(Context $ctx, Node $next): void
|
||||
{
|
||||
// Выполнение заблокировано?
|
||||
if ($ctx->get('stop')) return;
|
||||
|
||||
// Инициализация аккаунта Telegram
|
||||
$telegram = $ctx->getEffectiveUser();
|
||||
|
||||
// Инициализация аккаунта
|
||||
$account = account::initialization($telegram->getId(), $telegram);
|
||||
|
||||
if ($account) {
|
||||
// Инициализирован аккаунт
|
||||
|
||||
if ($account->banned) {
|
||||
// Заблокирован аккаунт
|
||||
|
||||
// Отправка сообщения
|
||||
$ctx->sendMessage('⛔ *Ты заблокирован*')
|
||||
->then(function ($message) use ($ctx) {
|
||||
// Завершение диалога
|
||||
$ctx->endConversation();
|
||||
});
|
||||
|
||||
// Блокировка дальнейшего выполнения
|
||||
$ctx->set('stop', true);
|
||||
} else {
|
||||
// Не заблокирован аккаунт
|
||||
|
||||
// Запись в буфер
|
||||
$ctx->set('account', $account);
|
||||
|
||||
// Продолжение выполнения
|
||||
$next($ctx);
|
||||
}
|
||||
} else {
|
||||
// Не инициализирован аккаунт
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Инициализация статуса технических работ (middleware)
|
||||
*
|
||||
* @param Context $ctx
|
||||
* @param Node $next
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function suspension(Context $ctx, Node $next): void
|
||||
{
|
||||
// Выполнение заблокировано?
|
||||
if ($ctx->get('stop')) return;
|
||||
|
||||
// Поиск технических работ
|
||||
$suspension = suspension::search();
|
||||
|
||||
if ($suspension && $suspension->targets['chat-robot']) {
|
||||
// Найдена активная приостановка
|
||||
|
||||
// Инициализация аккаунта
|
||||
$account = $ctx->get('account');
|
||||
|
||||
if ($account) {
|
||||
// Инициализирован аккаунт
|
||||
|
||||
foreach ($suspension->access as $type => $status) {
|
||||
// Перебор статусов доступа
|
||||
|
||||
if ($status && $account->{$type}) {
|
||||
// Авторизован аккаунт
|
||||
|
||||
// Продолжение выполнения
|
||||
$next($ctx);
|
||||
|
||||
// Выход (успех)
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Инициализация сообщения
|
||||
$message = "⚠️ *Работа приостановлена*\n*Оставшееся время\:* " . $suspension->message($account->language ?? controller::$settings?->language);
|
||||
|
||||
// Добавление описания причины приостановки, если найдена
|
||||
if (!empty($suspension->description)) $message .= "\n\n" . $suspension->description[$account->language ?? controller::$settings?->language] ?? array_values($suspension->description)[0];
|
||||
|
||||
// Отправка сообщения
|
||||
$ctx->sendMessage($message)
|
||||
->then(function ($message) use ($ctx) {
|
||||
// Завершение диалога
|
||||
$ctx->endConversation();
|
||||
});
|
||||
|
||||
// Блокировка дальнейшего выполнения
|
||||
$ctx->set('stop', true);
|
||||
} else {
|
||||
// Не найдена активная приостановка
|
||||
|
||||
// Продолжение выполнения
|
||||
$next($ctx);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write
|
||||
*
|
||||
* Write a property into an instance of the ArangoDB document
|
||||
*
|
||||
* @param string $name Name of the property
|
||||
* @param mixed $value Content of the property
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __set(string $name, mixed $value = null): void
|
||||
{
|
||||
// Write to the property into an instance of the ArangoDB document and exit (success)
|
||||
$this->document->{$name} = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read
|
||||
*
|
||||
* Read a property from an instance of the ArangoDB docuemnt
|
||||
*
|
||||
* @param string $name Name of the property
|
||||
*
|
||||
* @return mixed Content of the property
|
||||
*/
|
||||
public function __get(string $name): mixed
|
||||
{
|
||||
// Read a property from an instance of the ArangoDB document and exit (success)
|
||||
return match ($name) {
|
||||
'arangodb' => $this::$arangodb,
|
||||
default => $this->document->{$name}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete
|
||||
*
|
||||
* Deinitialize the property in an instance of the ArangoDB document
|
||||
*
|
||||
* @param string $name Name of the property
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __unset(string $name): void
|
||||
{
|
||||
// Delete the property in an instance of the ArangoDB document and exit (success)
|
||||
unset($this->document->{$name});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check of initialization
|
||||
*
|
||||
* Check of initialization of the property into an instance of the ArangoDB document
|
||||
*
|
||||
* @param string $name Name of the property
|
||||
*
|
||||
* @return bool The property is initialized?
|
||||
*/
|
||||
public function __isset(string $name): bool
|
||||
{
|
||||
// Check of initializatio nof the property and exit (success)
|
||||
return isset($this->document->{$name});
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a method
|
||||
*
|
||||
* Execute a method from an instance of the ArangoDB document
|
||||
*
|
||||
* @param string $name Name of the method
|
||||
* @param array $arguments Arguments for the method
|
||||
*
|
||||
* @return mixed Result of execution of the method
|
||||
*/
|
||||
public function __call(string $name, array $arguments = []): mixed
|
||||
{
|
||||
// Execute the method and exit (success)
|
||||
if (method_exists($this->document, $name)) return $this->document->{$name}($arguments);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,291 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace mirzaev\arming_bot\models;
|
||||
|
||||
// Framework for PHP
|
||||
use mirzaev\minimal\model;
|
||||
|
||||
// Framework for ArangoDB
|
||||
use mirzaev\arangodb\connection as arangodb,
|
||||
mirzaev\arangodb\collection,
|
||||
mirzaev\arangodb\document;
|
||||
|
||||
// Libraries for ArangoDB
|
||||
use ArangoDBClient\Document as _document,
|
||||
ArangoDBClient\DocumentHandler as _document_handler;
|
||||
|
||||
// Built-in libraries
|
||||
use exception;
|
||||
|
||||
/**
|
||||
* Core of models
|
||||
*
|
||||
* @package mirzaev\arming_bot\models
|
||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||
*/
|
||||
class core extends model
|
||||
{
|
||||
/**
|
||||
* Postfix for name of models files
|
||||
*/
|
||||
final public const string POSTFIX = '';
|
||||
|
||||
/**
|
||||
* Path to the file with settings of connecting to the ArangoDB
|
||||
*/
|
||||
final public const string ARANGODB = __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'settings' . DIRECTORY_SEPARATOR . 'arangodb.php';
|
||||
|
||||
/**
|
||||
* Instance of the session of ArangoDB
|
||||
*/
|
||||
protected static arangodb $arangodb;
|
||||
|
||||
/**
|
||||
* Name of the collection in ArangoDB
|
||||
*/
|
||||
public const string COLLECTION = 'THIS_COLLECTION_SHOULD_NOT_EXIST';
|
||||
|
||||
/**
|
||||
* Constructor of an instance
|
||||
*
|
||||
* @param bool $initialize Initialize a model?
|
||||
* @param ?arangodb $arangodb Instance of a session of ArangoDB
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(bool $initialize = true, ?arangodb $arangodb = null)
|
||||
{
|
||||
// For the extends system
|
||||
parent::__construct($initialize);
|
||||
|
||||
if ($initialize) {
|
||||
// Initializing is requested
|
||||
|
||||
if (isset($arangodb)) {
|
||||
// Recieved an instance of a session of ArangoDB
|
||||
|
||||
// Write an instance of a session of ArangoDB to the property
|
||||
$this->__set('arangodb', $arangodb);
|
||||
} else {
|
||||
// Not recieved an instance of a session of ArangoDB
|
||||
|
||||
// Initializing of an instance of a session of ArangoDB
|
||||
$this->__get('arangodb');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read from ArangoDB
|
||||
*
|
||||
* @param string $filter Expression for filtering (AQL)
|
||||
* @param string $sort Expression for sorting (AQL)
|
||||
* @param int $amount Amount of documents for collect
|
||||
* @param int $page Page
|
||||
* @param string $return Expression describing the parameters to return (AQL)
|
||||
* @param array &$errors The registry on errors
|
||||
*
|
||||
* @return mixed An array of instances of documents from ArangoDB, if they are found
|
||||
*/
|
||||
public static function _read(
|
||||
string $filter = '',
|
||||
string $sort = 'd.created DESC, d._key DESC',
|
||||
int $amount = 1,
|
||||
int $page = 1,
|
||||
string $return = 'd',
|
||||
array &$errors = []
|
||||
): _document|array|null {
|
||||
try {
|
||||
if (collection::init(static::$arangodb->session, static::COLLECTION)) {
|
||||
// Initialized the collection
|
||||
|
||||
// Read from ArangoDB and exit (success)
|
||||
$result = collection::search(
|
||||
static::$arangodb->session,
|
||||
sprintf(
|
||||
<<<'AQL'
|
||||
FOR d IN %s
|
||||
%s
|
||||
%s
|
||||
LIMIT %d, %d
|
||||
RETURN %s
|
||||
AQL,
|
||||
static::COLLECTION,
|
||||
empty($filter) ? '' : "FILTER $filter",
|
||||
empty($sort) ? '' : "SORT $sort",
|
||||
--$page <= 0 ? 0 : $page * $amount,
|
||||
$amount,
|
||||
$return
|
||||
)
|
||||
);
|
||||
|
||||
// Выход (успех)
|
||||
return is_array($result) ? $result : [$result];
|
||||
} else throw new exception('Failed to initialize the collection');
|
||||
} catch (exception $e) {
|
||||
// Write to the registry of errors
|
||||
$errors[] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
}
|
||||
|
||||
// Exit (fail)
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update in ArangoDB
|
||||
*
|
||||
* @param _document $instance Instance of the document from ArangoDB
|
||||
*
|
||||
* @return bool Writed to ArangoDB without errors?
|
||||
*/
|
||||
public static function _update(_document $instance): bool
|
||||
{
|
||||
// Update in ArangoDB and exit (success)
|
||||
return document::update(static::$arangodb->session, $instance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete from ArangoDB
|
||||
*
|
||||
* @param _document $instance Instance of the document from ArangoDB
|
||||
* @param array &$errors The registry on errors
|
||||
*
|
||||
* @return bool Deleted from ArangoDB without errors?
|
||||
*/
|
||||
public static function _delete(_document $instance, array &$errors = []): bool
|
||||
{
|
||||
try {
|
||||
if (collection::init(static::$arangodb->session, static::COLLECTION)) {
|
||||
// Initialized the collection
|
||||
|
||||
// Delete from ArangoDB and exit (success)
|
||||
return (new _document_handler(static::$arangodb->session))->remove($instance);
|
||||
} else throw new exception('Failed to initialize the collection');
|
||||
} catch (exception $e) {
|
||||
// Write to the registry of errors
|
||||
$errors[] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
}
|
||||
|
||||
// Exit (fail)
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write
|
||||
*
|
||||
* @param string $name Name of the property
|
||||
* @param mixed $value Value of the property
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __set(string $name, mixed $value = null): void
|
||||
{
|
||||
match ($name) {
|
||||
'arangodb' => (function () use ($value) {
|
||||
if ($this->__isset('arangodb')) {
|
||||
// Is alredy initialized
|
||||
|
||||
// Exit (fail)
|
||||
throw new exception('Forbidden to reinitialize the session of ArangoDB ($this::$arangodb)', 500);
|
||||
} else {
|
||||
// Is not already initialized
|
||||
|
||||
if ($value instanceof arangodb) {
|
||||
// Recieved an appropriate value
|
||||
|
||||
// Write the property and exit (success)
|
||||
self::$arangodb = $value;
|
||||
} else {
|
||||
// Recieved an inappropriate value
|
||||
|
||||
// Exit (fail)
|
||||
throw new exception('Session of ArangoDB ($this::$arangodb) is need to be mirzaev\arangodb\connection', 500);
|
||||
}
|
||||
}
|
||||
})(),
|
||||
default => parent::__set($name, $value)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Read
|
||||
*
|
||||
* @param string $name Name of the property
|
||||
*
|
||||
* @return mixed Content of the property, if they are found
|
||||
*/
|
||||
public function __get(string $name): mixed
|
||||
{
|
||||
return match ($name) {
|
||||
'arangodb' => (function () {
|
||||
try {
|
||||
if (!$this->__isset('arangodb')) {
|
||||
// Is not initialized
|
||||
|
||||
// Initializing of a default value from settings
|
||||
$this->__set('arangodb', new arangodb(require static::ARANGODB));
|
||||
}
|
||||
|
||||
// Exit (success)
|
||||
return self::$arangodb;
|
||||
} catch (exception) {
|
||||
// Exit (fail)
|
||||
return null;
|
||||
}
|
||||
})(),
|
||||
default => parent::__get($name)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete
|
||||
*
|
||||
* @param string $name Name of the property
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __unset(string $name): void
|
||||
{
|
||||
// Deleting a property and exit (success)
|
||||
parent::__unset($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check of initialization
|
||||
*
|
||||
* @param string $name Name of the property
|
||||
*
|
||||
* @return bool The property is initialized?
|
||||
*/
|
||||
public function __isset(string $name): bool
|
||||
{
|
||||
// Check of initialization of the property and exit (success)
|
||||
return parent::__isset($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call a static property or method
|
||||
*
|
||||
* @param string $name Name of the property or the method
|
||||
* @param array $arguments Arguments for the method
|
||||
*/
|
||||
public static function __callStatic(string $name, array $arguments): mixed
|
||||
{
|
||||
return match ($name) {
|
||||
'arangodb' => (new static)->__get('arangodb'),
|
||||
default => throw new exception("Not found: $name", 500)
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace mirzaev\arming_bot\models\enumerations;
|
||||
|
||||
enum session
|
||||
{
|
||||
case hash_only;
|
||||
case hash_else_address;
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace mirzaev\arming_bot\models\interfaces;
|
||||
|
||||
// Library для ArangoDB
|
||||
use ArangoDBClient\Document as _document;
|
||||
|
||||
// Framework for ArangoDB
|
||||
use mirzaev\arangodb\connection as arangodb;
|
||||
|
||||
/**
|
||||
* Interface for implementing a document instance from ArangoDB
|
||||
*
|
||||
* @param _document $document An instance of the ArangoDB document from ArangoDB (protected readonly)
|
||||
*
|
||||
* @package mirzaev\arming_bot\models\traits
|
||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||
*/
|
||||
interface document
|
||||
{
|
||||
/**
|
||||
* Write
|
||||
*
|
||||
* Write a property into an instance of the ArangoDB document
|
||||
*
|
||||
* @param string $name Name of the property
|
||||
* @param mixed $value Content of the property
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __set(string $name, mixed $value = null): void;
|
||||
|
||||
/**
|
||||
* Read
|
||||
*
|
||||
* Read a property from an instance of the ArangoDB docuemnt
|
||||
*
|
||||
* @param string $name Name of the property
|
||||
*
|
||||
* @return mixed Content of the property
|
||||
*/
|
||||
public function __get(string $name): mixed;
|
||||
|
||||
|
||||
/**
|
||||
* Delete
|
||||
*
|
||||
* Deinitialize the property in an instance of the ArangoDB document
|
||||
*
|
||||
* @param string $name Name of the property
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __unset(string $name): void;
|
||||
|
||||
/**
|
||||
* Check of initialization
|
||||
*
|
||||
* Check of initialization of the property into an instance of the ArangoDB document
|
||||
*
|
||||
* @param string $name Name of the property
|
||||
*
|
||||
* @return bool The property is initialized?
|
||||
*/
|
||||
public function __isset(string $name): bool;
|
||||
|
||||
/**
|
||||
* Execute a method
|
||||
*
|
||||
* Execute a method from an instance of the ArangoDB document
|
||||
*
|
||||
* @param string $name Name of the method
|
||||
* @param array $arguments Arguments for the method
|
||||
*
|
||||
* @return mixed Result of execution of the method
|
||||
*/
|
||||
public function __call(string $name, array $arguments = []): mixed;
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace mirzaev\arming_bot\models;
|
||||
|
||||
// Files of the project
|
||||
use mirzaev\arming_bot\models\categories;
|
||||
|
||||
/**
|
||||
* Model of a part
|
||||
*
|
||||
* @package mirzaev\arming_bot\models
|
||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||
*/
|
||||
final class part extends categories
|
||||
{
|
||||
/**
|
||||
* Name of the collection in ArangoDB
|
||||
*/
|
||||
final public const string COLLECTION = 'part';
|
||||
}
|
|
@ -0,0 +1,218 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace mirzaev\arming_bot\models;
|
||||
|
||||
// Files of the project
|
||||
use mirzaev\arming_bot\models\core;
|
||||
|
||||
// Framework for ArangoDB
|
||||
use mirzaev\arangodb\collection,
|
||||
mirzaev\arangodb\document;
|
||||
|
||||
// Library для ArangoDB
|
||||
use ArangoDBClient\Document as _document;
|
||||
|
||||
// Built-in libraries
|
||||
use exception;
|
||||
|
||||
/**
|
||||
* Model of a product
|
||||
*
|
||||
* @package mirzaev\arming_bot\models
|
||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||
*/
|
||||
final class product extends core
|
||||
{
|
||||
/**
|
||||
* Name of the collection in ArangoDB
|
||||
*/
|
||||
final public const string COLLECTION = 'product';
|
||||
|
||||
/**
|
||||
* Чтение товаров
|
||||
*
|
||||
* @param string|null $search Поиск
|
||||
* @param string|null $filter Фильтр
|
||||
* @param string|null $sort Сортировка
|
||||
* @param int $page Страница
|
||||
* @param int $amount Количество товаров на странице
|
||||
* @param string|null $return
|
||||
* @param array &$errors Registry of errors
|
||||
*
|
||||
* @return array Массив с найденными товарами (может быть пустым)
|
||||
*/
|
||||
public static function read(
|
||||
?string $search = null,
|
||||
?string $filter = 'd.deleted != true && d.hidden != true',
|
||||
?string $sort = 'd.promotion DESC, d.position ASC, d.created DESC',
|
||||
int $page = 1,
|
||||
int $amount = 100,
|
||||
?string $return = 'd',
|
||||
array &$errors = []
|
||||
): array {
|
||||
try {
|
||||
if (collection::init(core::$arangodb->session, self::COLLECTION)) {
|
||||
// Инициализирована коллекция
|
||||
|
||||
// Инициализация строки запроса
|
||||
$aql = sprintf(
|
||||
<<<AQL
|
||||
FOR d IN %s
|
||||
AQL,
|
||||
$search ? self::COLLECTION . 's_search' : self::COLLECTION
|
||||
);
|
||||
|
||||
if ($search) {
|
||||
// Search
|
||||
$aql .= sprintf(
|
||||
<<<AQL
|
||||
SEARCH
|
||||
LEVENSHTEIN_MATCH(
|
||||
d.title.ru,
|
||||
TOKENS("%s", "text_ru")[0],
|
||||
1,
|
||||
false
|
||||
) OR
|
||||
levenshtein_match(
|
||||
d.description.ru,
|
||||
tokens("%s", "text_ru")[0],
|
||||
1,
|
||||
false
|
||||
) OR
|
||||
levenshtein_match(
|
||||
d.compatibility.ru,
|
||||
tokens("%s", "text_ru")[0],
|
||||
1,
|
||||
false
|
||||
)
|
||||
AQL,
|
||||
$search,
|
||||
$search,
|
||||
$search
|
||||
);
|
||||
|
||||
// Adding sorting
|
||||
if ($sort) $sort = "BM25(d) DESC, $sort";
|
||||
else $sort = "BM25(d) DESC";
|
||||
}
|
||||
|
||||
if ($documents = collection::search(
|
||||
core::$arangodb->session,
|
||||
sprintf(
|
||||
$aql . <<<AQL
|
||||
%s
|
||||
%s
|
||||
LIMIT %u, %u
|
||||
RETURN %s
|
||||
AQL,
|
||||
empty($filter) ? '' : "FILTER $filter",
|
||||
empty($sort) ? '' : "SORT $sort",
|
||||
--$page <= 0 ? $page = 0 : $page * $amount,
|
||||
$amount,
|
||||
empty($return) ? 'd' : $return
|
||||
)
|
||||
)) {
|
||||
// Найдены товары
|
||||
|
||||
// Возврат (успех)
|
||||
return is_array($documents) ? $documents : [$documents];
|
||||
} else return [];
|
||||
} else throw new exception('Failed to initialize document collection: ' . self::COLLECTION);
|
||||
} catch (exception $e) {
|
||||
// Write to the registry of errors
|
||||
$errors[] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
}
|
||||
|
||||
// Выход (провал)
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Запись товара
|
||||
*
|
||||
* @param string $title Заголовок
|
||||
* @param string|null $description Описание
|
||||
* @param float $cost Цена
|
||||
* @param float $weight Вес
|
||||
* @param array $dimensions Габариты (float)
|
||||
* @param array $images Изображения (первое - обложка) (https, путь в storage, иначе будет поиск в storage)
|
||||
* @param array $hierarchy Иерархия вложенности (от родителей к потомкам: [ model, model ... ])
|
||||
* @param array $data Дополнительные данные
|
||||
* @param array &$errors Registry of errors
|
||||
*
|
||||
* @return string|null Идентификатор (_id) документа (товара), если создан
|
||||
*/
|
||||
public static function write(
|
||||
string $title,
|
||||
?string $description = null,
|
||||
float $cost = 0,
|
||||
float $weight = 0,
|
||||
array $dimensions = ['x' => 0, 'y' => 0, 'z' => 0],
|
||||
array $images = [],
|
||||
array $hierarchy = [],
|
||||
array $data = [],
|
||||
array &$errors = []
|
||||
): string|null {
|
||||
try {
|
||||
if (collection::init(core::$arangodb->session, self::COLLECTION)) {
|
||||
// Инициализирована коллекция
|
||||
|
||||
// Создание товара
|
||||
$product = document::write(
|
||||
core::$arangodb->session,
|
||||
self::COLLECTION,
|
||||
[
|
||||
'title' => $title,
|
||||
'description' => $description,
|
||||
'cost' => $cost ?? 0,
|
||||
'weight' => $weight ?? 0,
|
||||
'dimensions' => [
|
||||
'x' => $dimensions['x'] ?? 0,
|
||||
'y' => $dimensions['y'] ?? 0,
|
||||
'z' => $dimensions['z'] ?? 0,
|
||||
],
|
||||
'images' => $images,
|
||||
'version' => ROBOT_VERSION
|
||||
] + $data
|
||||
);
|
||||
|
||||
if ($product) {
|
||||
// Создан товар
|
||||
|
||||
if (collection::init(core::$arangodb->session, 'entry', true)) {
|
||||
// Инициализирована коллекция
|
||||
|
||||
foreach ($hierarchy as $model) {
|
||||
// Перебор иерархической структуры категорий
|
||||
|
||||
// Инициализация вложенной категории (следующей в массиве)
|
||||
$next = current($hierarchy);
|
||||
|
||||
// Поиск ребра описывающего иерархическую связь
|
||||
|
||||
document::write(core::$arangodb->session, 'entry');
|
||||
}
|
||||
} else throw new exception('Failed to initialize document collection: ' . self::COLLECTION);
|
||||
}
|
||||
} else throw new exception('Failed to initialize document collection: entry');
|
||||
} catch (exception $e) {
|
||||
// Write to the registry of errors
|
||||
$errors[] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
}
|
||||
|
||||
// Выход (провал)
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,338 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace mirzaev\arming_bot\models;
|
||||
|
||||
// Files of the project
|
||||
use mirzaev\arming_bot\models\account,
|
||||
mirzaev\arming_bot\models\enumerations\session as verification,
|
||||
mirzaev\arming_bot\models\traits\status,
|
||||
mirzaev\arming_bot\models\traits\document as arangodb_document_trait,
|
||||
mirzaev\arming_bot\models\interfaces\document as arangodb_document_interface;
|
||||
|
||||
// Framework for ArangoDB
|
||||
use mirzaev\arangodb\collection,
|
||||
mirzaev\arangodb\document;
|
||||
|
||||
// Library для ArangoDB
|
||||
use ArangoDBClient\Document as _document;
|
||||
|
||||
// Built-in libraries
|
||||
use exception;
|
||||
|
||||
/**
|
||||
* Model of a session
|
||||
*
|
||||
* @package mirzaev\arming_bot\models
|
||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||
*/
|
||||
final class session extends core implements arangodb_document_interface
|
||||
{
|
||||
use status, arangodb_document_trait;
|
||||
|
||||
/**
|
||||
* Name of the collection in ArangoDB
|
||||
*/
|
||||
final public const string COLLECTION = 'session';
|
||||
|
||||
/**
|
||||
* Type of session verification(
|
||||
*/
|
||||
public const verification VERIFICATION = verification::hash_else_address;
|
||||
|
||||
/**
|
||||
* Constructor of an instance
|
||||
*
|
||||
* Initialize of a session and write them to the $this->document property
|
||||
*
|
||||
* @param ?string $hash Hash of the session in ArangoDB
|
||||
* @param ?int $expires Date of expiring of the session (used for creating a new session)
|
||||
* @param array &$errors Registry of errors
|
||||
*
|
||||
* @return static instance of the ArangoDB document of session
|
||||
*/
|
||||
public function __construct(?string $hash = null, ?int $expires = null, array &$errors = [])
|
||||
{
|
||||
try {
|
||||
if (collection::init(static::$arangodb->session, self::COLLECTION)) {
|
||||
// Initialized the collection
|
||||
|
||||
if (isset($hash) && $document = $this->hash($hash, $errors)) {
|
||||
// Found an instance of the ArangoDB document of session and received a session hash
|
||||
|
||||
// Writing document instance of the session from ArangoDB to the property of the implementing object
|
||||
$this->document = $document;
|
||||
} else if (static::VERIFICATION === verification::hash_else_address && $document = $this->address($_SERVER['REMOTE_ADDR'], $errors)) {
|
||||
// Found an instance of the ArangoDB document of session and received a session hash
|
||||
|
||||
// Writing document instance of the session from ArangoDB to the property of the implementing object
|
||||
$this->document = $document;
|
||||
} else {
|
||||
// Not found an instance of the ArangoDB document of session
|
||||
|
||||
// Initializing a new session and write they into ArangoDB
|
||||
$_id = document::write($this::$arangodb->session, self::COLLECTION, [
|
||||
'active' => true,
|
||||
'expires' => $expires ?? time() + 604800,
|
||||
'address' => $_SERVER['REMOTE_ADDR'],
|
||||
'x-forwarded-for' => $_SERVER['HTTP_X_FORWARDED_FOR'] ?? null,
|
||||
'referer' => $_SERVER['HTTP_REFERER'] ?? null,
|
||||
'useragent' => $_SERVER['HTTP_USER_AGENT'] ?? null
|
||||
]);
|
||||
|
||||
if ($session = collection::search($this::$arangodb->session, sprintf(
|
||||
<<<AQL
|
||||
FOR d IN %s
|
||||
FILTER d._id == '%s' && d.expires > %d && d.active == true
|
||||
RETURN d
|
||||
AQL,
|
||||
self::COLLECTION,
|
||||
$_id,
|
||||
time()
|
||||
))) {
|
||||
// Found an instance of just created new session
|
||||
|
||||
// Generate a hash and write into an instance of the ArangoDB document of session property
|
||||
$session->hash = sodium_bin2hex(sodium_crypto_generichash($_id));
|
||||
|
||||
if (document::update($this::$arangodb->session, $session)) {
|
||||
// Is writed update
|
||||
|
||||
// Writing document instance of the session from ArangoDB to the property of the implementing object
|
||||
$this->document = $session;
|
||||
} else throw new exception('Could not write the session data');
|
||||
} else throw new exception('Could not create or find just created session');
|
||||
}
|
||||
} else throw new exception('Could not initialize the collection');
|
||||
} catch (exception $e) {
|
||||
// Write to the registry of errors
|
||||
$errors[] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for a connected account
|
||||
*
|
||||
* @param array &$errors Registry of errors
|
||||
*
|
||||
* @return account|null An object implementing the account instance from the database, if found
|
||||
*/
|
||||
public function account(array &$errors = []): ?account
|
||||
{
|
||||
try {
|
||||
if (collection::init(core::$arangodb->session, static::COLLECTION)) {
|
||||
if (collection::init(core::$arangodb->session, 'connect', true)) {
|
||||
if (collection::init(core::$arangodb->session, account::COLLECTION)) {
|
||||
// Инициализирована коллекция
|
||||
|
||||
if ($document = collection::search(
|
||||
core::$arangodb->session,
|
||||
sprintf(
|
||||
<<<AQL
|
||||
FOR v IN INBOUND "%s" GRAPH sessions
|
||||
SORT v.created DESC
|
||||
LIMIT 1
|
||||
RETURN v
|
||||
AQL,
|
||||
$this->getId(),
|
||||
)
|
||||
)) {
|
||||
// Найден аккаунт
|
||||
|
||||
// Инициализация объекта аккаунта
|
||||
$account = new account;
|
||||
|
||||
// Запись инстанции документа в объект
|
||||
$account->__document($document);
|
||||
|
||||
// Exit (success)
|
||||
return $account;
|
||||
} else return null;
|
||||
} else throw new exception('Failed to initialize document collection: ' . account::COLLECTION);
|
||||
} else throw new exception('Failed to initialize edge collection: connect');
|
||||
} else throw new exception('Failed to initialize document collection: ' . static::COLLECTION);
|
||||
} catch (exception $e) {
|
||||
// Write to the registry of errors
|
||||
$errors[] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
}
|
||||
|
||||
// Exit (fail)
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect account to session
|
||||
*
|
||||
* @param account $account Account
|
||||
* @param array &$errors Registry of errors
|
||||
*
|
||||
* @return string|null The identifier of the created edge of the "connect" collection, if created
|
||||
*/
|
||||
public function connect(account $account, array &$errors = []): ?string
|
||||
{
|
||||
try {
|
||||
if (collection::init(core::$arangodb->session, static::COLLECTION)) {
|
||||
if (collection::init(core::$arangodb->session, 'connect', true)) {
|
||||
if (collection::init(core::$arangodb->session, account::COLLECTION)) {
|
||||
// Collections initialized
|
||||
|
||||
// Writing document and exit (success)
|
||||
return document::write(
|
||||
core::$arangodb->session,
|
||||
'connect',
|
||||
[
|
||||
'_from' => $account->getId(),
|
||||
'_to' => $this->document->getId()
|
||||
]
|
||||
);
|
||||
} else throw new exception('Failed to initialize document collection: ' . account::COLLECTION);
|
||||
} else throw new exception('Failed to initialize edge collection: connect');
|
||||
} else throw new exception('Failed to initialize document collection: ' . static::COLLECTION);
|
||||
} catch (exception $e) {
|
||||
// Write to the registry of errors
|
||||
$errors[] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
}
|
||||
|
||||
// Exit (fail)
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search by hash
|
||||
*
|
||||
* Search for the session in ArangoDB by hash
|
||||
*
|
||||
* @param string $hash Hash of the session in ArangoDB
|
||||
* @param array &$errors Registry of errors
|
||||
*
|
||||
* @return _document|null instance of document of the session in ArangoDB
|
||||
*/
|
||||
public static function hash(string $hash, array &$errors = []): ?_document
|
||||
{
|
||||
try {
|
||||
if (collection::init(core::$arangodb->session, static::COLLECTION)) {
|
||||
// Collection initialized
|
||||
|
||||
// Search the session data in ArangoDB
|
||||
return collection::search(static::$arangodb->session, sprintf(
|
||||
<<<AQL
|
||||
FOR d IN %s
|
||||
FILTER d.hash == '%s' && d.expires > %d && d.active == true
|
||||
RETURN d
|
||||
AQL,
|
||||
static::COLLECTION,
|
||||
$hash,
|
||||
time()
|
||||
));
|
||||
} else throw new exception('Failed to initialize document collection: ' . static::COLLECTION);
|
||||
} catch (exception $e) {
|
||||
// Write to the registry of errors
|
||||
$errors[] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
}
|
||||
|
||||
// Exit (fail)
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search by IP-address
|
||||
*
|
||||
* Search for the session in ArangoDB by IP-address
|
||||
*
|
||||
* @param string $address IP-address writed to the session in ArangoDB
|
||||
* @param array &$errors Registry of errors
|
||||
*
|
||||
* @return _document|null instance of document of the session in ArangoDB
|
||||
*/
|
||||
public static function address(string $address, array &$errors = []): ?_document
|
||||
{
|
||||
try {
|
||||
if (collection::init(core::$arangodb->session, static::COLLECTION)) {
|
||||
// Collection initialized
|
||||
|
||||
// Search the session data in ArangoDB
|
||||
return collection::search(static::$arangodb->session, sprintf(
|
||||
<<<AQL
|
||||
FOR d IN %s
|
||||
FILTER d.address == '%s' && d.expires > %d && d.active == true
|
||||
RETURN d
|
||||
AQL,
|
||||
static::COLLECTION,
|
||||
$address,
|
||||
time()
|
||||
));
|
||||
} else throw new exception('Failed to initialize document collection: ' . static::COLLECTION);
|
||||
} catch (exception $e) {
|
||||
// Write to the registry of errors
|
||||
$errors[] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
}
|
||||
|
||||
// Exit (fail)
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write to buffer of the session
|
||||
*
|
||||
* @param array $data Data for merging
|
||||
* @param array &$errors Registry of errors
|
||||
*
|
||||
* @return bool Is data has written into the session buffer?
|
||||
*/
|
||||
public function write(array $data, array &$errors = []): bool
|
||||
{
|
||||
try {
|
||||
if (collection::init($this::$arangodb->session, self::COLLECTION)) {
|
||||
// Initialized the collection
|
||||
|
||||
// An instance of the ArangoDB document of session is initialized?
|
||||
if (!isset($this->document)) throw new exception('An instance of the ArangoDB document of session is not initialized');
|
||||
|
||||
// Write data into buffwer of an instance of the ArangoDB document of session
|
||||
$this->document->buffer = array_replace_recursive(
|
||||
$this->document->buffer ?? [],
|
||||
[$_SERVER['INTERFACE'] => array_replace_recursive($this->document->buffer[$_SERVER['INTERFACE']] ?? [], $data)]
|
||||
);
|
||||
|
||||
// Write to ArangoDB and exit (success)
|
||||
return document::update($this::$arangodb->session, $this->document) ? true : throw new exception('Не удалось записать данные в буфер сессии');
|
||||
} else throw new exception('Could not initialize the collection');
|
||||
} catch (exception $e) {
|
||||
// Write to the registry of errors
|
||||
$errors[] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace mirzaev\arming_bot\models;
|
||||
|
||||
// Files of the project
|
||||
use mirzaev\arming_bot\models\core,
|
||||
mirzaev\arming_bot\models\traits\document as arangodb_document_trait,
|
||||
mirzaev\arming_bot\models\interfaces\document as arangodb_document_interface;
|
||||
|
||||
// Framework for ArangoDB
|
||||
use mirzaev\arangodb\collection,
|
||||
mirzaev\arangodb\document;
|
||||
|
||||
// Library для ArangoDB
|
||||
use ArangoDBClient\Document as _document;
|
||||
|
||||
// Built-in libraries
|
||||
use exception;
|
||||
|
||||
/**
|
||||
* Model of settings
|
||||
*
|
||||
* @package mirzaev\arming_bot\models
|
||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||
*/
|
||||
final class settings extends core implements arangodb_document_interface
|
||||
{
|
||||
use arangodb_document_trait;
|
||||
|
||||
/**
|
||||
* Name of the collection in ArangoDB
|
||||
*/
|
||||
final public const string COLLECTION = 'settings';
|
||||
|
||||
/**
|
||||
* Search for active settings
|
||||
*
|
||||
* @param array|null $create Данные для создания, если настройки не найдены
|
||||
* @param array &$errors Registry of errors
|
||||
*
|
||||
* @return static|null Object implements an instance of settngs document from ArangoDB
|
||||
*/
|
||||
public static function active(array|null $create = null, array &$errors = []): static|null
|
||||
{
|
||||
try {
|
||||
if (collection::init(core::$arangodb->session, self::COLLECTION)) {
|
||||
if ($document = collection::search(core::$arangodb->session, sprintf("FOR d IN %s FILTER d.status == 'active' SORT d.updated DESC LIMIT 1 RETURN d", self::COLLECTION))) {
|
||||
// Найдены активные настройки
|
||||
|
||||
// Инициализация объекта настроек
|
||||
$settings = new static;
|
||||
|
||||
// Запись инстанции документа в объект
|
||||
$settings->document = $document;
|
||||
|
||||
// Возврат (успех)
|
||||
return $settings;
|
||||
} else if ($create) {
|
||||
// Не найдены активные настройки и запрошено создание
|
||||
|
||||
// Создание настроек
|
||||
document::write(core::$arangodb->session, self::COLLECTION, ['status' => 'active'] + $create);
|
||||
|
||||
// Инициализация (без создания)
|
||||
return static::active(errors: $errors);
|
||||
} else throw new exception('Settings not found');
|
||||
} else throw new exception('Failed to initialize document collection: ' . self::COLLECTION);
|
||||
} catch (exception $e) {
|
||||
// Write to the registry of errors
|
||||
$errors[] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
}
|
||||
|
||||
// Выход (провал)
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,166 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace mirzaev\arming_bot\models;
|
||||
|
||||
// Files of the project
|
||||
use mirzaev\arming_bot\models\core,
|
||||
mirzaev\arming_bot\controllers\core as controller,
|
||||
mirzaev\arming_bot\models\settings,
|
||||
mirzaev\arming_bot\models\traits\document as arangodb_document_trait,
|
||||
mirzaev\arming_bot\models\interfaces\document as arangodb_document_interface;
|
||||
|
||||
// Framework for ArangoDB
|
||||
use mirzaev\arangodb\collection,
|
||||
mirzaev\arangodb\document;
|
||||
|
||||
// Library для ArangoDB
|
||||
use ArangoDBClient\Document as _document;
|
||||
|
||||
// Built-in libraries
|
||||
use exception,
|
||||
datetime;
|
||||
|
||||
/**
|
||||
* Model of a suspension
|
||||
*
|
||||
* @package mirzaev\arming_bot\models
|
||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||
*/
|
||||
final class suspension extends core implements arangodb_document_interface
|
||||
{
|
||||
use arangodb_document_trait;
|
||||
|
||||
/**
|
||||
* Name of the collection in ArangoDB
|
||||
*/
|
||||
final public const string COLLECTION = 'suspension';
|
||||
|
||||
/**
|
||||
* Search for active suspension
|
||||
*
|
||||
* @param array &$errors Registry of errors
|
||||
*
|
||||
* @return static|null Object implements an instance of suspension from ArangoDB
|
||||
*/
|
||||
public static function search(array &$errors = []): static|null
|
||||
{
|
||||
try {
|
||||
if (collection::init(core::$arangodb->session, self::COLLECTION)) {
|
||||
if ($document = collection::search(core::$arangodb->session, sprintf("FOR d IN %s FILTER d.end > %u SORT d.end DESC LIMIT 1 RETURN d", self::COLLECTION, time()))) {
|
||||
// Найдены активные настройки
|
||||
|
||||
// Инициализация объекта настроек
|
||||
$suspension = new static;
|
||||
|
||||
// Запись инстанции документа в объект
|
||||
$suspension->document = $document;
|
||||
|
||||
// Возврат (успех)
|
||||
return $suspension;
|
||||
} else return null;
|
||||
} else throw new exception('Failed to initialize document collection: ' . self::COLLECTION);
|
||||
} catch (exception $e) {
|
||||
// Write to the registry of errors
|
||||
$errors[] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
}
|
||||
|
||||
// Exit (fail)
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate message about remaining time
|
||||
*
|
||||
* @param string|null $language Language of the generated text (otherwise used from settings.language)
|
||||
* @param array &$errors Registry of errors
|
||||
*
|
||||
* @return string|null Text: "? days, ? hours and ? minutes"
|
||||
*/
|
||||
public function message(?string $language = null, array &$errors = []): ?string
|
||||
{
|
||||
try {
|
||||
// Initializing default value
|
||||
$language ??= controller::$settings?->language ?? 'en';
|
||||
|
||||
// Initializing the time until the suspension ends
|
||||
$difference = date_diff(new datetime('@' . $this->document->end), new datetime());
|
||||
|
||||
// Generate text about remaining time and exit (success)
|
||||
return sprintf(
|
||||
'%u %s, %u %s и %u %s',
|
||||
$difference->d,
|
||||
match ($difference->d > 20 ? $difference->d % 10 : $difference->d % 100) {
|
||||
1 => match ($language) {
|
||||
'ru' => 'день',
|
||||
'en' => 'day',
|
||||
default => 'day'
|
||||
},
|
||||
2, 3, 4 => match ($language) {
|
||||
'ru' => 'дня',
|
||||
'en' => 'days',
|
||||
default => 'days'
|
||||
},
|
||||
default => match ($language) {
|
||||
'ru' => 'дней',
|
||||
'en' => 'days',
|
||||
default => 'days'
|
||||
}
|
||||
},
|
||||
$difference->h,
|
||||
match ($difference->h > 20 ? $difference->h % 10 : $difference->h % 100) {
|
||||
1 => match ($language) {
|
||||
'ru' => 'час',
|
||||
'en' => 'hours',
|
||||
default => 'hour'
|
||||
},
|
||||
2, 3, 4 => match ($language) {
|
||||
'ru' => 'часа',
|
||||
'en' => 'hours',
|
||||
default => 'hours'
|
||||
},
|
||||
default => match ($language) {
|
||||
'ru' => 'часов',
|
||||
'en' => 'hours',
|
||||
default => 'hours'
|
||||
}
|
||||
},
|
||||
$difference->i,
|
||||
match ($difference->i > 20 ? $difference->i % 10 : $difference->i % 100) {
|
||||
1 => match ($language) {
|
||||
'ru' => 'минута',
|
||||
'en' => 'minute',
|
||||
default => 'minute'
|
||||
},
|
||||
2, 3, 4 => match ($language) {
|
||||
'ru' => 'минуты',
|
||||
'en' => 'minutes',
|
||||
default => 'minutes'
|
||||
},
|
||||
default => match ($language) {
|
||||
'ru' => 'минут',
|
||||
'en' => 'minutes',
|
||||
default => 'minutes'
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (exception $e) {
|
||||
// Write to the registry of errors
|
||||
$errors[] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
}
|
||||
|
||||
// Exit (fail)
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace mirzaev\arming_bot\models\traits;
|
||||
|
||||
// Files of the project
|
||||
use mirzaev\arming_bot\models\core;
|
||||
|
||||
// Library для ArangoDB
|
||||
use ArangoDBClient\Document as _document;
|
||||
|
||||
/**
|
||||
* Trait for implementing a document instance from ArangoDB
|
||||
*
|
||||
* @package mirzaev\arming_bot\models\traits
|
||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||
*/
|
||||
trait document
|
||||
{
|
||||
/**
|
||||
* An instance of the ArangoDB document from ArangoDB
|
||||
*/
|
||||
protected readonly _document $document;
|
||||
|
||||
/**
|
||||
* Write or read document
|
||||
*
|
||||
* @param _document|null $document Instance of document from ArangoDB
|
||||
*
|
||||
* @return _document|null Instance of document from ArangoDB
|
||||
*/
|
||||
public function __document(?_document $document): ?_document
|
||||
{
|
||||
// Write a property storing a document instance to ArangoDB
|
||||
if ($document) $this->document = $document;
|
||||
|
||||
// Read a property storing a document instance to ArangoDB and exit (success)
|
||||
return $this->document;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write
|
||||
*
|
||||
* Write a property into an instance of the ArangoDB document
|
||||
*
|
||||
* @param string $name Name of the property
|
||||
* @param mixed $value Content of the property
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __set(string $name, mixed $value = null): void
|
||||
{
|
||||
// Write to the property into an instance of the ArangoDB document and exit (success)
|
||||
$this->document->{$name} = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read
|
||||
*
|
||||
* Read a property from an instance of the ArangoDB docuemnt
|
||||
*
|
||||
* @param string $name Name of the property
|
||||
*
|
||||
* @return mixed Content of the property
|
||||
*/
|
||||
public function __get(string $name): mixed
|
||||
{
|
||||
// Read a property from an instance of the ArangoDB document and exit (success)
|
||||
return match ($name) {
|
||||
'arangodb' => core::$arangodb,
|
||||
default => $this->document->{$name}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete
|
||||
*
|
||||
* Deinitialize the property in an instance of the ArangoDB document
|
||||
*
|
||||
* @param string $name Name of the property
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __unset(string $name): void
|
||||
{
|
||||
// Delete the property in an instance of the ArangoDB document and exit (success)
|
||||
unset($this->document->{$name});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check of initialization
|
||||
*
|
||||
* Check of initialization of the property into an instance of the ArangoDB document
|
||||
*
|
||||
* @param string $name Name of the property
|
||||
*
|
||||
* @return bool The property is initialized?
|
||||
*/
|
||||
public function __isset(string $name): bool
|
||||
{
|
||||
// Check of initializatio nof the property and exit (success)
|
||||
return isset($this->document->{$name});
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a method
|
||||
*
|
||||
* Execute a method from an instance of the ArangoDB document
|
||||
*
|
||||
* @param string $name Name of the method
|
||||
* @param array $arguments Arguments for the method
|
||||
*
|
||||
* @return mixed Result of execution of the method
|
||||
*/
|
||||
public function __call(string $name, array $arguments = []): mixed
|
||||
{
|
||||
// Execute the method and exit (success)
|
||||
return method_exists($this->document, $name) ?$this->document->{$name}($arguments) ?? null : null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace mirzaev\arming_bot\models\traits;
|
||||
|
||||
// Built-in libraries
|
||||
use exception;
|
||||
|
||||
/**
|
||||
* Trait for initialization of a status
|
||||
*
|
||||
* @package mirzaev\arming_bot\models\traits
|
||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||
*/
|
||||
trait status
|
||||
{
|
||||
/**
|
||||
* Initialize of a status
|
||||
*
|
||||
* @param array &$errors Registry of errors
|
||||
*
|
||||
* @return ?bool Status, if they are found
|
||||
*/
|
||||
public function status(array &$errors = []): ?bool
|
||||
{
|
||||
try {
|
||||
// Read from ArangoDB and exit (success)
|
||||
return $this->document->active ?? false;
|
||||
} catch (exception $e) {
|
||||
// Write to the registry of errors
|
||||
$errors[] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
}
|
||||
|
||||
// Exit (fail)
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace mirzaev\arming_bot;
|
||||
|
||||
// Files of the project
|
||||
use mirzaev\arming_bot\controllers\core as controller,
|
||||
mirzaev\arming_bot\models\core as model;
|
||||
|
||||
// Framework for PHP
|
||||
use mirzaev\minimal\core,
|
||||
mirzaev\minimal\router;
|
||||
|
||||
/* ini_set('error_reporting', E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
ini_set('display_startup_errors', 1); */
|
||||
|
||||
// Версия робота
|
||||
define('ROBOT_VERSION', '1.0.0');
|
||||
define('VIEWS', realpath('..' . DIRECTORY_SEPARATOR . 'views'));
|
||||
define('STORAGE', realpath('..' . DIRECTORY_SEPARATOR . 'storage'));
|
||||
define('SETTINGS', realpath('..' . DIRECTORY_SEPARATOR . 'settings'));
|
||||
define('INDEX', __DIR__);
|
||||
define('THEME', 'default');
|
||||
|
||||
// Инициализация библиотек
|
||||
require __DIR__ . DIRECTORY_SEPARATOR
|
||||
. '..' . DIRECTORY_SEPARATOR
|
||||
. '..' . DIRECTORY_SEPARATOR
|
||||
. '..' . DIRECTORY_SEPARATOR
|
||||
. '..' . DIRECTORY_SEPARATOR
|
||||
. 'vendor' . DIRECTORY_SEPARATOR
|
||||
. 'autoload.php';
|
||||
|
||||
// Инициализация маршрутизатора
|
||||
$router = new router;
|
||||
|
||||
// Initializing of routes
|
||||
$router
|
||||
->write('/', 'catalog', 'index', 'GET')
|
||||
->write('/search', 'catalog', 'search', 'POST')
|
||||
->write('/session/connect/telegram', 'session', 'telegram', 'POST')
|
||||
->write('/product/$id', 'catalog', 'product', 'POST')
|
||||
->write('/$categories...', 'catalog', 'index', 'POST');
|
||||
|
||||
/*
|
||||
|
||||
// Initializing of routes
|
||||
$router
|
||||
->write('/', 'catalog', 'index', 'GET')
|
||||
->write('/$sex', 'catalog', 'search', 'POST')
|
||||
->write('/$search', 'catalog', 'search', 'POST')
|
||||
->write('/search', 'catalog', 'search', 'POST')
|
||||
->write('/search/$asdasdasd', 'catalog', 'search', 'POST')
|
||||
->write('/ebala/$sex/$categories...', 'catalog', 'index', 'POST')
|
||||
->write('/$sex/$categories...', 'catalog', 'index', 'POST')
|
||||
->write('/$categories...', 'catalog', 'index', 'POST')
|
||||
->write('/ebala/$categories...', 'catalog', 'index', 'POST');
|
||||
|
||||
var_dump($router->routes);
|
||||
echo "\n\n\n\n\n\n";
|
||||
$router
|
||||
->sort();
|
||||
var_dump($router->routes); */
|
||||
|
||||
// Инициализация ядра
|
||||
$core = new core(namespace: __NAMESPACE__, router: $router, controller: new controller(false), model: new model(false));
|
||||
|
||||
// Обработка запроса
|
||||
echo $core->start();
|
|
@ -0,0 +1,62 @@
|
|||
"use strict";
|
||||
|
||||
// Import dependencies
|
||||
import("/js/core.js").then(() =>
|
||||
import("/js/damper.js").then(() => {
|
||||
import("/js/telegram.js").then(() => {
|
||||
const dependencies = setInterval(() => {
|
||||
if (
|
||||
typeof core === "function" &&
|
||||
typeof core.damper === "function" &&
|
||||
typeof core.telegram === "function"
|
||||
) {
|
||||
clearInterval(dependencies);
|
||||
clearTimeout(timeout);
|
||||
initialization();
|
||||
}
|
||||
}, 10);
|
||||
const timeout = setTimeout(() => clearInterval(dependencies), 5000);
|
||||
|
||||
function initialization() {
|
||||
const timer_for_response = setTimeout(() => {
|
||||
core.loading.setAttribute("disabled", true);
|
||||
|
||||
const p = document.createElement("p");
|
||||
p.innerText = "Not authenticated";
|
||||
|
||||
core.footer.appendChild(p);
|
||||
}, 3000);
|
||||
|
||||
core.request(
|
||||
"/session/connect/telegram",
|
||||
"authentication=telegram&" + core.telegram.api.initData,
|
||||
)
|
||||
.then((json) => {
|
||||
if (
|
||||
json.errors !== null &&
|
||||
typeof json.errors === "object" &&
|
||||
json.errors.length > 0
|
||||
) {
|
||||
// Errors received
|
||||
} else {
|
||||
// Errors not received
|
||||
|
||||
if (json.connected === true) {
|
||||
core.loading.setAttribute("disabled", true);
|
||||
|
||||
clearTimeout(timer_for_response);
|
||||
}
|
||||
|
||||
if (
|
||||
json.language !== null &&
|
||||
typeof json.language === "string" &&
|
||||
json.langiage.length === 2
|
||||
) {
|
||||
core.language = json.language;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
|
@ -0,0 +1,30 @@
|
|||
"use strict";
|
||||
|
||||
// Import dependencies
|
||||
import("/js/core.js").then(() =>
|
||||
import("/js/damper.js").then(() => {
|
||||
const dependencies = setInterval(() => {
|
||||
if (typeof core === "function" &&
|
||||
typeof core.damper === "function") {
|
||||
clearInterval(dependencies);
|
||||
clearTimeout(timeout);
|
||||
initialization();
|
||||
}
|
||||
}, 10);
|
||||
const timeout = setTimeout(() => clearInterval(dependencies), 5000);
|
||||
|
||||
function initialization() {
|
||||
if (typeof core.cart === "undefined") {
|
||||
// Not initialized
|
||||
|
||||
// Write to the core
|
||||
core.cart = class cart {
|
||||
/**
|
||||
* Products in cart ["product/148181", "product/148181", "product/148181"...]
|
||||
*/
|
||||
static cart = [];
|
||||
};
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
|
@ -0,0 +1,628 @@
|
|||
"use strict";
|
||||
|
||||
// Import dependencies
|
||||
import("/js/core.js").then(() =>
|
||||
import("/js/damper.js").then(() => {
|
||||
import("/js/telegram.js").then(() => {
|
||||
import("/js/hotline.js").then(() => {
|
||||
const dependencies = setInterval(() => {
|
||||
console.log(typeof core, typeof core.damper, typeof core.telegram, typeof core.hotline);
|
||||
if (
|
||||
typeof core === "function" &&
|
||||
typeof core.damper === "function" &&
|
||||
typeof core.telegram === "function" &&
|
||||
typeof core.hotline === "function"
|
||||
) {
|
||||
clearInterval(dependencies);
|
||||
clearTimeout(timeout);
|
||||
initialization();
|
||||
}
|
||||
}, 10);
|
||||
const timeout = setTimeout(() => clearInterval(dependencies), 5000);
|
||||
|
||||
function initialization() {
|
||||
if (typeof core.catalog === "undefined") {
|
||||
// Not initialized
|
||||
|
||||
// Write to the core
|
||||
core.catalog = class catalog {
|
||||
/**
|
||||
* Current position in hierarchy of the categories
|
||||
*/
|
||||
static categories = [];
|
||||
|
||||
/**
|
||||
* Select a category (interface)
|
||||
*
|
||||
* @param {HTMLElement} button Button of category <a>
|
||||
* @param {bool} clean Clear search bar?
|
||||
* @param {bool} force Ignore the damper?
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
static category(button, clean = true, force = false) {
|
||||
// Initialize of the new category name
|
||||
const category = button.getAttribute("data-category-name");
|
||||
|
||||
this._category(category, clean, force);
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a category (damper)
|
||||
*
|
||||
* @param {HTMLElement} button Button of category <a>
|
||||
* @param {bool} clean Clear search bar?
|
||||
* @param {bool} force Ignore the damper?
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
static _category = core.damper(
|
||||
(...variables) => this.__category(...variables),
|
||||
400,
|
||||
2,
|
||||
);
|
||||
|
||||
/**
|
||||
* Select a category (system)
|
||||
*
|
||||
* @param {HTMLElement} button Button of category <a>
|
||||
* @param {bool} clean Clear search bar?
|
||||
*
|
||||
* @return {Promise} Request to the server
|
||||
*/
|
||||
static __category(category = "", clean = true) {
|
||||
if (typeof category === "string") {
|
||||
//
|
||||
|
||||
let urn;
|
||||
if (category === "/" || category === "") urn = "/";
|
||||
else {urn = this.categories.length > 0
|
||||
? `/${this.categories.join("/")}/${category}`
|
||||
: `/${category}`;}
|
||||
|
||||
return core.request(urn)
|
||||
.then((json) => {
|
||||
if (
|
||||
json.errors !== null &&
|
||||
typeof json.errors === "object" &&
|
||||
json.errors.length > 0
|
||||
) {
|
||||
// Errors received
|
||||
} else {
|
||||
// Errors not received
|
||||
|
||||
if (clean) {
|
||||
// Clearing the search bar
|
||||
const search = core.main.querySelector(
|
||||
'search[data-section="search"]>input',
|
||||
);
|
||||
if (search instanceof HTMLElement) search.value = "";
|
||||
}
|
||||
|
||||
// Write the category to position in the categories hierarchy
|
||||
if (category !== "/" && category !== "") {
|
||||
this.categories.push(category);
|
||||
}
|
||||
|
||||
if (
|
||||
typeof json.title === "string" &&
|
||||
json.title.length > 0
|
||||
) {
|
||||
// Received the page title
|
||||
|
||||
// Initialize a link to the categories list
|
||||
const title = core.main.getElementsByTagName("h2")[0];
|
||||
|
||||
// Write the title
|
||||
title.innerText = json.title;
|
||||
}
|
||||
|
||||
if (
|
||||
typeof json.html.categories === "string" &&
|
||||
json.html.categories.length > 0
|
||||
) {
|
||||
// Received categories (reinitialization of the categories)
|
||||
|
||||
const categories = core.main.querySelector(
|
||||
'section[data-catalog-type="categories"]',
|
||||
);
|
||||
|
||||
if (categories instanceof HTMLElement) {
|
||||
// Found list of categories
|
||||
|
||||
categories.outerHTML = json.html.categories;
|
||||
} else {
|
||||
// Not found list of categories
|
||||
|
||||
const element = document.createElement("section");
|
||||
|
||||
const search = core.main.querySelector(
|
||||
'search[data-section="search"]',
|
||||
);
|
||||
|
||||
if (search instanceof HTMLElement) {
|
||||
core.main.insertBefore(
|
||||
element,
|
||||
search.nextSibling,
|
||||
);
|
||||
|
||||
element.outerHTML = json.html.categories;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Not received categories (deinitialization of the categories)
|
||||
|
||||
const categories = core.main.querySelector(
|
||||
'section[data-catalog-type="categories"',
|
||||
);
|
||||
if (categories instanceof HTMLElement) {
|
||||
categories.remove();
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
typeof json.html.products === "string" &&
|
||||
json.html.products.length > 0
|
||||
) {
|
||||
// Received products (reinitialization of the products)
|
||||
|
||||
const products = core.main.querySelector(
|
||||
'section[data-catalog-type="products"]',
|
||||
);
|
||||
|
||||
if (products instanceof HTMLElement) {
|
||||
// Found list of products
|
||||
|
||||
products.outerHTML = json.html.products;
|
||||
} else {
|
||||
// Not found list of products
|
||||
|
||||
const element = document.createElement("section");
|
||||
|
||||
const categories = core.main.querySelector(
|
||||
'section[data-catalog-type="categories"]',
|
||||
);
|
||||
|
||||
if (categories instanceof HTMLElement) {
|
||||
core.main.insertBefore(
|
||||
element,
|
||||
categories.nextSibling,
|
||||
);
|
||||
|
||||
element.outerHTML = json.html.products;
|
||||
} else {
|
||||
const search = core.main.querySelector(
|
||||
'search[data-section="search"]',
|
||||
);
|
||||
|
||||
if (search instanceof HTMLElement) {
|
||||
core.main.insertBefore(
|
||||
element,
|
||||
search.nextSibling,
|
||||
);
|
||||
|
||||
element.outerHTML = json.html.products;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Not received products (deinitialization of the products)
|
||||
|
||||
const products = core.main.querySelector(
|
||||
'section[data-catalog-type="products"',
|
||||
);
|
||||
if (products instanceof HTMLElement) {
|
||||
products
|
||||
.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a category (interface)
|
||||
*
|
||||
* @param {Event} event Event (keyup)
|
||||
* @param {HTMLElement} element Search bar <input>
|
||||
* @param {bool} force Ignore the damper?
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
static search(event, element, force = false) {
|
||||
element.classList.remove("error");
|
||||
|
||||
if (element.innerText.length === 1) {
|
||||
return;
|
||||
} else if (event.keyCode === 13) {
|
||||
// Button: "enter"
|
||||
|
||||
element.setAttribute("disabled", true);
|
||||
|
||||
this.__search(element);
|
||||
} else {
|
||||
// Button: any
|
||||
|
||||
this._search(element, force);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search in the catalog (damper)
|
||||
*
|
||||
* @param {HTMLElement} button Button of category <a>
|
||||
* @param {bool} clean Clear search bar?
|
||||
* @param {bool} force Ignore the damper?
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
static _search = core.damper(
|
||||
(...variables) => this.__search(...variables),
|
||||
1400,
|
||||
2,
|
||||
);
|
||||
|
||||
/**
|
||||
* Search in the catalog (system)
|
||||
*
|
||||
* @param {HTMLElement} element Search bar <input>
|
||||
*
|
||||
* @return {Promise} Request to the server
|
||||
*
|
||||
* @todo add animations of errors
|
||||
*/
|
||||
static __search(element) {
|
||||
// Deinitialization of position in the categories hierarchy
|
||||
this.categories = [];
|
||||
|
||||
return this.__category("/", false)
|
||||
.then(function () {
|
||||
core.request("/search", `text=${element.value}`)
|
||||
.then((json) => {
|
||||
element.removeAttribute("disabled");
|
||||
element.focus();
|
||||
|
||||
if (
|
||||
json.errors !== null &&
|
||||
typeof json.errors === "object" &&
|
||||
json.errors.length > 0
|
||||
) {
|
||||
// Errors received
|
||||
|
||||
element.classList.add("error");
|
||||
} else {
|
||||
// Errors not received
|
||||
|
||||
if (
|
||||
typeof json.title === "string" &&
|
||||
json.title.length > 0
|
||||
) {
|
||||
// Received the page title
|
||||
|
||||
// Initialize a link to the categories list
|
||||
const title =
|
||||
core.main.getElementsByTagName("h2")[0];
|
||||
|
||||
// Write the title
|
||||
title.innerText = json.title;
|
||||
}
|
||||
|
||||
// Deinitialization of the categories
|
||||
const categories = core.main.querySelector(
|
||||
'section[data-catalog-type="categories"]',
|
||||
);
|
||||
// if (categories instanceof HTMLElement) categories.remove();
|
||||
|
||||
if (
|
||||
typeof json.html.products === "string" &&
|
||||
json.html.products.length > 0
|
||||
) {
|
||||
// Received products (reinitialization of the products)
|
||||
|
||||
const products = core.main.querySelector(
|
||||
'section[data-catalog-type="products"]',
|
||||
);
|
||||
|
||||
if (products instanceof HTMLElement) {
|
||||
// Found list of products
|
||||
|
||||
products.outerHTML = json.html.products;
|
||||
} else {
|
||||
// Not found list of products
|
||||
|
||||
const element = document.createElement("section");
|
||||
|
||||
const categories = core.main.querySelector(
|
||||
'section[data-catalog-type="categories"',
|
||||
);
|
||||
|
||||
if (categories instanceof HTMLElement) {
|
||||
core.main.insertBefore(
|
||||
element,
|
||||
categories.nextSibling,
|
||||
);
|
||||
|
||||
element.outerHTML = json.html.products;
|
||||
} else {
|
||||
const search = core.main.querySelector(
|
||||
'search[data-section="search"]',
|
||||
);
|
||||
|
||||
if (search instanceof HTMLElement) {
|
||||
core.main.insertBefore(
|
||||
element,
|
||||
search.nextSibling,
|
||||
);
|
||||
|
||||
element.outerHTML = json.html.products;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Not received products (deinitialization of the products)
|
||||
|
||||
const products = core.main.querySelector(
|
||||
'section[data-catalog-type="products"]',
|
||||
);
|
||||
if (products instanceof HTMLElement) {
|
||||
products.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Open product card (interface)
|
||||
*
|
||||
* @param {string} id Identifier of a product
|
||||
* @param {bool} force Ignore the damper?
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
static product(id, force = false) {
|
||||
this._product(id, force);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open product card (damper)
|
||||
*
|
||||
* @param {string} id Identifier of a product
|
||||
* @param {bool} force Ignore the damper?
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
static _product = core.damper(
|
||||
(...variables) => this.__product(...variables),
|
||||
400,
|
||||
1,
|
||||
);
|
||||
|
||||
/**
|
||||
* Open product card (system)
|
||||
*
|
||||
* @param {string} id Identifier of a product
|
||||
*
|
||||
* @return {Promise} Request to the server
|
||||
*/
|
||||
static __product(id) {
|
||||
if (typeof id === "number") {
|
||||
//
|
||||
|
||||
return core.request(`/product/${id}`)
|
||||
.then((json) => {
|
||||
if (
|
||||
json.errors !== null &&
|
||||
typeof json.errors === "object" &&
|
||||
json.errors.length > 0
|
||||
) {
|
||||
// Errors received
|
||||
} else {
|
||||
// Errors not received
|
||||
|
||||
if (
|
||||
json.product !== null &&
|
||||
typeof json.product === "object"
|
||||
) {
|
||||
// Received data of the product
|
||||
|
||||
// Deinitializing of the old winow
|
||||
const old = document.getElementById("window");
|
||||
if (old instanceof HTMLElement) old.remove();
|
||||
|
||||
const wrap = document.createElement("section");
|
||||
wrap.setAttribute("id", "window");
|
||||
|
||||
const card = document.createElement("div");
|
||||
// card.classList.add("product", "card");
|
||||
card.classList.add("card", "unselectable");
|
||||
|
||||
const h3 = document.createElement("h3");
|
||||
h3.setAttribute("title", json.product.id);
|
||||
|
||||
const title = document.createElement("span");
|
||||
title.classList.add("title");
|
||||
title.innerText = json.product.title;
|
||||
|
||||
const brand = document.createElement("small");
|
||||
brand.classList.add("brand");
|
||||
brand.innerText = json.product.brand;
|
||||
|
||||
const images = document.createElement("div");
|
||||
images.classList.add("images", "unselectable");
|
||||
|
||||
for (const uri of json.product.images) {
|
||||
const image = document.createElement("img");
|
||||
image.setAttribute("src", uri);
|
||||
image.setAttribute("ondragstart", "return false;");
|
||||
|
||||
const button = core.telegram.api.isVisible;
|
||||
|
||||
const open = (event) => {
|
||||
if (event.target === from) {
|
||||
if (typeof images.hotline === "object") {
|
||||
if (images.hotline.moving) return;
|
||||
images.hotline.stop();
|
||||
}
|
||||
|
||||
image.classList.add("extend");
|
||||
|
||||
if (button) core.telegram.api.MainButton.hide();
|
||||
|
||||
setTimeout(() => {
|
||||
image.addEventListener("click", close);
|
||||
image.addEventListener("touch", close);
|
||||
}, 300);
|
||||
image.removeEventListener("mouseup", open);
|
||||
image.removeEventListener("touchend", open);
|
||||
}
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
if (typeof images.hotline === "object") {
|
||||
images.hotline.start();
|
||||
}
|
||||
|
||||
image.classList.remove("extend");
|
||||
|
||||
if (button) core.telegram.api.MainButton.show();
|
||||
|
||||
image.removeEventListener("click", close);
|
||||
image.removeEventListener("touch", close);
|
||||
image.addEventListener("mousedown", start);
|
||||
image.addEventListener("touchstart", start);
|
||||
};
|
||||
|
||||
const start = (event) => {
|
||||
if (
|
||||
event.type === "touchstart" ||
|
||||
event.button === 0
|
||||
) {
|
||||
image.removeEventListener("mousedown", start);
|
||||
image.removeEventListener("touchstart", start);
|
||||
image.addEventListener("mouseup", open);
|
||||
image.addEventListener("touchend", open);
|
||||
}
|
||||
};
|
||||
|
||||
image.addEventListener("mousedown", start);
|
||||
image.addEventListener("touchstart", start);
|
||||
|
||||
images.append(image);
|
||||
}
|
||||
|
||||
const description = document.createElement("p");
|
||||
description.classList.add("description");
|
||||
description.innerText = json.product.description;
|
||||
|
||||
const compatibility = document.createElement("p");
|
||||
compatibility.classList.add("compatibility");
|
||||
compatibility.innerText = json.product.compatibility;
|
||||
|
||||
const footer = document.createElement("div");
|
||||
footer.classList.add("footer");
|
||||
footer.classList.add("footer");
|
||||
|
||||
const dimensions = document.createElement("small");
|
||||
dimensions.classList.add("dimensions");
|
||||
dimensions.innerText = json.product.dimensions.x +
|
||||
"x" +
|
||||
json.product.dimensions.y + "x" +
|
||||
json.product.dimensions.z;
|
||||
|
||||
const weight = document.createElement("small");
|
||||
weight.classList.add("weight");
|
||||
weight.innerText = json.product.weight + "г";
|
||||
|
||||
const cost = document.createElement("p");
|
||||
cost.classList.add("cost");
|
||||
cost.innerText = json.product.cost + "р";
|
||||
|
||||
h3.append(title);
|
||||
h3.append(brand);
|
||||
card.append(h3);
|
||||
card.append(images);
|
||||
card.append(description);
|
||||
card.append(compatibility);
|
||||
footer.append(dimensions);
|
||||
footer.append(weight);
|
||||
footer.append(cost);
|
||||
card.append(footer);
|
||||
wrap.append(card);
|
||||
core.main.append(wrap);
|
||||
|
||||
let width = 0;
|
||||
let buffer;
|
||||
[...images.children].forEach((child) =>
|
||||
width += child.offsetWidth + (isNaN(
|
||||
buffer = parseFloat(
|
||||
getComputedStyle(child).marginRight,
|
||||
),
|
||||
)
|
||||
? 0
|
||||
: buffer)
|
||||
);
|
||||
|
||||
history.pushState(
|
||||
{ product_card: json.product.id },
|
||||
json.product.title,
|
||||
);
|
||||
|
||||
// блокировка закрытия карточки
|
||||
let from;
|
||||
const _from = (event) => from = event.target;
|
||||
wrap.addEventListener("mousedown", _from);
|
||||
wrap.addEventListener("touchstart", _from);
|
||||
|
||||
const remove = () => {
|
||||
wrap.remove();
|
||||
wrap.removeEventListener("mousedown", _from);
|
||||
wrap.removeEventListener("touchstart", _from);
|
||||
document.removeEventListener("click", close);
|
||||
document.removeEventListener("touch", close);
|
||||
window.removeEventListener("popstate", remove);
|
||||
};
|
||||
|
||||
const close = (event) => {
|
||||
if (
|
||||
from === wrap &&
|
||||
!card.contains(event.target) &&
|
||||
!!card &&
|
||||
!!(card.offsetWidth ||
|
||||
card.offsetHeight ||
|
||||
card.getClientRects().length)
|
||||
) {
|
||||
remove();
|
||||
}
|
||||
|
||||
from = undefined;
|
||||
};
|
||||
|
||||
document.addEventListener("click", close);
|
||||
document.addEventListener("touch", close);
|
||||
window.addEventListener("popstate", remove);
|
||||
|
||||
if (width > card.offsetWidth) {
|
||||
images.hotline = new core.hotline(
|
||||
json.product.id,
|
||||
images,
|
||||
);
|
||||
images.hotline.step = -0.3;
|
||||
images.hotline.wheel = true;
|
||||
images.hotline.touch = true;
|
||||
images.hotline.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
|
@ -0,0 +1,52 @@
|
|||
"use strict";
|
||||
|
||||
// Initialize of the class in global namespace
|
||||
const core = class core {
|
||||
// Domain
|
||||
static domain = window.location.hostname;
|
||||
|
||||
// Language
|
||||
static language = "ru";
|
||||
|
||||
// Label for the "loding" element
|
||||
static loading = document.getElementById("loading");
|
||||
|
||||
// Label for the <header> element
|
||||
static header = document.body.getElementsByTagName("header")[0];
|
||||
|
||||
// Label for the <aside> element
|
||||
static aside = document.body.getElementsByTagName("aside")[0];
|
||||
|
||||
// Label for the "menu" element
|
||||
static menu = document.body.querySelector("section[data-section='menu']");
|
||||
|
||||
// Label for the <main> element
|
||||
static main = document.body.getElementsByTagName("main")[0];
|
||||
|
||||
// Label for the <footer> element
|
||||
static footer = document.body.getElementsByTagName("footer")[0];
|
||||
|
||||
/**
|
||||
* Request
|
||||
*
|
||||
* @param {string} address
|
||||
* @param {string} body
|
||||
* @param {string} method POST, GET...
|
||||
* @param {object} headers
|
||||
* @param {string} type Format of response (json, text...)
|
||||
*
|
||||
* @return {Promise}
|
||||
*/
|
||||
static async request(
|
||||
address = "/",
|
||||
body,
|
||||
method = "POST",
|
||||
headers = {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
type = "json",
|
||||
) {
|
||||
return await fetch(encodeURI(address), { method, headers, body })
|
||||
.then((response) => response[type]());
|
||||
}
|
||||
};
|
|
@ -0,0 +1,68 @@
|
|||
"use strict";
|
||||
|
||||
// Import dependencies
|
||||
import("/js/core.js").then(() => {
|
||||
const dependencies = setInterval(() => {
|
||||
if (typeof core === "function") {
|
||||
clearInterval(dependencies);
|
||||
clearTimeout(timeout);
|
||||
initialization();
|
||||
}
|
||||
}, 10);
|
||||
const timeout = setTimeout(() => clearInterval(dependencies), 5000);
|
||||
|
||||
function initialization() {
|
||||
if (typeof core.damper === "undefined") {
|
||||
// Not initialized
|
||||
|
||||
/**
|
||||
* Damper
|
||||
*
|
||||
* @param {function} function Function to execute after damping
|
||||
* @param {number} timeout Timer in milliseconds (ms)
|
||||
* @param {number} force Argument number storing the status of enforcement execution (see @example)
|
||||
*
|
||||
* @example
|
||||
* $a = damper(
|
||||
* async (
|
||||
* a, // 0
|
||||
* b, // 1
|
||||
* c, // 2
|
||||
* force = false, // 3
|
||||
* d // 4
|
||||
* ) => {
|
||||
* // Body of function
|
||||
* },
|
||||
* 500,
|
||||
* 3, // 3 -> "force" argument
|
||||
* );
|
||||
*
|
||||
* $a('for a', 'for b', 'for c', true, 'for d'); // Force execute is enabled
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
core.damper = (func, timeout = 300, force) => {
|
||||
// Initializing of the timer
|
||||
let timer;
|
||||
|
||||
return (...args) => {
|
||||
// Deinitializing of the timer
|
||||
clearTimeout(timer);
|
||||
|
||||
if (typeof force === "number" && args[force]) {
|
||||
// Force execution (ignoring the timer)
|
||||
|
||||
func.apply(this, args);
|
||||
} else {
|
||||
// Normal execution
|
||||
|
||||
// Execute the handled function (entry into recursion)
|
||||
timer = setTimeout(() => {
|
||||
func.apply(this, args);
|
||||
}, timeout);
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
|
@ -0,0 +1,772 @@
|
|||
"use strict";
|
||||
|
||||
// Import dependencies
|
||||
import("/js/core.js").then(() => {
|
||||
const dependencies = setInterval(() => {
|
||||
if (typeof core === "function") {
|
||||
clearInterval(dependencies);
|
||||
clearTimeout(timeout);
|
||||
initialization();
|
||||
}
|
||||
}, 10);
|
||||
const timeout = setTimeout(() => clearInterval(dependencies), 5000);
|
||||
|
||||
function initialization() {
|
||||
if (typeof core.hotline === "undefined") {
|
||||
// Not initialized
|
||||
|
||||
/**
|
||||
* Бегущая строка
|
||||
*
|
||||
* @description
|
||||
* Простой, но мощный класс для создания бегущих строк. Поддерживает
|
||||
* перемещение мышью и прокрутку колесом, полностью настраивается очень гибок
|
||||
* для настроек в CSS и подразумевается, что отлично индексируется поисковыми роботами.
|
||||
* Имеет свой препроцессор, благодаря которому можно создавать бегущие строки
|
||||
* без программирования - с помощью HTML-аттрибутов, а так же возможность
|
||||
* изменять параметры (data-hotline-* аттрибуты) на лету. Есть возможность вызывать
|
||||
* события при выбранных действиях для того, чтобы пользователь имел возможность
|
||||
* дорабатывать функционал без изучения и изменения моего кода
|
||||
*
|
||||
* @example
|
||||
* сonst hotline = new hotline();
|
||||
* hotline.step = '-5';
|
||||
* hotline.start();
|
||||
*
|
||||
* @todo
|
||||
* 1. Бесконечный режим - элементы не удаляются если видны на экране (будут дубликаты).
|
||||
* Сейчас при БЫСТРОМ прокручивании можно заметит как элементы "появляются" в начале и конце строки.
|
||||
* 2. "gap" and "padding" in wrap should be removed! or added here to the calculations
|
||||
*
|
||||
* @copyright WTFPL
|
||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||
*/
|
||||
core.hotline = class hotline {
|
||||
// Идентификатор
|
||||
#id = 0;
|
||||
|
||||
// Оболочка (instanceof HTMLElement)
|
||||
#shell = document.getElementById("hotline");
|
||||
|
||||
// Инстанция горячей строки
|
||||
#instance = null;
|
||||
|
||||
// Перемещение
|
||||
#transfer = true;
|
||||
|
||||
// Движение
|
||||
#move = true;
|
||||
|
||||
// Наблюдатель
|
||||
#observer = null;
|
||||
|
||||
// Реестр запрещённых к изменению параметров
|
||||
#block = new Set(["events"]);
|
||||
|
||||
// Status (null, active, inactive)
|
||||
#status = null;
|
||||
|
||||
// Настраиваемые параметры
|
||||
transfer = null;
|
||||
move = null;
|
||||
delay = 10;
|
||||
step = 1;
|
||||
hover = true;
|
||||
movable = true;
|
||||
sticky = false;
|
||||
wheel = false;
|
||||
delta = null;
|
||||
vertical = false;
|
||||
button = 0; // button for grabbing. 0 is main mouse button (left)
|
||||
observe = false;
|
||||
events = new Map([
|
||||
["start", false],
|
||||
["stop", false],
|
||||
["move", false],
|
||||
["move.block", false],
|
||||
["move.unblock", false],
|
||||
["offset", false],
|
||||
["transfer.start", true],
|
||||
["transfer.end", true],
|
||||
["mousemove", false],
|
||||
["touchmove", false],
|
||||
]);
|
||||
|
||||
// Is hotline currently moving due to "onmousemove" or "ontouchmove"?
|
||||
moving = false;
|
||||
|
||||
constructor(id, shell) {
|
||||
// Запись идентификатора
|
||||
if (typeof id === "string" || typeof id === "number") this.#id = id;
|
||||
|
||||
// Запись оболочки
|
||||
if (shell instanceof HTMLElement) this.#shell = shell;
|
||||
}
|
||||
|
||||
start() {
|
||||
if (this.#instance === null) {
|
||||
// Нет запущенной инстанции бегущей строки
|
||||
|
||||
// Инициализация ссылки на ядро
|
||||
const _this = this;
|
||||
|
||||
// Запуск движения
|
||||
this.#instance = setInterval(function () {
|
||||
if (_this.#shell.childElementCount > 1) {
|
||||
// Найдено содержимое бегущей строки (2 и более)
|
||||
|
||||
// Инициализация буфера для временных данных
|
||||
let buffer;
|
||||
|
||||
// Инициализация данных первого элемента в строке
|
||||
const first = {
|
||||
element: (buffer = _this.#shell.firstElementChild),
|
||||
coords: buffer.getBoundingClientRect(),
|
||||
};
|
||||
|
||||
if (_this.vertical) {
|
||||
// Вертикальная бегущая строка
|
||||
|
||||
// Инициализация сдвига у первого элемента (движение)
|
||||
first.offset = isNaN(
|
||||
buffer = parseFloat(first.element.style.marginTop),
|
||||
)
|
||||
? 0
|
||||
: buffer;
|
||||
|
||||
// Инициализация отступа до второго элемента у первого элемента (разделение)
|
||||
first.separator = isNaN(
|
||||
buffer = parseFloat(
|
||||
getComputedStyle(first.element).marginBottom,
|
||||
),
|
||||
)
|
||||
? 0
|
||||
: buffer;
|
||||
|
||||
// Инициализация крайнего с конца ребра первого элемента в строке
|
||||
first.end = first.coords.y + first.coords.height +
|
||||
first.separator;
|
||||
} else {
|
||||
// Горизонтальная бегущая строка
|
||||
|
||||
// Инициализация отступа у первого элемента (движение)
|
||||
first.offset = isNaN(
|
||||
buffer = parseFloat(first.element.style.marginLeft),
|
||||
)
|
||||
? 0
|
||||
: buffer;
|
||||
|
||||
// Инициализация отступа до второго элемента у первого элемента (разделение)
|
||||
first.separator = isNaN(
|
||||
buffer = parseFloat(
|
||||
getComputedStyle(first.element).marginRight,
|
||||
),
|
||||
)
|
||||
? 0
|
||||
: buffer;
|
||||
|
||||
// Инициализация крайнего с конца ребра первого элемента в строке
|
||||
first.end = first.coords.x + first.coords.width +
|
||||
first.separator;
|
||||
}
|
||||
|
||||
if (
|
||||
(_this.vertical &&
|
||||
Math.round(first.end) < _this.#shell.offsetTop) ||
|
||||
(!_this.vertical &&
|
||||
Math.round(first.end) < _this.#shell.offsetLeft)
|
||||
) {
|
||||
// Элемент (вместе с отступом до второго элемента) вышел из области видимости (строки)
|
||||
|
||||
if (
|
||||
(_this.transfer === null && _this.#transfer) ||
|
||||
_this.transfer === true
|
||||
) {
|
||||
// Перенос разрешен
|
||||
|
||||
if (_this.vertical) {
|
||||
// Вертикальная бегущая строка
|
||||
|
||||
// Удаление отступов (движения)
|
||||
first.element.style.marginTop = null;
|
||||
} else {
|
||||
// Горизонтальная бегущая строка
|
||||
|
||||
// Удаление отступов (движения)
|
||||
first.element.style.marginLeft = null;
|
||||
}
|
||||
|
||||
// Копирование первого элемента в конец строки
|
||||
_this.#shell.appendChild(first.element);
|
||||
|
||||
if (_this.events.get("transfer.end")) {
|
||||
// Запрошен вызов события: "перемещение в конец"
|
||||
|
||||
// Вызов события: "перемещение в конец"
|
||||
document.dispatchEvent(
|
||||
new CustomEvent(`hotline.${_this.#id}.transfer.end`, {
|
||||
detail: {
|
||||
element: first.element,
|
||||
offset: -(
|
||||
(_this.vertical
|
||||
? first.coords.height
|
||||
: first.coords.width) + first.separator
|
||||
),
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
(_this.vertical &&
|
||||
Math.round(first.coords.y) > _this.#shell.offsetTop) ||
|
||||
(!_this.vertical &&
|
||||
Math.round(first.coords.x) > _this.#shell.offsetLeft)
|
||||
) {
|
||||
// Передняя (движущая) граница первого элемента вышла из области видимости
|
||||
|
||||
if (
|
||||
(_this.transfer === null && _this.#transfer) ||
|
||||
_this.transfer === true
|
||||
) {
|
||||
// Перенос разрешен
|
||||
|
||||
// Инициализация отступа у последнего элемента (разделение)
|
||||
const separator = (buffer = isNaN(
|
||||
buffer = parseFloat(
|
||||
getComputedStyle(_this.#shell.lastElementChild)[
|
||||
_this.vertical ? "marginBottom" : "marginRight"
|
||||
],
|
||||
),
|
||||
)
|
||||
? 0
|
||||
: buffer) === 0
|
||||
? first.separator
|
||||
: buffer;
|
||||
|
||||
// Инициализация координат первого элемента в строке
|
||||
const coords = _this.#shell.lastElementChild
|
||||
.getBoundingClientRect();
|
||||
|
||||
if (_this.vertical) {
|
||||
// Вертикальная бегущая строка
|
||||
|
||||
// Удаление отступов (движения)
|
||||
_this.#shell.lastElementChild.style.marginTop =
|
||||
-coords.height - separator + "px";
|
||||
} else {
|
||||
// Горизонтальная бегущая строка
|
||||
|
||||
// Удаление отступов (движения)
|
||||
_this.#shell.lastElementChild.style.marginLeft =
|
||||
-coords.width - separator + "px";
|
||||
}
|
||||
|
||||
// Копирование последнего элемента в начало строки
|
||||
_this.#shell.insertBefore(
|
||||
_this.#shell.lastElementChild,
|
||||
first.element,
|
||||
);
|
||||
|
||||
// Удаление отступов у второго элемента в строке (движения)
|
||||
_this.#shell.children[1].style[
|
||||
_this.vertical ? "marginTop" : "marginLeft"
|
||||
] = null;
|
||||
|
||||
if (_this.events.get("transfer.start")) {
|
||||
// Запрошен вызов события: "перемещение в начало"
|
||||
|
||||
// Вызов события: "перемещение в начало"
|
||||
document.dispatchEvent(
|
||||
new CustomEvent(`hotline.${_this.#id}.transfer.start`, {
|
||||
detail: {
|
||||
element: _this.#shell.lastElementChild,
|
||||
offset:
|
||||
(_this.vertical ? coords.height : coords.width) +
|
||||
separator,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Элемент в области видимости
|
||||
|
||||
if (
|
||||
(_this.move === null && _this.#move) || _this.move === true
|
||||
) {
|
||||
// Движение разрешено
|
||||
|
||||
// Запись новых координат сдвига
|
||||
const offset = first.offset + _this.step;
|
||||
|
||||
// Запись сдвига (движение)
|
||||
_this.offset(offset);
|
||||
|
||||
if (_this.events.get("move")) {
|
||||
// Запрошен вызов события: "движение"
|
||||
|
||||
// Вызов события: "движение"
|
||||
document.dispatchEvent(
|
||||
new CustomEvent(`hotline.${_this.#id}.move`, {
|
||||
detail: {
|
||||
from: first.offset,
|
||||
to: offset,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, _this.delay);
|
||||
|
||||
if (this.hover) {
|
||||
// Запрошена возможность останавливать бегущую строку
|
||||
|
||||
// Инициализация сдвига
|
||||
let offset = 0;
|
||||
|
||||
// Инициализация слушателя события при перемещении элемента в бегущей строке
|
||||
const listener = function (e) {
|
||||
// Увеличение сдвига
|
||||
offset += e.detail.offset ?? 0;
|
||||
};
|
||||
|
||||
// Объявление переменной в области видимости обработки остановки бегущей строки
|
||||
let move;
|
||||
|
||||
// Инициализация обработчика наведения курсора (остановка движения)
|
||||
this.#shell.onmouseover = function (e) {
|
||||
// Курсор наведён на бегущую строку
|
||||
|
||||
// Блокировка движения
|
||||
_this.#move = false;
|
||||
|
||||
if (_this.events.get("move.block")) {
|
||||
// Запрошен вызов события: "блокировка движения"
|
||||
|
||||
// Вызов события: "блокировка движения"
|
||||
document.dispatchEvent(
|
||||
new CustomEvent(`hotline.${_this.#id}.move.block`),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (this.movable) {
|
||||
// Запрошена возможность двигать бегущую строку
|
||||
|
||||
_this.#shell.onmousedown =
|
||||
_this.#shell.ontouchstart =
|
||||
function (
|
||||
start,
|
||||
) {
|
||||
// Handling a "mousedown" and a "touchstart" on hotline
|
||||
|
||||
if (
|
||||
start.type === "touchstart" ||
|
||||
start.button === _this.button
|
||||
) {
|
||||
const x = start.pageX || start.touches[0].pageX;
|
||||
const y = start.pageY || start.touches[0].pageY;
|
||||
|
||||
// Блокировка движения
|
||||
_this.#move = false;
|
||||
|
||||
if (_this.events.get("move.block")) {
|
||||
// Запрошен вызов события: "блокировка движения"
|
||||
|
||||
// Вызов события: "блокировка движения"
|
||||
document.dispatchEvent(
|
||||
new CustomEvent(`hotline.${_this.#id}.move.block`),
|
||||
);
|
||||
}
|
||||
|
||||
// Инициализация слушателей события перемещения элемента в бегущей строке
|
||||
document.addEventListener(
|
||||
`hotline.${_this.#id}.transfer.start`,
|
||||
listener,
|
||||
);
|
||||
document.addEventListener(
|
||||
`hotline.${_this.#id}.transfer.end`,
|
||||
listener,
|
||||
);
|
||||
|
||||
// Инициализация буфера для временных данных
|
||||
let buffer;
|
||||
|
||||
// Инициализация данных первого элемента в строке
|
||||
const first = {
|
||||
offset: isNaN(
|
||||
buffer = parseFloat(
|
||||
_this.vertical
|
||||
? _this.#shell.firstElementChild.style
|
||||
.marginTop
|
||||
: _this.#shell.firstElementChild.style
|
||||
.marginLeft,
|
||||
),
|
||||
)
|
||||
? 0
|
||||
: buffer,
|
||||
};
|
||||
|
||||
move = (move) => {
|
||||
// Обработка движения курсора
|
||||
|
||||
if (_this.#status === "active") {
|
||||
// Запись статуса ручного перемещения
|
||||
_this.moving = true;
|
||||
|
||||
const _x = move.pageX || move.touches[0].pageX;
|
||||
const _y = move.pageY || move.touches[0].pageY;
|
||||
|
||||
if (_this.vertical) {
|
||||
// Вертикальная бегущая строка
|
||||
|
||||
// Инициализация буфера местоположения
|
||||
const from =
|
||||
_this.#shell.firstElementChild.style.marginTop;
|
||||
const to = _y - (y + offset - first.offset);
|
||||
|
||||
// Движение
|
||||
_this.#shell.firstElementChild.style.marginTop =
|
||||
to +
|
||||
"px";
|
||||
} else {
|
||||
// Горизонтальная бегущая строка
|
||||
|
||||
// Инициализация буфера местоположения
|
||||
const from =
|
||||
_this.#shell.firstElementChild.style.marginLeft;
|
||||
const to = _x - (x + offset - first.offset);
|
||||
|
||||
// Движение
|
||||
_this.#shell.firstElementChild.style.marginLeft =
|
||||
to +
|
||||
"px";
|
||||
}
|
||||
|
||||
if (_this.events.get(move.type)) {
|
||||
// Запрошен вызов события: "перемещение" (мышью или касанием)
|
||||
|
||||
// Вызов события: "перемещение" (мышью или касанием)
|
||||
document.dispatchEvent(
|
||||
new CustomEvent(
|
||||
`hotline.${_this.#id}.${move.type}`,
|
||||
{
|
||||
detail: { from, to },
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Запись курсора
|
||||
_this.#shell.style.cursor = "grabbing";
|
||||
}
|
||||
};
|
||||
|
||||
// Запуск обработки движения
|
||||
document.addEventListener("mousemove", move);
|
||||
document.addEventListener("touchmove", move);
|
||||
}
|
||||
};
|
||||
|
||||
// Перещапись событий браузера (чтобы не дёргалось)
|
||||
_this.#shell.ondragstart = null;
|
||||
|
||||
_this.#shell.onmouseup = _this.#shell.ontouchend = function () {
|
||||
// Курсор деактивирован
|
||||
|
||||
// Запись статуса ручного перемещения
|
||||
_this.moving = false;
|
||||
|
||||
// Остановка обработки движения
|
||||
document.removeEventListener("mousemove", move);
|
||||
document.removeEventListener("touchmove", move);
|
||||
|
||||
// Сброс сдвига
|
||||
offset = 0;
|
||||
|
||||
document.removeEventListener(
|
||||
`hotline.${_this.#id}.transfer.start`,
|
||||
listener,
|
||||
);
|
||||
document.removeEventListener(
|
||||
`hotline.${_this.#id}.transfer.end`,
|
||||
listener,
|
||||
);
|
||||
|
||||
// Разблокировка движения
|
||||
_this.#move = true;
|
||||
|
||||
if (_this.events.get("move.unblock")) {
|
||||
// Запрошен вызов события: "разблокировка движения"
|
||||
|
||||
// Вызов события: "разблокировка движения"
|
||||
document.dispatchEvent(
|
||||
new CustomEvent(`hotline.${_this.#id}.move.unblock`),
|
||||
);
|
||||
}
|
||||
|
||||
// Восстановление курсора
|
||||
_this.#shell.style.cursor = null;
|
||||
};
|
||||
}
|
||||
|
||||
// Инициализация обработчика отведения курсора (остановка движения)
|
||||
this.#shell.onmouseleave = function (onmouseleave) {
|
||||
// Курсор отведён от бегущей строки
|
||||
|
||||
if (!_this.sticky) {
|
||||
// Отключено прилипание
|
||||
|
||||
// Запись статуса ручного перемещения
|
||||
_this.moving = false;
|
||||
|
||||
// Остановка обработки движения
|
||||
document.removeEventListener("mousemove", move);
|
||||
document.removeEventListener("touchmove", move);
|
||||
|
||||
document.removeEventListener(
|
||||
`hotline.${_this.#id}.transfer.start`,
|
||||
listener,
|
||||
);
|
||||
document.removeEventListener(
|
||||
`hotline.${_this.#id}.transfer.end`,
|
||||
listener,
|
||||
);
|
||||
|
||||
// Восстановление курсора
|
||||
_this.#shell.style.cursor = null;
|
||||
}
|
||||
|
||||
// Сброс сдвига
|
||||
offset = 0;
|
||||
|
||||
// Разблокировка движения
|
||||
_this.#move = true;
|
||||
|
||||
if (_this.events.get("move.unblock")) {
|
||||
// Запрошен вызов события: "разблокировка движения"
|
||||
|
||||
// Вызов события: "разблокировка движения"
|
||||
document.dispatchEvent(
|
||||
new CustomEvent(`hotline.${_this.#id}.move.unblock`),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (this.wheel) {
|
||||
// Запрошена возможность прокручивать колесом мыши
|
||||
|
||||
// Инициализация обработчика наведения курсора (остановка движения)
|
||||
this.#shell.onwheel = function (e) {
|
||||
// Курсор наведён на бегущую
|
||||
|
||||
// Инициализация буфера для временных данных
|
||||
let buffer;
|
||||
|
||||
// Перемещение
|
||||
_this.offset(
|
||||
(isNaN(
|
||||
buffer = parseFloat(
|
||||
_this.#shell.firstElementChild.style[
|
||||
_this.vertical ? "marginTop" : "marginLeft"
|
||||
],
|
||||
),
|
||||
)
|
||||
? 0
|
||||
: buffer) +
|
||||
(_this.delta === null
|
||||
? e.wheelDelta
|
||||
: e.wheelDelta > 0
|
||||
? _this.delta
|
||||
: -_this.delta),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
this.#status = "active";
|
||||
}
|
||||
|
||||
if (this.observe) {
|
||||
// Запрошено наблюдение за изменениями аттрибутов элемента бегущей строки
|
||||
|
||||
if (this.#observer === null) {
|
||||
// Отсутствует наблюдатель
|
||||
|
||||
// Инициализация ссылки на ядро
|
||||
const _this = this;
|
||||
|
||||
// Инициализация наблюдателя
|
||||
this.#observer = new MutationObserver(function (mutations) {
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.type === "attributes") {
|
||||
// Запись параметра в инстанцию бегущей строки
|
||||
_this.configure(mutation.attributeName);
|
||||
}
|
||||
}
|
||||
|
||||
// Перезапуск бегущей строки
|
||||
_this.restart();
|
||||
});
|
||||
|
||||
// Активация наблюдения
|
||||
this.#observer.observe(this.#shell, {
|
||||
attributes: true,
|
||||
});
|
||||
}
|
||||
} else if (this.#observer instanceof MutationObserver) {
|
||||
// Запрошено отключение наблюдения
|
||||
|
||||
// Деактивация наблюдения
|
||||
this.#observer.disconnect();
|
||||
|
||||
// Удаление наблюдателя
|
||||
this.#observer = null;
|
||||
}
|
||||
|
||||
if (this.events.get("start")) {
|
||||
// Запрошен вызов события: "запуск"
|
||||
|
||||
// Вызов события: "запуск"
|
||||
document.dispatchEvent(
|
||||
new CustomEvent(`hotline.${this.#id}.start`),
|
||||
);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.#status = "inactive";
|
||||
|
||||
// Остановка бегущей строки
|
||||
clearInterval(this.#instance);
|
||||
|
||||
// Удаление инстанции интервала
|
||||
this.#instance = null;
|
||||
|
||||
if (this.events.get("stop")) {
|
||||
// Запрошен вызов события: "остановка"
|
||||
|
||||
// Вызов события: "остановка"
|
||||
document.dispatchEvent(new CustomEvent(`hotline.${this.#id}.stop`));
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
restart() {
|
||||
// Остановка бегущей строки
|
||||
this.stop();
|
||||
|
||||
// Запуск бегущей строки
|
||||
this.start();
|
||||
}
|
||||
|
||||
configure(attribute) {
|
||||
// Инициализация названия параметра
|
||||
const parameter =
|
||||
(/^data-hotline-(\w+)$/.exec(attribute) ?? [, null])[1];
|
||||
|
||||
if (typeof parameter === "string") {
|
||||
// Параметр найден
|
||||
|
||||
// Проверка на разрешение изменения
|
||||
if (this.#block.has(parameter)) return;
|
||||
|
||||
// Инициализация значения параметра
|
||||
const value = this.#shell.getAttribute(attribute);
|
||||
|
||||
if (typeof value !== undefined || typeof value !== null) {
|
||||
// Найдено значение
|
||||
|
||||
// Инициализация буфера для временных данных
|
||||
let buffer;
|
||||
|
||||
// Запись параметра
|
||||
this[parameter] = isNaN(buffer = parseFloat(value))
|
||||
? value === "true" ? true : value === "false" ? false : value
|
||||
: buffer;
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
offset(value) {
|
||||
// Запись отступа
|
||||
this.#shell.firstElementChild.style[
|
||||
this.vertical ? "marginTop" : "marginLeft"
|
||||
] = value + "px";
|
||||
|
||||
if (this.events.get("offset")) {
|
||||
// Запрошен вызов события: "сдвиг"
|
||||
|
||||
// Вызов события: "сдвиг"
|
||||
document.dispatchEvent(
|
||||
new CustomEvent(`hotline.${this.#id}.offset`, {
|
||||
detail: {
|
||||
to: value,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
static preprocessing(event = false) {
|
||||
// Инициализация счётчиков инстанций горячей строки
|
||||
const success = new Set();
|
||||
let error = 0;
|
||||
|
||||
for (
|
||||
const element of document.querySelectorAll('*[data-hotline="true"]')
|
||||
) {
|
||||
// Перебор элементов для инициализации бегущих строк
|
||||
|
||||
if (typeof element.id === "string") {
|
||||
// Найден идентификатор
|
||||
|
||||
// Инициализация инстанции бегущей строки
|
||||
const hotline = new this(element.id, element);
|
||||
|
||||
for (const attribute of element.getAttributeNames()) {
|
||||
// Перебор аттрибутов
|
||||
|
||||
// Запись параметра в инстанцию бегущей строки
|
||||
hotline.configure(attribute);
|
||||
}
|
||||
|
||||
// Запуск бегущей строки
|
||||
hotline.start();
|
||||
|
||||
// Запись инстанции бегущей строки в элемент
|
||||
element.hotline = hotline;
|
||||
|
||||
// Запись в счётчик успешных инициализаций
|
||||
success.add(hotline);
|
||||
} else ++error;
|
||||
}
|
||||
|
||||
if (event) {
|
||||
// Запрошен вызов события: "предварительная подготовка"
|
||||
|
||||
// Вызов события: "предварительная подготовка"
|
||||
document.dispatchEvent(
|
||||
new CustomEvent(`hotline.preprocessed`, {
|
||||
detail: {
|
||||
success,
|
||||
error,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
|
@ -0,0 +1,40 @@
|
|||
"use strict";
|
||||
|
||||
// Import dependencies
|
||||
import("/js/core.js").then(() =>
|
||||
import("/js/damper.js").then(() => {
|
||||
const dependencies = setInterval(() => {
|
||||
if (typeof core === "function" &&
|
||||
typeof core.damper === "function") {
|
||||
clearInterval(dependencies);
|
||||
clearTimeout(timeout);
|
||||
initialization();
|
||||
}
|
||||
}, 10);
|
||||
const timeout = setTimeout(() => clearInterval(dependencies), 5000);
|
||||
|
||||
function initialization() {
|
||||
if (typeof core.session === "undefined") {
|
||||
// Not initialized
|
||||
|
||||
// Write to the core
|
||||
core.session = class session {
|
||||
/**
|
||||
* Current position in hierarchy of the categories
|
||||
*/
|
||||
static categories = [];
|
||||
|
||||
/**
|
||||
* @return {void}
|
||||
*/
|
||||
static connect() {
|
||||
core.request(
|
||||
"/session/connect/telegram",
|
||||
window.Telegram.WebApp.initData,
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
|
@ -0,0 +1,40 @@
|
|||
"use strict";
|
||||
|
||||
// Import dependencies
|
||||
import("/js/core.js").then(() =>
|
||||
import("/js/damper.js").then(() => {
|
||||
const dependencies = setInterval(() => {
|
||||
if (
|
||||
typeof core === "function" &&
|
||||
typeof core.damper === "function"
|
||||
) {
|
||||
clearInterval(dependencies);
|
||||
clearTimeout(timeout);
|
||||
initialization();
|
||||
}
|
||||
}, 10);
|
||||
const timeout = setTimeout(() => clearInterval(dependencies), 5000);
|
||||
|
||||
function initialization() {
|
||||
if (typeof core.telegram === "undefined") {
|
||||
// Not initialized
|
||||
|
||||
// Write to the core
|
||||
core.telegram = class telegram {
|
||||
/**
|
||||
* Telegram WebApp API
|
||||
*
|
||||
* @see {@link https://core.telegram.org/bots/webapps#initializing-mini-apps}
|
||||
*/
|
||||
static api = window.Telegram.WebApp;
|
||||
};
|
||||
|
||||
/* telegram.MainButton.text =
|
||||
typeof core === "object" && core.language === "ru"
|
||||
? "Корзина"
|
||||
: "Cart";
|
||||
telegram.MainButton.show(); */
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
|
@ -1,30 +1,25 @@
|
|||
<?php
|
||||
|
||||
// Фреймворк ArangoDB
|
||||
use mirzaev\arangodb\connection,
|
||||
mirzaev\arangodb\collection,
|
||||
mirzaev\arangodb\document;
|
||||
declare(strict_types=1);
|
||||
|
||||
// Библиотека для ArangoDB
|
||||
use ArangoDBClient\Document as _document,
|
||||
ArangoDBClient\Cursor,
|
||||
ArangoDBClient\Statement as _statement;
|
||||
namespace mirzaev\arming_bot;
|
||||
|
||||
// Files of the project
|
||||
use mirzaev\arming_bot\controllers\core as controller,
|
||||
mirzaev\arming_bot\models\core as model,
|
||||
mirzaev\arming_bot\models\chat;
|
||||
|
||||
// Фреймворк Telegram
|
||||
use Zanzara\Zanzara,
|
||||
Zanzara\Context,
|
||||
Zanzara\Config,
|
||||
Zanzara\Telegram\Type\Input\InputFile,
|
||||
Zanzara\Telegram\Type\File\Document as telegram_document,
|
||||
Zanzara\Telegram\Type\File\File,
|
||||
Zanzara\Middleware\MiddlewareNode as Node;
|
||||
Zanzara\Config;
|
||||
|
||||
ini_set('error_reporting', E_ALL);
|
||||
/* ini_set('error_reporting', E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
ini_set('display_startup_errors', 1);
|
||||
ini_set('display_startup_errors', 1); */
|
||||
|
||||
// Версия робота
|
||||
define('VERSION', '1.0.0');
|
||||
define('ROBOT_VERSION', '1.0.0');
|
||||
|
||||
// Путь до настроек
|
||||
define('SETTINGS', __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'settings');
|
||||
|
@ -42,574 +37,52 @@ define('CATALOG_IMPORT', STORAGE . DIRECTORY_SEPARATOR . 'import.xlsx');
|
|||
define('KEY', require(SETTINGS . DIRECTORY_SEPARATOR . 'key.php'));
|
||||
|
||||
// Инициализация библиотек
|
||||
require __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php';
|
||||
require __DIR__ . DIRECTORY_SEPARATOR
|
||||
. '..' . DIRECTORY_SEPARATOR
|
||||
. '..' . DIRECTORY_SEPARATOR
|
||||
. '..' . DIRECTORY_SEPARATOR
|
||||
. '..' . DIRECTORY_SEPARATOR
|
||||
. 'vendor' . DIRECTORY_SEPARATOR
|
||||
. 'autoload.php';
|
||||
|
||||
// Инициализация инстанции соединения с ArangoDB
|
||||
$arangodb = new connection(require SETTINGS . DIRECTORY_SEPARATOR . 'arangodb.php');
|
||||
// Инициализация ядра контроллеров MINIMAL
|
||||
new controller(false);
|
||||
|
||||
/**
|
||||
* Экранирование символов для Markdown
|
||||
*
|
||||
* @param string $text Текст для экранирования
|
||||
* @param array $exception Символы которые будут исключены из списка для экранирования
|
||||
*
|
||||
* @return string Экранированный текст
|
||||
*/
|
||||
function unmarkdown(string $text, array $exceptions = []): string
|
||||
{
|
||||
// Инициализация реестра символом для конвертации
|
||||
$from = array_diff(
|
||||
[
|
||||
'#',
|
||||
'*',
|
||||
'_',
|
||||
'=',
|
||||
'.',
|
||||
'[',
|
||||
']',
|
||||
'(',
|
||||
')',
|
||||
'-',
|
||||
'>',
|
||||
'<',
|
||||
'!',
|
||||
'`'
|
||||
],
|
||||
$exceptions
|
||||
);
|
||||
|
||||
// Инициализация реестра целей для конвертации
|
||||
$to = [];
|
||||
foreach ($from as $symbol) $to[] = "\\$symbol";
|
||||
|
||||
// Конвертация и выход (успех)
|
||||
return str_replace($from, $to, $text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Инициализация запчасти
|
||||
*
|
||||
* Проверяет существование запчасти
|
||||
*
|
||||
* @param string $spare Запчасть
|
||||
*
|
||||
* @return string|bool Запчасть, если найдена, иначе false
|
||||
*/
|
||||
function spares(string $spare): string|bool
|
||||
{
|
||||
// Поиск запчастей и выход (успех)
|
||||
return match (mb_strtolower($spare)) {
|
||||
'цевьё' => 'Цевьё',
|
||||
default => false
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Авторизация
|
||||
*
|
||||
* @param string $id Идентификатор Telegram
|
||||
* @param bool $registration Регистрация, если аккаунт не найден
|
||||
*
|
||||
* @return _document|null Инстанция аккаунта, если найден
|
||||
*/
|
||||
function authorization(string $id, bool $registration = true): _document|null
|
||||
{
|
||||
global $arangodb;
|
||||
|
||||
if (collection::init($arangodb->session, 'account')) {
|
||||
if ($account = collection::search($arangodb->session, sprintf("FOR d IN account FILTER d.id == '%s' RETURN d", $id))) {
|
||||
// Найден аккаунт
|
||||
|
||||
// Возврат (успех)
|
||||
return $account;
|
||||
} else if ($registration) {
|
||||
// Не найден аккаунт и запрошена его регистрация
|
||||
|
||||
// Создание аккаунта
|
||||
document::write($arangodb->session, 'account', ['id' => $id, 'banned' => false, 'settings' => false, 'version' => VERSION]);
|
||||
|
||||
// Авторизация (без регистрации)
|
||||
return authorization($id, false);
|
||||
} else {
|
||||
// Не найден аккаунт и не запрошена его регистрация
|
||||
|
||||
// Выход (провал)
|
||||
return null;
|
||||
}
|
||||
} else throw new exception('Не удалось инициализировать коллекцию: account');
|
||||
|
||||
// Выход (провал)
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Главное меню
|
||||
*
|
||||
* Команда: /start
|
||||
*
|
||||
* @param Context $ctx
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function menu(Context $ctx): void
|
||||
{
|
||||
// Инициализация клавиатуры
|
||||
$keyboard = [
|
||||
[
|
||||
['text' => '🛒 Каталог']
|
||||
],
|
||||
[
|
||||
['text' => '💬 Контакты'],
|
||||
['text' => '🏛️ О компании']
|
||||
],
|
||||
[
|
||||
['text' => '🎯 Сообщество']
|
||||
]
|
||||
];
|
||||
|
||||
if ($ctx->get('account')?->settings) $keyboard[] = [['text' => '⚙️ Настройки']];
|
||||
|
||||
// Отправка сообщения
|
||||
$ctx->sendMessage(
|
||||
unmarkdown(<<<TXT
|
||||
Это сообщение будет отображаться (оно должно быть обязательно) при вызове главного меню командой /start (создаёт кнопки меню снизу)
|
||||
TXT),
|
||||
[
|
||||
'reply_markup' => [
|
||||
'keyboard' => $keyboard,
|
||||
'resize_keyboard' => true
|
||||
],
|
||||
'disable_notification' => true
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Категории
|
||||
*
|
||||
* Команда: /catalog
|
||||
*
|
||||
* @param Context $ctx
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function categories(Context $ctx): void
|
||||
{
|
||||
// Отправка сообщения
|
||||
$ctx->sendMessage(unmarkdown(<<<TXT
|
||||
Выберите категорию
|
||||
TXT), [
|
||||
'reply_markup' => [
|
||||
'inline_keyboard' => [
|
||||
[
|
||||
['text' => '🗜️ Тюнинг', 'callback_data' => 'tuning']
|
||||
]
|
||||
]
|
||||
],
|
||||
'link_preview_options' => [
|
||||
'is_disabled' => true
|
||||
],
|
||||
'disable_notification' => true
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Тюнинг
|
||||
*
|
||||
* @param Context $ctx
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function tuning(Context $ctx): void
|
||||
{
|
||||
// Отправка сообщения
|
||||
$ctx->sendMessage(<<<TXT
|
||||
Выберите запчасть
|
||||
TXT, [
|
||||
'reply_markup' => [
|
||||
'inline_keyboard' => [
|
||||
[
|
||||
['text' => 'Цевьё', 'callback_data' => 'brands']
|
||||
]
|
||||
]
|
||||
],
|
||||
'link_preview_options' => [
|
||||
'is_disabled' => true
|
||||
],
|
||||
'disable_notification' => true
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Бренды
|
||||
*
|
||||
* @param Context $ctx
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function brands(Context $ctx): void
|
||||
{
|
||||
if ($spare = spares($ctx->getMessage()->getText())) {
|
||||
// Инициализирована запчасть
|
||||
|
||||
// Отправка сообщения
|
||||
$ctx->sendMessage(<<<TXT
|
||||
Выберите бренд
|
||||
TXT, [
|
||||
'link_preview_options' => [
|
||||
'is_disabled' => true
|
||||
],
|
||||
'disable_notification' => true
|
||||
])->then(function ($message) use ($ctx) {
|
||||
$ctx;
|
||||
});
|
||||
} else {
|
||||
// Не инициализирована запчасть
|
||||
|
||||
// Отправка сообщения
|
||||
$ctx->sendMessage('⚠️ *Не найдена запчасть*');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Контакты
|
||||
*
|
||||
* Команда: /contacts
|
||||
*
|
||||
* @param Context $ctx
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function contacts(Context $ctx): void
|
||||
{
|
||||
// Отправка сообщения
|
||||
$ctx->sendMessage(unmarkdown(<<<TXT
|
||||
Здесь придумать текст для раздела "Контакты"
|
||||
TXT), [
|
||||
'reply_markup' => [
|
||||
'inline_keyboard' => [
|
||||
[
|
||||
['text' => '⚡ Связь с менеджером', 'url' => 'https://t.me/iarming'],
|
||||
],
|
||||
[
|
||||
['text' => '📨 Почта', 'callback_data' => 'mail']
|
||||
],
|
||||
[
|
||||
['text' => '🪖 Сайт', 'url' => 'https://arming.ru'],
|
||||
['text' => '🛒 Wildberries', 'url' => 'https://arming.ru']
|
||||
]
|
||||
]
|
||||
],
|
||||
'link_preview_options' => [
|
||||
'is_disabled' => true
|
||||
],
|
||||
'disable_notification' => true
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Почта
|
||||
*
|
||||
* @param Context $ctx
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function _mail(Context $ctx): void
|
||||
{
|
||||
// Отправка сообщения
|
||||
$ctx->sendMessage(unmarkdown(<<<TXT
|
||||
[info@arming.ru](mailto::info@arming.ru)
|
||||
TXT, ['[', ']', '(', ')']), [
|
||||
'link_preview_options' => [
|
||||
'is_disabled' => true
|
||||
],
|
||||
'disable_notification' => true
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Компания
|
||||
*
|
||||
* Команда: /company
|
||||
*
|
||||
* @param Context $ctx
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function company(Context $ctx): void
|
||||
{
|
||||
// Отправка сообщения
|
||||
$ctx->sendMessage(
|
||||
unmarkdown(<<<TXT
|
||||
Здесь придумать текст для раздела "Компания"
|
||||
TXT),
|
||||
/* [
|
||||
'reply_markup' => [
|
||||
'inline_keyboard' => [
|
||||
[
|
||||
['text' => '⚡ Связь с менеджером', 'url' => 'https://git.mirzaev.sexy/mirzaev/mashtrash'],
|
||||
['text' => '📨 Почта', 'text' => ''],
|
||||
],
|
||||
[
|
||||
['text' => '🪖 Сайт', 'url' => '']
|
||||
['text' => '🛒 Wildberries', 'url' => '']
|
||||
]
|
||||
]
|
||||
],
|
||||
'link_preview_options' => [
|
||||
'is_disabled' => true
|
||||
]
|
||||
] */
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Сообщество
|
||||
*
|
||||
* Команда: /community
|
||||
*
|
||||
* @param Context $ctx
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function community(Context $ctx): void
|
||||
{
|
||||
// Отправка сообщения
|
||||
$ctx->sendMessage(unmarkdown(<<<TXT
|
||||
Здесь придумать текст для раздела "Сообщество"
|
||||
TXT), [
|
||||
'reply_markup' => [
|
||||
'inline_keyboard' => [
|
||||
[
|
||||
['text' => '💬 Основной чат', 'url' => 'https://t.me/arming_zone'],
|
||||
]
|
||||
]
|
||||
],
|
||||
'link_preview_options' => [
|
||||
'is_disabled' => true
|
||||
],
|
||||
'disable_notification' => true
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Настройки (доступ только авторизованным)
|
||||
*
|
||||
* Команда: /settings
|
||||
*
|
||||
* @param Context $ctx
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function settings(Context $ctx): void
|
||||
{
|
||||
if ($ctx->get('account')?->settings) {
|
||||
// Авторизован доступ к настройкам
|
||||
|
||||
// Отправка сообщения
|
||||
$ctx->sendMessage(
|
||||
unmarkdown(<<<TXT
|
||||
Панель управления чат-роботом ARMING
|
||||
TXT),
|
||||
[
|
||||
'reply_markup' => [
|
||||
'inline_keyboard' => [
|
||||
[
|
||||
['text' => '📦 Импорт товаров', 'callback_data' => 'import_request'],
|
||||
]
|
||||
]
|
||||
],
|
||||
'link_preview_options' => [
|
||||
'is_disabled' => true
|
||||
],
|
||||
'disable_notification' => true
|
||||
]
|
||||
);
|
||||
} else {
|
||||
// Не авторизован доступ к настройкам
|
||||
|
||||
// Отправка сообщения
|
||||
$ctx->sendMessage('⛔ *Нет доступа*');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Запросить файл для импорта товаров (доступ только авторизованным)
|
||||
*
|
||||
* @param Context $ctx
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function import_request(Context $ctx): void
|
||||
{
|
||||
if ($ctx->get('account')?->settings) {
|
||||
// Авторизован доступ к настройкам
|
||||
|
||||
// Отправка сообщения
|
||||
$ctx->sendMessage(unmarkdown('Отправьте документ в формате xlsx со списком товаров'))
|
||||
->then(function ($message) use ($ctx) {
|
||||
// Отправка файла
|
||||
$ctx->sendDocument(new InputFile(CATALOG_EXAMPLE), ['disable_notification' => true]);
|
||||
|
||||
// Импорт файла
|
||||
$ctx->nextStep("import");
|
||||
});
|
||||
} else {
|
||||
// Не авторизован доступ к настройкам
|
||||
|
||||
// Отправка сообщения
|
||||
$ctx->sendMessage('⛔ *Нет доступа*');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Импорт товаров (доступ только авторизованным)
|
||||
*
|
||||
* @param Context $ctx
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function import(Context $ctx): void
|
||||
{
|
||||
if ($ctx->get('account')?->settings) {
|
||||
// Авторизован доступ к настройкам
|
||||
|
||||
// Инициализация документа
|
||||
$document = $ctx->getMessage()?->getDocument();
|
||||
|
||||
if ($document instanceof telegram_document) {
|
||||
// Инициализирован документ
|
||||
|
||||
// Инициализация файла
|
||||
$ctx->getFile($document->getFileId())->then(function ($file) use ($ctx) {
|
||||
|
||||
if ($file->getFileSize() <= 50000000) {
|
||||
// Не превышает 50 мегабайт (50000000 байт) размер файла
|
||||
|
||||
if ($file->getFilePath()['extension'] === 'xlsx') {
|
||||
// Имеет расширение xlsx файл
|
||||
|
||||
// Сохранение файла
|
||||
file_put_contents(STORAGE . DIRECTORY_SEPARATOR . 'import.xlsx', file_get_contents('https://api.telegram.org/file/bot' . KEY . '/' . $file->getFilePath()));
|
||||
|
||||
// Инициализация счётчика загруженных товаров
|
||||
$loaded = $created = $updated = $deleted = $old = $new = 0;
|
||||
|
||||
|
||||
|
||||
// Отправка сообщения
|
||||
$ctx->sendMessage(<<<TXT
|
||||
*Загружено для обработки:* $loaded
|
||||
|
||||
*Добавлено:* $created
|
||||
*Обновлено:* $updated
|
||||
*Удалено:* $deleted
|
||||
|
||||
*Опубликовано в магазине:* $old \-\> *$new*
|
||||
TXT)
|
||||
->then(function ($message) use ($ctx) {
|
||||
// Завершение диалога
|
||||
$ctx->endConversation();
|
||||
});
|
||||
} else {
|
||||
// Не имеет расширение xlsx файл
|
||||
|
||||
// Отправка сообщения
|
||||
$ctx->sendMessage(unmarkdown('Файл должен иметь расширение xlsx'));
|
||||
}
|
||||
} else {
|
||||
// Превышает 50 мегабайт (50000000 байт) размер файла
|
||||
|
||||
// Отправка сообщения
|
||||
$ctx->sendMessage(unmarkdown('Размер файла не должен превышать 50 мегабайт'));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Не инициализирован документ
|
||||
|
||||
// Отправка сообщения
|
||||
$ctx->sendMessage(unmarkdown('Отправьте документ в формате xlsx со списком товаров'));
|
||||
}
|
||||
} else {
|
||||
// Не авторизован доступ к настройкам
|
||||
|
||||
// Отправка сообщения
|
||||
$ctx->sendMessage('⛔ *Нет доступа*');
|
||||
}
|
||||
}
|
||||
// Инициализация ядра моделей MINIMAL
|
||||
new model(true);
|
||||
|
||||
$config = new Config();
|
||||
$config->setParseMode(Config::PARSE_MODE_MARKDOWN);
|
||||
$config->useReactFileSystem(true);
|
||||
|
||||
$bot = new Zanzara(KEY, $config);
|
||||
$bot->onCommand('start', function (Context $ctx): void {
|
||||
// Главное меню
|
||||
menu($ctx);
|
||||
});
|
||||
|
||||
/* $bot->onUpdate(function (Context $ctx): void {}); */
|
||||
/* $bot->onUpdate(function (Context $ctx): void {
|
||||
var_dump($ctx->getMessage()->getWebAppData());
|
||||
var_dump($ctx->getEffectiveUser() );
|
||||
}); */
|
||||
|
||||
/**
|
||||
* Инициализация аккаунта (middleware)
|
||||
*
|
||||
* @param Context $ctx
|
||||
* @param Node $next
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
$account = function (Context $ctx, Node $next): void {
|
||||
// Выполнение заблокировано?
|
||||
if ($ctx->get('stop')) return;
|
||||
$bot->onCommand('start', fn($ctx) => chat::start($ctx));
|
||||
$bot->onCommand('contacts', fn($ctx) => chat::contacts($ctx));
|
||||
$bot->onCommand('company', fn($ctx) => chat::company($ctx));
|
||||
$bot->onCommand('community', fn($ctx) => chat::community($ctx));
|
||||
$bot->onCommand('settings', fn($ctx) => chat::settings($ctx));
|
||||
|
||||
// Авторизация аккаунта
|
||||
$account = authorization($ctx->getEffectiveUser()->getId());
|
||||
$bot->onText('💬 Контакты', fn($ctx) => chat::contacts($ctx));
|
||||
$bot->onText('🏛️ О компании', fn($ctx) => chat::company($ctx));
|
||||
$bot->onText('🎯 Сообщество', fn($ctx) => chat::community($ctx));
|
||||
$bot->onText('⚙️ Настройки', fn($ctx) => chat::settings($ctx));
|
||||
|
||||
if ($account instanceof _document) {
|
||||
// Инициализирован аккаунт (подразумевается)
|
||||
$bot->onCbQueryData(['mail'], fn($ctx) => chat::_mail($ctx));
|
||||
$bot->onCbQueryData(['import_request'], fn($ctx) => chat::import_request($ctx));
|
||||
$bot->onCbQueryData(['tuning'], fn($ctx) => chat::tuning($ctx));
|
||||
$bot->onCbQueryData(['brands'], fn($ctx) => chat::brands($ctx));
|
||||
|
||||
if ($account->banned) {
|
||||
// Заблокирован аккаунт
|
||||
// Инициализация middleware с обработкой аккаунта
|
||||
$bot->middleware([chat::class, "account"]);
|
||||
|
||||
// Отправка сообщения
|
||||
$ctx->sendMessage('⛔ *Ты заблокирован*');
|
||||
// Инициализация middleware с обработкой технических работ разных уровней
|
||||
$bot->middleware([chat::class, "suspension"]);
|
||||
|
||||
// Завершение диалога
|
||||
$ctx->endConversation();
|
||||
|
||||
// Блокировка дальнейшего выполнения
|
||||
$ctx->set('stop', true);
|
||||
} else {
|
||||
// Не заблокирован аккаунт
|
||||
|
||||
// Запись в буфер
|
||||
$ctx->set('account', $account);
|
||||
|
||||
// Продолжение выполнения
|
||||
$next($ctx);
|
||||
}
|
||||
} else {
|
||||
// Не инициализирован аккаунт
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
$bot->onCommand('catalog', fn($ctx) => categories($ctx));
|
||||
$bot->onCommand('contacts', fn($ctx) => contacts($ctx));
|
||||
$bot->onCommand('company', fn($ctx) => company($ctx));
|
||||
$bot->onCommand('community', fn($ctx) => community($ctx));
|
||||
$bot->onCommand('settings', fn($ctx) => settings($ctx));
|
||||
|
||||
$bot->onText('🛒 Каталог', fn($ctx) => categories($ctx));
|
||||
$bot->onText('💬 Контакты', fn($ctx) => contacts($ctx));
|
||||
$bot->onText('🏛️ О компании', fn($ctx) => company($ctx));
|
||||
$bot->onText('🎯 Сообщество', fn($ctx) => community($ctx));
|
||||
$bot->onText('⚙️ Настройки', fn($ctx) => settings($ctx));
|
||||
|
||||
$bot->onCbQueryData(['mail'], fn($ctx) => _mail($ctx));
|
||||
$bot->onCbQueryData(['import_request'], fn($ctx) => import_request($ctx));
|
||||
$bot->onCbQueryData(['tuning'], fn($ctx) => tuning($ctx));
|
||||
$bot->onCbQueryData(['brands'], fn($ctx) => brands($ctx));
|
||||
|
||||
$bot->middleware($account)->run();
|
||||
// Запуск чат-робота
|
||||
$bot->run();
|
||||
|
|
|
@ -0,0 +1,160 @@
|
|||
@charset "UTF-8";
|
||||
|
||||
main>section[data-section="catalog"] {
|
||||
width: var(--width);
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
gap: var(--gap, 5px);
|
||||
/* justify-content: space-between; */
|
||||
/* justify-content: space-evenly; */
|
||||
/* background-color: var(--tg-theme-secondary-bg-color); */
|
||||
}
|
||||
|
||||
main>section[data-section="catalog"]:last-child {
|
||||
margin-bottom: unset;
|
||||
}
|
||||
|
||||
main>section[data-section="catalog"]>a.category[type="button"] {
|
||||
height: 23px;
|
||||
padding: 8px 16px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
border-radius: 0.75rem;
|
||||
color: var(--tg-theme-button-text-color);
|
||||
background-color: var(--tg-theme-button-color);
|
||||
}
|
||||
|
||||
main>section[data-section="catalog"]>article.product {
|
||||
/* --product-height: 200px; */
|
||||
--product-height: 220px;
|
||||
--title-font-size: 0.9rem;
|
||||
--title-height: 1.5rem;
|
||||
--button-height: 33px;
|
||||
position: relative;
|
||||
/* width: calc((100% - var(--gap) * 2) / 3); */
|
||||
width: calc((100% - var(--gap)) / 2);
|
||||
height: var(--product-height);
|
||||
display: flex;
|
||||
flex-grow: 0;
|
||||
flex-direction: column;
|
||||
white-space: nowrap;
|
||||
border-radius: 0.75rem;
|
||||
overflow: clip;
|
||||
backdrop-filter: brightness(0.7);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
main>section[data-section="catalog"]>article.product:hover {
|
||||
/* flex-grow: 0.1; */
|
||||
/* background-color: var(--tg-theme-secondary-bg-color); */
|
||||
}
|
||||
|
||||
main>section[data-section="catalog"]>article.product:hover>* {
|
||||
transition: 0s;
|
||||
}
|
||||
|
||||
main>section[data-section="catalog"]>article.product:not(:hover)>* {
|
||||
transition: 0.2s ease-out;
|
||||
}
|
||||
|
||||
main>section[data-section="catalog"]>article.product>img:first-of-type {
|
||||
z-index: -50;
|
||||
position: absolute;
|
||||
bottom: var(--button-height);
|
||||
/* bottom: calc(var(--button-height) + var(--title-height)); */
|
||||
width: 100%;
|
||||
/* height: 100%; */
|
||||
height: calc(var(--product-height) - var(--button-height));
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
main>section[data-section="catalog"]>article.product>img:first-of-type+* {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
main>section[data-section="catalog"]>article.product>a {
|
||||
padding: 4px 8px 4px 8px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
word-break: break-all;
|
||||
font-weight: bold;
|
||||
backdrop-filter: brightness(0.4) contrast(1.2);
|
||||
color: var(--text-light, var(--tg-theme-text-color));
|
||||
}
|
||||
|
||||
main>section[data-section="catalog"]>article.product>a.title {
|
||||
padding: 0 8px 0 8px;
|
||||
height: var(--title-height);
|
||||
min-height: var(--title-height);
|
||||
line-height: var(--title-height);
|
||||
font-size: var(--title-font-size);
|
||||
}
|
||||
|
||||
main>section[data-section="catalog"]>article.product:hover>a.title {
|
||||
backdrop-filter: brightness(0.35) contrast(1.2);
|
||||
}
|
||||
|
||||
main>section[data-section="catalog"]>article.product>a+ :is(a, small) {
|
||||
padding: 0px 8px 0 8px;
|
||||
}
|
||||
|
||||
main>section[data-section="catalog"]>article.product>small {
|
||||
padding: 3px 8px 0 8px;
|
||||
white-space: normal;
|
||||
word-break: break-all;
|
||||
font-size: 0.7rem;
|
||||
backdrop-filter: brightness(0.4) contrast(1.2);
|
||||
color: var(--text-light, var(--tg-theme-text-color));
|
||||
}
|
||||
|
||||
main>section[data-section="catalog"]>article.product>small.description {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
main>section[data-section="catalog"]>article.product:hover>small.description {
|
||||
height: calc(var(--product-height) - var(--title-height) - var(--button-height) - 6px);
|
||||
}
|
||||
|
||||
main>section[data-section="catalog"]>article.product:not(:hover)>small.description {
|
||||
height: 0;
|
||||
padding: 0 8px 0px 8px !important;
|
||||
}
|
||||
|
||||
main>section[data-section="catalog"]>article.product>*:has(+ button:last-of-type) {
|
||||
--offset-before-button: 9px;
|
||||
padding: 4px 8px 13px 8px;
|
||||
}
|
||||
|
||||
main>section[data-section="catalog"]>article.product>button:last-of-type {
|
||||
height: var(--button-height);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
color: var(--tg-theme-button-text-color);
|
||||
background-color: var(--tg-theme-button-color);
|
||||
}
|
||||
|
||||
main>section[data-section="cart"] {
|
||||
--diameter: 4rem;
|
||||
z-index: 999;
|
||||
right: 5vw;
|
||||
bottom: 5vw;
|
||||
position: fixed;
|
||||
width: var(--diameter);
|
||||
height: var(--diameter);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
border-radius: 100%;
|
||||
background-color: var(--tg-theme-button-color);
|
||||
}
|
||||
|
||||
main>section[data-section="cart"]>i.icon.shopping.cart {
|
||||
top: -1px;
|
||||
color: var(--tg-theme-button-text-color);
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
@charset "UTF-8";
|
||||
|
||||
main>section[data-section="catalog"] {
|
||||
width: var(--width);
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
gap: var(--gap, 5px);
|
||||
}
|
||||
|
||||
main>section[data-section="catalog"][data-catalog-type="categories"]>a.category[type="button"] {
|
||||
height: 23px;
|
||||
padding: 8px 16px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
border-radius: 0.75rem;
|
||||
color: var(--tg-theme-button-text-color);
|
||||
background-color: var(--tg-theme-button-color);
|
||||
}
|
||||
|
||||
main>section[data-section="catalog"][data-catalog-type="categories"]:last-child {
|
||||
/* margin-bottom: unset; */
|
||||
}
|
||||
|
||||
main>section[data-section="catalog"][data-catalog-type="products"] {
|
||||
--column: calc((100% - var(--gap)) / 2);
|
||||
width: var(--width);
|
||||
display: grid;
|
||||
grid-gap: var(--gap);
|
||||
grid-template-columns: repeat(2, var(--column));
|
||||
grid-auto-flow: row dense;
|
||||
}
|
||||
|
||||
main>section[data-section="catalog"][data-catalog-type="products"]>div.column {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap);
|
||||
}
|
||||
|
||||
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-grow: 0;
|
||||
flex-direction: column;
|
||||
border-radius: 0.75rem;
|
||||
overflow: clip;
|
||||
backdrop-filter: brightness(0.7);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product:hover {
|
||||
/* flex-grow: 0.1; */
|
||||
/* background-color: var(--tg-theme-secondary-bg-color); */
|
||||
}
|
||||
|
||||
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product:hover>* {
|
||||
transition: 0s;
|
||||
}
|
||||
|
||||
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product:not(:hover)>* {
|
||||
transition: 0.2s ease-out;
|
||||
}
|
||||
|
||||
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product>a {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product>a>img:first-of-type {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product>a>img:first-of-type+* {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product>a>p.title {
|
||||
z-index: 50;
|
||||
margin: unset;
|
||||
padding: 4px 8px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: bold;
|
||||
overflow-wrap: anywhere;
|
||||
hyphens: auto;
|
||||
color: var(--tg-theme-text-color);
|
||||
background-color: var(--tg-theme-secondary-bg-color);
|
||||
}
|
||||
|
||||
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product>button:last-of-type {
|
||||
z-index: 100;
|
||||
height: 33px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
color: var(--tg-theme-button-text-color);
|
||||
background-color: var(--tg-theme-button-color);
|
||||
}
|
||||
|
||||
main>section[data-section="cart"] {
|
||||
--diameter: 4rem;
|
||||
z-index: 999;
|
||||
right: 5vw;
|
||||
bottom: 5vw;
|
||||
position: fixed;
|
||||
width: var(--diameter);
|
||||
height: var(--diameter);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
border-radius: 100%;
|
||||
background-color: var(--tg-theme-button-color);
|
||||
}
|
||||
|
||||
main>section[data-section="cart"]>i.icon.shopping.cart {
|
||||
top: -1px;
|
||||
color: var(--tg-theme-button-text-color);
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
@charset "UTF-8";
|
||||
|
||||
main>section[data-section="catalog"] {
|
||||
width: var(--width);
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
gap: var(--gap, 5px);
|
||||
}
|
||||
|
||||
main>section[data-section="catalog"][data-catalog-type="categories"]>a.category[type="button"] {
|
||||
height: 23px;
|
||||
padding: 8px 16px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
border-radius: 0.75rem;
|
||||
color: var(--tg-theme-button-text-color);
|
||||
background-color: var(--tg-theme-button-color);
|
||||
}
|
||||
|
||||
main>section[data-section="catalog"][data-catalog-type="categories"]:last-child {
|
||||
/* margin-bottom: unset; */
|
||||
}
|
||||
|
||||
main>section[data-section="catalog"][data-catalog-type="products"] {
|
||||
--column: calc((100% - var(--gap) * 2) / 3);
|
||||
width: var(--width);
|
||||
display: grid;
|
||||
grid-gap: var(--gap);
|
||||
grid-template-columns: repeat(3, var(--column));
|
||||
grid-auto-flow: row dense;
|
||||
}
|
||||
|
||||
main>section[data-section="catalog"][data-catalog-type="products"]>div.column {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap);
|
||||
}
|
||||
|
||||
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-grow: 0;
|
||||
flex-direction: column;
|
||||
border-radius: 0.75rem;
|
||||
overflow: clip;
|
||||
backdrop-filter: brightness(0.7);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product:hover {
|
||||
/* flex-grow: 0.1; */
|
||||
/* background-color: var(--tg-theme-secondary-bg-color); */
|
||||
}
|
||||
|
||||
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product:hover>* {
|
||||
transition: 0s;
|
||||
}
|
||||
|
||||
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product:not(:hover)>* {
|
||||
transition: 0.2s ease-out;
|
||||
}
|
||||
|
||||
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product>img:first-of-type {
|
||||
z-index: -50;
|
||||
width: 100%;
|
||||
max-height: 120px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product>img:first-of-type+* {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product>a.title {
|
||||
padding: 4px 8px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: bold;
|
||||
word-break: break-word;
|
||||
hyphens: auto;
|
||||
color: var(--tg-theme-text-color);
|
||||
background-color: var(--tg-theme-secondary-bg-color);
|
||||
}
|
||||
|
||||
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product>button:last-of-type {
|
||||
height: 33px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
color: var(--tg-theme-button-text-color);
|
||||
background-color: var(--tg-theme-button-color);
|
||||
}
|
||||
|
||||
main>section[data-section="cart"] {
|
||||
--diameter: 4rem;
|
||||
z-index: 999;
|
||||
right: 5vw;
|
||||
bottom: 5vw;
|
||||
position: fixed;
|
||||
width: var(--diameter);
|
||||
height: var(--diameter);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
border-radius: 100%;
|
||||
background-color: var(--tg-theme-button-color);
|
||||
}
|
||||
|
||||
main>section[data-section="cart"]>i.icon.shopping.cart {
|
||||
top: -1px;
|
||||
color: var(--tg-theme-button-text-color);
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
@charset "UTF-8";
|
||||
|
||||
@font-face {
|
||||
font-family: 'DejaVu';
|
||||
src: url("/themes/default/fonts/dejavu/DejaVuLGCSans-ExtraLight.ttf");
|
||||
font-weight: 200;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'DejaVu';
|
||||
src: url("/themes/default/fonts/dejavu/DejaVuLGCSans.ttf");
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'DejaVu';
|
||||
src: url("/themes/default/fonts/dejavu/DejaVuLGCSans-Oblique.ttf");
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'DejaVu';
|
||||
src: url("/themes/default/fonts/dejavu/DejaVuLGCSans-Bold.ttf");
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'DejaVu';
|
||||
src: url("/themes/default/fonts/dejavu/DejaVuLGCSans-BoldOblique.ttf");
|
||||
font-weight: 500;
|
||||
font-style: italic;
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
@charset "UTF-8";
|
||||
|
||||
@font-face {
|
||||
font-family: 'Kabrio';
|
||||
src: url("/themes/default/fonts/kabrio/Kabrio-Light.ttf");
|
||||
font-weight: 200;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Kabrio';
|
||||
src: url("/themes/default/fonts/kabrio/Kabrio-Regular.ttf");
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Kabrio';
|
||||
src: url("/themes/default/fonts/kabrio/Kabrio-Italic.ttf");
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Kabrio';
|
||||
src: url("/themes/default/fonts/kabrio/Kabrio-Heavy.ttf");
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Kabrio';
|
||||
src: url("/themes/default/fonts/kabrio/Kabrio-HeavyItalic.ttf");
|
||||
font-weight: 500;
|
||||
font-style: italic;
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
@charset "UTF-8";
|
||||
|
||||
@keyframes loading_spinner {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(359deg);
|
||||
}
|
||||
}
|
||||
|
||||
i.icon.loading.spinner,
|
||||
i.icon.loading.spinner::before {
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
i.icon.loading.spinner::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
border-radius: 100px;
|
||||
border: 3px solid transparent;
|
||||
border-top-color: currentColor;
|
||||
}
|
||||
|
||||
i.icon.loading.spinner.animated::before {
|
||||
animation: loading_spinner 1s cubic-bezier(0.6, 0, 0.4, 1) infinite;
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
@charset "UTF-8";
|
||||
|
||||
i.icon.search {
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
display: block;
|
||||
transform: scale(1);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid;
|
||||
border-radius: 100%;
|
||||
margin-left: -4px;
|
||||
margin-top: -4px;
|
||||
}
|
||||
|
||||
i.icon.search::after {
|
||||
content: "";
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
border-radius: 3px;
|
||||
width: 2px;
|
||||
height: 8px;
|
||||
background: currentColor;
|
||||
transform: rotate(-45deg);
|
||||
top: 10px;
|
||||
left: 12px;
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
@charset "UTF-8";
|
||||
|
||||
i.icon.shopping.cart {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
transform: scale(var(--ggs, 1));
|
||||
width: 20px;
|
||||
height: 21px;
|
||||
background:
|
||||
linear-gradient(to left, currentColor 12px, transparent 0) no-repeat -1px 6px/18px 2px,
|
||||
linear-gradient(to left, currentColor 12px, transparent 0) no-repeat 6px 14px/11px 2px,
|
||||
linear-gradient(to left, currentColor 12px, transparent 0) no-repeat 0 2px/4px 2px,
|
||||
radial-gradient(circle, currentColor 60%, transparent 40%) no-repeat 12px 17px/4px 4px,
|
||||
radial-gradient(circle, currentColor 60%, transparent 40%) no-repeat 6px 17px/4px 4px;
|
||||
}
|
||||
|
||||
i.icon.shopping.cart::after,
|
||||
i.icon.shopping.cart::before {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
width: 2px;
|
||||
height: 14px;
|
||||
background: currentColor;
|
||||
top: 2px;
|
||||
left: 4px;
|
||||
transform: skew(12deg);
|
||||
}
|
||||
|
||||
i.icon.shopping.cart::after {
|
||||
height: 10px;
|
||||
top: 6px;
|
||||
left: 16px;
|
||||
transform: skew(-12deg);
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
@charset "UTF-8";
|
||||
|
||||
@keyframes initialization {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
*:not(#loading) {
|
||||
animation: 0.2s ease-in 2s 1 normal forwards running initialization;
|
||||
/* animation: 0.2s ease-in 1s forwards initialization; */
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
@charset "UTF-8";
|
||||
|
||||
section#loading {
|
||||
z-index: 9999;
|
||||
position: fixed;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
backdrop-filter: brightness(50%) contrast(120%) grayscale(80%) blur(3px);
|
||||
cursor: progress;
|
||||
}
|
||||
|
||||
section#loading[disabled] {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: 0.3s ease-out;
|
||||
}
|
|
@ -0,0 +1,152 @@
|
|||
@charset "UTF-8";
|
||||
|
||||
:root {
|
||||
--text-light: #fafaff;
|
||||
}
|
||||
|
||||
* {
|
||||
text-decoration: none;
|
||||
outline: none;
|
||||
border: none;
|
||||
font-family: "DejaVu";
|
||||
color: var(--tg-theme-text-color);
|
||||
transition: 0.1s ease-out;
|
||||
}
|
||||
|
||||
a {
|
||||
cursor: pointer;
|
||||
color: var(--tg-theme-link-color);
|
||||
}
|
||||
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-x: clip;
|
||||
background-color: var(--tg-theme-bg-color);
|
||||
}
|
||||
|
||||
|
||||
aside {}
|
||||
|
||||
header {}
|
||||
|
||||
main {
|
||||
--offset-x: 2%;
|
||||
padding: 0 var(--offset-x);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 26px;
|
||||
transition: 0s;
|
||||
}
|
||||
|
||||
main>*[data-section] {
|
||||
--gap: 16px;
|
||||
--width: calc(100% - var(--gap) * 2);
|
||||
width: var(--width);
|
||||
}
|
||||
|
||||
main>section[data-section]>p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
main>search {
|
||||
--gap: 16px;
|
||||
--border-width: 1px;
|
||||
width: var(--width);
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
border-radius: 1.375rem;
|
||||
backdrop-filter: contrast(0.8);
|
||||
border: 2px solid transparent;
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
search:has(input:is(:focus, :active)) {
|
||||
border-color: var(--tg-theme-accent-text-color);
|
||||
transition: unset;
|
||||
}
|
||||
|
||||
search>label {
|
||||
margin-inline-start: 0.75rem;
|
||||
width: 1.5rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
search>label>i.icon {
|
||||
color: var(--tg-theme-subtitle-text-color);
|
||||
}
|
||||
|
||||
search:has(input:is(:focus, :active))>label>i.icon {
|
||||
color: var(--tg-theme-accent-text-color);
|
||||
transition: unset;
|
||||
}
|
||||
|
||||
search>input {
|
||||
width: 100%;
|
||||
max-width: calc(100% - 3.25rem);
|
||||
height: 2.5rem;
|
||||
touch-action: manipulation;
|
||||
padding: calc(.4375rem - var(--border-width)) calc(.625rem - var(--border-width)) calc(.5rem - var(--border-width)) calc(.75rem - var(--border-width));
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
search>input:disabled {
|
||||
cursor: progress;
|
||||
color: var(--tg-theme-subtitle-text-color);
|
||||
}
|
||||
|
||||
search:has(input:disabled) {
|
||||
backdrop-filter: contrast(0.5);
|
||||
}
|
||||
|
||||
:is(button, a[type="button"]) {
|
||||
padding: 8px 16px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
color: var(--tg-theme-button-text-color);
|
||||
background-color: var(--tg-theme-button-color);
|
||||
}
|
||||
|
||||
button {
|
||||
height: 33px;
|
||||
}
|
||||
|
||||
a[type="button"] {
|
||||
height: 23px;
|
||||
}
|
||||
|
||||
:is(button, a[type="button"]):is(:hover) {
|
||||
filter: brightness(120%);
|
||||
}
|
||||
|
||||
:is(button, a[type="button"]):active {
|
||||
filter: brightness(80%);
|
||||
transition: 0s;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
margin: 28px 0 0;
|
||||
}
|
||||
|
||||
footer {}
|
||||
|
||||
.unselectable {
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
@charset "UTF-8";
|
||||
|
||||
main>section[data-section="suspension"]>p.remain {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
main>section[data-section="suspension"]>p.remain>span.time {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
main>section[data-section="suspension"]>p.description {
|
||||
color: var(--tg-theme-hint-color);
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
@charset "UTF-8";
|
||||
|
||||
section#window {
|
||||
z-index: 1500;
|
||||
position: absolute;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
backdrop-filter: brightness(50%) contrast(120%) grayscale(60%) blur(1.2px);
|
||||
}
|
||||
|
||||
section#window>div.card {
|
||||
width: 85vw;
|
||||
max-height: 90vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 0.75rem;
|
||||
overflow: clip;
|
||||
background-color: var(--tg-theme-bg-color);
|
||||
}
|
||||
|
||||
section#window>div.card>h3 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
height: 23px;
|
||||
padding: 1rem 1.5rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
white-space: nowrap;
|
||||
background-color: var(--tg-theme-header-bg-color);
|
||||
}
|
||||
|
||||
section#window>div.card>h3>span.title {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
section#window>div.card>h3>small.brand {
|
||||
margin-left: auto;
|
||||
font-size: 0.8rem;
|
||||
font-weight: normal;
|
||||
color: var(--tg-theme-section-header-text-color);
|
||||
}
|
||||
|
||||
section#window>div.card>div.images {
|
||||
height: 10rem;
|
||||
display: flex;
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
section#window>div.card>div.images>img {
|
||||
margin-right: 0.5rem;
|
||||
width: 10rem;
|
||||
max-width: 10rem;
|
||||
height: 100%;
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
object-fit: cover;
|
||||
border-radius: 0.5rem;
|
||||
transition: 0s;
|
||||
cursor: zoom-in;
|
||||
}
|
||||
|
||||
section#window>div.card>div.images>img:last-child {
|
||||
margin-right: unset;
|
||||
}
|
||||
|
||||
section#window>div.card>div.images>img.extend {
|
||||
z-index: 9999999;
|
||||
left: 0;
|
||||
top: 0;
|
||||
margin: unset !important;
|
||||
position: absolute;
|
||||
width: 100vw;
|
||||
max-width: unset;
|
||||
height: 100vh;
|
||||
object-fit: contain;
|
||||
border-radius: unset;
|
||||
transition: 0s;
|
||||
cursor: zoom-out;
|
||||
}
|
||||
|
||||
section#window>div.card>p {
|
||||
margin-bottom: unset;
|
||||
min-height: 1rem;
|
||||
padding: 0 1rem;
|
||||
overflow-y: scroll;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
section#window>div.card>p:last-of-type {
|
||||
margin-bottom: revert;
|
||||
}
|
||||
|
||||
section#window>div.card>div.footer {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
align-items: baseline;
|
||||
gap: 0 0.8rem;
|
||||
background-color: var(--tg-theme-header-bg-color);
|
||||
}
|
||||
|
||||
section#window>div.card>div.footer>small.dimensions {
|
||||
margin-left: 1.5rem;
|
||||
color: var(--tg-theme-section-header-text-color);
|
||||
}
|
||||
|
||||
section#window>div.card>div.footer>small.weight {
|
||||
color: var(--tg-theme-section-header-text-color);
|
||||
}
|
||||
|
||||
section#window>div.card>div.footer>p.cost {
|
||||
margin-left: auto;
|
||||
margin-right: 1.5rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
section#window>div.card>div.footer>button.buy {
|
||||
width: 100%;
|
||||
height: 3.5rem;
|
||||
}
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue