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,
mirzaev\arming_bot\models\entry,
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
@ -24,6 +28,7 @@ final class catalog extends core
protected array $errors = [
'session' => [],
'account' => [],
'menu' => [],
'catalog' => []
];
@ -34,64 +39,160 @@ final class catalog extends core
*/
public function index(array $parameters = []): ?string
{
// Initializing identifier of a category
preg_match('/[\d]+/', $parameters['identifier'] ?? '', $matches);
$identifier = $matches[0] ?? null;
// Validating
if (!empty($parameters['category']) && preg_match('/[\d]+/', $parameters['category'], $matches)) $category = (int) $matches[0];
// Initializint the buffer of respnse
$html = [];
if (isset($category)) {
// Received and validated identifier of the category
if (!empty($parameters['identifier'])) {
// Передана категория (идентификатор)
// Инициализация актуальной категории
$category = category::_read('d.identifier == @identifier', parameters: ['identifier' => (int) $identifier], errors: $this->errors['catalog']);
// 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: 30,
errors: $this->errors['catalog']
);
// Объявление буферов категорий и товаров (важно - в единственном числе, по параметру из базы данных)
// Initialize buffers of entries (in singular, by parameter from ArangoDB)
$category = $product = [];
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;
}
// Запись категорий из буфера в глобальную переменную шаблонизатора
// 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;
// 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 = [
'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']
)
];
// 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" (самый верхний уровень)
$this->view->categories = entry::ascendants(descendant: new category, errors: $this->errors['catalog']);
if (isset($menu)) {
//
} 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') {
@ -102,6 +203,40 @@ final class catalog extends core
} else if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// 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
header('Content-Type: application/json');
header('Content-Encoding: none');
@ -112,11 +247,7 @@ final class catalog extends core
// Generating the reponse
echo json_encode(
[
'title' => $title ?? '',
'html' => $html + [
'categories' => $this->view->render('catalog/elements/categories.html'),
],
$response + [
'errors' => $this->errors
]
);
@ -135,122 +266,4 @@ final class catalog extends core
// Exit (fail)
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;
}
/**
* Connect session to the telegram account
* Write to the buffer
*
* @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) {
// Found data of the program and active session
@ -171,18 +171,15 @@ final class session extends core
// Validation of the parameter
if (mb_strlen($value) > 4096) continue;
// Write data of the program to the implement object of session document from ArangoDB
$this->session->{$name} = json_decode($value, true, 10);
// 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 session document from ArangoDB
$this->session->buffer = $parameter + ($this->session->buffer ?? []);
}
// Write from implement object to session document from ArangoDB
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
*
* @param string $return Return (AQL path)
* @param array $products Array with products system identifiers ["_id", "_id", "_id"...]
* @param array &$errors Registry of errors
*
* @return array Array with found unique parameter values from all products (can be empty)
*/
public static function collect(
string $return = 'd._key',
array $products = [],
language $language = language::en,
array $parameters = [],
array &$errors = []
): array {
try {
@ -230,13 +234,16 @@ final class product extends core
sprintf(
<<<'AQL'
FOR d IN @@collection
%s
RETURN DISTINCT %s
AQL,
empty($products) ? '' : 'FILTER POSITION(["' . implode('", "', $products) . '"], d._id)',
empty($return) ? 'd._key' : $return
),
[
'@collection' => static::COLLECTION,
],
'language' => $language->name,
] + $parameters,
errors: $errors
)) {
// Found parameters

View File

@ -39,12 +39,14 @@ $router = new router;
// Initialize routes
$router
->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/connect/telegram', 'session', 'telegram', 'POST')
->write('/category/$identifier', 'catalog', 'index', 'POST')
->write('/category', 'catalog', 'index', 'POST')
->write('/product/$identifier', 'catalog', 'product', 'POST');
/* ->write('/category/$identifier', 'catalog', 'index', 'POST') */
/* ->write('/category', 'catalog', 'index', 'POST') */
/* ->write('/product/$identifier', 'catalog', 'product', 'POST') */
;
/*

View File

@ -72,9 +72,10 @@ import("/js/core.js").then(() =>
}, 3000);
if (core.telegram.api.initData.length > 0) {
core.request(
core
.request(
"/session/connect/telegram",
core.telegram.api.initData,
core.telegram.api.initData
)
.then((json) => {
if (
@ -93,9 +94,8 @@ import("/js/core.js").then(() =>
const a =
core.status_account.getElementsByTagName("a")[0];
a.setAttribute("onclick", "core.account.profile()");
a.innerText = json.domain.length > 0
? "@" + json.domain
: "ERROR";
a.innerText =
json.domain.length > 0 ? "@" + json.domain : "ERROR";
}
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);
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) {
//
//
core.account.authentication();
}
//
core.telegram.api.ready();
}
})

View File

@ -29,81 +29,113 @@ import("/js/core.js").then(() =>
// Write to the core
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],
]);
/**
* Select a category (interface)
* Search (interface)
*
* @param {HTMLElement} button Button of a category <a>
* @param {bool} clean Clear search bar?
* @param {Event} event Event (keyup)
* @param {HTMLElement} element Search bar <input>
* @param {bool} force Ignore the damper?
*
* @return {void}
*/
static category(button, clean = true, force = false) {
// Initializing identifier of the category
const identifier = button.getAttribute(
"data-category-identifier",
);
static search(event, element, force = false) {
if (typeof element === "undefined") {
element = document.getElementById("search");
}
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 {bool} clean Clear search bar?
* @param {HTMLElement} element Search bar <input>
* @param {bool} force Ignore the damper?
*
* @return {void}
*/
static _category = core.damper(
(...variables) => this.__category(...variables),
400,
static _search = core.damper(
(...variables) => this.__search(...variables),
1400,
2,
);
/**
* Select a category (system)
* Search (system)
*
* @param {string} identifier Identifier of the category
* @param {bool} clean Clear search bar?
* @param {HTMLElement} element Search bar <input>
*
* @return {Promise} Request to the server
*
* @todo add animations of errors
*/
static __category(identifier, clean = true) {
if (typeof identifier === "string") {
// Received required parameters
static __search(element) {
if (typeof element === "undefined") {
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(
"/category" +
("/" + identifier).replace(/^\/*/, "/").trim().replace(
/\/*$/,
"",
),
"/",
urn,
)
.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 (clean) {
// Clearing the search bar
const search = core.main.querySelector(
'search[data-section="search"]>input',
history.pushState(
{},
urn,
urn,
);
if (search instanceof HTMLElement) search.value = "";
}
if (
typeof json.title === "string" &&
@ -118,6 +150,12 @@ import("/js/core.js").then(() =>
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 (
typeof json.html.categories === "string" &&
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 (
typeof json.html.products === "string" &&
json.html.products.length > 0
@ -361,6 +226,9 @@ import("/js/core.js").then(() =>
);
if (categories instanceof HTMLElement) {
//
//
core.main.insertBefore(
element,
categories.nextSibling,
@ -368,6 +236,9 @@ import("/js/core.js").then(() =>
element.outerHTML = json.html.products;
} else {
//
//
const search = core.main.querySelector(
'search[data-section="search"]',
);
@ -388,13 +259,13 @@ import("/js/core.js").then(() =>
const products = core.main.querySelector(
'section[data-catalog-type="products"]',
);
if (products instanceof HTMLElement) {
products.remove();
}
}
}
});
});
}
/**
@ -439,7 +310,10 @@ import("/js/core.js").then(() =>
if (typeof identifier === "string") {
//
return core.request(`/product/${identifier}`)
//
const urn = `?product=${identifier}`;
return core.request(urn)
.then((json) => {
if (
json.errors !== null &&
@ -471,7 +345,7 @@ import("/js/core.js").then(() =>
const name = document.createElement("span");
name.classList.add("name");
name.setAttribute("title", json.product.identifier);
name.setAttribute("title", identifier);
name.innerText = json.product.name;
const exit = document.createElement("a");
@ -671,8 +545,9 @@ import("/js/core.js").then(() =>
);
history.pushState(
{ product_card: json.product.identifier },
{ identifier },
json.product.name,
urn,
);
// блокировка закрытия карточки
@ -681,7 +556,12 @@ import("/js/core.js").then(() =>
wrap.addEventListener("mousedown", _from);
wrap.addEventListener("touchstart", _from);
const remove = () => {
const remove = (event) => {
if (
typeof event === "undefined" ||
event.type !== "popstate"
) history.back();
wrap.remove();
images.removeEventListener(
"mousedown",

View File

@ -46,13 +46,43 @@ const core = class core {
method = "POST",
headers = {
"Content-Type": "application/x-www-form-urlencoded",
"Accept": "application/json",
},
type = "json",
) {
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
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}
*/
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(
"/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;
box-sizing: border-box;
position: relative;
transform: scale(var(--ggs, 1));
width: 20px;
height: 21px;
background:

View File

@ -25,6 +25,9 @@ a {
}
body {
--gap: 16px;
--width: calc(100% - var(--gap) * 2);
--offset-x: 2%;
width: 100%;
height: 100%;
margin: 0;
@ -39,10 +42,20 @@ body {
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 {
--offset-x: 2%;
container-type: inline-size;
container-name: main;
padding: 0 var(--offset-x);
display: flex;
flex-direction: column;
@ -56,8 +69,6 @@ main>section:last-child {
}
main>*[data-section] {
--gap: 16px;
--width: calc(100% - var(--gap) * 2);
width: var(--width);
}
@ -150,7 +161,7 @@ a[type="button"] {
h1,
h2 {
margin: 28px 0 0;
margin: 1rem 0 0;
}
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"
data-catalog-level="{{ level ?? 0 }}">
{% 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 %}
<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">
<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>
<img src="{{ category.images.0.storage }}" alt="{{ category.name[language] }}" ondrugstart="return false;">
{% endif %}
<p>{{ category.name[language] }}</p>
</a>
{% endfor %}
</section>
{% endif %}

View File

@ -1,12 +1,15 @@
{% if filters is not empty %}
<section class="unselectble" data-section="filters">
<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>
<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>
{% 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>
{% endfor %}
</section>

View File

@ -1,4 +1,6 @@
<search data-section="search">
<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>

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('menu_css') }}
{% endblock %}
{% block body %}
<header>
{{ block('menu_body') }}
</header>
{% endblock %}
{% block js %}
{{ block('menu_js') }}
{% endblock %}

View File

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