sessions + websockets + telegram program data + bug fixes + filters

This commit is contained in:
Arsen Mirzaev Tatyano-Muradovich 2024-10-16 22:49:38 +03:00
parent 32cc78da1c
commit 5509b97148
33 changed files with 1678 additions and 219 deletions

View File

@ -27,7 +27,8 @@
"twig/twig": "^3.10",
"twig/extra-bundle": "^3.7",
"twig/intl-extra": "^3.10",
"avadim/fast-excel-reader": "^2.19"
"avadim/fast-excel-reader": "^2.19",
"openswoole/core": "22.1.5"
},
"autoload": {
"psr-4": {

186
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "e580e133abf1c4a60898f255e006a3e9",
"content-hash": "2a3687f9c81d7a26a57b654136653c18",
"packages": [
{
"name": "avadim/fast-excel-helper",
@ -918,6 +918,77 @@
],
"time": "2023-11-13T09:31:12+00:00"
},
{
"name": "openswoole/core",
"version": "22.1.5",
"source": {
"type": "git",
"url": "https://github.com/openswoole/core.git",
"reference": "06dae68fdac73341ccf565ecef388434bd893141"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/openswoole/core/zipball/06dae68fdac73341ccf565ecef388434bd893141",
"reference": "06dae68fdac73341ccf565ecef388434bd893141",
"shasum": ""
},
"require": {
"ext-openswoole": ">=22.0",
"php": ">=7.4",
"psr/http-message": "^1.0 || ^2.0",
"psr/http-server-middleware": "^1.0.0"
},
"require-dev": {
"ext-curl": "*",
"ext-sockets": "*",
"friendsofphp/php-cs-fixer": "^3.6",
"openswoole/ide-helper": "^22.0",
"php-http/psr7-integration-tests": "^1.1",
"phpunit/phpunit": "^9.5"
},
"suggest": {
"ext-mysqli": "*",
"ext-pdo": "*",
"ext-redis": "Required to use redis database, and the required version is greater than or equal to 3.1.3"
},
"type": "library",
"autoload": {
"files": [
"src/Coroutine/functions.php"
],
"psr-4": {
"OpenSwoole\\Core\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "OpenSwoole Group",
"email": "hello@openswoole.com"
}
],
"description": "Openswoole core library",
"homepage": "https://openswoole.com",
"keywords": [
"http",
"http2",
"mqtt",
"openswoole",
"php",
"tcp",
"websocket"
],
"support": {
"docs": "https://openswoole.com/docs",
"issues": "https://github.com/openswoole/openswoole/issues",
"pull-request": "https://github.com/openswoole/openswoole/pulls",
"source": "https://github.com/openswoole/openswoole"
},
"time": "2023-12-10T19:02:13+00:00"
},
{
"name": "opis/closure",
"version": "3.6.3",
@ -1596,6 +1667,119 @@
},
"time": "2023-04-04T09:50:52+00:00"
},
{
"name": "psr/http-server-handler",
"version": "1.0.2",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-server-handler.git",
"reference": "84c4fb66179be4caaf8e97bd239203245302e7d4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/84c4fb66179be4caaf8e97bd239203245302e7d4",
"reference": "84c4fb66179be4caaf8e97bd239203245302e7d4",
"shasum": ""
},
"require": {
"php": ">=7.0",
"psr/http-message": "^1.0 || ^2.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Http\\Server\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for HTTP server-side request handler",
"keywords": [
"handler",
"http",
"http-interop",
"psr",
"psr-15",
"psr-7",
"request",
"response",
"server"
],
"support": {
"source": "https://github.com/php-fig/http-server-handler/tree/1.0.2"
},
"time": "2023-04-10T20:06:20+00:00"
},
{
"name": "psr/http-server-middleware",
"version": "1.0.2",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-server-middleware.git",
"reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-server-middleware/zipball/c1481f747daaa6a0782775cd6a8c26a1bf4a3829",
"reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829",
"shasum": ""
},
"require": {
"php": ">=7.0",
"psr/http-message": "^1.0 || ^2.0",
"psr/http-server-handler": "^1.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Http\\Server\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for HTTP server-side middleware",
"keywords": [
"http",
"http-interop",
"middleware",
"psr",
"psr-15",
"psr-7",
"request",
"response"
],
"support": {
"issues": "https://github.com/php-fig/http-server-middleware/issues",
"source": "https://github.com/php-fig/http-server-middleware/tree/1.0.2"
},
"time": "2023-04-11T06:14:47+00:00"
},
{
"name": "psr/log",
"version": "1.1.4",

View File

@ -6,7 +6,6 @@ namespace mirzaev\arming_bot\controllers;
// Files of the project
use mirzaev\arming_bot\controllers\core,
mirzaev\arming_bot\models\catalog as model,
mirzaev\arming_bot\models\entry,
mirzaev\arming_bot\models\category,
mirzaev\arming_bot\models\product;
@ -39,6 +38,9 @@ final class catalog extends core
preg_match('/[\d]+/', $parameters['identifier'] ?? '', $matches);
$identifier = $matches[0] ?? null;
// Initializint the buffer of respnse
$html = [];
if (!empty($parameters['identifier'])) {
// Передана категория (идентификатор)
@ -70,21 +72,6 @@ final class catalog extends core
// Запись товаров из буфера в глобальную переменную шаблонизатора
$this->view->products = $product;
}
} else {
// Не передана категория
// Поиск категорий: "categories" (самый верхний уровень)
$this->view->categories = entry::ascendants(descendant: new category, 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']
); */
}
// Generating filters
$this->view->filters = [
@ -94,6 +81,19 @@ final class catalog extends core
)
];
// Generating HTML and writing to the buffer of response
$html = [
'filters' => count($this->view->products) > 0 ? $this->view->render('catalog/elements/filters.html') : null,
'products' => $this->view->render('catalog/elements/products/2columns.html')
];
}
} else {
// Не передана категория
// Поиск категорий: "categories" (самый верхний уровень)
$this->view->categories = entry::ascendants(descendant: new category, errors: $this->errors['catalog']);
}
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
// GET request
@ -114,10 +114,8 @@ final class catalog extends core
echo json_encode(
[
'title' => $title ?? '',
'html' => [
'html' => $html + [
'categories' => $this->view->render('catalog/elements/categories.html'),
'products' => $this->view->render('catalog/elements/products/2columns.html'),
'filters' => $this->view->render('catalog/elements/filters.html')
],
'errors' => $this->errors
]

View File

@ -117,8 +117,8 @@ class core extends controller
$this->settings = settings::active();
// Initializing of language
if ($this->account?->language) $this->language = $this->account->language ?? language::en;
else if ($this->settings?->language) $this->language = $this->settings->language ?? language::en;
if ($this->account?->language) $this->language = $this->account->language ?? language::en->name;
else if ($this->settings?->language) $this->language = $this->settings->language ?? language::en->name;
// Initializing of preprocessor of views
$this->view = new templater(

View File

@ -6,8 +6,12 @@ namespace mirzaev\arming_bot\controllers;
// Files of the project
use mirzaev\arming_bot\controllers\core,
mirzaev\arming_bot\models\session as model,
mirzaev\arming_bot\models\account;
// Framework for ArangoDB
use mirzaev\arangodb\document;
/**
* Controller of session
*
@ -34,11 +38,20 @@ final class session extends core
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// POST request
// Declaring variables in the correct scope
$identifier = $domain = $language = null;
if ($connected = isset($this->account)) {
// Found the account
// Initializing identifier of the account
$identifier = $this->account->identifier;
// Initializing language of the account
$language = $this->account->language;
// Initializing domain of the account
$domain = $this->account->domain;
} else {
// Not found the account
@ -46,7 +59,7 @@ final class session extends core
$buffer = $parameters;
unset($buffer['authentication'], $buffer['hash']);
unset($buffer['hash']);
ksort($buffer);
$prepared = [];
@ -71,10 +84,10 @@ final class session extends core
$data = json_decode($parameters['user']);
// Initializing of the account
$account = account::initialization(
$account = account::initialize(
$data->id,
[
'id' => $data->id,
'identifier' => $data->id,
'name' => [
'first' => $data->first_name,
'last' => $data->last_name
@ -92,8 +105,14 @@ final class session extends core
// Connecting the account to the session
$connected = $this->session->connect($account, $this->errors['session']);
// Initializing identifier of the account
$identifier = $account->identifier;
// Initializing language of the account
$language = $account->language;
// Initializing domain of the account
$domain = $account->domain;
}
}
}
@ -112,7 +131,9 @@ final class session extends core
echo json_encode(
[
'connected' => (bool) $connected,
'language' => $language ?? null,
'identifier' => $identifier ?? null,
'domain' => $domain ?? null,
'language' => $language?->name ?? null,
'errors' => $this->errors
]
);
@ -131,4 +152,37 @@ final class session extends core
// Exit (fail)
return null;
}
/**
* Connect session to the telegram account
*
* @param array $parameters Parameters of the request (POST + GET)
*/
public function write(array $parameters = []): ?string
{
if (!empty($parameters) && $this->session instanceof model) {
// Found data of the program and active session
foreach ($parameters as $name => $value) {
// Iterate over parameters
// Validation of the parameter
if (mb_strlen($value) > 4096) continue;
// Write data of the program to the implement object of session document from ArangoDB
$this->session->{$name} = json_decode($value, true, 10);
}
// Write from implement object to session document from ArangoDB
document::update($this->session->__document(), $this->errors['session']);
// Exit (success)
return null;
}
// Exit (fail)
return null;
}
}

View File

@ -104,12 +104,6 @@ final class account extends core implements arangodb_document_interface
],
'domain' => $registration->getUsername(),
'robot' => $registration->isBot(),
'banned' => false,
'tester' => false,
'developer' => false,
'access' => [
'settings' => false
],
'menus' => [
'attachments' => $registration->getAddedToAttachmentMenu()
],
@ -124,6 +118,12 @@ final class account extends core implements arangodb_document_interface
'inline' => $registration->getSupportsInlineQueries()
]
]) + [
'banned' => false,
'tester' => false,
'developer' => false,
'access' => [
'settings' => false
],
'version' => ROBOT_VERSION,
'active' => true
],

View File

@ -213,13 +213,13 @@ final class product extends core
/**
* Collect parameter from all products
*
* @param string $name Name of the parameter (AQL path)
* @param string $return Return (AQL path)
* @param array &$errors Registry of errors
*
* @return array Array with found unique parameter values from all products (can be empty)
*/
public static function collect(
string $name = 'd._key',
string $return = 'd._key',
array &$errors = []
): array {
try {
@ -227,13 +227,15 @@ final class product extends core
// Initialized the collection
if ($result = collection::execute(
sprintf(
<<<'AQL'
FOR d IN @@collection
RETURN DISTINCT @parameter
RETURN DISTINCT %s
AQL,
empty($return) ? 'd._key' : $return
),
[
'@collection' => static::COLLECTION,
'parameter' => $name
],
errors: $errors
)) {

View File

@ -10,7 +10,8 @@ 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;
mirzaev\arming_bot\models\interfaces\document as arangodb_document_interface,
mirzaev\arming_bot\models\enumerations\language;
// Framework for ArangoDB
use mirzaev\arangodb\collection,
@ -158,11 +159,14 @@ final class session extends core implements arangodb_document_interface
// Found active settings
// Initializing the object
$account = new static;
$account = new account;
if (method_exists($account, '__document')) {
// Object can implement a document from ArangoDB
// Abstractioning of parameters
$result->language = language::{$result->language} ?? 'en';
// Writing the instance of account document from ArangoDB to the implement object
$account->__document($result);

View File

@ -24,7 +24,7 @@ define('SETTINGS', realpath('..' . DIRECTORY_SEPARATOR . 'settings'));
define('INDEX', __DIR__);
define('THEME', 'default');
// Инициализация библиотек
// Initialize dependencies
require __DIR__ . DIRECTORY_SEPARATOR
. '..' . DIRECTORY_SEPARATOR
. '..' . DIRECTORY_SEPARATOR
@ -33,13 +33,14 @@ require __DIR__ . DIRECTORY_SEPARATOR
. 'vendor' . DIRECTORY_SEPARATOR
. 'autoload.php';
// Инициализация маршрутизатора
// Initialize the router
$router = new router;
// Initializing of routes
// Initialize routes
$router
->write('/', 'catalog', 'index', 'GET')
->write('/search', 'catalog', 'search', 'POST')
->write('/session/write', 'session', 'write', 'POST')
->write('/session/connect/telegram', 'session', 'telegram', 'POST')
->write('/category/$identifier', 'catalog', 'index', 'POST')
->write('/category', 'catalog', 'index', 'POST')
@ -65,8 +66,8 @@ $router
->sort();
var_dump($router->routes); */
// Инициализация ядра
// Initialize the core
$core = new core(namespace: __NAMESPACE__, router: $router, controller: new controller(false), model: new model(false));
// Обработка запроса
// Handle the request
echo $core->start();

View File

@ -0,0 +1,117 @@
"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);
initialization();
}, 5000);
function initialization() {
if (typeof core.account === "undefined") {
// Not initialized
// Write to the core
core.account = class account {
// Wrap of indicator of the account
static wrap = document.getElementById("account");
// Indicator of the account
static indicator = this.wrap.getElementsByTagName("i")[0];
// Description of the account
static description = this.wrap.getElementsByTagName("small")[0];
// Statuc of the account
static connected = false;
// Duration of the disconnected status
static timeout = 0;
// Instance of the time counter in disconnected status
static counter;
// Socket address (xn--e1ajlli это сокет)
static socket = "wss://arming.dev.mirzaev.sexy:9502";
// Instance of account to the socket
static session;
// Iterval for reconnect
static interval;
// Attempts to connect (when core.account.readyState === 0)
static attempts = 0;
// Interval for block
static block;
/**
* Authentication
*
* @return {void}
*/
static authentication() {
core.status_loading.removeAttribute("disabled");
const timer_for_response = setTimeout(() => {
core.status_loading.setAttribute("disabled", true);
}, 3000);
if (core.telegram.api.initData.length > 0) {
core.request(
"/session/connect/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.status_loading.setAttribute("disabled", true);
clearTimeout(timer_for_response);
const a =
core.status_account.getElementsByTagName("a")[0];
a.setAttribute("onclick", "core.account.profile()");
a.innerText = json.domain.length > 0
? "@" + json.domain
: "ERROR";
}
if (
json.language !== null &&
typeof json.language === "string" &&
json.langiage.length === 2
) {
core.language = json.language;
}
}
});
}
}
};
}
}
})
)
);

View File

@ -2,13 +2,15 @@
// Import dependencies
import("/js/core.js").then(() =>
import("/js/damper.js").then(() => {
import("/js/telegram.js").then(() => {
import("/js/damper.js").then(() =>
import("/js/telegram.js").then(() =>
import("/js/account.js").then(() => {
const dependencies = setInterval(() => {
if (
typeof core === "function" &&
typeof core.damper === "function" &&
typeof core.telegram === "function"
typeof core.telegram === "function" &&
typeof core.account === "function"
) {
clearInterval(dependencies);
clearTimeout(timeout);
@ -21,45 +23,13 @@ import("/js/core.js").then(() =>
}, 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);
core.session.telegram();
if (core.telegram.api.initData.length > 0) {
core.account.authentication();
}
if (
json.language !== null &&
typeof json.language === "string" &&
json.langiage.length === 2
) {
core.language = json.language;
core.telegram.api.ready();
}
}
});
}
});
})
)
)
);

View File

@ -38,7 +38,7 @@ import("/js/core.js").then(() =>
/**
* Select a category (interface)
*
* @param {HTMLElement} button Button of category <a>
* @param {HTMLElement} button Button of a category <a>
* @param {bool} clean Clear search bar?
* @param {bool} force Ignore the damper?
*
@ -76,13 +76,13 @@ import("/js/core.js").then(() =>
*
* @return {Promise} Request to the server
*/
static __category(category, clean = true) {
if (typeof category === "string") {
static __category(identifier, clean = true) {
if (typeof identifier === "string") {
// Received required parameters
return core.request(
"/category" +
("/" + category).replace(/^\/*/, "/").trim().replace(
("/" + identifier).replace(/^\/*/, "/").trim().replace(
/\/*$/,
"",
),
@ -146,21 +146,70 @@ import("/js/core.js").then(() =>
element,
search.nextSibling,
);
} else {
core.main.append(element);
}
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.filters === "string" &&
json.html.filters.length > 0
) {
// Received filters (reinitialization of the filters)
const filters = core.main.querySelector(
'section[data-section="filters"]',
);
if (filters instanceof HTMLElement) {
// Found list of filters
filters.outerHTML = json.html.filters;
} else {
// Not found list of categories
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,
);
} else {
core.main.append(element);
}
element.outerHTML = json.html.filters;
}
} else {
// Not received categories (deinitialization of the categories)
const filters = core.main.querySelector(
'section[data-section="filters"',
);
if (filters instanceof HTMLElement) {
filters.remove();
}
}
if (
typeof json.html.products === "string" &&
json.html.products.length > 0
@ -179,32 +228,8 @@ import("/js/core.js").then(() =>
// 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,
);
core.main.append(element);
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)
@ -234,14 +259,14 @@ import("/js/core.js").then(() =>
static search(event, element, force = false) {
element.classList.remove("error");
if (element.innerText.length === 1) {
if (element.value.length === 1) {
return;
} else if (event.keyCode === 13) {
// Button: "enter"
element.setAttribute("disabled", true);
this.__search(element);
this._search(element, force);
} else {
// Button: any
@ -375,12 +400,17 @@ import("/js/core.js").then(() =>
/**
* Open product card (interface)
*
* @param {string} identifier Identifier of a product
* @param {HTMLElement} button Button of a product <a>
* @param {bool} force Ignore the damper?
*
* @return {void}
*/
static product(identifier, force = false) {
static product(button, force = false) {
// Initializing identifier of the category
const identifier = button.getAttribute(
"data-product-identifier",
);
this._product(identifier, force);
}
@ -406,7 +436,7 @@ import("/js/core.js").then(() =>
* @return {Promise} Request to the server
*/
static __product(identifier) {
if (typeof identifier === "number") {
if (typeof identifier === "string") {
//
return core.request(`/product/${identifier}`)

View File

@ -0,0 +1,315 @@
"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);
initialization();
}, 5000);
function initialization() {
if (typeof core.connection === "undefined") {
// Not initialized
// Write to the core
core.connection = class connection {
// Wrap of indicator of the connection
static wrap = document.getElementById("connection");
// Indicator of the connection
static indicator = this.wrap.getElementsByTagName("i")[0];
// Description of the connection
static description = this.wrap.getElementsByTagName("small")[0];
// Statuc of the connection
static connected = false;
// Duration of the disconnected status
static timeout = 0;
// Instance of the time counter in disconnected status
static counter;
// Socket address (xn--e1ajlli это сокет)
static socket = "wss://arming.dev.mirzaev.sexy:9502";
// Instance of connection to the socket
static session;
// Iterval for reconnect
static interval;
// Attempts to connect (when core.connection.readyState === 0)
static attempts = 0;
// Interval for block
static block;
/**
* Initialize status of the connection to socket
*
* @param {bool} connected Connected?
*
* @return {void}
*/
static status(connected = false) {
if (this.indicator instanceof HTMLElement) {
// Initialized the indicator
if (this.connected = connected) {
// Connected
this.wrap.setAttribute("title", "Connected");
this.indicator.classList.remove("disconnected");
this.indicator.classList.add("connected");
clearInterval(this.counter);
this.description.innerText = "";
this.counter = undefined;
this.timeout = 0;
} else {
// Disconnected
this.wrap.setAttribute("title", "Disconnected");
this.indicator.classList.remove("connected");
this.indicator.classList.add("disconnected");
if (typeof this.counter === "undefined") {
this.counter = setInterval(() => {
this.timeout += 0.01;
this.description.innerText = this.timeout.toFixed(2);
}, 10);
}
}
}
}
/**
* Connect to the socket
*
* @param {bool|number} interval Connection check interval (ms)
* @param {function} preprocessing Will be executed every cycle
* @param {function} onmessage New message
* @param {function} onopen Connection opened
* @param {function} onclose Connection closed
* @param {function} onerror An error has occurred
*
* @return {Promise}
*/
static connect(
interval = false,
preprocessing,
onmessage,
onopen,
onclose,
onerror,
) {
return new Promise((resolve, reject) => {
try {
if (typeof interval === "number" && interval > 0) {
// Connect with automatic reconnect
if (typeof this.interval === "undefined") {
this.interval = setInterval(() => {
preprocessing();
if (
!(this.session instanceof WebSocket) ||
(this.session.readyState === 3 ||
this.session.readyState === 4) ||
(this.session.readyState === 0 &&
++this.attempts > 10)
) {
this.attempts = 0;
if (this.session instanceof WebSocket) {
this.session.close();
}
this.session = new WebSocket(this.socket);
this.session.addEventListener("message", (e) => {
try {
const json = JSON.parse(e.data);
if (json.type === "registration") {
// Подключение сокета к сессии
fetch("/socket/registration", {
method: "POST",
headers: {
"Content-Type":
"application/x-www-form-urlencoded",
},
body: `key=${json.key}`,
});
}
} catch (_e) {}
});
this.session.addEventListener("message", onmessage);
this.session.addEventListener("open", onopen);
this.session.addEventListener("close", onclose);
this.session.addEventListener("error", onerror);
resolve(this.session);
} else resolve(this.session);
}, interval);
}
} else {
// Connect without reconnecting
if (
!(this.session instanceof WebSocket) ||
(this.session.readyState === 3 ||
this.session.readyState === 4)
) {
if (this.session instanceof WebSocket) {
this.session.close();
}
this.session = new WebSocket(this.socket);
this.session.addEventListener("message", onmessage);
this.session.addEventListener("open", onopen);
this.session.addEventListener("close", onclose);
this.session.addEventListener("error", onerror);
resolve(this.session);
} else resolve(this.session);
}
} catch (_e) {}
});
}
/**
* Core is connected to the socket?
*
* @return {bool}
*/
static connected() {
return this.session instanceof WebSocket &&
this.session.readyState === 1;
}
};
}
core.connection.connect(
3000,
() =>
core.connection.status(
core.connection.session instanceof WebSocket &&
core.connection.session.readyState === 1,
),
(e) => {
try {
const json = JSON.parse(e.data);
if (json.target === "task") {
// Заявка
// Инициализация строки
const row = document.getElementById(json._key);
if (row instanceof HTMLElement) {
// Инициализирована строка
if (json.type === "blocked") {
// Заблокирована заявка
// Запись статуса: "заблокирована"
row.setAttribute("data-blocked", json.account._key);
row.setAttribute(
"title",
"Редактирует: " + json.account.name,
);
// Удалить блокировку (60000 === 1 минута) (в базе данных стоит expires 1 минута тоже)
setTimeout(() => {
// Удаление статуса: "заблокирована"
row.removeAttribute("data-blocked");
row.removeAttribute("title");
// Обновление строки
tasks.row(row);
}, 60000);
} else if (json.type === "unblocked") {
// Разблокирована заявка
// Удаление статуса: "заблокирована"
row.removeAttribute("data-blocked");
row.removeAttribute("title");
// Обновление строки
tasks.row(row);
} else if (json.type === "updated") {
// Обновлена заявка
// Обновление строки
tasks.row(row);
} else if (json.type === "deleted") {
// Удалена заявка
// Удаление строки
row.remove();
}
}
}
} catch (_e) {}
// Инициализация идентифиатора
//const id = row.getAttribute("id");
// Инициализация количества непрочитанных сообщений
//const messages = row.lastElementChild.innerText;
// Инициализация статуса активной строки
//const selected = row.getAttribute("data-selected");
// Реинициализация строки
//row.outerHTML = data.rows;
// Реинициализация перезаписанной строки
//row = document.getElementById(id);
// Копирование статуса активной строки
//if (
// typeof selected === "string" &&
// selected === "true" &&
// document.body.contains(document.getElementById("popup"))
//) {
// row.setAttribute("data-selected", "true");
//}
},
(e) => {
//connection.status(
// core.connection instanceof WebSocket &&
// core.connection.readyState === 1,
//)
//console.log("Connected to WebSocket!");
},
(e) => {
//connection.status(
// core.connection instanceof WebSocket &&
// core.connection.readyState === 1,
//)
//console.log("Connection closed");
},
(e) => {
//console.log("Error happens");
},
);
}
})
);

View File

@ -8,8 +8,11 @@ const core = class core {
// Language
static language = "ru";
// Label for the "loding" element
static loading = document.getElementById("loading");
// Label for the "loading" element
static status_loading = document.getElementById("loading");
// Label for the "account" element
static status_account = document.getElementById("account");
// Label for the <header> element
static header = document.body.getElementsByTagName("header")[0];
@ -49,4 +52,185 @@ const core = class core {
return await fetch(encodeURI(address), { method, headers, body })
.then((response) => response[type]());
}
/**
* Сгенерировать окно выбора действия
*
* @param {string} title Верхний колонтинул
* @param {string} text Основное содержимое окна
* @param {string} left Содержимое левой кнопки
* @param {object} left_css Перечисление CSS-классов левой кнопки (массив)
* @param {function} left_click Действие после нажатия на левую кнопку
* @param {string} right Содержимое правой кнопки
* @param {object} right_css Перечисление CSS-классов правой кнопки (массив)
* @param {function} right_click Действие после нажатия на правую кнопку
*
* @return {void}
*/
/* static choose = core.damper(
(
title = "Выбор действия",
text = "",
left = "Да",
left_css = ["grass"],
left_click = () => {},
right = "Нет",
right_css = ["clay"],
right_click = () => {},
) => {
// Инициализация оболочки всплывающего окна
this.popup_body.wrap = document.createElement("div");
this.popup_body.wrap.setAttribute("id", "popup");
// Инициализация всплывающего окна
const popup = document.createElement("section");
popup.classList.add("list", "small");
// Инициализация заголовка всплывающего окна
const title_h3 = document.createElement("h3");
title_h3.classList.add("unselectable");
title_h3.innerText = title;
// Инициализация оболочки с основной информацией
const main = document.createElement("section");
main.classList.add("main");
// Инициализация колонки
const column = document.createElement("div");
column.classList.add("column");
// Инициализация текста
const text_p = document.createElement("p");
text_p.innerText = text;
// Инициализация строки
const row = document.createElement("div");
row.classList.add("row", "buttons");
// Инициализация левой кнопки
const left_button = document.createElement("button");
left_button.classList.add(...left_css);
left_button.innerText = left;
left_button.addEventListener("click", left_click);
// Инициализация правой кнопки
const right_button = document.createElement("button");
right_button.classList.add(...right_css);
right_button.innerText = right;
right_button.addEventListener("click", right_click);
// Инициализация окна с ошибками
this.popup_body.errors = document.createElement("section");
this.popup_body.errors.classList.add(
"errors",
"window",
"list",
"calculated",
"hidden",
);
this.popup_body.errors.setAttribute("data-errors", true);
// Инициализация элемента-тела (оболочки) окна с ошибками
const errors = document.createElement("section");
errors.classList.add("body");
// Инициализация элемента-списка ошибок
const dl = document.createElement("dl");
// Инициализация активного всплывающего окна
const old = document.getElementById("popup");
if (old instanceof HTMLElement) {
// Найдено активное окно
// Деинициализация быстрых действий по кнопкам
document.removeEventListener("keydown", this.buttons);
// Сброс блокировки
this.freeze = false;
// Удаление активного окна
old.remove();
}
// Запись в документ
popup.appendChild(title_h3);
column.appendChild(text_p);
row.appendChild(left_button);
row.appendChild(right_button);
column.appendChild(row);
main.appendChild(column);
popup.appendChild(main);
this.popup_body.wrap.appendChild(popup);
document.body.appendChild(this.popup_body.wrap);
errors.appendChild(dl);
this.popup_body.errors.appendChild(errors);
this.popup_body.wrap.appendChild(this.popup_body.errors);
// Инициализация ширины окна с ошибками
this.popup_body.errors.style.setProperty(
"--calculated-width",
popup.offsetWidth + "px",
);
// Инициализация переменных для окна с ошибками (12 - это значение gap из div#popup)
function top(errors) {
errors.style.setProperty("transition", "0s");
errors.style.setProperty(
"--top",
popup.offsetTop + popup.offsetHeight + 12 + "px",
);
setTimeout(() => errors.style.removeProperty("transition"), 100);
}
top(this.popup_body.errors);
const resize = new ResizeObserver(() => top(this.popup_body.errors));
resize.observe(this.popup_body.wrap);
// Инициализация функции закрытия всплывающего окна
const click = () => {
// Блокировка
if (this.freeze) return;
// Удаление всплывающего окна
this.popup_body.wrap.remove();
// Удаление статуса активной строки
row.removeAttribute("data-selected");
// Деинициализация быстрых действий по кнопкам
document.removeEventListener("keydown", this.buttons);
// Сброс блокировки
this.freeze = false;
};
// Инициализация функции добавления функции закрытия всплывающего окна
const enable = () =>
this.popup_body.wrap.addEventListener("click", click);
// Инициализация функции удаления функции закрытия всплывающего окна
const disable = () =>
this.popup_body.wrap.removeEventListener("click", click);
// Первичная активация функции удаления всплывающего окна
enable();
// Добавление функции удаления всплывающего окна по событиям
popup.addEventListener("mouseenter", disable);
popup.addEventListener("mouseleave", enable);
// Добавление функции удаления всплывающего окна по кнопкам
left_button.addEventListener("click", click);
right_button.addEventListener("click", click);
// Фокусировка
right_button.focus();
},
300,
); */
};

View File

@ -2,11 +2,13 @@
// Import dependencies
import("/js/core.js").then(() =>
import("/js/damper.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.damper === "function" &&
typeof core.telegram === "function"
) {
clearInterval(dependencies);
clearTimeout(timeout);
@ -25,21 +27,22 @@ import("/js/core.js").then(() =>
// Write to the core
core.session = class session {
/**
* Current position in hierarchy of the categories
*/
static categories = [];
/**
* Send data of Telegram program settings to the session
*
* @return {void}
*/
static connect() {
static telegram() {
//
const { initData, initDataUnsafe, ...data } = core.telegram.api;
core.request(
"/session/connect/telegram",
window.Telegram.WebApp.initData,
"/session/write",
"telegram=" + JSON.stringify(data),
);
}
};
}
}
})
)
);

View File

@ -36,7 +36,7 @@ define('CATALOG_IMPORT', STORAGE . DIRECTORY_SEPARATOR . 'import.xlsx');
// Ключ чат-робота Telegram
define('KEY', require(SETTINGS . DIRECTORY_SEPARATOR . 'key.php'));
// Инициализация библиотек
// Initialize dependencies
require __DIR__ . DIRECTORY_SEPARATOR
. '..' . DIRECTORY_SEPARATOR
. '..' . DIRECTORY_SEPARATOR

View File

@ -0,0 +1,384 @@
<?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,
mirzaev\arming_bot\models\socket;
// Framework for PHP
use mirzaev\minimal\core;
// Framework for ArangoDB
use mirzaev\arangodb\connection as arangodb,
mirzaev\arangodb\collection,
mirzaev\arangodb\document;
// Library for ArangoDB
use ArangoDBClient\Document as _document;
// Built-in libraries
use exception;
// Server of WebSocket
use OpenSwoole\WebSocket\{Server, Frame};
use OpenSwoole\Constant,
OpenSwoole\Http\Request,
OpenSwoole\Table;
// Initialize dependencies
require __DIR__ . DIRECTORY_SEPARATOR
. '..' . DIRECTORY_SEPARATOR
. '..' . DIRECTORY_SEPARATOR
. '..' . DIRECTORY_SEPARATOR
. '..' . DIRECTORY_SEPARATOR
. 'vendor' . DIRECTORY_SEPARATOR
. 'autoload.php';
define('INDEX', __DIR__);
// Initialize the core
$core = new core(namespace: __NAMESPACE__, controller: new controller(false), model: $model = new model(false));
// https://dev.to/robertobutti/websocket-with-php-4k2c
$server = new Server("armng.dev.mirzaev.sexy", 9502, Server::SIMPLE_MODE, Constant::SOCK_TCP | Constant::SSL);
$accounts = new Table(1024);
$accounts->column('id', Table::TYPE_INT, 4);
$accounts->column('type', Table::TYPE_STRING, 16);
$accounts->create();
$server->set([
// Process
'daemonize' => 1,
'user' => 'www-data',
'group' => 'www-data',
/* 'chroot' => '/data/server/', */
'open_cpu_affinity' => true,
/* 'cpu_affinity_ignore' => [0, 1], */
'pid_file' => '/var/run/php/ebala-socket.pid',
// Server
/* 'reactor_num' => 8,
'worker_num' => 2, */
'message_queue_key' => 'mq1',
'dispatch_mode' => 4,
'discard_timeout_request' => true,
/* 'dispatch_func' => 'my_dispatch_function', */
// Worker
/* 'max_request' => 0,
'max_request_grace' => $max_request / 2, */
// HTTP Server max execution time, since v4.8.0
'max_request_execution_time' => 5, // 30s
// Task worker
/* 'task_ipc_mode' => 2,
'task_max_request' => 100,
'task_tmpdir' => '/tmp',
'task_worker_num' => 8,
'task_enable_coroutine' => true,
'task_use_object' => true, */
// Logging
'log_level' => 1,
'log_file' => __DIR__ . '/../logs/openswoole.log',
'log_rotation' => Constant::LOG_ROTATION_DAILY,
'log_date_format' => '%Y-%m-%d %H:%M:%S',
'log_date_with_microseconds' => false,
/* 'request_slowlog_file' => false, */
// Enable trace logs
'trace_flags' => Constant::TRACE_ALL,
// TCP
'input_buffer_size' => 2097152,
'buffer_output_size' => 32 * 1024 * 1024, // byte in unit
'tcp_fastopen' => true,
'max_conn' => 10000,
'tcp_defer_accept' => false,
'open_tcp_keepalive' => true,
'tcp_keepidle' => 30, // Check if there is no data for 4s.
/* 'tcp_keepinterval' => 1, // Check if there is data every 1s */
'tcp_keepcount' => 10,
'open_tcp_nodelay' => true,
/* 'pipe_buffer_size' => 32 * 1024 * 1024, */
'socket_buffer_size' => 128 * 1024 * 1024,
// Kernel
'backlog' => 512,
'kernel_socket_send_buffer_size' => 65535,
'kernel_socket_recv_buffer_size' => 65535,
// TCP Parser
'open_eof_check' => true,
'open_eof_split' => true,
'package_eof' => '\r\n',
'open_length_check' => true,
'package_length_type' => 'N',
'package_body_offset' => 8,
'package_length_offset' => 8,
'package_max_length' => 2 * 1024 * 1024, // 2MB
/* 'package_length_func' => 'my_package_length_func', */
// Coroutine
'enable_coroutine' => true,
'max_coroutine' => 3000,
'send_yield' => true,
// tcp server
'heartbeat_idle_time' => 600,
'heartbeat_check_interval' => 30,
'enable_delay_receive' => false,
'enable_reuse_port' => false,
'enable_unsafe_event' => false,
// Protocol
'open_http_protocol' => true,
'open_http2_protocol' => true,
'open_websocket_protocol' => true,
'open_mqtt_protocol' => false,
// HTTP2
'http2_header_table_size' => 4095,
'http2_initial_window_size' => 65534,
'http2_max_concurrent_streams' => 1281,
'http2_max_frame_size' => 16383,
'http2_max_header_list_size' => 4095,
// SSL
'ssl_cert_file' => '/etc/letsencrypt/live/xn--80aksgi6f.xn--24-mlca2chbdebu6a.xn--p1ai/fullchain.pem',
'ssl_key_file' => '/etc/letsencrypt/live/xn--80aksgi6f.xn--24-mlca2chbdebu6a.xn--p1ai/privkey.pem',
'ssl_ciphers' => 'ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP',
'ssl_protocols' => Constant::SSL_TLSv1_3, // added from v4.5.4
'ssl_verify_peer' => false,
/* 'ssl_sni_certs' => [
"cs.php.net" => [
'ssl_cert_file' => __DIR__ . "/config/sni_server_cs_cert.pem",
'ssl_key_file' => __DIR__ . "/config/sni_server_cs_key.pem"
],
"uk.php.net" => [
'ssl_cert_file' => __DIR__ . "/config/sni_server_uk_cert.pem",
'ssl_key_file' => __DIR__ . "/config/sni_server_uk_key.pem"
],
"us.php.net" => [
'ssl_cert_file' => __DIR__ . "/config/sni_server_us_cert.pem",
'ssl_key_file' => __DIR__ . "/config/sni_server_us_key.pem",
],
], */
// Static Files
'document_root' => __DIR__,
'enable_static_handler' => true,
/* 'static_handler_locations' => ['/static', '/app/images'], */
'http_index_files' => ['index.html', 'index.txt'],
// Source File Reloading
'reload_async' => true,
'max_wait_time' => 30,
// HTTP Server
'http_parse_post' => true,
'http_parse_cookie' => true,
'upload_tmp_dir' => '/tmp',
// Compression
'http_compression' => true,
'http_compression_level' => 3, // 1 - 9
'compression_min_length' => 20,
// Websocket
'websocket_compression' => true,
'open_websocket_close_frame' => true,
'open_websocket_ping_frame' => true, // added from v4.5.4
'open_websocket_pong_frame' => true, // added from v4.5.4
// TCP User Timeout
'tcp_user_timeout' => 120,
// DNS Server
'dns_server' => '1.1.1.1',
/* 'dns_cache_refresh_time' => 60, */
/* 'enable_preemptive_scheduler' => 0, */
/* 'open_fastcgi_protocol' => 0, */
'open_redis_protocol' => 0,
'event_object' => false,
]);
/* $server->set([
'ssl_cert_file' => __DIR__
. DIRECTORY_SEPARATOR . '..'
. DIRECTORY_SEPARATOR . 'settings'
. DIRECTORY_SEPARATOR . 'certificates'
. DIRECTORY_SEPARATOR . '84.22.137.106.pem',
'ssl_key_file' => __DIR__
. DIRECTORY_SEPARATOR . '..'
. DIRECTORY_SEPARATOR . 'settings'
. DIRECTORY_SEPARATOR . 'certificates'
. DIRECTORY_SEPARATOR . '84.22.137.106-key.pem'
]); */
$server->on("Start", function (Server $server) use ($core, $model) {
// Очистка баз данных для сокетов
collection::truncate($model->arangodb->session, 'socket');
collection::truncate($model->arangodb->session, 'session_edge_socket');
// Запись в буфер вывода (журнал)
echo "Swoole WebSocket Server is started at " . $server->host . ":" . $server->port . "\n";
});
$server->on('Open', function (Server $server, Request $request) use ($accounts, $core, $model) {
// Инициализация идентификатора
$id = $request->fd;
// Запись в базу данных
$accounts->set((string) $id, [
'id' => $id
]);
// Запись в буфер вывода (журнал)
echo '[' . date('Y.m.d h:i:s', time()) . "][$id] Connected ({$accounts->count()})" . PHP_EOL;
// Регистрация
if (document::write($model->arangodb->session, 'socket', [
'id' => (int) $id,
'key' => $key = sodium_bin2hex(sodium_crypto_generichash((string) $id)),
'expires' => (int) strtotime('+1 hour')
])) {
// Создана инстанция сокета (регистрация)
// Отправка данных для подключения
$server->push($id, sprintf(
<<<JSON
{
"type": "registration",
"key": "%s"
}
JSON,
$key
));
}
});
$server->on('Message', function (Server $server, Frame $frame) use ($accounts, $core, $model) {
// Запись в буфер вывода (журнал)
echo '[' . date('Y.m.d h:i:s', time()) . "][$frame->fd] Message: $frame->data" . PHP_EOL;
if ($account = socket::account($frame->fd)) {
// Авторизован аккаунт
// Инициализация полученных данных
$message = json_decode($frame->data, false, 2);
// Инициализация имени
$name = trim((!empty($account->name['first']) ? mb_strtoupper(mb_substr($account->name['first'], 0, 1)) . '.' : '') . (!empty($account->name['last']) ? ' ' . mb_strtoupper(mb_substr($account->name['last'], 0, 1)) . '.' : '') . (!empty($account->name['second']) ? ' ' . $account->name['second'] : ''), ' ');
try {
if ($message->target === 'task') {
// Заявка
if ($message->type === 'block') {
// Блокировка редактирования
if ($account->status() && ($account->type === 'administrator' || $account->type === 'operator'))
if (task::block((int) $message->_key, (int) $account->getKey())) {
// Заблокирована заявка
// Отправка сообщения всем сокетам
foreach ($accounts as $key => $value)
if ((int) $key !== $frame->fd)
$server->push((int) $key, <<<JSON
{
"type": "blocked",
"target": "task",
"_key": $message->_key,
"account": {
"_key": {$account->getKey()},
"name": "$name"
}
}
JSON);
}
} else if ($message->type === 'unblock') {
// Разблокировка редактирования
if ($account->status() && ($account->type === 'administrator' || $account->type === 'operator'))
if (task::unblock((int) $message->_key, (int) $account->getKey())) {
// Заблокирована заявка
// Отправка сообщения всем сокетам
foreach ($accounts as $key => $value)
if ((int) $key !== $frame->fd)
$server->push((int) $key, <<<JSON
{
"type": "unblocked",
"target": "task",
"_key": $message->_key,
"account": {
"_key": {$account->getKey()},
"name": "$name"
}
}
JSON);
}
} else if ($message->type === 'update') {
// Обновление
// Отправка сообщения всем сокетам
foreach ($accounts as $key => $value)
if ((int) $key !== $frame->fd)
$server->push((int) $key, <<<JSON
{
"type": "updated",
"target": "task",
"_key": $message->_key
}
JSON);
} else if ($message->type === 'delete') {
// Обновление
// Отправка сообщения всем сокетам
foreach ($accounts as $key => $value)
if ((int) $key !== $frame->fd)
$server->push((int) $key, <<<JSON
{
"type": "deleted",
"target": "task",
"_key": $message->_key
}
JSON);
}
}
} catch (exception $e) {
var_dump($e);
}
}
});
$server->on('Close', function (Server $server, int $id) use ($accounts, $core, $model) {
// Удаление из базы данных
$accounts->del((string) $id);
// Запись в буфер вывода (журнал)
echo '[' . date('Y.m.d h:i:s', time()) . "][$id] Disconnect (server) ({$accounts->count()})" . PHP_EOL;
});
$server->on('Disconnect', function (Server $server, int $id) use ($accounts, $core, $model) {
// Удаление из базы данных
$accounts->del((string) $id);
// Запись в буфер вывода (журнал)
echo '[' . date('Y.m.d h:i:s', time()) . "][$id] Disconnect (client) ({$accounts->count()})" . PHP_EOL;
});
// Запуск (точка входа)
$server->start();

View File

@ -0,0 +1,11 @@
@charset "UTF-8";
section#account {
z-index: 999;
position: fixed;
bottom: 20px;
left: 20px;
height: 16px;
display: flex;
}

View File

@ -79,7 +79,7 @@ main>section[data-section="catalog"]>article.product>a {
padding: 4px 8px 4px 8px;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
white-space: nowrap;
font-weight: bold;
backdrop-filter: brightness(0.4) contrast(1.2);
color: var(--text-light, var(--tg-theme-text-color));

View File

@ -44,7 +44,7 @@ main>section[data-section="catalog"][data-catalog-type="categories"]>a.category[
filter: brightness(60%);
}
main>section[data-section="catalog"][data-catalog-type="categories"]>a.category[type="button"]:hover>img {
main>section[data-section="catalog"][data-catalog-type="categories"]>a.category[type="button"]:is(:hover, :focus)>img {
filter: unset;
}
@ -92,16 +92,16 @@ main>section[data-section="catalog"][data-catalog-type="products"]>div.column>ar
cursor: pointer;
}
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product:hover {
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product:is(:hover, :focus) {
/* 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>* {
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product:is(:hover, :focus)>* {
transition: 0s;
}
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product:not(:hover)>* {
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product:not(:is(:hover, :focus))>* {
transition: 0.2s ease-out;
}
@ -163,19 +163,7 @@ main>section[data-section="cart"]>i.icon.shopping.cart {
}
main>section[data-section="filters"] {
--diameter: 4rem;
z-index: 999;
right: 5vw;
bottom: 5vw;
position: fixed;
width: var(--diameter);
height: var(--diameter);
max-height: 2.5rem;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
border-radius: 100%;
background-color: var(--tg-theme-button-color);
align-items: start;
}
main>section[data-section="filters"][data-filter="brand"] {}

View File

@ -0,0 +1,35 @@
@charset "UTF-8";
section#connection {
z-index: 999;
position: fixed;
bottom: 20px;
right: 20px;
height: 16px;
display: flex;
}
section#connection>i#indicator {
width: 16px;
height: 16px;
display: block;
cursor: help;
border-radius: 100%;
}
section#connection>small {
margin-right: 7px;
height: 16px;
display: flex;
align-items: center;
font-weight: bold;
color: var(--socket-text);
}
section#connection>i#indicator.disconnected:not(.connected) {
background-color: var(--socket-disconnected);
}
section#connection>i#indicator.connected:not(.disconnected) {
background-color: var(--socket-connected);
}

View File

@ -0,0 +1,115 @@
@charset "UTF-8";
section[data-type="select"] {
--width: max(14rem, 20vw);
--height-element: 2rem;
--height-close: var(--height-element);
--height-open: max-content;
position: relative;
width: var(--width);
height: var(--height-close);
display: flex;
flex-direction: column;
cursor: context-menu;
border-radius: 0.75rem;
overflow-x: hidden;
background-color: var(--tg-theme-button-color);
transition: 0s;
}
section[data-type="select"]:not(:focus):after {
z-index: 30;
content: '';
top: calc(50% - 2.5px);
right: 1rem;
position: absolute;
width: 0;
height: 0;
pointer-events: none;
border-left: 5px solid transparent;
border-top: 5px solid var(--tg-theme-button-text-color, black);
border-right: 5px solid transparent;
}
section[data-type="select"]>input {
left: -99999px;
position: absolute;
opacity: 0;
}
section[data-type="select"]>label {
z-index: 10;
order: 2;
top: 0;
position: absolute;
width: 100%;
height: var(--height-element);
padding: 0 1rem;
display: none;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
box-sizing: border-box;
pointer-events: none;
color: var(--tg-theme-button-text-color);
transition: 0s;
}
section[data-type="select"]:is([data-select="open"], :focus)>input:not(:checked)+label[for$='title'] {
display: none;
}
section[data-type="select"]:is([data-select="open"], :focus)>input:not(:checked)+label[for$='all']:not(:hover, :active, :focus) {
filter: brightness(90%);
}
section[data-type="select"]>input:not(:checked)+label {
cursor: pointer;
filter: brightness(80%);
}
section[data-type="select"]:is([data-select="open"], :focus)>input+label:hover {
filter: brightness(110%);
}
section[data-type="select"]:is([data-select="open"], :focus)>input:checked+label:hover {
filter: brightness(120%);
}
section[data-type="select"]:is([data-select="open"], :focus)>input+label:is(:active, :focus) {
filter: brightness(60%);
}
section[data-type="select"]:is([data-select="open"], :focus)>input:checked+label:is(:active, :focus) {
filter: brightness(70%);
}
section[data-type="select"]>input:checked+label {
order: 1;
max-width: calc(var(--width) - 2rem - 10px);
display: inline;
line-height: var(--height-element);
padding-right: 0;
}
section[data-type="select"]:is([data-select="open"], :focus)>input:checked+label {
max-width: initial;
padding-right: initial;
}
section[data-type="select"]:is([data-select="open"], :focus) {
height: var(--height-open, max-content);
}
section[data-type="select"]:is([data-select="open"], :focus)>label {
position: relative;
display: inline;
line-height: var(--height-element);
pointer-events: all;
}
@media only screen and (max-width: 500px) {
section[data-type="select"]:only-child {
--width: 100%
}
}

View File

@ -2,8 +2,14 @@
:root {
--text-light: #fafaff;
--socket-connected: #2be851;
--socket-disconnected: #8e8181;
--socket-text: #b09999;
}
* {
text-decoration: none;
outline: none;
@ -45,6 +51,10 @@ main {
transition: 0s;
}
main>section:last-child {
margin-bottom: 5rem;
}
main>*[data-section] {
--gap: 16px;
--width: calc(100% - var(--gap) * 2);
@ -112,7 +122,7 @@ search:has(input:disabled) {
cursor: pointer;
}
:is(button, a[type="button"]) {
:is(button, :is(a, label)[type="button"]) {
padding: 8px 16px;
display: flex;
justify-content: center;
@ -129,11 +139,11 @@ a[type="button"] {
height: 23px;
}
:is(button, a[type="button"]):is(:hover) {
:is(button, :is(a, label)[type="button"]):is(:hover, :focus) {
filter: brightness(120%);
}
:is(button, a[type="button"]):active {
:is(button, :is(a, label)[type="button"]):active {
filter: brightness(80%);
transition: 0s;
}

View File

@ -0,0 +1,23 @@
{% block css %}
<link type="text/css" rel="stylesheet" href="/themes/default/css/account.css">
{% endblock %}
{% block body %}
{% if account is empty %}
<section id="account">
<a onclick="core.account.authentication()">
<!-- {{ language == 'ru' ? "Аутентификация" : "Authentication" }} -->
</a>
</section>
{% else %}
<section id="account">
<a onclick="core.account.profile()">
@{{ account.domain }}
</a>
</section>
{% endif %}
{% endblock %}
{% block js %}
<script type="text/javascript" src="/js/account.js"></script>
{% endblock %}

View File

@ -3,15 +3,15 @@
data-catalog-level="{{ level ?? 0 }}">
{% for category in categories %}
{% if category.images %}
<a id="{{ category.getId() }}" class="category" type="button" onclick="return core.catalog.category(this);"
data-category-identifier="{{ category.identifier }}">
<a id="{{ category.getId() }}" class="category" type="button" onclick="core.catalog.category(this)" onkeydown="event.keyCode === 13 && core.catalog.category(this)"
data-category-identifier="{{ category.identifier }}" tabindex="3">
<img src="{{ category.images.0.storage }}"
alt="{{ category.name[language] }}" ondrugstart="return false;">
<p>{{ category.name[language] }}</p>
</a>
{% else %}
<a id="{{ category.getId() }}" class="category" type="button" onclick="return core.catalog.category(this);"
data-category-identifier="{{ category.identifier }}">
<a id="{{ category.getId() }}" class="category" type="button" onclick="core.catalog.category(this)" onkeydown="event.keyCode === 13 && core.catalog.category(this)"
data-category-identifier="{{ category.identifier }}" tabindex="3">
<p>{{ category.name[language] }}</p>
</a>
{% endif %}

View File

@ -1,8 +1,14 @@
{% if filters is not empty %}
<section data-section="filters" data-filter="brand">
<section class="unselectble" data-section="filters">
<section class="unselectable" data-type="select" data-filter="brand" tabindex="4">
<input name="brand" type="radio" id="brand_title"{% if session.filters.brand is empty %} checked{% endif %}>
<label for="brand_title" type="button">{{ language == 'ru' ? 'Бренд' : 'Brand' }}</label>
<input name="brand" type="radio" id="brand_all">
<label for="brand_all" type="button">{{ language == 'ru' ? 'Все бренды' : 'All brands' }}</label>
{% for brand in filters.brands %}
<input class="menu" name="brand" type="radio" id="brand_{{ loop.index }}" checked>
<label for="brand_{{ loop.index }}" class="menu option">{{ brand }}</label>
<input name="brand" type="radio" id="brand_{{ loop.index }}"{% if session.filters.brand == brand %} checked{% endif %}>
<label for="brand_{{ loop.index }}" type="button">{{ brand }}</label>
{% endfor %}
</section>
</section>
{% endif %}

View File

@ -1,13 +1,13 @@
{% macro card(product) %}
{% set title = product.name[language] ~ ' ' ~ product.brand[language] ~ format_dimensions(product.dimensions.x, product.dimensions.y, product.dimensions.z, ' ') ~ ' ' ~ product.weight ~ 'г' %}
<article id="{{ product.getId() }}" class="product unselectable">
<a onclick="core.catalog.product({{ product.identifier }})">
<a data-product-identifier="{{ product.identifier }}" onclick="core.catalog.product(this)" onkeydown="event.keyCode === 13 && core.catalog.product(this)" tabindex="10">
<img src="{{ product.images.0.storage }}" alt="{{ product.name[language] }}" ondrugstart="return false;">
<p class="title" title="{{ product.name[language] }}">
{{ title | length > 45 ? title | slice(0, 45) ~ '...' : title }}
</p>
</a>
<button title="Добавить в корзину" onclick="catalog.cart.add({{ product.getKey() }})">
<button title="Добавить в корзину" onclick="catalog.cart.add(this)" tabindex="15">
{{ product.cost }}р
</button>
</article>

View File

@ -1,4 +1,4 @@
<search data-section="search">
<label><i class="icon search"></i></label>
<input placeholder="Поиск по каталогу" type="search" onkeyup="return core.catalog.search(event, this);" />
<input id="search" placeholder="Поиск по каталогу" type="search" onkeyup="event.keyCode !== 9 && core.catalog.search(event, this)" tabindex="1"/>
</search>

View File

@ -3,6 +3,7 @@
{% block css %}
{{ parent() }}
<link type="text/css" rel="stylesheet" href="/themes/{{ theme }}/css/catalog/2columns.css" />
<link type="text/css" rel="stylesheet" href="/themes/{{ theme }}/css/interface/select.css" />
<link type="text/css" rel="stylesheet" href="/themes/{{ theme }}/css/icons/search.css" />
<link type="text/css" rel="stylesheet" href="/themes/{{ theme }}/css/icons/shopping_cart.css" />
<link type="text/css" rel="stylesheet" href="/themes/{{ theme }}/css/icons/close.css" />
@ -13,9 +14,9 @@
{% include "/themes/default/catalog/elements/search.html" %}
{% include "/themes/default/catalog/elements/categories.html" %}
{% include "/themes/default/catalog/elements/filters.html" %}
{% include "/themes/default/catalog/elements/products/2columns.html" %}
<!-- {% include "/themes/default/catalog/elements/cart.html" %} -->
{% include "/themes/default/catalog/elements/filters.html" %}
{% endblock %}
{% block js %}

View File

@ -0,0 +1,14 @@
{% block css %}
<link type="text/css" rel="stylesheet" href="/themes/default/css/connection.css">
{% endblock %}
{% block body %}
<section id='connection' class="unselectable" title="Disconnected">
<small>0.00</small>
<i id="indicator" class="disconnected"></i>
</section>
{% endblock %}
{% block js %}
<script type="text/javascript" src="/js/connection.js"></script>
{% endblock %}

View File

@ -19,7 +19,7 @@
</head>
<body>
<section id="loading">
<section id="loading" disabled>
<i class="icon loading spinner animated"></i>
</section>

View File

@ -1,16 +1,22 @@
{% extends "/themes/default/core.html" %}
{% use "/themes/default/header.html" with css as header_css, body as header, js as header_js %}
{% use "/themes/default/connection.html" with css as connection_css, body as connection_body, js as connection_js %}
{% use "/themes/default/account.html" with css as account_css, body as account_body, js as account_js %}
{% use "/themes/default/footer.html" with css as footer_css, body as footer, js as footer_js %}
{% block css %}
{{ parent() }}
{{ block('header_css') }}
{{ block('connection_css') }}
{{ block('account_css') }}
{{ block('footer_css') }}
{% endblock %}
{% block body %}
{{ block('header') }}
{{ block('connection_body') }}
{{ block('account_body') }}
<main>
{% block main %}
{{ main|raw }}
@ -21,6 +27,8 @@
{% block js %}
{{ parent() }}
{{ block('footer_js') }}
{{ block('header_js') }}
{{ block('connection_js') }}
{{ block('account_js') }}
{{ block('footer_js') }}
{% endblock %}

View File

@ -3,5 +3,6 @@
<script src="/js/core.js" defer></script>
<script src="/js/damper.js"></script>
<script src="/js/telegram.js"></script>
<script src="/js/session.js"></script>
<script src="/js/authentication.js"></script>
{% endblock %}