menu + catelog REBUILD + new icons

This commit is contained in:
Arsen Mirzaev Tatyano-Muradovich 2024-10-20 00:17:11 +03:00
parent 5509b97148
commit 16be453b07
22 changed files with 747 additions and 556 deletions

View File

@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace mirzaev\arming_bot\controllers;
// Files of the project
use mirzaev\arming_bot\controllers\core,
mirzaev\arming_bot\models\session,
mirzaev\arming_bot\models\account as model;
// Framework for ArangoDB
use mirzaev\arangodb\document;
/**
* Controller of account
*
* @package mirzaev\arming_bot\controllers
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class account extends core
{
/**
* Registry of errors
*/
protected array $errors = [
'session' => [],
'account' => []
];
/**
* Write to the buffer
*
* @param array $parameters Parameters of the request (POST + GET)
*
* @return void
*/
public function write(array $parameters = []): void
{
if (!empty($parameters) && $this->account instanceof model) {
// Found data of the program and active account
foreach ($parameters as $name => $value) {
// Iterate over parameters
// Validation of the parameter
if (mb_strlen($value) > 4096) continue;
// Convert name to multidimensional array
foreach (array_reverse(explode('_', $name)) as $key) $parameter = [$key => $parameter ?? json_validate($value) ? json_decode($value, true, 10) : $value];
// Write data of to the buffer parameter in the implement object of account document from ArangoDB
$this->account->buffer = $parameter + $this->account->buffer ?? [];
}
// Write from implement object to account document from ArangoDB
document::update($this->account->__document(), $this->errors['account']);
}
}
}

View File

@ -8,7 +8,11 @@ namespace mirzaev\arming_bot\controllers;
use mirzaev\arming_bot\controllers\core, use mirzaev\arming_bot\controllers\core,
mirzaev\arming_bot\models\entry, mirzaev\arming_bot\models\entry,
mirzaev\arming_bot\models\category, mirzaev\arming_bot\models\category,
mirzaev\arming_bot\models\product; mirzaev\arming_bot\models\product,
mirzaev\arming_bot\models\menu;
// Library for ArangoDB
use ArangoDBClient\Document as _document;
/** /**
* Controller of catalog * Controller of catalog
@ -24,6 +28,7 @@ final class catalog extends core
protected array $errors = [ protected array $errors = [
'session' => [], 'session' => [],
'account' => [], 'account' => [],
'menu' => [],
'catalog' => [] 'catalog' => []
]; ];
@ -34,64 +39,160 @@ final class catalog extends core
*/ */
public function index(array $parameters = []): ?string public function index(array $parameters = []): ?string
{ {
// Initializing identifier of a category // Validating
preg_match('/[\d]+/', $parameters['identifier'] ?? '', $matches); if (!empty($parameters['category']) && preg_match('/[\d]+/', $parameters['category'], $matches)) $category = (int) $matches[0];
$identifier = $matches[0] ?? null;
// Initializint the buffer of respnse if (isset($category)) {
$html = []; // Received and validated identifier of the category
if (!empty($parameters['identifier'])) { // Initialize of category
// Передана категория (идентификатор) $category = category::_read('d.identifier == @identifier', parameters: ['identifier' => $category], errors: $this->errors['catalog']);
// Инициализация актуальной категории
$category = category::_read('d.identifier == @identifier', parameters: ['identifier' => (int) $identifier], errors: $this->errors['catalog']);
if ($category instanceof category) { if ($category instanceof category) {
// Found the 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( $entries = entry::search(
document: $category, document: $category,
amount: 30, amount: 30,
errors: $this->errors['catalog'] errors: $this->errors['catalog']
); );
// Объявление буферов категорий и товаров (важно - в единственном числе, по параметру из базы данных) // Initialize buffers of entries (in singular, by parameter from ArangoDB)
$category = $product = []; $category = $product = [];
foreach ($entries as $entry) { foreach ($entries as $entry) {
// Перебор вхождений // Iterate over entries (descendands)
// Запись массивов категорий и товаров ($category и $product) в буфер глобальной переменной шаблонизатора // Write entry to the buffer of entries (sort by $category and $product)
${$entry->_type}[] = $entry; ${$entry->_type}[] = $entry;
} }
// Запись категорий из буфера в глобальную переменную шаблонизатора // Write to the buffer of global variables of view templater
$this->view->categories = $category; $this->view->categories = $category;
// Запись товаров из буфера в глобальную переменную шаблонизатора // Write to the buffer of global variables of view templater
$this->view->products = $product; $this->view->products = $product;
// Generating filters // 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']);
}
// 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
// Search for product and write to the buffer of global variables of view templater
$this->view->product = product::read(
filter: "d.identifier == @identifier && d.deleted != true && d.hidden != true",
sort: 'd.created DESC',
amount: 1,
return: '{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],
language: $this->language,
errors: $this->errors['catalog']
)[0]?->getAll() ?? null;
}
// Validating
if (!empty($parameters['text']) && preg_match('/[\w\s]+/u', $parameters['text'], $matches)) $text = $matches[0];
if (isset($text)) {
// Received and validated text for search
// Intialize buffer of query parameters
$_parameters = [];
// Initialize buffer of filters query (AQL)
$_filters = 'd.deleted != true && d.hidden != true';
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]+/', $parameters['brand'], $matches)) $brand = $matches[0];
if (isset($brand)) {
// Received and validated filter by brand
// Write to the buffer of filters query (AQL)
$_filters .= ' && d.brand.@language == @brand';
// Write to the buffer of query parameters
$_parameters['brand'] = $brand;
}
// Initialize buffer of filters query (AQL)
$_sort = 'd.position ASC, d.name ASC, d.created DESC';
// Validating
if (!empty($parameters['sort']) && preg_match('/[\w]+/', $parameters['sort'], $matches)) $sort = $matches[0];
if (isset($sort)) {
// Received and validated sort
// Write to the buffer of sort query (AQL)
$_sort = "d.@sort DESC, $_sort";
// Write to the buffer of query parameters
$_parameters['sort'] = $sort;
}
// Search for products and write to the buffer of global variables of view templater
$this->view->products = product::read(
search: $text,
filter: $_filters,
sort: $_sort,
amount: 30,
language: $this->language,
parameters: $_parameters,
errors: $this->errors['catalog']
);
}
if (isset($this->view->products) && count($this->view->products) > 0) {
// Amount of rendered products is more than 0
// Search for filters and write to the buffer of global variables of view templater
$this->view->filters = [ $this->view->filters = [
'brands' => product::collect( 'brands' => product::collect(
'd.brand.' . $this->language->name, return: 'd.brand.@language',
products: array_map(fn(_document $document): string => $document->getId(), $this->view->products),
language: $this->language,
errors: $this->errors['catalog'] errors: $this->errors['catalog']
) )
]; ];
// Generating HTML and writing to the buffer of response
$html = [
'filters' => count($this->view->products) > 0 ? $this->view->render('catalog/elements/filters.html') : null,
'products' => $this->view->render('catalog/elements/products/2columns.html')
];
} }
} else {
// Не передана категория
// Поиск категорий: "categories" (самый верхний уровень) if (isset($menu)) {
$this->view->categories = entry::ascendants(descendant: new category, errors: $this->errors['catalog']); //
} else {
// Not received ... menu
// Search for filters and write to the buffer of global variables of view templater
$this->view->menu = menu::_read(
return: 'MERGE(d, { name: d.name.@language })',
sort: 'd.position ASC, d.created DESC, d._key DESC',
amount: 4,
parameters: ['language' => $this->language->name],
errors: $this->errors['menu']
);
} }
if ($_SERVER['REQUEST_METHOD'] === 'GET') { if ($_SERVER['REQUEST_METHOD'] === 'GET') {
@ -102,6 +203,40 @@ final class catalog extends core
} else if ($_SERVER['REQUEST_METHOD'] === 'POST') { } else if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// POST request // POST request
// Initializing the buffer of response
$response = [
'title' => $title ?? null
];
if (isset($this->view->categories)) {
// Initialized categories
// Render HTML-code of categories and write to the response buffer
$response['html'] ??= [];
$response['html']['categories'] = $this->view->render('catalog/elements/categories.html');
}
if (isset($this->view->product)) {
// Initialized product
}
if (isset($this->view->products)) {
// Initialized products
// Render HTML-code of products and write to the response buffer
$response['html'] ??= [];
$response['html']['products'] = $this->view->render('catalog/elements/products/2columns.html');
}
if (isset($this->view->filters)) {
// Initialized filters
// Render HTML-code of filters and write to the response buffer
$response['html'] ??= [];
$response['html']['filters'] = $this->view->render('catalog/elements/filters.html');
}
// Initializing a response headers // Initializing a response headers
header('Content-Type: application/json'); header('Content-Type: application/json');
header('Content-Encoding: none'); header('Content-Encoding: none');
@ -112,11 +247,7 @@ final class catalog extends core
// Generating the reponse // Generating the reponse
echo json_encode( echo json_encode(
[ $response + [
'title' => $title ?? '',
'html' => $html + [
'categories' => $this->view->render('catalog/elements/categories.html'),
],
'errors' => $this->errors 'errors' => $this->errors
] ]
); );
@ -135,122 +266,4 @@ final class catalog extends core
// Exit (fail) // Exit (fail)
return null; return null;
} }
/**
* Search
*
* @param array $parameters Parameters of the request (POST + GET)
*/
public function search(array $parameters = []): ?string
{
// Initializing of text fore search
preg_match('/[\w\s]+/u', $parameters['text'] ?? '', $matches);
$text = $matches[0] ?? null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// POST request
// Search for products
$this->view->products = isset($text) ? product::read(
search: $text,
filter: 'd.deleted != true && d.hidden != true',
sort: 'd.position ASC, d.name ASC, d.created DESC',
amount: 30,
language: $this->language,
errors: $this->errors['catalog']
) : [];
// 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(
[
'title' => $title ?? '',
'html' => [
'products' => $this->view->render('catalog/elements/products/2columns.html')
],
'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;
}
/**
* Product
*
* @param array $parameters Parameters of the request (POST + GET)
*/
public function product(array $parameters = []): ?string
{
// Initializing identifier of a product
preg_match('/[\d]+/', $parameters['identifier'] ?? '', $matches);
$identifier = $matches[0] ?? null;
if (!empty($identifier)) {
// Received identifier of the product
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// POST request
// Search for products
$product = product::read(
filter: "d.identifier == @identifier && d.deleted != true && d.hidden != true",
sort: 'd.created DESC',
amount: 1,
return: '{identifier: d.identifier, name: d.name.@language, description: d.description.@language, cost: d.cost, weight: d.weight, dimensions: d.dimensions, brand: d.brand.@language, compatibility: d.compatibility.@language, images: d.images[*].storage}',
parameters: ['identifier' => (int) $identifier],
language: $this->language,
errors: $this->errors['catalog']
)[0]?->getAll();
// 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(
[
'product' => $product,
'errors' => $this->errors
]
);
// Initializing a response headers
header('Content-Length: ' . ob_get_length());
// Sending and deinitializing of the output buffer
ob_end_flush();
flush();
// Exit (success)
return null;
}
}
// Exit (fail)
return null;
}
} }

View File

@ -153,14 +153,14 @@ final class session extends core
return null; return null;
} }
/** /**
* Connect session to the telegram account * Write to the buffer
* *
* @param array $parameters Parameters of the request (POST + GET) * @param array $parameters Parameters of the request (POST + GET)
*
* @return void
*/ */
public function write(array $parameters = []): ?string public function write(array $parameters = []): void
{ {
if (!empty($parameters) && $this->session instanceof model) { if (!empty($parameters) && $this->session instanceof model) {
// Found data of the program and active session // Found data of the program and active session
@ -171,18 +171,15 @@ final class session extends core
// Validation of the parameter // Validation of the parameter
if (mb_strlen($value) > 4096) continue; if (mb_strlen($value) > 4096) continue;
// Write data of the program to the implement object of session document from ArangoDB // Convert name to multidimensional array
$this->session->{$name} = json_decode($value, true, 10); foreach (array_reverse(explode('_', $name)) as $key) $parameter = [$key => $parameter ?? json_validate($value) ? json_decode($value, true, 10) : $value];
// Write data of to the buffer parameter in the implement object of session document from ArangoDB
$this->session->buffer = $parameter + ($this->session->buffer ?? []);
} }
// Write from implement object to session document from ArangoDB // Write from implement object to session document from ArangoDB
document::update($this->session->__document(), $this->errors['session']); document::update($this->session->__document(), $this->errors['session']);
}
// Exit (success)
return null;
}
// Exit (fail)
return null;
} }
} }

View File

@ -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 arangodb_document_trait,
mirzaev\arming_bot\models\interfaces\document as arangodb_document_interface;
/**
* Model of menu
*
* @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 menu extends core implements arangodb_document_interface
{
use arangodb_document_trait;
/**
* Name of the collection in ArangoDB
*/
final public const string COLLECTION = 'menu';
}

View File

@ -214,12 +214,16 @@ final class product extends core
* Collect parameter from all products * Collect parameter from all products
* *
* @param string $return Return (AQL path) * @param string $return Return (AQL path)
* @param array $products Array with products system identifiers ["_id", "_id", "_id"...]
* @param array &$errors Registry of errors * @param array &$errors Registry of errors
* *
* @return array Array with found unique parameter values from all products (can be empty) * @return array Array with found unique parameter values from all products (can be empty)
*/ */
public static function collect( public static function collect(
string $return = 'd._key', string $return = 'd._key',
array $products = [],
language $language = language::en,
array $parameters = [],
array &$errors = [] array &$errors = []
): array { ): array {
try { try {
@ -230,13 +234,16 @@ final class product extends core
sprintf( sprintf(
<<<'AQL' <<<'AQL'
FOR d IN @@collection FOR d IN @@collection
%s
RETURN DISTINCT %s RETURN DISTINCT %s
AQL, AQL,
empty($products) ? '' : 'FILTER POSITION(["' . implode('", "', $products) . '"], d._id)',
empty($return) ? 'd._key' : $return empty($return) ? 'd._key' : $return
), ),
[ [
'@collection' => static::COLLECTION, '@collection' => static::COLLECTION,
], 'language' => $language->name,
] + $parameters,
errors: $errors errors: $errors
)) { )) {
// Found parameters // Found parameters

View File

@ -39,12 +39,14 @@ $router = new router;
// Initialize routes // Initialize routes
$router $router
->write('/', 'catalog', 'index', 'GET') ->write('/', 'catalog', 'index', 'GET')
->write('/search', 'catalog', 'search', 'POST') ->write('/', 'catalog', 'index', 'POST')
->write('/account/write', 'account', 'write', 'POST')
->write('/session/write', 'session', 'write', 'POST') ->write('/session/write', 'session', 'write', 'POST')
->write('/session/connect/telegram', 'session', 'telegram', 'POST') ->write('/session/connect/telegram', 'session', 'telegram', 'POST')
->write('/category/$identifier', 'catalog', 'index', 'POST') /* ->write('/category/$identifier', 'catalog', 'index', 'POST') */
->write('/category', 'catalog', 'index', 'POST') /* ->write('/category', 'catalog', 'index', 'POST') */
->write('/product/$identifier', 'catalog', 'product', 'POST'); /* ->write('/product/$identifier', 'catalog', 'product', 'POST') */
;
/* /*

View File

@ -72,9 +72,10 @@ import("/js/core.js").then(() =>
}, 3000); }, 3000);
if (core.telegram.api.initData.length > 0) { if (core.telegram.api.initData.length > 0) {
core.request( core
.request(
"/session/connect/telegram", "/session/connect/telegram",
core.telegram.api.initData, core.telegram.api.initData
) )
.then((json) => { .then((json) => {
if ( if (
@ -93,9 +94,8 @@ import("/js/core.js").then(() =>
const a = const a =
core.status_account.getElementsByTagName("a")[0]; core.status_account.getElementsByTagName("a")[0];
a.setAttribute("onclick", "core.account.profile()"); a.setAttribute("onclick", "core.account.profile()");
a.innerText = json.domain.length > 0 a.innerText =
? "@" + json.domain json.domain.length > 0 ? "@" + json.domain : "ERROR";
: "ERROR";
} }
if ( if (
@ -109,6 +109,34 @@ import("/js/core.js").then(() =>
}); });
} }
} }
/**
* Buffer
*/
static buffer = class buffer {
/**
* Write to the account buffer
*
* @param {string} name Name of the parameter
* @param {string} value Value of the parameter (it can be JSON)
*
* @return {void}
*/
static write(name, value) {
if (typeof name === "string" && typeof value === "string") {
//
// Send request to the server
core.request(
"/account/write",
`${name}=${value}`,
"POST",
{},
null
);
}
}
};
}; };
} }
} }

View File

@ -23,10 +23,20 @@ import("/js/core.js").then(() =>
}, 5000); }, 5000);
function initialization() { function initialization() {
core.session.telegram(); //
const { initData, initDataUnsafe, ...data } = core.telegram.api;
//
core.session.buffer.write("telegram_program", JSON.stringify(data));
if (core.telegram.api.initData.length > 0) { if (core.telegram.api.initData.length > 0) {
//
//
core.account.authentication(); core.account.authentication();
} }
//
core.telegram.api.ready(); core.telegram.api.ready();
} }
}) })

View File

@ -29,81 +29,113 @@ import("/js/core.js").then(() =>
// Write to the core // Write to the core
core.catalog = class catalog { core.catalog = class catalog {
/** /**
* Registry of filters (instead of cookies) * Parameters of search
*
* Will be converted to the body of the GET request ("?name=value&name2=value2")
*/ */
static filters = new Map([ static parameters = new Map([
["text", null],
["category", null],
["sort", null],
["brand", null], ["brand", null],
]); ]);
/** /**
* Select a category (interface) * Search (interface)
* *
* @param {HTMLElement} button Button of a category <a> * @param {Event} event Event (keyup)
* @param {bool} clean Clear search bar? * @param {HTMLElement} element Search bar <input>
* @param {bool} force Ignore the damper? * @param {bool} force Ignore the damper?
* *
* @return {void} * @return {void}
*/ */
static category(button, clean = true, force = false) { static search(event, element, force = false) {
// Initializing identifier of the category if (typeof element === "undefined") {
const identifier = button.getAttribute( element = document.getElementById("search");
"data-category-identifier", }
);
this._category(identifier, clean, force); element.classList.remove("error");
if (element.value.length === 1) {
return;
} else {
if (typeof event === "undefined") {
//
this._search(element, true);
} else if (event.keyCode === 13) {
// Button: "enter"
element.setAttribute("disabled", true);
this._search(element, force);
} else {
// Button: any
this._search(element, force);
}
}
} }
/** /**
* Select a category (damper) * Search (damper)
* *
* @param {HTMLElement} button Button of category <a> * @param {HTMLElement} element Search bar <input>
* @param {bool} clean Clear search bar?
* @param {bool} force Ignore the damper? * @param {bool} force Ignore the damper?
* *
* @return {void} * @return {void}
*/ */
static _category = core.damper( static _search = core.damper(
(...variables) => this.__category(...variables), (...variables) => this.__search(...variables),
400, 1400,
2, 2,
); );
/** /**
* Select a category (system) * Search (system)
* *
* @param {string} identifier Identifier of the category * @param {HTMLElement} element Search bar <input>
* @param {bool} clean Clear search bar?
* *
* @return {Promise} Request to the server * @return {Promise} Request to the server
*
* @todo add animations of errors
*/ */
static __category(identifier, clean = true) { static __search(element) {
if (typeof identifier === "string") { if (typeof element === "undefined") {
// Received required parameters element = document.getElementById("search");
}
const urn = Array.from(this.parameters)
.filter(([k, v]) =>
typeof v === "string" || typeof v === "number"
)
.map(([k, v]) => `${k}=${v}`)
.join("&");
return core.request( return core.request(
"/category" + "/",
("/" + identifier).replace(/^\/*/, "/").trim().replace( urn,
/\/*$/,
"",
),
) )
.then((json) => { .then((json) => {
element.removeAttribute("disabled");
element.focus();
if ( if (
json.errors !== null && json.errors !== null &&
typeof json.errors === "object" && typeof json.errors === "object" &&
json.errors.length > 0 json.errors.length > 0
) { ) {
// Errors received // Errors received
element.classList.add("error");
} else { } else {
// Errors not received // Errors not received
if (clean) { history.pushState(
// Clearing the search bar {},
const search = core.main.querySelector( urn,
'search[data-section="search"]>input', urn,
); );
if (search instanceof HTMLElement) search.value = "";
}
if ( if (
typeof json.title === "string" && typeof json.title === "string" &&
@ -118,6 +150,12 @@ import("/js/core.js").then(() =>
title.innerText = json.title; title.innerText = json.title;
} }
// Deinitialization of the categories
const categories = core.main.querySelector(
'section[data-catalog-type="categories"]',
);
// if (categories instanceof HTMLElement) categories.remove();
if ( if (
typeof json.html.categories === "string" && typeof json.html.categories === "string" &&
json.html.categories.length > 0 json.html.categories.length > 0
@ -164,179 +202,6 @@ import("/js/core.js").then(() =>
} }
} }
if (
typeof json.html.filters === "string" &&
json.html.filters.length > 0
) {
// Received filters (reinitialization of the filters)
const filters = core.main.querySelector(
'section[data-section="filters"]',
);
if (filters instanceof HTMLElement) {
// Found list of filters
filters.outerHTML = json.html.filters;
} else {
// Not found list of categories
const element = document.createElement("section");
const categories = core.main.querySelector(
'section[data-catalog-type="categories"]',
);
if (categories instanceof HTMLElement) {
core.main.insertBefore(
element,
categories.nextSibling,
);
} else {
core.main.append(element);
}
element.outerHTML = json.html.filters;
}
} else {
// Not received categories (deinitialization of the categories)
const filters = core.main.querySelector(
'section[data-section="filters"',
);
if (filters instanceof HTMLElement) {
filters.remove();
}
}
if (
typeof json.html.products === "string" &&
json.html.products.length > 0
) {
// Received products (reinitialization of the products)
const products = core.main.querySelector(
'section[data-catalog-type="products"]',
);
if (products instanceof HTMLElement) {
// Found list of products
products.outerHTML = json.html.products;
} else {
// Not found list of products
const element = document.createElement("section");
core.main.append(element);
element.outerHTML = json.html.products;
}
} else {
// Not received products (deinitialization of the products)
const products = core.main.querySelector(
'section[data-catalog-type="products"',
);
if (products instanceof HTMLElement) {
products
.remove();
}
}
}
});
}
}
/**
* Select a category (interface)
*
* @param {Event} event Event (keyup)
* @param {HTMLElement} element Search bar <input>
* @param {bool} force Ignore the damper?
*
* @return {void}
*/
static search(event, element, force = false) {
element.classList.remove("error");
if (element.value.length === 1) {
return;
} else if (event.keyCode === 13) {
// Button: "enter"
element.setAttribute("disabled", true);
this._search(element, force);
} else {
// Button: any
this._search(element, force);
}
}
/**
* Search in the catalog (damper)
*
* @param {HTMLElement} button Button of category <a>
* @param {bool} clean Clear search bar?
* @param {bool} force Ignore the damper?
*
* @return {void}
*/
static _search = core.damper(
(...variables) => this.__search(...variables),
1400,
2,
);
/**
* Search in the catalog (system)
*
* @param {HTMLElement} element Search bar <input>
*
* @return {Promise} Request to the server
*
* @todo add animations of errors
*/
static __search(element) {
return this.__category("/", false)
.then(function () {
core.request("/search", `text=${element.value}`)
.then((json) => {
element.removeAttribute("disabled");
element.focus();
if (
json.errors !== null &&
typeof json.errors === "object" &&
json.errors.length > 0
) {
// Errors received
element.classList.add("error");
} else {
// Errors not received
if (
typeof json.title === "string" &&
json.title.length > 0
) {
// Received the page title
// Initialize a link to the categories list
const title =
core.main.getElementsByTagName("h2")[0];
// Write the title
title.innerText = json.title;
}
// Deinitialization of the categories
const categories = core.main.querySelector(
'section[data-catalog-type="categories"]',
);
// if (categories instanceof HTMLElement) categories.remove();
if ( if (
typeof json.html.products === "string" && typeof json.html.products === "string" &&
json.html.products.length > 0 json.html.products.length > 0
@ -361,6 +226,9 @@ import("/js/core.js").then(() =>
); );
if (categories instanceof HTMLElement) { if (categories instanceof HTMLElement) {
//
//
core.main.insertBefore( core.main.insertBefore(
element, element,
categories.nextSibling, categories.nextSibling,
@ -368,6 +236,9 @@ import("/js/core.js").then(() =>
element.outerHTML = json.html.products; element.outerHTML = json.html.products;
} else { } else {
//
//
const search = core.main.querySelector( const search = core.main.querySelector(
'search[data-section="search"]', 'search[data-section="search"]',
); );
@ -388,13 +259,13 @@ import("/js/core.js").then(() =>
const products = core.main.querySelector( const products = core.main.querySelector(
'section[data-catalog-type="products"]', 'section[data-catalog-type="products"]',
); );
if (products instanceof HTMLElement) { if (products instanceof HTMLElement) {
products.remove(); products.remove();
} }
} }
} }
}); });
});
} }
/** /**
@ -439,7 +310,10 @@ import("/js/core.js").then(() =>
if (typeof identifier === "string") { if (typeof identifier === "string") {
// //
return core.request(`/product/${identifier}`) //
const urn = `?product=${identifier}`;
return core.request(urn)
.then((json) => { .then((json) => {
if ( if (
json.errors !== null && json.errors !== null &&
@ -471,7 +345,7 @@ import("/js/core.js").then(() =>
const name = document.createElement("span"); const name = document.createElement("span");
name.classList.add("name"); name.classList.add("name");
name.setAttribute("title", json.product.identifier); name.setAttribute("title", identifier);
name.innerText = json.product.name; name.innerText = json.product.name;
const exit = document.createElement("a"); const exit = document.createElement("a");
@ -671,8 +545,9 @@ import("/js/core.js").then(() =>
); );
history.pushState( history.pushState(
{ product_card: json.product.identifier }, { identifier },
json.product.name, json.product.name,
urn,
); );
// блокировка закрытия карточки // блокировка закрытия карточки
@ -681,7 +556,12 @@ import("/js/core.js").then(() =>
wrap.addEventListener("mousedown", _from); wrap.addEventListener("mousedown", _from);
wrap.addEventListener("touchstart", _from); wrap.addEventListener("touchstart", _from);
const remove = () => { const remove = (event) => {
if (
typeof event === "undefined" ||
event.type !== "popstate"
) history.back();
wrap.remove(); wrap.remove();
images.removeEventListener( images.removeEventListener(
"mousedown", "mousedown",

View File

@ -46,13 +46,43 @@ const core = class core {
method = "POST", method = "POST",
headers = { headers = {
"Content-Type": "application/x-www-form-urlencoded", "Content-Type": "application/x-www-form-urlencoded",
"Accept": "application/json",
}, },
type = "json", type = "json",
) { ) {
return await fetch(encodeURI(address), { method, headers, body }) return await fetch(encodeURI(address), { method, headers, body })
.then((response) => response[type]()); .then((response) => type === null || response[type]());
} }
/**
* Buffer
*/
static buffer = class buffer {
/**
* Write to buffers
*
* @param {string} name Name of the parameter
* @param {string} value Value of the parameter (it can be JSON)
*
* @return {void}
*/
static write(name, value) {
if (typeof this.session === "function") {
// Initialized the session implement object
// Write to the session buffer
core.session.buffer.write(name, value);
}
if (typeof this.account === "function") {
// Initialized the account implement object
// Write to the account buffer
core.account.buffer.write(name, value);
}
}
};
/** /**
* Сгенерировать окно выбора действия * Сгенерировать окно выбора действия
* *

View File

@ -27,19 +27,32 @@ import("/js/core.js").then(() =>
// Write to the core // Write to the core
core.session = class session { core.session = class session {
/** /**
* Send data of Telegram program settings to the session * Buffer
*/
static buffer = class buffer {
/**
* Write to the session buffer
*
* @param {string} name Name of the parameter
* @param {string} value Value of the parameter (it can be JSON)
* *
* @return {void} * @return {void}
*/ */
static telegram() { static write(name, value) {
if (typeof name === "string" && typeof value === "string") {
// //
const { initData, initDataUnsafe, ...data } = core.telegram.api;
// Send request to the server
core.request( core.request(
"/session/write", "/session/write",
"telegram=" + JSON.stringify(data), `${name}=${value}`,
"POST",
{},
null
); );
} }
}
};
}; };
} }
} }

View File

@ -0,0 +1,37 @@
@charset "UTF-8";
i.icon.arrow.top.left {
box-sizing: border-box;
position: relative;
display: block;
width: 22px;
height: 22px;
border: 2px solid;
border-radius: 20px;
}
i.icon.arrow.top.left::after,
i.icon.arrow.top.left::before {
content: "";
display: block;
box-sizing: border-box;
position: absolute;
}
i.icon.arrow.top.left::after {
width: 10px;
height: 2px;
background: currentColor;
transform: rotate(45deg);
bottom: 8px;
right: 4px;
}
i.icon.arrow.top.left::before {
width: 6px;
height: 6px;
left: 4px;
top: 4px;
border-top: 2px solid;
border-left: 2px solid;
}

View File

@ -4,7 +4,6 @@ i.icon.shopping.cart {
display: block; display: block;
box-sizing: border-box; box-sizing: border-box;
position: relative; position: relative;
transform: scale(var(--ggs, 1));
width: 20px; width: 20px;
height: 21px; height: 21px;
background: background:

View File

@ -25,6 +25,9 @@ a {
} }
body { body {
--gap: 16px;
--width: calc(100% - var(--gap) * 2);
--offset-x: 2%;
width: 100%; width: 100%;
height: 100%; height: 100%;
margin: 0; margin: 0;
@ -39,10 +42,20 @@ body {
aside {} aside {}
header {} header {
container-type: inline-size;
container-name: header;
margin: 2rem 0 1rem;
padding: 0 var(--offset-x);
display: flex;
flex-direction: column;
align-items: center;
gap: 26px;
}
main { main {
--offset-x: 2%; container-type: inline-size;
container-name: main;
padding: 0 var(--offset-x); padding: 0 var(--offset-x);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -56,8 +69,6 @@ main>section:last-child {
} }
main>*[data-section] { main>*[data-section] {
--gap: 16px;
--width: calc(100% - var(--gap) * 2);
width: var(--width); width: var(--width);
} }
@ -150,7 +161,7 @@ a[type="button"] {
h1, h1,
h2 { h2 {
margin: 28px 0 0; margin: 1rem 0 0;
} }
footer {} footer {}

View File

@ -0,0 +1,53 @@
@charset "UTF-8";
header>nav#menu {
container-type: inline-size;
container-name: menu;
width: var(--width);
min-height: 3rem;
display: flex;
flex-flow: row wrap;
gap: 1rem;
border-radius: 1.375rem;
overflow: hidden;
}
header>nav#menu>a[type="button"] {
height: 3rem;
padding: unset;
border-radius: 1.375rem;
color: var(--unsafe-color, var(--tg-theme-button-text-color));
background-color: var(--unsafe-background-color, var(--tg-theme-button-color));
}
header>nav#menu>a[type=button]>:first-child {
margin-left: 1rem;
}
header>nav#menu>a[type="button"]>* {
margin-right: 1rem;
}
@container header (max-width: 450px) {
header>nav#menu > a[type="button"]:nth-child(1)> i.icon+span {
display: none;
}
}
@container header (max-width: 350px) {
header>nav#menu > a[type="button"]:nth-child(2)> i.icon+span {
display: none;
}
}
@container header (max-width: 250px) {
header>nav#menu > a[type="button"]:nth-child(3)> i.icon+span {
display: none;
}
}
@container header (max-width: 150px) {
header>nav#menu > a[type="button"]> i.icon+span {
display: none;
}
}

View File

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

View File

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

View File

@ -1,4 +1,6 @@
<search data-section="search"> <search data-section="search">
<label><i class="icon search"></i></label> <label><i class="icon search"></i></label>
<input id="search" placeholder="Поиск по каталогу" type="search" onkeyup="event.keyCode !== 9 && core.catalog.search(event, this)" tabindex="1"/> {% set search = account.buffer.search ?? session.buffer.search %}
<input id="search" placeholder="Поиск по каталогу" type="search" tabindex="1"
onkeyup="event.keyCode === 9 || core.catalog.parameters.set('text', this.value); core.catalog.search(event, this)"{% if search is not empty %} value="{{ search }}"{% endif %} />
</search> </search>

View File

@ -1,10 +1,15 @@
{% use "/themes/default/menu.html" with css as menu_css, body as menu_body, js as menu_js %}
{% block css %} {% block css %}
{{ block('menu_css') }}
{% endblock %} {% endblock %}
{% block body %} {% block body %}
<header> <header>
{{ block('menu_body') }}
</header> </header>
{% endblock %} {% endblock %}
{% block js %} {% block js %}
{{ block('menu_js') }}
{% endblock %} {% endblock %}

View File

@ -7,16 +7,16 @@
{% block css %} {% block css %}
{{ parent() }} {{ parent() }}
{{ block('header_css') }}
{{ block('connection_css') }} {{ block('connection_css') }}
{{ block('account_css') }} {{ block('account_css') }}
{{ block('header_css') }}
{{ block('footer_css') }} {{ block('footer_css') }}
{% endblock %} {% endblock %}
{% block body %} {% block body %}
{{ block('header') }}
{{ block('connection_body') }} {{ block('connection_body') }}
{{ block('account_body') }} {{ block('account_body') }}
{{ block('header') }}
<main> <main>
{% block main %} {% block main %}
{{ main|raw }} {{ main|raw }}
@ -27,8 +27,8 @@
{% block js %} {% block js %}
{{ parent() }} {{ parent() }}
{{ block('header_js') }}
{{ block('connection_js') }} {{ block('connection_js') }}
{{ block('account_js') }} {{ block('account_js') }}
{{ block('header_js') }}
{{ block('footer_js') }} {{ block('footer_js') }}
{% endblock %} {% endblock %}

View File

@ -0,0 +1,30 @@
{% block css %}
<link type="text/css" rel="stylesheet" href="/themes/default/css/menu.css">
{% for button in menu %}
{% if button.icon %}
<link type="text/css" rel="stylesheet" href="/themes/default/css/icons/{{ button.icon|replace({' ': '-'}) }}.css">
{% endif %}
{% endfor %}
{% endblock %}
{% block body %}
<nav id="menu">
{% for button in menu %}
<a href='{{ button.urn }}' onclick="return core.loader.load('{{ button.urn }}');" type="button" class="unselectable"
title="{{ button.name }}"
style="order: {{ button.position }};{% for target, color in button.color %} --unsafe-{{ target }}: {{ color|e }};{% endfor %}">
{% if button.icon %}
<i class="icon {{ button.icon }}"></i>
{% endif %}
<span>{{ button.name }}</span>
{% if button.image.storage %}
<img src="{{ button.image.storage }}" alt="{{ button.name }}" ondrugstart="return false;">
{% endif %}
</a>
{% endfor %}
</nav>
{% endblock %}
{% block js %}
<script type="text/javascript" src="/js/menu.js"></script>
{% endblock %}

View File

@ -1,14 +0,0 @@
{% extends "/themes/default/index.html" %}
{% block css %}
{{ parent() }}
<link type="text/css" rel="stylesheet" href="/themes/{{ theme }}/css/catalog.css" />
{% endblock %}
{% block main %}
{% endblock %}
{% block js %}
{{ parent() }}
<script src="/js/catalog.js" defer></script>
{% endblock %}