created cart system
This commit is contained in:
parent
ac7694f716
commit
8efce7d6e6
|
@ -0,0 +1,121 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace mirzaev\arming_bot\controllers;
|
||||
|
||||
// Files of the project
|
||||
use mirzaev\arming_bot\controllers\core,
|
||||
mirzaev\arming_bot\models\product,
|
||||
mirzaev\arming_bot\models\cart as model;
|
||||
|
||||
// Framework for ArangoDB
|
||||
use mirzaev\arangodb\document;
|
||||
|
||||
/**
|
||||
* Controller of cart
|
||||
*
|
||||
* @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 cart extends core
|
||||
{
|
||||
/**
|
||||
* Registry of errors
|
||||
*/
|
||||
protected array $errors = [
|
||||
'session' => [],
|
||||
'account' => [],
|
||||
'cart' => []
|
||||
];
|
||||
|
||||
/**
|
||||
* Write or delete from the cart
|
||||
*
|
||||
* @param array $parameters Parameters of the request (POST + GET)
|
||||
*/
|
||||
public function write(array $parameters = []): ?string
|
||||
{
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
// POST request
|
||||
|
||||
// The cart contains the product?
|
||||
$status = false;
|
||||
|
||||
// Validating @todo add throwing errors
|
||||
if (!empty($parameters['product']) && preg_match('/[\d]+/', urldecode($parameters['product']), $matches)) $product = (int) $matches[0];
|
||||
|
||||
if (isset($product)) {
|
||||
// Received and validated identfier of the product
|
||||
|
||||
// Search for the product
|
||||
$product = product::read(
|
||||
filter: "d.identifier == @identifier && d.deleted != true && d.hidden != true",
|
||||
sort: 'd.created DESC',
|
||||
amount: 1,
|
||||
parameters: ['identifier' => $product],
|
||||
errors: $this->errors['cart']
|
||||
);
|
||||
|
||||
if ($product instanceof product) {
|
||||
// Initialized the product
|
||||
|
||||
// Initializing of the cart
|
||||
$cart = $this->session->cart(errors: $this->errors['cart']);
|
||||
|
||||
if ($cart instanceof model) {
|
||||
// Initialized the cart
|
||||
|
||||
if (0 < $amount = $cart->has($product, errors: $this->errors['cart'])) {
|
||||
// The cart contains the product
|
||||
|
||||
// temporary
|
||||
/* $cart->disconnect($product, errors: $this->errors['cart']); */
|
||||
$cart->cancel($product, errors: $this->errors['cart']);
|
||||
|
||||
$status = false;
|
||||
} else {
|
||||
// The cart not contains the product
|
||||
|
||||
// temporary
|
||||
$cart->connect($product, errors: $this->errors['cart']);
|
||||
|
||||
$status = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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(
|
||||
[
|
||||
'status' => $status ?? false,
|
||||
'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;
|
||||
}
|
||||
}
|
|
@ -42,64 +42,14 @@ final class catalog extends core
|
|||
*/
|
||||
public function index(array $parameters = []): ?string
|
||||
{
|
||||
// Validating
|
||||
if (!empty($parameters['category']) && preg_match('/[\d]+/', $parameters['category'], $matches)) $category = (int) $matches[0];
|
||||
|
||||
if (isset($category)) {
|
||||
// Received and validated identifier of the category
|
||||
|
||||
// Initialize of category
|
||||
$category = category::_read('d.identifier == @identifier', parameters: ['identifier' => $category], errors: $this->errors['catalog']);
|
||||
|
||||
if ($category instanceof category) {
|
||||
// Found the category
|
||||
|
||||
// Write to the response buffer
|
||||
$response['category'] = ['name' => $category->name ?? null];
|
||||
|
||||
// Search for categories that are descendants of $category
|
||||
$entries = entry::search(
|
||||
document: $category,
|
||||
amount: 50,
|
||||
errors: $this->errors['catalog']
|
||||
);
|
||||
/* var_dump($entries); die; */
|
||||
|
||||
// Initialize buffers of entries (in singular, by parameter from ArangoDB)
|
||||
$category = $product = [];
|
||||
|
||||
foreach ($entries as $entry) {
|
||||
// Iterate over entries (descendands)
|
||||
|
||||
// Write entry to the buffer of entries (sort by $category and $product)
|
||||
${$entry->_type}[] = $entry;
|
||||
}
|
||||
|
||||
// Write to the buffer of global variables of view templater
|
||||
$this->view->categories = $category ?? null;
|
||||
|
||||
// Write to the buffer of global variables of view templater
|
||||
$this->view->products = $product ?? null;
|
||||
|
||||
/* var_dump($this->view->products); die; */
|
||||
|
||||
// Delete buffers
|
||||
unset($category, $product);
|
||||
}
|
||||
} else if (!isset($parameters['category'])) {
|
||||
// Not received identifie of the category
|
||||
|
||||
// Search for root ascendants categories
|
||||
$this->view->categories = entry::ascendants(descendant: new category, errors: $this->errors['catalog']) ?? null;
|
||||
}
|
||||
// Validating
|
||||
// validating
|
||||
if (!empty($parameters['product']) && preg_match('/[\d]+/', $parameters['product'], $matches)) $product = (int) $matches[0];
|
||||
|
||||
if (isset($product)) {
|
||||
// Received and validated identifier of the product
|
||||
// received and validated identifier of the product
|
||||
|
||||
// Search for the product data and write to the buffer of global variables of view templater
|
||||
$this->view->product = product::read(
|
||||
$this->view->product = product::read(
|
||||
filter: "d.identifier == @identifier && d.deleted != true && d.hidden != true",
|
||||
sort: 'd.created DESC',
|
||||
amount: 1,
|
||||
|
@ -116,14 +66,6 @@ final class catalog extends core
|
|||
// Initializing buffer of filters query (AQL)
|
||||
$_filters = 'd.deleted != true && d.hidden != true';
|
||||
|
||||
// Search among products in the $category
|
||||
if (isset($this->view->products) && count($this->view->products) > 0) {
|
||||
// Amount of rendered products is more than 0
|
||||
|
||||
// Write to the buffer of filters query (AQL)
|
||||
$_filters .= ' && POSITION(["' . implode('", "', array_map(fn(_document $document): string => $document->getId(), $this->view->products)) . '"], d._id)';
|
||||
}
|
||||
|
||||
// Validating
|
||||
if (!empty($parameters['brand']) && preg_match('/[\w\s]+/u', urldecode($parameters['brand']), $matches)) $brand = $matches[0];
|
||||
|
||||
|
@ -264,18 +206,6 @@ final class catalog extends core
|
|||
]
|
||||
]
|
||||
);
|
||||
|
||||
// Search for products and write to the buffer of global variables of view templater
|
||||
$this->view->products = product::read(
|
||||
search: $text ?? null,
|
||||
filter: $_filters,
|
||||
sort: $_sort,
|
||||
amount: 50, // @todo pagination
|
||||
language: $this->language,
|
||||
parameters: $_parameters,
|
||||
return: 'DISTINCT MERGE(d, {name: d.name.@language, description: d.description.@language, compatibility: d.compatibility.@language})',
|
||||
errors: $this->errors['catalog']
|
||||
);
|
||||
} else {
|
||||
// Not received or not validated filter by brand
|
||||
|
||||
|
@ -302,6 +232,62 @@ final class catalog extends core
|
|||
);
|
||||
}
|
||||
|
||||
// Validating
|
||||
if (!empty($parameters['category']) && preg_match('/[\d]+/', $parameters['category'], $matches)) $category = (int) $matches[0];
|
||||
|
||||
if (isset($category)) {
|
||||
// Received and validated identifier of the category
|
||||
|
||||
// Initialize of category
|
||||
$category = category::_read('d.identifier == @identifier', parameters: ['identifier' => $category], errors: $this->errors['catalog']);
|
||||
|
||||
if ($category instanceof category) {
|
||||
// Found the category
|
||||
|
||||
// Write to the response buffer
|
||||
$response['category'] = ['name' => $category->name ?? null];
|
||||
|
||||
// Search for categories that are descendants of $category
|
||||
$entries = entry::search(
|
||||
document: $category,
|
||||
amount: 50,
|
||||
errors: $this->errors['catalog']
|
||||
);
|
||||
|
||||
// Initialize buffers of entries (in singular, by parameter from ArangoDB)
|
||||
$category = $product = [];
|
||||
|
||||
foreach ($entries as $entry) {
|
||||
// Iterate over entries (descendands)
|
||||
|
||||
// Write entry to the buffer of entries (sort by $category and $product)
|
||||
${$entry->_type}[] = $entry;
|
||||
}
|
||||
|
||||
// Write to the buffer of global variables of view templater
|
||||
$this->view->categories = $category;
|
||||
|
||||
// Write to the buffer of global variables of view templater
|
||||
$this->view->products = $product;
|
||||
|
||||
if (isset($this->view->products) && count($this->view->products) > 0) {
|
||||
// Amount of rendered products is more than 0
|
||||
|
||||
// Write to the buffer of filters query (AQL)
|
||||
$_filters .= ' && POSITION(["' . implode('", "', array_map(fn(_document $document): string => $document->getId(), $this->view->products)) . '"], d._id)';
|
||||
}
|
||||
|
||||
// Deleting buffers
|
||||
unset($category, $product);
|
||||
}
|
||||
} else if (!isset($parameters['category'])) {
|
||||
// Not received identifier of the category
|
||||
|
||||
// search for root ascendants categories
|
||||
$this->view->categories = entry::ascendants(descendant: new category, errors: $this->errors['catalog']) ?? null;
|
||||
}
|
||||
|
||||
// Search among products in the $category
|
||||
if (isset($this->view->products) && count($this->view->products) > 0) {
|
||||
// Amount of rendered products is more than 0
|
||||
|
||||
|
@ -314,6 +300,18 @@ final class catalog extends core
|
|||
errors: $this->errors['catalog']
|
||||
)
|
||||
];
|
||||
|
||||
// Search for products and write to the buffer of global variables of view templater
|
||||
$this->view->products = product::read(
|
||||
search: $text ?? null,
|
||||
filter: $_filters,
|
||||
sort: $_sort,
|
||||
amount: 50, // @todo pagination
|
||||
language: $this->language,
|
||||
parameters: $_parameters,
|
||||
return: 'DISTINCT MERGE(d, {name: d.name.@language, description: d.description.@language, compatibility: d.compatibility.@language})',
|
||||
errors: $this->errors['catalog']
|
||||
);
|
||||
}
|
||||
|
||||
if (isset($menu)) {
|
||||
|
@ -347,7 +345,9 @@ final class catalog extends core
|
|||
}
|
||||
|
||||
// Exit (success)
|
||||
return $this->view->render('catalog/page.html');
|
||||
return $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
|
||||
|
||||
|
|
|
@ -119,8 +119,8 @@ class core extends controller
|
|||
$this->settings = settings::active();
|
||||
|
||||
// Initializing of language
|
||||
if ($this->account?->language) $this->language = $this->account->language ?? language::en->name;
|
||||
else if ($this->settings?->language) $this->language = $this->settings->language ?? language::en->name;
|
||||
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 preprocessor of views
|
||||
$this->view = new templater(
|
||||
|
|
|
@ -161,6 +161,8 @@ final class session extends core
|
|||
* @param array $parameters Parameters of the request (POST + GET)
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @todo переделать под trait buffer
|
||||
*/
|
||||
public function write(array $parameters = []): void
|
||||
{
|
||||
|
|
|
@ -7,9 +7,10 @@ namespace mirzaev\arming_bot\models;
|
|||
// Files of the project
|
||||
use mirzaev\arming_bot\models\core,
|
||||
mirzaev\arming_bot\models\traits\status,
|
||||
mirzaev\arming_bot\models\traits\document as arangodb_document_trait,
|
||||
mirzaev\arming_bot\models\traits\buffer,
|
||||
mirzaev\arming_bot\models\interfaces\document as arangodb_document_interface,
|
||||
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;
|
||||
|
||||
// Framework for ArangoDB
|
||||
|
@ -33,9 +34,9 @@ use exception;
|
|||
* @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 implements arangodb_document_interface
|
||||
final class account extends core implements document_interface, collection_interface
|
||||
{
|
||||
use status, arangodb_document_trait, buffer {
|
||||
use status, document_trait, buffer {
|
||||
buffer::write as write;
|
||||
}
|
||||
|
||||
|
@ -77,20 +78,20 @@ final class account extends core implements arangodb_document_interface
|
|||
// Initialized the account
|
||||
|
||||
// Initializing the object
|
||||
$account = new account;
|
||||
$account = new static;
|
||||
|
||||
if (method_exists($account, '__document')) {
|
||||
// Object can implement a document from ArangoDB
|
||||
|
||||
// Abstractioning of parameters
|
||||
$result->language = language::{$result->language} ?? 'en';
|
||||
if (isset($result->language)) $result->language = language::{$result->language};
|
||||
|
||||
// Writing the instance of account document from ArangoDB to the implement object
|
||||
$account->__document($result);
|
||||
|
||||
// Exit (success)
|
||||
return $account;
|
||||
}
|
||||
} else throw new exception('Class ' . static::class . ' does not implement a document from ArangoDB');
|
||||
} else if ($registration) {
|
||||
// Not found the account and registration is requested
|
||||
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace mirzaev\arming_bot\models;
|
||||
|
||||
// Files of the project
|
||||
use mirzaev\arming_bot\models\core,
|
||||
mirzaev\arming_bot\models\traits\document as document_trait,
|
||||
mirzaev\arming_bot\models\interfaces\document as document_interface,
|
||||
mirzaev\arming_bot\models\interfaces\collection as collection_interface;
|
||||
|
||||
/**
|
||||
* Model of 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 cart extends core implements document_interface, collection_interface
|
||||
{
|
||||
use document_trait;
|
||||
|
||||
/**
|
||||
* Name of the collection in ArangoDB
|
||||
*/
|
||||
final public const string COLLECTION = 'cart';
|
||||
}
|
|
@ -6,8 +6,9 @@ namespace mirzaev\arming_bot\models;
|
|||
|
||||
// Files of the project
|
||||
use mirzaev\arming_bot\models\core,
|
||||
mirzaev\arming_bot\models\traits\document as arangodb_document_trait,
|
||||
mirzaev\arming_bot\models\interfaces\document as arangodb_document_interface;
|
||||
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\collection,
|
||||
|
@ -25,9 +26,9 @@ use exception;
|
|||
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||
*/
|
||||
final class category extends core implements arangodb_document_interface
|
||||
final class category extends core implements document_interface, collection_interface
|
||||
{
|
||||
use arangodb_document_trait;
|
||||
use document_trait;
|
||||
|
||||
/**
|
||||
* Name of the collection in ArangoDB
|
||||
|
|
|
@ -6,8 +6,9 @@ namespace mirzaev\arming_bot\models;
|
|||
|
||||
// Files of the project
|
||||
use mirzaev\arming_bot\models\core,
|
||||
mirzaev\arming_bot\models\traits\document as arangodb_document_trait,
|
||||
mirzaev\arming_bot\models\interfaces\document as arangodb_document_interface;
|
||||
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;
|
||||
|
@ -20,9 +21,9 @@ use mirzaev\arangodb\enumerations\collection\type;
|
|||
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||
*/
|
||||
final class connect extends core implements arangodb_document_interface
|
||||
final class connect extends core implements document_interface, collection_interface
|
||||
{
|
||||
use arangodb_document_trait;
|
||||
use document_trait;
|
||||
|
||||
/**
|
||||
* Name of the collection in ArangoDB
|
||||
|
|
|
@ -4,9 +4,6 @@ declare(strict_types=1);
|
|||
|
||||
namespace mirzaev\arming_bot\models;
|
||||
|
||||
// Files of the project
|
||||
use mirzaev\arming_bot\models\interfaces\collection as collection_interface;
|
||||
|
||||
// Framework for PHP
|
||||
use mirzaev\minimal\model;
|
||||
|
||||
|
@ -28,7 +25,7 @@ use exception;
|
|||
* @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 model implements collection_interface
|
||||
class core extends model
|
||||
{
|
||||
/**
|
||||
* Postfix for name of models files
|
||||
|
|
|
@ -6,8 +6,9 @@ namespace mirzaev\arming_bot\models;
|
|||
|
||||
// Files of the project
|
||||
use mirzaev\arming_bot\models\core,
|
||||
mirzaev\arming_bot\models\traits\document as arangodb_document_trait,
|
||||
mirzaev\arming_bot\models\interfaces\document as arangodb_document_interface;
|
||||
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;
|
||||
|
||||
// Library for ArangoDB
|
||||
use ArangoDBClient\Document as _document;
|
||||
|
@ -28,9 +29,9 @@ use exception;
|
|||
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||
*/
|
||||
final class entry extends core implements arangodb_document_interface
|
||||
final class entry extends core implements document_interface, collection_interface
|
||||
{
|
||||
use arangodb_document_trait;
|
||||
use document_trait;
|
||||
|
||||
/**
|
||||
* Name of the collection in ArangoDB
|
||||
|
|
|
@ -6,9 +6,9 @@ namespace mirzaev\arming_bot\models;
|
|||
|
||||
// Files of the project
|
||||
use mirzaev\arming_bot\models\core,
|
||||
mirzaev\arming_bot\models\traits\document as arangodb_document_trait,
|
||||
mirzaev\arming_bot\models\interfaces\document as arangodb_document_interface;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* Model of menu
|
||||
|
@ -18,9 +18,9 @@ use mirzaev\arming_bot\models\core,
|
|||
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||
*/
|
||||
final class menu extends core implements arangodb_document_interface
|
||||
final class menu extends core implements document_interface, collection_interface
|
||||
{
|
||||
use arangodb_document_trait;
|
||||
use document_trait;
|
||||
|
||||
/**
|
||||
* Name of the collection in ArangoDB
|
||||
|
|
|
@ -7,12 +7,17 @@ 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 arangodb_document_trait;
|
||||
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\collection,
|
||||
mirzaev\arangodb\document;
|
||||
|
||||
// Library for ArangoDB
|
||||
use ArangoDBClient\Document as _document;
|
||||
|
||||
// Built-in libraries
|
||||
use exception;
|
||||
|
||||
|
@ -24,9 +29,9 @@ use exception;
|
|||
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||
*/
|
||||
final class product extends core
|
||||
final class product extends core implements document_interface, collection_interface
|
||||
{
|
||||
use arangodb_document_trait;
|
||||
use document_trait;
|
||||
|
||||
/**
|
||||
* Name of the collection in ArangoDB
|
||||
|
@ -118,11 +123,11 @@ final class product extends core
|
|||
* @param int $page Page
|
||||
* @param int $amount Amount per page
|
||||
* @param string|null $return Return (AQL)
|
||||
* @param language $language Language code (en, ru...)
|
||||
* @param language|null $language Language code (en, ru...)
|
||||
* @param array $parameters Binded parameters for placeholders ['placeholder' => parameter]
|
||||
* @param array &$errors Registry of errors
|
||||
*
|
||||
* @return array Found products (can be empty)
|
||||
* @return array|static Found products or instance of the product from ArangoDB (can be empty)
|
||||
*/
|
||||
public static function read(
|
||||
?string $search = null,
|
||||
|
@ -131,22 +136,25 @@ final class product extends core
|
|||
int $page = 1,
|
||||
int $amount = 100,
|
||||
?string $return = 'DISTINCT d',
|
||||
language $language = language::en,
|
||||
?language $language = null,
|
||||
array $parameters = [],
|
||||
array &$errors = []
|
||||
): array {
|
||||
): array|static {
|
||||
try {
|
||||
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
|
||||
// Initialized the collection
|
||||
|
||||
// Initializing of the language parameter
|
||||
if ($language instanceof language) $parameters['language'] = $language->name;
|
||||
|
||||
// Initializing parameters for search
|
||||
if ($search) $parameters += [
|
||||
'search' => $search,
|
||||
'analyzer' => 'text_' . $language->name
|
||||
'analyzer' => 'text_' . $language->name ?? language::en->name
|
||||
];
|
||||
|
||||
// Reading products
|
||||
$documents = collection::execute(
|
||||
// Search for products
|
||||
$result = collection::execute(
|
||||
sprintf(
|
||||
<<<'AQL'
|
||||
FOR d IN @@collection %s
|
||||
|
@ -182,19 +190,31 @@ final class product extends core
|
|||
),
|
||||
[
|
||||
'@collection' => empty($search) ? static::COLLECTION : static::COLLECTION . 's_search',
|
||||
'language' => $language->name,
|
||||
'offset' => --$page <= 0 ? $page = 0 : $page * $amount,
|
||||
'amount' => $amount,
|
||||
] + $parameters,
|
||||
errors: $errors
|
||||
);
|
||||
|
||||
if ($documents) {
|
||||
// Found products
|
||||
if ($result instanceof _document) {
|
||||
// Found product
|
||||
|
||||
// Exit (success)
|
||||
return is_array($documents) ? $documents : [$documents];
|
||||
} else return [];
|
||||
// Initializing the object
|
||||
$product = new static;
|
||||
|
||||
if (method_exists($product, '__document')) {
|
||||
// Object can implement a document from ArangoDB
|
||||
|
||||
// Writing the instance of product document from ArangoDB to the implement object
|
||||
$product->__document($result);
|
||||
|
||||
// Exit (success)
|
||||
return $product;
|
||||
} else throw new exception('Class ' . static::class . ' does not implement a document from ArangoDB');
|
||||
}
|
||||
|
||||
// Exit (success)
|
||||
return $result ?? [];
|
||||
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
|
||||
} catch (exception $e) {
|
||||
// Writing to the registry of errors
|
||||
|
|
|
@ -7,11 +7,13 @@ 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\document as arangodb_document_trait,
|
||||
mirzaev\arming_bot\models\traits\buffer,
|
||||
mirzaev\arming_bot\models\interfaces\document as arangodb_document_interface,
|
||||
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;
|
||||
|
||||
// Framework for ArangoDB
|
||||
|
@ -32,9 +34,9 @@ use exception;
|
|||
* @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 implements arangodb_document_interface
|
||||
final class session extends core implements document_interface, collection_interface
|
||||
{
|
||||
use status, arangodb_document_trait, buffer {
|
||||
use status, document_trait, buffer {
|
||||
buffer::write as write;
|
||||
}
|
||||
|
||||
|
@ -98,7 +100,7 @@ final class session extends core implements arangodb_document_interface
|
|||
RETURN d
|
||||
AQL,
|
||||
[
|
||||
'@collection' => static::COLLECTION,
|
||||
'@collection' => static::COLLECTION,
|
||||
'_id' => $_id,
|
||||
'time' => time()
|
||||
],
|
||||
|
@ -148,6 +150,7 @@ final class session extends core implements arangodb_document_interface
|
|||
$result = collection::execute(
|
||||
<<<AQL
|
||||
FOR v IN INBOUND @session GRAPH sessions
|
||||
FILTER IS_SAME_COLLECTION(account, v._id)
|
||||
SORT v.created DESC
|
||||
LIMIT 1
|
||||
RETURN v
|
||||
|
@ -159,7 +162,7 @@ final class session extends core implements arangodb_document_interface
|
|||
);
|
||||
|
||||
if ($result instanceof _document) {
|
||||
// Found active settings
|
||||
// Found the account
|
||||
|
||||
// Initializing the object
|
||||
$account = new account;
|
||||
|
@ -168,14 +171,14 @@ final class session extends core implements arangodb_document_interface
|
|||
// Object can implement a document from ArangoDB
|
||||
|
||||
// Abstractioning of parameters
|
||||
$result->language = language::{$result->language} ?? 'en';
|
||||
if (isset($result->language)) $result->language = language::{$result->language};
|
||||
|
||||
// Writing the instance of account document from ArangoDB to the implement object
|
||||
$account->__document($result);
|
||||
|
||||
// Exit (success)
|
||||
return $account;
|
||||
}
|
||||
} else throw new exception('Class ' . account::class . ' does not implement a document from ArangoDB');
|
||||
} else return null;
|
||||
} else throw new exception('Failed to initialize ' . account::TYPE . ' collection: ' . account::COLLECTION);
|
||||
} else throw new exception('Failed to initialize ' . connect::TYPE . ' collection: ' . connect::COLLECTION);
|
||||
|
@ -195,34 +198,97 @@ final class session extends core implements arangodb_document_interface
|
|||
}
|
||||
|
||||
/**
|
||||
* Connect account to session
|
||||
* Search for a connected cart
|
||||
*
|
||||
* @param account $account Account
|
||||
* @param array &$errors Registry of errors
|
||||
*
|
||||
* @return string|null The identifier of the created edge of the "connect" collection, if created
|
||||
* @return cart|null An object implements the instance of the cart document from ArangoDB, if found
|
||||
*/
|
||||
public function connect(account $account, array &$errors = []): ?string
|
||||
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(account::COLLECTION, account::TYPE, errors: $errors)) {
|
||||
// Collections initialized
|
||||
if (collection::initialize(cart::COLLECTION, cart::TYPE, errors: $errors)) {
|
||||
// Initialized collections
|
||||
|
||||
// The instance of the session document from ArangoDB is initialized?
|
||||
isset($this->document) || throw new exception('The instance of the sessoin document from ArangoDB is not initialized');
|
||||
|
||||
// Writing document and exit (success)
|
||||
return document::write(
|
||||
connect::COLLECTION,
|
||||
// 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,
|
||||
[
|
||||
'_from' => $account->getId(),
|
||||
'_to' => $this->document->getId()
|
||||
'session' => $this->getId()
|
||||
],
|
||||
errors: $errors
|
||||
);
|
||||
} else throw new exception('Failed to initialize ' . account::TYPE . ' collection: ' . account::COLLECTION);
|
||||
|
||||
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) {
|
||||
|
|
|
@ -6,15 +6,16 @@ namespace mirzaev\arming_bot\models;
|
|||
|
||||
// Files of the project
|
||||
use mirzaev\arming_bot\models\core,
|
||||
mirzaev\arming_bot\models\traits\document as arangodb_document_trait,
|
||||
mirzaev\arming_bot\models\interfaces\document as arangodb_document_interface,
|
||||
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;
|
||||
|
||||
// Framework for ArangoDB
|
||||
use mirzaev\arangodb\collection,
|
||||
mirzaev\arangodb\document;
|
||||
|
||||
// Library для ArangoDB
|
||||
// Library for ArangoDB
|
||||
use ArangoDBClient\Document as _document;
|
||||
|
||||
// Built-in libraries
|
||||
|
@ -28,9 +29,9 @@ use exception;
|
|||
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||
*/
|
||||
final class settings extends core implements arangodb_document_interface
|
||||
final class settings extends core implements document_interface, collection_interface
|
||||
{
|
||||
use arangodb_document_trait;
|
||||
use document_trait;
|
||||
|
||||
/**
|
||||
* Name of the collection in ArangoDB
|
||||
|
@ -76,14 +77,14 @@ final class settings extends core implements arangodb_document_interface
|
|||
// Object can implement a document from ArangoDB
|
||||
|
||||
// Abstractioning of parameters
|
||||
$result->language = language::{$result->language} ?? 'en';
|
||||
if (isset($result->language)) $result->language = language::{$result->language};
|
||||
|
||||
// Writing the instance of settings document from ArangoDB to the implement object
|
||||
$settings->__document($result);
|
||||
|
||||
// Exit (success)
|
||||
return $settings;
|
||||
}
|
||||
} else throw new exception('Class ' . static::class . ' does not implement a document from ArangoDB');
|
||||
} else if ($create) {
|
||||
// Not found active settings and requested their creating
|
||||
|
||||
|
|
|
@ -6,9 +6,10 @@ 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 arangodb_document_trait,
|
||||
mirzaev\arming_bot\models\interfaces\document as arangodb_document_interface;
|
||||
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;
|
||||
|
||||
// Framework for ArangoDB
|
||||
use mirzaev\arangodb\collection,
|
||||
|
@ -29,9 +30,9 @@ use exception,
|
|||
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||
*/
|
||||
final class suspension extends core implements arangodb_document_interface
|
||||
final class suspension extends core implements document_interface, collection_interface
|
||||
{
|
||||
use arangodb_document_trait;
|
||||
use document_trait;
|
||||
|
||||
/**
|
||||
* Name of the collection in ArangoDB
|
||||
|
@ -81,7 +82,7 @@ final class suspension extends core implements arangodb_document_interface
|
|||
|
||||
// Exit (success)
|
||||
return $suspension;
|
||||
}
|
||||
} else throw new exception('Class ' . static::class . ' does not implement a document from ArangoDB');
|
||||
} else return null;
|
||||
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
|
||||
} catch (exception $e) {
|
||||
|
|
|
@ -5,13 +5,17 @@ declare(strict_types=1);
|
|||
namespace mirzaev\arming_bot\models\traits;
|
||||
|
||||
// Files of the project
|
||||
use mirzaev\arming_bot\models\core;
|
||||
use mirzaev\arming_bot\models\interfaces\document as document_interface,
|
||||
mirzaev\arming_bot\models\interfaces\collection as collection_interface,
|
||||
mirzaev\arming_bot\models\connect;
|
||||
|
||||
// Library для ArangoDB
|
||||
use ArangoDBClient\Document as _document;
|
||||
|
||||
// Framework for ArangoDB
|
||||
use mirzaev\arangodb\connection as arangodb;
|
||||
use mirzaev\arangodb\connection as arangodb,
|
||||
mirzaev\arangodb\document as framework_document,
|
||||
mirzaev\arangodb\collection;
|
||||
|
||||
// Built-in libraries
|
||||
use exception;
|
||||
|
@ -19,6 +23,8 @@ 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
|
||||
*
|
||||
* @package mirzaev\arming_bot\models\traits
|
||||
|
@ -71,6 +77,52 @@ trait document
|
|||
return $this->document ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect
|
||||
*
|
||||
* @param collecton_interface $document Document
|
||||
* @param array &$errors Registry of errors
|
||||
*
|
||||
* @return string|null The identifier of the created edge of the "connect" collection, if created
|
||||
*/
|
||||
public function connect(collection_interface $document, array &$errors = []): ?string
|
||||
{
|
||||
try {
|
||||
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
|
||||
if (collection::initialize(connect::COLLECTION, connect::TYPE, errors: $errors)) {
|
||||
if (collection::initialize($document::COLLECTION, $document::TYPE, errors: $errors)) {
|
||||
// Initialized collections
|
||||
|
||||
if ($this->document instanceof _document) {
|
||||
// Initialized instance of the document from ArangoDB
|
||||
|
||||
// Writing document and exit (success)
|
||||
return framework_document::write(
|
||||
connect::COLLECTION,
|
||||
[
|
||||
'_from' => $document->getId(),
|
||||
'_to' => $this->document->getId()
|
||||
],
|
||||
errors: $errors
|
||||
);
|
||||
} else throw new exception('The instance of the document from ArangoDB is not initialized');
|
||||
} else throw new exception('Failed to initialize ' . $document::TYPE . ' collection: ' . $document::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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write
|
||||
*
|
||||
|
|
|
@ -40,6 +40,8 @@ $router = new router;
|
|||
$router
|
||||
->write('/', 'catalog', 'index', 'GET')
|
||||
->write('/', 'catalog', 'index', 'POST')
|
||||
->write('/cart', 'cart', 'index', 'GET')
|
||||
->write('/cart/write', 'cart', 'write', 'POST')
|
||||
->write('/account/write', 'account', 'write', 'POST')
|
||||
->write('/session/write', 'session', 'write', 'POST')
|
||||
->write('/session/connect/telegram', 'session', 'telegram', 'POST')
|
||||
|
|
|
@ -35,9 +35,90 @@ import("/js/core.js").then(() =>
|
|||
*/
|
||||
core.cart = class cart {
|
||||
/**
|
||||
* Products in cart ["product/148181", "product/148181", "product/148181"...]
|
||||
* Write or delete product from the cart (interface)
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
static cart = [];
|
||||
static toggle(button, force = false) {
|
||||
// Blocking the button
|
||||
button.setAttribute("disabled", "true");
|
||||
|
||||
// Execute under damper
|
||||
this.toggle_damper(button, force);
|
||||
|
||||
// Exit (success)
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write or delete product from the cart (damper)
|
||||
*
|
||||
* @param {HTMLElement} button Button of the product <a>
|
||||
* @param {bool} force Ignore the damper? (false)
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
static toggle_damper = core.damper(
|
||||
(...variables) => this.toggle_system(...variables),
|
||||
800,
|
||||
1,
|
||||
);
|
||||
|
||||
/**
|
||||
* Write or delete product from the cart (system)
|
||||
*
|
||||
* @param {HTMLElement} button Button of the product <a>
|
||||
*
|
||||
* @return {Promise} Request to the server
|
||||
*
|
||||
* @todo add unblocking button by timer + everywhere
|
||||
*/
|
||||
static async toggle_system(button) {
|
||||
if (button instanceof HTMLElement) {
|
||||
// Validated
|
||||
|
||||
// Initializing of identifier of the product
|
||||
const identifier = button.getAttribute("data-product-identifier");
|
||||
|
||||
if (typeof identifier === "string" && identifier.length > 0) {
|
||||
// Validated identifier
|
||||
|
||||
return await core.request(
|
||||
"/cart/write",
|
||||
`product=${identifier}`,
|
||||
)
|
||||
.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");
|
||||
|
||||
if (json.status) {
|
||||
// The product in the cart
|
||||
|
||||
// Writing style of added to the cart button
|
||||
button.classList.add("cart");
|
||||
} else {
|
||||
// The product is not in the cart
|
||||
|
||||
// Deleting style of added to the cart button
|
||||
button.classList.remove("cart");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -797,7 +797,7 @@ import("/js/core.js").then(() =>
|
|||
*
|
||||
* @return {Promise} Request to the server
|
||||
*/
|
||||
static product_system(identifier) {
|
||||
static async product_system(identifier) {
|
||||
if (typeof identifier === "string") {
|
||||
// Validated identifier
|
||||
|
||||
|
@ -807,7 +807,7 @@ import("/js/core.js").then(() =>
|
|||
// Write parameter to the buffer of URN parameters
|
||||
parameters.set("product", identifier);
|
||||
|
||||
return core.request("?" + parameters).then((json) => {
|
||||
return await core.request("?" + parameters).then((json) => {
|
||||
if (
|
||||
json.errors !== null &&
|
||||
typeof json.errors === "object" &&
|
||||
|
|
|
@ -137,6 +137,10 @@ 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 {
|
||||
z-index: 100;
|
||||
height: 33px;
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
{% macro card(product) %}
|
||||
{% set title = product.name[language] ~ ' ' ~ product.brand[language] ~ format_dimensions(product.dimensions.x, product.dimensions.y, product.dimensions.z, ' ') ~ ' ' ~ product.weight ~ 'г' %}
|
||||
{% set title = product.name[language] ~ ' ' ~ product.brand[language] ~ format_dimensions(product.dimensions.x,
|
||||
product.dimensions.y, product.dimensions.z, ' ') ~ ' ' ~ product.weight ~ 'г' %}
|
||||
<article id="{{ product.getId() }}" class="product unselectable">
|
||||
<a 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">
|
||||
<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] }}">
|
||||
{{ title | length > 45 ? title | slice(0, 45) ~ '...' : title }}
|
||||
</p>
|
||||
</a>
|
||||
<button title="Добавить в корзину" onclick="catalog.cart.add(this)" tabindex="15">
|
||||
<button title="Добавить в корзину" onclick="core.cart.toggle(this)" data-product-identifier="{{ product.identifier }}"
|
||||
data-product-cost="{{ product.cost }}" tabindex="15">
|
||||
{{ product.cost }}р
|
||||
</button>
|
||||
</article>
|
||||
|
|
Loading…
Reference in New Issue