BIG UPDATE + resolved #5, resolved #6, resolved #7, resolved #8, resolved #10, resolved #11, resolved #14, resolved #18, resolved #19, resolved #20

This commit is contained in:
Arsen Mirzaev Tatyano-Muradovich 2024-11-19 04:15:30 +03:00
parent cf0e32e954
commit 58f065f312
82 changed files with 5334 additions and 2182 deletions

145
README.md
View File

@ -1,5 +1,4 @@
# huesos
Basis for developing chat-robots with "Web App" technology for Telegram
## Installation
@ -7,36 +6,48 @@ Basis for developing chat-robots with "Web App" technology for Telegram
### AnangoDB
1. Create a Graph with the specified values
**Name:** catalog<br/>
<br/>
**edgeDefinition:** entry<br/>
**fromCollections:** categoy, product<br/>
**Name:** catalog<br>
<br>
* Relatin 1
**edgeDefinition:** entry<br>
**fromCollections:** categoy, product<br>
**toCollections:** category
* Relation 2
**edgeDefinition:** reservation<br>
**fromCollections:** product<br>
**toCollections:** cart
2. Create a Graph with the specified values
**Name:** sessions<br/>
<br/>
**edgeDefinition:** connect<br/>
**fromCollections:** account<br/>
**toCollections:** session
**Name:** users<br>
<br>
* Relation 1
**edgeDefinition:** connect<br>
**fromCollections:** cart, session<br>
**toCollections:** account, session
**Orphan Collections:** product
3. Create indexes for the "product" collection
**Type:** "Inverted Index"<br/>
**Fields:** name.ru<br/>
**Analyzer:** "text_ru"<br/>
**Search field:** true<br/>
**Name:** name_ru<br/>
<br/>
*Add indexes for all search parameters and for all languages (search language is selected based on the user's language, <br/>
otherwise from the default language specified in the active settings from **settings** collection document)*<br/>
<br/>
*See fields in the `mirzaev/arming_bot/models/product`<br/>
**Type:** "Inverted Index"<br>
**Fields:** name.ru<br>
**Analyzer:** "text_ru"<br>
**Search field:** true<br>
**Name:** name_ru<br>
<br>
*Add indexes for all search parameters and for all languages (search language is selected based on the user's language, <br>
otherwise from the default language specified in the active settings from **settings** collection document)*<br>
<br>
*See fields in the `mirzaev/arming_bot/models/product`<br>
**name.ru**, **description.ru** and **compatibility.ru***
4. Create a View with the specified values
**type:** search-alias (you can also use "arangosearch")<br/>
**name:** **product**s_search<br/>
**indexes:**
**type:** search-alias (you can also use "arangosearch")<br>
**name:** **product**s_search<br>
**indexes:**<br>
<br>
You can copy an example of view file from here: `/examples/arangodb/views/products_search.json`
```json
"indexes": [
{
@ -48,70 +59,62 @@ otherwise from the default language specified in the active settings from **sett
### NGINX
1. Example of NGINX server file
```nginx
location / {
try_files $uri $uri/ /index.php;
}
1. Create a NGINX server
You can copy an example of server file from here: `/examples/nginx/server.conf`
location ~ /(?<type>categories|products) {
root /var/www/arming_bot/mirzaev/arming_bot/system/storage;
try_files $uri =404;
}
location ~ \.php$ {
...
}
```
2. Add support for javascript modules
Edit the file `/etc/nginx/mime.types`<br>
`application/javascript js;` -> `application/javascript js mjs;`
### SystemD (or any alternative you like)
1. Execute: `sudo cp telegram-huesos.service /etc/systemd/system/telegram-huesos.service`
*before you execute the command think about **what it does** and whether the **paths** are specified correctly*<br/>
You can copy an example of systemd file from here: `/examples/systemd/huesos.service`<br>
<br>
**Execute:** `sudo cp huesos.service /etc/systemd/system/huesos.service && sudo chmod +x /etc/systemd/system/huesos.service`<br>
<br>
*before you execute the command think about **what it does** and whether the **paths** are specified correctly*<br>
*the configuration file is very simple and you can remake it for any alternative to SystemD that you like*
## Settings
Settings of chat-robot and Web App<br/>
<br/>
Make sure you have a **settings** collection (can be created automatically) and at least one document with the "status" parameter set to "active"
Settings of chat-robot and Web App<br>
<br>
Make sure you have a **settings** collection (can be created automatically) and at least one document with the "status" parameter set to "active"<br>
You can copy a clean settings document without comments from here: `/examples/arangodb/collections/settings.json`
```json
{
"status": "active",
"project": {
"name": "NAME_OF_THE_PROJECT"
},
"language": "en",
"currency": "usd"
"status": "active",
"project": {
"name": "PROJECT"
},
"language": "en", // Will be converted to an instance of enumeration `mirzaev\arming_bot\models\enumerations\language`
"currency": "usd", // Will be converted to an instance of enumeration `mirzaev\arming_bot\models\enumerations\currency`
"company": {
"identifier": null, // Example: "000000000000000" (string|null) (if `null` it will not be displayed)
"tax": null, // Example: "000000000000" (string|null) (if `null` it will not be displayed)
"name": null, // Example: "COMPANY" (string|null) (if `null` it will not be displayed)
"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)
"mail": null // Example: "name@domain.zone" (string|null) (if `null` it will not be displayed)
}
}
```
### Language
Language for render of interface, if account or session language is not initialized<br/>
<br/>
**Value:** en<br/>
**⚠️ The value will be converted to an instance of enumeration** `mirzaev\arming_bot\models\enumerations\language`
### Currency
Currency for calculations and render of interface, if account or session currency is not initialized<br/>
<br/>
**Value:** usd<br/>
**⚠️ The value will be converted to an instance of enumeration** `mirzaev\arming_bot\models\enumerations\currency`
## Suspensions
System of suspensions of chat-robot and Web App<br/>
<br/>
Make sure you have a **suspension** collection (can be created automatically)
System of suspensions of chat-robot and Web App<br>
<br>
Make sure you have a **suspension** collection (can be created automatically)<br>
You can copy a clean suspension document without comments from here: `/examples/arangodb/collections/suspension.json`
```json
{
"end": 1726068961,
"end": 1726068961, // Unixtime
"targets": {
"chat-robot": true,
"web app": true
}
"chat-robot": true, // Block chat-robot
"web app": true // Block "Web App"
},
"access": {
"tester": true,
"developer": true
"tester": true, // Account with `"tester": true`
"developer": true // Account with `"developer": true`
},
"description": {
"ru": "Разрабатываю каталог, поиск и корзину",

View File

@ -18,6 +18,8 @@
}
],
"require": {
"php": "^8.4",
"ext-gd": "^8.4",
"triagens/arangodb": "^3.8",
"mirzaev/minimal": "^2.2",
"mirzaev/arangodb": "^1.3",
@ -28,7 +30,6 @@
"twig/extra-bundle": "^3.7",
"twig/intl-extra": "^3.10",
"avadim/fast-excel-reader": "^2.19",
"openswoole/core": "22.1.5",
"ttatpuot/cdek-sdk2.0": "^1.2",
"guzzlehttp/guzzle": "^7.9",
"php-http/guzzle7-adapter": "^1.0"

542
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,16 @@
{
"status": "active",
"project": {
"name": "PROJECT"
},
"language": "en",
"currency": "usd",
"company": {
"identifier": null,
"tax": null,
"name": null,
"offer": false,
"sim": null,
"mail": null
}
}

View File

@ -0,0 +1,15 @@
{
"end": 1726068961,
"targets": {
"chat-robot": true,
"web app": true
},
"access": {
"tester": true,
"developer": true
},
"description": {
"ru": "Разрабатываю каталог, поиск и корзину",
"en": "I am developing a catalog, search and cart"
}
}

View File

@ -0,0 +1,20 @@
{
"type": "search-alias",
"name": "products_search",
"indexes": [
{
"collection": "product",
"index": "name_ru"
},
{
"collection": "product",
"index": "description_ru"
},
{
"collection": "product",
"index": "compatibility_ru"
}
],
"id": "1368785",
"globallyUniqueId": "hB561949FBEF8/1368785"
}

View File

@ -0,0 +1,43 @@
server {
listen 443 ssl;
listen [::]:443 ssl ipv6only=on;
server_name domain.zone;
root /var/www/project/mirzaev/huesos/system/public;
index index.php;
ssl_certificate /etc/letsencrypt/live/domain.zone/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/domain.zone/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
location / {
try_files $uri $uri/ /index.php;
}
location ~ /(?<type>categories|products) {
root /var/www/arming_bot/mirzaev/arming_bot/system/storage;
try_files $uri =404;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.4-fpm.sock;
}
}
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name domain.zone;
if ($host = domain.zone) {
return 301 https://$host$request_uri;
}
return 404;
}

View File

@ -1,12 +1,12 @@
[Unit]
Description=Telegram-robot - @arming_bot
Description=Telegram chat-robot: @domain_of_your_robot_here
Wants=network.target
After=syslog.target network-online.target
[Service]
ExecStart=sudo -u www-data /usr/bin/php /var/www/arming_bot/mirzaev/arming_bot/system/public/robot.php
PIDFile=/var/run/php/telegram-arming_bot.pid
ExecStart=sudo -u www-data /usr/bin/php /var/www/project/mirzaev/huesos/system/public/robot.php
PIDFile=/var/run/php/huesos.pid
RemainAfterExit=no
RuntimeMaxSec=3600s
Restart=always

View File

@ -5,42 +5,49 @@ declare(strict_types=1);
namespace mirzaev\arming_bot\controllers;
// Files of the project
use mirzaev\arming_bot\controllers\core,
mirzaev\arming_bot\models\session,
mirzaev\arming_bot\models\account as model;
use mirzaev\arming_bot\controllers\core;
// Framework for ArangoDB
use mirzaev\arangodb\document;
// Framework for PHP
use mirzaev\minimal\http\enumerations\status;
/**
* Controller of account
*
* @package mirzaev\arming_bot\controllers
*
* @param array $errors Registry of errors
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class account extends core
{
/**
* Registry of errors
* Errors
*
* @var array $errors Registry of errors
*/
protected array $errors = [
'session' => [],
'account' => []
'account' => [],
'buffer' => []
];
/**
* Write
*
* Write to the buffer
*
* @param array $parameters Parameters of the request (POST + GET)
* @param mixed ...$parameters Parameters for writing to the buffer
*
* @return void
* @return null
*
* @todo переделать под trait buffer
*/
public function write(array $parameters = []): void
public function write(mixed ...$parameters): null
{
if (!empty($parameters) && $this->account instanceof model) {
// Received parameters and initialized account
if (!empty($parameters) && isset($this->account)) {
// Received parameters and initialized model with buffer trait
// Declaring the buffer of deserialized parameters
$deserialized = [];
@ -49,22 +56,28 @@ final class account extends core
// Iterate over parameters
// Validation of the parameter value
if (mb_strlen($value) > 4096) continue;
if (mb_strlen(serialize($value)) > 4096) continue;
// Declaring the buffer of deserialized parameter
$parameter = null;
// Deserializing name to multidimensional array
foreach (array_reverse(explode('_', $name)) as $key)
$parameter = [$key => $parameter ?? (json_validate($value) ? json_decode($value, true, 10) : $value)];
foreach (array_reverse(explode('_', (string) $name)) as $key)
$parameter = [$key => $parameter ?? (is_string($value) && json_validate($value) ? json_decode($value, true, 10) : $value)];
// Writing into the buffer of deserialized parameters
$deserialized = array_merge_recursive($parameter, $deserialized);
}
// Write to the document from ArangoDB
if (!empty($deserialized)) $this->account->write($deserialized, $this->errors['buffer']);
// Write to the account document from ArangoDB
if (!empty($deserialized)) $this->account->write($deserialized, $this->errors['account']);
// Writing status of response
$this->response->status = status::created;
}
// Exit (success)
return null;
}
}

View File

@ -11,40 +11,54 @@ use mirzaev\arming_bot\controllers\core,
mirzaev\arming_bot\models\menu,
mirzaev\arming_bot\models\enumerations\language;
// Framework for ArangoDB
use mirzaev\arangodb\document;
// Framework for PHP
use mirzaev\minimal\http\enumerations\content,
mirzaev\minimal\http\enumerations\status;
/**
* Controller of cart
*
* @package mirzaev\arming_bot\controllers
*
* @param model|null $cart Instance of the cart
* @param array $errors Registry of errors
*
* @method null index() HTML-document with shopping cart and delivery settings
* @method null product(int|string|null $identifier, ?string $type, int|string|null $amount) Change status of the product in the cart
* @method null summary() Information about products in the cart
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class cart extends core
{
/**
* Instance of the cart
* Cart
*
* @var model|null $cart Instance of the cart
*/
protected readonly ?model $cart;
/**
* Registry of errors
* Errors
*
* @var array $errors Registry of errors
*/
protected array $errors = [
'session' => [],
'account' => [],
'cart' => [],
'menu' => [],
'cart' => []
];
/**
* Cart
* Index
*
* @param array $parameters Parameters of the request (POST + GET)
* HTML-document with shopping cart and delivery settings
*
* @param null
*/
public function index(array $parameters = []): ?string
public function index(): null
{
if (isset($menu)) {
//
@ -63,7 +77,7 @@ final class cart extends core
}
// Initializing the cart
$this->cart = $this->account?->cart() ?? $this->session?->cart();
$this->cart ??= $this->account?->cart() ?? $this->session?->cart();
// Initializing the cart data
$this->view->cart = [
@ -78,68 +92,75 @@ final class cart extends core
]
];
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
// GET request
if (str_contains($this->request->headers['accept'], content::json->value)) {
// Request for JSON response
// Exit (success)
return $this->view->render('cart/page.html', [
'h2' => $this->language === language::ru ? 'Корзина' : 'Cart' // @see https://git.mirzaev.sexy/mirzaev/huesos/issues/1
]);
} else if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// POST request
// Initializing a response headers
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Initializing of the output buffer
ob_start();
// Generating the reponse
echo json_encode(
[
// Sending response
$this->response
->start()
->clean()
->sse()
->json([
'main' => '',
'errors' => $this->errors
]
);
])
->validate($this->request)
?->body()
->end();
} else if (str_contains($this->request->headers['accept'], content::any->value)) {
// Request for any response
// Initializing a response headers
header('Content-Length: ' . ob_get_length());
// Render page
$page = $this->view->render('cart/page.html', [
'h2' => $this->language === language::ru ? 'Корзина' : 'Cart' // @see https://git.mirzaev.sexy/mirzaev/huesos/issues/1
]);
// Sending and deinitializing of the output buffer
ob_end_flush();
flush();
// Sending response
$this->response
->start()
->clean()
->sse()
->write($page)
->validate($this->request)
?->body()
->end();
// Exit (success)
return null;
// Deinitializing rendered page
unset($page);
}
// Exit (fail)
// Exit (success/fail)
return null;
}
/**
* Product
*
* Write or delete the product in the cart
* Change status of the product in the cart
*
* @param array $parameters Parameters of the request (POST + GET)
* @param int|string|null $identifier Product identifier
* @param string|null $type Action type (toggle, write, delete, set)
* @param int|string|null $amount Amount of actions with the product (for write, delete and differently used by set)
*
* @return null
*
* @todo
* 1. Add a limit on adding products to the cart based on the number of products in stock
*/
public function product(array $parameters = []): ?string
{
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// POST request
public function product(
int|string|null $identifier = null,
?string $type = null,
int|string|null $amount = null
): null {
if (str_contains($this->request->headers['accept'], content::json->value)) {
// Request for JSON response
// Declaring of the buffer with amount of the product in the cart
$amount = 0;
// Declaring buffer with amount of the product in the cart
$cart = 0;
// Validating @todo add throwing errors
$identifier = null;
if (isset($parameters['identifier']) && preg_match('/[\d]+/', urldecode($parameters['identifier']), $matches)) $identifier = (int) $matches[0];
if (isset($identifier) && preg_match('/[\d]+/', urldecode($identifier), $matches)) $identifier = (int) $matches[0];
else unset($identifier);
if (isset($identifier)) {
// Received and validated identfier of the product
@ -157,20 +178,20 @@ final class cart extends core
// Initialized the product
// Initializing the cart
$this->cart = $this->account?->cart() ?? $this->session?->cart();
$this->cart ??= $this->account?->cart() ?? $this->session?->cart();
if ($this->cart instanceof model) {
// Initialized the cart
// Initializing the buffer with amount of the product in the cart
$amount = $this->cart->count(product: $product, limit: 100, errors: $this->errors['cart']) ?? 0;
// Initializing buffer with amount of the product in the cart
$cart = $this->cart->count(product: $product, limit: 100, errors: $this->errors['cart']) ?? 0;
if ($this->cart instanceof model) {
// Initialized the cart
// Validating @todo add throwing errors
$type = null;
if (isset($parameters['type']) && preg_match('/[\w]+/', urldecode($parameters['type']), $matches)) $type = $matches[0];
if (isset($type) && preg_match('/[\w]+/', urldecode($type), $matches)) $type = $matches[0];
else unset($type);
if (isset($type)) {
// Received and validated type of action with the product
@ -178,14 +199,14 @@ final class cart extends core
if ($type === 'toggle') {
// Write the product to the cart if is not in the cart and vice versa
if ($amount > 0) {
if ($cart > 0) {
// The cart has the product
// Deleting the product from the cart
$this->cart->delete(product: $product, amount: $amount, errors: $this->errors['cart']);
$this->cart->delete(product: $product, amount: $cart, errors: $this->errors['cart']);
// Reinitializing the buffer with amount of the product in the cart
$amount = 0;
$cart = 0;
} else {
// The cart has no the product
@ -193,62 +214,62 @@ final class cart extends core
$this->cart->write(product: $product, amount: 1, errors: $this->errors['cart']);
// Reinitializing the buffer with amount of the product in the cart
$amount = 1;
$cart = 1;
}
} else {
// Received not the "toggle" command
// Validating @todo add throwing errors
$_amount = null;
if (isset($parameters['amount']) && preg_match('/[\d]+/', urldecode($parameters['amount']), $matches)) $_amount = (int) $matches[0];
if (isset($amount) && preg_match('/[\d]+/', urldecode($amount), $matches)) $amount = (int) $matches[0];
else unset($amount);
if (isset($_amount)) {
if (isset($amount)) {
// Received and validated amount parameter for action with the product
if ($type === 'write') {
// Increase amount of the product in the cart
if ($amount + $_amount < 101) {
if ($cart + $amount < 101) {
// Validated amount to wrting
// Writing the product to the cart
$this->cart->write(product: $product, amount: $_amount, errors: $this->errors['cart']);
$this->cart->write(product: $product, amount: $amount, errors: $this->errors['cart']);
// Reinitialize the buffer with amount of the product in the cart
$amount += $_amount;
$cart += $amount;
}
} else if ($type === 'delete') {
// Decrease amount of the product in the cart
if ($amount - $_amount > -1) {
if ($cart - $amount > -1) {
// Validated amount to deleting
// Deleting the product from the cart
$this->cart->delete(product: $product, amount: $_amount, errors: $this->errors['cart']);
$this->cart->delete(product: $product, amount: $amount, errors: $this->errors['cart']);
// Reinitialize the buffer with amount of the product in the cart
$amount -= $_amount;
$cart -= $amount;
}
} else if ($type === 'set') {
// Set amount of the product in the cart
if ($_amount > -1 && $_amount < 101) {
if ($amount > -1 && $amount < 101) {
// Validated amount to setting
if ($_amount > $amount) {
if ($amount > $cart) {
// Requested amount more than actual amount of the product in the cart
// Writing the product from the cart
$this->cart->write(product: $product, amount: $_amount - $amount, errors: $this->errors['cart']);
// Writing the product to the cart
$this->cart->write(product: $product, amount: $amount - $cart, errors: $this->errors['cart']);
} else {
// Requested amount less than actual amount of the product in the cart
// Deleting the product from the cart
$this->cart->delete(product: $product, amount: $amount - $_amount, errors: $this->errors['cart']);
$this->cart->delete(product: $product, amount: $cart - $amount, errors: $this->errors['cart']);
}
// Reinitializing the buffer with amount of the product in the cart
$amount = $_amount;
$cart = $amount;
}
}
}
@ -259,49 +280,41 @@ final class cart extends core
}
}
// Initializing a response headers
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Initializing of the output buffer
ob_start();
// Generating the reponse
echo json_encode(
[
'amount' => $amount, // $amount does not store a real value, but is calculated without a repeated request to ArangoDB
// Sending response
$this->response
->start()
->clean()
->sse()
->json([
'amount' => $cart, // $cart does not store a real value, but is calculated without a repeated request to ArangoDB
'errors' => $this->errors
]
);
])
->validate($this->request)
?->body()
->end();
// Initializing a response headers
header('Content-Length: ' . ob_get_length());
// Sending and deinitializing of the output buffer
ob_end_flush();
flush();
// Exit (success)
return null;
// Deinitializing buffer with amount of the product in the cart
unset($cart);
}
// Exit (fail)
// Exit (success/fail)
return null;
}
/**
* Summary
*
* @param array $parameters Parameters of the request (POST + GET)
* Information about products in the cart
*
* @return null
*/
public function summary(array $parameters = []): ?string
public function summary(): null
{
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// POST request
if (str_contains($this->request->headers['accept'], content::json->value)) {
// Request for JSON response
// Initializing the cart
$this->cart = $this->account?->cart() ?? $this->session?->cart();
$this->cart ??= $this->account?->cart() ?? $this->session?->cart();
if ($this->cart instanceof model) {
// Initialized the cart
@ -309,36 +322,156 @@ final class cart extends core
// Initializing summary data of the cart
$summary = $this->cart?->summary(currency: $this->currency, errors: $this->errors['cart']);
// Initializing response headers
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Initializing of the output buffer
ob_start();
// Generating the reponse
echo json_encode(
[
// Sending response
$this->response
->start()
->clean()
->sse()
->json([
'cost' => $summary['cost'] ?? 0,
'amount' => $summary['amount'] ?? 0,
'errors' => $this->errors
]
);
])
->validate($this->request)
?->body()
->end();
// Initializing a response headers
header('Content-Length: ' . ob_get_length());
// Sending and deinitializing of the output buffer
ob_end_flush();
flush();
// Exit (success)
return null;
// Deinitializing summary data of the cart
unset($summary);
}
}
// Exit (fail)
// Exit (success/fail)
return null;
}
/**
* Share
*
* Share the cart
*
* @return null
*/
public function share(): null
{
if (str_contains($this->request->headers['accept'], content::json->value)) {
// Request for JSON response
// Initializing the cart
$this->cart ??= $this->account?->cart() ?? $this->session?->cart();
if ($this->cart instanceof model) {
// Initialized the cart
if ($share = $this->cart->share ?? $this->cart->share()) {
// The cart is available for sharing
// Sending response
$this->response
->start()
->clean()
->sse()
->json([
'share' => $share,
'errors' => $this->errors
])
->validate($this->request)
?->body()
->end();
}
// Deinitializing unnecessary variables
unset($hash);
}
}
// Exit (success/fail)
return null;
}
/**
* Pay
*
* Pay for the cart
*
* @return null
*/
public function pay(): null
{
if (str_contains($this->request->headers['accept'], content::json->value)) {
// Request for JSON response
// Initializing the cart
$this->cart ??= $this->account?->cart() ?? $this->session?->cart();
if ($this->cart instanceof model) {
// Initialized the cart
if ($share = $this->cart->share ?? $this->cart->share()) {
// The cart is available for sharing
// Sending response
$this->response
->start()
->clean()
->sse()
->json([
'share' => $share,
'errors' => $this->errors
])
->validate($this->request)
?->body()
->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)
return null;
}
}

View File

@ -10,8 +10,14 @@ use mirzaev\arming_bot\controllers\core,
mirzaev\arming_bot\models\entry,
mirzaev\arming_bot\models\category,
mirzaev\arming_bot\models\product,
mirzaev\arming_bot\models\cart,
mirzaev\arming_bot\models\menu;
// Framework for PHP
use mirzaev\minimal\http\enumerations\content,
mirzaev\minimal\http\enumerations\protocol,
mirzaev\minimal\http\request;
// Library for ArangoDB
use ArangoDBClient\Document as _document;
@ -20,13 +26,27 @@ use ArangoDBClient\Document as _document;
*
* @package mirzaev\arming_bot\controllers
*
* @param cart|null $cart Instance of the cart
* @param array $errors Registry of errors
*
* @method null index(?string $product, ?string $category, ?string $brand, ?string $sort, ?string $text) Catalog
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class catalog extends core
{
/**
* Registry of errors
* Cart
*
* @var model|null $cart Instance of the cart
*/
protected readonly ?cart $cart;
/**
* Errors
*
* @var array $errors Registry of errors
*/
protected array $errors = [
'session' => [],
@ -38,203 +58,124 @@ final class catalog extends core
/**
* Catalog
*
* @param array $parameters Parameters of the request (POST + GET)
* Browsing the catalog and receiving product data at the same time
*
* Receiving product data is necessary so that you can simultaneously open the product card at the desired location when opening the page
*
* @param string|null $product Product identifier (&product=1)
* @param string|null $category Category identifier (&category=1)
* @param string|null $brand Brand name (&brand=mirzaev)
* @param string|null $sort Sort typ (&sort=cost)
* @param string|null $text Search text (&text=zaloopa)
*
* @return null
*/
public function index(array $parameters = []): ?string
{
// validating
if (isset($parameters['product']) && preg_match('/[\d]+/', $parameters['product'], $matches)) $product = (int) $matches[0];
public function index(
?string $product = null,
?string $category = null,
?string $brand = null,
?string $sort = null,
?string $text = null
): null {
// Initializing the cart
$this->cart ??= $this->account?->cart() ?? $this->session?->cart();
// Initializing the cart data
$this->view->cart = [
'products' => $this->cart?->products(language: $this->language, currency: $this->currency)
];
// Validating received product identifier
if (isset($product) && preg_match('/[\d]+/', $product, $matches)) $product = (int) $matches[0];
else unset($product);
if (isset($product)) {
// Received and validated identifier of the product
// Received and validated product identifier
// Search for the product data and write to the buffer of global variables of view templater
$this->view->product = product::read(
filter: "d.identifier == @identifier && d.deleted != true && d.hidden != true",
sort: 'd.created DESC',
amount: 1,
return: '{identifier: d.identifier, name: d.name.@language, description: d.description.@language, cost: d.cost.@currency, weight: d.weight, dimensions: d.dimensions, brand: d.brand.@language, compatibility: d.compatibility.@language, cost: d.cost.@currency, images: d.images[*].storage}',
return: '{_id: d._id, identifier: d.identifier, name: d.name.@language, description: d.description.@language, cost: d.cost.@currency, weight: d.weight, dimensions: d.dimensions, brand: d.brand.@language, compatibility: d.compatibility.@language, cost: d.cost.@currency, images: {"200": d.images[*].storage["200"], "800": d.images[*].storage["800"]}}',
language: $this->language,
currency: $this->currency,
parameters: ['identifier' => $product],
errors: $this->errors['catalog']
)?->getAll();
);
// This is only for generate product card @todo need to move that in backed templated
if (!empty($this->view->product)) {
// Initialized the product
// Writing data about being in the cart
$this->view->product = [
'cart' => [
'amount' => $this->view->cart['products'][$this->view->product->getId()]['amount'] ?? 0,
'text' => [
'add' => $this->language === language::ru ? 'Добавить в корзину' : 'Add to the cart',
'added' => $this->language === language::ru ? 'Добавлено в корзину' : 'Added to the cart'
]
]
] + $this->view->product->getAll();
}
}
// Intializing buffer of query parameters
$_parameters = [];
$parameters = [];
// Initializing buffer of filters query (AQL)
$_filters = 'd.deleted != true && d.hidden != true';
// Validating
if (isset($parameters['brand']) && preg_match('/[\w\s]+/u', urldecode($parameters['brand']), $matches)) $brand = $matches[0];
// Validating received brand name
if (isset($brand) && preg_match('/[\w\s]+/u', urldecode($brand), $matches)) $brand = $matches[0];
else unset($brand);
if (isset($brand)) {
// Received and validated filter by brand
// Writing to the account buffer
$this->account?->write(
[
'catalog' => [
'filters' => [
'brand' => $brand
]
]
]
);
// Writing to the session buffer
$this->session?->write(
[
'catalog' => [
'filters' => [
'brand' => $brand
]
]
]
);
// Writing to the buffer of filters query (AQL)
$_filters .= ' && d.brand.@language == @brand';
// Writing to the buffer of query parameters
$_parameters['brand'] = $brand;
} else {
// Not received or not validated filter by brand
// Writing to the account buffer
$this->account?->write(
[
'catalog' => [
'filters' => [
'brand' => null
]
]
]
);
// Writing to the session buffer
$this->session?->write(
[
'catalog' => [
'filters' => [
'brand' => null
]
]
]
);
$parameters['brand'] = $brand;
}
// Writing to the session buffer
$this->core->request(new request('PATCH', '/session/write', protocol::http_3, parameters: ['catalog_filters_brand' => $brand ?? null]));
// Writing to the account buffer
$this->core->request(new request('PATCH', '/account/write', protocol::http_3, parameters: ['catalog_filters_brand' => $brand ?? null]));
// Writing to the current implementator of the buffer
$this->session->buffer = ['catalog' => ['filters' => ['brand' => $brand ?? null]]] + $this->session->buffer;
// Initialize buffer of filters query (AQL)
$_sort = 'd.position ASC, d.name ASC, d.created DESC';
// Validating
if (isset($parameters['sort']) && preg_match('/[\w\s]+/u', $parameters['sort'], $matches)) $sort = $matches[0];
// Validating received sort
if (isset($sort) && preg_match('/[\w\s]+/u', $sort, $matches)) $sort = $matches[0];
else unset($sort);
if (isset($sort)) {
// Received and validated sort
// Writing to the account buffer
$this->account?->write(
[
'catalog' => [
'sort' => $sort
]
]
);
// Writing to the session buffer
$this->session?->write(
[
'catalog' => [
'sort' => $sort
]
]
);
// Write to the buffer of sort query (AQL)
$_sort = "d.@sort DESC, $_sort";
// Write to the buffer of query parameters
$_parameters['sort'] = $sort;
} else {
// Not received or not validated filter by brand
// Writing to the account buffer
$this->account?->write(
[
'catalog' => [
'sort' => null
]
]
);
// Writing to the session buffer
$this->session?->write(
[
'catalog' => [
'sort' => null
]
]
);
$parameters['sort'] = $sort;
}
// Validating @todo add throwing errors
if (isset($parameters['text']) && preg_match('/[\w\s]+/u', urldecode($parameters['text']), $matches) && mb_strlen($matches[0]) > 2) $text = $matches[0];
// Writing to the session buffer
$this->core->request(new request('PATCH', '/session/write', protocol::http_3, parameters: ['catalog_sort' => $sort ?? null]));
if (isset($text)) {
// Received and validated text
// Writing to the account buffer (useless becouse rewrite itself to null with every request)
$this->account?->write(
[
'catalog' => [
'search' => [
'text' => $text
]
]
]
);
// Writing to the session buffer (useless becouse rewrite itself to null with every request)
$this->session?->write(
[
'catalog' => [
'search' => [
'text' => $text
]
]
]
);
} else {
// Not received or not validated filter by brand
// Writing to the account buffer
$this->account?->write(
[
'catalog' => [
'search' => [
'text' => null
]
]
]
);
// Writing to the session buffer
$this->session?->write(
[
'catalog' => [
'search' => [
'text' => null
]
]
]
);
}
// Writing to the account buffer
$this->core->request(new request('PATCH', '/account/write', protocol::http_3, parameters: ['catalog_sort' => $sort ?? null]));
// Validating
if (isset($parameters['category']) && preg_match('/[\d]+/', $parameters['category'], $matches)) $category = (int) $matches[0];
if (isset($category) && preg_match('/[\d]+/', $category, $matches)) $category = (int) $matches[0];
else unset($category);
if (isset($category)) {
// Received and validated identifier of the category
@ -284,7 +225,7 @@ final class catalog extends core
// Deleting buffers
unset($category, $product);
}
} else if (!isset($parameters['category'])) {
} else if (!isset($category)) {
// Not received identifier of the category
// search for root ascendants categories
@ -296,6 +237,16 @@ final class catalog extends core
) ?? null;
}
// Validating @todo add throwing errors
if (isset($text) && preg_match('/[\w\s]+/u', urldecode($text), $matches) && mb_strlen($matches[0]) > 2) $text = $matches[0];
else unset($text);
// Writing to the session buffer
$this->core->request(new request('PATCH', '/session/write', protocol::http_3, parameters: ['catalog_search_text' => $text ?? null]));
// Writing to the account buffer
$this->core->request(new request('PATCH', '/account/write', protocol::http_3, parameters: ['catalog_search_text' => $text ?? null]));
if (isset($brand) || isset($text) || (isset($this->view->products) && count($this->view->products) > 0)) {
// Received and validated at least one of filters or amount of rendered products is more than 0
@ -323,7 +274,7 @@ final class catalog extends core
return: 'DISTINCT MERGE(d, {name: d.name.@language, description: d.description.@language, compatibility: d.compatibility.@language, brand: d.brand.@language, cost: d.cost.@currency})',
language: $this->language,
currency: $this->currency,
parameters: $_parameters,
parameters: $parameters,
errors: $this->errors['catalog']
);
}
@ -344,8 +295,56 @@ final class catalog extends core
);
}
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
// GET request
if (str_contains($this->request->headers['accept'], content::json->value)) {
// Request for JSON response
// Initializing the response body buffer
$body = [
'title' => $this->language === language::ru ? 'Каталог' : 'Catalog' // @see https://git.mirzaev.sexy/mirzaev/huesos/issues/1
];
if (isset($this->view->categories)) {
// Initialized categories
// Render HTML-code of categories and write to the response body buffer
$body['categories'] = $this->view->render('catalog/elements/categories.html');
}
if (isset($this->view->product)) {
// Initialized product
// Writing data of the product to the response body buffer @todo GENERATE THIS ON THE SERVER
$body['product'] = $this->view->product;
}
if (isset($this->view->products)) {
// Initialized products
// Render HTML-code of products and write to the response body buffer
$body['products'] = $this->view->render('catalog/elements/products.html');
}
if (isset($this->view->filters)) {
// Initialized filters
// Render HTML-code of filters and write to the response body buffer
$body['filters'] = $this->view->render('catalog/elements/filters.html');
}
// Sending response
$this->response
->start()
->clean()
->sse()
->json($body + ['errors' => $this->errors])
->validate($this->request)
?->body()
->end();
// Deinitializing the response body buffer
unset($body);
} else if (str_contains($this->request->headers['accept'], content::any->value)) {
// Request for any response
if (!empty($this->view->product)) {
// Initialized the product data
@ -363,73 +362,26 @@ final class catalog extends core
] + ($this->view->javascript ?? []);
}
// Exit (success)
return $this->view->render('catalog/page.html', [
// Render page
$page = $this->view->render('catalog/page.html', [
'h2' => $this->language === language::ru ? 'Каталог' : 'Catalog' // @see https://git.mirzaev.sexy/mirzaev/huesos/issues/1
]);
} else if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// POST request
// Initializing the buffer of response
$response = [
'title' => $this->language === language::ru ? 'Каталог' : 'Catalog' // @see https://git.mirzaev.sexy/mirzaev/huesos/issues/1
];
// Sending response
$this->response
->start()
->clean()
->sse()
->write($page)
->validate($this->request)
?->body()
->end();
if (isset($this->view->categories)) {
// Initialized categories
// Render HTML-code of categories and write to the response buffer
$response['categories'] = $this->view->render('catalog/elements/categories.html');
}
if (isset($this->view->product)) {
// Initialized product
// Writing data of the product to the response buffer @todo GENERATE THIS ON THE SERVER
$response['product'] = $this->view->product;
}
if (isset($this->view->products)) {
// Initialized products
// Render HTML-code of products and write to the response buffer
$response['products'] = $this->view->render('catalog/elements/products.html');
}
if (isset($this->view->filters)) {
// Initialized filters
// Render HTML-code of filters and write to the response buffer
$response['filters'] = $this->view->render('catalog/elements/filters.html');
}
// Initializing a response headers
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Initializing of the output buffer
ob_start();
// Generating the reponse
echo json_encode(
$response + [
'errors' => $this->errors
]
);
// Initializing a response headers
header('Content-Length: ' . ob_get_length());
// Sending and deinitializing of the output buffer
ob_end_flush();
flush();
// Exit (success)
return null;
// Deinitializing rendered page
unset($page);
}
// Exit (fail)
// Exit (success/fail)
return null;
}
}

View File

@ -16,55 +16,90 @@ use mirzaev\arming_bot\views\templater,
mirzaev\arming_bot\models\enumerations\currency;
// Framework for PHP
use mirzaev\minimal\controller;
use mirzaev\minimal\core as minimal,
mirzaev\minimal\controller,
mirzaev\minimal\http\response,
mirzaev\minimal\http\enumerations\status;
/**
* Core of controllers
*
* @package mirzaev\arming_bot\controllers
*
* @param settings $settings Instance of the settings
* @param session $session Instance of the session
* @param account|null $account Instance of the account
* @param cart|null $cart Instance of the cart
* @param language $language Language
* @param currency $currency Currency
* @param response $response Response
* @param array $errors Registry of errors
*
* @method void __construct(minimal $core, bool $initialize) Constructor
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
class core extends controller
{
/**
* Postfix for name of controllers files
*/
final public const string POSTFIX = '';
/**
* Instance of the settings
* Settings
*
* @var settings $settings Instance of the settings
*/
protected readonly settings $settings;
/**
* Instance of the session
* Session
*
* @var session|null $session Instance of the session
*/
protected readonly session $session;
/**
* Instance of the account
* Account
*
* @var account|null $account Instance of the account
*/
protected readonly ?account $account;
/**
* Instance of the cart
* Cart
*
* @var cart|null $cart Instance of the cart
*/
protected readonly ?cart $cart;
/**
* Language
*
* @var language $language Language
*/
protected language $language = language::en;
/**
* Currency
*
* @var currency $currency Currency
*/
protected currency $currency = currency::usd;
/**
* Registry of errors
* Response
*
* @see https://wiki.php.net/rfc/property-hooks (find a table about backed and virtual hooks)
*
* @var response $response Response
*/
protected response $response {
// Read
get => $this->response ??= $this->request->response();
}
/**
* Errors
*
* @var array $errors Registry of errors
*/
protected array $errors = [
'session' => [],
@ -72,8 +107,9 @@ class core extends controller
];
/**
* Constructor of an instance
* Constructor
*
* @param minimal $core Initialize a controller?
* @param bool $initialize Initialize a controller?
*
* @return void
@ -82,13 +118,13 @@ class core extends controller
* 1. settings account и session не имеют проверок на возврат null
* 2. TRANSIT EVERYTHING TO MIDDLEWARES
*/
public function __construct(bool $initialize = true)
public function __construct(minimal $core, bool $initialize = true)
{
// Blocking requests from CloudFlare (better to write this blocking into nginx config file)
if (isset($_SERVER['HTTP_USER_AGENT']) && $_SERVER['HTTP_USER_AGENT'] === 'nginx-ssl early hints') return;
if (isset($_SERVER['HTTP_USER_AGENT']) && $_SERVER['HTTP_USER_AGENT'] === 'nginx-ssl early hints') return status::bruh->label;
// For the extends system
parent::__construct($initialize);
parent::__construct(core: $core);
if ($initialize) {
// Initializing is requested
@ -107,9 +143,7 @@ class core extends controller
// Handle a problems with initializing a session
if (!empty($this->errors['session'])) exit(1);
// телеграм не сохраняет куки
/* else if ($_COOKIE["session"] !== $this->session->hash) {
else if ($_COOKIE["session"] !== $this->session->hash) {
// Hash of the session is changed (implies that the session has expired and recreated)
// Write a new hash of the session to cookies
@ -124,7 +158,10 @@ class core extends controller
'samesite' => 'strict'
]
);
} */
}
// Initializing registry of account errors
$this->errors['account'];
// Initializing of the account
$this->account = $this->session->account($this->errors['account']);
@ -204,21 +241,4 @@ class core extends controller
}
}
}
/**
* Check of initialization
*
* Checks whether a property is initialized in a document instance from ArangoDB
*
* @param string $name Name of the property from ArangoDB
*
* @return bool The property is initialized?
*/
public function __isset(string $name): bool
{
// Check of initialization of the property and exit (success)
return match ($name) {
default => isset($this->{$name})
};
}
}

View File

@ -1,78 +0,0 @@
<?php
declare(strict_types=1);
namespace mirzaev\arming_bot\controllers;
// Files of the project
use mirzaev\arming_bot\controllers\core,
mirzaev\arming_bot\models\enumerations\language,
mirzaev\arming_bot\models\cart,
mirzaev\arming_bot\models\product,
mirzaev\arming_bot\models\menu;
// Framework for ArangoDB
use mirzaev\arangodb\document;
/**
* Controller of cdek
*
* @package mirzaev\arming_bot\controllers
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class cdek extends core
{
/**
* Registry of errors
*/
protected array $errors = [
'delivery' => []
];
/**
* Calculate
*
* @param array $parameters Parameters of the request (POST + GET)
*/
public function calculate(array $parameters = []): ?string
{
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// POST request
$this->model::calculate();
die;
// Initializing a response headers
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Initializing of the output buffer
ob_start();
// Generating the reponse
echo json_encode(
[
'main' => '',
'errors' => $this->errors
]
);
// Initializing a response headers
header('Content-Length: ' . ob_get_length());
// Sending and deinitializing of the output buffer
ob_end_flush();
flush();
// Exit (success)
return null;
}
// Exit (fail)
return null;
}
}

View File

@ -0,0 +1,566 @@
<?php
declare(strict_types=1);
namespace mirzaev\arming_bot\controllers;
// Files of the project
use mirzaev\arming_bot\controllers\core,
mirzaev\arming_bot\models\cart,
mirzaev\arming_bot\models\deliveries\cdek;
// Framework for PHP
use mirzaev\minimal\http\enumerations\content,
mirzaev\minimal\http\enumerations\protocol,
mirzaev\minimal\http\request;
// Build-in libraries
use DateTime as datetime;
/**
* Controller of delivery
*
* @package mirzaev\arming_bot\controllers
*
* @param model|null $cart Instance of the cart
* @param array $errors Registry of errors
*
* @method null write(string|null $company, ?string $location, ?string $street) Validate and write delivery data to account and session buffers
* @method null calculate() Calculate delivery by validated data from buffers
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class delivery extends core
{
/**
* Cart
*
* @var model|null $cart Instance of the cart
*/
protected readonly ?cart $cart;
/**
* Errors
*
* @var array $errors Registry of errors
*/
protected array $errors = [
'session' => [],
'account' => [],
'buffer' => [],
'delivery' => []
];
/**
* Write
*
* Validate and write delivery data to account and session buffers
*
* @param string|null $company Name of delivery company
* @param ?string $location location
* @param ?string $street Address with street and house
*
* @return null
*/
public function write(
/* string|company|null $company = null, */
string|null $company = null,
/* string|location|null $location = null, */
?string $location = null,
?string $street = null
): null {
if (str_contains($this->request->headers['accept'], content::json->value)) {
// Request for JSON response
// Declaring response buffer
$response = [
'ready' => false
];
if (!empty($company)) {
// Received company name
if (mb_strlen($company) < 65) {
// Validated company name
// Declating variable for normalized value of company namme
$normalized = '';
// Normalizing company name
if (preg_match('/[\w]+/u', urldecode($company), $matches)) $normalized = $matches[0] ?? '';
// Deinitializing unnecessary variables
unset($matches);
// Writing to the session buffer
$this->core->request(new request('PATCH', '/session/write', protocol::http_3, parameters: ['delivery_company' => $normalized]));
// Writing to the account buffer
$this->core->request(new request('PATCH', '/account/write', protocol::http_3, parameters: ['delivery_company' => $normalized]));
// Writing to the buffer of the response
/* $response['company'] = $normalized; */
// Initialization buffer of delivery parameters
$delivery = $this->account?->buffer['delivery'] ?? $this->session?->buffer['delivery'] ?? [];
if (isset($delivery[$normalized])) {
// Initialized delivery company data
if (!empty($delivery[$normalized]['location']) && !empty($delivery[$normalized]['street'])) {
// Required parameters initialized: company, location, street
// Writing readiness status to the buffer of the response
$response['ready'] = true;
}
}
// Deinitializing unnecessary variables
unset($delivery);
// Writing status of execution to the buffer of the response
$response['status'] = 'success';
// Deinitializing variable for normalized value of the parameter
unset($normalized);
}
} else if (!empty($location)) {
// Received location name
if (mb_strlen($location) < 257) {
// Validated location name
// Declating variable for result value of location name
$result = '';
// Declating variable for separated values of location name
$separated = '';
// Separating location name
if (preg_match_all('/[^\W][\w\s\-]+/u', trim(urldecode($location)), $matches)) $separated = $matches[0];
// Deinitializing unnecessary variables
unset($matches);
if (!empty($separated)) {
// Serapated location name
// Declaring variable for normalized separated values of location name
$normalized = [];
// Normalizing location name
foreach ($separated as $value) $normalized[] = mb_ucfirst($value);
// Deinitializing unnecessary variables
unset($separated, $value);
// Initialization buffer of delivery parameters
$delivery = $this->account?->buffer['delivery'] ?? $this->session?->buffer['delivery'] ?? [];
if (isset($delivery['company'])) {
// Initialized delivery company
// Declaring of universalized locations buffer
$locations = null;
if ($delivery['company'] === 'cdek') {
// Delivery by CDEK
// Searching for locations by name (first part with spaces before first comma or any non word symbol)
$cdek = cdek::location($normalized[0], $this->errors['delivery'])?->items;
foreach ($cdek ?? [] as $location) {
// Iterating over found locations
// Universalizing and writing to locations buffer
$locations[] = [
'identifier' => $location->code,
'name' => $location->city,
'structure' => [$location->country, $location->region, $location->sub_region],
'longitude' => $location->longitude,
'latitude' => $location->latitude,
'data' => $location
];
}
// Deinitializing of response data from CDEK
unset($cdek, $location);
}
if (!empty($locations)) {
// Found locations
// Declaring buffer of validated locations by input values
$buffer = [];
// Initialization of 80% of received input values (required minimum to match location characteristics)
$minimum = count($normalized) * 80 / 100;
foreach ($locations as $location) {
// Iterating over locations
// Declaring variable with score of matching input values with location characteristics
$score = 0;
foreach ([$location['name'], ...$location['structure']] as $value) {
// Iterating over location characteristics
foreach ($normalized as $_value) {
// Iterating over normalized parts of location name
// Match normalized parts of location name with location characteristics using the Levenshtein algorithm
$match = levenshtein($value, $_value);
if ($match < 3) {
// The values are approximately the same
if (++$score > $minimum) {
// More than $minimum matches found
// Writing to buffer of validated locations by normalized parts of location name
$buffer[] = $location;
// Exit from iteration of location characteristics
break 2;
}
}
}
// Deinitializing unnecessary variables
unset($_value);
}
// Deinitializing unnecessary variables
unset($value);
}
// Deinitializing unnecessary variables
unset($location);
// Reinitializating locations from buffer of validated locations by input values
$locations = $buffer;
if (count($locations) === 1) {
// Identificated location
// Writing to the session buffer
$this->core->request(new request('PATCH', '/session/write', protocol::http_3, parameters: [$name = 'delivery_' . $delivery['company'] . '_location' => $locations[0]]));
// Writing to the account buffer
$this->core->request(new request('PATCH', '/account/write', protocol::http_3, parameters: [$name => $locations[0]]));
// Deinitializing unnecessary variables
unset($name);
// Writing location data to the buffer of the response
/* $response['location'] = [
'identifier' => $locations[0]['identifier'],
'input' => $result = $locations[0]['name'] . ', ' . implode(', ', array_reverse($locations[0]['structure']))
]; */
if (!empty($delivery[$delivery['company']]['street'])) {
// Required parameters initialized: company, location, street
// Writing readiness status to the buffer of the response
$response['ready'] = true;
}
// Writing status of execution to the buffer of the response
$response['status'] = 'success';
// Writing locations into response buffer
$response['locations'] = [];
} else {
// Not identificated location
// Writing to the session buffer
$this->core->request(new request('PATCH', '/session/write', protocol::http_3, parameters: [$name = 'delivery_' . $delivery['company'] . '_location' => null]));
// Writing to the account buffer
$this->core->request(new request('PATCH', '/account/write', protocol::http_3, parameters: [$name => null]));
// Deinitializing unnecessary variables
unset($name);
// Declaring buffer of data to send
$buffer = [];
foreach ($locations as $location) {
// Iterating over locations
// Writing to buffer of data to send
$buffer[] = [
'name' => $location['name'],
'structure' => $location['structure']
];
}
// Deinitializing unnecessary variables
unset($location);
// Writing locations into response buffer
$response['locations'] = $buffer;
// Deinitializing buffer of data to send
unset($buffer);
}
} else {
// Not found locations
// Writing to the session buffer
$this->core->request(new request('PATCH', '/session/write', protocol::http_3, parameters: [$name = 'delivery_' . $delivery['company'] . '_location' => null]));
// Writing to the account buffer
$this->core->request(new request('PATCH', '/account/write', protocol::http_3, parameters: [$name => null]));
// Deinitializing unnecessary variables
unset($name);
}
// Deinitializing unnecessary variables
unset($locations);
}
// Deinitializing unnecessary variables
unset($delivery, $normalized);
}
// Writing to the session buffer
$this->core->request(new request('PATCH', '/session/write', protocol::http_3, parameters: ['delivery_location' => $result]));
// Writing to the account buffer
$this->core->request(new request('PATCH', '/account/write', protocol::http_3, parameters: ['delivery_location' => $result]));
// Deinitializing unnecessary variables
unset($result, $normalized);
}
} else if (!empty($street)) {
// Received sreet
if (mb_strlen($street) < 129) {
// Validated street
// Declating variable for normalized value of the parameter
$normalized = '';
// Normalizing street
if (preg_match('/[\w\d\s]+/u', urldecode($street), $matches)) $normalized = mb_ucfirst($matches[0] ?? '');
// Deinitializing unnecessary variables
unset($matches);
// Initialization buffer of delivery parameters
$delivery = $this->account?->buffer['delivery'] ?? $this->session?->buffer['delivery'] ?? [];
if (isset($delivery['company'])) {
// Initialized delivery company
// Writing to the session buffer
$this->core->request(new request('PATCH', '/session/write', protocol::http_3, parameters: [$name = 'delivery_' . $delivery['company'] . '_street' => $normalized]));
// Writing to the account buffer
$this->core->request(new request('PATCH', '/account/write', protocol::http_3, parameters: [$name => $normalized]));
// Deinitializing unnecessary variables
unset($name);
if (!empty($delivery[$delivery['company']]['location'])) {
// Required parameters initialized: company, location, street
// Writing readiness status to the buffer of the response
$response['ready'] = true;
}
// Writing status of execution to the buffer of the response
$response['status'] = 'success';
}
// Deinitializing unnecessary variables
unset($delivery);
// Writing to the session buffer
$this->core->request(new request('PATCH', '/session/write', protocol::http_3, parameters: ['delivery_street' => $normalized]));
// Writing to the account buffer
$this->core->request(new request('PATCH', '/account/write', protocol::http_3, parameters: ['delivery_street' => $normalized]));
// Writing street to the buffer of the response
/* $response['street'] = $normalized; */
// Deinitializing unnecessary variables
unset($normalized);
}
}
// Sending response
$this->response
->start()
->clean()
->sse()
->json($response + [
'errors' => $this->errors
])
->validate($this->request)
?->body()
->end();
// Deinitializing response buffer
unset($response);
}
// Exit (success/fail)
return null;
}
/**
* Calculate
*
* Calculate delivery by validated data from buffers
* and write to the cart buffer
*
* @return null
*/
public function calculate(): null
{
if (str_contains($this->request->headers['accept'], content::json->value)) {
// Request for JSON response
// Initialization buffer of delivery parameters
$delivery = $this->account?->buffer['delivery'] ?? $this->session?->buffer['delivery'] ?? [];
if (isset($delivery['company'])) {
// Initialized delivery company
// Declaring response buffer
$response = [];
if (!empty($delivery[$delivery['company']]['location']) && !empty($delivery[$delivery['company']]['street'])) {
// Required parameters initialized: company, location, street
if ($delivery['company'] === 'cdek') {
// Delivery by CDEK
// Initializing the cart
$this->cart ??= $this->account?->cart() ?? $this->session?->cart();
// Initializing products in the cart
$products = $this->cart?->products(language: $this->language, currency: $this->currency);
if (!empty($products)) {
// Initialized products
// Declaring buffer of formatted products
$formatted = [];
foreach ($products as $product) {
// Iterating over products
// Formatting products
for ($i = 0; $i < $product['amount']; ++$i) {
$formatted[] = [
'weight' => $product['document']['weight'] ?? 0,
...$product['document']['dimensions']
];
}
}
// Deinitializing unnecessary variables
unset($products, $product);
if (!empty($formatted)) {
// Formatted products
// Calculating delivery by validated data from buffers
$cdek = @cdek::calculate(
from_location: 248,
from_street: 'Екатерининская 116', // @todo issues #13
to_location: $delivery[$delivery['company']]['location']['identifier'],
to_street: $delivery[$delivery['company']]['street'],
tariff: 368, // ИМ : warehouse-terminal (склад-постамат)
// tariff: 486, // Обычная доставка : warehouse-terminal (склад-постамат)
products: $formatted,
date: new datetime(), // @todo weekdays only? + timezones
errors: $this->errors['delivery']
);
// Deinitializing unnecessary variables
unset($formatted);
if ($cdek) {
// Calculated delivery
// Writing to the session buffer
$this->core->request(
new request(
'PATCH',
'/session/write',
protocol::http_3,
parameters: [
'delivery_' . $delivery['company'] . '_cost' => $cdek->total_sum,
'delivery_' . $delivery['company'] . '_days' => $cdek->calendar_max
]
)
);
// Writing to the account buffer
$this->core->request(
new request(
'PATCH',
'/account/write',
protocol::http_3,
parameters: [
'delivery_' . $delivery['company'] . '_cost' => $cdek->total_sum,
'delivery_' . $delivery['company'] . '_days' => $cdek->calendar_max
]
)
);
// Writing to the cart buffer
$this->cart->buffer_write(['delivery' => [
'company' => 'cdek',
'location' => [
'identifier' => $delivery[$delivery['company']]['location']['identifier'],
'name' => $delivery[$delivery['company']]['location']['name'],
],
'street' => $delivery[$delivery['company']]['street'],
'cost' => $cdek->total_sum,
'days' => $cdek->calendar_max
]], $this->errors['buffer']);
// Writing to response buffer
$response['cost'] = $cdek->total_sum;
$response['days'] = $cdek->calendar_max;
}
}
// Deinitializing unnecessary variables
unset($formatted);
}
}
}
// Sending response
$this->response
->start()
->clean()
->sse()
->json($response + [
'errors' => $this->errors
])
->validate($this->request)
?->body()
->end();
// Deinitializing response buffer
unset($response);
}
// Deinitializing unnecessary variables
unset($delivery);
}
// Exit (success/fail)
return null;
}
}

View File

@ -6,30 +6,79 @@ namespace mirzaev\arming_bot\controllers;
// Files of the project
use mirzaev\arming_bot\controllers\core,
mirzaev\arming_bot\models\product;
mirzaev\arming_bot\models\menu;
// Framework for PHP
use mirzaev\minimal\http\enumerations\content;
/**
* Index controller
* Controller of pages
*
* @package mirzaev\arming_bot\controllers
* @param array $errors Registry of errors
*
* @method null offer() Public offer
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class index extends core
{
/**
* Errors
*
* @var array $errors Registry of errors
*/
protected array $errors = [
'session' => [],
'account' => [],
'menu' => []
];
/**
* Render the main page
* Public offer
*
* @param array $parameters Parameters of the request (POST + GET)
* @return null
*/
public function index(array $parameters = []): ?string
public function offer(): null
{
// Exit (success)
if ($_SERVER['REQUEST_METHOD'] === 'GET') return $this->view->render('index.html');
if (isset($menu)) {
//
// Exit (fail)
} else {
// Not received ... menu
// Search for filters and write to the buffer of global variables of view templater
$this->view->menu = menu::_read(
return: 'MERGE(d, { name: d.name.@language })',
sort: 'd.style.order ASC, d.created DESC, d._key DESC',
amount: 4,
parameters: ['language' => $this->language->name],
errors: $this->errors['menu']
);
}
if (str_contains($this->request->headers['accept'], content::any->value)) {
// Request for any response
// Render page
$page = $this->view->render('offer/page.html');
// 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

@ -6,9 +6,12 @@ namespace mirzaev\arming_bot\controllers;
// Files of the project
use mirzaev\arming_bot\controllers\core,
mirzaev\arming_bot\models\session as model,
mirzaev\arming_bot\models\account;
// Framework for PHP
use mirzaev\minimal\http\enumerations\content,
mirzaev\minimal\http\enumerations\status;
// Framework for ArangoDB
use mirzaev\arangodb\document;
@ -17,28 +20,52 @@ use mirzaev\arangodb\document;
*
* @package mirzaev\arming_bot\controllers
*
* @param array $errors Registry of errors
*
* @method null write(string ...$parameters) Write to the session buffer
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class session extends core
{
/**
* Registry of errors
* Errors
*
* @var array $errors Registry of errors
*/
protected array $errors = [
'session' => [],
'account' => []
'account' => [],
'buffer' => []
];
/**
* Connect session to the telegram account
*
* @param array $parameters Parameters of the request (POST + GET)
* @see https://core.telegram.org/bots/webapps#initializing-mini-apps
* @see https://core.telegram.org/bots/webapps#validating-data-received-via-the-mini-app
* @see https://core.telegram.org/bots/webapps#webappinitdata
*
* @param ?string $user JSON
* @param ?string $chat_instance
* @param ?string $chat_type
* @param ?string $auth_date
* @param ?string $hash
* @param ?string $query_id
*
* @return null
*/
public function telegram(array $parameters = []): ?string
{
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// POST request
public function telegram(
?string $user = null,
?string $chat_instance = null,
?string $chat_type = null,
?string $auth_date = null,
?string $hash = null,
?string $query_id = null
): null {
if (str_contains($this->request->headers['accept'], content::json->value)) {
// Request for JSON response
// Declaring variables in the correct scope
$identifier = $domain = $language = null;
@ -57,11 +84,11 @@ final class session extends core
} else {
// Not found the account
if (count($parameters) > 1 && isset($parameters['hash'])) {
if (isset($user, $chat_instance, $chat_type, $auth_date, $hash)) {
// Received required parameters
$buffer = $parameters;
$buffer = ['user' => $user, 'chat_instance' => $chat_instance, 'chat_type' => $chat_type, 'auth_date' => $auth_date];
unset($buffer['hash']);
ksort($buffer);
$prepared = [];
@ -73,17 +100,17 @@ final class session extends core
}
}
$key = hash_hmac('sha256', require(SETTINGS . DIRECTORY_SEPARATOR . 'key.php'), 'WebAppData', true);
$hash = bin2hex(hash_hmac('sha256', implode(PHP_EOL, $prepared), $key, true));
$key = hash_hmac('sha256', require(SETTINGS . DIRECTORY_SEPARATOR . 'telegram.php'), 'WebAppData', true);
$_hash = bin2hex(hash_hmac('sha256', implode(PHP_EOL, $prepared), $key, true));
if (hash_equals($hash, $parameters['hash'])) {
if (hash_equals($hash, $_hash)) {
// Data confirmed (according to telegram documentation)
if (time() - $parameters['auth_date'] < 86400) {
if (time() - $auth_date < 86400) {
// Authorization date less than 1 day ago
// Initializing data of the account
$data = json_decode($parameters['user']);
$data = json_decode($user);
// Initializing of the account
$account = account::initialize(
@ -96,7 +123,11 @@ final class session extends core
],
'domain' => $data->username,
'language' => $data->language_code,
'messages' => $data->allows_write_to_pm
'messages' => $data->allows_write_to_pm,
'chat' => [
'type' => $chat_type,
'instance' => $chat_instance
]
],
$this->errors['account']
);
@ -121,53 +152,42 @@ final class session extends core
}
}
// Initializing a response headers
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Initializing of the output buffer
ob_start();
// Generating the reponse
echo json_encode(
[
// Sending response
$this->response
->start()
->clean()
->sse()
->json([
'connected' => (bool) $connected,
'identifier' => $identifier ?? null,
'domain' => $domain ?? null,
'language' => $language?->name ?? null,
'errors' => $this->errors
]
);
// Initializing a response headers
header('Content-Length: ' . ob_get_length());
// Sending and deinitializing of the output buffer
ob_end_flush();
flush();
// Exit (success)
return null;
])
->validate($this->request)
?->body()
->end();
}
// Exit (fail)
// Exit (success/fail)
return null;
}
/**
* Write
*
* Write to the buffer
*
* @param array $parameters Parameters of the request (POST + GET)
* @param mixed ...$parameters Parameters for writing to the buffer
*
* @return void
* @return null
*
* @todo переделать под trait buffer
*/
public function write(array $parameters = []): void
public function write(mixed ...$parameters): null
{
if (!empty($parameters) && $this->session instanceof model) {
// Received parameters and initialized session
if (!empty($parameters) && isset($this->session)) {
// Received parameters and initialized model with buffer trait
// Declaring the buffer of deserialized parameters
$deserialized = [];
@ -176,21 +196,27 @@ final class session extends core
// Iterate over parameters
// Validation of the parameter value
if (mb_strlen($value) > 4096) continue;
if (mb_strlen(serialize($value)) > 4096) continue;
// Declaring the buffer of deserialized parameter
$parameter = null;
// Deserializing name to multidimensional array
foreach (array_reverse(explode('_', $name)) as $key)
$parameter = [$key => $parameter ?? (json_validate($value) ? json_decode($value, true, 10) : $value)];
foreach (array_reverse(explode('_', (string) $name)) as $key)
$parameter = [$key => $parameter ?? (is_string($value) && json_validate($value) ? json_decode($value, true, 10) : $value)];
// Writing into the buffer of deserialized parameters
$deserialized = array_merge_recursive($parameter, $deserialized);
}
// Write to the session document from ArangoDB
if (!empty($deserialized)) $this->session->write($deserialized, $this->errors['session']);
// Write to the document from ArangoDB
if (!empty($deserialized)) $this->session->write($deserialized, $this->errors['buffer']);
// Writing status of response
$this->response->status = status::created;
}
// Exit (success)
return null;
}
}

View File

@ -7,6 +7,7 @@ namespace mirzaev\arming_bot\models;
// Files of the project
use mirzaev\arming_bot\models\core,
mirzaev\arming_bot\models\reservation,
mirzaev\arming_bot\models\traits\buffer,
mirzaev\arming_bot\models\traits\document as document_trait,
mirzaev\arming_bot\models\interfaces\document as document_interface,
mirzaev\arming_bot\models\interfaces\collection as collection_interface,
@ -34,7 +35,9 @@ use exception;
*/
final class cart extends core implements document_interface, collection_interface
{
use document_trait;
use document_trait, buffer {
buffer::write as buffer_write;
}
/**
* Name of the collection in ArangoDB
@ -334,4 +337,113 @@ final class cart extends core implements document_interface, collection_interfac
];
}
}
/*
* Share
*
* Generate hash for sharing the cart
*
* @param array &$errors Registry of errors
*
* @return string|false Hash for sharing, if generated and writed to ArangoDB
*/
public function share(array &$errors = []): string|false
{
try {
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
// Initialized collection
// Generating hash and writing to the cart implement instance
$this->share = sodium_bin2hex(sodium_crypto_generichash($this->getId()));
if (document::update($this->__document(), errors: $errors)) {
// Writed to ArangoDB
// Exit (success)
return $this->share;
} else throw new exception('Failed to write confirmed cart to ArangoDB');
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
} 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;
}
/*
* Unshare
*
* Deleting hash for sharing the cart
*
* @param array &$errors Registry of errors
*
* @return bool Is the cart unshared?
*/
public function unshare(array &$errors = []): bool
{
try {
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
// Initialized collection
// Deleting hash and writing to the cart implement instance
$this->share = null;
if (document::update($this->__document(), errors: $errors)) {
// Writed to ArangoDB
// Exit (success)
return true;
} else throw new exception('Failed to write confirmed cart to ArangoDB');
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
} 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;
}
/*
* Order
*
*
*
* @param array &$errors Registry of errors
*
* @return void
*/
public function order(array &$errors = []): void
{
try {
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)) {
// Initialized collections
} else throw new exception('Failed to initialize ' . product::TYPE . ' collection: ' . product::COLLECTION);
} else throw new exception('Failed to initialize ' . reservation::TYPE . ' collection: ' . reservation::COLLECTION);
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
}
}

View File

@ -14,6 +14,9 @@ use mirzaev\arming_bot\models\core,
mirzaev\arming_bot\models\enumerations\currency,
mirzaev\arming_bot\models\traits\yandex\disk as yandex;
// Framework for PHP
use mirzaev\minimal\http\enumerations\content;
// Framework for ArangoDB
use mirzaev\arangodb\collection,
mirzaev\arangodb\document;
@ -24,6 +27,9 @@ use avadim\FastExcelReader\Excel as excel;
// Built-in libraries
use exception;
// GD library
use GdImage as image;
/**
* Model of the catalog
*
@ -143,10 +149,19 @@ final class catalog extends core
// Not initialized the category
// Creating the category
$_id = $created = category::write((int) $row['identifier'], [$language->name => $row['name']], $row['position'] ?? null, $errors);
$_id = $created = category::write(
identifier: (int) $row['identifier'],
name: [$language->name => $row['name']],
position: (int) $row['position'] ?? null,
errors: $errors
);
// Initializing the category
$category = category::_read('d._id == @_id', parameters: ['_id' => $_id], errors: $errors);
$category = category::_read(
filter: 'd._id == @_id',
parameters: ['_id' => $_id],
errors: $errors
);
// Incrementing the counter of created categories
if ($created) ++$categories_created;
@ -159,7 +174,11 @@ final class catalog extends core
// Received the ascendant category
// Initializing the ascendant category
$ascendant = category::_read('d.identifier == @identifier', parameters: ['identifier' => (int) $row['category']], errors: $errors);
$ascendant = category::_read(
filter: 'd.identifier == @identifier',
parameters: ['identifier' => (int) $row['category']],
errors: $errors
);
if ($ascendant instanceof category) {
// Found the ascendant category
@ -176,13 +195,14 @@ final class catalog extends core
// Received images
// Initializing new images of the category
$images = explode(' ', trim($row['images']));
$images = explode(' ', mb_trim($row['images']));
// 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); */
$reinitialize = true;
// Checking the identity of existing images with new images (if reinitialization of images has not yet been requested)
if (!$reinitialize) foreach ($category->images as $key => $image) if ($reinitialize = $image['source'] !== $images[$key]) break;
/* if (!$reinitialize) foreach ($category->images as $key => $image) if ($reinitialize = $image['source'] !== $images[$key]) break; */
if ($reinitialize) {
// Requested reinitialization of images
@ -190,35 +210,93 @@ final class catalog extends core
// Initializing the buffer of images
$buffer = [];
foreach ($images as $index => $image) {
foreach ($images as $index => $file) {
// Iterating over new images
// Skipping empty URI`s
if (empty($image = trim($image))) continue;
if (empty($file = mb_trim($file))) continue;
// Initializing path to directory of the image in storage
$directory = DIRECTORY_SEPARATOR . 'categories' . DIRECTORY_SEPARATOR . $row['identifier'];
// Initializing path to directory of images in storage
$directory = DIRECTORY_SEPARATOR . 'categories' . DIRECTORY_SEPARATOR . $row['identifier'] . DIRECTORY_SEPARATOR . $index;
// Initializing URL of the image in storage
$url = STORAGE . $directory;
// Initializing URN of the image in storage
$urn = $index . '.jpg';
// Initializing URI of the image in storage
$uri = $url . DIRECTORY_SEPARATOR . $urn;
// Initializing the directory in storage
if (!file_exists($url)) mkdir($url, 0775, true);
if (static::yandex($image, $uri, errors: $errors)) {
// The image is downloaded
if ($downloaded = static::yandex(
uri: $file,
destination: $url,
name: 'source',
errors: $errors
)) {
// The image is downloaded and initialized data of the image in storage
// Writing the image to the buffer if images
$buffer[] = [
'source' => $image,
'storage' => $directory . DIRECTORY_SEPARATOR . $urn
];
// Initializing URI of the image
$uri = $downloaded['destination'] . DIRECTORY_SEPARATOR . $downloaded['name'] . '.' . $downloaded['content']->extension();
// Initializing size of the image
$size = getimagesize($uri);
if ($downloaded['content'] === content::png) {
// PNG
// Initializing implementator of the image
$boba = imagecreatefrompng($uri);
} else if ($downloaded['content'] === content::jpeg) {
// JPEG
// Initializing implementator of the image
$boba = imagecreatefromjpeg($uri);
}
// Enabling better antialiasing
imageantialias($boba, true);
if ($boba instanceof image) {
// Initialized implementator of the image
// Initializing buffer of resized images
$resized = [];
foreach ([1400, 800, 400, 200] as $resize) {
// Iterating over sizes for creating images
if ($size[0] >= $size[1]) {
// The width ($size[0]) is longer than the height ($size[1])
$width = $resize;
$height = (int) ($resize * $size[1] / $size[0]);
} else {
// The height ($size[1]) is longer than the width ($size[0])
$width = (int) ($resize * $size[0] / $size[1]);
$height = $resize;
}
// Resizing the image
$biba = imagecreatetruecolor($width, $height);
imagecopyresampled($biba, $boba, 0, 0, 0, 0, $width, $height, $size[0], $size[1]);
// Initializing URI of the resized image
$uri = $directory . DIRECTORY_SEPARATOR . "$resize." . $downloaded['content']->extension();
// Saving the image
imagePng($biba, STORAGE . $uri);
// Writing the resized image to the buffer of resized images
$resized[$resize] = $uri;
}
// Writing the image to the buffer if images
$buffer[] = [
'source' => $file,
'storage' => [
'source' => $directory . DIRECTORY_SEPARATOR . $downloaded['name'] . '.' . $downloaded['content']->extension(),
] + $resized
];
}
}
}
@ -232,7 +310,7 @@ final class catalog extends core
// Incrementing the counter of updated categories
if ($updated && !$created) ++$categories_updated;
} else throw new exception("Failed to initialize category: {$row['name']} ($number)");
} else throw new exception('Failed to initialize category: ' . $row['name'] . " ($number)");
}
// Writing to the registry of handled categories and products
@ -313,15 +391,15 @@ final class catalog extends core
// Creating the product
$_id = product::write(
(int) $row['identifier'],
[$language->name => $row['name']],
[$language->name => $row['description']],
[$currency->name => (float) $row['cost']],
(float) $row['weight'],
['x' => $row['x'], 'y' => $row['y'], 'z' => $row['z']],
[$language->name => $row['brand']],
[$language->name => $row['compatibility']],
$row['position'] ?? null,
identifier: (int) $row['identifier'],
name: [$language->name => $row['name']],
description: [$language->name => $row['description']],
cost: [$currency->name => (float) $row['cost']],
weight: (float) $row['weight'],
dimensions: ['x' => $row['x'], 'y' => $row['y'], 'z' => $row['z']],
brand: [$language->name => $row['brand']],
compatibility: [$language->name => $row['compatibility']],
position: (int) $row['position'] ?? null,
errors: $errors
);
@ -339,7 +417,10 @@ final class catalog extends core
// Received the category
// Initializing the category
$category = category::_read(sprintf('d.identifier == %u', (int) $row['category']), errors: $errors);
$category = category::_read(
filter: sprintf('d.identifier == %u', (int) $row['category']),
errors: $errors
);
if ($category instanceof category) {
// Found the ascendant category
@ -356,13 +437,14 @@ final class catalog extends core
// Received images
// Initializing new images of the category
$images = explode(' ', trim($row['images']));
$images = explode(' ', mb_trim($row['images']));
// 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); */
$reinitialize = true;
// Checking the identity of existing images with new images (if reinitialization of images has not yet been requested)
if (!$reinitialize) foreach ($product->images as $key => $image) if ($reinitialize = $image['source'] !== $images[$key]) break;
/* if (!$reinitialize) foreach ($product->images as $key => $image) if ($reinitialize = $image['source'] !== $images[$key]) break; */
if ($reinitialize) {
// Requested reinitialization of images
@ -370,39 +452,97 @@ final class catalog extends core
// Initializing the buffer of images
$buffer = [];
foreach ($images as $index => $image) {
foreach ($images as $index => $file) {
// Iterating over new images
// Skipping empty URI`s
if (empty($image = trim($image))) continue;
if (empty($file = mb_trim($file))) continue;
// Initializing path to directory of the image in storage
$directory = DIRECTORY_SEPARATOR . 'products' . DIRECTORY_SEPARATOR . $row['identifier'];
// Initializing path to directory of images in storage
$directory = DIRECTORY_SEPARATOR . 'products' . DIRECTORY_SEPARATOR . $row['identifier'] . DIRECTORY_SEPARATOR . $index;
// Initializing URL of the image in storage
$url = STORAGE . $directory;
// Initializing URN of the image in storage
$urn = $index . '.jpg';
// Initializing URI of the image in storage
$uri = $url . DIRECTORY_SEPARATOR . $urn;
// Initializing the directory in storage
if (!file_exists($url)) mkdir($url, 0775, true);
if (static::yandex($image, $uri, errors: $errors)) {
// The image is downloaded
if ($downloaded = static::yandex(
uri: $file,
destination: $url,
name: 'source',
errors: $errors
)) {
// The image is downloaded and initialized data of the image in storage
// Writing the image to the buffer if images
$buffer[] = [
'source' => $image,
'storage' => $directory . DIRECTORY_SEPARATOR . $urn
];
// Initializing URI of the image
$uri = $downloaded['destination'] . DIRECTORY_SEPARATOR . $downloaded['name'] . '.' . $downloaded['content']->extension();
// Initializing size of the image
$size = getimagesize($uri);
if ($downloaded['content'] === content::png) {
// PNG
// Initializing implementator of the image
$boba = imagecreatefrompng($uri);
} else if ($downloaded['content'] === content::jpeg) {
// JPEG
// Initializing implementator of the image
$boba = imagecreatefromjpeg($uri);
}
// Enabling better antialiasing
imageantialias($boba, true);
if ($boba instanceof image) {
// Initialized implementator of the image
// Initializing buffer of resized images
$resized = [];
foreach ([1400, 800, 400, 200] as $resize) {
// Iterating over sizes for creating images
if ($size[0] >= $size[1]) {
// The width ($size[0]) is longer than the height ($size[1])
$width = $resize;
$height = (int) ($resize * $size[1] / $size[0]);
} else {
// The height ($size[1]) is longer than the width ($size[0])
$width = (int) ($resize * $size[0] / $size[1]);
$height = $resize;
}
// Resizing the image
$biba = imagecreatetruecolor($width, $height);
imagecopyresampled($biba, $boba, 0, 0, 0, 0, $width, $height, $size[0], $size[1]);
// Initializing URI of the resized image
$uri = $directory . DIRECTORY_SEPARATOR . "$resize." . $downloaded['content']->extension();
// Saving the image
imagePng($biba, STORAGE . $uri);
// Writing the resized image to the buffer of resized images
$resized[$resize] = $uri;
}
// Writing the image to the buffer if images
$buffer[] = [
'source' => $file,
'storage' => [
'source' => $directory . DIRECTORY_SEPARATOR . $downloaded['name'] . '.' . $downloaded['content']->extension(),
] + $resized
];
}
}
}
// Initializing images of the category
// Initializing images of the product
$product->images = $buffer;
}
}
@ -410,9 +550,9 @@ final class catalog extends core
// Writing in ArangoDB
$updated = document::update($product->__document(), errors: $errors);
// Incrementing the counter of updated categories
// Incrementing the counter of updated products
if ($updated && !$created) ++$products_updated;
} else throw new exception("Failed to initialize product: {$row['name']} ($number)");
} else throw new exception('Failed to initialize product: ' . $row['name'] . " ($number)");
}
// Writing to the registry of handled categories and products
@ -446,16 +586,25 @@ final class catalog extends core
$category instanceof category
&& array_search($category->identifier, $handled['categories']) === false
) {
// Not found identifier of the product in the buffer of handled categories and products
// Not found identifier of the category in the buffer of handled categories and products
// Deleting images of the category from storage
static::delete(STORAGE . DIRECTORY_SEPARATOR . 'categories' . DIRECTORY_SEPARATOR . $category->identifier, errors: $errors);
static::delete(
directory: STORAGE . DIRECTORY_SEPARATOR . 'categories' . DIRECTORY_SEPARATOR . $category->identifier,
errors: $errors
);
// Deleting entries of the category in ArangoDB
entry::banish($category, errors: $errors);
entry::banish(
document: $category,
errors: $errors
);
// Deleting the category in ArangoDB
document::delete($category->__document(), errors: $errors);
document::delete(
document: $category->__document(),
errors: $errors
);
// Incrementing the counter of deleted categories
++$categories_deleted;
@ -483,13 +632,22 @@ final class catalog extends core
// Not found identifier of the product in the buffer of handled categories and products
// Deleting images of the product from storage
static::delete(STORAGE . DIRECTORY_SEPARATOR . 'products' . DIRECTORY_SEPARATOR . $product->identifier, errors: $errors);
static::delete(
directory: STORAGE . DIRECTORY_SEPARATOR . 'products' . DIRECTORY_SEPARATOR . $product->identifier,
errors: $errors
);
// Deleting entries of the product in ArangoDB
entry::banish($product, errors: $errors);
entry::banish(
document: $product,
errors: $errors
);
// Deleting the product in ArangoDB
document::delete($product->__document(), errors: $errors);
document::delete(
document: $product->__document(),
errors: $errors
);
// Incrementing the counter of deleted products
++$products_deleted;
@ -497,8 +655,8 @@ final class catalog extends core
}
// Counting new documents
$categories_new = collection::count(category::COLLECTION, errors: $errors);
$products_new = collection::count(product::COLLECTION, errors: $errors);
$categories_new = collection::count(collection: category::COLLECTION, errors: $errors);
$products_new = collection::count(collection: product::COLLECTION, errors: $errors);
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [

View File

@ -27,11 +27,6 @@ use exception;
*/
class core extends model
{
/**
* Postfix for name of models files
*/
final public const string POSTFIX = '';
/**
* Path to the file with settings of connecting to the ArangoDB
*/
@ -47,7 +42,7 @@ class core extends model
/**
* Constructor of an instance
*
* @param bool $initialize Initialize a model?
* @param bool $initialize Initialize ...?
* @param ?arangodb $arangodb Instance of a session of ArangoDB
*
* @return void

View File

@ -15,16 +15,25 @@ use Http\Adapter\Guzzle7\Client as guzzle;
// Framework for CDEK
use CdekSDK2\Client as client,
CdekSDK2\Dto\CityList as cities;
CdekSDK2\Dto\CityList as _cities,
CdekSDK2\Dto\Tariff as _tariff,
CdekSDK2\BaseTypes\Tariff as tariff,
CdekSDK2\BaseTypes\Tarifflist as tariffs,
CdekSDK2\Constraints\Currencies as currencies,
CdekSDK2\BaseTypes\Location as location,
CdekSDK2\BaseTypes\Package as package;
// Built-in libraries
use exception;
use exception,
DateTime as datetime;
/**
* Model of CDEK
*
* @package mirzaev\arming_bot\models\deliveries
*
* @method cities|null location(string $name, array &$errors) Search for CDEK location by name
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
@ -33,40 +42,144 @@ final class cdek extends core implements document_interface, collection_interfac
use document_trait;
/**
* Name of the collection in ArangoDB
* Location
*
* Search for CDEK location by name (different villages, towns and farms may have the same names)
*
* @see https://api-docs.cdek.ru/182405028.html
* @see https://github.com/TTATPuOT/cdek-sdk2.0/blob/master/docs/index.md#получение-cписка-городов
*
* @param string $name Location name
* @param array &$errors Registry of errors
*
* @return _cities|null Locations, if found
*/
final public const string COLLECTION = 'delivery';
public static function location(string $name, array &$errors = []): ?_cities
{
try {
if (!empty(CDEK)) {
// Initialized CDEK account data
// Initializing HTTP-client
$client = new client(new guzzle, CDEK['account'], CDEK['secret']);
// Request
$response = $client->cities()->getFiltered(['country_codes' => 'RU', 'city' => $name]);
if ($response->isOk()) {
// Received response
// Exit (success)
return $client->formatResponseList($response, _cities::class);
}
}
} 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;
}
/**
* Calculate
*
* Calculate delivery by CDEK
* Calculate delivery from warehouse to recipient according to tariff
*
* @see https://api-docs.cdek.ru/63345430.html
* @see https://github.com/TTATPuOT/cdek-sdk2.0/blob/master/docs/index.md#калькулятор-расчет-по-коду-тарифа
*
* @param int $from_location Location code (sender)
* @param string $from_street Street (sender)
* @param int $to_location Location code (receiver)
* @param string $to_street Street (receiver)
* @param string $tariff Tariff (identifier)
* @param array $products Products that will be sent [weight, x, y, z]
* @param datetime $date Date of sending
* @param array &$errors Registry of errors
*
* @return
* @return _tariff|null Calculating data, if calculated
*/
public static function calculate(array &$errors = []): static|null
{
public static function calculate(
int $from_location,
string $from_street,
int $to_location,
string $to_street,
int $tariff,
array $products,
datetime $date,
array &$errors = []
): ?_tariff {
try {
//
/* $client = new client(new guzzle, 'account', 'secure'); */
$client = new client(new guzzle);
$client->setTest(true);
if (!empty(CDEK)) {
// Initialized CDEK account data
$result = $client->cities()->getFiltered(['country_codes' => 'RU', 'city' => 'зеленогорск']);
// Initializing HTTP-client
$client = new client(new guzzle, CDEK['account'], CDEK['secret']);
if ($result->isOk()) {
//
// Initializing buffer of packages
$packages = [];
//Запрос успешно выполнился
$cities = $client->formatResponseList($result, cities::class);
// Initializing packages from products
foreach ($products as $product)
$packages[] = package::create([
'weight' => $product['weight'],
'width' => $product['x'],
'height' => $product['y'],
'length' => $product['z']
]);
foreach ($cities->items as $city) {
var_dump($city);
// Deinitializing unnecessary variables
unset($product);
if (!empty($packages)) {
// Initialized packages
// Request
$response = $client
->calculator()
->add(
tariff::create([
'type' => tariffs::TYPE_ECOMMERCE,
'currency' => currencies::RUBLE, // @todo globalize this
/* 'date' => $date->format(datetime::ISO8601_EXPANDED), */
'date' => $date->format(datetime::ISO8601),
'lang' => tariffs::LANG_RUS, // @todo globalize this
'tariff_code' => $tariff,
'from_location' => location::create([
'code' => $from_location,
'address' => $from_street,
'country_code' => 'RU' // @todo globalize this
]),
'to_location' => location::create([
'code' => $to_location,
'address' => $to_street,
'country_code' => 'RU' // @todo globalize this
]),
'packages' => $packages
])
);
if ($response->hasErrors()) {
// Receied response with errors
}
if ($response->isOk()) {
// Received response
// Exit (success)
return $client->formatBaseResponse($response, _tariff::class);
}
}
die;
// Deinitializing unnecessary variables
unset($client, $packages);
}
} catch (exception $e) {
// Writing to the registry of errors

View File

View File

View File

View File

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace mirzaev\arming_bot\models;
// Files of the project
use mirzaev\arming_bot\models\core,
mirzaev\arming_bot\models\reservation,
mirzaev\arming_bot\models\traits\document as document_trait,
mirzaev\arming_bot\models\interfaces\document as document_interface,
mirzaev\arming_bot\models\interfaces\collection as collection_interface,
mirzaev\arming_bot\models\enumerations\language,
mirzaev\arming_bot\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;
/**
* Model of order
*
* @uses !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
* @package mirzaev\arming_bot\models
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class order extends core implements document_interface, collection_interface
{
use document_trait;
/**
* Name of the collection in ArangoDB
*/
final public const string COLLECTION = 'order';
}

View File

@ -53,15 +53,15 @@ final class session extends core implements document_interface, collection_inter
final public const verification VERIFICATION = verification::hash_else_address;
/**
* Constructor of instance
* Constructor
*
* Initialize of a session and write them to the $this->document property
* Initialize session and write into the $this->document property
*
* @param ?string $hash Hash of the session in ArangoDB
* @param ?int $expires Date of expiring of the session (used for creating a new session)
* @param array &$errors Registry of errors
*
* @return static
* @return void
*/
public function __construct(?string $hash = null, ?int $expires = null, array &$errors = [])
{
@ -71,7 +71,7 @@ final class session extends core implements document_interface, collection_inter
if (isset($hash) && $document = $this->hash($hash, errors: $errors)) {
// Found the instance of the ArangoDB document of session and received a session hash
// Writing document instance of the session from ArangoDB to the property of the implementing object
$this->__document($document);
} else if (static::VERIFICATION === verification::hash_else_address && $document = $this->address($_SERVER['REMOTE_ADDR'], errors: $errors)) {
@ -114,7 +114,7 @@ final class session extends core implements document_interface, collection_inter
$session->hash = sodium_bin2hex(sodium_crypto_generichash($_id));
if (document::update($session, errors: $errors)) {
// Update is writed to ArangoDB
// Writed to ArangoDB
// Writing instance of the session document from ArangoDB to the property of the implementing object
$this->__document($session);
@ -222,7 +222,7 @@ final class session extends core implements document_interface, collection_inter
return collection::execute(
<<<'AQL'
FOR d IN @@collection
FILTER d.hash == @hash && d.expires > $time && d.active == true
FILTER d.hash == @hash && d.expires > @time && d.active == true
RETURN d
AQL,
[

View File

@ -9,14 +9,17 @@ use mirzaev\arming_bot\models\core,
mirzaev\arming_bot\controllers\core as controller,
mirzaev\arming_bot\models\catalog,
mirzaev\arming_bot\models\suspension,
mirzaev\arming_bot\models\account;
mirzaev\arming_bot\models\account,
mirzaev\arming_bot\models\enumerations\language,
mirzaev\arming_bot\models\enumerations\currency;
// Framework for Telegram
use Zanzara\Zanzara,
Zanzara\Context,
Zanzara\Context as context,
Zanzara\Telegram\Type\Input\InputFile,
Zanzara\Telegram\Type\File\Document as telegram_document,
Zanzara\Middleware\MiddlewareNode as Node;
Zanzara\Middleware\MiddlewareNode as Node,
Zanzara\Telegram\Type\User as user;
/**
* Model of chat (telegram)
@ -90,11 +93,11 @@ final class telegram extends core
*
* Команда: /start
*
* @param Context $ctx
* @param context $ctx
*
* @return void
*/
public static function menu(Context $ctx): void
public static function menu(context $ctx): void
{
// Инициализация клавиатуры
$keyboard = [
@ -132,11 +135,11 @@ final class telegram extends core
*
* Команда: /start
*
* @param Context $ctx
* @param context $ctx
*
* @return void
*/
public static function start(Context $ctx): void
public static function start(context $ctx): void
{
// Главное меню
static::menu($ctx);
@ -147,11 +150,11 @@ final class telegram extends core
*
* Команда: /contacts
*
* @param Context $ctx
* @param context $ctx
*
* @return void
*/
public static function contacts(Context $ctx): void
public static function contacts(context $ctx): void
{
// Отправка сообщения
$ctx->sendMessage(static::unmarkdown(<<<TXT
@ -182,11 +185,11 @@ final class telegram extends core
/**
* Почта
*
* @param Context $ctx
* @param context $ctx
*
* @return void
*/
public static function _mail(Context $ctx): void
public static function _mail(context $ctx): void
{
// Отправка сообщения
$ctx->sendMessage(static::unmarkdown(<<<TXT
@ -204,34 +207,29 @@ final class telegram extends core
*
* Команда: /company
*
* @param Context $ctx
* @param context $ctx
*
* @return void
*/
public static function company(Context $ctx): void
public static function company(context $ctx): void
{
// Отправка сообщения
$ctx->sendMessage(
static::unmarkdown(<<<TXT
Здесь придумать текст для раздела "Компания"
TXT),
/* [
'reply_markup' => [
'inline_keyboard' => [
[
['text' => '⚡ Связь с менеджером', 'url' => 'https://git.mirzaev.sexy/mirzaev/mashtrash'],
['text' => '📨 Почта', 'text' => ''],
Здесь придумать текст для раздела "Компания"
TXT),
[
'reply_markup' => [
'inline_keyboard' => [
[
['text' => '📄 Публичная оферта', 'web_app' => ['url' => 'https://arming.dev.mirzaev.sexy/offer']],
]
]
],
[
['text' => '🪖 Сайт', 'url' => '']
['text' => '🛒 Wildberries', 'url' => '']
'link_preview_options' => [
'is_disabled' => true
]
]
],
'link_preview_options' => [
'is_disabled' => true
]
] */
);
}
@ -240,11 +238,11 @@ final class telegram extends core
*
* Команда: /community
*
* @param Context $ctx
* @param context $ctx
*
* @return void
*/
public static function community(Context $ctx): void
public static function community(context $ctx): void
{
// Отправка сообщения
$ctx->sendMessage(static::unmarkdown(<<<TXT
@ -269,11 +267,11 @@ final class telegram extends core
*
* Команда: /settings
*
* @param Context $ctx
* @param context $ctx
*
* @return void
*/
public static function settings(Context $ctx): void
public static function settings(context $ctx): void
{
if ($ctx->get('account')?->access['settings']) {
// Авторизован доступ к настройкам
@ -308,11 +306,11 @@ final class telegram extends core
/**
* Запросить файл для импорта товаров (доступ только авторизованным)
*
* @param Context $ctx
* @param context $ctx
*
* @return void
*/
public static function import_request(Context $ctx): void
public static function import_request(context $ctx): void
{
if ($ctx->get('account')?->access['settings']) {
// Авторизован доступ к настройкам
@ -337,11 +335,11 @@ final class telegram extends core
/**
* Импорт товаров (доступ только авторизованным)
*
* @param Context $ctx
* @param context $ctx
*
* @return void
*/
public static function import(Context $ctx): void
public static function import(context $ctx): void
{
if (($account = $ctx->get('account'))?->access['settings']) {
// Авторизован доступ к настройкам
@ -410,7 +408,8 @@ final class telegram extends core
$products_deleted,
$products_old,
$products_new,
language: $account->language ?? settings::active()?->language ?? 'en'
language: $account->language ?? settings::active()?->language ?? language::en, // @todo add languages
currency: $account->currency ?? settings::active()?->currency ?? currency::usd // @todo add currencies
);
// Отправка сообщения
@ -475,12 +474,12 @@ final class telegram extends core
/**
* Инициализация аккаунта (middleware)
*
* @param Context $ctx
* @param context $ctx
* @param Node $next
*
* @return void
*/
public static function account(Context $ctx, Node $next): void
public static function account(context $ctx, Node $next): void
{
// Выполнение заблокировано?
if ($ctx->get('stop')) return;
@ -523,12 +522,12 @@ final class telegram extends core
/**
* Инициализация статуса технических работ (middleware)
*
* @param Context $ctx
* @param context $ctx
* @param Node $next
*
* @return void
*/
public static function suspension(Context $ctx, Node $next): void
public static function suspension(context $ctx, Node $next): void
{
// Выполнение заблокировано?
if ($ctx->get('stop')) return;
@ -583,4 +582,122 @@ final class telegram extends core
$next($ctx);
}
}
/**
* Cart attach
*
* @param context $ctx
* @param string $share Sharing hash
*
* @return void
*/
public static function cart_attach(context $ctx, string $share): void
{
// Initializing account
$account = $ctx->get('account');
if ($account) {
// Initialized the account
// Initializing cart
$cart = cart::_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);
// Unsharing the cart
$cart->unshare();
if ($cart instanceof cart) {
// Initialized the cart
// Connecting the cart to the account
$edge = $account->connect($cart);
if (!empty($edge)) {
// Connected the cart to the account
// Initializing products in the cart
$products = $cart->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;
// Declaring formatted list of products for message
$list = '';
// Initializing iterator of rows
$row = 0;
foreach ($products as $product) {
// Iterating over products
// Generating formatted list of products for message
$list .= static::unmarkdown(++$row . '. ' . $product['document']['name'] . ' (' . $product['amount'] . 'шт)') . "\n";
// Generating total cost of products
$cost += $product['document']['cost'] * $product['amount'];
}
// Deinitializing unnecessary variables
unset($products, $product, $row);
// Initializing currency symbol
$symbol = ($account->currency ?? currency::rub)->symbol();
// Initializing delivery cost for message
$delivery_cost = $cart->buffer['delivery']['cost'];
// Initializing delivery days for message
$delivery_days = $cart->buffer['delivery']['days'];
// Initializing delivery address for message
$delivery_address = $cart->buffer['delivery']['location']['name'] . ', ' . $cart->buffer['delivery']['street'];
// Deinitializing unnecessary variables
unset($cart);
$ctx->sendMessage(
<<<TXT
🛒 *Добавлена корзина*
$list
*Стоимость:* $cost$symbol \+ $delivery_cost$symbol \($delivery_days дней\)
*Адрес доставки:* $delivery_address
TXT,
[
'reply_markup' => [
'inline_keyboard' => [
[
/* ['text' => '🧾 Оплатить', 'web_app' => ['url' => 'https://arming.dev.mirzaev.sexy']] */
['text' => '📦 Оформить заказ', 'url' => 'https://auth.robokassa.ru/Merchant/Index.aspx?MerchantLogin=demo&OutSum=11&Description=Покупка в демо магазине&SignatureValue=2c113e992e2c985e43e348ff3c12f32b'],
]
],
'disable_notification' => true
]
]
);
}
// Deinitializing unnecessary variables
unset($cart, $list);
}
}
// Deinitializing unnecessary variables
unset($cart);
}
// Deinitializing unnecessary variables
unset($account);
}
}

View File

@ -50,20 +50,20 @@ trait buffer
// Initialized the collection
// Is the instance of the document from ArangoDB are initialized?
if (!isset($this->document)) throw new exception('The instance of the sessoin document from ArangoDB is not initialized');
if (!($this->__document() instanceof _document)) throw new exception('The instance of the document from ArangoDB is not initialized');
// Writing data into buffer of the instance of the document from ArangoDB
$this->document->buffer = array_replace_recursive($this->document->buffer ?? [], $data);
$this->__document()->buffer = array_replace_recursive($this->document->buffer ?? [], $data);
// Is the buffer of the instance of the document from ArangoDB exceed 10 megabytes?
if (mb_strlen(json_encode($this->document->buffer)) > 10485760) throw new exception('The buffer size exceeds 10 megabytes');
if (mb_strlen(json_encode($this->__document()->buffer)) > 10485760) throw new exception('The buffer size exceeds 10 megabytes');
// Serializing parameters
if ($this->document->language instanceof language) $this->document->language = $this->document->language->name;
if ($this->document->currency instanceof currency) $this->document->currency = $this->document->currency->name;
// Serializing parameters @todo ЗАЧЕМУ ЭТО ЗДЕСЬ?
/* if ($this->__document()->language instanceof language) $this->__document()->language = $this->__document()->language->name;
if ($this->__document()->currency instanceof currency) $this->__document()->currency = $this->__document()->currency->name; */
// Writing to ArangoDB and exit (success)
return document::update($this->document, errors: $errors);
return document::update($this->__document(), errors: $errors);
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
} catch (exception $e) {
// Writing to the registry of errors

View File

@ -4,6 +4,9 @@ declare(strict_types=1);
namespace mirzaev\arming_bot\models\traits\yandex;
// Framework for PHP
use mirzaev\minimal\http\enumerations\content;
// Built-in libraries
use exception;
@ -22,15 +25,17 @@ trait disk
*
* @param string $uri URI of the file from "Yandex Disk"
* @param string $destination Destination to write the file
* @param string|int $name Name for the file
* @param array &$errors Registry of errors
*
* @return bool The file is downloaded?
* @return array|false The [destination, name, content] of the downloaded file, if the file was downloaded
*/
private static function download(
string $uri,
string $destination,
string|int $name,
array &$errors = []
): bool {
): array|false {
try {
if (!empty($uri)) {
// Not empty URI
@ -42,17 +47,44 @@ trait disk
$url = "https://cloud-api.yandex.net/v1/disk/public/resources/download?public_key=$uri";
// Checking if the file is available for download
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
$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 file is available for download
// Downloading the file and exit (success)
return file_put_contents($destination, file_get_contents(json_decode(file_get_contents($url))?->href)) > 0;
// Initializing URI of the file
$uri = json_decode(file_get_contents($url))?->href;
// Downloading the file
$session = curl_init($uri);
curl_setopt($session, CURLOPT_RETURNTRANSFER, true);
curl_setopt($session, CURLOPT_AUTOREFERER, true);
curl_setopt($session, CURLOPT_FOLLOWLOCATION, true);
curl_exec($session);
$file = curl_exec($session);
$code = curl_getinfo($session, CURLINFO_RESPONSE_CODE);
preg_match('/^\w+\/\w+/', curl_getinfo($session, CURLINFO_CONTENT_TYPE), $matches);
$type = content::from($matches[0]);
curl_close($session);
if ($code === 200 && $file) {
// The file is downloaded
if ($type instanceof content) {
// Initialized content-type header
if (file_put_contents($destination . DIRECTORY_SEPARATOR . "$name." . $type->extension(), $file) > 0) {
// Downloaded the file
// Exit (success)
return ['destination' => $destination, 'name' => $name, 'content' => $type];
}
} else throw new exception("Failed to initialize content-type header");
} else throw new exception("Failed to download the file by link: $uri");
} else throw new exception("File not available for download: $uri");
} else throw new exception("Empty destination");
} else throw new exception("Empty URI");

View File

@ -4,14 +4,11 @@ declare(strict_types=1);
namespace mirzaev\arming_bot;
// Files of the project
use mirzaev\arming_bot\controllers\core as controller,
mirzaev\arming_bot\models\core as model;
// Framework for PHP
use mirzaev\minimal\core,
mirzaev\minimal\router;
mirzaev\minimal\route;
// Enabling debugging
ini_set('error_reporting', E_ALL);
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
@ -22,59 +19,32 @@ define('VIEWS', realpath('..' . DIRECTORY_SEPARATOR . 'views'));
define('STORAGE', realpath('..' . DIRECTORY_SEPARATOR . 'storage'));
define('SETTINGS', realpath('..' . DIRECTORY_SEPARATOR . 'settings'));
define('INDEX', __DIR__);
define('ROOT', INDEX . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR);
define('THEME', 'default');
define('CDEK', require(SETTINGS . DIRECTORY_SEPARATOR . 'deliveries' . DIRECTORY_SEPARATOR . 'cdek.php'));
// Initialize dependencies
require __DIR__ . DIRECTORY_SEPARATOR
. '..' . DIRECTORY_SEPARATOR
. '..' . DIRECTORY_SEPARATOR
. '..' . DIRECTORY_SEPARATOR
. '..' . DIRECTORY_SEPARATOR
. 'vendor' . DIRECTORY_SEPARATOR
. 'autoload.php';
require ROOT . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php';
// Initialize the router
$router = new router;
// Initializing core
$core = new core(namespace: __NAMESPACE__);
// Initialize routes
$router
->write('/', 'catalog', 'index', 'GET')
->write('/', 'catalog', 'index', 'POST')
->write('/cart', 'cart', 'index', 'GET')
->write('/cart', 'cart', 'index', 'POST')
->write('/cart/product', 'cart', 'product', 'POST')
->write('/cart/summary', 'cart', 'summary', 'POST')
->write('/account/write', 'account', 'write', 'POST')
->write('/session/write', 'session', 'write', 'POST')
->write('/session/connect/telegram', 'session', 'telegram', 'POST')
->write('/cdek/calculate', 'deliveries\\cdek', 'calculate', 'POST')
/* ->write('/category/$identifier', 'catalog', 'index', 'POST') */
/* ->write('/category', 'catalog', 'index', 'POST') */
/* ->write('/product/$identifier', 'catalog', 'product', 'POST') */
;
// Initializing routes
$core->router
->write('/', new route('catalog', 'index', 'catalog'), 'GET')
->write('/offer', new route('index', 'offer'), 'GET')
->write('/cart', new route('cart', 'index', 'cart'), 'GET')
->write('/cart/product', new route('cart', 'product', 'cart'), 'PATCH')
->write('/cart/summary', new route('cart', 'summary', 'cart'), 'GET')
/* ->write('/cart/share', new route('cart', 'share', 'cart'), 'POST') */
->write('/cart/pay', new route('cart', 'pay', 'cart'), 'POST')
->write('/order/robokassa', new route('cart', 'robokassa', 'cart'), 'GET')
->write('/account/write', new route('account', 'write', 'account'), 'PATCH')
->write('/session/write', new route('session', 'write', 'session'), 'PATCH')
->write('/session/connect/telegram', new route('session', 'telegram', 'session'), 'PUT')
->write('/delivery/write', new route('delivery', 'write'), 'PATCH')
->write('/delivery/calculate', new route('delivery', 'calculate'), 'GET');
/*
// Initializing of routes
$router
->write('/', 'catalog', 'index', 'GET')
->write('/$sex', 'catalog', 'search', 'POST')
->write('/$search', 'catalog', 'search', 'POST')
->write('/search', 'catalog', 'search', 'POST')
->write('/search/$asdasdasd', 'catalog', 'search', 'POST')
->write('/ebala/$sex/$categories...', 'catalog', 'index', 'POST')
->write('/$sex/$categories...', 'catalog', 'index', 'POST')
->write('/$categories...', 'catalog', 'index', 'POST')
->write('/ebala/$categories...', 'catalog', 'index', 'POST');
var_dump($router->routes);
echo "\n\n\n\n\n\n";
$router
->sort();
var_dump($router->routes); */
// Initialize the core
$core = new core(namespace: __NAMESPACE__, router: $router, controller: new controller(false), model: new model(false));
// Handle the request
echo $core->start();
// Handling request
$core->start();

View File

@ -1,20 +0,0 @@
"use strict";
core.modules.connect(["session", "account", "telegram"])
.then(() => {
//
const { initData, initDataUnsafe, ...data } = core.telegram.api;
//
core.session.buffer.write("telegram_program", JSON.stringify(data));
if (core.telegram.api.initData.length > 0) {
//
//
core.account.authentication();
}
//
core.telegram.api.ready();
});

View File

@ -52,7 +52,7 @@ class core {
static async request(
uri = "/",
body,
method = "POST",
method = "GET",
headers = {
"Content-Type": "application/x-www-form-urlencoded",
"Accept": "application/json",
@ -320,7 +320,7 @@ Object.assign(
// Downloading, importing and writing the module into a core property and into registry of loaded modules
core[module] =
loaded[module] =
await (await import(`./modules/${module}.js`)).default;
await (await import(`./modules/${module}.mjs`)).default;
}
// Exit (success)
@ -382,14 +382,14 @@ Object.assign(
// Imported the session module
// Write to the session buffer
session.default.buffer?.write(name, value);
core.session.buffer?.write(name, value);
});
core.modules.connect("account").then(() => {
// Imported the account module
// Write to the account buffer
account.default.buffer?.write(name, value);
core.account.buffer?.write(name, value);
});
// Exit (success)

View File

@ -34,7 +34,7 @@ export default class account {
const timer_for_response = setTimeout(() => {
core.status_loading.setAttribute("disabled", true);
}, 3000);
}, 200);
core.modules.connect("telegram").then(() => {
// Imported the telegram module
@ -44,6 +44,7 @@ export default class account {
.request(
"/session/connect/telegram",
core.telegram.api.initData,
'PUT'
)
.then((json) => {
if (json) {
@ -54,9 +55,12 @@ export default class account {
typeof json.errors === "object" &&
json.errors.length > 0
) {
// Errors received
// Fail (received errors)
// Exit (fail)
reject(json);
} else {
// Errors not received
// Success (not received errors)
if (json.connected === true) {
core.status_loading.setAttribute("disabled", true);
@ -97,7 +101,7 @@ export default class account {
* @param {string} value Value of the parameter (it can be JSON)
* @param {bool} force Ignore the damper? (false)
*
* @return {bool} Execution completed with an error?
* @return {bool} Did the execution complete without errors?
*/
static write = (name, value, force = false) => {
core.modules.connect("damper").then(
@ -116,7 +120,7 @@ export default class account {
);
// Exit (success)
return false;
return true;
};
};
}
@ -128,10 +132,12 @@ core.modules.connect("damper").then(() => {
account.buffer.write,
{
/**
* @name Write
* @name Write (damper)
*
* @description
* Write to the account buffer (damper)
* Write to the account buffer
*
* @memberof account.buffer.write
*
* @param {string} name Name of the parameter
* @param {string} value Value of the parameter (it can be JSON)
@ -152,10 +158,12 @@ Object.assign(
account.buffer.write,
{
/**
* @name Write
* @name Write (system)
*
* @description
* Write to the account buffer (system)
* Write to the account buffer
*
* @memberof account.buffer.write
*
* @param {string} name Name of the parameter
* @param {string} value Value of the parameter (it can be JSON)
@ -169,22 +177,38 @@ Object.assign(
reject = () => {},
) {
try {
if (typeof name === "string" && typeof value === "string") {
if (
typeof name === "string" &&
(typeof value === "string" || typeof value === "number")
) {
// Received and validated required arguments
// Sending request to the server
return await core.request(
"/account/write",
`${name}=${value}`,
"POST",
"PATCH",
)
.then(
(json) => {
if (json) {
// Received a JSON-response
// Exit (success)
resolve(json);
if (
json.errors !== null &&
typeof json.errors === "object" &&
json.errors.length > 0
) {
// Fail (received errors)
// Exit (fail)
reject(json);
} else {
// Success (not received errors)
// Exit (success)
resolve(json);
}
}
},
() => reject(),

View File

@ -1,528 +0,0 @@
"use strict";
/**
* @name Cart
*
* @description
* Implements actions with cart
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
export default class cart {
/**
* @name Type of the program
*/
static type = "module";
/**
* Toggle
*
* Toggle the product in the cart (interface)
*
* @param {HTMLButtonElement|HTMLInputElement} element Handler elememnt of the product
* @param {HTMLElement} product The product element
* @param {bool} remove Remove the product element if json.amount === 0?
* @param {bool} force Ignore the damper? (false)
*
* @return {bool} Execution completed with an error?
*/
static toggle(element, product, remove = false, force = false) {
// Blocking the element
element.setAttribute("disabled", "true");
core.modules.connect("damper").then(
() => {
// Imported the damper module
// Execute under damper
this.product.damper(
element,
product,
"toggle",
undefined,
remove,
force,
);
},
() => {
// Not imported the damper module
// Execute
this.product.system(
element,
product,
"toggle",
undefined,
remove,
force,
);
},
);
// Exit (success)
return false;
}
/**
* Write
*
* Write the product in the cart (interface)
*
* @param {HTMLButtonElement|HTMLInputElement} element Handler elememnt of the product
* @param {HTMLElement} product The product element
* @param {number} amount Amount of writings
* @param {bool} remove Remove the product element if json.amount === 0?
* @param {bool} force Ignore the damper? (false)
*
* @return {bool} Execution completed with an error?
*/
static write(
element,
product,
amount = 1,
remove = false,
force = false,
) {
// Blocking the element
element.setAttribute("disabled", "true");
core.modules.connect("damper").then(
() => {
// Imported the damper module
// Execute under damper
this.product.damper(
element,
product,
"write",
amount,
remove,
force,
);
},
() => {
// Not imported the damper module
// Execute
this.product.system(
element,
product,
"write",
amount,
remove,
force,
);
},
);
// Exit (success)
return false;
}
/**
* Delete
*
* Delete the product from the cart (interface)
*
* @param {HTMLButtonElement|HTMLInputElement} element Handler elememnt of the product
* @param {HTMLElement} product The product element
* @param {number} amount Amount of deletings
* @param {bool} remove Remove the product element if json.amount === 0?
* @param {bool} force Ignore the damper? (false)
*
* @return {bool} Execution completed with an error?
*/
static delete(
element,
product,
amount = 1,
remove = false,
force = false,
) {
// Blocking the element
element.setAttribute("disabled", "true");
core.modules.connect("damper").then(
() => {
// Imported the damper module
// Execute under damper
this.product.damper(
element,
product,
"delete",
amount,
remove,
force,
);
},
() => {
// Not imported the damper module
// Execute
this.product.system(
element,
product,
"delete",
amount,
remove,
force,
);
},
);
// Exit (success)
return false;
}
/**
* Set
*
* Set amount of the product in the cart (interface)
*
* @param {HTMLButtonElement|HTMLInputElement} element Handler elememnt of the product
* @param {HTMLElement} product The product element
* @param {number} amount Amount of the product in the cart to be setted
* @param {bool} remove Remove the product element if json.amount === 0?
* @param {bool} force Ignore the damper? (false)
*
* @return {bool} Execution completed with an error?
*/
static set(
element,
product,
amount = 1,
remove = false,
force = false,
) {
// Blocking the element
element.setAttribute("disabled", "true");
core.modules.connect("damper").then(
() => {
// Imported the damper module
// Execute under damper
this.product.damper(
element,
product,
"set",
amount,
remove,
force,
);
},
() => {
// Not imported the damper module
// Execute
this.product.system(
element,
product,
"set",
amount,
remove,
force,
);
},
);
// Exit (success)
return false;
}
/**
* The product
*
* Handle the product in the cart (system)
*
* @param {HTMLButtonElement|HTMLInputElement} element Handler element of the product
* @param {HTMLElement} product The product element
* @param {string} type Type of action with the product
* @param {number} amount Amount of product to handle
* @param {bool} remove Remove the product element if json.amount === 0?
*
* @return {bool} Execution completed with an error?
*/
static product(
element,
product,
type,
amount = null,
remove = false,
) {
core.modules.connect("damper").then(
() => {
// Imported the damper module
// Execute under damper
this.product.damper(
element,
product,
type,
amount,
remove,
force,
);
},
() => {
// Not imported the damper module
// Execute
this.product.system(
element,
product,
type,
amount,
remove,
force,
);
},
);
// Exit (success)
return false;
}
/**
* @name Summary
*
* @description
* Initialize summary of products the cart (system)
*
* @return {Promise}
*/
static async summary() {
// Request
return await core.request("/cart/summary")
.then((json) => {
if (json) {
// Received a JSON-response
if (
json.errors !== null &&
typeof json.errors === "object" &&
json.errors.length > 0
) {
// Fail (received errors)
} else {
// Success (not received errors)
// Initializing the summary amount <span> element
const amount = document.getElementById("amount");
if (amount instanceof HTMLElement) {
// Initialized the summary amount element
// Writing summmary amount into the summary amount element
amount.innerText = json.amount;
}
// Initializing the summary cost <span> element
const cost = document.getElementById("cost");
if (cost instanceof HTMLElement) {
// Initialized the summary cost element
// Writing summmary cost into the summary cost element
cost.innerText = json.cost;
}
}
}
});
}
}
core.modules.connect("damper").then(() => {
// Imported the damper module
Object.assign(
cart.product,
{
/**
* Toggle
*
* Toggle the product in the cart (damper)
*
* @param {HTMLButtonElement|HTMLInputElement} element Handler elememnt of the product
* @param {HTMLElement} product The product element
* @param {bool} remove Remove the product element if json.amount === 0?
* @param {bool} force Ignore the damper? (false)
*
* @return {Promise}
*/
damper: core.damper(
(...variables) => cart.product.system(...variables).then(cart.summary),
300,
6,
),
},
);
});
Object.assign(
cart.product,
{
/**
* The product
*
* Handle the product in the cart (system)
*
* @param {HTMLButtonElement|HTMLInputElement} element Handler element of the product
* @param {HTMLElement} product The product element
* @param {string} type Type of action with the product
* @param {number} amount Amount of product to handle
* @param {bool} remove Remove the product element if json.amount === 0?
*
* @return {Promise|null}
*/
async system(
element,
product,
type,
amount = null,
remove = false,
resolve = () => {},
reject = () => {},
) {
try {
if (
(element instanceof HTMLButtonElement ||
element instanceof HTMLInputElement) &&
product instanceof HTMLElement
) {
// Validated
// Initializing the buffer of request body
let request = "";
// Initializing of identifier of the product
const identifier = +product.getAttribute(
"data-product-identifier",
);
if (typeof identifier === "number") {
// Validated identifier
// Writing to the buffer of request body
request += "&identifier=" + identifier;
if (
type === "toggle" ||
type === "write" ||
type === "delete" ||
type === "set"
) {
// Validated type
// Writing to the buffer of request body
request += "&type=" + type;
console.log(type, amount);
if (
(type === "toggle" &&
amount === null ||
typeof amount === "undefined") ||
(type === "set" &&
amount === 0 ||
amount === 100) ||
typeof amount === "number" &&
amount > 0 &&
amount < 100
) {
// Validated amount
console.log(amount);
if (type !== "toggle") {
// Not a toggle request
// Writing to the buffer of request body
request += "&amount=" + amount;
}
// Request
return await core.request(
"/cart/product",
request,
)
.then(
(json) => {
if (json) {
// Received a JSON-response
if (
json.errors !== null &&
typeof json.errors === "object" &&
json.errors.length > 0
) {
// Fail (received errors)
} else {
// Success (not received errors)
if (remove && json.amount === 0) {
// Requested deleting of the product element when there is no the product in the cart
// Deleting the product element
product.remove();
} else {
// Not requested deleting the product element when there is no the product in the cart
// Unblocking the element
element.removeAttribute("disabled");
// Writing offset of hue-rotate to indicate that the product is in the cart
product.style.setProperty(
"--hue-rotate-offset",
json.amount + "0deg",
);
// Writing attribute with amount of the product in the cart
product.setAttribute(
"data-product-amount",
json.amount,
);
// Initializing the amount <span> element
const amounts = product.querySelectorAll(
'[data-product-parameter="amount"]',
);
for (const amount of amounts) {
// Iterating over an amount elements
if (amount instanceof HTMLInputElement) {
// The <input> element
// Writing amount of the product in the cart
amount.value = json.amount;
} else {
// Not the <input> element
// Writing amount of the product in the cart
amount.innerText = json.amount;
}
}
}
// Exit (success)
resolve();
}
}
},
() => reject(),
);
}
}
}
}
} catch (e) {
// Exit (fail)
reject(e);
}
},
},
);
// Connecting to the core
if (!core.cart) core.cart = cart;

View File

@ -0,0 +1,902 @@
"use strict";
/**
* @name Cart
*
* @description
* Implements actions with cart
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
export default class cart {
/**
* @name Type of the program
*/
static type = "module";
/**
* @name Toggle (interface)
*
* @description
* Toggle the product in the cart
*
* @param {HTMLButtonElement|HTMLInputElement|null} element Handler elememnt of the product
* @param {HTMLElement} product The product element
* @param {bool} remove Remove the product element if json.amount === 0?
* @param {bool} force Ignore the damper? (false)
*
* @return {bool} Did the execution complete without errors?
*/
static toggle(element, product, remove = false, force = false) {
// Blocking the element
element?.setAttribute("disabled", "true");
core.modules.connect("damper").then(
() => {
// Imported the damper module
// Execute under damper
this.product.damper(
element,
product,
"toggle",
undefined,
remove,
force,
);
},
() => {
// Not imported the damper module
// Execute
this.product.system(
element,
product,
"toggle",
undefined,
remove,
force,
);
},
);
// Exit (success)
return true;
}
/**
* @name Write (interface)
*
* @description
* Write the product in the cart
*
* @param {HTMLButtonElement|HTMLInputElement|null} element Handler elememnt of the product
* @param {HTMLElement} product The product element
* @param {number} amount Amount of writings
* @param {bool} remove Remove the product element if json.amount === 0?
* @param {bool} force Ignore the damper? (false)
*
* @return {bool} Did the execution complete without errors?
*/
static write(
element,
product,
amount = 1,
remove = false,
force = false,
) {
// Blocking the element
element?.setAttribute("disabled", "true");
core.modules.connect("damper").then(
() => {
// Imported the damper module
// Execute under damper
this.product.damper(
element,
product,
"write",
amount,
remove,
force,
);
},
() => {
// Not imported the damper module
// Execute
this.product.system(
element,
product,
"write",
amount,
remove,
force,
);
},
);
// Exit (success)
return true;
}
/**
* @name Delete (interface)
*
* @description
* Delete the product from the cart
*
* @param {HTMLButtonElement|HTMLInputElement|null} element Handler elememnt of the product
* @param {HTMLElement} product The product element
* @param {number} amount Amount of deletings
* @param {bool} remove Remove the product element if json.amount === 0?
* @param {bool} force Ignore the damper? (false)
*
* @return {bool} Did the execution complete without errors?
*/
static delete(
element,
product,
amount = 1,
remove = false,
force = false,
) {
// Blocking the element
element?.setAttribute("disabled", "true");
core.modules.connect("damper").then(
() => {
// Imported the damper module
// Execute under damper
this.product.damper(
element,
product,
"delete",
amount,
remove,
force,
);
},
() => {
// Not imported the damper module
// Execute
this.product.system(
element,
product,
"delete",
amount,
remove,
force,
);
},
);
// Exit (success)
return true;
}
/**
* @name Set (interface)
*
* @description
* Set amount of the product in the cart
*
* @param {HTMLButtonElement|HTMLInputElement|null} element Handler elememnt of the product
* @param {HTMLElement} product The product element
* @param {number} amount Amount of the product in the cart to be setted
* @param {bool} remove Remove the product element if json.amount === 0?
* @param {bool} force Ignore the damper? (false)
*
* @return {bool} Did the execution complete without errors?
*/
static set(
element,
product,
amount = 1,
remove = false,
force = false,
) {
// Blocking the element
element?.setAttribute("disabled", "true");
core.modules.connect("damper").then(
() => {
// Imported the damper module
// Execute under damper
this.product.damper(
element,
product,
"set",
amount,
remove,
force,
);
},
() => {
// Not imported the damper module
// Execute
this.product.system(
element,
product,
"set",
amount,
remove,
force,
);
},
);
// Exit (success)
return true;
}
/**
* @name Product (interface)
*
* @description
* Handle the product in the cart
*
* @param {HTMLButtonElement|HTMLInputElement|null} element Handler element of the product
* @param {HTMLElement} product The product element
* @param {string} type Type of action with the product
* @param {number} amount Amount of product to handle
* @param {bool} remove Remove the product element if json.amount === 0?
*
* @return {bool} Did the execution complete without errors?
*/
static product(
element,
product,
type,
amount = null,
remove = false,
) {
core.modules.connect("damper").then(
() => {
// Imported the damper module
// Execute under damper
this.product.damper(
element,
product,
type,
amount,
remove,
force,
);
},
() => {
// Not imported the damper module
// Execute
this.product.system(
element,
product,
type,
amount,
remove,
force,
);
},
);
// Exit (success)
return true;
}
/**
* @name Summary (interface)
*
* @description
* Initialize summary of products the cart
*
* @param {HTMLButtonElement} button Button <button>
*
* @return {bool} Did the execution complete without errors?
*/
static summary(button) {
// Disabling button
button?.setAttribute("disabled", true);
// Initializing timer of enabling the delivery company <input> (radio) element
const enabling = setTimeout(() => {
// Enabling button
button?.removeAttribute("disabled");
}, 3000);
/**
* @name Resolved
*
* @description
* Render the result of the resolved request
*
* @param {object} json
*
* @return {void}
*/
const resolved = (json) => {
if (json) {
// Received JSON-response
// Deinitializing timer of enabling the delivery company <input> (radio) element
clearTimeout(enabling);
// Enabling button
button?.removeAttribute("disabled");
}
};
/**
* @name Rejected
*
* @description
* Render the result of the rejected request
*
* @param {object} json
*
* @return {void}
*/
const rejected = (json) => {
if (json) {
// Received JSON-response
// Deinitializing timer of enabling the delivery company <input> (radio) element
clearTimeout(enabling);
// Enabling button
button?.removeAttribute("disabled");
}
};
core.modules.connect("damper").then(
() => {
// Imported the damper module
// Execute under damper
this.summary.damper()
.then(resolved, rejected);
},
() => {
// Not imported the damper module
// Execute
this.summary.system()
.then(resolved, rejected);
},
);
// Exit (success)
return true;
}
/**
* @name Share (interface)
*
* @description
* Generate sharing hash of the cart and sent ot to the chat-robot
*
* @param {HTMLButtonElement} button Button <button>
*
* @return {bool} Did the execution complete without errors?
*/
static async share(button) {
return await core.modules.connect("telegram").then(
() => {
// Imported the damper module
// Disabling button
button?.setAttribute("disabled", true);
// Initializing timer of enabling the delivery company <input> (radio) element
const enabling = setTimeout(() => {
// Enabling button
button?.removeAttribute("disabled");
}, 3000);
/**
* @name Resolved
*
* @description
* Render the result of the resolved request
*
* @param {object} json
*
* @return {void}
*/
const resolved = (json) => {
if (json) {
// Received JSON-response
// Deinitializing timer of enabling the delivery company <input> (radio) element
clearTimeout(enabling);
// Enabling button
button?.removeAttribute("disabled");
}
};
/**
* @name Rejected
*
* @description
* Render the result of the rejected request
*
* @param {object} json
*
* @return {void}
*/
const rejected = (json) => {
if (json) {
// Received JSON-response
// Deinitializing timer of enabling the delivery company <input> (radio) element
clearTimeout(enabling);
// Enabling button
button?.removeAttribute("disabled");
}
};
core.modules.connect("damper").then(
() => {
// Imported the damper module
// Execute under damper
this.share.damper()
.then(resolved, rejected);
},
() => {
// Not imported the damper module
// Execute
this.share.system()
.then(resolved, rejected);
},
);
// Exit (success)
return true;
},
// Exit (fail)
() => false,
);
}
}
core.modules.connect("damper").then(() => {
// Imported the damper module
Object.assign(
cart.product,
{
/**
* @name Product (damper)
*
* @description
* Handle the product in the cart
*
* @memberof cart.product
*
* @param {HTMLButtonElement|HTMLInputElement} element Handler elememnt of the product
* @param {HTMLElement} product The product element
* @param {bool} remove Remove the product element if json.amount === 0?
* @param {bool} force Ignore the damper? (false)
*
* @return {Promise}
*/
damper: core.damper(
(...variables) =>
cart.product.system(...variables).then(cart.summary.system),
300,
6,
),
},
);
Object.assign(
cart.summary,
{
/**
* @name Summary (damper)
*
* @description
* Initialize summary of products the cart
*
* @memberof cart.summary
*
* @return {Promise}
*/
damper: core.damper(
(...variables) => cart.summary.system(...variables),
300,
),
},
);
Object.assign(
cart.share,
{
/**
* @name Share (damper)
*
* @description
* Generate sharing hash of the cart and sent ot to the chat-robot
*
* @memberof cart.share
*
* @return {Promise}
*/
damper: core.damper(
(...variables) => cart.share.system(...variables),
400,
),
},
);
});
Object.assign(
cart.product,
{
/**
* @name Product (system)
*
* @description
* Handle the product in the cart
*
* @memberof cart.product
*
* @param {HTMLButtonElement|HTMLInputElement|null} element Handler element of the product
* @param {HTMLElement} product The product element
* @param {string} type Type of action with the product
* @param {number} amount Amount of product to handle
* @param {bool} remove Remove the product element if json.amount === 0?
*
* @return {Promise|null}
*/
async system(
element,
product,
type,
amount = null,
remove = false,
resolve = () => {},
reject = () => {},
) {
try {
if (product instanceof HTMLElement) {
// Validated required arguments
// Initializing the buffer of request body
let request = "";
// Initializing of identifier of the product
const identifier = +product.getAttribute(
"data-product-identifier",
);
if (typeof identifier === "number") {
// Validated identifier
// Writing to the buffer of request body
request += "&identifier=" + identifier;
if (
type === "toggle" ||
type === "write" ||
type === "delete" ||
type === "set"
) {
// Validated type
// Writing to the buffer of request body
request += "&type=" + type;
if (
(type === "toggle" &&
amount === null ||
typeof amount === "undefined") ||
(type === "set" &&
amount === 0 ||
amount === 100) ||
typeof amount === "number" &&
amount > 0 &&
amount < 100
) {
// Validated amount
if (type !== "toggle") {
// Not a toggle request
// Writing to the buffer of request body
request += "&amount=" + amount;
}
// Request
return await core.request(
"/cart/product",
request,
"PATCH",
)
.then(
(json) => {
if (json) {
// Received a JSON-response
if (
json.errors !== null &&
typeof json.errors === "object" &&
json.errors.length > 0
) {
// Fail (received errors)
core.modules.connect("telegram").then(() => {
// Imported the telegram module
core.telegram.api.HapticFeedback
.notificationOccurred("error");
});
// Exit (fail)
reject(json);
} else {
// Success (not received errors)
core.modules.connect("telegram").then(() => {
// Imported the telegram module
core.telegram.api.HapticFeedback
.notificationOccurred("success");
});
core.modules.connect("delivery").then(
() => {
// Imported the damper module
// Calculating delivery
core.delivery.calculate();
},
);
if (remove && json.amount === 0) {
// Requested deleting of the product element when there is no the product in the cart
// Deleting the product element
product.remove();
} else {
// Not requested deleting the product element when there is no the product in the cart
// Unblocking the element
element?.removeAttribute("disabled");
// Writing offset of hue-rotate to indicate that the product is in the cart
product.style.setProperty(
"--hue-rotate-offset",
json.amount + "0deg",
);
// Writing attribute with amount of the product in the cart
product.setAttribute(
"data-product-amount",
json.amount,
);
// Initializing the amount <span> element
const amounts = product.querySelectorAll(
'[data-product-parameter="amount"]',
);
for (const amount of amounts) {
// Iterating over an amount elements
if (amount instanceof HTMLInputElement) {
// The <input> element
// Writing amount of the product in the cart
amount.value = json.amount;
} else {
// Not the <input> element
// Writing amount of the product in the cart
amount.innerText = json.amount;
}
}
}
// Exit (success)
resolve();
}
}
},
() => reject(),
);
}
}
}
}
} catch (e) {
// Exit (fail)
reject(e);
}
},
},
);
Object.assign(
cart.summary,
{
/**
* @name Summary (system)
*
* @description
* Initialize summary of products the cart
*
* @memberof cart.summary
*
* @return {Promise}
*/
async system(
resolve = () => {},
reject = () => {},
) {
try {
// Request
return await core.request("/cart/summary", undefined, "GET")
.then((json) => {
if (json) {
// Received a JSON-response
if (
json.errors !== null &&
typeof json.errors === "object" &&
json.errors.length > 0
) {
// Fail (received errors)
core.modules.connect("telegram").then(() => {
// Imported the telegram module
core.telegram.api.HapticFeedback.notificationOccurred(
"error",
);
});
// Exit (fail)
reject(json);
} else {
// Success (not received errors)
// Initializing the summary amount <span> element
const amount = document.getElementById("amount");
if (amount instanceof HTMLElement) {
// Initialized the summary amount element
// Writing summary amount into the summary amount element
amount.innerText = json.amount;
}
// Initializing the summary cost <span> element
const cost = document.getElementById("cost");
if (cost instanceof HTMLElement) {
// Initialized the summary cost element
core.modules.connect("telegram").then(() => {
// Imported the telegram module
core.telegram.api.HapticFeedback.notificationOccurred(
"success",
);
});
// Writing summary cost into the summary cost element
cost.innerText = json.cost;
}
// Exit (success)
resolve(json);
}
// Exit (fail)
reject(json);
}
});
} catch (e) {
// Exit (fail)
reject(e);
}
},
},
);
Object.assign(
cart.share,
{
/**
* @name Share (system)
*
* @description
* Generate sharing hash of the cart and sent ot to the chat-robot
*
* @memberof cart.share
*
* @return {Promise}
*/
async system(
resolve = () => {},
reject = () => {},
) {
try {
// Request
return await core.request("/cart/pay", undefined, "POST")
.then((json) => {
if (json) {
// Received a JSON-response
if (
json.errors !== null &&
typeof json.errors === "object" &&
json.errors.length > 0
) {
// Fail (received errors)
// Exit (fail)
reject(json);
} else {
// Success (not received errors)
/* if (json.share) {
// Received sharing hash
// Request to the chat-robot
core.telegram.api.sendData(
JSON.stringify({
type: "cart_share",
hash: json.share,
}),
);
} */
if (json.robokassa) {
// Received data for the Robokassa acquiring
// Initializing iframe
Robokassa.StartPayment({
MerchantLogin: json.robokassa.identifier,
OutSum: json.robokassa.cost,
InvId: json.robokassa.cart,
Description: json.robokassa.description,
Culture: json.robokassa.language,
Encoding: "utf-8",
Settings: JSON.stringify({
PaymentMethods: ["BankCard", "SBP"],
Mode: "modal",
}),
SignatureValue: json.robokassa.hash,
});
}
// Exit (success)
resolve(json);
}
// Exit (fail)
reject(json);
}
});
} catch (e) {
// Exit (fail)
reject(e);
}
},
},
);
// Connecting to the core
if (!core.cart) core.cart = cart;

View File

@ -42,14 +42,15 @@ export default class catalog {
]);
/**
* Search (interface)
* @name Search (interface)
*
* Request search in the catalog and render in the core.main
* @description
* Request search in the catalog and render result in the core.main
*
* @param {Event} event Event (keyup)
* @param {bool} force Ignore the damper? (false)
*
* @return {bool} True if an error occurs to continue the event execution
* @return {bool} Did the execution complete without errors?
*/
static search(event, force = false) {
// Initializing the search <input> element
@ -68,7 +69,7 @@ export default class catalog {
// Only 1-2 character entered (this is not a search reset and is not an adequate search)
// Exit (fail)
return true;
return false;
}
}
@ -96,7 +97,7 @@ export default class catalog {
// Less than 3 character entered (this is not a search reset and is not an adequate search)
// Exit (fail)
return true;
return false;
} else {
// Executed by any else button
@ -136,22 +137,23 @@ export default class catalog {
}
// Exit (success)
return false;
return true;
}
// Exit (fail)
return true;
return false;
}
/**
* Product card (interface)
* @name Product card (interface)
*
* Request product data and render in core.window
* @description
* Request product data and render result in core.window
*
* @param {HTMLElement} button Button of the product <a>
* @param {bool} force Ignore the damper? (false)
*
* @return {bool} True if an error occurs to continue the event execution
* @return {bool} Did the execution complete without errors?
*/
static product(button, force = false) {
// Initializing identifier of the category
@ -175,7 +177,7 @@ export default class catalog {
);
// Exit (success)
return false;
return true;
}
}
@ -186,9 +188,12 @@ core.modules.connect("damper").then(() => {
catalog.search,
{
/**
* Search (damper)
* @name Search (system)
*
* Request search in the catalog and render in the core.main
* @description
* Request search in the catalog and render result in the core.main
*
* @memberof catalog.search
*
* @param {bool} force Ignore the damper? (false)
*
@ -196,7 +201,7 @@ core.modules.connect("damper").then(() => {
*/
damper: core.damper(
(...variables) => catalog.search.system(...variables),
1400,
300,
1,
),
},
@ -206,9 +211,12 @@ core.modules.connect("damper").then(() => {
catalog.product,
{
/**
* Product card (damper)
* @name Product card (damper)
*
* Request product data and render in core.window
* @description
* Request product data and render result in the core.window
*
* @memberof catalog.product
*
* @param {string} identifier Identifier of the product
* @param {bool} force Ignore the damper? (false)
@ -217,7 +225,7 @@ core.modules.connect("damper").then(() => {
*/
damper: core.damper(
(...variables) => catalog.product.system(...variables),
400,
200,
2,
),
},
@ -228,9 +236,12 @@ Object.assign(
catalog.product,
{
/**
* Product card (system)
* @name Product card (system)
*
* Request product data and render in core.window
* @description
* Request product data and render result in the core.window
*
* @memberof catalog.product
*
* @param {string} identifier Identifier of the product
*
@ -256,8 +267,8 @@ Object.assign(
// Intializing URI of the request
const uri = "?" + parameters;
return await core.request(uri).then(
(json) => {
return await core.request(uri, undefined, "GET").then(
async (json) => {
if (json) {
// Received a JSON-response
@ -267,6 +278,9 @@ Object.assign(
json.errors.length > 0
) {
// Fail (received errors)
// Exit (fail)
reject(json);
} else {
// Success (not received errors)
@ -277,7 +291,7 @@ Object.assign(
core.window.remove();
}
core.modules.connect("telegram").then(() => {
core.modules.connect(["cart", "telegram"]).then(async () => {
// Imported the telegram module
if (
@ -317,7 +331,36 @@ Object.assign(
const images = document.createElement("div");
images.classList.add("images", "unselectable");
const button = core.telegram.api.isVisible;
for (
const [index, uri] of json.product.images["200"]
.entries()
) {
const image = document.createElement("img");
image.setAttribute("src", uri);
image.setAttribute("data-image-small", uri);
image.setAttribute(
"data-image-big",
json.product.images["800"][index],
);
image.setAttribute("ondragstart", "return false;");
images.appendChild(image);
}
let width = 0;
let buffer;
[...images.children].forEach(
(child) => (width += child.offsetWidth +
(isNaN(
buffer = parseFloat(
getComputedStyle(child).marginRight,
),
)
? 0
: buffer)),
);
const button = core.telegram.api.isVisibless;
// блокировка закрытия изображений
let images_from;
@ -331,11 +374,11 @@ Object.assign(
event.button === 0
) {
{
let x = event.pageX || event.touches[0].pageX;
let y = event.pageY || event.touches[0].pageY;
let _x = images_from.pageX ||
const x = event.pageX || event.touches[0].pageX;
const y = event.pageY || event.touches[0].pageY;
const _x = images_from.pageX ||
images_from.touches[0].pageX;
let _y = images_from.pageY ||
const _y = images_from.pageY ||
images_from.touches[0].pageY;
if (
@ -344,6 +387,17 @@ Object.assign(
_y - y < 10 &&
_y - y > -10
) {
// Replacing small images with big images
Array.from(images.children).forEach((image) =>
image.setAttribute(
"src",
image.getAttribute("data-image-big"),
)
);
// Expanding the "Web App" window
core.telegram.api.expand();
images.classList.add("extend");
if (button) {
@ -365,11 +419,11 @@ Object.assign(
};
const _close = (event) => {
let x = event.pageX || event.touches[0].pageX;
let y = event.pageY || event.touches[0].pageY;
let _x = images_from.pageX ||
const x = event.pageX || event.touches[0].pageX;
const y = event.pageY || event.touches[0].pageY;
const _x = images_from.pageX ||
images_from.touches[0].pageX;
let _y = images_from.pageY ||
const _y = images_from.pageY ||
images_from.touches[0].pageY;
if (
@ -392,6 +446,15 @@ Object.assign(
if (width < card.offsetWidth) {
images.hotline.stop();
}
// Replacing big images with small images
Array.from(images.children).forEach((image) =>
image.setAttribute(
"src",
image.getAttribute("data-image-small"),
)
);
images.classList.remove("extend");
if (button) {
@ -406,29 +469,6 @@ Object.assign(
}
};
const _start = (event) => {
if (
event.type === "touchstart" ||
event.button === 0
) {
images.removeEventListener("mousedown", _start);
images.removeEventListener("touchstart", _start);
images.addEventListener("mouseup", _open);
images.addEventListener("touchend", _open);
}
};
images.addEventListener("mousedown", _start);
images.addEventListener("touchstart", _start);
for (const uri of json.product.images) {
const image = document.createElement("img");
image.setAttribute("src", uri);
image.setAttribute("ondragstart", "return false;");
images.append(image);
}
const header = document.createElement("p");
header.classList.add("header");
@ -477,37 +517,91 @@ Object.assign(
cost.classList.add("cost", "currency");
cost.innerText = json.product.cost;
h3.append(name);
exit.append(exit_icon);
h3.append(exit);
card.append(h3);
card.append(images);
header.append(brand);
card.append(header);
card.append(description);
card.append(compatibility);
footer.append(dimensions);
footer.append(weight);
footer.append(cost);
card.append(footer);
wrap.append(card);
document.body.append(wrap);
h3.appendChild(name);
exit.appendChild(exit_icon);
h3.appendChild(exit);
card.appendChild(h3);
card.appendChild(images);
header.appendChild(brand);
card.appendChild(header);
card.appendChild(description);
card.appendChild(compatibility);
footer.appendChild(dimensions);
footer.appendChild(weight);
footer.appendChild(cost);
card.appendChild(footer);
wrap.appendChild(card);
document.body.appendChild(wrap);
// Reinitialize parameter
core.window = document.getElementById("window");
let width = 0;
let buffer;
[...images.children].forEach(
(child) => (width += child.offsetWidth +
(isNaN(
buffer = parseFloat(
getComputedStyle(child).marginRight,
),
)
? 0
: buffer)),
);
// Write
const add = () => {
core.cart.write(
undefined,
document.getElementById(
"product_" + json.product.identifier,
),
);
core.telegram.api.MainButton
.setText(json.product.cart.text.added)
.setParams({
color: "#90be36",
has_shine_effect: true,
})
.offClick(add)
.onClick(added);
};
// Delete
const added = () => {
core.cart.set(
undefined,
document.getElementById(
"product_" + json.product.identifier,
),
0,
);
core.telegram.api.MainButton
.setText(json.product.cart.text.add)
.setParams({
color: core.telegram.api.themeParams.button_color,
// has_shine_effect: json.product.discount > 0,
has_shine_effect: false,
})
.offClick(added)
.onClick(add);
};
if (json.product.cart.amount > 0) {
// Initializing the "Main Button" of the "Web App" window
core.telegram.api.MainButton
.setText(json.product.cart.text.added)
.setParams({
color: "#90be36",
has_shine_effect: true,
})
.onClick(added)
.hideProgress()
.enable()
.show();
} else {
// Initializing the "Main Button" of the "Web App" window
core.telegram.api.MainButton
.setText(json.product.cart.text.add)
.setParams({
color: core.telegram.api.themeParams.button_color,
// has_shine_effect: json.product.discount > 0,
has_shine_effect: false,
})
.onClick(add)
.hideProgress()
.enable()
.show();
}
// блокировка закрытия карточки
let from;
@ -516,11 +610,36 @@ Object.assign(
wrap.addEventListener("touchstart", _from);
const remove = (event) => {
console.log("BABLO: " + typeof event);
if (
typeof event === "undefined" ||
event.type !== "popstate"
) {
history.back();
// Initialize the buffer of URN parameters @todo after opening window add to document.location.search
const parameters = new URLSearchParams(
document.location.search,
);
if (parameters.get("product") === identifier) {
// The previous window with the product card was exactly the same
console.log("aboba");
// Deleting product card
parameters.delete("product");
// Writing to the browser history
history.pushState(
{},
json.product.name,
"?" + parameters,
);
}
// Write parameter to the buffer of URN parameters
parameters.set("product", identifier);
}
wrap.remove();
@ -539,6 +658,17 @@ Object.assign(
document.removeEventListener("click", close);
document.removeEventListener("touch", close);
window.removeEventListener("popstate", remove);
// Denitializing the "Back Button" of the "Web App" window
core.telegram.api.BackButton.hide();
core.telegram.api.BackButton.offClick(remove);
// Deinitializing the "Main Button" of the "Web App" window
core.telegram.api.MainButton
.offClick(add)
.offClick(added)
.disable()
.hide();
};
const close = (event) => {
@ -564,21 +694,42 @@ Object.assign(
document.addEventListener("touch", close);
window.addEventListener("popstate", remove);
import("../hotline.js").then((hotline) => {
// Imported the hotline module
// Initializing the "Back Button" of the "Web App" window
core.telegram.api.BackButton.show();
core.telegram.api.BackButton.onClick(remove);
images.hotline = new hotline.default(
json.product.identfier,
images,
);
images.hotline.step = -0.3;
images.hotline.wheel = true;
images.hotline.touch = true;
setTimeout(() => {
import("./hotline.js").then((hotline) => {
// Imported the hotline module
if (width > card.offsetWidth) {
images.hotline.start();
}
});
const _start = (event) => {
if (
event.type === "touchstart" ||
event.button === 0
) {
images.removeEventListener("mousedown", _start);
images.removeEventListener("touchstart", _start);
images.addEventListener("mouseup", _open);
images.addEventListener("touchend", _open);
}
};
images.addEventListener("mousedown", _start);
images.addEventListener("touchstart", _start);
images.hotline = new hotline.default(
json.product.identfier,
images,
);
images.hotline.step = -0.3;
images.hotline.wheel = true;
images.hotline.touch = true;
if (width > card.offsetWidth) {
images.hotline.start();
}
});
}, 300);
}
});
@ -602,9 +753,12 @@ Object.assign(
catalog.search,
{
/**
* Search (system)
* @name Search (system)
*
* Request search in the catalog and render in the core.main
* @description
* Request search in the catalog and render result in the core.main
*
* @memberof catalog.search
*
* @return {Promise} Request to the server
*
@ -627,10 +781,7 @@ Object.assign(
if (
(typeof value === "string" ||
typeof value === "number") &&
!(
(parameter === "text" && value === null) ||
value.length < 3
)
!(parameter === "text" && value === null)
) {
// Validated value
@ -655,7 +806,7 @@ Object.assign(
// Found the search <input> element
input.removeAttribute("disabled");
input.focus();
// input.focus();
}
/**
@ -743,7 +894,7 @@ Object.assign(
// Writing the search <search> element after the title <h2> element in the <main> element
core.main.insertBefore(
search,
title.nextSibling,
title.nextElementSibling,
);
} else {
// Not initialized the title <h2> element in the <main> element
@ -814,7 +965,7 @@ Object.assign(
// Writing the categories <section> element after the search <search> element in the <main> element
core.main.insertBefore(
categories,
search.nextSibling,
search.nextElementSibling,
);
} else {
// Not initialized the search <search> element in the <main> element
@ -830,7 +981,7 @@ Object.assign(
// Writing the categories <section> element after the title <h2> element in the <main> element
core.main.insertBefore(
categories,
title.nextSibling,
title.nextElementSibling,
);
} else {
// Not initialized the title <h2> element in the <main> element
@ -900,7 +1051,7 @@ Object.assign(
// Writing the filters <section> element after the categories <section> element in the <main> element
core.main.insertBefore(
filters,
categories.nextSibling,
categories.nextElementSibling,
);
} else {
// Not initialized the categories <section> element in the <main> element
@ -916,7 +1067,7 @@ Object.assign(
// Writing the filters <section> element after the search <search> element in the <main> element
core.main.insertBefore(
filters,
search.nextSibling,
search.nextElementSibling,
);
} else {
// Not initialized the search <search> element in the <main> element
@ -932,7 +1083,7 @@ Object.assign(
// Writing the filters <section> element after the title <h2> element in the <main> element
core.main.insertBefore(
filters,
title.nextSibling,
title.nextElementSibling,
);
} else {
// Not initialized the title <h2> element in the <main> element
@ -999,7 +1150,7 @@ Object.assign(
// Writing the sorting <section> element after the filters <section> element in the <main> element
core.main.insertBefore(
sorting,
filters.nextSibling,
filters.nextElementSibling,
);
} else {
// Not initialized the filters <section> element in the <main> element
@ -1017,7 +1168,7 @@ Object.assign(
// Writing the sorting <section> element after the categories <section> element in the <main> element
core.main.insertBefore(
sorting,
categories.nextSibling,
categories.nextElementSibling,
);
} else {
// Notnitialized the categories <section> element in the <main> element
@ -1033,7 +1184,7 @@ Object.assign(
// Writing the sorting <section> element after the search <search> element in the <main> element
core.main.insertBefore(
sorting,
search.nextSibling,
search.nextElementSibling,
);
} else {
// Not nitialized the search <search> element in the <main> element
@ -1049,7 +1200,7 @@ Object.assign(
// Writing the sorting <section> element after the title <h2> element in the <main> element
core.main.insertBefore(
sorting,
title.nextSibling,
title.nextElementSibling,
);
} else {
// Not initialized the title <h2> element in the <main> element
@ -1117,7 +1268,7 @@ Object.assign(
// Writing the products <section> element after the sorting <section> element in the <main> element
core.main.insertBefore(
products,
sorting.nextSibling,
sorting.nextElementSibling,
);
} else {
// Initialized the sorting <section> element in the <main> element
@ -1133,7 +1284,7 @@ Object.assign(
// Writing the products <section> element after the filters <section> element in the <main> element
core.main.insertBefore(
products,
filters.nextSibling,
filters.nextElementSibling,
);
} else {
// Not initialized the filters <section> element in the <main> element
@ -1151,7 +1302,7 @@ Object.assign(
// Writing the products <section> element after the categories <section> element in the <main> element
core.main.insertBefore(
products,
categories.nextSibling,
categories.nextElementSibling,
);
} else {
// Not initialized the categories <section> element in the <main> element
@ -1167,7 +1318,7 @@ Object.assign(
// Writing the products <section> element after the search <search> element in the <main> element
core.main.insertBefore(
products,
search.nextSibling,
search.nextElementSibling,
);
} else {
// Not initialized the search <search> element in the <main> element
@ -1183,7 +1334,7 @@ Object.assign(
// Writing the products <section> element after the title <h2> element in the <main> element
core.main.insertBefore(
products,
title.nextSibling,
title.nextElementSibling,
);
} else {
// Not initialized the title <h2> element in the <main> element

View File

@ -60,7 +60,7 @@ export default function damper(func, timeout = 300, force) {
// Normal execution
// Deleting the force argument
if (typeof force === "number") delete args[force - 1];
if (typeof force === "number") args.splice(force - 1, force);
// Writing promise handlers into the arguments variable
args.push(resolve, reject);

View File

@ -1,161 +0,0 @@
"use strict";
/**
* @name Delivery
*
* @description
* Implements actions with delivery
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
export default class delivery {
/**
* @name Type of the program
*/
static type = "module";
/**
* @name Company
*
* @description
* Choose a delivery company (interface)
*
* @param {HTNLElement} element Handler element
* @param {string} identifier Identifier of the delivery company
* @param {bool} force Ignore the damper? (false)
*
* @return {bool} Execution completed with an error?
*/
static company = (element, identifier, force = false) => {
core.modules.connect("damper").then(
() => {
// Imported the damper module
// Execute under damper
this.company.damper(element, identifier, force);
},
() => {
// Not imported the damper module
// Execute
this.company.system(element, identifier, force);
},
);
// Exit (success)
return false;
};
/**
* @name City
*
* @description
* Write name of a city for delivery (interface)
*
* @param {HTNLInputElement} element Handler element <input>
* @param {bool} force Ignore the damper? (false)
*
* @return {bool} Execution completed with an error?
*/
static company = (element, force = false) => {
core.modules.connect("damper").then(
() => {
// Imported the damper module
// Execute under damper
this.city.damper(element, identifier, force);
},
() => {
// Not imported the damper module
// Execute
this.company.system(element, identifier, force);
},
);
// Exit (success)
return false;
};
}
core.modules.connect("damper").then(() => {
// Imported the damper module
Object.assign(
delivery.company,
{
/**
* @name Write
*
* @description
* Choose a delivery company (damper)
*
* @param {HTNLElement} element Handler element
* @param {string} identifier Identifier of the delivery company
* @param {bool} force Ignore the damper? (false)
*
* @return {Promise}
*/
damper: core.damper(
(...variables) => delivery.company.system(...variables),
300,
3,
),
},
);
});
Object.assign(
delivery.company,
{
/**
* @name Write
*
* @description
* Choose a delivery company (system)
*
* @param {string} identifier Identifier of the delivery company
*
* @return {Promise}
*/
async system(
type,
value,
resolve = () => {},
reject = () => {},
) {
try {
if (
typeof type === "string" &&
(typeof value === "string" || typeof valye === "number")
) {
// Received and validated required arguments
// Sending request to the server
return await core.buffer.write(`delivery_${type}`, value)
.then(
(json) => {
try {
// Exit (success)
resolve(json);
} catch (e) {
// Exit (fail)
reject(e);
}
},
() => reject(),
);
}
} catch (e) {
// Exit (fail)
reject(e);
}
},
},
);
// Connecting to the core
if (!core.delivery) core.delivery = delivery;

View File

@ -0,0 +1,898 @@
"use strict";
/**
* @name Delivery
*
* @description
* Implements actions with delivery
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
export default class delivery {
/**
* @name Type of the program
*/
static type = "module";
/**
* @name Company (interface)
*
* @description
* Choose a delivery company
*
* @param {HTMLInputElement} company The button for choose a delivery company <input> (radio)
* @param {bool} force Ignore the damper? (false)
*
* @return {bool} Did the execution complete without errors?
*/
static company = (company, force = false) => {
if (company instanceof HTMLInputElement) {
// Initialized the delivery company <input> (radio) element
// Disabling the delivery company <input> (radio) element
company.setAttribute("disabled", true);
// Initializing timer of enabling the delivery company <input> (radio) element
const enabling = setTimeout(() => {
// Enabling the delivery company <input> (radio) element
company.removeAttribute("disabled");
}, 3000);
// Initializing the order <button> element
const order = document.getElementById("order");
if (order instanceof HTMLButtonElement) {
// Initializeg the order <button> element
// Disabling the order <button> element
order.setAttribute("disabled", true);
}
/**
* @name Resolved
*
* @description
* Render the result of the resolved request
*
* @param {object} json
*
* @return {void}
*/
const resolved = (json) => {
if (json) {
// Received JSON-response
// Deinitializing timer of enabling the delivery company <input> (radio) element
clearTimeout(enabling);
// Enabling the delivery company <input> (radio) element
company.removeAttribute("disabled");
if (json.status === "success") {
// Received "success" status of execution
// Select the delivery company <input> (radio) element
company.checked = true;
}
if (json.ready) {
// Received readiness status
if (order instanceof HTMLButtonElement) {
// Initializeg the order <button> element
// Enabling the order <button> element
order.removeAttribute("disabled");
}
// Calculating delivery
this.calculate();
}
if (json.company) {
// Received company property
// Initializing the delivery company <input> (radio) element
const button = document.getElementById(json.company);
if (button instanceof HTMLInputElement) {
// Initialized the delivery company <input> (radio) element
// Select the delivery company <input> (radio) element
button.checked = true;
}
}
}
};
/**
* @name Rejected
*
* @description
* Render the result of the rejected request
*
* @param {object} json
*
* @return {void}
*/
const rejected = (json) => {
if (json) {
// Received JSON-response
// Deinitializing timer of enabling the delivery company <input> (radio) element
clearTimeout(enabling);
// Enabling the delivery company <input> (radio) element
company.removeAttribute("disabled");
}
};
core.modules.connect("damper").then(
() => {
// Imported the damper module
// Execute under damper
this.write.damper("company", company.getAttribute("id"), force)
.then(resolved, rejected);
},
() => {
// Not imported the damper module
// Execute
this.write.system("company", company.getAttribute("id"), force)
.then(resolved, rejected);
},
);
// Exit (success)
return true;
}
// Exit (fail)
return false;
};
/**
* @name Location (interface)
*
* @description
* Write name of the location for delivery
*
* @param {HTMLInputElement} location The input element with name of the location <input>
* @param {bool} force Ignore the damper? (false)
*
* @return {bool} Did the execution complete without errors?
*/
static location = (location, force = false) => {
if (location instanceof HTMLInputElement) {
// Initialized the location <input> element
// Deleting statuses from the location <input> element
location.classList.remove("success", "fail");
// Initializing the order <button> element
const order = document.getElementById("order");
if (order instanceof HTMLButtonElement) {
// Initializeg the order <button> element
// Disabling the order <button> element
order.setAttribute("disabled", true);
}
/**
* @name Resolved
*
* @description
* Render the result of the resolved request
*
* @param {object} json
*
* @return {void}
*/
const resolved = (json) => {
if (json) {
// Received JSON-response
if (json.status) {
// Received status of execution
// Deleting statuses from the location <input> element
location.classList.remove("success", "fail");
// Writing status of execution as CSS class
location.classList.add(json.status);
}
if (json.ready) {
// Received readiness status
if (order instanceof HTMLButtonElement) {
// Initializeg the order <button> element
// Enabling the order <button> element
order.removeAttribute("disabled");
}
// Calculating delivery
this.calculate();
}
if (json.location) {
// Received location
if (json.location.identifier) {
// Received location identifier
if (location.value !== json.location) {
// The value entered differs from what was written on the server
// Writing attribute with location identifier
/* location.setAttribute(
"data-location-identifier",
json.location.identifier,
); */
// Writing CSS variable with location identifier
/* location.style.setProperty(
"--identifier",
json.location.identifier,
); */
}
}
if (json.location.input) {
// Received location value for <input> element
if (location.value !== json.location.input) {
// The value entered differs from what was written on the server
// Writing location property to the location <input> element
location.value = json.location.input;
}
}
}
if (json.locations) {
// Reveived locations suggestions
// Initializing the locations suggestions <datalist> element
let locations = document.getElementById("locations");
if (!(locations instanceof HTMLDataListElement)) {
// Not initialized the locations suggestions <datalist> element
// Initializing the locations suggestions <datalist> element
const datalist = document.createElement("datalist");
datalist.setAttribute("id", "locations");
// Connection the locations suggestions <datalist> element to the location <input> element
location.setAttribute("list", "locations");
// Writing the locations suggestions <datalist> element after the location <input> element
location.parentElement.insertBefore(
datalist,
location.nextElementSibling,
);
// Reinitializing the locations suggestions <datalist> element
locations = document.getElementById("locations");
}
}
if (locations instanceof HTMLDataListElement) {
// Initialized locations suggestions <datalist> element
// Initializing buffer of created options
const options = new Set();
for (const location of json.locations ?? []) {
// Iterating over locations suggestions
// Initializing the location <option> element
const option = document.createElement("option");
option.setAttribute(
"value",
(location.name + ", " + location.structure.reverse().join(", "))
.trim(),
);
// Writing to buffer of created options
options.add(option);
}
// Replacing content of the locations suggestions <datalist> element
locations.replaceChildren(...Array.from(options));
// locations.focus();
}
}
};
/**
* @name Rejected
*
* @description
* Render the result of the rejected request
*
* @param {object} json
*
* @return {void}
*/
const rejected = (json) => {
if (json) {
// Received JSON-response
if (json.status) {
// Reveived status of execution
// Deleting statuses from the location <input> element
location.classList.remove("success", "fail");
// Writing status of execution as CSS class
location.classList.add(json.status);
}
}
};
core.modules.connect("damper").then(
() => {
// Imported the damper module
// Execute under damper
this.write.damper("location", location.value, force)
.then(resolved, rejected);
},
() => {
// Not imported the damper module
// Execute
this.write.system("location", location.value, force)
.then(resolved, rejected);
},
);
// Exit (success)
return true;
}
// Exit (fail)
return false;
};
/**
* @name Street (interface)
*
* @description
* Write name of the street for delivery
*
* @param {HTMLInputElement} element The input element with name of the street <input>
* @param {bool} force Ignore the damper? (false)
*
* @return {bool} Did the execution complete without errors?
*/
static street = (street, force = false) => {
if (street instanceof HTMLInputElement) {
// Initialized the street <input> element
// Deleting statuses from the street <input> element
street.classList.remove("success", "fail");
// Initializing the order <button> element
const order = document.getElementById("order");
if (order instanceof HTMLButtonElement) {
// Initializeg the order <button> element
// Disabling the order <button> element
order.setAttribute("disabled", true);
}
/**
* @name Resolved
*
* @description
* Render the result of the resolved request
*
* @param {object} json
*
* @return {void}
*/
const resolved = (json) => {
if (json) {
// Received JSON-response
if (json.status) {
// Reveived status of execution
// Deleting statuses from the street <input> element
street.classList.remove("success", "fail");
// Writing status of execution as CSS class
street.classList.add(json.status);
}
if (json.ready) {
// Received readiness status
if (order instanceof HTMLButtonElement) {
// Initializeg the order <button> element
// Enabling the order <button> element
order.removeAttribute("disabled");
}
core.modules.connect("telegram").then(() => {
// Imported the telegram module
core.telegram.api.HapticFeedback.notificationOccurred("success");
});
// Calculating delivery
this.calculate();
}
if (json.street) {
// Received street property
if (street.value !== json.street) {
// The value entered differs from what was written on the server
// Writing street property to the street element <input>
street.value = json.street;
}
}
}
};
/**
* @name Rejected
*
* @description
* Render the result of the rejected request
*
* @param {object} json
*
* @return {void}
*/
const rejected = (json) => {
if (json) {
// Received JSON-response
if (json.status) {
// Reveived status of execution
// Deleting statuses from the street <input> element
street.classList.remove("success", "fail");
// Writing status of execution as CSS class
street.classList.add(json.status);
}
}
};
core.modules.connect("damper").then(
() => {
// Imported the damper module
// Execute under damper
this.write.damper("street", street.value, force)
.then(resolved, rejected);
},
() => {
// Not imported the damper module
// Execute
this.write.system("street", street.value, force)
.then(resolved, rejected);
},
);
// Exit (success)
return true;
}
// Exit (fail)
return false;
};
/**
* @name Write (interface)
*
* @return {void}
*/
static write = () => {};
/**
* @name Calculate (interface)
*
* @description
* Calculate delivery by validated data from buffers
*
* @return {bool} Did the execution complete without errors?
*/
static calculate = () => {
core.modules.connect("damper").then(
() => {
// Imported the damper module
// Execute under damper
this.calculate.damper();
},
() => {
// Not imported the damper module
// Execute
this.calculate.system();
},
);
// Exit (success)
return true;
};
}
core.modules.connect("damper").then(() => {
// Imported the damper module
Object.assign(
delivery.write,
{
/**
* @name Write (damper)
*
* @description
* Validate and write a delivery parameter into buffers
*
* @memberof delivery.write
*
* @param {string} name Name of the parameter
* @param {string|number} value Value of the parameter
* @param {bool} force Ignore the damper? (false)
*
* @return {Promise}
*/
damper: core.damper(
(...variables) => delivery.write.system(...variables),
1200,
3,
),
},
);
Object.assign(
delivery.calculate,
{
/**
* @name Calculate (damper)
*
* @description
* Calculate delivery by validated data from buffers
*
* @memberof delivery.calculate
*
* @return {Promise}
*/
damper: core.damper(
(...variables) => delivery.calculate.system(...variables),
300,
),
},
);
});
Object.assign(
delivery.write,
{
/**
* @name Write (system)
*
* @description
* Validate and write a delivery parameter into buffers
*
* @memberof delivery.write
*
* @param {string} name Name of the parameter
* @param {string|number} value Value of the parameter
*
* @return {Promise}
*/
async system(
name,
value,
resolve = () => {},
reject = () => {},
) {
try {
if (
typeof name === "string" &&
(typeof value === "string" || typeof value === "number")
) {
// Received and validated required arguments
// Sending request to the server
return await core.request(
"/delivery/write",
`${name}=${value}`,
"PATCH",
)
.then(
(json) => {
if (json) {
// Received a JSON-response
if (
json.errors !== null &&
typeof json.errors === "object" &&
json.errors.length > 0
) {
// Fail (received errors)
// Exit (fail)
reject(json);
} else {
// Success (not received errors)
if (json.validated) {
// Validated delivery data from all fields
// Unblocking the "continue" button in the summary <section> element
document.getElementById("summary")
?.classList.remove("disabled");
}
// Exit (success)
resolve(json);
}
}
},
() => reject(),
);
}
} catch (e) {
// Exit (fail)
reject(e);
}
},
},
);
Object.assign(
delivery.calculate,
{
/**
* @name Calculate (system)
*
* @description
* Calculate delivery by validated data from buffers
*
* @memberof delivery.calculate
*
* @return {Promise}
*/
async system(
resolve = () => {},
reject = () => {},
) {
try {
// Sending request to the server
return await core.request(
"/delivery/calculate",
undefined,
"GET",
)
.then(
(json) => {
if (json) {
// Received JSON-response
if (json.cost) {
// Received delivery cost
// Initializing the delivery cost <span> element
const cost = document.getElementById("cost_delivery");
if (cost instanceof HTMLElement) {
// Initialized the delivery cost <span> element
// Writing delivery cost to the delivery cost <span> element
cost.innerText = json.cost;
} else {
// Not initialized the delivery cost <span> element
// Initializing the delivery cost <span> element
const cost = document.createElement("span");
cost.setAttribute("id", "cost_delivery");
cost.classList.add(
"cost",
"delivery",
"currency",
"plus",
"hint",
);
// Initializing the products cost <span> element
const products = document.getElementById("cost");
if (products instanceof HTMLElement) {
// Initialized the products cost <span> element
// Writing the delivery cost <span> element after the products cost <span> element
products.parentElement.insertBefore(
cost,
products.nextElementSibling,
);
} else {
// Not initialized the products cost <span> element
// Initializing the products amount <span> element
const amount = document.getElementById("cost");
if (amount instanceof HTMLElement) {
// Initialized the products amount <span> element
// Writing the delivery cost <span> element after the products amount <span> element
amount.parentElement.insertBefore(
cost,
amount.nextElementSibling,
);
} else {
// Not initialized the products amount <span> element
// Initializing the order <button> element
const order = document.getElementById("cost");
if (order instanceof HTMLButtonElement) {
// Initialized the order <button> element
// Writing the delivery cost <span> element before the order <button> element
order.parentElement.insertBefore(
cost,
order,
);
} else {
// Not initialized the order <button> element
// Initializing the summary first row <div> element
const summary = core.main.querySelector(
"#summary>div.row",
);
if (summary instanceof HTMLElement) {
// Initialized the summary first row <div> element
// Writing the delivery cost <span> element into the summary first row <div> element
summary.appendChild(cost);
}
}
}
}
// Writing delivery cost to the delivery cost <span> element
cost.innerText = json.cost;
}
} else {
// Not received delivery cost
// Deleting the delivery cost <span> element
document.getElementById("cost_delivery")?.remove();
}
if (json.days) {
// Received delivery days
// Initializing the delivery days <span> element
const days = document.getElementById("shipping");
if (days instanceof HTMLElement) {
// Initialized the delivery days <span> element
// Writing delivery days to the delivery days <span> element
days.innerText = json.days;
} else {
// Not initialized the delivery days <span> element
// Initializing the delivery days <span> element
const days = document.createElement("span");
days.setAttribute("id", "shipping");
days.classList.add("delivery", "days", "hint");
// Initializing the delivery cost <span> element
const cost = document.getElementById("cost_delivery");
if (cost instanceof HTMLElement) {
// Initialized the delivery cost <span> element
// Writing the delivery days <span> element after the delivery cost <span> element
cost.parentElement.insertBefore(
days,
cost.nextElementSibling,
);
} else {
// Not initialized the delivery cost <span> element
// Initializing the products cost <span> element
const products = document.getElementById("cost");
if (products instanceof HTMLElement) {
// Initialized the products cost <span> element
// Writing the delivery days <span> element after the products cost <span> element
products.parentElement.insertBefore(
days,
products.nextElementSibling,
);
} else {
// Not initialized the products cost <span> element
// Initializing the products amount <span> element
const amount = document.getElementById("cost");
if (amount instanceof HTMLElement) {
// Initialized the products amount <span> element
// Writing the delivery days <span> element after the products amount <span> element
amount.parentElement.insertBefore(
days,
amount.nextElementSibling,
);
} else {
// Not initialized the products amount <span> element
// Initializing the order <button> element
const order = document.getElementById("cost");
if (order instanceof HTMLButtonElement) {
// Initialized the order <button> element
// Writing the delivery days <span> element before the order <button> element
order.parentElement.insertBefore(
days,
order,
);
} else {
// Not initialized the order <button> element
// Initializing the summary first row <div> element
const summary = core.main.querySelector(
"#summary>div.row",
);
if (summary instanceof HTMLElement) {
// Initialized the summary first row <div> element
// Writing the delivery days <span> element into the summary first row <div> element
summary.appendChild(days);
}
}
}
}
}
// Writing delivery days to the delivery days <span> element
days.innerText = json.days;
}
} else {
// Not received delivery days
// Deleting the delivery days <span> element
document.getElementById("shipping")?.remove();
}
// Exit (success)
resolve(json);
}
},
() => reject(),
);
} catch (e) {
// Exit (fail)
reject(e);
}
},
},
);
// Connecting to the core
if (!core.delivery) core.delivery = delivery;

View File

@ -31,7 +31,7 @@ export default class loader {
.request(
uri,
body,
"POST",
"GET",
{
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
@ -48,6 +48,9 @@ export default class loader {
json.errors.length > 0
) {
// Fail (received errors)
// Exit (fail)
reject(json);
} else {
// Success (not received errors)
@ -147,7 +150,7 @@ export default class loader {
// Writing the <header> element after the last <section> element inside the <body> element
body.insertBefore(
core.header,
section.nextSibling,
section.nextElementSibling,
);
} else {
// Not found section elements <section> inside the <body> element
@ -200,7 +203,7 @@ export default class loader {
// Writing the <main> element after the <header> element
body.insertBefore(
core.main,
core.header.nextSibling,
core.header.nextElementSibling,
);
} else if (core.footer instanceof HTMLElement) {
// Fount the <footer> element
@ -219,7 +222,7 @@ export default class loader {
// Found the last <section> element inside the <body> element
// Writing the <main> element after the last <section> element inside the <body> element
body.insertBefore(core.main, section.nextSibling);
body.insertBefore(core.main, section.nextElementSibling);
} else {
// Not found section elements <section> inside the <body> element

View File

@ -29,7 +29,7 @@ export default class session {
* @param {string} value Value of the parameter (it can be JSON)
* @param {bool} force Ignore the damper? (false)
*
* @return {bool} Execution completed with an error?
* @return {bool} Did the execution complete without errors?
*/
static write = (name, value, force = false) => {
core.modules.connect("damper").then(
@ -48,7 +48,7 @@ export default class session {
);
// Exit (success)
return false;
return true;
};
};
}
@ -60,10 +60,12 @@ core.modules.connect("damper").then(() => {
session.buffer.write,
{
/**
* @name Write
* @name Write (damper)
*
* @description
* Write to the session buffer (damper)
* Write to the session buffer
*
* @memberof session.buffer.write
*
* @param {string} name Name of the parameter
* @param {string} value Value of the parameter (it can be JSON)
@ -84,10 +86,12 @@ Object.assign(
session.buffer.write,
{
/**
* @name Write
* @name Write (system)
*
* @description
* Write to the session buffer (system)
* Write to the session buffer
*
* @memberof session.buffer.write
*
* @param {string} name Name of the parameter
* @param {string} value Value of the parameter (it can be JSON)
@ -100,22 +104,38 @@ Object.assign(
resolve = () => {},
reject = () => {},
) {
if (typeof name === "string" && typeof value === "string") {
if (
typeof name === "string" &&
(typeof value === "string" || typeof value === "number")
) {
// Received and validated required arguments
// Sending request to the server
return await core.request(
"/session/write",
`${name}=${value}`,
"POST",
"PATCH",
)
.then(
(json) => {
if (json) {
// Received a JSON-response
// Exit (success)
resolve(json);
if (
json.errors !== null &&
typeof json.errors === "object" &&
json.errors.length > 0
) {
// Fail (received errors)
// Exit (fail)
reject(json);
} else {
// Success (not received errors)
// Exit (success)
resolve(json);
}
}
},
() => reject(),

View File

@ -0,0 +1,47 @@
"use strict";
core.modules.connect(["telegram"])
.then(() => {
//
// Expanding the "Web App" window
// core.telegram.api.expand();
// Writing settings for the "Web App" window
core.telegram.api.enableVerticalSwipes();
core.telegram.api.disableClosingConfirmation();
core.telegram.api.setHeaderColor(core.telegram.api.themeParams.header_bg_color);
core.telegram.api.setBackgroundColor(core.telegram.api.themeParams.secondary_bg_color);
core.telegram.api.setBottomBarColor(core.telegram.api.themeParams.bottom_bar_bg_color);
// Writing settings for the "Back Button" of the "Web App" window
core.telegram.api.BackButton.hide();
if (core.telegram.api.themeParams.section_bg_color === core.telegram.api.themeParams.bg_color) {
// The sections color matches the background color
// Replacing the sections color with the secondary background color
document.documentElement.style.setProperty('--tg-theme-section-bg-color', 'var(--tg-theme-secondary-bg-color)');
}
core.modules.connect(["session", "account"])
.then(() => {
//
const { initData, initDataUnsafe, ...data } = core.telegram.api;
//
core.session.buffer.write("telegram_program", JSON.stringify(data));
if (core.telegram.api.initData.length > 0) {
//
//
core.account.authentication();
}
});
//
core.telegram.api.ready();
});

View File

@ -7,14 +7,15 @@ namespace mirzaev\arming_bot;
// Files of the project
use mirzaev\arming_bot\controllers\core as controller,
mirzaev\arming_bot\models\core as model,
mirzaev\arming_bot\models\cart,
mirzaev\arming_bot\models\telegram;
// Фреймворк Telegram
// Framework for Telegram
use Zanzara\Zanzara,
Zanzara\Context,
Zanzara\Config;
ini_set('error_reporting', E_ALL);
ini_set('error_reporting', E_ALL ^ E_DEPRECATED);
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
@ -50,7 +51,7 @@ require __DIR__ . DIRECTORY_SEPARATOR
. 'autoload.php';
// Инициализация ядра контроллеров MINIMAL
new controller(false);
/* new controller(new core, false); */
// Инициализация ядра моделей MINIMAL
new model(true);
@ -61,10 +62,26 @@ $config->useReactFileSystem(true);
$bot = new Zanzara(TELEGRAM_KEY, $config);
/* $bot->onUpdate(function (Context $ctx): void {
var_dump($ctx->getMessage()->getWebAppData());
var_dump($ctx->getEffectiveUser() );
}); */
$bot->onUpdate(function (Context $ctx): void {
// Initializing 'web app" data
$app = $ctx->getMessage()?->getWebAppData();
if (!empty($app)) {
// Initialized "web app" data
// Initializing request from "web app" data
$request = json_decode($app->getData(), false, 10);
if ($request->type === 'cart_share') {
// Cart attaching
// Attaching cart to the Telegram account
telegram::cart_attach($ctx, $request->hash, $request->delivery);
}
}
unset($app);
});
$bot->onCommand('start', fn($ctx) => telegram::start($ctx));
$bot->onCommand('contacts', fn($ctx) => telegram::contacts($ctx));
@ -82,6 +99,10 @@ $bot->onCbQueryData(['import_request'], fn($ctx) => telegram::import_request($ct
$bot->onCbQueryData(['tuning'], fn($ctx) => telegram::tuning($ctx));
$bot->onCbQueryData(['brands'], fn($ctx) => telegram::brands($ctx));
$bot->onException(function (Context $ctx, $exception) {
var_dump($exception);
});
// Инициализация middleware с обработкой аккаунта
$bot->middleware([telegram::class, "account"]);

0
mirzaev/arming_bot/system/public/socket.php Normal file → Executable file
View File

View File

View File

@ -9,7 +9,7 @@ main>section:is(#summary, #products, #delivery) {
main>section#delivery>div.column {
padding: 1rem;
gap: var(--gap);
background-color: var(--tg-theme-secondary-bg-color);
background-color: var(--tg-theme-section-bg-color);
}
main>section#delivery>div.column>div#deliveries>input {
@ -19,10 +19,11 @@ main>section#delivery>div.column>div#deliveries>input {
main>section#delivery>div.column>div#deliveries>input+label {
/* backdrop-filter: brightness(1.2); */
/* background-color: unset; */
filter: brightness(0.6);
}
main>section#delivery>div.column>div#deliveries>input+label:is(:hover, :focus) {
filter: brightness(1.1) hue-rotate(30deg);
filter: brightness(1.3);
/* filter: unset; */
/* backdrop-filter: brightness(1.4); */
}
@ -33,15 +34,15 @@ main>section#delivery>div.column>div#deliveries>input+label:active {
}
main>section#delivery>div.column>div#deliveries>input:checked+label {
filter: hue-rotate(30deg);
filter: unset;
}
main>section#delivery>div.column>div#deliveries>input:checked+label:is(:hover, :focus) {
filter: brightness(1.1) hue-rotate(30deg);
filter: brightness(1.5);
}
main>section#delivery>div.column>div#deliveries>input:checked+label:active {
filter: brightness(0.9) hue-rotate(30deg);
filter: brightness(0.8);
}
main>section#delivery>div.column>div#address {
@ -49,7 +50,7 @@ main>section#delivery>div.column>div#address {
gap: 0.7rem;
}
main>section#delivery>div.column>div#address>input#city {
main>section#delivery>div.column>div#address>input#location {
min-width: max(6rem, 20%);
width: min(6rem, 100%);
flex-grow: 1;
@ -61,6 +62,10 @@ main>section#delivery>div.column>div#address>input#street {
flex-grow: 10;
}
main>section#delivery>div.column:not(:has(div#deliveries>input:checked))>div#address {
display: none;
}
main:has(section#summary.disabled:hover)>section#delivery>div {
filter: brightness(1.2);
}
@ -74,7 +79,7 @@ main>section#summary>div {
overflow: hidden;
pointer-events: auto;
border-radius: 1.375rem;
background-color: var(--tg-theme-secondary-bg-color);
background-color: var(--tg-theme-section-bg-color);
}
main>section#summary>div>span {
@ -89,6 +94,11 @@ main>section#summary>div>button#order {
/* margin-left: 0.3rem; */
margin-left: auto;
padding-left: 0.7rem;
background: unset;
}
main>section#summary>div>button#order * {
color: var(--tg-theme-button-color);
}
main>section#summary.disabled,
@ -112,7 +122,7 @@ main>section#products>article.product {
display: flex;
border-radius: 0.75rem;
overflow: hidden;
background-color: var(--tg-theme-secondary-bg-color);
background-color: var(--tg-theme-section-bg-color);
}
main>section#products>article.product[data-product-amount]:not([data-product-amount="0"]) * {
@ -132,11 +142,15 @@ main>section#products>article.product>a {
}
main>section#products>article.product>a>img:first-of-type {
width: 5rem;
/* width: 5rem; */
min-width: 5rem;
min-height: 100%;
object-fit: cover;
image-rendering: auto;
border-radius: 0.75rem;
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);
}
main>section#products>article.product>div {
@ -215,8 +229,12 @@ main>section#products>article.product>div>div.footer>input {
text-align: center;
}
@container summary (max-width: 350px) {
main>section#summary>div>span:not(#cost) {
@container summary (max-width: 450px) {
main>section#summary>div>span.days {
--days: var(--days-short);
}
main>section#summary>div>span:not(#cost, #cost_delivery, #shipping) {
display: none;
}
}

View File

@ -56,7 +56,10 @@ main>section#categories>a.category[type="button"]:has(>img)>p {
margin: unset;
width: min-content;
border-radius: 0.75rem;
background: var(--tg-theme-secondary-bg-color);
background: var(--tg-theme-section-bg-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 {
@ -95,13 +98,14 @@ main>section#products>div.column>article.product {
flex-direction: column;
border-radius: 0.75rem;
overflow: clip;
backdrop-filter: brightness(0.7);
cursor: pointer;
backdrop-filter: brightness(0.7);
background-color: var(--tg-theme-section-bg-color);
}
main>section#products>div.column>article.product:is(:hover, :focus) {
/* flex-grow: 0.1; */
/* background-color: var(--tg-theme-secondary-bg-color); */
/* background-color: var(--tg-theme-section-bg-color); */
}
main>section#products>div.column>article.product:is(:hover, :focus)>* {
@ -112,14 +116,13 @@ main>section#products>div.column>article.product:not(:is(:hover, :focus))>* {
transition: 0.2s ease-out;
}
main>section#products>div.column>article.product>a {
display: contents;
}
main>section#products>div.column>article.product>a>img:first-of-type {
width: 100%;
height: 100%;
image-rendering: auto;
border-radius: 0.75rem;
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);
}
main>section#products>div.column>article.product>a>img:first-of-type+* {
@ -129,13 +132,15 @@ main>section#products>div.column>article.product>a>img:first-of-type+* {
main>section#products>div.column>article.product>a>p.title {
z-index: 50;
margin: unset;
padding: 4px 8px;
padding: 4px 8px 8px;
font-size: 0.9rem;
font-weight: bold;
overflow-wrap: anywhere;
hyphens: auto;
color: var(--tg-theme-text-color);
background-color: var(--tg-theme-secondary-bg-color);
}
main>section#products>div.column>article.product>a>p.title>span {
color: var(--tg-theme-hint-color);
}
main>section#products>div.column>article.product>div[data-product="buttons"]:last-of-type {
@ -144,6 +149,8 @@ main>section#products>div.column>article.product>div[data-product="buttons"]:las
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
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 {

View File

View File

@ -2,29 +2,30 @@
i.icon.close {
--diameter: 22px;
box-sizing: border-box;
position: relative;
display: block;
width: var(--diameter);
height: var(--diameter);
border: 2px solid transparent;
border-radius: 40px;
}
i.icon.close::after,
i.icon.close::before {
content: "";
display: block;
box-sizing: border-box;
position: absolute;
width: 16px;
height: 2px;
background: currentColor;
transform: rotate(45deg);
border-radius: 5px;
top: 8px;
left: 1px;
}
i.icon.close::after {
transform: rotate(-45deg);
box-sizing: border-box;
position: relative;
display: block;
width: var(--diameter);
height: var(--diameter);
border: 2px solid transparent;
border-radius: 40px;
}
i.icon.close::after,
i.icon.close::before {
content: "";
display: block;
box-sizing: border-box;
position: absolute;
width: 16px;
height: 2px;
background: currentColor;
transform: rotate(45deg);
border-radius: 5px;
top: 8px;
left: 1px;
}
i.icon.close::after {
transform: rotate(-45deg);
}

View File

@ -10,14 +10,25 @@ section[data-type="select"] {
height: var(--height-close);
display: flex;
flex-direction: column;
cursor: context-menu;
cursor: pointer;
border-radius: 0.75rem;
overflow-x: hidden;
background-color: var(--tg-theme-button-color);
transition: 0s;
}
section[data-type="select"]:not(:focus):after {
section[data-type="select"]>button:has(>i.icon.close) {
align-self: end;
height: 100%;
padding: 0 0.4rem;
}
section[data-type="select"]:has(input[id$="title"]:checked)>button:has(>i.icon.close),
section[data-type="select"]:focus>button:has(>i.icon.close) {
display: none;
}
section[data-type="select"]:not(:focus, :has(>button>i.icon.close)):after {
z-index: 30;
content: '';
top: calc(50% - 2.5px);

View File

@ -6,6 +6,9 @@
--socket-connected: #2be851;
--socket-disconnected: #8e8181;
--socket-text: #b09999;
outline: none;
-webkit-tap-highlight-color: transparent;
}
* {
@ -31,19 +34,23 @@ body {
margin: 0;
min-height: 100vh;
padding: 0;
gap: 1rem;
display: flex;
flex-direction: column;
overflow-x: clip;
overflow-x: hidden;
background-color: var(--tg-theme-bg-color);
}
body:has(section#window) {
overflow-y: hidden;
}
aside {}
header {
container-type: inline-size;
container-name: header;
margin: 2rem 0 1rem;
margin-top: 2rem;
padding: 0 var(--offset-x);
display: flex;
flex-direction: column;
@ -54,6 +61,7 @@ header {
main {
container-type: inline-size;
container-name: main;
flex-grow: 1;
padding: 0 var(--offset-x);
display: flex;
flex-direction: column;
@ -62,10 +70,6 @@ main {
transition: 0s;
}
main>section:last-child {
margin-bottom: 5rem;
}
main>*[data-section] {
width: var(--width);
}
@ -74,6 +78,24 @@ main>section[data-section]>p {
margin: 0;
}
main>article {
padding: 0 1rem;
}
main>article>:is(h2, h2 + small) {
text-align: center;
display: block;
}
main>article>h2+small {
margin-bottom: 1.8rem;
color: var(--tg-theme-hint-color);
}
main>article>h3 {
margin-top: 2rem;
}
main>search {
--gap: 16px;
--border-width: 1px;
@ -88,6 +110,51 @@ main>search {
footer {}
footer>section#govno {
align-items: center;
padding: 1rem;
gap: 0.2rem;
font-size: small;
}
footer>section#govno>p {
margin: unset;
display: inline-flex;
flex-flow: row wrap;
justify-content: center;
gap: 0 0.4rem;
}
footer>section#govno>span#company {
display: block;
text-align: center;
}
footer>section#govno>p>span#identifier {
color: var(--tg-theme-hint-color);
}
footer>section#govno>p>span#tax {
color: var(--tg-theme-hint-color);
}
footer>section#govno>p>span#identifier:before {
content: var(--company-identifier) ':';
margin-right: 0.1rem;
color: var(--tg-theme-text-color);
}
footer>section#govno>p>span#tax:before {
content: var(--company-tax) ':';
margin-right: 0.1rem;
color: var(--tg-theme-text-color);
}
footer>section#govno>a:last-child {
margin-top: 0.4rem;
text-align: center;
}
search:has(input:is(:focus, :active)) {
border-color: var(--tg-theme-accent-text-color);
transition: unset;
@ -180,6 +247,16 @@ input {
margin-left: var(--currency-offset, 0.1rem);
}
.cost.plus:before {
content: '+';
margin-left: var(--plus-offset, 0.1rem);
}
.delivery.days:after {
content: var(--days);
margin-left: var(--days-offset, 0.1rem);
}
.rounded {
border-radius: 0.75rem;
}
@ -194,6 +271,19 @@ input {
flex-direction: column;
}
.success {
/* color: var(--tg-theme-hint-color, green); */
color: #697d30;
}
.fail {
color: var(--tg-theme-destructive_text_color, red);
}
.hint {
color: var(--tg-theme-hint-color);
}
.unselectable {
-webkit-touch-callout: none;
-webkit-user-select: none;

View File

@ -8,7 +8,18 @@ section#window {
display: flex;
justify-content: center;
align-items: center;
backdrop-filter: brightness(50%) contrast(120%) grayscale(60%) blur(1.2px);
/* backdrop-filter: brightness(40%) contrast(120%) grayscale(60%) blur(1.2px); */
}
section#window:before {
content: '';
z-index: -100;
position: absolute;
width: 100vw;
height: 100vh;
opacity: 0.7;
background: #000;
filter: brightness(50%);
}
section#window>div.card {
@ -60,8 +71,10 @@ section#window>div.card>h3>a.exit[type="button"] {
section#window>div.card>div.images {
--image-width: 10rem;
height: 10rem;
min-height: 10rem;
max-height: 10rem;
display: flex;
overflow: clip;
overflow: hidden;
cursor: zoom-in;
transition: 0s;
}
@ -70,35 +83,43 @@ section#window>div.card>div.images.extend {
--image-width: max(30rem, 40vw);
z-index: 9999999;
left: 0;
top: 0;
top: 10vh;
margin: unset !important;
position: absolute;
width: 100vw;
max-width: unset;
height: 100vh;
height: 80vh;
min-height: 80vh;
max-height: 80vh;
align-items: center;
cursor: normal;
cursor: default;
transition: 0s;
}
section#window>div.card:has(>div.images.extend)>:not(div.images.extend) {
filter: brightness(50%);
}
section#window>div.card>div.images>img {
margin-right: 0.5rem;
width: var(--image-width);
max-width: var(--image-width);
height: 100%;
width: auto;
height: 10rem;
flex-shrink: 0;
flex-grow: 0;
object-fit: cover;
object-fit: contain;
image-rendering: auto;
border-radius: 0.5rem;
transition: 0s;
}
section#window>div.card>div.images.extend>img {
margin-right: 2vw;
height: max-content;
object-fit: unset;
border-radius: unset;
margin-right: 3vw;
width: auto;
height: 100%;
object-fit: contain;
border-radius: 0.75rem;
}
section#window>div.card>div.images>img:last-child {

View File

@ -1,3 +1,4 @@
*
!deliveries/
!.gitignore
!*.sample

View File

@ -0,0 +1,6 @@
<?php
return [
'account' => '',
'secret' => ''
];

0
mirzaev/arming_bot/system/storage/categories/.gitignore vendored Normal file → Executable file
View File

0
mirzaev/arming_bot/system/storage/import/.gitignore vendored Normal file → Executable file
View File

0
mirzaev/arming_bot/system/storage/products/.gitignore vendored Normal file → Executable file
View File

View File

@ -23,15 +23,22 @@ use Twig\Loader\FilesystemLoader,
// Built-in libraries
use ArrayAccess;
use ArrayAccess as array_access,
Error as error;
/**
* Templater core
* Templater
*
* @package mirzaev\arming_bot\views
* @author ${REPO_OWNER} < mail >
*
* @param
*
* @method
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class templater extends controller implements ArrayAccess
final class templater extends controller implements array_access
{
/**
* Registry of global variables of view
@ -73,9 +80,12 @@ final class templater extends controller implements ArrayAccess
$this->twig->addGlobal('language', $language = $account?->language ?? $session?->buffer['language'] ?? $settings?->language ?? language::en);
$this->twig->addGlobal('currency', $currency = $account?->currency ?? $session?->buffer['currency'] ?? $settings?->currency ?? currency::usd);
// FUCKING SAFARI
/* $this->twig->addGlobal('safari', str_contains(strtolower($_SERVER['HTTP_USER_AGENT']), 'safari')); */
// @todo move functions into controllers
// Initialize function of dimensions formatting
// Initializing function of dimensions formatting
$this->twig->addFunction(
new TwigFunction(
'format_dimensions',
@ -105,6 +115,57 @@ final class templater extends controller implements ArrayAccess
)
);
// Initializing filter for formatting string to numbers only
$this->twig->addFilter(
new TwigFilter(
'numbers_only',
function (string|null $string = null) {
// Universalizing SIM-number
preg_match_all('/\d/', $string, $numbers);
$universalized = implode($numbers[0]);
// Deinitializing unnecessary variables(
unset($numbers);
// Exit (success)
return $universalized;
}
)
);
// Initializing filter for formatting SIM-numbers string to E.164
$this->twig->addFilter(
new TwigFilter(
'sim_format',
function (string|int|float|null $sim = null) {
// Universalizing SIM-number
preg_match_all('/\d/', $sim, $numbers);
$universalized = implode($numbers[0]);
// Deinitializing unnecessary variables(
unset($numbers);
// Parsing parts of SIM-number
preg_match('/^(\d)(\d{3})(\d{1,11})$/', $universalized, $parsed);
unset($parsed[0]);
// Deinitializing unnecessary variables(
unset($universalized);
try {
// Generating formatted SIM-number by E.164
$formatted = sprintf('+%u %s %s', ...$parsed);
// Exit (success)
return $formatted;
} catch (error $e) {
// Exit (fail)
return $sim;
}
}
)
);
// Initializing of twig extensions
$this->twig->addExtension(new intl());
}

View File

@ -5,9 +5,9 @@
{% block body %}
{% if account is empty %}
<section id="account">
<a onclick="core.account.authentication()">
<!-- <a onclick="core.account.authentication()">
{{ language == 'ru' ? "Аутентификация" : "Authentication" }}
</a>
</a> -->
</section>
{% else %}
<section id="account">
@ -19,5 +19,5 @@
{% endblock %}
{% block js %}
<script src="/js/modules/account.js" type="module"></script>
<script src="/js/modules/account.mjs" type="module"></script>
{% endblock %}

View File

@ -3,22 +3,22 @@
<div class="column">
<div id="deliveries" class="row">
{% for identifier, delivery in deliveries %}
<input id="{{ identifier }}" name="delivery" type="radio" {% if identifier==session.buffer.delivery.company %}
<input id="{{ identifier }}" name="delivery" type="radio" {% if identifier == (account.buffer.delivery.company ?? session.buffer.delivery.company) %}
checked{% endif %}>
<label class="rounded" for="{{ identifier }}" type="button"
title="{{ (language.name == 'ru' ? 'Выбрать ' : 'Choose ') ~ delivery.label }}"
onclick="core.buffer.write.damper('delivery_company', '{{ identifier }}').then(() => core.delivery.check())" tabindex="5">
onclick="core.delivery.company(this.previousElementSibling)" tabindex="5">
{{ delivery.label }}
</label>
{% endfor %}
</div>
<div id="address" class="row">
<input id="city" placeholder="{{ language.name == 'ru' ? 'Город' : 'City' }}"
value="{{ session.buffer.delivery.city ?? account.buffer.delivery.city }}"
onkeydown="core.buffer.write.damper('delivery_city', this.value).then(() => core.delivery.check())" tabindex="6" />
<input id="street" placeholder="{{ language.name == 'ru' ? 'Улица' : 'Street' }}"
value="{{ session.buffer.delivery.street ?? session.buffer.delivery.street }}"
onkeydown="core.buffer.write.damper('delivery_street', this.value).then(() => core.delivery.check())" tabindex="6" />
<input id="location" class="identifier{% if session.buffer.delivery[account.buffer.delivery.company ?? session.buffer.delivery.company].location.identifier %} success{% endif %}" placeholder="{{ language.name == 'ru' ? 'Город' : 'location' }}"
value="{{ session.buffer.delivery[account.buffer.delivery.company ?? session.buffer.delivery.company] ? session.buffer.delivery[account.buffer.delivery.company ?? session.buffer.delivery.company].location.name ~ ', ' ~ (session.buffer.delivery[account.buffer.delivery.company ?? session.buffer.delivery.company].location.structure|reverse|join(', ')) : (session.buffer.delivery.location ?? account.buffer.delivery.location) }}"
onkeyup="core.delivery.location(this)" oninput="core.delivery.location(this)" tabindex="6" />
<input id="street"{% if session.buffer.delivery[account.buffer.delivery.company ?? session.buffer.delivery.company].street %} class="success"{% endif %} placeholder="{{ language.name == 'ru' ? 'Улица' : 'Street' }}"
value="{{ session.buffer.delivery[account.buffer.delivery.company ?? session.buffer.delivery.company] ? session.buffer.delivery[account.buffer.delivery.company ?? session.buffer.delivery.company].street : (session.buffer.delivery.street ?? session.buffer.delivery.street) }}"
onkeyup="core.delivery.street(this)" oninput="core.delivery.location(this)" tabindex="6" />
</div>
</div>
</section>

View File

@ -1,16 +1,16 @@
{% macro card(product, amount) %}
<article id="{{ product._id }}" class="product" data-product-identifier="{{ product.identifier }}"
<article id="product_{{ product.identifier ?? 'FAIL' }}" class="product" data-product-identifier="{{ product.identifier }}"
data-product-amount="{{ amount }}" {% if amount> 0 %} style="--hue-rotate-offset: {{ amount }}0deg;"{% endif %}>
<a data-product="cover" href="?product={{ product.identifier }}" onclick="return core.catalog.product(this);"
<a data-product="cover" href="?product={{ product.identifier }}" onclick="return !core.catalog.product(this);"
onkeydown="event.keyCode === 13 && core.catalog.product(this)" tabindex="10">
<img src="{{ product.images.0.storage }}" alt="{{ product.name }}" ondrugstart="return false;">
<img src="{{ product.images.0.storage.200 }}" alt="{{ product.name }}" ondragstart="return false;">
</a>
<div>
<div class="head" title="{{ product.name }}">
{{ product.name | length > 65 ? product.name | slice(0, 65) ~ '...' : product.name }}
<!-- <button data-product-button="list" onclick="core.cart.list(this, document.getElementById('{{ product._id }}'))" title="{{ language.name == 'ru' ? 'Добавить в список' : 'Add to a list' }}"><i class="icon small list add"></i></button> -->
<!-- <button data-product-button="list" onclick="core.cart.list(this, document.getElementById('product_{{ product.identifier ?? 'FAIL' }}'))" title="{{ language.name == 'ru' ? 'Добавить в список' : 'Add to a list' }}"><i class="icon small list add"></i></button> -->
<button data-product-button="toggle"
onclick="core.cart.toggle(this, document.getElementById('{{ product._id }}'), true)"
onclick="core.cart.toggle(this, document.getElementById('product_{{ product.identifier ?? 'FAIL' }}'), true)"
title="{{ language.name == 'ru' ? 'Удалить' : 'Delete' }}"><i class="icon small trash"></i></button>
</div>
<div class="body">
@ -26,14 +26,14 @@
<div class="footer">
<span class="cost currency" data-product-parameter="cost">{{ product.cost }}</span>
<button data-product-button="delete"
onclick="core.cart.delete(this, document.getElementById('{{ product._id }}'), 1)"
onclick="core.cart.delete(this, document.getElementById('product_{{ product.identifier ?? 'FAIL' }}'), 1)"
title="{{ language.name == 'ru' ? 'Уменьшить' : 'Decrease' }}"><i class="icon small minus"></i></button>
<input type="text" value="{{ amount ?? 1 }}" title="{{ language.name == 'ru' ? 'Количество' : 'Amount' }}"
data-product-parameter="amount"
onchange="core.cart.set(this, document.getElementById('{{ product._id }}'), +this.value)"
onchange="core.cart.set(this, document.getElementById('product_{{ product.identifier ?? 'FAIL' }}'), +this.value)"
oninput="this.value = (this.value = +this.value.replaceAll(/[^\d]/g, '')) > 100 ? 100 : (this.value < 0 ? 0 : this.value)"></input>
<button data-product-button="write"
onclick="core.cart.write(this, document.getElementById('{{ product._id }}'), 1)"
onclick="core.cart.write(this, document.getElementById('product_{{ product.identifier ?? 'FAIL' }}'), 1)"
title="{{ language.name == 'ru' ? 'Увеличить' : 'Increase' }}"><i class="icon small plus"></i></button>
</div>
</div>

View File

@ -1,13 +1,19 @@
{% if cart.products is not empty %}
<section id="summary"
class="column {% if delivery is empty or delivery.city is empty or delivery.street is empty %}disabled{% endif %} unselectable"
{% if delivery is empty %}title="{{ language.name == " ru" ? "Выберите тип доставки" : "Choose delivery type" }}"{%
endif %}>
class="column unselectable">
<div class="row">
<span id="amount">{{ cart.summary.amount ?? 0 }}</span>
<span>{{ language.name == "ru" ? "товаров на сумму" : "products worth" }}</span>
<span id="cost" class="cost currency">{{ cart.summary.cost ?? 0 }}</span>
<button id="order" onclick="core.cart.order(this)" {% if delivery is empty %} disabled="true" {% endif %}
{% 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>
{% endif %}
{% if session.buffer.delivery[account.buffer.delivery.company ?? session.buffer.delivery.company].days %}
<span id="shipping" class="delivery days hint">{{ session.buffer.delivery[account.buffer.delivery.company ?? session.buffer.delivery.company].days }}</span>
{% endif %}
<button id="order" onclick="core.cart.share(this)" {% if session.buffer.delivery[account.buffer.delivery.company ??
session.buffer.delivery.company].location is empty or session.buffer.delivery[account.buffer.delivery.company ??
session.buffer.delivery.company].street is empty %} disabled="true" {% endif %}
title="{{ language.name == 'ru' ? 'Оформить заказ' : 'Place an order' }}"><i class="icon arrow"></i></button>
</div>
</section>

View File

@ -20,7 +20,8 @@
{% block js %}
{{ parent() }}
<script src="/js/modules/delivery.js" type="module"></script>
<script src="/js/modules/cart.js" type="module"></script>
<script type="text/javascript" src="https://auth.robokassa.ru/Merchant/bundle/robokassa_iframe.js"></script>
<script src="/js/modules/delivery.mjs" type="module"></script>
<script src="/js/modules/cart.mjs" type="module"></script>
<script src="/js/modules/hotline.js" type="module"></script>
{% endblock %}

View File

@ -1,12 +1,13 @@
{% if categories is not empty %}
<section id="categories" class="unselectable">
{% for category in categories %}
<a id="{{ category.getId() }}" class="category" type="button" href="?category={{ category.identifier }}"
onclick="core.catalog.parameters.set('category', {{ category.identifier }}); return core.catalog.search(event);"
<a id="category_{{ category.identifier }}" data-category-identifier="{{ category.identifier }}" class="category"
type="button" href="?category={{ category.identifier }}"
onclick="core.catalog.parameters.set('category', {{ category.identifier }}); return !core.catalog.search(event);"
onkeydown="event.keyCode === 13 && (core.catalog.parameters.set('category', {{ category.identifier }}), core.catalog.search(event))"
tabindex="3">
{% if category.images %}
<img src="{{ category.images.0.storage }}" alt="{{ category.name }}" ondrugstart="return false;">
<img src="{{ category.images.0.storage.400 }}" alt="{{ category.name }}" ondragstart="return false;">
{% endif %}
<p>{{ category.name }}</p>
</a>

View File

@ -2,21 +2,22 @@
<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" onclick="core.catalog.parameters.set('brand', null); core.catalog.search(event)">
{{ language.name == 'ru' ? 'Бренд' : 'Brand' }}
</label>
<input name="brand" type="radio" id="brand_all">
<label for="brand_all" type="button" onclick="core.catalog.parameters.set('brand', null); core.catalog.search(event)">
<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>
</label> -->
{% for brand in filters.brands %}
<input name="brand" type="radio" id="brand_{{ loop.index }}" {% if brand == buffer_brand %} checked{% endif %}>
<label for="brand_{{ loop.index }}" type="button"
onclick="core.catalog.parameters.set('brand', '{{ brand }}'); core.catalog.search(event)">
<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,23 +1,24 @@
{% macro card(product) %}
{% set title = product.name ~ ' ' ~ product.brand ~ format_dimensions(product.dimensions.x,
{% macro card(cart, product) %}
{% set title = product.brand ~ format_dimensions(product.dimensions.x,
product.dimensions.y, product.dimensions.z, ' ') ~ ' ' ~ product.weight ~ 'г' %}
{% set amount = cart[product.getId()].amount ?? 0 %}
<article id="{{ product.getId() }}" class="product unselectable" data-product-identifier="{{ product.identifier }}"
{% set amount = cart.products[product.getId()].amount ?? 0 %}
<article id="product_{{ product.identifier ?? 'FAIL' }}" class="product unselectable" data-product-identifier="{{ product.identifier }}"
data-product-amount="{{ amount }}"{% if amount > 0 %} style="--hue-rotate-offset: {{ amount }}0deg;"{% endif %}>
<a data-product="cover" href="?product={{ product.identifier }}" onclick="return core.catalog.product(this);"
<a data-product="cover" href="?product={{ product.identifier }}" onclick="return !core.catalog.product(this);"
onkeydown="event.keyCode === 13 && core.catalog.product(this)" tabindex="10">
<img src="{{ product.images.0.storage }}" alt="{{ product.name }}" ondrugstart="return false;">
<p class="title" title="{{ product.name }}">
{{ title | length > 45 ? title | slice(0, 45) ~ '...' : title }}
<img src="{{ product.images.0.storage.400 }}" alt="{{ product.name }}" ondragstart="return false;">
<p class="title" title="{{ product.name }} {{ title }}">
{{ product.name }}
<span>{{ title | length > 45 ? title | slice(0, 45) ~ '...' : title }}</span>
</p>
</a>
<div data-product="buttons">
<button data-product-button="delete" onclick="core.cart.delete(this, document.getElementById('{{ product.getId() }}'), 1)" title="{{ language.name == 'ru' ? 'Уменьшить' : 'Decrease' }}"><i class="icon small minus"></i></button>
<button data-product-button="toggle" onclick="core.cart.toggle(this, document.getElementById('{{ product.getId() }}'))" tabindex="15">
<button data-product-button="delete" onclick="core.cart.delete(this, document.getElementById('product_{{ product.identifier ?? 'FAIL' }}'), 1)" title="{{ language.name == 'ru' ? 'Уменьшить' : 'Decrease' }}"><i class="icon small minus"></i></button>
<button data-product-button="toggle" onclick="core.cart.toggle(this, document.getElementById('product_{{ product.identifier ?? 'FAIL' }}'))" tabindex="15">
<span data-product-parameter="amount">{{ amount }}</span>
<span class="cost currency" data-product-parameter="cost">{{ product.cost }}</span>
</button>
<button data-product-button="write" onclick="core.cart.write(this, document.getElementById('{{ product.getId() }}'), 1)" title="{{ language.name == 'ru' ? 'Увеличить' : 'Increase' }}"><i class="icon small plus"></i></button>
<button data-product-button="write" onclick="core.cart.write(this, document.getElementById('product_{{ product.identifier ?? 'FAIL' }}'), 1)" title="{{ language.name == 'ru' ? 'Увеличить' : 'Increase' }}"><i class="icon small plus"></i></button>
</div>
</article>
{% endmacro %}
@ -26,14 +27,14 @@ product.dimensions.y, product.dimensions.z, ' ') ~ ' ' ~ product.weight ~ 'г' %
<div class="column">
{% for product in products %}
{% if loop.index % 2 == 1 %}
{{ _self.card(product) }}
{{ _self.card(cart, product) }}
{% endif %}
{% endfor %}
</div>
<div class="column">
{% for product in products %}
{% if loop.index % 2 == 0 %}
{{ _self.card(product) }}
{{ _self.card(cart, product) }}
{% endif %}
{% endfor %}
</div>

View File

@ -2,6 +2,6 @@
<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); 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 %} />
</search>

View File

@ -21,7 +21,7 @@
{% block js %}
{{ parent() }}
<script src="/js/modules/catalog.js" type="module"></script>
<script src="/js/modules/cart.js" type="module"></script>
<script src="/js/hotline.js" type="module"></script>
<script src="/js/modules/catalog.mjs" type="module"></script>
<script src="/js/modules/cart.mjs" type="module"></script>
<script src="/js/modules/hotline.js" type="module"></script>
{% endblock %}

View File

@ -10,5 +10,5 @@
{% endblock %}
{% block js %}
<script src="/js/modules/connection.js" type="module"></script>
<script src="/js/modules/connection.mjs" type="module"></script>
{% endblock %}

View File

@ -1,10 +1,15 @@
{% use "/themes/default/offer.html" with css as offer_css, body as offer_body, js as offer_js %}
{% block css %}
{{ block('offer_css') }}
{% endblock %}
{% block body %}
<footer>
{{ block('offer_body') }}
</footer>
{% endblock %}
{% block js %}
{{ block('offer_js') }}
{% endblock %}

View File

@ -12,14 +12,16 @@
{% block css %}
<link type="text/css" rel="stylesheet" href="/themes/{{ theme }}/css/main.css" />
<link type="text/css" rel="stylesheet" href="/themes/{{ theme }}/css/icons/loading_spinner.css" />
<link type="text/css" rel="stylesheet" href="/themes/{{ theme }}/css/loading.css" />
<link type="text/css" rel="stylesheet" href="/themes/{{ theme }}/css/icons/loading_spinner.css" rel="preload" />
<link type="text/css" rel="stylesheet" href="/themes/{{ theme }}/css/loading.css" rel="preload" />
<link type="text/css" rel="stylesheet" href="/themes/{{ theme }}/css/window.css" />
<link type="text/css" rel="stylesheet" href="/themes/{{ theme }}/css/fonts/dejavu.css" />
<link type="text/css" rel="stylesheet" href="/themes/{{ theme }}/css/fonts/kabrio.css" />
<link type="text/css" rel="stylesheet" href="/themes/{{ theme }}/css/fonts/kabrio.css" rel="preload" />
<style>
:root {
--currency: "{{ currency.symbol ?? '$' }}";
--days: "{{ language.name == 'ru' ? 'дней' : 'days' }}";
--days-short: "{{ language.name == 'ru' ? 'дн' : 'd' }}";
}
</style>
{% endblock %}

View File

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
</head>
<body style="margin: 0;">
<script type="text/javascript" src="https://auth.robokassa.ru/Merchant/bundle/robokassa_iframe.js"></script>
<script>
console.log(Robokassa.StartPayment({
MerchantLogin: 'MIRZAEV',
OutSum: '{{ summary.cost + cart.buffer.delivery.cost }}',
InvId: {{ cart.getKey() }},
Description: '{{ language.name == 'ru' ? 'Оплата заказа' : 'Payment for the order' }}',
Culture: '{{ language.name }}',
Encoding: 'utf-8',
Settings: JSON.stringify ({PaymentMethods:['BankCard','SBP'], Mode:'modal'}),
SignatureValue: '3925b771e47d405cbcbb492daa936824'
}));
</script>
</body>
</html>

View File

@ -15,7 +15,7 @@
{% block body %}
<!-- {{ block('connection_body') }} -->
<!-- {{ block('account_body') }} -->
{{ block('account_body') }}
{{ block('header') }}
<main>
{% block main %}
@ -27,7 +27,7 @@
{% block js %}
{{ parent() }}
{{ block('connection_js') }}
<!-- {{ block('connection_js') }} -->
{{ block('account_js') }}
{{ block('header_js') }}
{{ block('footer_js') }}

View File

@ -1,11 +1,12 @@
{% block js %}
<script src="https://telegram.org/js/telegram-web-app.js"></script>
<script src="/js/modules/damper.js" type="module"></script>
<script src="/js/modules/damper.mjs" type="module"></script>
<script src="/js/core.js"></script>
<script src="/js/modules/loader.js" type="module"></script>
<script src="/js/modules/telegram.js" type="module"></script>
<script src="/js/modules/session.js" type="module"></script>
<script src="/js/modules/loader.mjs" type="module"></script>
<script src="/js/modules/telegram.mjs" type="module"></script>
<script src="/js/modules/session.mjs" type="module"></script>
<script src="/js/authentication.js"></script>
<script src="/js/telegram.js"></script>
{% if javascript is not empty %}
<script>
core.modules.connect("damper").then(() => {

View File

@ -20,7 +20,7 @@
{% endif %}
<span>{{ button.name }}</span>
{% if button.image.storage %}
<img src="{{ button.image.storage }}" alt="{{ button.name }}" ondrugstart="return false;">
<img src="{{ button.image.storage }}" alt="{{ button.name }}" ondragstart="return false;">
{% endif %}
</a>
{% endfor %}

View File

@ -0,0 +1,29 @@
{% block css %}
{% if settings.company %}
<style>
:root {
--company-identifier: "{{ language.name == 'ru' ? 'ОГРН' : 'ID' }}";
--company-tax: "{{ language.name == 'ru' ? 'ИНН' : 'TAX' }}";
}
</style>
{% endif %}
{% endblock %}
{% block body %}
{% if settings.company %}
<section id="govno" class="column">
{% if settings.company.name %}
<span id="company">{{ settings.company.name }}</span>
{% endif %}
{% if settings.company.tax and settings.company.identifier %}
<p class="row"><span id="tax">{{ settings.company.tax }}</span><span id="identifier">{{ settings.company.identifier }}</span></p>
{% endif %}
{% if settings.company.offer %}
<a href="/offer" onclick="return core.loader.load('/offer');">{{ language.name == 'ru' ? 'Публичная оферта' : 'Public offer'}}</a>
{% endif %}
</section>
{% endif %}
{% endblock %}
{% block js %}
{% endblock %}

View File

@ -0,0 +1,368 @@
{% extends "/themes/default/index.html" %}
{% use "/themes/default/header.html" with css as header_css, body as header, js as header_js %}
{% block css %}
{{ parent() }}
<style>
main>article#offer {
padding-bottom: 1rem;
}
main>article#offer ol {
counter-reset: item;
display: flex;
flex-direction: column;
gap: 0.4rem;
}
main>article#offer ul {
list-style-type: disc;
}
main>article#offer>ol {
margin: unset;
padding: unset;
gap: 1.2rem;
}
main>article#offer>ol>li ol {
padding: 0 0 0 1rem;
}
main>article#offer>ol>li ul {
padding: 0 0 0 1.5rem;
}
main>article#offer>ol>li:before {
content: counters(item, ".") ".";
counter-increment: item;
margin-right: unset;
float: left;
font-size: 1.1rem;
}
main>article#offer>ol>:is(li, section)>h3 {
margin: 0 0 0.6rem;
font-size: 1.1rem;
font-weight: bold;
}
main>article#offer>ol>section>h3:has(+ p) {
margin: 0.6rem 0 1rem;
text-align: center;
}
main>article#offer>ol>section>h3+p {
margin: unset;
}
main>article#offer>ol>li>:is(ol, ul)>li:has(>h4) {
margin-top: 0.2rem;
}
main>article#offer>ol>li>:is(ol, ul)>li>h4 {
margin: 0 0 0.4rem;
font-size: 1rem;
font-weight: normal;
}
main>article#offer ol>li {
display: block;
}
main>article#offer ol>li:before {
content: counters(item, ".") ".";
counter-increment: item;
float: left;
}
main>article#offer :is(ol, ul)>li:before {
margin-right: 0.4rem;
color: var(--tg-theme-text-color);
}
main>article#offer :is(ol, ul)>li::marker {
margin-right: 0.4rem;
color: var(--tg-theme-text-color);
}
main>article#offer :is(ol, ul) {
color: var(--tg-theme-hint-color);
}
main>article#offer :is(ol, ul)>li {
color: var(--tg-theme-hint-color);
}
main>article#offer ol>span.separator {
margin-top: 0.4rem;
font-weight: bold;
}
</style>
{% endblock %}
{% block body %}
{{ block('header') }}
<main>
<article id="offer">
<h2>ПУБЛИЧНАЯ ОФЕРТА</h2>
<small>о заключении договора купли-продажи</small>
<ol>
<li>
<h3>Общие положения</h3>
<ol>
В настоящей Публичной оферте содержатся условия заключения Договора купли-продажи (далее по тексту - «Договор
купли-продажи» и/или «Договор»). Настоящей офертой признается предложение, адресованное одному или нескольким
конкретным лицам, которое достаточно определенно и выражает намерение лица, сделавшего предложение, считать
себя
заключившим Договор с адресатом, которым будет принято предложение. Совершение указанных в настоящей Оферте
действий
является подтверждением согласия обеих Сторон заключить договор купли-продажи на условиях, в порядке и объеме,
изложенных в настоящей Оферте. Нижеизложенный текст Публичной оферты является официальным публичным
предложением
Продавца, адресованный заинтересованному кругу лиц заключить Договор купли-продажи в соответствии с
положениями
пункта 2 статьи 437 Гражданского кодекса РФ. Договор купли-продажи считается заключенным и приобретает силу с
момента совершения Сторонами действий, предусмотренных в настоящей Оферте, и, означающих безоговорочное, а
также
полное принятие всех условий настоящей оферты без каких-либо изъятий или ограничений на условиях
присоединения.
<span class="separator">Термины и определения:</span>
Договор текст настоящей Оферты с Приложениями, являющимися неотъемлемой частью настоящей Оферты,
акцептованный
Покупателем путем совершения конклюдентных действий, предусмотренных настоящей Офертой. Конклюдентные действия
-
это поведение, которое выражает согласие с предложением контрагента заключить, изменить или расторгнуть
договор.
Действия состоят в полном или частичном выполнении условий, которые предложил контрагент. Сайт Продавца в сети
«Интернет» совокупность программ для электронных вычислительных машин и иной информации, содержащейся в
информационной системе, доступ к которой обеспечивается посредством сети «Интернет» по доменному имени и
сетевому адресу: __________ Стороны Договора (Стороны) Продавец и Покупатель. Товар - товаром по договору
купли-продажи
могут быть любые вещи с соблюдением правил, предусмотренных статьей 29 Гражданского кодекса РФ.
</ol>
</li>
<li>
<h3>Предмет Договора</h3>
<ol>
<li>По настоящему Договору Продавец обязуется передать вещь (Товар) в собственность Покупателя, а Покупатель
обязуется принять Товар и уплатить за него определенную денежную сумму.</li>
<li>Наименование, количество, а также ассортимент Товара, его стоимость, порядок доставки и иные условия
определяются на основании сведений Продавца при оформлении заявки Покупателем, либо устанавливаются на сайте
Продавца в сети «Интернет»______</li>
<li>
<h4>Подтверждение настоящей Оферты выражается в совершении конклюдентных действий, в частности:</h4>
<ul>
<li>действиях, связанных с регистрацией учетной записи на Сайте Продавца в сети «Интернет» при наличии
необходимости регистрации учетной записи</li>
<li>путем составления и заполнения заявки на оформление заказа Товара</li>
<li>путем сообщения требуемых для заключения Договора сведений по телефону, электронной почте, указанными
на
сайте Продавца в сети «Интернет», в том числе, при обратном звонке Продавца по заявке Покупателя</li>
<li>оплаты Товара Покупателем. Данный перечень не является исчерпывающим, могут быть и другие действия,
которые ясно выражают намерение лица принять предложение контрагента.</li>
</ul>
</ol>
</li>
<li>
<h3>Права и обязанности Сторон</h3>
<ol>
<li>
<h4>Права и обязанности Продавца</h4>
<ol>
<li>Продавец вправе требовать оплаты Товаров и их доставки в порядке и на условиях, предусмотренных
Договором</li>
<li>Отказать в заключении Договора на основании настоящей Оферты Покупателю в случае его
недобросовестного поведения, в частности, в случае</li>
<ul>
<li>более 2 (Двух) отказов от Товаров надлежащего качества в течение года</li>
<li>предоставления заведомо недостоверной персональной информации</li>
<li>возврата испорченного Покупателем Товара или Товара, бывшего в употреблении</li>
<li>иных случаях недобросовестного поведения, свидетельствующих о заключении Покупателем Договора с
целью
злоупотребления правами, и отсутствии обычной экономической цели Договора - приобретения Товара.</li>
</ul>
<li>Продавец обязуется передать Покупателю Товар надлежащего качества и в надлежащей упаковке</li>
<li>Передать Товар свободным от прав третьих лиц</li>
<li>Организовать доставку Товаров Покупателю</li>
<li>Предоставить Покупателю всю необходимую информацию в соответствии с требованиями действующего
законодательства РФ и настоящей Оферты</li>
</ol>
</li>
<li>
<h4>Права и обязанности Покупателя:</h4>
<ol>
<li>Покупатель вправе требовать передачи Товара в порядке и на условиях, предусмотренным Договором</li>
<li>Требовать предоставления всей необходимой информации в соответствии с требованиями действующего
законодательства РФ и настоящей Оферты</li>
<li>Отказаться от Товара по основаниям, предусмотренным Договором и действующим законодательством
Российской Федерации</li>
<li>Покупатель обязуется предоставить Продавцу достоверную информацию, необходимую для надлежащего
исполнения Договора</li>
<li>Принять и оплатить Товар в соответствии с условиями Договора</li>
<li>Покупатель гарантирует, что все условия Договора ему понятны; Покупатель принимает условия без
оговорок,
а также в полном объеме</li>
</ol>
</li>
</ol>
</li>
<li>
<h3>Цена и порядок расчетов</h3>
<ol>
<li>Стоимость, а также порядок оплаты Товара определяется на основании сведений Продавца при оформлении
заявки Покупателем, либо устанавливаются на сайте Продавца в сети «Интернет»: ____</li>
<li>Все расчеты по Договору производятся в безналичном порядке.</li>
</ol>
</li>
<li>
<h3>Обмен и возврат Товара</h3>
<ol>
<li>Покупатель вправе осуществить возврат (обмен) Продавцу Товара, приобретенный дистанционным способом, за
исключением перечня товаров, не подлежащих обмену и возврату согласно действующему законодательству
Российской Федерации. Условия, сроки и порядок возврата Товара надлежащего и ненадлежащего качества
установлены в соответствии с требованиями Гражданского кодекса РФ, Закона РФ от 07.02.1992 N 2300-1 «О
защите
прав потребителей»; Правил, утвержденных Постановлением Правительства РФ от 31.12.2020 N 2463.</li>
<li>Требование Покупателя об обмене либо о возврате Товара подлежит удовлетворению, если Товар не был в
употреблении, сохранены его потребительские свойства и имеются доказательства приобретения его у Продавца
</li>
</ol>
</li>
<li>
<h3>Конфиденциальность и безопасность</h3>
<ol>
<li>При реализации настоящего Договора Стороны обеспечивают конфиденциальность и безопасность персональных
данных в соответствии с актуальной редакцией ФЗ от 27.07.2006 г. № 152-ФЗ «О персональных данных» и ФЗ от
27.07.2006 г. №149-ФЗ «Об информации, информационных технологиях и о защите информации».</li>
<li>Стороны обязуются сохранять конфиденциальность информации, полученной в ходе исполнения настоящего
Договора, и принять все возможные меры, чтобы предохранить полученную информацию от разглашения.</li>
<li>Под конфиденциальной информацией понимается любая информация, передаваемая Продавцом и Покупателем в
процессе реализации Договора и подлежащая защите, исключения указаны ниже.</li>
<li>Такая информация может содержаться в предоставляемых Продавцом локальных нормативных актах, договорах,
письмах, отчетах, аналитических материалах, результатах исследований, схемах, графиках, спецификациях и
других
документах, оформленных как на бумажных, так и на электронных носителях.</li>
</ol>
</li>
<li>
<h3>Форс-мажор</h3>
<ol>
<li>Стороны освобождаются от ответственности за неисполнение или ненадлежащее исполнение обязательств по
Договору, если надлежащее исполнение оказалось невозможным вследствие непреодолимой силы, то есть
чрезвычайных
и
непредотвратимых при данных условиях обстоятельств, под которыми понимаются: запретные действия властей,
эпидемии, блокада, эмбарго, землетрясения, наводнения, пожары или другие стихийные бедствия.</li>
<li>В случае наступления этих обстоятельств Сторона обязана в течение 30 (Тридцати) рабочих дней уведомить об
этом другую Сторону.</li>
<li>Документ, выданный уполномоченным государственным органом, является достаточным подтверждением наличия
и продолжительности действия непреодолимой силы.</li>
<li>Если обстоятельства непреодолимой силы продолжают действовать более 60 (Шестидесяти) рабочих дней, то
каждая Сторона вправе отказаться от настоящего Договора в одностороннем порядке.</li>
</ol>
</li>
<li>
<h3>Ответственность Сторон</h3>
<ol>
<li>В случае неисполнения и/или ненадлежащего исполнения своих обязательств по Договору, Стороны несут
ответственность в соответствии с условиями настоящей Оферты</li>
<li>Сторона, не исполнившая или ненадлежащим образом исполнившая обязательства по Договору, обязана
возместить другой Стороне причиненные такими нарушениями убытки.</li>
</ol>
</li>
<li>
<h3>Срок действия настоящей Оферты</h3>
<ol>
<li>Оферта вступает в силу с момента размещения на Сайте Продавца и действует до момента её отзыва
Продавцом.</li>
<li>Продавец оставляет за собой право внести изменения в условия Оферты и/или отозвать Оферту в любой момент
по своему усмотрению. Сведения об изменении или отзыве Оферты доводятся до Покупателя по выбору Продавца
посредством размещения на сайте Продавца в сети «Интернет», в Личном кабинете Покупателя, либо путем
направления
соответствующего уведомления на электронный или почтовый адрес, указанный Покупателем при заключении
Договора
или в ходе его исполнения</li>
<li>Договор вступает в силу с момента Акцепта условий настоящей Оферты Покупателем и действует до полного
исполнения Сторонами обязательств по Договору</li>
<li>Изменения, внесенные Продавцом в Договор и опубликованные на сайте в форме актуализированной Оферты,
считаются принятыми Покупателем в полном объеме</li>
</ol>
</li>
<li>
<h3>Дополнительные условия</h3>
<ol>
<li>Договор, его заключение и исполнение регулируется действующим законодательством Российской Федерации.
Все вопросы, не урегулированные настоящей Офертой или урегулированные не полностью, регулируются в
соответствии
с материальным правом Российской Федерации</li>
<li>В случае возникновения спора, который может возникнуть между Сторонами в ходе исполнения ими своих<br />
обязательств по Договору, заключенному на условиях настоящей Оферты, Стороны обязаны урегулировать спор
мирным
путем до начала судебного разбирательства. Судебное разбирательство осуществляется в соответствии с
законодательством Российской Федерации. Споры или разногласия, по которым Стороны не достигли
договоренности,
подлежат разрешению в соответствии с законодательством РФ. Досудебный порядок урегулирования спора является
обязательным</li>
<li>В качестве языка Договора, заключаемого на условиях настоящей Оферты, а также языка, используемого при
любом взаимодействии Сторон (включая ведение переписки, предоставление требований/уведомлений/разъяснений,
предоставление документов и т. д.), Стороны определили русский язык</li>
<li>Все документы, подлежащие предоставлению в соответствии с условиями настоящей Оферты, должны быть
составлены на русском языке либо иметь перевод на русский язык, удостоверенный в установленном порядке</li>
<li>Бездействие одной из Сторон в случае нарушения условий настоящей Оферты не лишает права
заинтересованной Стороны осуществлять защиту своих интересов позднее, а также не означает отказа от своих
прав
в случае совершения одной из Сторон подобных либо сходных нарушений в будущем</li>
<li>Если на Сайте Продавца в сети «Интернет» есть ссылки на другие веб-сайты и материалы третьих лиц, такие
ссылки размещены исключительно в целях информирования, и Продавец не имеет контроля в отношении содержания
таких сайтов или материалов. Продавец не несет ответственность за любые убытки или ущерб, которые могут
возникнуть в результате использования таких ссылок</li>
</ol>
</li>
<section>
<h3>Реквизиты Продавца</h3>
<p class="column" style="gap: 0.4rem;">
{% if settings.company.name %}
<span class="row">
<b>Наименование:</b>
<span style="margin-left: auto; text-align: right;">{{ settings.company.name }}</span>
</span>
{% endif %}
{% if settings.company.tax %}
<span class="row">
<b>ИНН:</b>
<span style="margin-left: auto; text-align: right;">{{ settings.company.tax }}</span>
</span>
{% endif %}
{% if settings.company.identifier %}
<span class="row">
<b>ОГРН/ОГРНИП:</b>
<span style="margin-left: auto; text-align: right;">{{ settings.company.identifier }}</span>
</span>
{% endif %}
{% if settings.company.sim %}
<span class="row">
<b>Телефон:</b>
<span style="margin-left: auto; text-align: right;">
<a href="tel:{{ settings.company.sim|numbers_only }}">{{ settings.company.sim|sim_format }}</a>
</span>
</span>
{% endif %}
{% if settings.company.identifier %}
<span class="row">
<b>Почта:</b>
<span style="margin-left: auto; text-align: right;">
<a href="mailto:{{ settings.company.mail }}">{{ settings.company.mail }}</a>
</span>
</span>
{% endif %}
</p>
</section>
</ol>
</article>
</main>
{% endblock %}
{% block js %}
{{ parent() }}
{% endblock %}

View File

@ -19,6 +19,6 @@
{% block js %}
{{ parent() }}
<script src="/js/modules/cart.js" type="module"></script>
<script src="/js/modules/hotline.js" type="module"></script>
<script src="/js/modules/cart.mjs" type="module"></script>
<script src="/js/module/hotline.js" type="module"></script>
{% endblock %}