resolved #9, resolved #15, resolved #17, resolved #22, resolved #36, resolved #37, resolved #38, resolved #39

This commit is contained in:
Arsen Mirzaev Tatyano-Muradovich 2025-04-03 22:18:23 +03:00
parent 7847d15da1
commit 5382e15af2
67 changed files with 5376 additions and 1511 deletions

View File

@ -195,7 +195,7 @@ You can copy a clean menu documents without comments from here: `/examples/arang
"style": { // The `style` attribute "style": { // The `style` attribute
"order": 0 "order": 0
}, },
"class": {}, "class": "",
"icon": { // Icon from `/themes/default/css/icons` "icon": { // Icon from `/themes/default/css/icons`
"style": { // The `style` attribute "style": { // The `style` attribute
"rotate": "-135deg" "rotate": "-135deg"
@ -228,6 +228,17 @@ You can copy a clean settings document without comments from here: `/examples/ar
"offer": false, // Display the data of a public offer in the footer? (bool) (does not affect the `/offer` page generation) "offer": false, // Display the data of a public offer in the footer? (bool) (does not affect the `/offer` page generation)
"sim": null, // Examples: "+7 000 000-00-00", "70000000000" (string|null) (if `null` it will not be displayed) "sim": null, // Examples: "+7 000 000-00-00", "70000000000" (string|null) (if `null` it will not be displayed)
"mail": null // Example: "name@domain.zone" (string|null) (if `null` it will not be displayed) "mail": null // Example: "name@domain.zone" (string|null) (if `null` it will not be displayed)
},
"search": {
"enabled": true, // Enable the search input field?
"position": "fixed" // Values: "fixed", "relative"
},
"cart": {
"enabled": true // Enable the cart button?
},
"css": {
"catalog-button-cart-background": "#40a7e3",
"product-button-cart-background": "#40a7e3"
} }
} }
``` ```

View File

@ -33,7 +33,8 @@
"avadim/fast-excel-reader": "^2.19", "avadim/fast-excel-reader": "^2.19",
"ttatpuot/cdek-sdk2.0": "^1.2", "ttatpuot/cdek-sdk2.0": "^1.2",
"guzzlehttp/guzzle": "^7.9", "guzzlehttp/guzzle": "^7.9",
"php-http/guzzle7-adapter": "^1.0" "php-http/guzzle7-adapter": "^1.0",
"react/async": "^4.3"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {

77
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "d59bfe9a2623bc1b6bd95caaf90e9127", "content-hash": "0c66c5ef998a3ccf4d0738e41bf2a3d3",
"packages": [ "packages": [
{ {
"name": "avadim/fast-excel-helper", "name": "avadim/fast-excel-helper",
@ -2687,6 +2687,81 @@
}, },
"time": "2019-03-08T08:55:37+00:00" "time": "2019-03-08T08:55:37+00:00"
}, },
{
"name": "react/async",
"version": "v4.3.0",
"source": {
"type": "git",
"url": "https://github.com/reactphp/async.git",
"reference": "635d50e30844a484495713e8cb8d9e079c0008a5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/reactphp/async/zipball/635d50e30844a484495713e8cb8d9e079c0008a5",
"reference": "635d50e30844a484495713e8cb8d9e079c0008a5",
"shasum": ""
},
"require": {
"php": ">=8.1",
"react/event-loop": "^1.2",
"react/promise": "^3.2 || ^2.8 || ^1.2.1"
},
"require-dev": {
"phpstan/phpstan": "1.10.39",
"phpunit/phpunit": "^9.6"
},
"type": "library",
"autoload": {
"files": [
"src/functions_include.php"
],
"psr-4": {
"React\\Async\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Christian Lück",
"email": "christian@clue.engineering",
"homepage": "https://clue.engineering/"
},
{
"name": "Cees-Jan Kiewiet",
"email": "reactphp@ceesjankiewiet.nl",
"homepage": "https://wyrihaximus.net/"
},
{
"name": "Jan Sorgalla",
"email": "jsorgalla@gmail.com",
"homepage": "https://sorgalla.com/"
},
{
"name": "Chris Boden",
"email": "cboden@gmail.com",
"homepage": "https://cboden.dev/"
}
],
"description": "Async utilities and fibers for ReactPHP",
"keywords": [
"async",
"reactphp"
],
"support": {
"issues": "https://github.com/reactphp/async/issues",
"source": "https://github.com/reactphp/async/tree/v4.3.0"
},
"funding": [
{
"url": "https://opencollective.com/reactphp",
"type": "open_collective"
}
],
"time": "2024-06-04T14:40:02+00:00"
},
{ {
"name": "react/cache", "name": "react/cache",
"version": "v1.2.0", "version": "v1.2.0",

View File

@ -0,0 +1,19 @@
{
"urn": "/account",
"name": {
"en": "Account",
"ru": "Аккаунт"
},
"identifier": "account",
"style": {
"order": 1
},
"class": "",
"icon": {
"style": {},
"class": "loading spinner animated"
},
"image": {
"storage": null
}
}

View File

@ -5,9 +5,9 @@
"ru": "Корзина" "ru": "Корзина"
}, },
"style": { "style": {
"order": 1 "order": 999
}, },
"class": {}, "class": "cart",
"icon": { "icon": {
"style": {}, "style": {},
"class": "shopping cart" "class": "shopping cart"

View File

@ -7,7 +7,7 @@
"style": { "style": {
"order": 0 "order": 0
}, },
"class": {}, "class": "",
"icon": { "icon": {
"style": {}, "style": {},
"class": "house" "class": "house"

View File

@ -12,5 +12,16 @@
"offer": false, "offer": false,
"sim": null, "sim": null,
"mail": null "mail": null
},
"search": {
"enabled": true,
"position": "fixed"
},
"cart": {
"enabled": true
},
"css": {
"catalog-button-cart-background": "#40a7e3",
"product-button-cart-background": "#40a7e3"
} }
} }

View File

@ -14,7 +14,7 @@ server {
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
location / { location / {
try_files $uri $uri/ /index.php; try_files $uri $uri/ /index.php?$query_string;
} }
location /api/cdek { location /api/cdek {

View File

@ -0,0 +1,375 @@
<?php
declare(strict_types=1);
namespace mirzaev\huesos\controllers\api\acquirings;
// Files of the project
use mirzaev\huesos\controllers\core,
mirzaev\huesos\models\order,
mirzaev\huesos\models\cart,
mirzaev\huesos\models\product,
mirzaev\huesos\models\telegram,
mirzaev\huesos\models\acquirings\robokassa as model,
mirzaev\huesos\models\enumerations\currency,
mirzaev\huesos\models\enumerations\language;
// Framework for PHP
use mirzaev\minimal\http\enumerations\content,
mirzaev\minimal\http\enumerations\status;
// Framework for ArangoDB
use mirzaev\arangodb\collection,
mirzaev\arangodb\document;
// Framework for Telegram
use Zanzara\Zanzara as zanzara,
Zanzara\Context as context,
Zanzara\Config as config,
Zanzara\Telegram\Type\Message as message;
// Event manager for PHP
use React\EventLoop\Loop as loop;
// Built-in libraries
use DateTime as datetime,
Exception as exception;
/**
* Controller of robokassa
*
* @package mirzaev\huesos\controllers\api\acquirings
*
* @param array $errors Registry of errors
*
* @method null result()
* @method null success()
* @method null fail()
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class robokassa extends core
{
/**
* Errors
*
* @var array $errors Registry of errors
*/
protected array $errors = [
'session' => [],
'account' => [],
'order' => [],
'robokassa' => []
];
/**
* Result
*
* @param null
*/
public function result(
?string $OutSum = null,
?string $InvId = null,
?string $SignatureValue = null,
?string $PaymentMethod = null,
?string $IncSum = null,
?string $IncCurrLabel = null,
?string $IsTest = null,
?string $EMail = null,
?string $Fee = null,
string ...$other
): null {
// Searching for the order
$order = order::_read(
filter: 'd._key == @_key && d.paid != true',
sort: 'd.created DESC, d._key DESC',
amount: 1,
parameters: ['_key' => $InvId],
errors: $this->errors['robokassa']
);
if ($order instanceof order) {
// Initialized the order
if (model::result(identifier: (int) $InvId, cost: $OutSum, hash: $SignatureValue, test: (bool) $IsTest, errors: $this->errors['robokassa'])) {
// Verified the payment
// Initializing the cart
$cart = $order->cart();
if ($cart instanceof cart) {
// Initialized the cart
if ((float) ($cart->cost() + ($cart->buffer['delivery']['cost'] ?? 0)) === (float) $OutSum) {
// Full payment received
// Writing that the order being been paid
$order->paid = true;
/* $order->term = null; */
if (document::update($order->__document(), errors: $this->errors['order'])) {
// Writed into ArangoDB
// Initializing identifier of the order
$identifier = $order->__document()->getKey();
// Initializing the customer account
$customer = $order->account();
$config = new config();
$config->setParseMode(config::PARSE_MODE_MARKDOWN);
$config->useReactFileSystem(true);
/* $config->setLoop(loop::get()); */
$robot = new zanzara(TELEGRAM_KEY, $config);
// Sending the message
$robot->getTelegram()->sendMessage("✅ *Заказ \#$identifier оплачен*", ['chat_id' => $customer->identifier])
->then(function ($message) use ($robot, $cart, $order, $identifier, $customer) {
// Sended the message
// Initializing the chats for sending registry
$chats = [];
foreach ($this->settings->chats as $chat) {
// Iteration over chats
if ($chat['orders']) {
// Authorized to receive orders
// Writing into the chats for sending registry
$chats[] = $chat['id'];
}
}
if (count($chats) > 0) {
// Initialized chats
// Declaring the formatted list of products for message
$list = '';
// Declaring total cost of products
$cost = $cart->cost($list);
// Escaping the formatted list of products for the message
$list = telegram::unmarkdown($list);
// Initializing currency symbol
$symbol = ($account->currency ?? currency::rub)->symbol();
// Declaring delivery texts
$delivery_cost = $delivery_days = $delivery_address = '';
if (!empty($cart->buffer['delivery'])) {
// Initialized delibery data
// Initializing delivery cost for message
$delivery_cost = telegram::unmarkdown((string) $cart->buffer['delivery']['cost']);
// Initializing delivery days for message
$delivery_days = telegram::unmarkdown((string) $cart->buffer['delivery']['days']);
// Initializing delivery address for message
$delivery_address = telegram::unmarkdown($cart->buffer['delivery']['location']['name'] . ', ' . $cart->buffer['delivery']['street']);
}
// Initializing receiver domain for the message
$domain = telegram::unmarkdown($customer->domain);
// Initializing receiver SIM for the message
$sim = telegram::unmarkdown((string) $customer->receiver['sim']);
// Initializing receiver name for the message
$name = telegram::unmarkdown($customer->receiver['name']);
// Initializing receiver address for the message
$address = telegram::unmarkdown($customer->receiver['address']);
// Initializing the message cost part
$part_cost = "*Стоимость:* $cost$symbol";
if (!empty($delivery_cost)) $part_cost .= " \+ $delivery_cost$symbol";
if (!empty($delivery_days)) $part_cost .= " \($delivery_days дней\)";
// Initializing the message delivery part
$part_delivery = '';
if (!empty($delivery_address)) $part_delivery .= "*Адрес доставки:* $delivery_address";
if (!empty($part_delivery)) $part_delivery = "\n$part_delivery";
// Initializing the message receiver part
$part_receiver = "*Получатель:* @$domain";
if (!empty($sim)) $part_receiver .= " $sim";
if (!empty($name)) $part_receiver .= "\n*$name*";
if (!empty($address)) $part_receiver .= "\n\n*Адрес:* $address";
// Initializing the message commentary part
$part_commentary = '';
if (!empty($order->commentary)) $part_commentary .= "\n\n*Комментарий:* " . $order->commentary;
// Sending messages
$robot->getTelegram()->sendBulkMessage(
$chats,
<<<TXT
📦 *Заказ* \#$identifier
$list
$part_cost$part_delivery
$part_receiver$part_commentary
TXT,
[
'reply_markup' => [
'inline_keyboard' => [
[
['text' => '✉️ Чат с покупателем', 'url' => 'https://t.me/' . $customer->domain]
]
],
'disable_notification' => true
]
]
);
}
});
// Sending response
$this->response
->start()
->clean()
->sse()
->write("OK$identifier")
->validate($this->request)
?->body()
->end();
} else throw new exception('Failed to update the order');
}
}
}
}
// Exit (success/fail)
return null;
}
/**
* Success
*
* @return null
*/
public function success(
?string $OutSum = null,
?string $InvId = null,
?string $SignatureValue = null,
?string $IsTest = null,
?string $Culture = null,
string ...$other
): null {
// Searching for the order
$order = order::_read(
filter: 'd._key == @_key',
sort: 'd.created DESC, d._key DESC',
amount: 1,
parameters: ['_key' => $InvId],
errors: $this->errors['robokassa']
);
if ($order instanceof order) {
// Initialized the order
if (model::success(identifier: (int) $InvId, cost: $OutSum, hash: $SignatureValue, test: (bool) $IsTest, errors: $this->errors['robokassa'])) {
// Verified the payment
if ($IsTest == 1) {
// Enabled the test mode
// Initializing a paragraph abount the test mode
$this->view->test = $this->language === language::ru ? 'Тестовый режим' : 'Test mode';
}
// Render page
$page = $this->view->render('api/acquirings/robokassa/success.html', [
'h2' => $this->language === language::ru ? 'Заказ #' . $order->getKey() . ' оплачен' : 'Order #' . $order->getKey() . ' paid',
'identifier' => $order->getKey(),
'closing' => [
'title' => $this->language === language::ru ? 'Закрытие окна' : 'Window closing',
'iterator' => 30
]
]);
// Sending response
$this->response
->start()
->clean()
->sse()
->write($page)
->validate($this->request)
?->body()
->end();
// Deinitializing rendered page
unset($page);
}
}
// Exit (success/fail)
return null;
}
/**
* Fail
*
* @return null
*/
public function fail(
?string $OutSum = null,
?string $InvId = null,
?string $IsTest = null,
?string $Culture = null,
string ...$other
): null {
// Searching for the order
$order = order::_read(
filter: 'd._key == @_key',
sort: 'd.created DESC, d._key DESC',
amount: 1,
parameters: ['_key' => $InvId],
errors: $this->errors['robokassa']
);
if ($order instanceof order) {
// Initialized the order
if ($IsTest == 1) {
// Enabled the test mode
// Initializing a paragraph abount the test mode
$this->view->test = $this->language === language::ru ? 'Тестовый режим' : 'Test mode';
}
// Render page
$page = $this->view->render('api/acquirings/robokassa/fail.html', [
'h2' => $this->language === language::ru ? 'Заказ #' . $order->getKey() . ' не оплачен' : 'Order #' . $order->getKey() . ' not paid',
'identifier' => $order->getKey(),
'closing' => [
'title' => $this->language === language::ru ? 'Закрытие окна' : 'Window closing',
'iterator' => 30
]
]);
// Sending response
$this->response
->start()
->clean()
->sse()
->write($page)
->validate($this->request)
?->body()
->end();
// Deinitializing rendered page
unset($page);
}
// Exit (success/fail)
return null;
}
}

View File

@ -9,12 +9,23 @@ use mirzaev\huesos\controllers\core,
mirzaev\huesos\models\cart as model, mirzaev\huesos\models\cart as model,
mirzaev\huesos\models\product, mirzaev\huesos\models\product,
mirzaev\huesos\models\menu, mirzaev\huesos\models\menu,
mirzaev\huesos\models\telegram,
mirzaev\huesos\models\enumerations\currency,
mirzaev\huesos\models\enumerations\language; mirzaev\huesos\models\enumerations\language;
// Framework for PHP // Framework for PHP
use mirzaev\minimal\http\enumerations\content, use mirzaev\minimal\http\enumerations\content,
mirzaev\minimal\http\enumerations\status; mirzaev\minimal\http\enumerations\status;
// Framework for Telegram
use Zanzara\Zanzara as zanzara,
Zanzara\Context as context,
Zanzara\Config as config,
Zanzara\Telegram\Type\Message as message;
// Event manager for PHP
use React\EventLoop\Loop as loop;
/** /**
* Controller of cart * Controller of cart
* *
@ -113,12 +124,26 @@ final class cart extends core
$this->view->formatted = $formatted; $this->view->formatted = $formatted;
} }
// Initializing types of avaiabld deliveries if (array_search('address', $this->settings->input['deliveries']['site'], true) !== false) {
$this->view->deliveries = [ // The deliveries input is enabled for the site
'cdek' => [
'label' => 'CDEK' // Declaring the deliveries list
] $deliveries = [];
];
foreach ($this->settings->deliveries as $key => $value) {
// Iterating over deliveries
if ($value['enabled']) {
// The delivery is enabled
// Writing into the deliveries list
$deliveries[$key] = ['label' => $value['label']];
}
}
// Writing the deliveries list into the templater variables
$this->view->deliveries = $deliveries;
}
if (str_contains($this->request->headers['accept'], content::json->value)) { if (str_contains($this->request->headers['accept'], content::json->value)) {
// Request for JSON response // Request for JSON response
@ -444,25 +469,134 @@ final class cart extends core
} }
/** /**
* Pay * Attach
* *
* Pay for the cart * Attach the cart
* *
* @return null * @return null
*
* @todo кажется я сделал хуйню
*/ */
public function pay(): null public function attach(?string $share = null): null
{ {
if (str_contains($this->request->headers['accept'], content::json->value)) { if (str_contains($this->request->headers['accept'], content::json->value)) {
// Request for JSON response // Request for JSON response
// Initializing the cart if ($this->account) {
$this->cart ??= $this->account?->cart() ?? $this->session?->cart(); // Initialized the account
if ($this->cart instanceof model) { $config = new config();
$config->setParseMode(config::PARSE_MODE_MARKDOWN);
$config->useReactFileSystem(true);
/* $config->setLoop(loop::get()); */
$robot = new zanzara(TELEGRAM_KEY, $config);
// Initializing cart
$cart = model::_read(
filter: 'd.share == @share',
sort: 'd.updated DESC, d.created DESC, d._key DESC',
amount: 1,
page: 1,
parameters: ['share' => $share]
);
// Deinitializing unnecessary variables
unset($share);
if ($cart instanceof model) {
// Initialized the cart // Initialized the cart
if ($share = $this->cart->share ?? $this->cart->share()) { // Unsharing the cart
// The cart is available for sharing $cart->unshare();
// Connecting the cart to the account
$edge = $this->account->connect($cart);
if (!empty($edge)) {
// Connected the cart to the account
// Initializing products in the cart
$products = $cart->products(language: $this->account->language ?? language::ru, currency: $this->account->currency ?? currency::rub);
if (!empty($products)) {
// Initialized products in the cart
// Declaring the formatted list of products for message
$list = '';
// Declaring total cost of products
$cost = $cart->cost($list);
// Escaping the formatted list of products for the message
$list = telegram::unmarkdown($list);
// Deinitializing unnecessary variables
unset($products, $product, $row);
// Initializing identifier of the cart
$identifier = $cart->getKey();
// Initializing currency symbol
$symbol = ($this->account->currency ?? currency::rub)->symbol();
// Declaring delivery texts
$delivery_cost = $delivery_days = $delivery_address = '';
if (!empty($cart->buffer['delivery'])) {
// Initialized delibery data
// Initializing delivery cost for message
$delivery_cost = telegram::unmarkdown((string) $cart->buffer['delivery']['cost']);
// Initializing delivery days for message
$delivery_days = telegram::unmarkdown((string) $cart->buffer['delivery']['days']);
// Initializing delivery address for message
$delivery_address = telegram::unmarkdown($cart->buffer['delivery']['location']['name'] . ', ' . $cart->buffer['delivery']['street']);
}
// Initializing the message cost part
$part_cost = "*Стоимость:* $cost$symbol";
if (!empty($delivery_cost)) $part_cost .= " \+ $delivery_cost$symbol";
if (!empty($delivery_days)) $part_cost .= " \($delivery_days дней\)";
// Initializing the message delivery part
$part_delivery = '';
if (!empty($delivery_address)) $part_cost .= "*Адрес доставки:* $delivery_address";
if (!empty($part_delivery)) $part_delivery = "\n$part_delivery";
$robot->getTelegram()->sendMessage(
<<<TXT
🛒 *Добавлена корзина* \#$identifier
$list
$part_cost$part_delivery
TXT,
[
'chat_id' => $this->account->identifier,
'reply_markup' => [
'inline_keyboard' => [
[
['text' => '🚚 Оформить доставку', 'callback_data' => 'cart_delivery'],
],
],
'disable_notification' => true
]
]
);
}
}
// Deinitializing unnecessary variables
unset($cart, $list);
}
}
// Deinitializing unnecessary variables
unset($cart);
$robot->getLoop()->run();
// Sending response // Sending response
$this->response $this->response
@ -470,7 +604,7 @@ final class cart extends core
->clean() ->clean()
->sse() ->sse()
->json([ ->json([
'share' => $share, 'success' => true,
'errors' => $this->errors 'errors' => $this->errors
]) ])
->validate($this->request) ->validate($this->request)
@ -478,53 +612,6 @@ final class cart extends core
->end(); ->end();
} }
// Deinitializing unnecessary variables
unset($hash);
}
}
// Exit (success/fail)
return null;
}
/**
* Robokassa
*
* HTML-document with robokassa iframe
*
* @param null
*
* @todo THIS MUST BE A PAYMENT OF ORDER IN THE FUTURE, NOT CART
*/
public function robokassa(): null
{
// Initializing the cart
$this->cart ??= $this->account?->cart() ?? $this->session?->cart();
// Initializing the cart data
$this->view->cart = $this->cart;
$this->view->summary = $this->cart?->summary(currency: $this->currency);
if (str_contains($this->request->headers['accept'], content::any->value)) {
// Request for any response
// Render page
$page = $this->view->render('iframes/robokassa.html');
// Sending response
$this->response
->start()
->clean()
->sse()
->write($page)
->validate($this->request)
?->body()
->end();
// Deinitializing rendered page
unset($page);
}
// Exit (success/fail) // Exit (success/fail)
return null; return null;
} }

View File

@ -52,7 +52,8 @@ final class catalog extends core
'session' => [], 'session' => [],
'account' => [], 'account' => [],
'menu' => [], 'menu' => [],
'catalog' => [] 'catalog' => [],
'cart' => []
]; ];
/** /**
@ -80,9 +81,13 @@ final class catalog extends core
// Initializing the cart // Initializing the cart
$this->cart ??= $this->account?->cart() ?? $this->session?->cart(); $this->cart ??= $this->account?->cart() ?? $this->session?->cart();
// Initializing summary data of the cart
$summary = $this->cart?->summary(currency: $this->currency, errors: $this->errors['cart']);
// Initializing the cart data // Initializing the cart data
$this->view->cart = [ $this->view->cart = [
'products' => $this->cart?->products(language: $this->language, currency: $this->currency) 'products' => $this->cart?->products(language: $this->language, currency: $this->currency),
'summary' => $summary
]; ];
// Validating received product identifier // Validating received product identifier
@ -113,6 +118,7 @@ final class catalog extends core
'cart' => [ 'cart' => [
'amount' => $this->view->cart['products'][$this->view->product->getId()]['amount'] ?? 0, 'amount' => $this->view->cart['products'][$this->view->product->getId()]['amount'] ?? 0,
'text' => [ 'text' => [
'cart' => $this->language === language::ru ? 'Корзина' : 'Cart',
'add' => $this->language === language::ru ? 'Добавить в корзину' : 'Add to the cart', 'add' => $this->language === language::ru ? 'Добавить в корзину' : 'Add to the cart',
'added' => $this->language === language::ru ? 'Добавлено в корзину' : 'Added to the cart' 'added' => $this->language === language::ru ? 'Добавлено в корзину' : 'Added to the cart'
] ]
@ -231,7 +237,7 @@ final class catalog extends core
// search for root ascendants categories // search for root ascendants categories
$this->view->categories = entry::ascendants( $this->view->categories = entry::ascendants(
descendant: new category, descendant: new category,
return: 'DISTINCT MERGE(ascendant, { name: ascendant.name.@language})', return: 'DISTINCT MERGE(d, { name: d.name.@language})',
parameters: ['language' => $this->language->name], parameters: ['language' => $this->language->name],
errors: $this->errors['catalog'] errors: $this->errors['catalog']
) ?? null; ) ?? null;
@ -298,7 +304,7 @@ final class catalog extends core
if ($this->view->menu instanceof _document) $this->view->menu = [$this->view->menu]; if ($this->view->menu instanceof _document) $this->view->menu = [$this->view->menu];
} }
if (str_contains($this->request->headers['accept'], content::json->value)) { if (str_contains($this->request->headers['accept'] ?? [], content::json->value)) {
// Request for JSON response // Request for JSON response
// Initializing the response body buffer // Initializing the response body buffer

View File

@ -87,6 +87,9 @@ final class delivery extends core
'ready' => false 'ready' => false
]; ];
if (array_search('address', $this->settings->input['deliveries']['site'], true) !== false) {
// The deliveries address input is enabled for the site
if (isset($company)) { if (isset($company)) {
// Received company name // Received company name
@ -102,6 +105,9 @@ final class delivery extends core
// Deinitializing unnecessary variables // Deinitializing unnecessary variables
unset($matches); unset($matches);
if ($this->settings->deliveries[$normalized]['enabled']) {
// The delivery found and enabled
// Writing to the session buffer // Writing to the session buffer
$this->core->request(new request('PATCH', '/session/write', protocol::http_3, parameters: ['delivery_company' => $normalized])); $this->core->request(new request('PATCH', '/session/write', protocol::http_3, parameters: ['delivery_company' => $normalized]));
@ -202,6 +208,7 @@ final class delivery extends core
// Writing status of execution to the buffer of the response // Writing status of execution to the buffer of the response
$response['status'] = 'success'; $response['status'] = 'success';
} }
}
} else if (isset($location)) { } else if (isset($location)) {
// Received location name // Received location name
@ -1026,6 +1033,12 @@ final class delivery extends core
unset($delivery, $location, $result, $normalized); unset($delivery, $location, $result, $normalized);
} }
} }
} else {
// The deliveries input is not enabled for the site
// Reinitializing the ready status
$response['ready'] = true;
}
// Sending response // Sending response
$this->response $this->response
@ -1057,7 +1070,7 @@ final class delivery extends core
*/ */
public function calculate(): null public function calculate(): null
{ {
if (str_contains($this->request->headers['accept'], content::json->value)) { if (str_contains($this->request->headers['accept'] ?? '', content::json->value)) {
// Request for JSON response // Request for JSON response
// Initialization buffer of delivery parameters // Initialization buffer of delivery parameters

View File

@ -53,6 +53,7 @@ final class session extends core
* @param ?string $auth_date * @param ?string $auth_date
* @param ?string $hash * @param ?string $hash
* @param ?string $query_id * @param ?string $query_id
* @param ?string $signature
* *
* @return null * @return null
*/ */
@ -62,7 +63,8 @@ final class session extends core
?string $chat_type = null, ?string $chat_type = null,
?string $auth_date = null, ?string $auth_date = null,
?string $hash = null, ?string $hash = null,
?string $query_id = null ?string $query_id = null,
?string $signature = null
): null { ): null {
if (str_contains($this->request->headers['accept'], content::json->value)) { if (str_contains($this->request->headers['accept'], content::json->value)) {
// Request for JSON response // Request for JSON response
@ -70,6 +72,12 @@ final class session extends core
// Declaring variables in the correct scope // Declaring variables in the correct scope
$identifier = $domain = $language = null; $identifier = $domain = $language = null;
// Initializing data of the account
$data = json_decode($user);
// Initializing avatar of the account
$avatar = $data->photo_url;
if ($connected = isset($this->account)) { if ($connected = isset($this->account)) {
// Found the account // Found the account
@ -84,10 +92,17 @@ final class session extends core
} else { } else {
// Not found the account // Not found the account
if (isset($user, $chat_instance, $chat_type, $auth_date, $hash)) { if (isset($user, $auth_date, $hash)) {
// Received required parameters // Received required parameters
$buffer = ['user' => $user, 'chat_instance' => $chat_instance, 'chat_type' => $chat_type, 'auth_date' => $auth_date]; $buffer = ['user' => $user];
if (isset($query_id)) $buffer += ['query_id' => $query_id];
if (isset($signature)) $buffer += ['signature' => $signature];
if (isset($chat_instance)) $buffer += ['chat_instance' => $chat_instance];
if (isset($chat_type)) $buffer += ['chat_type' => $chat_type];
$buffer += ['auth_date' => $auth_date];
ksort($buffer); ksort($buffer);
@ -109,9 +124,6 @@ final class session extends core
if (time() - $auth_date < 86400) { if (time() - $auth_date < 86400) {
// Authorization date less than 1 day ago // Authorization date less than 1 day ago
// Initializing data of the account
$data = json_decode($user);
// Initializing of the account // Initializing of the account
$account = account::initialize( $account = account::initialize(
$data->id, $data->id,
@ -146,6 +158,9 @@ final class session extends core
// Initializing domain of the account // Initializing domain of the account
$domain = $account->domain; $domain = $account->domain;
// Initializing avatar of the account
$avatar = $data->photo_url;
} }
} }
} }
@ -161,6 +176,7 @@ final class session extends core
'connected' => (bool) $connected, 'connected' => (bool) $connected,
'identifier' => $identifier ?? null, 'identifier' => $identifier ?? null,
'domain' => $domain ?? null, 'domain' => $domain ?? null,
'avatar' => $avatar ?? null,
'language' => $language?->name ?? null, 'language' => $language?->name ?? null,
'errors' => $this->errors 'errors' => $this->errors
]) ])

View File

@ -86,7 +86,7 @@ final class account extends core implements document_interface, collection_inter
if (method_exists($account, '__document')) { if (method_exists($account, '__document')) {
// Object can implement a document from ArangoDB // Object can implement a document from ArangoDB
// Abstractioning of parameters // Implementinf parameters
if (isset($result->language)) $result->language = language::{$result->language}; if (isset($result->language)) $result->language = language::{$result->language};
if (isset($result->currency)) $result->currency = currency::{$result->currency}; if (isset($result->currency)) $result->currency = currency::{$result->currency};
@ -121,7 +121,7 @@ final class account extends core implements document_interface, collection_inter
'messages' => $registration->getCanReadAllGroupMessages() 'messages' => $registration->getCanReadAllGroupMessages()
], ],
'premium' => $registration->isPremium(), 'premium' => $registration->isPremium(),
'language' => language::{$registration->getLanguageCode()}->name ?? 'en', 'language' => language::{$registration->getLanguageCode() ?? 'en'}?->name ?? 'en',
'queries' => [ 'queries' => [
'inline' => $registration->getSupportsInlineQueries() 'inline' => $registration->getSupportsInlineQueries()
] ]
@ -147,7 +147,6 @@ final class account extends core implements document_interface, collection_inter
} else throw new exception('Failed to find account'); } else throw new exception('Failed to find account');
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::COLLECTION); } else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::COLLECTION);
} catch (exception $e) { } catch (exception $e) {
var_dump($errors);
// Writing to the registry of errors // Writing to the registry of errors
$errors[] = [ $errors[] = [
'text' => $e->getMessage(), 'text' => $e->getMessage(),

View File

@ -0,0 +1,260 @@
<?php
declare(strict_types=1);
namespace mirzaev\huesos\models\acquirings;
// Files of the project
use mirzaev\huesos\models\core,
mirzaev\huesos\models\settings,
mirzaev\huesos\models\cart,
mirzaev\huesos\models\order,
mirzaev\huesos\models\enumerations\currency;
// Framework for ArangoDB
use mirzaev\arangodb\collection,
mirzaev\arangodb\document;
// Library for ArangoDB
use ArangoDBClient\Document as _document;
// Built-in libraries
use exception,
DomainException as exception_domain,
RuntimeException as exception_runtime;
/**
* Model of robokassa
*
* @package mirzaev\huesos\models\acquirings
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class robokassa extends core
{
/**
* Link
*
* Generate the link to pay for the order
*
* @param order $order The order
* @param array &$errors Registry of errors
*
* @return string|null The link to pay for the order, if generated
*/
public static function link(order $order, array &$errors = []): ?string
{
try {
// Initializing the settings
$settings = settings::active();
if ($settings instanceof settings) {
// Initialized the settings
if ($settings->acquirings['robokassa']['enabled']) {
// Enabled the robokassa acquiring
// Declaring the robokassa settings
$robokassa = null;
// Initializing the test mode register
$test = 0;
if ($settings->acquirings['robokassa']['mode'] === 'work') {
// Work mode
// Initializing the robokassa settings
$robokassa = require(SETTINGS . DIRECTORY_SEPARATOR . 'acquirings' . DIRECTORY_SEPARATOR . 'robokassa' . DIRECTORY_SEPARATOR . 'work.php');
} else if ($settings->acquirings['robokassa']['mode'] === 'test') {
// Test mode
// Reinitializing the test mode register
$test = 1;
// Initializing the robokassa settings
$robokassa = require(SETTINGS . DIRECTORY_SEPARATOR . 'acquirings' . DIRECTORY_SEPARATOR . 'robokassa' . DIRECTORY_SEPARATOR . 'test.php');
} else {
// Failed to initialized the mode
// Exit (fail)
throw new exception_domain('Failed to initialize the robokassa mode');
}
// Initializing the cart
$cart = $order->cart();
if ($cart instanceof cart) {
// Initialized the cart
// Initializing robokassa parameters
$login = $robokassa['login'];
$password = $robokassa['passwords'][0];
// Initializing the order parameters
$identifier = $order->getKey();
// Initializing the invoice parameters
$description = "Оплата заказа";
$cost = $cart->cost() + ($cart->buffer['delivery']['cost'] ?? 0);
// Generatings the MD5 hash
$md5 = md5("$login:$cost:$identifier:$password");
/*
// Writing the MD5 hash into the order implementator
$order->acquirings = ['robokassa' => ['hash' => $md5] + ($order->acquirings['robokassa'] ?? [])] + ($order->acquirings ?? []);
if (document::update($order->__document(), errors: $errors)) {
// Writed to ArangoDB */
// Exit (success)
return "https://auth.robokassa.ru/Merchant/Index.aspx?MerchantLogin=$login&OutSum=$cost&InvId=$identifier&Description=$description&SignatureValue=$md5&IsTest=$test";
/* } else throw new exception('Failed to write the robokassa hash into the order'); */
} else {
// Not initialized the cart
// Exit (fail)
throw new exception_runtime('Failed to initialize the cart');
}
}
}
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Exit (fail)
return null;
}
/**
* Verify (result)
*
* Generate hash by arguments and verify for result with the hash
*
* @param int $identifier Identifier of the order
* @param string $cost Cost of the order (can be with two zeros, like 1000.00)
* @param string $hash Hash of the order
* @param array &$errors Registry of errors
*
* @return string|null The link to pay for the order, if generated
*/
public static function result(int $identifier, string $cost, string $hash, bool $test = false, array &$errors = []): ?bool
{
try {
// Initializing the settings
$settings = settings::active();
if ($settings instanceof settings) {
// Initialized the settings
if ($settings->acquirings['robokassa']['enabled']) {
// Enabled the robokassa acquiring
// Declaring the robokassa settings
$robokassa = null;
if ($test) {
// Test mode
// Initializing the robokassa settings
$robokassa = require(SETTINGS . DIRECTORY_SEPARATOR . 'acquirings' . DIRECTORY_SEPARATOR . 'robokassa' . DIRECTORY_SEPARATOR . 'test.php');
} else {
// Work mode
// Initializing the robokassa settings
$robokassa = require(SETTINGS . DIRECTORY_SEPARATOR . 'acquirings' . DIRECTORY_SEPARATOR . 'robokassa' . DIRECTORY_SEPARATOR . 'work.php');
}
// Initializing robokassa parameters
$password = $robokassa['passwords'][1];
// Generatings the MD5 hash
$md5 = strtoupper(md5("$cost:$identifier:$password"));
// Exit (success)
return $md5 === $hash;
}
}
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Exit (fail)
return null;
}
/**
* Verify (success)
*
* Generate hash by arguments and verify for success with the hash
*
* @param int $identifier Identifier of the order
* @param string $cost Cost of the order (can be with two zeros, like 1000.00)
* @param string $hash Hash of the order
* @param array &$errors Registry of errors
*
* @return string|null The link to pay for the order, if generated
*/
public static function success(int $identifier, string $cost, string $hash, bool $test = false, array &$errors = []): ?bool
{
try {
// Initializing the settings
$settings = settings::active();
if ($settings instanceof settings) {
// Initialized the settings
if ($settings->acquirings['robokassa']['enabled']) {
// Enabled the robokassa acquiring
// Declaring the robokassa settings
$robokassa = null;
if ($test) {
// Test mode
// Initializing the robokassa settings
$robokassa = require(SETTINGS . DIRECTORY_SEPARATOR . 'acquirings' . DIRECTORY_SEPARATOR . 'robokassa' . DIRECTORY_SEPARATOR . 'test.php');
} else {
// Work mode
// Initializing the robokassa settings
$robokassa = require(SETTINGS . DIRECTORY_SEPARATOR . 'acquirings' . DIRECTORY_SEPARATOR . 'robokassa' . DIRECTORY_SEPARATOR . 'work.php');
}
// Initializing robokassa parameters
$password = $robokassa['passwords'][0];
// Generatings the MD5 hash
$md5 = md5("$cost:$identifier:$password");
// Exit (success)
return $md5 === $hash;
}
}
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Exit (fail)
return null;
}
}

View File

@ -22,7 +22,8 @@ use mirzaev\arangodb\collection,
use ArangoDBClient\Document as _document; use ArangoDBClient\Document as _document;
// Built-in libraries // Built-in libraries
use exception; use DateTime as datetime,
Exception as exception;
/** /**
* Model of cart * Model of cart
@ -184,7 +185,6 @@ final class cart extends core implements document_interface, collection_interfac
return null; return null;
} }
/** /**
* Count * Count
* *
@ -242,7 +242,7 @@ final class cart extends core implements document_interface, collection_interfac
/* /*
* Write * Write
* *
* Write the product in the cart * Write the product into the cart
* *
* @param product $product The product * @param product $product The product
* @param int $amount Amount of writings * @param int $amount Amount of writings
@ -416,25 +416,262 @@ final class cart extends core implements document_interface, collection_interfac
return false; return false;
} }
/* /**
* Order * Account
*
* *
* Search for the connected account
* *
* @param array &$errors Registry of errors * @param array &$errors Registry of errors
* *
* @return void * @return account|null The connected account, if found
*/ */
public function order(array &$errors = []): void public function account(array &$errors = []): ?account
{
try {
if ($result = collection::execute(
<<<'AQL'
FOR v IN 1..1 OUTBOUND @cart GRAPH users
FILTER PARSE_IDENTIFIER(v._id).collection == @account
LIMIT 1
return v
AQL,
[
'cart' => $this->document->getId(),
'account' => account::COLLECTION
],
errors: $errors
)) {
// Found the instance of the account connected to the account
// Initializing the object
$account = new account;
if (method_exists($account, '__document')) {
// Object can implement a document from ArangoDB
// Implementinf parameters
if (isset($result->language)) $result->language = language::{$result->language};
if (isset($result->currency)) $result->currency = currency::{$result->currency};
// Writing the instance of the account document from ArangoDB to the implement object
$account->__document($result);
// Exit (success)
return $account;
}
}
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Exit (fail)
return null;
}
/**
* Cost
*
* Generate the cart products cost
*
* @param string &$list List of the calculated products
* @param array &$errors Registry of errors
*
* @return float|int|null The cart products cost, if generated
*/
public function cost(string &$list = '', array &$errors = []): float|int|null
{
try {
// Initializing the account
$account = $this->account($errors);
if ($account instanceof account) {
// Initialized the account
// Initializing products in the cart
$products = $this->products(language: $account->language ?? language::ru, currency: $account->currency ?? currency::rub);
if (!empty($products)) {
// Initialized products in the cart
// Declaring total cost of products
$cost = 0;
// Initializing iterator of rows
$row = 0;
foreach ($products as $product) {
// Iterating over products
// Generating formatted list of products for message
$list .= ++$row . '. ' . $product['document']['name'] . ' (' . $product['amount'] . 'шт)' . "\n";
// Generating total cost of products
$cost += $product['document']['cost'] * $product['amount'];
}
// Exit (success)
return $cost;
}
}
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Exit (fail)
return null;
}
/*
* Order
*
* Create the order and connect to the cart and to the account
* Make the cart ordered (the account will create a new cart)
*
* @param array &$errors Registry of errors
*
* @return order|null The order, if created
*
* @todo Handling errors
*/
public function order(array &$errors = []): ?order
{ {
try { try {
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) { if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
if (collection::initialize(reservation::COLLECTION, reservation::TYPE, errors: $errors)) {
if (collection::initialize(order::COLLECTION, order::TYPE, errors: $errors)) { if (collection::initialize(order::COLLECTION, order::TYPE, errors: $errors)) {
if (collection::initialize(reservation::COLLECTION, reservation::TYPE, errors: $errors)) {
if (collection::initialize(account::COLLECTION, account::TYPE, errors: $errors)) {
// Initialized collections // Initialized collections
} else throw new exception('Failed to initialize ' . product::TYPE->name . ' collection: ' . product::COLLECTION); // Searching for the order
$existed = collection::execute(
<<<'AQL'
FOR v IN 1..1 OUTBOUND @cart GRAPH users
FILTER PARSE_IDENTIFIER(v._id).collection == @order
LIMIT 1
return v
AQL,
[
'cart' => $this->document->getId(),
'order' => order::COLLECTION
],
errors: $errors
);
if ($existed) {
// The order has already been created
// Initializing the object
$order = new order;
if (method_exists($order, '__document')) {
// Object can implement a document from ArangoDB
// Writing the instance of the order document from ArangoDB to the implement object
$order->__document($existed);
// Exit (success)
return $order;
}
} else {
// The order has not been created
// Initializing a new order
$_id = document::write(
order::COLLECTION,
[
'active' => true,
/* 'term' => (int) new datetime('+15 minutes')->ormat('U') */
]
);
if ($result = collection::execute(
<<<'AQL'
FOR d IN @@collection
FILTER d._id == @_id && d.active == true
RETURN d
AQL,
[
'@collection' => order::COLLECTION,
'_id' => $_id
],
errors: $errors
)) {
// Found the instance of just created the new order
// Writing the ordered status for the cart
$this->document->ordered = true;
if (document::update($this->__document(), errors: $errors)) {
// Writed into ArangoDB
// Initializing the object
$order = new order;
if (method_exists($order, '__document')) {
// Object can implement a document from ArangoDB
// Writing the instance of the order document from ArangoDB to the implement object
$order->__document($result);
// Connecting the cart to the order
$connected = $order->connect($this, $errors);
if ($connected) {
// Connected the cart with the order
if ($result = collection::execute(
<<<'AQL'
FOR v IN 1..1 OUTBOUND @cart GRAPH users
FILTER PARSE_IDENTIFIER(v._id).collection == @account
LIMIT 1
return v
AQL,
[
'cart' => $this->document->getId(),
'account' => account::COLLECTION
],
errors: $errors
)) {
// Found the instance of the account connected to the cart
// Initializing the object
$account = new account;
if (method_exists($account, '__document')) {
// Object can implement a document from ArangoDB
// Writing the instance of the account document from ArangoDB to the implement object
$account->__document($result);
// Connecting the account with the order
$connected = $account->connect($order, $errors);
if ($connected) {
// Connected the account with the order
// Exit (success)
return $order;
}
}
}
}
} else throw new exception('Class ' . order::class . ' does not implement a document from ArangoDB');
} else throw new exception('Failed to write the ordered status for the cart');
} else throw new exception('Failed to create or find just created ' . static::class);
}
} else throw new exception('Failed to initialize ' . account::TYPE->name . ' collection: ' . account::COLLECTION);
} else throw new exception('Failed to initialize ' . reservation::TYPE->name . ' collection: ' . reservation::COLLECTION); } else throw new exception('Failed to initialize ' . reservation::TYPE->name . ' collection: ' . reservation::COLLECTION);
} else throw new exception('Failed to initialize ' . order::TYPE->name . ' collection: ' . order::COLLECTION);
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::COLLECTION); } else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::COLLECTION);
} catch (exception $e) { } catch (exception $e) {
// Writing to the registry of errors // Writing to the registry of errors
@ -445,5 +682,8 @@ final class cart extends core implements document_interface, collection_interfac
'stack' => $e->getTrace() 'stack' => $e->getTrace()
]; ];
} }
// Exit (fail)
return null;
} }
} }

View File

@ -41,7 +41,8 @@ use GdImage as image;
final class catalog extends core final class catalog extends core
{ {
use yandex, files { use yandex, files {
yandex::download as yandex; yandex::download as file;
yandex::list as folder;
} }
/** /**
@ -120,7 +121,7 @@ final class catalog extends core
// Iterate over categories // Iterate over categories
try { try {
if (!empty($row['identifier']) && !empty($row['name'])) { if (!empty($row['identifier']) && is_int($row['identifier']) && $row['identifier'] > 0 && !empty($row['name'])) {
// Required cells are filled in // Required cells are filled in
// Incrementing the counter of loaded categories // Incrementing the counter of loaded categories
@ -195,7 +196,10 @@ final class catalog extends core
// Received images // Received images
// Initializing new images of the category // Initializing new images of the category
$images = explode(' ', mb_trim($row['images'])); $images = static::folder(
uri: explode(' ', mb_trim($row['images']))[0],
errors: $errors
);
// Reinitialize images? (true, if no images found or their amount does not match) // Reinitialize images? (true, if no images found or their amount does not match)
/* $reinitialize = !$category->images || count($category->images) !== count($images); */ /* $reinitialize = !$category->images || count($category->images) !== count($images); */
@ -210,14 +214,14 @@ final class catalog extends core
// Initializing the buffer of images // Initializing the buffer of images
$buffer = []; $buffer = [];
foreach ($images as $index => $file) { foreach (is_array($images) ? $images : [] as $image) {
// Iterating over new images // Iterating over new images
// Skipping empty URI`s // Initializing identifier of the image
if (empty($file = mb_trim($file))) continue; $identifier = preg_replace('/\.\w+$/', '', $image->name);
// Initializing path to directory of images in storage // Initializing path to directory of images in storage
$directory = DIRECTORY_SEPARATOR . 'categories' . DIRECTORY_SEPARATOR . $row['identifier'] . DIRECTORY_SEPARATOR . $index; $directory = DIRECTORY_SEPARATOR . 'categories' . DIRECTORY_SEPARATOR . $row['identifier'] . DIRECTORY_SEPARATOR . $identifier;
// Initializing URL of the image in storage // Initializing URL of the image in storage
$url = STORAGE . $directory; $url = STORAGE . $directory;
@ -225,8 +229,8 @@ final class catalog extends core
// Initializing the directory in storage // Initializing the directory in storage
if (!file_exists($url)) mkdir($url, 0775, true); if (!file_exists($url)) mkdir($url, 0775, true);
if ($downloaded = static::yandex( if ($downloaded = static::file(
uri: $file, uri: $image->public_url ?? $image->public_key,
destination: $url, destination: $url,
name: 'source', name: 'source',
errors: $errors errors: $errors
@ -249,6 +253,11 @@ final class catalog extends core
// Initializing implementator of the image // Initializing implementator of the image
$boba = imagecreatefromjpeg($uri); $boba = imagecreatefromjpeg($uri);
} else if ($downloaded['content'] === content::webp) {
// WEBP
// Initializing implementator of the image
$boba = imagecreatefromwebp($uri);
} }
// Enabling better antialiasing // Enabling better antialiasing
@ -280,10 +289,10 @@ final class catalog extends core
imagecopyresampled($biba, $boba, 0, 0, 0, 0, $width, $height, $size[0], $size[1]); imagecopyresampled($biba, $boba, 0, 0, 0, 0, $width, $height, $size[0], $size[1]);
// Initializing URI of the resized image // Initializing URI of the resized image
$uri = $directory . DIRECTORY_SEPARATOR . "$resize." . $downloaded['content']->extension(); $uri = $directory . DIRECTORY_SEPARATOR . "$resize.webp";
// Saving the image // Saving the image
imagePng($biba, STORAGE . $uri); imagewebp($biba, STORAGE . $uri);
// Writing the resized image to the buffer of resized images // Writing the resized image to the buffer of resized images
$resized[$resize] = $uri; $resized[$resize] = $uri;
@ -291,7 +300,7 @@ final class catalog extends core
// Writing the image to the buffer if images // Writing the image to the buffer if images
$buffer[] = [ $buffer[] = [
'source' => $file, 'source' => $image->public_url ?? null,
'storage' => [ 'storage' => [
'source' => $directory . DIRECTORY_SEPARATOR . $downloaded['name'] . '.' . $downloaded['content']->extension(), 'source' => $directory . DIRECTORY_SEPARATOR . $downloaded['name'] . '.' . $downloaded['content']->extension(),
] + $resized ] + $resized
@ -349,7 +358,7 @@ final class catalog extends core
// Iterate over products // Iterate over products
try { try {
if (!empty($row['identifier']) && !empty($row['name'])) { if (!empty($row['identifier']) && is_int($row['identifier']) && $row['identifier'] > 0 && !empty($row['name'])) {
// Required cells are filled in // Required cells are filled in
// Incrementing the counter of loaded products // Incrementing the counter of loaded products
@ -385,7 +394,7 @@ final class catalog extends core
// Initializing position of the product // Initializing position of the product
if (empty($product->position) || $product->position !== $row['position']) if (empty($product->position) || $product->position !== $row['position'])
$product->position = $row['position']; $product->position = isset($row['position']) ? (int) $row['position'] : 0;
} else { } else {
// Not initialized the product // Not initialized the product
@ -399,7 +408,7 @@ final class catalog extends core
dimensions: ['x' => $row['x'], 'y' => $row['y'], 'z' => $row['z']], dimensions: ['x' => $row['x'], 'y' => $row['y'], 'z' => $row['z']],
brand: [$language->name => $row['brand']], brand: [$language->name => $row['brand']],
compatibility: [$language->name => $row['compatibility']], compatibility: [$language->name => $row['compatibility']],
position: (int) $row['position'] ?? null, position: isset($row['position']) ? (int) $row['position'] : 0,
errors: $errors errors: $errors
); );
@ -436,8 +445,11 @@ final class catalog extends core
if (!empty($row['images'])) { if (!empty($row['images'])) {
// Received images // Received images
// Initializing new images of the category // Initializing new images of the product
$images = explode(' ', mb_trim($row['images'])); $images = static::folder(
uri: explode(' ', mb_trim($row['images']))[0],
errors: $errors
);
// Reinitialize images? (true, if no images found or their amount does not match) // Reinitialize images? (true, if no images found or their amount does not match)
/* $reinitialize = !$product->images || count($product->images) !== count($images); */ /* $reinitialize = !$product->images || count($product->images) !== count($images); */
@ -452,14 +464,14 @@ final class catalog extends core
// Initializing the buffer of images // Initializing the buffer of images
$buffer = []; $buffer = [];
foreach ($images as $index => $file) { foreach (is_array($images) ? $images : [] as $image) {
// Iterating over new images // Iterating over new images
// Skipping empty URI`s // Initializing identifier of the image
if (empty($file = mb_trim($file))) continue; $identifier = preg_replace('/\.\w+$/', '', $image->name);
// Initializing path to directory of images in storage // Initializing path to directory of images in storage
$directory = DIRECTORY_SEPARATOR . 'products' . DIRECTORY_SEPARATOR . $row['identifier'] . DIRECTORY_SEPARATOR . $index; $directory = DIRECTORY_SEPARATOR . 'products' . DIRECTORY_SEPARATOR . $row['identifier'] . DIRECTORY_SEPARATOR . $identifier;
// Initializing URL of the image in storage // Initializing URL of the image in storage
$url = STORAGE . $directory; $url = STORAGE . $directory;
@ -467,8 +479,8 @@ final class catalog extends core
// Initializing the directory in storage // Initializing the directory in storage
if (!file_exists($url)) mkdir($url, 0775, true); if (!file_exists($url)) mkdir($url, 0775, true);
if ($downloaded = static::yandex( if ($downloaded = static::file(
uri: $file, uri: $image->public_url ?? $image->public_key,
destination: $url, destination: $url,
name: 'source', name: 'source',
errors: $errors errors: $errors
@ -491,6 +503,11 @@ final class catalog extends core
// Initializing implementator of the image // Initializing implementator of the image
$boba = imagecreatefromjpeg($uri); $boba = imagecreatefromjpeg($uri);
} else if ($downloaded['content'] === content::webp) {
// WEBP
// Initializing implementator of the image
$boba = imagecreatefromwebp($uri);
} }
// Enabling better antialiasing // Enabling better antialiasing
@ -522,10 +539,10 @@ final class catalog extends core
imagecopyresampled($biba, $boba, 0, 0, 0, 0, $width, $height, $size[0], $size[1]); imagecopyresampled($biba, $boba, 0, 0, 0, 0, $width, $height, $size[0], $size[1]);
// Initializing URI of the resized image // Initializing URI of the resized image
$uri = $directory . DIRECTORY_SEPARATOR . "$resize." . $downloaded['content']->extension(); $uri = $directory . DIRECTORY_SEPARATOR . "$resize.webp";
// Saving the image // Saving the image
imagePng($biba, STORAGE . $uri); imagewebp($biba, STORAGE . $uri);
// Writing the resized image to the buffer of resized images // Writing the resized image to the buffer of resized images
$resized[$resize] = $uri; $resized[$resize] = $uri;
@ -533,7 +550,7 @@ final class catalog extends core
// Writing the image to the buffer if images // Writing the image to the buffer if images
$buffer[] = [ $buffer[] = [
'source' => $file, 'source' => $image->public_url ?? null,
'storage' => [ 'storage' => [
'source' => $directory . DIRECTORY_SEPARATOR . $downloaded['name'] . '.' . $downloaded['content']->extension(), 'source' => $directory . DIRECTORY_SEPARATOR . $downloaded['name'] . '.' . $downloaded['content']->extension(),
] + $resized ] + $resized

View File

@ -116,11 +116,16 @@ final class entry extends core implements document_interface, collection_interfa
if ($result = collection::execute( if ($result = collection::execute(
sprintf( sprintf(
<<<'AQL' <<<'AQL'
FOR d IN @@collection let from = (
FOR ascendant IN OUTBOUND d @@edge FOR e IN @@edge
RETURN DISTINCT e._from
)
FOR d in @@collection
FILTER !POSITION(from, d._id)
RETURN %s RETURN %s
AQL, AQL,
empty($return) ? 'DISTINCT ascendant' : $return empty($return) ? 'DISTINCT d' : $return
), ),
[ [
'@collection' => $descendant::COLLECTION, '@collection' => $descendant::COLLECTION,

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace mirzaev\huesos\models\enumerations;
// Files of the project
use mirzaev\huesos\models\enumerations\language;
/**
* Types of destination
*
* @package mirzaev\huesos\models\enumerations
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
enum destination
{
case office;
case door;
/**
* Label
*
* Initialize label of the destination
*
* @param language|null $language Language into which to translate
*
* @return string Translated label of the destination
*/
public function label(?language $language = language::en): string
{
// Exit (success)
return match ($this) {
destination::office => match ($language) {
language::en => 'Office',
language::ru => 'Пункт выдачи'
},
destination::door => match ($language) {
language::en => 'Door',
language::ru => 'До двери'
}
};
}
}

View File

@ -6,6 +6,8 @@ namespace mirzaev\huesos\models;
// Files of the project // Files of the project
use mirzaev\huesos\models\core, use mirzaev\huesos\models\core,
mirzaev\huesos\models\cart,
mirzaev\huesos\models\account,
mirzaev\huesos\models\reservation, mirzaev\huesos\models\reservation,
mirzaev\huesos\models\traits\document as document_trait, mirzaev\huesos\models\traits\document as document_trait,
mirzaev\huesos\models\interfaces\document as document_interface, mirzaev\huesos\models\interfaces\document as document_interface,
@ -40,4 +42,115 @@ final class order extends core implements document_interface, collection_interfa
* Name of the collection in ArangoDB * Name of the collection in ArangoDB
*/ */
final public const string COLLECTION = 'order'; final public const string COLLECTION = 'order';
/**
* Cart
*
* Search for the connected cart
*
* @param array &$errors Registry of errors
*
* @return cart|null The connected cart, if found
*/
public function cart(array &$errors = []): ?cart
{
try {
if ($result = collection::execute(
<<<'AQL'
FOR v IN 1..1 INBOUND @order GRAPH users
FILTER PARSE_IDENTIFIER(v._id).collection == @cart
LIMIT 1
return v
AQL,
[
'order' => $this->document->getId(),
'cart' => cart::COLLECTION
],
errors: $errors
)) {
// Found the instance of the cart connected to the order
// Initializing the object
$cart = new cart;
if (method_exists($cart, '__document')) {
// Object can implement a document from ArangoDB
// Writing the instance of the cart document from ArangoDB to the implement object
$cart->__document($result);
// Exit (success)
return $cart;
}
}
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Exit (fail)
return null;
}
/**
* Account
*
* Search for the connected account
*
* @param array &$errors Registry of errors
*
* @return account|null The connected account, if found
*/
public function account(array &$errors = []): ?account
{
try {
if ($result = collection::execute(
<<<'AQL'
FOR v IN 1..1 OUTBOUND @order GRAPH users
FILTER PARSE_IDENTIFIER(v._id).collection == @account
LIMIT 1
return v
AQL,
[
'order' => $this->document->getId(),
'account' => account::COLLECTION
],
errors: $errors
)) {
// Found the instance of the account connected to the account
// Initializing the object
$account = new account;
if (method_exists($account, '__document')) {
// Object can implement a document from ArangoDB
// Implementinf parameters
if (isset($result->language)) $result->language = language::{$result->language};
if (isset($result->currency)) $result->currency = currency::{$result->currency};
// Writing the instance of the account document from ArangoDB to the implement object
$account->__document($result);
// Exit (success)
return $account;
}
}
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Exit (fail)
return null;
}
} }

View File

@ -114,7 +114,7 @@ final class session extends core implements document_interface, collection_inter
$session->hash = sodium_bin2hex(sodium_crypto_generichash($_id)); $session->hash = sodium_bin2hex(sodium_crypto_generichash($_id));
if (document::update($session, errors: $errors)) { if (document::update($session, errors: $errors)) {
// Writed to ArangoDB // Writed into ArangoDB
// Writing instance of the session document from ArangoDB to the property of the implementing object // Writing instance of the session document from ArangoDB to the property of the implementing object
$this->__document($session); $this->__document($session);

File diff suppressed because it is too large Load Diff

View File

@ -79,10 +79,12 @@ trait document
/** /**
* Connect * Connect
* *
* Searches for a connection document, otherwise creates one
*
* @param collecton_interface $document Document * @param collecton_interface $document Document
* @param array &$errors Registry of errors * @param array &$errors Registry of errors
* *
* @return string|null The identifier of the created edge of the "connect" collection, if created * @return string|null The identifier of the "connect" edge collection, if created or found
*/ */
public function connect(collection_interface $document, array &$errors = []): ?string public function connect(collection_interface $document, array &$errors = []): ?string
{ {
@ -95,8 +97,31 @@ trait document
if ($this->document instanceof _document) { if ($this->document instanceof _document) {
// Initialized instance of the document from ArangoDB // Initialized instance of the document from ArangoDB
// Writing document and exit (success) // Searching for a connection
return framework_document::write( $found = collection::execute(
<<<'AQL'
FOR d IN @@collection
FILTER d._from == @_from && d._to == @_to && d.active == true
RETURN d
AQL,
[
'@collection' => connect::COLLECTION,
'_from' => $document->getId(),
'_to' => $this->document->getId()
],
errors: $errors
);
if ($found) {
// Found the connection document
// Exit (success)
return $found->getId();
} else {
// Not found the connection document
// Creting the connection document
$created = framework_document::write(
connect::COLLECTION, connect::COLLECTION,
[ [
'_from' => $document->getId(), '_from' => $document->getId(),
@ -104,6 +129,14 @@ trait document
], ],
errors: $errors errors: $errors
); );
if ($created) {
// Created the connection document
// Exit (success)
return $created;
}
}
} else throw new exception('The instance of the document from ArangoDB is not initialized'); } else throw new exception('The instance of the document from ArangoDB is not initialized');
} else throw new exception('Failed to initialize ' . $document::TYPE->name . ' collection: ' . $document::COLLECTION); } else throw new exception('Failed to initialize ' . $document::TYPE->name . ' collection: ' . $document::COLLECTION);
} else throw new exception('Failed to initialize ' . connect::TYPE->name . ' collection: ' . connect::COLLECTION); } else throw new exception('Failed to initialize ' . connect::TYPE->name . ' collection: ' . connect::COLLECTION);

View File

@ -21,14 +21,14 @@ use exception;
trait disk trait disk
{ {
/** /**
* Download file from "Yandex Disk" * Download the file from "Yandex Disk"
* *
* @param string $uri URI of the file from "Yandex Disk" * @param string $uri URI of the file from "Yandex Disk"
* @param string $destination Destination to write the file * @param string $destination Destination to write the file
* @param string|int $name Name for the file * @param string|int $name Name for the file
* @param array &$errors Registry of errors * @param array &$errors Registry of errors
* *
* @return array|false The [destination, name, content] of the downloaded file, if the file was downloaded * @return array|false The [destination, name, content] array of the downloaded file, if the file was downloaded
*/ */
private static function download( private static function download(
string $uri, string $uri,
@ -101,4 +101,52 @@ trait disk
// Exit (fail) // Exit (fail)
return false; return false;
} }
/**
* Initialize list of files inside the folder from "Yandex Disk"
*
* @param string $uri URI of the folder from "Yandex Disk"
* @param array &$errors Registry of errors
*
* @return array|false JSON objects list of files in the folder
*/
private static function list(string $uri, array &$errors = []): array|false
{
try {
if (!empty($uri)) {
// Not empty URI
// Initializing URL of the file
$url = "https://cloud-api.yandex.net/v1/disk/public/resources?public_key=$uri";
// Checking if the folder is available
$session = curl_init($url);
curl_setopt($session, CURLOPT_RETURNTRANSFER, true);
curl_exec($session);
$code = curl_getinfo($session, CURLINFO_RESPONSE_CODE);
curl_close($session);
if ($code === 200) {
// The folder is available
// Downloading the list of files in the folder
$files = json_decode(file_get_contents($url));
// Exit (success)
return isset($files?->_embedded?->items) ? $files->_embedded->items : [$files];
} else throw new exception("Failed to download list of files inside the folder by link: $uri");
} else throw new exception("Empty URI");
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Exit (fail)
return false;
}
} }

View File

@ -25,6 +25,8 @@ define('THEME', 'default');
define('CDEK', require(SETTINGS . DIRECTORY_SEPARATOR . 'deliveries' . DIRECTORY_SEPARATOR . 'cdek.php')); define('CDEK', require(SETTINGS . DIRECTORY_SEPARATOR . 'deliveries' . DIRECTORY_SEPARATOR . 'cdek.php'));
define('TELEGRAM_KEY', require(SETTINGS . DIRECTORY_SEPARATOR . 'telegram.php'));
// Initialize dependencies // Initialize dependencies
require ROOT . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php'; require ROOT . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php';
@ -38,9 +40,12 @@ $core->router
->write('/cart', new route('cart', 'index', 'cart'), 'GET') ->write('/cart', new route('cart', 'index', 'cart'), 'GET')
->write('/cart/product', new route('cart', 'product', 'cart'), 'PATCH') ->write('/cart/product', new route('cart', 'product', 'cart'), 'PATCH')
->write('/cart/summary', new route('cart', 'summary', 'cart'), 'GET') ->write('/cart/summary', new route('cart', 'summary', 'cart'), 'GET')
/* ->write('/cart/share', new route('cart', 'share', 'cart'), 'POST') */
->write('/cart/share', new route('cart', 'share', 'cart'), 'POST') ->write('/cart/share', new route('cart', 'share', 'cart'), 'POST')
->write('/cart/attach', new route('cart', 'attach', 'cart'), 'POST')
->write('/order/robokassa', new route('cart', 'robokassa', 'cart'), 'GET') ->write('/order/robokassa', new route('cart', 'robokassa', 'cart'), 'GET')
->write('/api/robokassa/result', new route('api\acquirings\robokassa', 'result'), 'POST')
->write('/robokassa/success', new route('api\acquirings\robokassa', 'success'), 'GET')
->write('/robokassa/fail', new route('api\acquirings\robokassa', 'fail'), 'GET')
->write('/account/write', new route('account', 'write', 'account'), 'PATCH') ->write('/account/write', new route('account', 'write', 'account'), 'PATCH')
->write('/session/write', new route('session', 'write', 'session'), 'PATCH') ->write('/session/write', new route('session', 'write', 'session'), 'PATCH')
->write('/session/connect/telegram', new route('session', 'telegram', 'session'), 'PUT') ->write('/session/connect/telegram', new route('session', 'telegram', 'session'), 'PUT')

View File

@ -0,0 +1,28 @@
// Initializing the closing element
const closing = document.getElementById("closing");
console.log(closing);
// Initializing the closing iterator
let iterator =
parseInt(closing.style.getPropertyValue("--iterator").replaceAll("'", "")) ||
0;
console.log(iterator);
// Initializing the closing inteval
const interval = setInterval(function () {
if (iterator-- <= 0) {
// Deinitializing the closing inteval
clearInterval(interval);
core.modules.connect("telegram").then(() => {
// Imported the telegram module
// Closing the window
core.telegram.api.close();
});
// Exit (success)
return;
}
// Writing the iterator into the closing element
closing.style.setProperty("--iterator", "'" + iterator + "'");
}, 1000);

View File

@ -17,11 +17,11 @@ class core {
// Window // Window
static window; static window;
// The "loading" element // Account
static status_loading = document.getElementById("loading"); static account;
// The "account" element // The "loading" element
static status_account = document.getElementById("account"); static loading = document.getElementById("loading");
// The <header> element // The <header> element
static header = document.body.getElementsByTagName("header")[0]; static header = document.body.getElementsByTagName("header")[0];

View File

@ -30,10 +30,10 @@ export default class account {
* @return {void} * @return {void}
*/ */
static authentication() { static authentication() {
core.status_loading.removeAttribute("disabled"); core.loading.removeAttribute("disabled");
const timer_for_response = setTimeout(() => { const timer_for_response = setTimeout(() => {
core.status_loading.setAttribute("disabled", true); core.loading.setAttribute("disabled", true);
}, 200); }, 200);
core.modules.connect("telegram").then(() => { core.modules.connect("telegram").then(() => {
@ -44,7 +44,7 @@ export default class account {
.request( .request(
"/session/connect/telegram", "/session/connect/telegram",
core.telegram.api.initData, core.telegram.api.initData,
'PUT' "PUT",
) )
.then((json) => { .then((json) => {
if (json) { if (json) {
@ -63,20 +63,66 @@ export default class account {
// Success (not received errors) // Success (not received errors)
if (json.connected === true) { if (json.connected === true) {
core.status_loading.setAttribute("disabled", true); // Deactivating the loading screen
core.loading.setAttribute("disabled", true);
clearTimeout(timer_for_response); clearTimeout(timer_for_response);
const a = core.status_account.getElementsByTagName("a")[0]; core.account = {
identifier: json.identifier
};
// Initializing the account element
const account = document.getElementById("account");
if (account instanceof HTMLElement) {
// Initialized the account element
// Initializing the account link
const a = account.getElementsByTagName("a")[0];
if (a instanceof HTMLElement) {
// Initialized the account link
a.setAttribute("onclick", "core.account.profile()"); a.setAttribute("onclick", "core.account.profile()");
a.innerText = json.domain.length > 0 a.innerText = json.domain.length > 0
? "@" + json.domain ? "@" + json.domain
: "ERROR"; : "ERROR";
} else {
// Not initialized the account link
if (json.avatar) {
// Received the avatar image
// Initialize the menu button icon
const icon = account.getElementsByTagName("i")[0];
if (icon instanceof HTMLElement) {
// Initialized the menu button icon
setTimeout(function () {
// Hiding the menu button icon
icon.classList.add("hidden");
}, 3000);
}
// Initializing the avatar image element
const image = document.createElement("img");
image.setAttribute("src", json.avatar);
image.style.setProperty("opacity", "0");
image.style.setProperty("--animation-delay", "2s");
image.classList.add("opacity", "animated");
// Writing the avatar image element
account.appendChild(image);
}
}
}
} }
if ( if (
json.language !== null && json.language !== null &&
typeof json.language === "string" && typeof json.language === "string" &&
json.langiage.length === 2 json.language.length === 2
) { ) {
core.language = json.language; core.language = json.language;
} }

View File

@ -69,7 +69,7 @@ export default class cart {
* @name Write (interface) * @name Write (interface)
* *
* @description * @description
* Write the product in the cart * Write the product into the cart
* *
* @param {HTMLButtonElement|HTMLInputElement|null} element Handler elememnt of the product * @param {HTMLButtonElement|HTMLInputElement|null} element Handler elememnt of the product
* @param {HTMLElement} product The product element * @param {HTMLElement} product The product element
@ -388,7 +388,7 @@ export default class cart {
static async share(button) { static async share(button) {
return await core.modules.connect("telegram").then( return await core.modules.connect("telegram").then(
() => { () => {
// Imported the damper module // Imported the telegram module
// Disabling button // Disabling button
button?.setAttribute("disabled", true); button?.setAttribute("disabled", true);
@ -771,6 +771,24 @@ Object.assign(
} else { } else {
// Success (not received errors) // Success (not received errors)
if (json.amount > 0) {
// The cart has products
// Writing the CSS variable of the document element
document.documentElement.style.setProperty(
"--cart-amount",
'"' + json.amount + '"',
);
} else {
// The cart has no products
// Writing the CSS variable of the document element
document.documentElement.style.setProperty(
"--cart-amount",
"unset",
);
}
// Initializing the summary amount <span> element // Initializing the summary amount <span> element
const amount = document.getElementById("amount"); const amount = document.getElementById("amount");
@ -844,7 +862,7 @@ Object.assign(
try { try {
// Request // Request
return await core.request("/cart/share", undefined, "POST") return await core.request("/cart/share", undefined, "POST")
.then((json) => { .then(async (json) => {
if (json) { if (json) {
// Received a JSON-response // Received a JSON-response
@ -863,13 +881,43 @@ Object.assign(
if (json.share) { if (json.share) {
// Received sharing hash // Received sharing hash
// Request to the chat-robot // Sending the request to the chat-robot
core.telegram.api.sendData( const sended = core.telegram.api.sendData(
JSON.stringify({ JSON.stringify({
type: "cart_share", type: "cart_share",
hash: json.share, hash: json.share,
}), }),
); );
if (sended === undefined) {
// Failed to send the request
if (core.account.identifier > 0) {
// Initialized the account identifier
// Request
return await core.request(
"/cart/attach",
'share=' + json.share,
"POST",
).then((json) => {
if (json) {
// Received a JSON-response
if (json.success) {
// Received the success status
core.modules.connect("telegram").then(() => {
// Imported the telegram module
// Closing the Telegram Mini App
core.telegram.api.close();
});
}
}
});
}
}
} }
/* if (json.robokassa) { /* if (json.robokassa) {

View File

@ -415,13 +415,16 @@ Object.assign(
// Initializing the search <search> element // Initializing the search <search> element
const search = document.getElementById("search"); const search = document.getElementById("search");
if (search instanceof HTMLElement) { if (
// Found the search <search> element search instanceof HTMLElement &&
search.parentElement?.getAttribute("id") !== "top"
) {
// Found the search <search> element in the <main> element and it is not fixed to the top
// Writing into the search <search> element // Writing into the search <search> element in the <main> element
search.outerHTML = json.search; search.outerHTML = json.search;
} else { } else {
// Not found the search <search> element // Not found the search <search> element in the <main> element
// Initialize the search <search> element // Initialize the search <search> element
const search = document.createElement("search"); const search = document.createElement("search");
@ -503,8 +506,11 @@ Object.assign(
// Initializing the search <search> element // Initializing the search <search> element
const search = document.getElementById("search"); const search = document.getElementById("search");
if (search instanceof HTMLElement) { if (
// Initialized the search <search> element in the <main> element search instanceof HTMLElement &&
search.parentElement?.getAttribute("id") !== "top"
) {
// Initialized the search <search> element in the <main> element and it is not fixed to the top
// Writing the categories <section> element after the search <search> element in the <main> element // Writing the categories <section> element after the search <search> element in the <main> element
core.main.insertBefore( core.main.insertBefore(
@ -604,9 +610,10 @@ Object.assign(
const search = document.getElementById("search"); const search = document.getElementById("search");
if ( if (
search instanceof HTMLElement search instanceof HTMLElement &&
search.parentElement?.getAttribute("id") !== "top"
) { ) {
// Initialized the search <search> element in the <main> element // Initialized the search <search> element in the <main> element and it is not fixed to the top
// Writing the filters <section> element after the search <search> element in the <main> element // Writing the filters <section> element after the search <search> element in the <main> element
core.main.insertBefore( core.main.insertBefore(
@ -721,9 +728,10 @@ Object.assign(
const search = document.getElementById("search"); const search = document.getElementById("search");
if ( if (
search instanceof HTMLElement search instanceof HTMLElement &&
search.parentElement?.getAttribute("id") !== "top"
) { ) {
// Initialized the search <search> element in the <main> element // Initialized the search <search> element in the <main> element and it is not fixed to the top
// Writing the sorting <section> element after the search <search> element in the <main> element // Writing the sorting <section> element after the search <search> element in the <main> element
core.main.insertBefore( core.main.insertBefore(
@ -855,9 +863,11 @@ Object.assign(
const search = document.getElementById("search"); const search = document.getElementById("search");
if ( if (
search instanceof HTMLElement search instanceof HTMLElement &&
search.parentElement?.getAttribute("id") !==
"top"
) { ) {
// Initialized the search <search> element in the <main> element // Initialized the search <search> element in the <main> element and it is not fixed to the top
// Writing the products <section> element after the search <search> element in the <main> element // Writing the products <section> element after the search <search> element in the <main> element
core.main.insertBefore( core.main.insertBefore(
@ -1035,13 +1045,16 @@ Object.assign(
// Initializing the search <search> element // Initializing the search <search> element
const search = document.getElementById("search"); const search = document.getElementById("search");
if (search instanceof HTMLElement) { if (
// Found the search <search> element search instanceof HTMLElement &&
search.parentElement?.getAttribute("id") !== "top"
) {
// Found the search <search> element int the <main> element and it is not fixed to the top
// Writing into the search <search> element // Writing into the search <search> element in the <main> element
search.outerHTML = json.search; search.outerHTML = json.search;
} else { } else {
// Not found the search <search> element // Not found the search <search> element in the <main> element
// Initialize the search <search> element // Initialize the search <search> element
const search = document.createElement("search"); const search = document.createElement("search");
@ -1123,8 +1136,11 @@ Object.assign(
// Initializing the search <search> element // Initializing the search <search> element
const search = document.getElementById("search"); const search = document.getElementById("search");
if (search instanceof HTMLElement) { if (
// Initialized the search <search> element in the <main> element search instanceof HTMLElement &&
search.parentElement?.getAttribute("id") !== "top"
) {
// Initialized the search <search> element in the <main> element and it is not fixed to the top
// Writing the categories <section> element after the search <search> element in the <main> element // Writing the categories <section> element after the search <search> element in the <main> element
core.main.insertBefore( core.main.insertBefore(
@ -1224,9 +1240,10 @@ Object.assign(
const search = document.getElementById("search"); const search = document.getElementById("search");
if ( if (
search instanceof HTMLElement search instanceof HTMLElement &&
search.parentElement?.getAttribute("id") !== "top"
) { ) {
// Initialized the search <search> element in the <main> element // Initialized the search <search> element in the <main> element and it is not fixed to the top
// Writing the filters <section> element after the search <search> element in the <main> element // Writing the filters <section> element after the search <search> element in the <main> element
core.main.insertBefore( core.main.insertBefore(
@ -1341,9 +1358,10 @@ Object.assign(
const search = document.getElementById("search"); const search = document.getElementById("search");
if ( if (
search instanceof HTMLElement search instanceof HTMLElement &&
search.parentElement?.getAttribute("id") !== "top"
) { ) {
// Initialized the search <search> element in the <main> element // Initialized the search <search> element in the <main> element and it is not fixed to the top
// Writing the sorting <section> element after the search <search> element in the <main> element // Writing the sorting <section> element after the search <search> element in the <main> element
core.main.insertBefore( core.main.insertBefore(
@ -1475,9 +1493,10 @@ Object.assign(
const search = document.getElementById("search"); const search = document.getElementById("search");
if ( if (
search instanceof HTMLElement search instanceof HTMLElement &&
search.parentElement?.getAttribute("id") !== "top"
) { ) {
// Initialized the search <search> element in the <main> element // Initialized the search <search> element in the <main> element and it is not fixed to the top
// Writing the products <section> element after the search <search> element in the <main> element // Writing the products <section> element after the search <search> element in the <main> element
core.main.insertBefore( core.main.insertBefore(
@ -1730,6 +1749,7 @@ Object.assign(
if (button) { if (button) {
core.telegram.api.MainButton.hide(); core.telegram.api.MainButton.hide();
// core.telegram.api.SecondaryButton.hide();
} }
setTimeout(() => { setTimeout(() => {
@ -1798,6 +1818,7 @@ Object.assign(
if (button) { if (button) {
core.telegram.api.MainButton.show(); core.telegram.api.MainButton.show();
// core.telegram.api.SecondaryButton.show();
} }
images.removeEventListener("mouseup", _close); images.removeEventListener("mouseup", _close);
@ -1895,6 +1916,11 @@ Object.assign(
// Reinitialize parameter // Reinitialize parameter
core.window = document.getElementById("window"); core.window = document.getElementById("window");
// Go to the cart
const to_the_cart = () => {
window.location = "/cart";
};
// Write // Write
const add = () => { const add = () => {
core.cart.write( core.cart.write(
@ -1907,7 +1933,7 @@ Object.assign(
core.telegram.api.MainButton core.telegram.api.MainButton
.setText(json.product.cart.text.added) .setText(json.product.cart.text.added)
.setParams({ .setParams({
color: "#90be36", color: getComputedStyle(document.body).getPropertyValue('--product-button-cart-added-background') || core.telegram.api.themeParams.button_color,
has_shine_effect: true, has_shine_effect: true,
}) })
.offClick(add) .offClick(add)
@ -1927,7 +1953,7 @@ Object.assign(
core.telegram.api.MainButton core.telegram.api.MainButton
.setText(json.product.cart.text.add) .setText(json.product.cart.text.add)
.setParams({ .setParams({
color: core.telegram.api.themeParams.button_color, color: getComputedStyle(document.body).getPropertyValue('--product-button-cart-background') || core.telegram.api.themeParams.button_color,
// has_shine_effect: json.product.discount > 0, // has_shine_effect: json.product.discount > 0,
has_shine_effect: false, has_shine_effect: false,
}) })
@ -1940,7 +1966,7 @@ Object.assign(
core.telegram.api.MainButton core.telegram.api.MainButton
.setText(json.product.cart.text.added) .setText(json.product.cart.text.added)
.setParams({ .setParams({
color: "#90be36", color: getComputedStyle(document.body).getPropertyValue('--product-button-cart-added-background') || core.telegram.api.themeParams.button_color,
has_shine_effect: true, has_shine_effect: true,
}) })
.onClick(added) .onClick(added)
@ -1952,7 +1978,7 @@ Object.assign(
core.telegram.api.MainButton core.telegram.api.MainButton
.setText(json.product.cart.text.add) .setText(json.product.cart.text.add)
.setParams({ .setParams({
color: core.telegram.api.themeParams.button_color, color: getComputedStyle(document.body).getPropertyValue('--product-button-cart-background') || core.telegram.api.themeParams.button_color,
// has_shine_effect: json.product.discount > 0, // has_shine_effect: json.product.discount > 0,
has_shine_effect: false, has_shine_effect: false,
}) })
@ -1962,6 +1988,19 @@ Object.assign(
.show(); .show();
} }
/* core.telegram.api.SecondaryButton
.setText(json.product.cart.text.cart)
.setParams({
position: "bottom",
text_color: core.telegram.api.themeParams.text_color,
color:
core.telegram.api.themeParams.secondary_bg_color,
})
.onClick(to_the_cart)
.hideProgress()
.enable()
.show(); */
// блокировка закрытия карточки // блокировка закрытия карточки
let from; let from;
const _from = (event) => (from = event.target); const _from = (event) => (from = event.target);
@ -2026,6 +2065,12 @@ Object.assign(
.offClick(added) .offClick(added)
.disable() .disable()
.hide(); .hide();
// Deinitializin the "Secondary Button" of the "Web App" window
/* core.telegram.api.SecondaryButton
.offClick(to_the_cart)
.disable()
.hide(); */
}; };
const close = (event) => { const close = (event) => {

View File

@ -5,7 +5,7 @@ core.modules.connect(["telegram"])
// Imported the telegram module // Imported the telegram module
// Expanding the "Web App" window // Expanding the "Web App" window
// core.telegram.api.expand(); core.telegram.api.expand();
// Writing settings for the "Web App" window // Writing settings for the "Web App" window
core.telegram.api.enableVerticalSwipes(); core.telegram.api.enableVerticalSwipes();

View File

@ -11,9 +11,9 @@ use mirzaev\huesos\controllers\core as controller,
mirzaev\huesos\models\telegram; mirzaev\huesos\models\telegram;
// Framework for Telegram // Framework for Telegram
use Zanzara\Zanzara, use Zanzara\Zanzara as zanzara,
Zanzara\Context, Zanzara\Context as context,
Zanzara\Config; Zanzara\Config as config;
// Framework for ArangoDB // Framework for ArangoDB
use mirzaev\arangodb\document; use mirzaev\arangodb\document;
@ -34,6 +34,9 @@ define('STORAGE', __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '
// Файл в формате xlsx с примером excel-документа для импорта каталога // Файл в формате xlsx с примером excel-документа для импорта каталога
define('CATALOG_EXAMPLE', STORAGE . DIRECTORY_SEPARATOR . 'example.xlsx'); define('CATALOG_EXAMPLE', STORAGE . DIRECTORY_SEPARATOR . 'example.xlsx');
// Файл
define('GREETING_VIDEO', STORAGE . DIRECTORY_SEPARATOR . 'greeting.mp4');
// Файл в формате xlsx для импорта каталога // Файл в формате xlsx для импорта каталога
define('CATALOG_IMPORT', STORAGE . DIRECTORY_SEPARATOR . 'import.xlsx'); define('CATALOG_IMPORT', STORAGE . DIRECTORY_SEPARATOR . 'import.xlsx');
@ -59,15 +62,15 @@ require __DIR__ . DIRECTORY_SEPARATOR
// Инициализация ядра моделей MINIMAL // Инициализация ядра моделей MINIMAL
new model(true); new model(true);
$config = new Config(); $config = new config();
$config->setParseMode(Config::PARSE_MODE_MARKDOWN); $config->setParseMode(config::PARSE_MODE_MARKDOWN);
$config->useReactFileSystem(true); $config->useReactFileSystem(true);
$bot = new Zanzara(TELEGRAM_KEY, $config); $robot = new zanzara(TELEGRAM_KEY, $config);
$bot->onUpdate(function (Context $ctx): void { $robot->onUpdate(function (context $context): void {
// Initializing the message // Initializing the message
$message = $ctx->getMessage(); $message = $context->getMessage();
// Initializing the "web app" data // Initializing the "web app" data
$app = $message?->getWebAppData(); $app = $message?->getWebAppData();
@ -82,13 +85,13 @@ $bot->onUpdate(function (Context $ctx): void {
// Cart attaching // Cart attaching
// Attaching cart to the Telegram account // Attaching cart to the Telegram account
telegram::cart_attach($ctx, $request->hash); telegram::cart_attach($context, $request->hash);
} }
} else { } else {
// Not initialized the "web app" data // Not initialized the "web app" data
// Initializing account // Initializing account
$account = $ctx->get('account'); $account = $context->get('account');
if ($account) { if ($account) {
// Initialized the account // Initialized the account
@ -117,7 +120,9 @@ $bot->onUpdate(function (Context $ctx): void {
if (document::update($account->__document())) { if (document::update($account->__document())) {
// Writed the account instance into the ArangoDB document // Writed the account instance into the ArangoDB document
$ctx->sendMessage( /*
// Sending the message
$context->sendMessage(
<<<TXT <<<TXT
*SIM\-номер зарегистрирован:* $sanitized *SIM\-номер зарегистрирован:* $sanitized
TXT, TXT,
@ -126,17 +131,34 @@ $bot->onUpdate(function (Context $ctx): void {
'remove_keyboard' => true 'remove_keyboard' => true
] ]
] ]
)->then(function ($message) use ($ctx) { )->then(function ($message) use ($context) {
// Sended message // Sended message
// Sending the account parameters menu // Sending the account parameters menu
telegram::account_parameters($ctx); telegram::account_parameters($context);
}); */
// Sending the message
$context->sendMessage(
<<<TXT
*Номер телефона зарегистрирован:* $sanitized
TXT,
[
'reply_markup' => [
'remove_keyboard' => true
]
]
)->then(function ($message) use ($context) {
// Sended message
// Sending the account parameters menu
telegram::account_parameters($context);
}); });
} else { } else {
// Not writed the account instance into the ArangoDB document // Not writed the account instance into the ArangoDB document
// Sending the message // Sending the message
$ctx->sendMessage('⚠️ *Не удалось записать SIM\-номер*'); $context->sendMessage('⚠️ *Не удалось записать SIM\-номер*');
} }
} }
} }
@ -147,42 +169,71 @@ $bot->onUpdate(function (Context $ctx): void {
unset($app); unset($app);
}); });
$bot->onCommand('start', fn($ctx) => telegram::start($ctx)); $robot->onCommand('start', fn(context $context) => telegram::start($context));
$bot->onCommand('contacts', fn($ctx) => telegram::contacts($ctx)); $robot->onCommand('contacts', fn(context $context) => telegram::contacts($context));
$bot->onCommand('company', fn($ctx) => telegram::company($ctx)); $robot->onCommand('company', fn(context $context) => telegram::company($context));
$bot->onCommand('community', fn($ctx) => telegram::community($ctx)); $robot->onCommand('community', fn(context $context) => telegram::community($context));
$bot->onCommand('settings', fn($ctx) => telegram::settings($ctx)); $robot->onCommand('settings', fn(context $context) => telegram::settings($context));
$bot->onText('💬 Контакты', fn($ctx) => telegram::contacts($ctx)); $robot->onText('💬 Контакты', fn(context $context) => telegram::contacts($context));
$bot->onText('🏛️ О компании', fn($ctx) => telegram::company($ctx)); $robot->onText('🏛️ О компании', fn(context $context) => telegram::company($context));
$bot->onText('🎯 Сообщество', fn($ctx) => telegram::community($ctx)); $robot->onText('🎯 Сообщество', fn(context $context) => telegram::community($context));
$bot->onText('⚙️ Настройки', fn($ctx) => telegram::settings($ctx)); $robot->onText('⚙️ Настройки', fn(context $context) => telegram::settings($context));
$bot->onCbQueryData(['mail'], fn($ctx) => telegram::_mail($ctx)); $robot->onCbQueryData(['contacts'], fn(context $context) => telegram::contacts($context));
$bot->onCbQueryData(['import_request'], fn($ctx) => telegram::import_request($ctx)); $robot->onCbQueryData(['company'], fn(context $context) => telegram::company($context));
$bot->onCbQueryData(['order'], fn($ctx) => telegram::order($ctx)); $robot->onCbQueryData(['community'], fn(context $context) => telegram::community($context));
$bot->onCbQueryData(['tuning'], fn($ctx) => telegram::tuning($ctx)); $robot->onCbQueryData(['settings'], fn(context $context) => telegram::settings($context));
$bot->onCbQueryData(['brands'], fn($ctx) => telegram::brands($ctx));
$bot->onCbQueryData(['receiver_sim_choose'], fn($ctx) => telegram::receiver_sim_choose($ctx)); $robot->onCbQueryData(['mail'], fn(context $context) => telegram::_mail($context));
$bot->onCbQueryData(['receiver_sim_request'], fn($ctx) => telegram::receiver_sim_request($ctx)); $robot->onCbQueryData(['import_request'], fn(context $context) => telegram::import_request($context));
$bot->onCbQueryData(['receiver_sim_input'], fn($ctx) => telegram::receiver_sim_input($ctx));
$bot->onCbQueryData(['receiver_sim_write'], fn($ctx) => telegram::receiver_sim_write($ctx));
$bot->onCbQueryData(['receiver_name_choose'], fn($ctx) => telegram::receiver_name_choose($ctx)); $robot->onCbQueryData(['order_commentary_request'], fn(context $context) => telegram::order_commentary_request($context));
$bot->onCbQueryData(['receiver_name_request'], fn($ctx) => telegram::receiver_name_request($ctx)); $robot->onCbQueryData(['order'], fn(context $context) => telegram::order($context));
$bot->onCbQueryData(['receiver_name_input'], fn($ctx) => telegram::receiver_name_input($ctx));
$bot->onCbQueryData(['receiver_name_write'], fn($ctx) => telegram::receiver_name_write($ctx));
$bot->onException(function (Context $ctx, $exception) { $robot->onCbQueryData(['tuning'], fn(context $context) => telegram::tuning($context));
$robot->onCbQueryData(['brands'], fn(context $context) => telegram::brands($context));
$robot->onCbQueryData(['cart_delivery'], fn(context $context) => telegram::cart_delivery($context));
$robot->onCbQueryData(['delivery_registration_destination_office'], fn(context $context) => telegram::delivery_registration_destination_office($context));
$robot->onCbQueryData(['delivery_registration_destination_door'], fn(context $context) => telegram::delivery_registration_destination_door($context));
$robot->onCbQueryData(['delivery_registration_sim_input'], fn(context $context) => telegram::delivery_registration_sim_input($context));
$robot->onCbQueryData(['delivery_registration_name_input'], fn(context $context) => telegram::delivery_registration_name_input($context));
$robot->onCbQueryData(['delivery_registration_address_input'], fn(context $context) => telegram::delivery_registration_address_input($context));
$robot->onCbQueryData(['delivery_registration'], fn(context $context) => telegram::delivery_registration($context));
$robot->onCbQueryData(['account_parameters_force'], fn(context $context) => telegram::account_parameters($context, force: true));
$robot->onCbQueryData(['receiver_sim_choose'], fn(context $context) => telegram::receiver_sim_choose($context));
$robot->onCbQueryData(['receiver_sim_request'], fn(context $context) => telegram::receiver_sim_request($context));
/* $robot->onCbQueryData(['receiver_sim_input'], fn(context $context) => telegram::receiver_sim_input($context)); */
$robot->onCbQueryData(['receiver_sim_write'], fn(context $context) => telegram::receiver_sim_write($context));
$robot->onCbQueryData(['receiver_name_choose'], fn(context $context) => telegram::receiver_name_choose($context));
$robot->onCbQueryData(['receiver_name_request'], fn(context $context) => telegram::receiver_name_request($context));
/* $robot->onCbQueryData(['receiver_name_input'], fn(context $context) => telegram::receiver_name_input($context)); */
$robot->onCbQueryData(['receiver_name_write'], fn(context $context) => telegram::receiver_name_write($context));
$robot->onCbQueryData(['receiver_destination_choose'], fn(context $context) => telegram::receiver_destination_choose($context));
$robot->onCbQueryData(['receiver_destination_office'], fn(context $context) => telegram::receiver_destination_office($context));
$robot->onCbQueryData(['receiver_destination_door'], fn(context $context) => telegram::receiver_destination_door($context));
$robot->onCbQueryData(['receiver_address_choose'], fn(context $context) => telegram::receiver_address_choose($context));
$robot->onCbQueryData(['receiver_address_request_office'], fn(context $context) => telegram::receiver_address_request($context, office: true));
$robot->onCbQueryData(['receiver_address_request_door'], fn(context $context) => telegram::receiver_address_request($context, office: false));
/* $robot->onCbQueryData(['receiver_address_input'], fn(context $context) => telegram::receiver_address_input($context)); */
$robot->onCbQueryData(['receiver_address_write'], fn(context $context) => telegram::receiver_address_write($context));
$robot->onException(function (Context $context, $exception) {
var_dump($exception); var_dump($exception);
}); });
// Инициализация middleware с обработкой аккаунта // Инициализация middleware с обработкой аккаунта
$bot->middleware([telegram::class, "account"]); $robot->middleware([telegram::class, "account"]);
// Инициализация middleware с обработкой технических работ разных уровней // Инициализация middleware с обработкой технических работ разных уровней
$bot->middleware([telegram::class, "suspension"]); $robot->middleware([telegram::class, "suspension"]);
// Запуск чат-робота // Запуск чат-робота
$bot->run(); $robot->run();

View File

@ -9,3 +9,11 @@ section#account {
display: flex; display: flex;
} }
nav#menu>a#account>img {
position: absolute;
width: auto;
height: inherit;
object-fit: contain;
border-radius: 100%;
border: 2px solid var(--tg-theme-bottom-bar-bg-color);
}

View File

@ -0,0 +1,40 @@
@charset "UTF-8";
main {
--offset-bottom: 2rem;
justify-content: center;
gap: unset;
}
main>:is(:first-child, :last-child) {
margin-top: auto;
}
main>h2 {
margin: unset;
margin-bottom: 0.5rem;
font-size: 1.2rem;
color: var(--tg-theme-accent-text-color);
}
main>h2+small {
margin-bottom: calc(0 - var(--offset-bottom));
color: var(--tg-theme-subtitle-text-color);
}
main>#closing {
display: flex;
align-items: center;
font-size: 0.8rem;
margin-bottom: var(--offset-bottom);
}
main>#closing>i.icon:first-child {
margin-right: 1rem;
}
main>#closing:after {
content: ': ' var(--iterator);
font-weight: bold;
color: var(--tg-theme-accent-text-color);
}

View File

@ -0,0 +1,40 @@
@charset "UTF-8";
main {
--offset-bottom: 2rem;
justify-content: center;
gap: unset;
}
main>:is(:first-child, :last-child) {
margin-top: auto;
}
main>h2 {
margin: unset;
margin-bottom: 0.5rem;
font-size: 1.2rem;
color: var(--tg-theme-accent-text-color);
}
main>h2+small {
margin-bottom: calc(0 - var(--offset-bottom));
color: var(--tg-theme-subtitle-text-color);
}
main>#closing {
display: flex;
align-items: center;
font-size: 0.8rem;
margin-bottom: var(--offset-bottom);
}
main>#closing>i.icon:first-child {
margin-right: 1rem;
}
main>#closing:after {
content: ': ' var(--iterator);
font-weight: bold;
color: var(--tg-theme-accent-text-color);
}

View File

@ -1,5 +1,24 @@
@charset "UTF-8"; @charset "UTF-8";
@keyframes slide-down {
0% {
transform: translate(0, -100%);
clip-path: polygon(0% 100%, 100% 100%, 100% 200%, 0% 200%);
}
100% {
transform: translate(0, 0%);
clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%);
}
}
.slide.down.animated {
animation-duration: var(--animation-duration, 0.25s);
animation-name: slide-down;
animation-fill-mode: forwards;
animation-timing-function: var(--animatiom-timing, ease-out);
}
@keyframes slide-down-revert { @keyframes slide-down-revert {
0% { 0% {
transform: translate(0, 0%); transform: translate(0, 0%);
@ -13,8 +32,26 @@
} }
.slide.down.revert.animated { .slide.down.revert.animated {
animation-duration: var(--animation-duration, 0.2s); animation-duration: var(--animation-duration, 0.25s);
animation-name: slide-down-revert; animation-name: slide-down-revert;
animation-fill-mode: forwards; animation-fill-mode: forwards;
animation-timing-function: cubic-bezier(1, 0, 1, 1); animation-timing-function: var(--animatiom-timing, ease-in);
}
@keyframes opacity {
0% {
opacity: 0%;
}
100% {
opacity: 100%;
}
}
.opacity.animated {
animation-duration: var(--animation-duration, 0.25s);
animation-name: opacity;
animation-fill-mode: forwards;
animation-timing-function: var(--animatiom-timing, ease-in);
animation-delay: var(--animation-delay, 0s);
} }

View File

@ -7,6 +7,10 @@ main>section:is(#summary, #products, #delivery, #delivery_request) {
overflow: hidden; overflow: hidden;
} }
main:not(:has(h2#title + section#delivery))>h2#title {
margin-bottom: 1rem;
}
main>section:is(#summary, #delivery) { main>section:is(#summary, #delivery) {
background-color: var(--tg-theme-section-bg-color); background-color: var(--tg-theme-section-bg-color);
} }
@ -245,12 +249,13 @@ main>section#products>article.product>a {
} }
main>section#products>article.product>a>img:first-of-type { main>section#products>article.product>a>img:first-of-type {
/* width: 5rem; */ width: 7rem;
min-width: 5rem; /* min-width: 5rem; */
min-height: 100%; min-height: 100%;
object-fit: cover; object-fit: cover;
image-rendering: auto; image-rendering: auto;
border-radius: 0.75rem; border-radius: 0.75rem;
border: 0.2rem solid var(--tg-theme-hint-color);
box-shadow: -5px 0px 50px 10px rgba(0, 0, 0, 0.4); box-shadow: -5px 0px 50px 10px rgba(0, 0, 0, 0.4);
-webkit-box-shadow: -5px 0px 50px 10px rgba(0, 0, 0, 0.4); -webkit-box-shadow: -5px 0px 50px 10px rgba(0, 0, 0, 0.4);
-moz-box-shadow: -5px 0px 50px 10px rgba(0, 0, 0, 0.4); -moz-box-shadow: -5px 0px 50px 10px rgba(0, 0, 0, 0.4);
@ -276,12 +281,17 @@ main>section#products>article.product>div>div.head {
gap: 1rem; gap: 1rem;
} }
main>section#products>article.product>div>div.head>button {
align-self: start;
}
main>section#products>article.product>div>div>button:first-of-type { main>section#products>article.product>div>div>button:first-of-type {
margin-left: auto; margin-left: auto;
} }
main>section#products>article.product>div>div>button { main>section#products>article.product>div>div>button {
padding: 0.4rem; padding: 0.4rem;
color: var(--tg-theme-text-color);
background: unset; background: unset;
} }

View File

@ -19,9 +19,20 @@ main>section#categories>a.category[type="button"] {
overflow: hidden; overflow: hidden;
border-radius: 0.75rem; border-radius: 0.75rem;
color: var(--tg-theme-button-text-color); color: var(--tg-theme-button-text-color);
background-color: var(--tg-theme-button-color); background: unset;
} }
main>section#categories>a.category[type="button"]:after {
z-index: -100;
position: absoulute;
left: 0;
top: 0;
content: '';
width: 100%;
height: 100%;
filter: brightness(0.8);
background-color: var(--tg-theme-button-color);
}
main>section#categories:last-child { main>section#categories:last-child {
/* margin-bottom: unset; */ /* margin-bottom: unset; */
@ -47,7 +58,7 @@ main>section#categories>a.category[type="button"]>img {
height: 110%; height: 110%;
object-fit: cover; object-fit: cover;
/* filter: blur(1px); */ /* filter: blur(1px); */
filter: brightness(60%); filter: contrast(1.2) brightness(60%);
} }
main>section#categories>a.category[type="button"]:is(:hover, :focus)>img { main>section#categories>a.category[type="button"]:is(:hover, :focus)>img {
@ -63,10 +74,22 @@ main>section#categories>a.category[type="button"]>p {
margin: unset; margin: unset;
width: min-content; width: min-content;
border-radius: 0.75rem; border-radius: 0.75rem;
background: var(--tg-theme-section-bg-color); /* background: var(--tg-theme-hint-color); */
box-shadow: 0px 0px 8px 4px rgba(0, 0, 0, 0.3); }
-webkit-box-shadow: 0px 0px 8px 4px rgba(0, 0, 0, 0.3);
-moz-box-shadow: 0px 0px 8px 4px rgba(0, 0, 0, 0.3); main>section#categories>a.category[type="button"]>p:after {
z-index: -100;
position: absolute;
left: 0;
top: 0;
content: '';
width: 100%;
height: 100%;
border-radius: 0.75rem;
background-color: var(--tg-theme-button-color);
box-shadow: 0px 0px 8px 4px rgba(0, 0, 0, 0.1);
-webkit-box-shadow: 0px 0px 8px 4px rgba(0, 0, 0, 0.1);
-moz-box-shadow: 0px 0px 8px 4px rgba(0, 0, 0, 0.1);
} }
main>section#categories>a.category[type="button"]>p { main>section#categories>a.category[type="button"]>p {
@ -75,8 +98,9 @@ main>section#categories>a.category[type="button"]>p {
} }
main>section#filters { main>section#filters {
--filters-height: 2rem;
width: var(--width); width: var(--width);
max-height: 2.5rem; max-height: var(--filters-height);
display: flex; display: flex;
align-items: start; align-items: start;
} }
@ -124,9 +148,11 @@ main>section#products>div.column>article.product:not(:is(:hover, :focus))>* {
} }
main>section#products>div.column>article.product>a>img:first-of-type { main>section#products>div.column>article.product>a>img:first-of-type {
width: 100%; --border: 0.2rem;
width: calc(100% - var(--border) * 2);
image-rendering: auto; image-rendering: auto;
border-radius: 0.75rem; border-radius: 0.75rem;
border: 0.2rem solid var(--tg-theme-hint-color);
box-shadow: 0px 5px 70px 20px rgba(0, 0, 0, 0.3); box-shadow: 0px 5px 70px 20px rgba(0, 0, 0, 0.3);
-webkit-box-shadow: 0px 5px 70px 20px rgba(0, 0, 0, 0.3); -webkit-box-shadow: 0px 5px 70px 20px rgba(0, 0, 0, 0.3);
-moz-box-shadow: 0px 5px 70px 20px rgba(0, 0, 0, 0.3); -moz-box-shadow: 0px 5px 70px 20px rgba(0, 0, 0, 0.3);
@ -144,9 +170,14 @@ main>section#products>div.column>article.product>a>p.title {
font-weight: bold; font-weight: bold;
overflow-wrap: anywhere; overflow-wrap: anywhere;
hyphens: auto; hyphens: auto;
color: var(--tg-theme-text-color);
} }
main>section#products>div.column>article.product>a>p.title>span { main>section#products>div.column>article.product>a>p.title>span {
margin-top: 0.2rem;
display: block;
font-weight: 400;
font-size: small;
color: var(--tg-theme-hint-color); color: var(--tg-theme-hint-color);
} }
@ -160,7 +191,8 @@ main>section#products>div.column>article.product>div[data-product="buttons"]:las
border-radius: 0.75rem; border-radius: 0.75rem;
} }
main>section#products>div.column>article.product[data-product-amount]:not(:is([data-product-amount="0"], [data-product-amount="1"]))>div[data-product="buttons"]:last-of-type { /* main>section#products>div.column>article.product[data-product-amount]:not(:is([data-product-amount="0"], [data-product-amount="1"]))>div[data-product="buttons"]:last-of-type { */
main>section#products>div.column>article.product[data-product-amount]:not(:is([data-product-amount="0"]))>div[data-product="buttons"]:last-of-type {
container-type: inline-size; container-type: inline-size;
container-name: product-buttons; container-name: product-buttons;
} }
@ -168,9 +200,11 @@ main>section#products>div.column>article.product[data-product-amount]:not(:is([d
main>section#products>div.column>article.product>div[data-product="buttons"]>button[data-product-button="toggle"] { main>section#products>div.column>article.product>div[data-product="buttons"]>button[data-product-button="toggle"] {
padding: 0; padding: 0;
flex-grow: 1; flex-grow: 1;
background-color: var(--catalog-button-cart-background, var(--tg-theme-button-color));
} }
main>section#products>div.column>article.product:is([data-product-amount="0"], [data-product-amount="1"])>div[data-product="buttons"]>button[data-product-button="toggle"]>span[data-product-parameter="amount"], /* main>section#products>div.column>article.product:is([data-product-amount="0"], [data-product-amount="1"])>div[data-product="buttons"]>button[data-product-button="toggle"]>span[data-product-parameter="amount"], */
main>section#products>div.column>article.product:is([data-product-amount="0"])>div[data-product="buttons"]>button[data-product-button="toggle"]>span[data-product-parameter="amount"],
main>section#products>div.column>article.product[data-product-amount="0"]>div[data-product="buttons"]>button:is([data-product-button="write"], [data-product-button="delete"]) { main>section#products>div.column>article.product[data-product-amount="0"]>div[data-product="buttons"]>button:is([data-product-button="write"], [data-product-button="delete"]) {
display: none; display: none;
} }
@ -180,8 +214,17 @@ main>section#products>div.column>article.product>div[data-product="buttons"]>but
margin: 0 0.2rem; margin: 0 0.2rem;
} }
main>section#products>div.column>article.product[data-product-amount]:not([data-product-amount="0"])>div[data-product="buttons"]>button[data-product-button="toggle"] {
background-color: var(--catalog-button-cart-added-background, var(--tg-theme-button-color));
}
main>section#products>div.column>article.product[data-product-amount]:not([data-product-amount="0"])>div[data-product="buttons"] { main>section#products>div.column>article.product[data-product-amount]:not([data-product-amount="0"])>div[data-product="buttons"] {
filter: hue-rotate(calc(120deg + var(--hue-rotate-offset, 0deg))); /* hehe */
filter: var(--catalog-button-cart-added-background, hue-rotate(calc(120deg + var(--hue-rotate-offset, 0deg))));
}
main>section#products>div.column>article.product>div[data-product="buttons"]>button {
background-color: var(--catalog-button-cart-added-background, var(--tg-theme-button-color));
} }
@container product-buttons (max-width: 200px) { @container product-buttons (max-width: 200px) {

View File

@ -2,7 +2,7 @@
i.icon.house { i.icon.house {
position: relative; position: relative;
margin-bottom: -2px; margin-bottom: -7px;
width: 18px; width: 18px;
height: 14px; height: 14px;
display: block; display: block;

View File

@ -0,0 +1,35 @@
@charset "UTF-8";
i.icon.user {
width: 12px;
height: 18px;
display: block;
box-sizing: border-box;
transform: scale(1);
}
i.icon.user::after,
i.icon.user::before {
content: "";
position: absolute;
display: block;
box-sizing: border-box;
border: 2px solid;
}
i.icon.user::before {
left: 2px;
top: 0;
width: 8px;
height: 8px;
border-radius: 30px;
}
i.icon.user::after {
top: 9px;
width: 12px;
height: 9px;
border-bottom: 0;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
}

View File

@ -1,15 +1,16 @@
@charset "UTF-8"; @charset "UTF-8";
section[data-type="select"] { div[data-type="select"] {
--width: max(14rem, 20vw); --filter-width: max(14rem, 30vw);
--height-element: 2rem; --filter-button-width: 2.5rem;
--height-close: var(--height-element); --filter-height-element: var(--filters-height, 2rem);
--height-open: max-content; --filter-height-close: var(--filter-height-element);
--filter-height-open: max-content;
position: relative; position: relative;
width: var(--width); width: var(--filter-width);
min-width: var(--width);
height: var(--height-close); height: var(--height-close);
display: flex; display: flex;
flex-direction: column;
cursor: pointer; cursor: pointer;
border-radius: 0.75rem; border-radius: 0.75rem;
overflow-x: hidden; overflow-x: hidden;
@ -17,18 +18,31 @@ section[data-type="select"] {
transition: 0s; transition: 0s;
} }
section[data-type="select"]>button:has(>i.icon.close) { div[data-type="select"]>section {
align-self: end; position: relative;
height: 100%; height: var(--filter-height-close);
display: flex;
flex-direction: column;
flex-grow: 1;
cursor: pointer;
background-color: var(--tg-theme-button-color);
transition: 0s;
}
div[data-type="select"]>button:has(>i.icon.close) {
z-index: 100;
align-self: start;
width: var(--filter-button-width);
height: var(--filter-height-close);
padding: 0 0.4rem; padding: 0 0.4rem;
} }
section[data-type="select"]:has(input[id$="title"]:checked)>button:has(>i.icon.close), div[data-type="select"]:has(>section>input[id$="title"]:checked)>button:has(>i.icon.close),
section[data-type="select"]:focus>button:has(>i.icon.close) { div[data-type="select"]:focus>button:has(>i.icon.close) {
display: none; display: none;
} }
section[data-type="select"]:not(:focus, :has(>button>i.icon.close)):after { div[data-type="select"]:not(>section:focus, >section:has(>button>i.icon.close)):after {
z-index: 30; z-index: 30;
content: ''; content: '';
top: calc(50% - 2.5px); top: calc(50% - 2.5px);
@ -42,19 +56,19 @@ section[data-type="select"]:not(:focus, :has(>button>i.icon.close)):after {
border-right: 5px solid transparent; border-right: 5px solid transparent;
} }
section[data-type="select"]>input { div[data-type="select"]>section>input {
left: -99999px; left: -99999px;
position: absolute; position: absolute;
opacity: 0; opacity: 0;
} }
section[data-type="select"]>label { div[data-type="select"]>section>label {
z-index: 10; z-index: 10;
order: 2; order: 2;
top: 0; top: 0;
position: absolute; position: absolute;
width: 100%; width: calc(100% + var(--filter-button-width));
height: var(--height-element); height: var(--filter-height-element);
padding: 0 1rem; padding: 0 1rem;
display: none; display: none;
overflow: hidden; overflow: hidden;
@ -66,61 +80,62 @@ section[data-type="select"]>label {
transition: 0s; transition: 0s;
} }
section[data-type="select"]:is([data-select="open"], :focus)>input:not(:checked)+label[for$='title'] { div[data-type="select"]>section:is([data-select="open"], :focus)>input:not(:checked)+label[for$='title'] {
display: none; display: none;
} }
section[data-type="select"]:is([data-select="open"], :focus)>input:not(:checked)+label[for$='all']:not(:hover, :active, :focus) { div[data-type="select"]>section:is([data-select="open"], :focus)>input:not(:checked)+label[for$='all']:not(:hover, :active, :focus) {
filter: brightness(90%); filter: brightness(90%);
} }
section[data-type="select"]>input:not(:checked)+label { div[data-type="select"]>section>input:not(:checked)+label {
cursor: pointer; cursor: pointer;
filter: brightness(80%); filter: brightness(80%);
} }
section[data-type="select"]:is([data-select="open"], :focus)>input+label:hover { div[data-type="select"]>section:is([data-select="open"], :focus)>input+label:hover {
filter: brightness(110%); filter: brightness(110%);
} }
section[data-type="select"]:is([data-select="open"], :focus)>input:checked+label:hover { div[data-type="select"]>section:is([data-select="open"], :focus)>input:checked+label:hover {
filter: brightness(120%); filter: brightness(120%);
} }
section[data-type="select"]:is([data-select="open"], :focus)>input+label:is(:active, :focus) { div[data-type="select"]>section:is([data-select="open"], :focus)>input+label:is(:active, :focus) {
filter: brightness(60%); filter: brightness(60%);
} }
section[data-type="select"]:is([data-select="open"], :focus)>input:checked+label:is(:active, :focus) { div[data-type="select"]>section:is([data-select="open"], :focus)>input:checked+label:is(:active, :focus) {
filter: brightness(70%); filter: brightness(70%);
} }
section[data-type="select"]>input:checked+label { div[data-type="select"]>section>input:checked+label {
order: 1; order: 1;
max-width: calc(var(--width) - 2rem - 10px); max-width: calc(var(--filter-width) - 2rem - 10px);
display: inline; display: inline;
line-height: var(--height-element); line-height: var(--filter-height-element);
padding-right: 0; padding-right: 0;
} }
section[data-type="select"]:is([data-select="open"], :focus)>input:checked+label { div[data-type="select"]>section:is([data-select="open"], :focus)>input:checked+label {
max-width: initial; max-width: initial;
padding-right: initial; padding-right: initial;
} }
section[data-type="select"]:is([data-select="open"], :focus) { div[data-type="select"]>section:is([data-select="open"], :focus) {
height: var(--height-open, max-content); height: var(--filter-height-open, max-content);
} }
section[data-type="select"]:is([data-select="open"], :focus)>label { div[data-type="select"]>section:is([data-select="open"], :focus)>label {
position: relative; position: relative;
display: inline; display: inline;
line-height: var(--height-element); line-height: var(--filter-height-element);
pointer-events: all; pointer-events: all;
} }
@media only screen and (max-width: 500px) { @media only screen and (max-width: 500px) {
section[data-type="select"]:only-child { div[data-type="select"]>section:only-child {
--width: 100% --width: 100%
} }
} }

View File

@ -28,7 +28,8 @@ a {
body { body {
--gap: 16px; --gap: 16px;
--width: calc(100% - var(--gap) * 2); /* --width: calc(100% - var(--gap) * 2); */
--width: calc(100vw - var(--gap) * 2 - 1rem);
--offset-x: 2%; --offset-x: 2%;
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -49,9 +50,10 @@ body:has(section#window) {
aside {} aside {}
header { header {
z-index: 1000;
container-type: inline-size; container-type: inline-size;
container-name: header; container-name: header;
margin-top: 2rem; position: relative;
padding: 0 var(--offset-x); padding: 0 var(--offset-x);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -59,7 +61,12 @@ header {
gap: 26px; gap: 26px;
} }
body:has(>div#top>search)>header {
margin-top: var(--header-margin-top, 2rem);
}
main { main {
--gap-main: calc(var(--gap) + 10px);
container-type: inline-size; container-type: inline-size;
container-name: main; container-name: main;
flex-grow: 1; flex-grow: 1;
@ -67,7 +74,7 @@ main {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: calc(var(--gap) + 10px); gap: var(--gap-main);
transition: 0s; transition: 0s;
} }
@ -101,7 +108,7 @@ main>article>h3 {
margin-top: 2rem; margin-top: 2rem;
} }
main>search { main>search.relative {
--gap: 16px; --gap: 16px;
--border-width: 1px; --border-width: 1px;
width: var(--width); width: var(--width);
@ -113,7 +120,71 @@ main>search {
overflow: clip; overflow: clip;
} }
footer {} div#top {
z-index: 1000;
position: fixed !important;
top: 0;
padding: 0.6rem 1rem;
display: flex;
flex-direction: column;
align-items: center;
overflow: hidden;
background-color: var(--tg-theme-bg-color);
transform: translate3d(0, 0, 0);
}
div#top>search.fixed {
--gap: 16px;
--border-width: 1px;
width: var(--width);
display: flex;
flex-flow: row;
border-radius: 1.375rem;
backdrop-filter: contrast(0.8);
overflow: hidden;
border: 2px solid transparent;
}
a#cart {
--size: 2.2rem;
position: fixed !important;
bottom: 1rem;
right: 1rem;
width: var(--size);
height: var(--size);
padding: 0.25rem 0.3rem 0.35rem 0.3rem;
border-radius: 0.75rem;
overflow: hidden;
background-color: var(--tg-theme-section-header-text-color);
box-shadow: 0px 0px 12px 3px rgba(0, 0, 0, 0.2);
-webkit-box-shadow: 0px 0px 12px 3px rgba(0, 0, 0, 0.2);
-moz-box-shadow: 0px 0px 12px 3px rgba(0, 0, 0, 0.2);
transform: translate3d(0, 0, 0);
}
a#cart:before {
content: var(--cart-amount);
position: absolute;
right: -0.5rem;
top: -0.6rem;
padding: 0.2rem 0.3rem;
min-width: 0.7rem;
display: flex;
justify-content: center;
align-items: center;
font-size: small;
border-radius: 1.125rem;
border: 0.2rem solid var(--tg-theme-button-color);
color: var(--tg-theme-button-color);
background-color: var(--tg-theme-button-text-color);
box-shadow: 0px 0px 12px 3px rgba(0, 0, 0, 0.2);
-webkit-box-shadow: 0px 0px 12px 3px rgba(0, 0, 0, 0.2);
-moz-box-shadow: 0px 0px 12px 3px rgba(0, 0, 0, 0.2);
}
footer {
margin-bottom: var(--footer-margin-bottom);
}
footer>section#govno { footer>section#govno {
align-items: center; align-items: center;
@ -256,6 +327,7 @@ input {
.cost.currency:after { .cost.currency:after {
content: var(--currency); content: var(--currency);
margin-left: var(--currency-offset, 0.1rem); margin-left: var(--currency-offset, 0.1rem);
font-weight: normal;
} }
.cost.plus:before { .cost.plus:before {

View File

@ -1,9 +1,8 @@
@charset "UTF-8"; @charset "UTF-8";
header>nav#menu { nav#menu.relative {
container-type: inline-size; container-type: inline-size;
container-name: menu; container-name: menu;
margin-bottom: 1rem;
width: var(--width); width: var(--width);
min-height: 3rem; min-height: 3rem;
display: flex; display: flex;
@ -13,7 +12,7 @@ header>nav#menu {
overflow: hidden; overflow: hidden;
} }
header>nav#menu>a[type="button"] { nav#menu.relative>a[type="button"] {
height: 3rem; height: 3rem;
padding: unset; padding: unset;
border-radius: 1.375rem; border-radius: 1.375rem;
@ -21,34 +20,76 @@ header>nav#menu>a[type="button"] {
background-color: var(--tg-theme-button-color); background-color: var(--tg-theme-button-color);
} }
header>nav#menu>a[type=button]>:first-child { nav#menu.relative>a[type=button]>:first-child {
margin-left: 1rem; margin-left: 1rem;
} }
header>nav#menu>a[type="button"]>* { nav#menu.relative>a[type="button"]>* {
margin-right: 1rem; margin-right: 1rem;
} }
@container header (max-width: 450px) { @container header (max-width: 450px) {
header>nav#menu>a[type="button"]:nth-child(1)>i.icon+span { nav#menu.relative>a[type="button"]:nth-child(1)>i.icon+span {
display: none; display: none;
} }
} }
@container header (max-width: 350px) { @container header (max-width: 350px) {
header>nav#menu>a[type="button"]:nth-child(2)>i.icon+span { nav#menu.relative>a[type="button"]:nth-child(2)>i.icon+span {
display: none; display: none;
} }
} }
@container header (max-width: 250px) { @container header (max-width: 250px) {
header>nav#menu>a[type="button"]:nth-child(3)>i.icon+span { nav#menu.relative>a[type="button"]:nth-child(3)>i.icon+span {
display: none; display: none;
} }
} }
@container header (max-width: 150px) { @container header (max-width: 150px) {
header>nav#menu>a[type="button"]>i.icon+span { nav#menu.relative>a[type="button"]>i.icon+span {
display: none; display: none;
} }
} }
nav#menu.fixed {
z-index: 1000;
position: fixed !important;
left: 0;
bottom: 0;
width: 100%;
height: var(--menu-height, 3rem);
display: flex;
flex-flow: row;
justify-content: space-around;
align-items: center;
overflow: hidden;
background-color: var(--tg-theme-secondary-bg-color);
transform: translate3d(0, 0, 0);
}
nav#menu.fixed>a[type="button"] {
position: relative;
flex-grow: 1;
padding: 0.6rem;
color: var(--tg-theme-text-color);
background-color: unset;
}
nav#menu.fixed>a[type="button"]>i.icon {
position: absolute;
}
nav#menu.fixed>a[type="button"].cart:before {
content: var(--cart-amount);
position: absolute;
margin-right: -1.3rem;
margin-top: -1.5rem;
padding: 0.2rem 0.3rem;
min-width: 0.7rem;
display: flex;
justify-content: center;
align-items: center;
font-size: small;
color: var(--tg-color-scheme);
}

View File

@ -1,4 +1,6 @@
* *
!acquirings/
!acquirings/robokassa
!deliveries/ !deliveries/
!.gitignore !.gitignore
!*.sample !*.sample

View File

@ -0,0 +1,7 @@
<?php
// Exit (success)
return [
'login' => '',
'passwords' => ['', '']
];

View File

@ -0,0 +1,7 @@
<?php
// Exit (success)
return [
'login' => '',
'passwords' => ['', '']
];

View File

@ -13,5 +13,59 @@ return [
'offer' => false, 'offer' => false,
'sim' => null, 'sim' => null,
'mail' => null 'mail' => null
],
'search' => [
'enabled' => true,
'position' => 'fixed'
],
'cart' => [
'enabled' => false
],
'css' => [
'catalog-button-cart-background' => '#40a7e3',
'product-button-cart-background' => '#40a7e3',
'catalog-button-cart-added-background' => '#90be36',
'product-button-cart-added-background' => '#90be36'
],
'account' => [
'enabled' => false
],
'menu' => [
'enabled' => true,
'position' => 'fixed'
],
'header' => [
'enabled' => true,
'position' => 'fixed'
],
'acquirings' => [
'robokassa' => [
'enabled' => true,
'mode' => 'test'
]
],
'input' => [
'deliveries' => [
'site' => [],
'chat' => [
'sim',
'name',
'destination',
'address',
'commentary'
]
]
],
'deliveries' => [
'cdek' => [
'enabled' => true,
'label' => 'CDEK'
]
],
'chats' => [
[
'id' => -1002599391893,
'orders' => true
]
] ]
]; ];

View File

@ -0,0 +1 @@
!*.mp4

BIN
mirzaev/huesos/system/storage/example.xlsx Executable file → Normal file

Binary file not shown.

Binary file not shown.

View File

@ -8,6 +8,7 @@ namespace mirzaev\huesos\views;
use mirzaev\huesos\models\session, use mirzaev\huesos\models\session,
mirzaev\huesos\models\account, mirzaev\huesos\models\account,
mirzaev\huesos\models\settings, mirzaev\huesos\models\settings,
mirzaev\huesos\models\cart,
mirzaev\huesos\models\enumerations\language, mirzaev\huesos\models\enumerations\language,
mirzaev\huesos\models\enumerations\currency; mirzaev\huesos\models\enumerations\currency;
@ -139,7 +140,7 @@ final class templater extends controller implements array_access
'sim_format', 'sim_format',
function (string|int|float|null $sim = null) { function (string|int|float|null $sim = null) {
// Universalizing SIM-number // Universalizing SIM-number
preg_match_all('/\d/', $sim, $numbers); preg_match_all('/\d/', (string) $sim, $numbers);
$universalized = implode($numbers[0]); $universalized = implode($numbers[0]);
// Deinitializing unnecessary variables( // Deinitializing unnecessary variables(

View File

@ -3,6 +3,7 @@
{% endblock %} {% endblock %}
{% block body %} {% block body %}
{% if settings.account.enabled and not (settings.menu.enabled and settings.menu.position == 'fixed') %}
{% if account is empty %} {% if account is empty %}
<section id="account"> <section id="account">
<!-- <a onclick="core.account.authentication()"> <!-- <a onclick="core.account.authentication()">
@ -16,6 +17,7 @@
</a> </a>
</section> </section>
{% endif %} {% endif %}
{% endif %}
{% endblock %} {% endblock %}
{% block js %} {% block js %}

View File

@ -0,0 +1,16 @@
{% extends "/themes/default/core.html" %}
{% block body %}
<link type="text/css" rel="stylesheet" href="/themes/{{ theme }}/css/acquirings/robokassa/fail.css" />
<main>
<h2 id="title" class="unselectable">{{ h2 }}</h2>
{% if test %}
<small>{{ test }}</small>
{% endif %}
<span id="closing" style="--iterator: '{{ closing.iterator }}';"><i class="icon loading spinner animated"></i>{{
closing.title }}</span>
</main>
<script src="/js/closing.js"></script>
{% endblock %}

View File

@ -0,0 +1,16 @@
{% extends "/themes/default/core.html" %}
{% block body %}
<link type="text/css" rel="stylesheet" href="/themes/{{ theme }}/css/acquirings/robokassa/success.css" />
<main>
<h2 id="title" class="unselectable">{{ h2 }}</h2>
{% if test %}
<small>{{ test }}</small>
{% endif %}
<span id="closing" style="--iterator: '{{ closing.iterator }}';"><i class="icon loading spinner animated"></i>{{
closing.title }}</span>
</main>
<script src="/js/closing.js"></script>
{% endblock %}

View File

@ -1,5 +1,5 @@
{% if cart.products is not empty %} {% if cart.products is not empty %}
<button id="order" class="rounded merged column unselectable" onclick="core.cart.share(this)" {% if <button id="order" class="rounded merged column unselectable" onclick="core.cart.share(this)" {% if
session.buffer.delivery[account.buffer.delivery.company ?? session.buffer.delivery.company].ready==false %} session.buffer.delivery[account.buffer.delivery.company ?? session.buffer.delivery.company].ready==false and 'address' in settings.input.deliveries.site %}
disabled="true" {% endif %}>Оформить заказ</button> disabled="true" {% endif %}>Оформить заказ</button>
{% endif %} {% endif %}

View File

@ -3,7 +3,8 @@
class="rounded column unselectable"> class="rounded column unselectable">
<div class="row"> <div class="row">
<span id="amount">{{ cart.summary.amount ?? 0 }}</span> <span id="amount">{{ cart.summary.amount ?? 0 }}</span>
<span>{{ language.name == "ru" ? "товаров за " : "products worth" }}</span> <!-- <span>{{ language.name == "ru" ? "товаров за " : "products worth" }}</span> -->
<span>{{ language.name == "ru" ? "товаров на " : "products worth" }}</span>
<span id="cost" class="cost currency">{{ cart.summary.cost ?? 0 }}</span> <span id="cost" class="cost currency">{{ cart.summary.cost ?? 0 }}</span>
{% if session.buffer.delivery[account.buffer.delivery.company ?? session.buffer.delivery.company].cost %} {% if session.buffer.delivery[account.buffer.delivery.company ?? session.buffer.delivery.company].cost %}
<span id="cost_delivery" class="cost delivery currency plus hint">{{ session.buffer.delivery[account.buffer.delivery.company ?? session.buffer.delivery.company].cost }}</span> <span id="cost_delivery" class="cost delivery currency plus hint">{{ session.buffer.delivery[account.buffer.delivery.company ?? session.buffer.delivery.company].cost }}</span>

View File

@ -0,0 +1,3 @@
<a id="cart" class="animated opacity" type="button" href="/cart">
<i class="icon shopping cart"></i>
</a>

View File

@ -1,6 +1,7 @@
{% if filters is not empty %} {% if filters is not empty %}
<section id="filters" class="unselectble"> <section id="filters" class="unselectble">
<section class="unselectable" data-type="select" tabindex="4"> <div data-type="select">
<section class="unselectable" tabindex="4">
{% set buffer_brand = session.buffer.catalog.filters.brand ?? account.buffer.catalog.filters.brand %} {% set buffer_brand = session.buffer.catalog.filters.brand ?? account.buffer.catalog.filters.brand %}
<input name="brand" type="radio" id="brand_title" {% if buffer_brand is empty %}checked{% endif %}> <input name="brand" type="radio" id="brand_title" {% if buffer_brand is empty %}checked{% endif %}>
<label for="brand_title" type="button">{{ language.name == 'ru' ? 'Бренд' : 'Brand' }}</label> <label for="brand_title" type="button">{{ language.name == 'ru' ? 'Бренд' : 'Brand' }}</label>
@ -16,8 +17,10 @@
{{ brand }} {{ brand }}
</label> </label>
{% endfor %} {% endfor %}
<button onclick="core.catalog.parameters.set('brand', null); core.catalog.search(event)"><i
class="icon small close"></i></button>
</section> </section>
<button type="button" onclick="core.catalog.parameters.set('brand', null); core.catalog.search(event)">
<i class="icon small close"></i>
</button>
</div>
</section> </section>
{% endif %} {% endif %}

View File

@ -0,0 +1,23 @@
{% if filters is not empty %}
<section id="filters" class="unselectble">
<section class="unselectable" data-type="select" tabindex="4">
{% set buffer_brand = session.buffer.catalog.filters.brand ?? account.buffer.catalog.filters.brand %}
<input name="brand" type="radio" id="brand_title" {% if buffer_brand is empty %}checked{% endif %}>
<label for="brand_title" type="button">{{ language.name == 'ru' ? 'Бренд' : 'Brand' }}</label>
<!-- <input name="brand" type="radio" id="brand_all" oninput="core.catalog.parameters.set('brand', null); core.catalog.search(event)">
<label for="brand_all" type="button">
{{ language.name == 'ru' ? 'Все бренды' : 'All brands' }}
</label> -->
{% for brand in filters.brands %}
<input name="brand" type="radio" id="brand_{{ loop.index }}"
oninput="core.catalog.parameters.set('brand', '{{ brand }}'); core.catalog.search(event)" {% if
brand==buffer_brand %}checked{% endif %}>
<label for="brand_{{ loop.index }}" type="button">
{{ brand }}
</label>
{% endfor %}
<button onclick="core.catalog.parameters.set('brand', null); core.catalog.search(event)"><i
class="icon small close"></i></button>
</section>
</section>
{% endif %}

View File

@ -1,7 +1,21 @@
<search id="search"> {% if settings.search.position == 'fixed' %}
<div id="top">
<search id="search" class="fixed">
<label class="unselectable"><i class="icon search"></i></label> <label class="unselectable"><i class="icon search"></i></label>
{% set buffer_search = session.buffer.catalog.search.text ?? account.buffer.catalog.search.text %} {% set buffer_search = session.buffer.catalog.search.text ?? account.buffer.catalog.search.text %}
<input placeholder="{{ language.name == 'ru' ? 'Поиск по каталогу' : 'Search in the catalog' }}" type="search" tabindex="1" <input placeholder="{{ language.name == 'ru' ? 'Поиск по каталогу' : 'Search in the catalog' }}" type="search"
tabindex="1"
onkeyup="event.keyCode === 9 || core.catalog.parameters.set('text', this.value); return core.catalog.search(event, this)"
{% if buffer_search is not empty %} value="{{ buffer_search }}" {% endif %} />
</search>
</div>
{% elseif settings.search.position == 'relative' %}
<search id="search" class="relative">
<label class="unselectable"><i class="icon search"></i></label>
{% set buffer_search = session.buffer.catalog.search.text ?? account.buffer.catalog.search.text %}
<input placeholder="{{ language.name == 'ru' ? 'Поиск по каталогу' : 'Search in the catalog' }}" type="search"
tabindex="1"
onkeyup="event.keyCode === 9 || core.catalog.parameters.set('text', this.value); return core.catalog.search(event, this)" onkeyup="event.keyCode === 9 || core.catalog.parameters.set('text', this.value); return core.catalog.search(event, this)"
{% if buffer_search is not empty %} value="{{ buffer_search }}" {% endif %} /> {% if buffer_search is not empty %} value="{{ buffer_search }}" {% endif %} />
</search> </search>
{% endif %}

View File

@ -11,12 +11,23 @@
<link type="text/css" rel="stylesheet" href="/themes/{{ theme }}/css/icons/minus.css" /> <link type="text/css" rel="stylesheet" href="/themes/{{ theme }}/css/icons/minus.css" />
{% endblock %} {% endblock %}
{% block search %}
{% if settings.search.enabled and settings.search.position == 'fixed' %}
{% include "/themes/default/catalog/elements/search.html" %}
{% endif %}
{% endblock %}
{% block main %} {% block main %}
<h2 id="title" class="unselectable">{{ h2 }}</h2> <h2 id="title" class="unselectable">{{ h2 }}</h2>
{% if settings.search.enabled and settings.search.position == 'relative' %}
{% include "/themes/default/catalog/elements/search.html" %} {% include "/themes/default/catalog/elements/search.html" %}
{% endif %}
{% include "/themes/default/catalog/elements/categories.html" %} {% include "/themes/default/catalog/elements/categories.html" %}
{% include "/themes/default/catalog/elements/filters.html" %} {% include "/themes/default/catalog/elements/filters.html" %}
{% include "/themes/default/catalog/elements/products.html" %} {% include "/themes/default/catalog/elements/products.html" %}
{% if settings.cart.enabled %}
{% include "/themes/default/catalog/elements/cart.html" %}
{% endif %}
{% endblock %} {% endblock %}
{% block js %} {% block js %}

View File

@ -0,0 +1,83 @@
<script>
const search = document.getElementById('search');
const search_next = search.nextElementSibling
let search_to_fixed, search_to_normal;
setInterval(function () {
if (search_next.getBoundingClientRect()?.top < window.scrollY) {
clearTimeout(search_to_normal);
if (search_to_fixed == null) {
search_to_fixed = setTimeout(function () {
let search_wrap = document.getElementById('wrap_search');
if (search_wrap instanceof HTMLElement) {
if (search.parentElement !== search_wrap) {
search_wrap.appendChild(search);
}
} else {
const search_wrap = document.createElement('div');
search_wrap.setAttribute('id', 'wrap_search');
search_wrap.classList.add('animated', 'slide', 'down');
search_wrap.appendChild(search);
document.body.appendChild(search_wrap);
}
search_to_fixed = null;
search_to_normal = null;
}, 200);
}
} else {
clearTimeout(search_to_fixed);
if (search_to_normal == null) {
search_to_normal = setTimeout(function () {
let search_wrap = document.getElementById('wrap_search');
if (search_wrap instanceof HTMLElement) {
if (core.main instanceof HTMLElement) {
// Found the <main> element
// Initializing the title <h2> element
const title = document.getElementById("title");
if (title instanceof HTMLElement) {
// Initialized the title <h2> elemment in the <main> element
// Writing the search <search> element after the title <h2> element in the <main> element
core.main.insertBefore(
search,
title.nextElementSibling,
);
} else {
// Not initialized the title <h2> element in the <main> element
// Inititalize the first element in the <main> element
const first = core.main.firstElementChild;
if (first instanceof HTMLElement) {
// Initialized the first element in the <main> element
// Writing the search <search> element before the first element in the <main> element
core.main.insertBefore(
search,
first,
);
}
}
}
search_wrap?.remove();
}
search_to_fixed = null;
search_to_normal = null;
}, 0);
}
}
}, 50);
</script>

View File

@ -4,7 +4,8 @@
{% block meta %} {% block meta %}
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <!-- <meta name="viewport" content="width=device-width, height=device-height, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=0, shrink-to-fit=no, viewport-fit=cover"> -->
<meta name="viewport" content="width=device-width, height=device-height, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=0, viewport-fit=cover">
{% for meta in head.metas %} {% for meta in head.metas %}
<meta {% for name, value in meta.attributes %}{{ name }}="{{ value }}" {% endfor %}> <meta {% for name, value in meta.attributes %}{{ name }}="{{ value }}" {% endfor %}>
{% endfor %} {% endfor %}
@ -22,6 +23,27 @@
--currency: "{{ currency.symbol ?? '$' }}"; --currency: "{{ currency.symbol ?? '$' }}";
--days: "{{ language.name == 'ru' ? 'дней' : 'days' }}"; --days: "{{ language.name == 'ru' ? 'дней' : 'days' }}";
--days-short: "{{ language.name == 'ru' ? 'дн' : 'd' }}"; --days-short: "{{ language.name == 'ru' ? 'дн' : 'd' }}";
{% if settings.search.enabled and settings.search.position == 'fixed' %}
{% if settings.menu.enabled %}
{% if settings.menu.position == 'fixed' %}
--header-margin-top: 3rem;
{% elseif settings.menu.position == 'relative' %}
--header-margin-top: 5rem;
{% endif %}
{% else %}
--header-margin-top: 3rem;
{% endif %}
{% endif %}
{% if settings.menu.enabled and settings.menu.position == 'fixed' %}
--menu-height: {{ settings.menu.height ?? '3rem' }};
--footer-margin-bottom: var(--menu-height);
{% endif %}
{% for parameter, value in settings.css %}
--{{ parameter }}: {{ value }};
{% endfor %}
{% if cart.summary.amount > 0 %}
--cart-amount: "{{ cart.summary.amount }}";
{% endif %}
} }
</style> </style>
<link type="text/css" rel="stylesheet" href="/themes/{{ theme }}/css/animations.css" /> <link type="text/css" rel="stylesheet" href="/themes/{{ theme }}/css/animations.css" />

View File

@ -1,15 +1,24 @@
{% use "/themes/default/menu.html" with css as menu_css, body as menu_body, js as menu_js %} {% use "/themes/default/menu.html" with css as menu_css, body as menu_body, js as menu_js %}
{% block css %} {% block css %}
{% if settings.menu.enabled %}
{{ block('menu_css') }} {{ block('menu_css') }}
{% endif %}
{% endblock %} {% endblock %}
{% block body %} {% block body %}
<header> <header class="{{ settings.header.position }}">
{% if settings.menu.enabled and settings.menu.position == 'relative' %}
{{ block('menu_body') }} {{ block('menu_body') }}
{% endif %}
</header> </header>
{% if settings.menu.enabled and settings.menu.position == 'fixed' %}
{{ block('menu_body') }}
{% endif %}
{% endblock %} {% endblock %}
{% block js %} {% block js %}
{% if settings.menu.enabled %}
{{ block('menu_js') }} {{ block('menu_js') }}
{% endif %}
{% endblock %} {% endblock %}

View File

@ -16,12 +16,20 @@
{% block body %} {% block body %}
<!-- {{ block('connection_body') }} --> <!-- {{ block('connection_body') }} -->
{{ block('account_body') }} {{ block('account_body') }}
{% if settings.header.enabled %}
{{ block('header') }} {{ block('header') }}
{% endif %}
{% block search %}
{% endblock %}
<main> <main>
{% block main %} {% block main %}
{{ main|raw }} {{ main|raw }}
{% endblock %} {% endblock %}
</main> </main>
{{ block('footer') }} {{ block('footer') }}
{% endblock %} {% endblock %}

View File

@ -8,11 +8,29 @@
{% endblock %} {% endblock %}
{% block body %} {% block body %}
<nav id="menu"> {% if settings.menu.position == 'fixed' %}
<nav id="menu" class="fixed">
{% for button in menu %} {% for button in menu %}
<a href='{{ button.urn }}' onclick="return core.loader.load('{{ button.urn }}');" type="button" class="unselectable" <a{% if button.identifier %} id="{{ button.identifier }}" {% endif %} href='{{ button.urn }}' onclick="return core.loader.load('{{ button.urn }}');" type="button"
title="{{ button.name }}" {% if button.style %} class="unselectable{% if button.class %} {{ button.class }}{% endif %}" title="{{ button.name }}" {% if button.style
style="{% for parameter, value in button.style %}{{ parameter ~ ': ' ~ value ~ '; ' }}{% endfor %}" {% endif %}> %} style="{% for parameter, value in button.style %}{{ parameter ~ ': ' ~ value ~ '; ' }}{% endfor %}" {% endif %}>
{% if button.icon %}
<i class="icon {{ button.icon.class }}" {% if button.icon.style %}
style="{% for parameter, value in button.icon.style %}{{ parameter ~ ': ' ~ value ~ '; ' }}{% endfor %}" {% endif
%}></i>
{% endif %}
</a>
{% endfor %}
</nav>
{% elseif settings.menu.position == 'relative' %}
<nav id="menu" class="relative">
{% for button in menu %}
<a{% if button.identifier %} id="{{ button.identifier }}" {% endif %}href='{{ button.urn }}' onclick="return core.loader.load('{{ button.urn }}');" type="button"
class="unselectable{% if button.class %} {{ button.class }}{% endif %}" title="{{ button.name }}" {% if button.style
%} style="{% for parameter, value in button.style %}{{ parameter ~ ': ' ~ value ~ '; ' }}{% endfor %}" {% endif %}>
{% if button.icon %} {% if button.icon %}
<i class="icon {{ button.icon.class }}" {% if button.icon.style %} <i class="icon {{ button.icon.class }}" {% if button.icon.style %}
style="{% for parameter, value in button.icon.style %}{{ parameter ~ ': ' ~ value ~ '; ' }}{% endfor %}" {% endif style="{% for parameter, value in button.icon.style %}{{ parameter ~ ': ' ~ value ~ '; ' }}{% endfor %}" {% endif
@ -25,6 +43,8 @@
</a> </a>
{% endfor %} {% endfor %}
</nav> </nav>
{% endif %}
{% endblock %} {% endblock %}
{% block js %} {% block js %}

View File

@ -5,6 +5,7 @@
{{ parent() }} {{ parent() }}
<style> <style>
main>article#offer { main>article#offer {
margin-bottom: var(--footer-margin-bottom);
padding-bottom: 1rem; padding-bottom: 1rem;
} }