cart + interfase + reservation + currencies

This commit is contained in:
Arsen Mirzaev Tatyano-Muradovich 2024-10-22 19:28:49 +03:00
parent 8efce7d6e6
commit 140e40e79a
30 changed files with 1294 additions and 270 deletions

View File

@ -7,35 +7,35 @@ 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/>
**edgeDefinition:** entry<br/>
**fromCollections:** categoy, product<br/>
**toCollections:** category
2. Create a Graph with the specified values
**Name:** sessions<br>
<br>
**edgeDefinition:** connect<br>
**fromCollections:** account<br>
**Name:** sessions<br/>
<br/>
**edgeDefinition:** connect<br/>
**fromCollections:** account<br/>
**toCollections:** session
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>
**type:** search-alias (you can also use "arangosearch")<br/>
**name:** **product**s_search<br/>
**indexes:**
```json
"indexes": [
@ -68,27 +68,39 @@ location ~ \.php$ {
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>
*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>
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"
```json
{
"status": "active"
"status": "active",
"project": {
"name": "NAME_OF_THE_PROJECT"
},
"language": "en",
"currency": "usd"
}
```
### language
Language for system messages if user language could not be determined<br>
<br>
**Value:** en
### 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>
System of suspensions of chat-robot and Web App<br/>
<br/>
Make sure you have a **suspension** collection (can be created automatically)
```json
{

View File

@ -32,22 +32,28 @@ final class cart extends core
];
/**
* Write or delete from the cart
* Product
*
* Write or delete the product in the cart
*
* @param array $parameters Parameters of the request (POST + GET)
*
* @todo
* 1. Add a limit on adding products to the cart based on the number of products in stock
*/
public function write(array $parameters = []): ?string
public function product(array $parameters = []): ?string
{
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// POST request
// The cart contains the product?
$status = false;
// Declaring of the buffer with amount of the product in the cart
$amount = 0;
// Validating @todo add throwing errors
if (!empty($parameters['product']) && preg_match('/[\d]+/', urldecode($parameters['product']), $matches)) $product = (int) $matches[0];
$identifier = null;
if (!empty($parameters['identifier']) && preg_match('/[\d]+/', urldecode($parameters['identifier']), $matches)) $identifier = (int) $matches[0];
if (isset($product)) {
if (isset($identifier)) {
// Received and validated identfier of the product
// Search for the product
@ -55,34 +61,98 @@ final class cart extends core
filter: "d.identifier == @identifier && d.deleted != true && d.hidden != true",
sort: 'd.created DESC',
amount: 1,
parameters: ['identifier' => $product],
parameters: ['identifier' => $identifier],
errors: $this->errors['cart']
);
if ($product instanceof product) {
// Initialized the product
// Initializing of the cart
$cart = $this->session->cart(errors: $this->errors['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;
if ($cart instanceof model) {
if ($this->cart instanceof model) {
// Initialized the cart
if (0 < $amount = $cart->has($product, errors: $this->errors['cart'])) {
// The cart contains the product
// Validating @todo add throwing errors
$type = null;
if (!empty($parameters['type']) && preg_match('/[\w]+/', urldecode($parameters['type']), $matches)) $type = $matches[0];
// temporary
/* $cart->disconnect($product, errors: $this->errors['cart']); */
$cart->cancel($product, errors: $this->errors['cart']);
if (isset($type)) {
// Received and validated type of action with the product
$status = false;
if ($type === 'toggle') {
// Write the product to the cart if is not in the cart and vice versa
if ($amount > 0) {
// The cart has the product
// Deleting the product from the cart
$this->cart->delete(product: $product, amount: $amount, errors: $this->errors['cart']);
// Reinitializing the buffer with amount of the product in the cart
$amount = 0;
} else {
// The cart not contains the product
// The cart has no the product
// temporary
$cart->connect($product, errors: $this->errors['cart']);
// Writing the product to the cart
$this->cart->write(product: $product, amount: 1, errors: $this->errors['cart']);
$status = true;
// Reinitializing the buffer with amount of the product in the cart
$amount = 1;
}
} else {
// Received not the "toggle" command
// Validating @todo add throwing errors
$_amount = null;
if (!empty($parameters['amount']) && preg_match('/[\d]+/', urldecode($parameters['amount']), $matches)) $_amount = (int) $matches[0];
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 (101 > $amount += $_amount) {
// Validated amount to wrting
// Writing the product to the cart
$this->cart->write(product: $product, amount: $_amount, errors: $this->errors['cart']);
}
} else if ($type === 'delete') {
// Decrease amount of the product in the cart
if (-1 < $amount -= $_amount) {
// Validated amount to deleting
// Deleting the product from the cart
$this->cart->delete(product: $product, amount: $_amount, errors: $this->errors['cart']);
}
} else if ($type === 'set') {
// Set amount of the product in the cart
if ($_amount > -1 && $_amount < 101) {
// Validated amount to setting
if ($_amount > $amount) {
// 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']);
} 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']);
}
// Reinitializing the buffer with amount of the product in the cart
$amount = $_amount;
}
}
}
}
}
}
}
@ -99,7 +169,7 @@ final class cart extends core
// Generating the reponse
echo json_encode(
[
'status' => $status ?? false,
'amount' => $amount, // $amount does not store a real value, but is calculated without a repeated request to ArangoDB
'errors' => $this->errors
]
);

View File

@ -53,9 +53,10 @@ final class catalog extends core
filter: "d.identifier == @identifier && d.deleted != true && d.hidden != true",
sort: 'd.created DESC',
amount: 1,
return: '{name: d.name.@language, description: d.description.@language, cost: d.cost, weight: d.weight, dimensions: d.dimensions, brand: d.brand.@language, compatibility: d.compatibility.@language, images: d.images[*].storage}',
parameters: ['identifier' => $product],
return: '{name: d.name.@language, description: d.description.@language, cost: d.cost, weight: d.weight, dimensions: d.dimensions, brand: d.brand.@language, compatibility: d.compatibility.@language, cost: d.cost.@currency, images: d.images[*].storage}',
language: $this->language,
currency: $this->currency,
parameters: ['identifier' => $product],
errors: $this->errors['catalog']
)[0]?->getAll() ?? null;
}
@ -251,6 +252,9 @@ final class catalog extends core
$entries = entry::search(
document: $category,
amount: 50,
categories_merge: 'name: v.name.@language',
/* products_merge: 'DISTINCT MERGE(d, {name: d.name.@language, description: d.description.@language, compatibility: d.compatibility.@language, brand: d.brand.@language, cost: d.cost.@currency})', */
parameters: ['language' => $this->language->name],
errors: $this->errors['catalog']
);
@ -284,7 +288,12 @@ final class catalog extends core
// Not received identifier of the category
// search for root ascendants categories
$this->view->categories = entry::ascendants(descendant: new category, errors: $this->errors['catalog']) ?? null;
$this->view->categories = entry::ascendants(
descendant: new category,
return: 'DISTINCT MERGE(ascendant, { name: ascendant.name.@language})',
parameters: ['language' => $this->language->name],
errors: $this->errors['catalog']
) ?? null;
}
// Search among products in the $category
@ -307,9 +316,10 @@ final class catalog extends core
filter: $_filters,
sort: $_sort,
amount: 50, // @todo pagination
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,
return: 'DISTINCT MERGE(d, {name: d.name.@language, description: d.description.@language, compatibility: d.compatibility.@language})',
errors: $this->errors['catalog']
);
}

View File

@ -10,8 +10,10 @@ use mirzaev\arming_bot\views\templater,
mirzaev\arming_bot\models\account,
mirzaev\arming_bot\models\session,
mirzaev\arming_bot\models\settings,
mirzaev\arming_bot\models\cart,
mirzaev\arming_bot\models\suspension,
mirzaev\arming_bot\models\enumerations\language;
mirzaev\arming_bot\models\enumerations\language,
mirzaev\arming_bot\models\enumerations\currency;
// Framework for PHP
use mirzaev\minimal\controller;
@ -37,20 +39,30 @@ class core extends controller
protected readonly settings $settings;
/**
* Instance of a session
* Instance of the session
*/
protected readonly session $session;
/**
* Instance of an account
* Instance of the account
*/
protected readonly ?account $account;
/**
* Instance of the cart
*/
protected readonly ?cart $cart;
/**
* Language
*/
protected language $language = language::en;
/**
* Currency
*/
protected currency $currency = currency::usd;
/**
* Registry of errors
*/
@ -66,7 +78,9 @@ class core extends controller
*
* @return void
*
* @todo settings account и session не имеют проверок на возврат null
* @todo
* 1. settings account и session не имеют проверок на возврат null
* 2. TRANSIT EVERYTHING TO MIDDLEWARES
*/
public function __construct(bool $initialize = true)
{
@ -118,15 +132,21 @@ class core extends controller
// Initializing of the settings
$this->settings = settings::active();
// Initializing of language
if ($this->account?->language) $this->language = $this->account->language ?? language::en;
else if ($this->settings?->language) $this->language = $this->settings->language ?? language::en;
// Initializing of the language
$this->language = $this->account?->language ?? $this->session?->buffer['language'] ?? $this->settings?->language ?? language::en;
// Initializing of the currency
$this->currency = $this->account?->currency ?? $this->session?->buffer['currency'] ?? $this->settings?->currency ?? currency::usd;
// Initializing of the cart
$this->cart = $this->account?->cart() ?? $this->session?->cart();
// Initializing of preprocessor of views
$this->view = new templater(
session: $this->session,
account: $this->account,
settings: $this->settings
settings: $this->settings,
cart: $this->cart
);
// @todo перенести в middleware

View File

@ -8,10 +8,12 @@ namespace mirzaev\arming_bot\models;
use mirzaev\arming_bot\models\core,
mirzaev\arming_bot\models\traits\status,
mirzaev\arming_bot\models\traits\buffer,
mirzaev\arming_bot\models\traits\cart,
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\language,
mirzaev\arming_bot\models\enumerations\currency;
// Framework for ArangoDB
use mirzaev\arangodb\collection,
@ -36,8 +38,9 @@ use exception;
*/
final class account extends core implements document_interface, collection_interface
{
use status, document_trait, buffer {
use status, document_trait, buffer, cart {
buffer::write as write;
cart::initialize as cart;
}
/**
@ -85,6 +88,7 @@ final class account extends core implements document_interface, collection_inter
// Abstractioning of parameters
if (isset($result->language)) $result->language = language::{$result->language};
if (isset($result->currency)) $result->currency = currency::{$result->currency};
// Writing the instance of account document from ArangoDB to the implement object
$account->__document($result);

View File

@ -6,13 +6,27 @@ 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\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 cart
*
* @uses reservation
* @package mirzaev\arming_bot\models
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
@ -26,4 +40,240 @@ final class cart extends core implements document_interface, collection_interfac
* Name of the collection in ArangoDB
*/
final public const string COLLECTION = 'cart';
/**
* Search for all products
*
* Search for all products in the cart
*
* @param language|null $language Language
* @param currency|null $currency Currency
* @param array &$errors Registry of errors
*
* @return array|null Array with implementing objects of documents from ArangoDB, if found
*/
public function all(
?language $language = language::en,
?currency $currency = currency::usd,
array &$errors = []
): ?array {
try {
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
if (collection::initialize(reservation::COLLECTION, reservation::TYPE, errors: $errors)) {
if (collection::initialize(product::COLLECTION, product::TYPE, errors: $errors)) {
// Initialized collections
// Search for all products in the cart
$result = @collection::execute(
<<<AQL
FOR v IN 1..1 INBOUND @cart GRAPH @graph
FILTER IS_SAME_COLLECTION(@collection, v._id)
COLLECT d = v WITH COUNT INTO amount
RETURN {
[d._id]: {
amount,
product: MERGE(d, {
name: d.name.@language,
description: d.description.@language,
compatibility: d.compatibility.@language,
brand: d.brand.@language,
cost: d.cost.@currency
})
}
}
AQL,
[
'graph' => 'catalog',
'cart' => $this->getId(),
'collection' => product::COLLECTION,
'language' => $language->name,
'currency' => $currency->name
],
flat: true,
errors: $errors
);
/*
* МеНЯ ЭТО РАЗДРАЖАЕТ
*/
$products = [];
foreach ($result ?? [] as $raw) {
foreach ($raw as $key => $value) {
$products[$key] = $value;
}
}
// Exit (success)
return $products;
} 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()
];
}
// Exit (fail)
return null;
}
/**
* Count
*
* Count of the product in the cart
*
* @param product $product The product
* @param int $limit Limit for counting
* @param array &$errors Registry of errors
*
* @return int|null Amount of the product in the cart, if counted
*/
public function count(product $product, int $limit = 100, array &$errors = []): ?int
{
try {
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
if (collection::initialize(reservation::COLLECTION, reservation::TYPE, errors: $errors)) {
if (collection::initialize(product::COLLECTION, product::TYPE, errors: $errors)) {
// Initialized collections
// Search for the products in the cart and count them
return (int) collection::execute(
<<<AQL
FOR v IN 1..1 INBOUND @cart GRAPH @graph
FILTER IS_SAME_COLLECTION(@collection, v._id) && v._id == @product
LIMIT @limit
COLLECT WITH COUNT INTO length
RETURN length
AQL,
[
'graph' => 'catalog',
'cart' => $this->getId(),
'collection' => product::COLLECTION,
'product' => $product->getId(),
'limit' => $limit
],
errors: $errors
);
} 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()
];
}
// Exit (fail)
return null;
}
/*
* Write
*
* Write the product in the cart
*
* @param product $product The product
* @param int $amount Amount of writings
* @param array &$errors Registry of errors
*
* @return void
*/
public function write(product $product, int $amount = 1, 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(product::COLLECTION, product::TYPE, errors: $errors)) {
// Initialized collections
// Writing the product to the cart
collection::execute(
<<<AQL
FOR i IN 1..@amount
INSERT { _from: @product, _to: @cart, created: @created, updated: @created, active: true } INTO @@edge
AQL,
[
'cart' => $this->getId(),
'product' => $product->getId(),
'@edge' => reservation::COLLECTION,
'amount' => $amount,
'created' => time()
],
errors: $errors
);
} 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()
];
}
}
/*
* Delete
*
* Delete the product from the cart
*
* @param product $product The product
* @param int $amount Amount of deletings
* @param array &$errors Registry of errors
*
* @return void
*/
public function delete(product $product, int $amount = 1, 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(product::COLLECTION, product::TYPE, errors: $errors)) {
// Initialized collections
// Deleting the product from the cart
collection::execute(
<<<AQL
FOR v, e IN 1..1 INBOUND @cart GRAPH @graph
FILTER IS_SAME_COLLECTION(@collection, v._id) && v._id == @product
LIMIT @amount
REMOVE e._key IN @@reservation
AQL,
[
'graph' => 'catalog',
'cart' => $this->getId(),
'collection' => product::COLLECTION,
'product' => $product->getId(),
'@reservation' => reservation::COLLECTION,
'amount' => $amount
],
errors: $errors
);
} 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

@ -11,6 +11,7 @@ use mirzaev\arming_bot\models\core,
mirzaev\arming_bot\models\entry,
mirzaev\arming_bot\models\traits\files,
mirzaev\arming_bot\models\enumerations\language,
mirzaev\arming_bot\models\enumerations\currency,
mirzaev\arming_bot\models\traits\yandex\disk as yandex;
// Framework for ArangoDB
@ -77,6 +78,7 @@ final class catalog extends core
int &$products_old = 0,
int &$products_new = 0,
language $language = language::en,
currency $currency = currency::usd,
array &$errors = []
): void {
try {
@ -314,7 +316,7 @@ final class catalog extends core
(int) $row['identifier'],
[$language->name => $row['name']],
[$language->name => $row['description']],
(float) $row['cost'],
[$currency->name => (float) $row['cost']],
(float) $row['weight'],
['x' => $row['x'], 'y' => $row['y'], 'z' => $row['z']],
[$language->name => $row['brand']],

View File

@ -113,8 +113,8 @@ class core extends model
errors: $errors
);
if ($result instanceof _document) {
// Received only 1 document and
if ($amount === 1 && $result instanceof _document) {
// Received only 1 document and @todo rebuild
// Initializing the object
$object = new static;

View File

@ -91,38 +91,47 @@ final class entry extends core implements document_interface, collection_interfa
}
/**
* Find ascendants
* Ascendants
*
* Find ascendants that are not descendants for anyone
* Search for ascendants that are not descendants for anyone
*
* @param category|product $descendant Descendant document
* @param string|null $return Return (AQL)
* @param array $parameters Binded parameters for placeholders ['placeholder' => parameter]
* @param array &$errors Registry of errors
*
* @return array|null Ascendants that are not descendants for anyone, if found
*/
public static function ascendants(
category|product $descendant,
?string $return = 'DISTINCT ascendant',
array $parameters = [],
array &$errors = []
): ?array {
try {
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
// Initialized the collection
if ($ascendants = collection::execute(
// Search for ascendants
if ($result = collection::execute(
sprintf(
<<<'AQL'
FOR d IN @@collection
FOR ascendant IN OUTBOUND d @@edge
RETURN DISTINCT ascendant
RETURN %s
AQL,
empty($return) ? 'DISTINCT ascendant' : $return
),
[
'@collection' => $descendant::COLLECTION,
'@edge' => static::COLLECTION
],
] + $parameters,
errors: $errors
)) {
// Found ascendants
// Exit (success)
return is_array($ascendants) ? $ascendants : [$ascendants];
return is_array($result) ? $result : [$result];
} else return [];
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
} catch (exception $e) {
@ -211,6 +220,9 @@ final class entry extends core implements document_interface, collection_interfa
* @param string|null $sort Expression for sorting (AQL)
* @param int $page Страница
* @param int $amount Количество товаров на странице
* @param string|null $categories_merge Expression with paremeters to return for categories (AQL)
* @param string|null $products_merge Expression with paremeters to return for products (AQL)
* @param array $parameters Binded parameters for placeholders ['placeholder' => parameter]
* @param array &$errors Registry of errors
*
* @return array Массив с найденными вхождениями (может быть пустым)
@ -221,6 +233,9 @@ final class entry extends core implements document_interface, collection_interfa
?string $sort = 'v.position ASC, v.created DESC',
int $page = 1,
int $amount = 100,
?string $categories_merge = null,
?string $products_merge = null,
array $parameters = [],
array &$errors = []
): array {
try {
@ -236,18 +251,21 @@ final class entry extends core implements document_interface, collection_interfa
%s
%s
LIMIT @offset, @amount
LET _type = (FOR v2 IN INBOUND v._id GRAPH @graph RETURN v2)[0] ? "category" : "product"
RETURN MERGE(v, {_type})
RETURN DISTINCT IS_SAME_COLLECTION(@category, v._id) ? MERGE(v, {_type: @category%s}) : MERGE(v, {_type: @product%s})
AQL,
empty($filter) ? '' : "FILTER $filter",
empty($sort) ? '' : "SORT $sort",
empty($categories_merge) ? '' : ", $categories_merge",
empty($products_merge) ? '' : ", $products_merge"
),
[
'category' => category::COLLECTION,
'product' => product::COLLECTION,
'graph' => 'catalog',
'document' => $document->getId(),
'offset' => --$page <= 0 ? $page = 0 : $page * $amount,
'amount' => $amount
],
] + $parameters,
errors: $errors
)) ? $result : [$result];
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);

View File

@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace mirzaev\arming_bot\models\enumerations;
// Files of the project
use mirzaev\arming_bot\models\enumerations\language;
/**
* Types of currencies by ISO 4217 standart
*
* @package mirzaev\arming_bot\models\enumerations
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
enum currency
{
case usd;
case rub;
/**
* Label
*
* Initialize label of the currency
*
* @param language|null $language Language into which to translate
*
* @return string Translated label of the currency
*
* @todo
* 1. More currencies
* 2. Cases???
*/
public function label(?language $language = language::en): string
{
// Exit (success)
return match ($this) {
currency::usd => match ($language) {
language::en => 'Dollar',
language::ru => 'Доллар'
},
currency::rub => match ($language) {
language::en => 'Ruble',
language::ru => 'Рубль'
}
};
}
/**
* Symbol
*
* Initialize symbol of the currency
*
* @return string Symbol of the currency
*
* @todo
* 1. More currencies
*/
public function symbol(): string
{
// Exit (success)
return match ($this) {
currency::usd => '$',
currency::rub => '₽'
};
}
}

View File

@ -5,7 +5,7 @@ declare(strict_types=1);
namespace mirzaev\arming_bot\models\enumerations;
/**
* Types of human languages
* Types of languages by ISO 639-1 standart
*
* @package mirzaev\arming_bot\models\enumerations
*
@ -18,17 +18,19 @@ enum language
case ru;
/**
* Translate label of language
* Label
*
* Initialize label of the language
*
* @param language|null $language Language into which to translate
*
* @return string Translated label of language
* @return string Translated label of the language
*
* @todo
* 1. More languages
* 2. Cases???
*/
public function translate(?language $language = language::en): string
public function label(?language $language = language::en): string
{
// Exit (success)
return match ($this) {

View File

@ -6,10 +6,11 @@ namespace mirzaev\arming_bot\models;
// Files of the project
use mirzaev\arming_bot\models\core,
mirzaev\arming_bot\models\enumerations\language,
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\interfaces\collection as collection_interface,
mirzaev\arming_bot\models\enumerations\language,
mirzaev\arming_bot\models\enumerations\currency;
// Framework for ArangoDB
use mirzaev\arangodb\collection,
@ -63,7 +64,7 @@ final class product extends core implements document_interface, collection_inter
int $identifier,
array $name = [['en' => 'ERROR']],
?array $description = [['en' => 'ERROR']],
float $cost = 0,
array $cost = [['usd' => 0]],
float $weight = 0,
array $dimensions = ['x' => 0, 'y' => 0, 'z' => 0],
?array $brand = [['en' => 'ERROR']],
@ -123,11 +124,14 @@ final class product extends core implements document_interface, collection_inter
* @param int $page Page
* @param int $amount Amount per page
* @param string|null $return Return (AQL)
* @param language|null $language Language code (en, ru...)
* @param language|null $language Language
* @param currency|null $currency Currency
* @param array $parameters Binded parameters for placeholders ['placeholder' => parameter]
* @param array &$errors Registry of errors
*
* @return array|static Found products or instance of the product from ArangoDB (can be empty)
*
* @todo убрать language и currency
*/
public static function read(
?string $search = null,
@ -137,6 +141,7 @@ final class product extends core implements document_interface, collection_inter
int $amount = 100,
?string $return = 'DISTINCT d',
?language $language = null,
?currency $currency = null,
array $parameters = [],
array &$errors = []
): array|static {
@ -147,6 +152,9 @@ final class product extends core implements document_interface, collection_inter
// Initializing of the language parameter
if ($language instanceof language) $parameters['language'] = $language->name;
// Initializing of the currency parameter
if ($currency instanceof currency) $parameters['currency'] = $currency->name;
// Initializing parameters for search
if ($search) $parameters += [
'search' => $search,
@ -196,8 +204,8 @@ final class product extends core implements document_interface, collection_inter
errors: $errors
);
if ($result instanceof _document) {
// Found product
if ($amount === 1 && $result instanceof _document) {
// Found product @todo need to rebuild this
// Initializing the object
$product = new static;
@ -214,7 +222,7 @@ final class product extends core implements document_interface, collection_inter
}
// Exit (success)
return $result ?? [];
return is_array($result) ? $result : [$result];
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
} catch (exception $e) {
// Writing to the registry of errors

View File

@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace mirzaev\arming_bot\models;
// Files of the project
use mirzaev\arming_bot\models\core,
mirzaev\arming_bot\models\cart,
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;
// Framework for ArangoDB
use mirzaev\arangodb\enumerations\collection\type;
/**
* Model of reservtion
*
* @used-by cart
* @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 reservation extends core implements document_interface, collection_interface
{
use document_trait;
/**
* Name of the collection in ArangoDB
*/
final public const string COLLECTION = 'reservation';
/**
* Type of the collection in ArangoDB
*/
public const type TYPE = type::edge;
}

View File

@ -7,20 +7,21 @@ namespace mirzaev\arming_bot\models;
// Files of the project
use mirzaev\arming_bot\models\account,
mirzaev\arming_bot\models\connect,
mirzaev\arming_bot\models\cart,
mirzaev\arming_bot\models\enumerations\session as verification,
mirzaev\arming_bot\models\traits\status,
mirzaev\arming_bot\models\traits\buffer,
mirzaev\arming_bot\models\traits\cart,
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\language,
mirzaev\arming_bot\models\enumerations\currency;
// Framework for ArangoDB
use mirzaev\arangodb\collection,
mirzaev\arangodb\document;
// Library для ArangoDB
// Library for ArangoDB
use ArangoDBClient\Document as _document;
// Built-in libraries
@ -36,8 +37,9 @@ use exception;
*/
final class session extends core implements document_interface, collection_interface
{
use status, document_trait, buffer {
use status, document_trait, buffer, cart {
buffer::write as write;
cart::initialize as cart;
}
/**
@ -149,13 +151,15 @@ final class session extends core implements document_interface, collection_inter
// Search for connected account
$result = collection::execute(
<<<AQL
FOR v IN INBOUND @session GRAPH sessions
FILTER IS_SAME_COLLECTION(account, v._id)
FOR v IN INBOUND @session GRAPH @graph
FILTER IS_SAME_COLLECTION(@collection, v._id)
SORT v.created DESC
LIMIT 1
RETURN v
AQL,
[
'graph' => 'users',
'collection' => account::COLLECTION,
'session' => $this->getId()
],
errors: $errors
@ -172,6 +176,7 @@ final class session extends core implements document_interface, collection_inter
// Abstractioning of parameters
if (isset($result->language)) $result->language = language::{$result->language};
if (isset($result->currency)) $result->currency = currency::{$result->currency};
// Writing the instance of account document from ArangoDB to the implement object
$account->__document($result);
@ -197,114 +202,6 @@ final class session extends core implements document_interface, collection_inter
return null;
}
/**
* Search for a connected cart
*
* @param array &$errors Registry of errors
*
* @return cart|null An object implements the instance of the cart document from ArangoDB, if found
*/
public function cart(array &$errors = []): ?cart
{
try {
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
if (collection::initialize(connect::COLLECTION, connect::TYPE, errors: $errors)) {
if (collection::initialize(cart::COLLECTION, cart::TYPE, errors: $errors)) {
// Initialized collections
// Search for connected cart
$result = collection::execute(
<<<AQL
FOR v IN INBOUND @session GRAPH sessions
FILTER IS_SAME_COLLECTION(cart, v._id) && v.active == true && v.ordered != true
SORT v.updated DESC, v.created DESC
LIMIT 1
RETURN v
AQL,
[
'session' => $this->getId()
],
errors: $errors
);
if ($result instanceof _document) {
// Found the cart
// Initializing the object
$cart = new cart;
if (method_exists($cart, '__document')) {
// Object can implement a document from ArangoDB
// Writing the instance of cart document from ArangoDB to the implement object
$cart->__document($result);
// Exit (success)
return $cart;
}
} else {
// Not found the cart
// Initializing a new cart and write they into ArangoDB
$_id = document::write(
cart::COLLECTION,
[
'active' => true,
]
);
if ($result = collection::execute(
<<<'AQL'
FOR d IN @@collection
FILTER d._id == @_id && d.active == true
RETURN d
AQL,
[
'@collection' => cart::COLLECTION,
'_id' => $_id
],
errors: $errors
)) {
// Found the instance of just created new cart
// Initializing the object
$cart = new cart;
if (method_exists($cart, '__document')) {
// Object can implement a document from ArangoDB
// Writing the instance of cart document from ArangoDB to the implement object
$cart->__document($result);
// Connecting the cart to the session
$connected = $this->connect($cart, $errors);
if ($connected) {
// The cart has been connected to the session
// Exit (success)
return $cart;
} else throw new exception('Failed to connect the cart to the session');
} else throw new exception('Class ' . cart::class . ' does not implement a document from ArangoDB');
} else throw new exception('Failed to create or find just created session');
}
} else throw new exception('Failed to initialize ' . cart::TYPE . ' collection: ' . cart::COLLECTION);
} else throw new exception('Failed to initialize ' . connect::TYPE . ' collection: ' . connect::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()
];
}
// Exit (fail)
return null;
}
/**
* Search by hash
*

View File

@ -9,7 +9,8 @@ use mirzaev\arming_bot\models\core,
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\language,
mirzaev\arming_bot\models\enumerations\currency;
// Framework for ArangoDB
use mirzaev\arangodb\collection,
@ -78,6 +79,7 @@ final class settings extends core implements document_interface, collection_inte
// Abstractioning of parameters
if (isset($result->language)) $result->language = language::{$result->language};
if (isset($result->currency)) $result->currency = currency::{$result->currency};
// Writing the instance of settings document from ArangoDB to the implement object
$settings->__document($result);

View File

@ -18,13 +18,14 @@ use mirzaev\arangodb\collection,
use exception;
/**
* Trait for buffer
* Buffer
*
* @uses collection_interface
* Storage of data in the document from ArangoDB
*
* @param static COLLECTION Name of the collection in ArangoDB
* @param static TYPE Type of the collection in ArangoDB
*
* @uses collection_interface
* @package mirzaev\arming_bot\models\traits
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
@ -33,12 +34,12 @@ use exception;
trait buffer
{
/**
* Write to buffer of the session
* Write to buffer of the document
*
* @param array $data Data for writing (merge)
* @param array &$errors Registry of errors
*
* @return bool Is data has written into the session document from ArangoDB?
* @return bool Is data has written into the document from ArangoDB?
*/
public function write(array $data, array &$errors = []): bool
{
@ -46,10 +47,10 @@ trait buffer
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
// Initialized the collection
// The instance of the session document from ArangoDB is initialized?
// The instance of the document from ArangoDB is initialized?
isset($this->document) || throw new exception('The instance of the sessoin document from ArangoDB is not initialized');
// Writing data into buffer of the instance of the session document from ArangoDB
// Writing data into buffer of the instance of the document from ArangoDB
$this->document->buffer = array_replace_recursive($this->document->buffer ?? [], $data);
// Writing to ArangoDB and exit (success)

View File

@ -0,0 +1,151 @@
<?php
declare(strict_types=1);
namespace mirzaev\arming_bot\models\traits;
// Files of the project
use mirzaev\arming_bot\models\interfaces\collection as collection_interface,
mirzaev\arming_bot\models\interfaces\document as document_interface,
mirzaev\arming_bot\models\traits\document as document_trait,
mirzaev\arming_bot\models\connect,
mirzaev\arming_bot\models\cart as model;
// Library для ArangoDB
use ArangoDBClient\Document as _document;
// Framework for ArangoDB
use mirzaev\arangodb\collection,
mirzaev\arangodb\document;
// Built-in libraries
use exception;
/**
* Cart
*
* Cart for a document from ArangoDB
*
* @uses collection_interface
* @uses document_interface
* @package mirzaev\arming_bot\models\traits
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
trait cart
{
use document_trait;
/**
* Cart
*
* Search for a connected cart
*
* @param array &$errors Registry of errors
*
* @return model|null An object implements the instance of the cart document from ArangoDB, if found
*/
public function initialize(array &$errors = []): ?model
{
try {
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
if (collection::initialize(connect::COLLECTION, connect::TYPE, errors: $errors)) {
if (collection::initialize(model::COLLECTION, model::TYPE, errors: $errors)) {
// Initialized collections
// Search for connected cart
$result = collection::execute(
<<<AQL
FOR v IN 1..1 INBOUND @document GRAPH @graph
FILTER IS_SAME_COLLECTION(@cart, v._id) && v.active == true && v.ordered != true
SORT v.updated DESC, v.created DESC
LIMIT 1
RETURN v
AQL,
[
'cart' => model::COLLECTION,
'graph' => 'users',
'document' => $this->document->getId()
],
errors: $errors
);
if ($result instanceof _document) {
// Found the cart
// Initializing the object
$cart = new model;
if (method_exists($cart, '__document')) {
// Object can implement a document from ArangoDB
// Writing the instance of cart document from ArangoDB to the implement object
$cart->__document($result);
// Exit (success)
return $cart;
}
} else {
// Not found the cart
// Initializing a new cart and write they into ArangoDB
$_id = document::write(
model::COLLECTION,
[
'active' => true,
]
);
if ($result = collection::execute(
<<<'AQL'
FOR d IN @@collection
FILTER d._id == @_id && d.active == true
RETURN d
AQL,
[
'@collection' => model::COLLECTION,
'_id' => $_id
],
errors: $errors
)) {
// Found the instance of just created new cart
// Initializing the object
$cart = new model;
if (method_exists($cart, '__document')) {
// Object can implement a document from ArangoDB
// Writing the instance of cart document from ArangoDB to the implement object
$cart->__document($result);
// Connecting the cart to the document
$connected = $this->connect($cart, $errors);
if ($connected) {
// The cart has been connected to the document
// Exit (success)
return $cart;
} else throw new exception('Failed to connect the ' . model::class . ' to the ' . static::class);
} else throw new exception('Class ' . model::class . ' does not implement a document from ArangoDB');
} else throw new exception('Failed to create or find just created ' . static::class);
}
} else throw new exception('Failed to initialize ' . model::TYPE . ' collection: ' . model::COLLECTION);
} else throw new exception('Failed to initialize ' . connect::TYPE . ' collection: ' . connect::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()
];
}
// Exit (fail)
return null;
}
}

View File

@ -23,10 +23,9 @@ use exception;
/**
* Trait for implementing a document instance from ArangoDB
*
* @uses document_interface
*
* @var protected readonly _document|null $document An instance of the ArangoDB document
*
* @uses document_interface
* @package mirzaev\arming_bot\models\traits
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License

View File

@ -41,7 +41,8 @@ $router
->write('/', 'catalog', 'index', 'GET')
->write('/', 'catalog', 'index', 'POST')
->write('/cart', 'cart', 'index', 'GET')
->write('/cart/write', 'cart', 'write', 'POST')
->write('/cart', 'cart', 'index', 'POST')
->write('/cart/product', 'cart', 'product', 'POST')
->write('/account/write', 'account', 'write', 'POST')
->write('/session/write', 'session', 'write', 'POST')
->write('/session/connect/telegram', 'session', 'telegram', 'POST')

View File

@ -35,7 +35,9 @@ import("/js/core.js").then(() =>
*/
core.cart = class cart {
/**
* Write or delete product from the cart (interface)
* Toggle
*
* Toggle the product in the cart (interface)
*
* @param {HTMLElement} button Button of the product <a>
* @param {bool} force Ignore the damper? (false)
@ -54,7 +56,9 @@ import("/js/core.js").then(() =>
}
/**
* Write or delete product from the cart (damper)
* Toggle
*
* Toggle the product in the cart (damper)
*
* @param {HTMLElement} button Button of the product <a>
* @param {bool} force Ignore the damper? (false)
@ -63,12 +67,14 @@ import("/js/core.js").then(() =>
*/
static toggle_damper = core.damper(
(...variables) => this.toggle_system(...variables),
800,
1,
300,
2,
);
/**
* Write or delete product from the cart (system)
* Toggle
*
* Toggle the product in the cart (system)
*
* @param {HTMLElement} button Button of the product <a>
*
@ -80,15 +86,23 @@ import("/js/core.js").then(() =>
if (button instanceof HTMLElement) {
// Validated
// Initializing of the wrap of buttons
const wrap = button.parentElement;
// Initializing of the product
const product = wrap.parentElement;
// Initializing of identifier of the product
const identifier = button.getAttribute("data-product-identifier");
const identifier = product.getAttribute(
"data-product-identifier",
);
if (typeof identifier === "string" && identifier.length > 0) {
// Validated identifier
return await core.request(
"/cart/write",
`product=${identifier}`,
"/cart/product",
`identifier=${identifier}&type=toggle`,
)
.then((json) => {
if (
@ -103,16 +117,388 @@ import("/js/core.js").then(() =>
// Unblocking the button
button.removeAttribute("disabled");
if (json.status) {
// The product in the cart
// Writing offset of hue-rotate to indicate that the product is in the cart
wrap.style.setProperty(
"--hue-rotate-offset",
json.amount + '0deg',
);
// Writing style of added to the cart button
button.classList.add("cart");
// Writing attribute with amount of the product in the cart
product.setAttribute(
"data-product-amount",
json.amount,
);
// Initializing the amount <span> element
const amount = wrap.querySelector(
'span[data-product-button-text="amount"]',
);
if (amount instanceof HTMLElement) {
// Initialized the amount <span> element
// Writing amount of the product in the cart
amount.innerText = json.amount;
}
}
});
}
}
}
/**
* Write
*
* Write the product in the cart (interface)
*
* @param {HTMLElement} button Button of the product <a>
* @param {number} amount Amount of writings
* @param {bool} force Ignore the damper? (false)
*
* @return {bool} True if an error occurs to continue the event execution
*/
static write(button, amount = 1, force = false) {
// Blocking the button
button.setAttribute("disabled", "true");
// Execute under damper
this.write_damper(button, amount, force);
// Exit (success)
return false;
}
/**
* Write
*
* Write the product in the cart (damper)
*
* @param {HTMLElement} button Button of the product <a>
* @param {number} amount Amount of writings
* @param {bool} force Ignore the damper? (false)
*
* @return {void}
*/
static write_damper = core.damper(
(...variables) => this.write_system(...variables),
300,
3,
);
/**
* Write
*
* Write the product in the cart (system)
*
* @param {HTMLElement} button Button of the product <a>
* @param {number} amount Amount of writings
*
* @return {Promise} Request to the server
*
* @todo add unblocking button by timer + everywhere
*/
static async write_system(button, amount = 1) {
if (
button instanceof HTMLElement &&
typeof amount === "number" &&
amount > -1 &&
amount < 100
) {
// Validated
// Initializing of the wrap of buttons
const wrap = button.parentElement;
// Initializing of the product
const product = wrap.parentElement;
// Initializing of identifier of the product
const identifier = product.getAttribute(
"data-product-identifier",
);
if (typeof identifier === "string" && identifier.length > 0) {
// Validated identifier
return await core.request(
"/cart/product",
`identifier=${identifier}&type=write&amount=${amount}`,
)
.then((json) => {
if (
json.errors !== null &&
typeof json.errors === "object" &&
json.errors.length > 0
) {
// Fail (received errors)
} else {
// The product is not in the cart
// Success (not received errors)
// Deleting style of added to the cart button
button.classList.remove("cart");
// Unblocking the button
button.removeAttribute("disabled");
// Writing offset of hue-rotate to indicate that the product is in the cart
wrap.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 amount = wrap.querySelector(
'span[data-product-button-text="amount"]',
);
if (amount instanceof HTMLElement) {
// Initialized the amount <span> element
// Writing amount of the product in the cart
amount.innerText = json.amount;
}
}
});
}
}
}
/**
* Delete
*
* Delete the product from the cart (interface)
*
* @param {HTMLElement} button Button of the product <a>
* @param {number} amount Amount of deletings
* @param {bool} force Ignore the damper? (false)
*
* @return {bool} True if an error occurs to continue the event execution
*/
static delete(button, amount = 1, force = false) {
// Blocking the button
button.setAttribute("disabled", "true");
// Execute under damper
this.delete_damper(button, amount, force);
// Exit (success)
return false;
}
/**
* Delete
*
* Delete the product from the cart (damper)
*
* @param {HTMLElement} button Button of the product <a>
* @param {number} amount Amount of deletings
* @param {bool} force Ignore the damper? (false)
*
* @return {void}
*/
static delete_damper = core.damper(
(...variables) => this.delete_system(...variables),
300,
3,
);
/**
* Delete
*
* Delete the product from the cart (system)
*
* @param {HTMLElement} button Button of the product <a>
* @param {number} amount Amount of deletings
*
* @return {Promise} Request to the server
*
* @todo add unblocking button by timer + everywhere
*/
static async delete_system(button, amount = 1) {
if (
button instanceof HTMLElement &&
typeof amount === "number" &&
amount > 0 &&
amount < 101
) {
// Validated
// Initializing of the wrap of buttons
const wrap = button.parentElement;
// Initializing of the product
const product = wrap.parentElement;
// Initializing of identifier of the product
const identifier = product.getAttribute(
"data-product-identifier",
);
if (typeof identifier === "string" && identifier.length > 0) {
// Validated identifier
return await core.request(
"/cart/product",
`identifier=${identifier}&type=delete&amount=${amount}`,
)
.then((json) => {
if (
json.errors !== null &&
typeof json.errors === "object" &&
json.errors.length > 0
) {
// Fail (received errors)
} else {
// Success (not received errors)
// Unblocking the button
button.removeAttribute("disabled");
// Writing offset of hue-rotate to indicate that the product is in the cart
wrap.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 amount = wrap.querySelector(
'span[data-product-button-text="amount"]',
);
if (amount instanceof HTMLElement) {
// Initialized the amount <span> element
// Writing amount of the product in the cart
amount.innerText = json.amount;
}
}
});
}
}
}
/**
* Set
*
* Set amount of the product in the cart (interface)
*
* @param {HTMLElement} button Button of the product <a>
* @param {number} amount Amount of the product in the cart to be setted
* @param {bool} force Ignore the damper? (false)
*
* @return {bool} True if an error occurs to continue the event execution
*/
static set(button, amount = 1, force = false) {
// Blocking the button
button.setAttribute("disabled", "true");
// Execute under damper
this.set_damper(button, amount, force);
// Exit (success)
return false;
}
/**
* Set
*
* Set the product in the cart (damper)
*
* @param {HTMLElement} button Button of the product <a>
* @param {number} amount Amount of the product in the cart to be setted
* @param {bool} force Ignore the damper? (false)
*
* @return {void}
*/
static set_damper = core.damper(
(...variables) => this.set_system(...variables),
300,
3,
);
/**
* Set
*
* Set the product in the cart (system)
*
* @param {HTMLElement} button Button of the product <a>
* @param {number} amount Amount of the product in the cart to be setted
*
* @return {Promise} Request to the server
*
* @todo add unblocking button by timer + everywhere
*/
static async set_system(button, amount = 1) {
if (
button instanceof HTMLElement &&
typeof amount === "number" &&
amount > -1 &&
amount < 101
) {
// Validated
// Initializing of the wrap of buttons
const wrap = button.parentElement;
// Initializing of the product
const product = wrap.parentElement;
// Initializing of identifier of the product
const identifier = product.getAttribute(
"data-product-identifier",
);
if (typeof identifier === "string" && identifier.length > 0) {
// Validated identifier
return await core.request(
"/cart/product",
`identifier=${identifier}&type=set&amount=${amount}`,
)
.then((json) => {
if (
json.errors !== null &&
typeof json.errors === "object" &&
json.errors.length > 0
) {
// Fail (received errors)
} else {
// Success (not received errors)
// Unblocking the button
button.removeAttribute("disabled");
// Writing offset of hue-rotate to indicate that the product is in the cart
wrap.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 amount = wrap.querySelector(
'span[data-product-button-text="amount"]',
);
if (amount instanceof HTMLElement) {
// Initialized the amount <span> element
// Writing amount of the product in the cart
amount.innerText = json.amount;
}
}
});

View File

@ -137,17 +137,44 @@ main>section#products>div.column>article.product>a>p.title {
background-color: var(--tg-theme-secondary-bg-color);
}
main>section#products>div.column>article.product>button.cart {
filter: hue-rotate(120deg);
}
main>section#products>div.column>article.product>button:last-of-type {
main>section#products>div.column>article.product>div[data-product="buttons"]:last-of-type {
z-index: 100;
height: 33px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
color: var(--tg-theme-button-text-color);
background-color: var(--tg-theme-button-color);
}
main>section#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 {
container-type: inline-size;
container-name: product-buttons;
}
main>section#products>div.column>article.product>div[data-product="buttons"]>button[data-product-button="toggle"] {
padding: 0;
flex-grow: 1;
}
main>section#products>div.column>article.product:is([data-product-amount="0"], [data-product-amount="1"])>div[data-product="buttons"]>button[data-product-button="toggle"]>span[data-product-button-text="amount"],
main>section#products>div.column>article.product[data-product-amount="0"]>div[data-product="buttons"]>button:is([data-product-button="write"], [data-product-button="delete"]) {
display: none;
}
main>section#products>div.column>article.product>div[data-product="buttons"]>button[data-product-button="toggle"]>span[data-product-button-text="amount"]:after {
content: '*';
margin: 0 0.2rem;
}
main>section#products>div.column>article.product[data-product-amount]:not([data-product-amount="0"])>div[data-product="buttons"] {
filter: hue-rotate(calc(120deg + var(--hue-rotate-offset, 0deg)));
}
@container product-buttons (max-width: 200px) {
main>section#products>div.column>article.product>div[data-product="buttons"]>button[data-product-button="toggle"]>span:is([data-product-button-text="cost"], [data-product-button-text="currency"]) {
display: none;
}
main>section#products>div.column>article.product>div[data-product="buttons"]>button[data-product-button="toggle"]>span[data-product-button-text="amount"]:after {
content: unset;
}
}

View File

@ -0,0 +1,12 @@
@charset "UTF-8";
i.icon.minus {
box-sizing: border-box;
position: relative;
display: block;
width: 16px;
height: 2px;
background: currentColor;
border-radius: 10px;
}

View File

@ -0,0 +1,24 @@
@charset "UTF-8";
i.icon.plus,
i.icon.plus::after {
display: block;
box-sizing: border-box;
background: currentColor;
border-radius: 10px;
}
i.icon.plus {
margin-top: -2px;
position: relative;
width: 16px;
height: 2px;
}
i.icon.plus::after {
content: "";
position: absolute;
width: 2px;
height: 16px;
top: -7px;
left: 7px;
}

View File

@ -129,7 +129,7 @@ search:has(input:disabled) {
backdrop-filter: contrast(0.5);
}
*[type="button"] {
button, *[type="button"] {
cursor: pointer;
}
@ -144,6 +144,8 @@ search:has(input:disabled) {
button {
height: 33px;
color: var(--tg-theme-button-text-color);
background-color: var(--tg-theme-button-color);
}
a[type="button"] {

View File

@ -7,7 +7,10 @@ namespace mirzaev\arming_bot\views;
// Files of the project
use mirzaev\arming_bot\models\session,
mirzaev\arming_bot\models\account,
mirzaev\arming_bot\models\settings;
mirzaev\arming_bot\models\settings,
mirzaev\arming_bot\models\cart,
mirzaev\arming_bot\models\enumerations\language,
mirzaev\arming_bot\models\enumerations\currency;
// Framework for PHP
use mirzaev\minimal\controller;
@ -46,28 +49,35 @@ final class templater extends controller implements ArrayAccess
*
* @param session|null $session The object implementing a session instance from ArangoDB
* @param account|null $account The object implementing an account instance from ArangoDB
* @param settings|null $settings The object implementing an account instance from ArangoDB
* @param settings|null $settings The object implementing a settings instance from ArangoDB
* @param cart|null $cart The object implementing a cart instance from ArangoDB
*
* @return void
*/
public function __construct(
?session $session = null,
?account $account = null,
?settings $settings = null
?settings $settings = null,
?cart $cart = null
) {
// Initializing of an instance of twig
// Initializing an instance of twig
$this->twig = new twig(new FilesystemLoader(VIEWS));
// Initializing of global variables
// Declaring buffers for initializinf global variables
$language = $currency = null;
// Initializing global variables
$this->twig->addGlobal('theme', 'default');
$this->twig->addGlobal('server', $_SERVER);
$this->twig->addGlobal('cookies', $_COOKIE);
$this->twig->addGlobal('settings', $settings);
if (!empty($session?->status())) $this->twig->addGlobal('session', $session);
if (!empty($account?->status())) $this->twig->addGlobal('account', $account);
$this->twig->addGlobal('language', $account?->language->name ?? $settings?->language->name ?? 'en');
$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);
$this->twig->addGlobal('cart', $cart->all(language: $language, currency: $currency));
// Initialize function of dimensions formattinx
// Initialize function of dimensions formatting
$this->twig->addFunction(
new TwigFunction(
'format_dimensions',

View File

@ -6,9 +6,9 @@
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[language] }}" ondrugstart="return false;">
<img src="{{ category.images.0.storage }}" alt="{{ category.name }}" ondrugstart="return false;">
{% endif %}
<p>{{ category.name[language] }}</p>
<p>{{ category.name }}</p>
</a>
{% endfor %}
</section>

View File

@ -4,11 +4,11 @@
{% set buffer_brand = account.buffer.catalog.filters.brand ?? session.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 == 'ru' ? 'Бренд' : 'Brand' }}
{{ 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)">
{{ language == 'ru' ? 'Все бренды' : 'All brands' }}
{{ language.name == 'ru' ? 'Все бренды' : 'All brands' }}
</label>
{% for brand in filters.brands %}
<input name="brand" type="radio" id="brand_{{ loop.index }}" {% if brand == buffer_brand %} checked{% endif %}>

View File

@ -1,19 +1,25 @@
{% macro card(product) %}
{% set title = product.name[language] ~ ' ' ~ product.brand[language] ~ format_dimensions(product.dimensions.x,
{% set title = product.name ~ ' ' ~ product.brand ~ format_dimensions(product.dimensions.x,
product.dimensions.y, product.dimensions.z, ' ') ~ ' ' ~ product.weight ~ 'г' %}
<article id="{{ product.getId() }}" class="product unselectable">
<a data-product-identifier="{{ product.identifier }}" 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[language] }}" ondrugstart="return false;">
<p class="title" title="{{ product.name[language] }}">
{% set amount = cart[product.getId()].amount ?? 0 %}
<article id="{{ product.getId() }}" class="product unselectable" data-product-identifier="{{ product.identifier }}"
data-product-amount="{{ amount }}">
<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 }}
</p>
</a>
<button title="Добавить в корзину" onclick="core.cart.toggle(this)" data-product-identifier="{{ product.identifier }}"
data-product-cost="{{ product.cost }}" tabindex="15">
{{ product.cost }}р
<div data-product="buttons"{% if amount > 0 %} style="--hue-rotate-offset: {{ amount }}0deg;"{% endif %}>
<button data-product-button="delete" onclick="core.cart.delete(this, 1)"><i class="icon minus"></i></button>
<button data-product-button="toggle" onclick="core.cart.toggle(this)" tabindex="15">
<span data-product-button-text="amount">{{ amount }}</span>
<span data-product-button-text="cost">{{ product.cost }}</span>
<span data-product-button-text="currency">{{ currency.symbol }}</span>
</button>
<button data-product-button="write" onclick="core.cart.write(this, 1)"><i class="icon plus"></i></button>
</div>
</article>
{% endmacro %}
{% if products is not empty %}

View File

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

View File

@ -7,6 +7,8 @@
<link type="text/css" rel="stylesheet" href="/themes/{{ theme }}/css/icons/search.css" />
<link type="text/css" rel="stylesheet" href="/themes/{{ theme }}/css/icons/shopping_cart.css" />
<link type="text/css" rel="stylesheet" href="/themes/{{ theme }}/css/icons/close.css" />
<link type="text/css" rel="stylesheet" href="/themes/{{ theme }}/css/icons/plus.css" />
<link type="text/css" rel="stylesheet" href="/themes/{{ theme }}/css/icons/minus.css" />
{% endblock %}
{% block main %}