Compare commits
No commits in common. "stable" and "0.3.1" have entirely different histories.
|
@ -1,2 +1 @@
|
||||||
node_modules
|
|
||||||
vendor
|
vendor
|
||||||
|
|
355
README.md
355
README.md
|
@ -1,160 +1,42 @@
|
||||||
# huesos
|
# huesos
|
||||||
Base for creating shop chat-robots using Web App technology for [Telegram](https://telegram.org)
|
|
||||||
|
|
||||||
## Functions
|
Basis for developing chat-robots with "Web App" technology for Telegram
|
||||||
1. Tree-structured catalog
|
|
||||||
2. Product cards with images carousel ([mirzaev/hotline.mjs](https://git.svoboda.works/mirzaev/hotline.mjs))
|
|
||||||
3. Cart (CRUD with limits and session binding)
|
|
||||||
4. Saving user data (and session) for all devices
|
|
||||||
5. Deliveries settings (with interactive maps and automatic geolocation detection on smartphones)
|
|
||||||
6. Real time price generation
|
|
||||||
7. Interface according to all Telegram standards
|
|
||||||
8. Public offer, dynamic settings and suspensions
|
|
||||||
9. Multi-language and easy to add new languages
|
|
||||||
10. Multi-currency and easy to add new currencies
|
|
||||||
11. Loading products and categories from an excel-file with automatic updating of existing ones
|
|
||||||
12. Flag authorization system, separate access for testers
|
|
||||||
13. Sending the generated order directly to the chat-robot
|
|
||||||
14. Intelligent search by titles, descriptions and other parameters (Levenshtein algorithm + separate settings for different languages)
|
|
||||||
15. Asynchronous chat-robot and Web App based on dynamic queries (AJAX)
|
|
||||||
16. Modern non-relational database ready for scaling and integration with third-party CRM
|
|
||||||
17. Fully documented code in English
|
|
||||||
18. Customizable menu buttons
|
|
||||||
19. Responsive design with built-in Telegram buttons and haptic functions
|
|
||||||
20. Automatic download and compression of images in 4 sizes (currently only from Yandex.Disk, but the system is ready to add new sources)
|
|
||||||
21. Commercially approved fonts and pure CSS icons
|
|
||||||
22. Product filter panel using pure CSS
|
|
||||||
23. Damper technology on all user interaction functions ([mirzaev/damper.mjs](https://git.svoboda.works/mirzaev/damper.mjs))
|
|
||||||
24. Two-step registration system (entering other data after creating an order)
|
|
||||||
25. Delivery company selection system (ready for scaling)
|
|
||||||
26. Acquiring company selection system (ready for scaling)
|
|
||||||
27. Sending paid orders to the operators chat with the customer contacts
|
|
||||||
|
|
||||||
## Integrations
|
|
||||||
|
|
||||||
### Import
|
|
||||||
*Methods for importing products into the shop*<br>
|
|
||||||
1. Excel-file (products and categories)
|
|
||||||
|
|
||||||
### Images download
|
|
||||||
*Methods of transferring images when importing products into the shop*<br>
|
|
||||||
1. [Yandex.Disk](https://360.yandex.ru/disk/) (russian) ([API](https://yandex.com/dev/disk/))
|
|
||||||
|
|
||||||
### Delivery companies
|
|
||||||
*Companies that deliver products from the shop*<br>
|
|
||||||
1. [CDEK](https://www.cdek.ru/) (russian) ([API](https://api-docs.cdek.ru/29923741.html)) ([PHP library](https://github.com/TTATPuOT/cdek-sdk2.0))
|
|
||||||
|
|
||||||
### Acquiring companies
|
|
||||||
*Companies that provide acquiring for the shop*<br>
|
|
||||||
1. [Robokassa](https://robokassa.com) (russian) (no swift) ([API](https://docs.robokassa.ru/pay-interface/))
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
1. [PHP 8.4](https://www.php.net/releases/8.4/en.php)
|
|
||||||
2. [Composer](https://getcomposer.org/) (php package manager)
|
|
||||||
3. [MINIMAL](https://git.svoboda.works/mirzaev/minimal) (PHP framework)
|
|
||||||
4. [Twig](https://twig.symfony.com/) (HTML templater)
|
|
||||||
5. [Zanzara](https://github.com/badfarm/zanzara) (Telegram framework + ReactPHP)
|
|
||||||
6. [ArangoDB](https://docs.arangodb.com/3.11/about-arangodb/) (non-relational database)
|
|
||||||
7. [NGINX](https://nginx.org/en/) (web server) *(can be replaced)*
|
|
||||||
8. [SystemD](https://systemd.io/) (service manager) *(can be replaced)*
|
|
||||||
|
|
||||||
<small>You can find other dependencies in the file `/composer.json`</small>
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
### AnangoDB
|
### AnangoDB
|
||||||
|
|
||||||
1. **Configure unix-socket**<br>
|
1. Create a Graph with the specified values
|
||||||
|
|
||||||
Edit the file `/etc/arangodb3/arangod.conf`<br>
|
|
||||||
`endpoint = tcp://127.0.0.1:8529` -> `endpoint = unix:///var/run/arangodb3/arango.sock` (this will disable the web panel)<br>
|
|
||||||
<br>
|
|
||||||
To make the web panel work, you can add this to the NGINX server settings:
|
|
||||||
```lua
|
|
||||||
server {
|
|
||||||
...
|
|
||||||
|
|
||||||
server_name arangodb.domain.zone;
|
|
||||||
|
|
||||||
...
|
|
||||||
|
|
||||||
allow YOUR_IP_ADDRESS;
|
|
||||||
allow 192.168.1.1/24;
|
|
||||||
allow 127.0.0.1;
|
|
||||||
deny all;
|
|
||||||
|
|
||||||
# ArangoDB
|
|
||||||
location / {
|
|
||||||
proxy_pass http://arangodb;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
upstream arangodb {
|
|
||||||
server unix:/var/run/arangodb3/arango.sock;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
[here is my solution for "permission denied" problem on Ubuntu (accepted by ArangoDB maintainer)](https://github.com/arangodb/arangodb/issues/17302)<br>
|
|
||||||
|
|
||||||
1. **Configure TCP (instead of unix-socket)**<br>
|
|
||||||
|
|
||||||
Edit the file `/etc/arangodb3/arangod.conf`<br>
|
|
||||||
`endpoint = tcp://127.0.0.1:8529` -> `endpoint = tcp://0.0.0.0:8529`<br>
|
|
||||||
|
|
||||||
Edit the file `mirzaev/huesos/system/settings/arangodb.php`<br>
|
|
||||||
`unix:///var/run/arangodb3/arango.sock` -> `tcp://YOUR_IP_ADDRESS:8529` (it is slow and not secure)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
2. **Create a Graph with the specified values**<br>
|
|
||||||
**Name:** catalog<br>
|
**Name:** catalog<br>
|
||||||
|
<br>
|
||||||
* Relation 1<br>
|
|
||||||
**edgeDefinition:** entry<br>
|
**edgeDefinition:** entry<br>
|
||||||
**fromCollections:** category, product<br>
|
**fromCollections:** categoy, product<br>
|
||||||
**toCollections:** category
|
**toCollections:** category
|
||||||
|
|
||||||
* Relation 2<br>
|
2. Create a Graph with the specified values
|
||||||
**edgeDefinition:** reservation<br>
|
**Name:** sessions<br>
|
||||||
**fromCollections:** product<br>
|
<br>
|
||||||
**toCollections:** cart
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
3. **Create a Graph with the specified values**<br>
|
|
||||||
**Name:** users<br>
|
|
||||||
|
|
||||||
* Relation 1<br>
|
|
||||||
**edgeDefinition:** connect<br>
|
**edgeDefinition:** connect<br>
|
||||||
**fromCollections:** cart, session<br>
|
**fromCollections:** account<br>
|
||||||
**toCollections:** account, session<br>
|
**toCollections:** session
|
||||||
|
|
||||||
* Orphan Collections<br>
|
3. Create indexes for the "product" collection
|
||||||
product
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
4. **Create indexes for the "product" collection**<br>
|
|
||||||
**Type:** "Inverted Index"<br>
|
**Type:** "Inverted Index"<br>
|
||||||
**Fields:** name.ru<br>
|
**Fields:** name.ru<br>
|
||||||
**Analyzer:** "text_ru"<br>
|
**Analyzer:** "text_ru"<br>
|
||||||
**Search field:** true<br>
|
**Search field:** true<br>
|
||||||
**Name:** name_ru<br><br>
|
**Name:** name_ru<br>
|
||||||
|
<br>
|
||||||
*Add indexes for all search parameters and for all languages (search language is selected based on the user's language, <br>
|
*Add indexes for all search parameters and for all languages (search language is selected based on the user's language, <br>
|
||||||
otherwise from the default language specified in the active settings from **settings** collection document)*<br>
|
otherwise from the default language specified in the active settings from **settings** collection document)*<br>
|
||||||
<br>
|
<br>
|
||||||
*See fields in the `mirzaev/arming_bot/models/product`<br>
|
*See fields in the `mirzaev/arming_bot/models/product`<br>
|
||||||
**name.ru**, **description.ru** and **compatibility.ru***<br>
|
**name.ru**, **description.ru** and **compatibility.ru***
|
||||||
|
|
||||||
---
|
4. Create a View with the specified values
|
||||||
|
|
||||||
5. **Create a View with the specified values**<br>
|
|
||||||
**type:** search-alias (you can also use "arangosearch")<br>
|
**type:** search-alias (you can also use "arangosearch")<br>
|
||||||
**name:** **product**s_search<br>
|
**name:** **product**s_search<br>
|
||||||
**indexes:**<br><br>
|
**indexes:**
|
||||||
|
|
||||||
You can copy an example of view file from here: `/examples/arangodb/views/products_search.json`
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"indexes": [
|
"indexes": [
|
||||||
{
|
{
|
||||||
|
@ -166,189 +48,58 @@ You can copy an example of view file from here: `/examples/arangodb/views/produc
|
||||||
|
|
||||||
### NGINX
|
### NGINX
|
||||||
|
|
||||||
1. **Create a NGINX server**<br>
|
1. Example of NGINX server file
|
||||||
You can copy an example of server file from here: `/examples/nginx/server.conf`
|
```nginx
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.php;
|
||||||
|
}
|
||||||
|
|
||||||
2. **Add support for javascript modules**<br>
|
location ~ /(?<type>categories|products) {
|
||||||
Edit the file `/etc/nginx/mime.types`<br>
|
root /var/www/arming_bot/mirzaev/arming_bot/system/storage;
|
||||||
`application/javascript js;` -> `application/javascript js mjs;`
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
3. **Generate a TLS/SSL sertificate** (via [certbot](http://certbot.eff.org/) for [ubuntu](https://ubuntu.com/))<br>
|
location ~ \.php$ {
|
||||||
3.1. `sudo apt install certbot python3-certbot-nginx`<br>
|
...
|
||||||
3.2. `sudo certbot certonly --nginx` (The **domain** must already be **bound** to the **IP-address** of the server by `A-record` or `AAAA-record`)
|
}
|
||||||
|
```
|
||||||
5. **Firewall rules for HTTP and HTTPS** (for [ubuntu](https://ubuntu.com/))<br>
|
|
||||||
4.1 `sudo ufw allow "NGINX Full"`<br>
|
|
||||||
4.1.1 `sudo ufw allow 22` (make sure that the port for SSH connection is open)<br>
|
|
||||||
4.2 `sudo ufw enable`
|
|
||||||
|
|
||||||
### SystemD (or any alternative you like)
|
### SystemD (or any alternative you like)
|
||||||
You can copy an example of systemd file from here: `/examples/systemd/huesos.service`<br>
|
|
||||||
1. `sudo cp huesos.service /etc/systemd/system/huesos.service && sudo chmod +x /etc/systemd/system/huesos.service`
|
1. Execute: `sudo cp telegram-huesos.service /etc/systemd/system/telegram-huesos.service`
|
||||||
2. `sudo systemctl daemon-reload`
|
|
||||||
3. `sudo systemctl enable huesos`<br>
|
|
||||||
|
|
||||||
*before you execute the command think about **what it does** and whether the **paths** are specified correctly*<br>
|
*before you execute the command think about **what it does** and whether the **paths** are specified correctly*<br>
|
||||||
*the configuration file is very simple and you can remake it for any alternative to SystemD that you like*
|
*the configuration file is very simple and you can remake it for any alternative to SystemD that you like*
|
||||||
|
|
||||||
## Menu
|
|
||||||
*Menu inside the Web App*<br><br>
|
|
||||||
Make sure you have a **menu** collection (can be created automatically)<br>
|
|
||||||
You can copy a clean menu documents without comments from here: `/examples/arangodb/collections/menu`
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"urn": "/", // Link
|
|
||||||
"name": {
|
|
||||||
"en": "Main page",
|
|
||||||
"ru": "Главная страница"
|
|
||||||
},
|
|
||||||
"style": { // The `style` attribute
|
|
||||||
"order": 0
|
|
||||||
},
|
|
||||||
"class": "",
|
|
||||||
"icon": { // Icon from `/themes/default/css/icons`
|
|
||||||
"style": { // The `style` attribute
|
|
||||||
"rotate": "-135deg"
|
|
||||||
},
|
|
||||||
"class": "arrow circle" // Classes of the icon
|
|
||||||
},
|
|
||||||
"image": { // Image at the background @deprecated?
|
|
||||||
"storage": null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Settings
|
## Settings
|
||||||
*Settings of chat-robot and Web App*<br><br>
|
Settings of chat-robot and Web App<br>
|
||||||
Make sure you have a **settings** collection (can be created automatically) and at least one document with the "status" parameter set to "active"<br>
|
<br>
|
||||||
You can copy a clean settings document without comments from here: `/examples/arangodb/collections/settings.json`
|
Make sure you have a **settings** collection (can be created automatically) and at least one document with the "status" parameter set to "active"
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"status": "active", // Values: "active", "inactive" (string) Status of the settings document?
|
"status": "active"
|
||||||
"project": {
|
|
||||||
"name": "PROJECT" // Name of the projext (string)
|
|
||||||
},
|
|
||||||
"language": "en", // Will be converted to an instance of enumeration `mirzaev\arming_bot\models\enumerations\language`
|
|
||||||
"currency": "usd", // Will be converted to an instance of enumeration `mirzaev\arming_bot\models\enumerations\currency`
|
|
||||||
"company": {
|
|
||||||
"identifier": null, // Example: "000000000000000" (string|null) (if `null` it will not be displayed)
|
|
||||||
"tax": null, // Example: "000000000000" (string|null) (if `null` it will not be displayed)
|
|
||||||
"name": null, // Example: "COMPANY" (string|null) (if `null` it will not be displayed)
|
|
||||||
"offer": false, // Display the data of a public offer in the footer? (bool) (does not affect the `/offer` page generation)
|
|
||||||
"sim": null, // Examples: "+7 000 000-00-00", "70000000000" (string|null) (if `null` it will not be displayed)
|
|
||||||
"mail": null // Example: "name@domain.zone" (string|null) (if `null` it will not be displayed)
|
|
||||||
},
|
|
||||||
"search": {
|
|
||||||
"enabled": true, // Enable the search input field?
|
|
||||||
"position": "fixed" // Values: "fixed", "relative"
|
|
||||||
},
|
|
||||||
"catalog": {
|
|
||||||
"categories": {
|
|
||||||
"display": "column", // Values: "row" (flex wrap), "column" (rows) Type of the catalog display
|
|
||||||
"structure": "lists", // Values: "pages" (pages with categories and products), "lists" (all categories as tree lists on the main page)
|
|
||||||
"buttons": {
|
|
||||||
"height": "120px", // Examples: "80px", "120px", "180px" (string|null) Height of buttons
|
|
||||||
"background": "#fafafa", // Examples: "#fafafa", "yellow" (string|null) Color of buttons background
|
|
||||||
"separator": {
|
|
||||||
"enabled": true, // Enable separators?
|
|
||||||
"width": "60%" // Exaples: "100%", "80%", "60%" (string|null) Width of separators over images (relative to image width from the left)
|
|
||||||
},
|
|
||||||
"lists": {
|
|
||||||
"height": "800px", // Examples: "500px", "100%" (string|null) Maximum height of lists (`max-height` for animations working)
|
|
||||||
"background": null, // Examples: "#fafafa", "yellow" (string|null) Color of lists
|
|
||||||
"separator": null, // Examples: "#fafafa", "yellow" (string|null) Color of separators between rows
|
|
||||||
"separated": true, // Separate lists from its buttons?
|
|
||||||
"blocks": true, // Blocks instead of plain text?
|
|
||||||
"arrow": true // Add arrow at the right?
|
|
||||||
},
|
|
||||||
"texts": {
|
|
||||||
"position": {
|
|
||||||
"vertical": "center" // Values: "top", "center", "bottom" (string|null) Position of texts of ascendants categories
|
|
||||||
},
|
|
||||||
"width": "max(8rem, 20%)", // Examples: "60%", "5rem", "max(8rem, 20%)" (string|null) Width of the text section (left side of buttons)
|
|
||||||
"background": false, // Enable wrapping element for texts?
|
|
||||||
"title": {
|
|
||||||
"color": "#020202" // Examples: "#fafafa", "yellow" (string|null) Color of titles
|
|
||||||
},
|
|
||||||
"description": {
|
|
||||||
"color": "#121212" // Examples: "#fafafa", "yellow" (string|null) Color of descriptions
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"images": {
|
|
||||||
"filter": "contrast(1.2)" // Example: "contrast(1.2)" (string|null) Filter for images
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"cart": {
|
|
||||||
"enabled": true // Enable the cart button?
|
|
||||||
},
|
|
||||||
"css": {
|
|
||||||
"catalog-button-cart-background": "#40a7e3",
|
|
||||||
"product-button-cart-background": "#40a7e3"
|
|
||||||
"catalog-button-cart-added-background": "#90be36",
|
|
||||||
"product-button-cart-added-background": "#90be36"
|
|
||||||
},
|
|
||||||
"account": {
|
|
||||||
"enabled": false // Enable the account section? (works only when opened from telegram `inline-button`)
|
|
||||||
},
|
|
||||||
"menu": {
|
|
||||||
"enabled": true, // Enable the main menu?
|
|
||||||
"position": "fixed" // Values: "fixed" (fixed to the bottom as a solid line), "relative" (at the top as separated buttons) (stirng) Position of the main menu
|
|
||||||
},
|
|
||||||
"header": {
|
|
||||||
"enabled": true, // Enable the header?
|
|
||||||
"position": "fixed" // Values: "fixed" (fixed to the bottom), "relative" (at the top) (stirng) Position of the header
|
|
||||||
},
|
|
||||||
"acquirings": {
|
|
||||||
"robokassa": {
|
|
||||||
"enabled": true, // Enable the Robokassa acquiring?
|
|
||||||
"mode": "test" // Values: "work", "test" (string) Mode of the Robokassa acquiring
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"input": {
|
|
||||||
"deliveries": {
|
|
||||||
"site": [], // Values: "sim", "name", "destination", "address", "commentary"
|
|
||||||
"chat": [
|
|
||||||
"sim",
|
|
||||||
"name",
|
|
||||||
"destination",
|
|
||||||
"address",
|
|
||||||
"commentary"
|
|
||||||
] // Values: "sim", "name", "destination", "address", "commentary"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"deliveries": {
|
|
||||||
"cdek": {
|
|
||||||
"enabled": true, // Enable CDEK delivery?
|
|
||||||
"label": "CDEK" // Name of the CDEK delivery
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"chats": [
|
|
||||||
{
|
|
||||||
"id": null, // Example: -1002599391893 (int) (negative number) The telegram chat identifier
|
|
||||||
"orders": true // Send orders? (for moderators)
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### language
|
||||||
|
Language for system messages if user language could not be determined<br>
|
||||||
|
<br>
|
||||||
|
**Value:** en
|
||||||
|
|
||||||
## Suspensions
|
## Suspensions
|
||||||
*System of suspensions of chat-robot and Web App*<br><br>
|
System of suspensions of chat-robot and Web App<br>
|
||||||
Make sure you have a **suspension** collection (can be created automatically)<br>
|
<br>
|
||||||
You can copy a clean suspension document without comments from here: `/examples/arangodb/collections/suspension.json`
|
Make sure you have a **suspension** collection (can be created automatically)
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"end": 1726068961, // Unixtime
|
"end": 1726068961,
|
||||||
"targets": {
|
"targets": {
|
||||||
"chat-robot": true, // Block chat-robot
|
"chat-robot": true,
|
||||||
"web app": true // Block "Web App"
|
"web app": true
|
||||||
},
|
}
|
||||||
"access": {
|
"access": {
|
||||||
"tester": true, // Account with `"tester": true`
|
"tester": true,
|
||||||
"developer": true // Account with `"developer": true`
|
"developer": true
|
||||||
},
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"ru": "Разрабатываю каталог, поиск и корзину",
|
"ru": "Разрабатываю каталог, поиск и корзину",
|
||||||
|
@ -356,13 +107,3 @@ You can copy a clean suspension document without comments from here: `/examples/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Used by
|
|
||||||
*List of projects created on the basis of [huesos](https://git.svoboda.works/mirzaev/huesos)*
|
|
||||||
|
|
||||||
- ARMING [@arming_bot](https://t.me/arming_bot)<br>
|
|
||||||
*Russian weapons tuning shop* ([repository](https://git.svoboda.works/mirzaev/arming))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,51 +1,44 @@
|
||||||
{
|
{
|
||||||
"name": "mirzaev/huesos",
|
"name": "mirzaev/arming_bot",
|
||||||
"description": "Chat-robot for tuning weapons",
|
"description": "Chat-robot for tuning weapons",
|
||||||
"homepage": "https://t.me/huesos",
|
"homepage": "https://t.me/arming_bot",
|
||||||
"type": "chat-robot",
|
"type": "chat-robot",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"telegram",
|
"telegram",
|
||||||
"chat-robot",
|
"chat-robot",
|
||||||
"military",
|
"military",
|
||||||
"shop"
|
"shop"
|
||||||
],
|
],
|
||||||
"readme": "README.md",
|
"readme": "README.md",
|
||||||
"license": "WTFPL",
|
"license": "WTFPL",
|
||||||
"authors": [
|
"authors": [
|
||||||
{
|
{
|
||||||
"name": "Arsen Mirzaev Tatyano-Muradovich",
|
"name": "Arsen Mirzaev Tatyano-Muradovich",
|
||||||
"email": "arsen@mirzaev.sexy"
|
"email": "arsen@mirzaev.sexy"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.4",
|
"triagens/arangodb": "^3.8",
|
||||||
"ext-gd": "^8.4",
|
"mirzaev/minimal": "^2.2",
|
||||||
"ext-intl": "^8.4",
|
"mirzaev/arangodb": "^1.3",
|
||||||
"triagens/arangodb": "^3.8",
|
"badfarm/zanzara": "^0.9.1",
|
||||||
"mirzaev/minimal": "^3.4.0",
|
"nyholm/psr7": "^1.8",
|
||||||
"mirzaev/arangodb": "^2",
|
"react/filesystem": "^0.1.2",
|
||||||
"badfarm/zanzara": "^0.9.1",
|
"twig/twig": "^3.10",
|
||||||
"nyholm/psr7": "^1.8",
|
"twig/extra-bundle": "^3.7",
|
||||||
"react/filesystem": "^0.1.2",
|
"twig/intl-extra": "^3.10",
|
||||||
"twig/twig": "^3.10",
|
"avadim/fast-excel-reader": "^2.19"
|
||||||
"twig/extra-bundle": "^3.7",
|
},
|
||||||
"twig/intl-extra": "^3.10",
|
"autoload": {
|
||||||
"avadim/fast-excel-reader": "^2.19",
|
"psr-4": {
|
||||||
"ttatpuot/cdek-sdk2.0": "^1.2",
|
"mirzaev\\arming_bot\\": "mirzaev/arming_bot/system/"
|
||||||
"guzzlehttp/guzzle": "^7.9",
|
}
|
||||||
"php-http/guzzle7-adapter": "^1.0",
|
},
|
||||||
"react/async": "^4.3"
|
"minimum-stability": "stable",
|
||||||
},
|
"config": {
|
||||||
"autoload": {
|
"allow-plugins": {
|
||||||
"psr-4": {
|
"php-http/discovery": true,
|
||||||
"mirzaev\\huesos\\": "mirzaev/huesos/system/"
|
"wyrihaximus/composer-update-bin-autoload-path": true
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"minimum-stability": "stable",
|
|
||||||
"config": {
|
|
||||||
"allow-plugins": {
|
|
||||||
"php-http/discovery": true,
|
|
||||||
"wyrihaximus/composer-update-bin-autoload-path": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,19 +0,0 @@
|
||||||
{
|
|
||||||
"urn": "/account",
|
|
||||||
"name": {
|
|
||||||
"en": "Account",
|
|
||||||
"ru": "Аккаунт"
|
|
||||||
},
|
|
||||||
"identifier": "account",
|
|
||||||
"style": {
|
|
||||||
"order": 1
|
|
||||||
},
|
|
||||||
"class": "",
|
|
||||||
"icon": {
|
|
||||||
"style": {},
|
|
||||||
"class": "loading spinner animated"
|
|
||||||
},
|
|
||||||
"image": {
|
|
||||||
"storage": null
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
{
|
|
||||||
"urn": "/cart",
|
|
||||||
"name": {
|
|
||||||
"en": "Cart",
|
|
||||||
"ru": "Корзина"
|
|
||||||
},
|
|
||||||
"style": {
|
|
||||||
"order": 999
|
|
||||||
},
|
|
||||||
"class": "cart",
|
|
||||||
"icon": {
|
|
||||||
"style": {},
|
|
||||||
"class": "shopping cart"
|
|
||||||
},
|
|
||||||
"image": {
|
|
||||||
"storage": null
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
{
|
|
||||||
"urn": "/",
|
|
||||||
"name": {
|
|
||||||
"en": "Main page",
|
|
||||||
"ru": "Главная страница"
|
|
||||||
},
|
|
||||||
"style": {
|
|
||||||
"order": 0
|
|
||||||
},
|
|
||||||
"class": "",
|
|
||||||
"icon": {
|
|
||||||
"style": {},
|
|
||||||
"class": "house"
|
|
||||||
},
|
|
||||||
"image": {
|
|
||||||
"storage": null
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
{
|
|
||||||
"status": "active",
|
|
||||||
"project": {
|
|
||||||
"name": "PROJECT"
|
|
||||||
},
|
|
||||||
"language": "en",
|
|
||||||
"currency": "usd",
|
|
||||||
"company": {
|
|
||||||
"identifier": null,
|
|
||||||
"tax": null,
|
|
||||||
"name": null,
|
|
||||||
"offer": false,
|
|
||||||
"sim": null,
|
|
||||||
"mail": null
|
|
||||||
},
|
|
||||||
"search": {
|
|
||||||
"enabled": true,
|
|
||||||
"position": "fixed"
|
|
||||||
},
|
|
||||||
"cart": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"css": {
|
|
||||||
"catalog-button-cart-background": "#40a7e3",
|
|
||||||
"product-button-cart-background": "#40a7e3"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
{
|
|
||||||
"end": 1726068961,
|
|
||||||
"targets": {
|
|
||||||
"chat-robot": true,
|
|
||||||
"web app": true
|
|
||||||
},
|
|
||||||
"access": {
|
|
||||||
"tester": true,
|
|
||||||
"developer": true
|
|
||||||
},
|
|
||||||
"description": {
|
|
||||||
"ru": "Разрабатываю каталог, поиск и корзину",
|
|
||||||
"en": "I am developing a catalog, search and cart"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
{
|
|
||||||
"type": "search-alias",
|
|
||||||
"name": "products_search",
|
|
||||||
"indexes": [
|
|
||||||
{
|
|
||||||
"collection": "product",
|
|
||||||
"index": "name_ru"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"collection": "product",
|
|
||||||
"index": "description_ru"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"collection": "product",
|
|
||||||
"index": "compatibility_ru"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"id": "1368785",
|
|
||||||
"globallyUniqueId": "hB561949FBEF8/1368785"
|
|
||||||
}
|
|
|
@ -1,52 +0,0 @@
|
||||||
#
|
|
||||||
# This section is commented out to make it possible to run NGINX without errors
|
|
||||||
# to generate TLS/SSL certificate via CertBot (see README.md)
|
|
||||||
#
|
|
||||||
# server {
|
|
||||||
# listen 443 default_server ssl;
|
|
||||||
# listen [::]:443 ssl default_server;
|
|
||||||
|
|
||||||
# server_name domain.zone;
|
|
||||||
|
|
||||||
# root /var/www/huesos/mirzaev/huesos/system/public;
|
|
||||||
|
|
||||||
# index index.php;
|
|
||||||
|
|
||||||
# ssl_certificate /etc/letsencrypt/live/domain.zone/fullchain.pem;
|
|
||||||
# ssl_certificate_key /etc/letsencrypt/live/domain.zone/privkey.pem;
|
|
||||||
# include /etc/letsencrypt/options-ssl-nginx.conf;
|
|
||||||
# ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
|
||||||
|
|
||||||
# location / {
|
|
||||||
# try_files $uri $uri/ /index.php?$query_string;
|
|
||||||
# }
|
|
||||||
|
|
||||||
# location /api/cdek {
|
|
||||||
# rewrite ^/api/cdek(.*)$ /$1 break;
|
|
||||||
# index cdek.php;
|
|
||||||
# }
|
|
||||||
|
|
||||||
# location ~ /(?<type>categories|products) {
|
|
||||||
# root /var/www/huesos/mirzaev/huesos/system/storage;
|
|
||||||
# try_files $uri =404;
|
|
||||||
# }
|
|
||||||
|
|
||||||
# location ~ \.php$ {
|
|
||||||
# include snippets/fastcgi-php.conf;
|
|
||||||
# fastcgi_pass unix:/run/php/php8.4-fpm.sock;
|
|
||||||
# }
|
|
||||||
# }
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 80 default_server;
|
|
||||||
listen [::]:80 default_server;
|
|
||||||
|
|
||||||
server_name domain.zone;
|
|
||||||
|
|
||||||
if ($host = domain.zone) {
|
|
||||||
return 301 https://$host$request_uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 404;
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,276 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace mirzaev\arming_bot\controllers;
|
||||||
|
|
||||||
|
// Files of the project
|
||||||
|
use mirzaev\arming_bot\controllers\core,
|
||||||
|
mirzaev\arming_bot\models\catalog as model,
|
||||||
|
mirzaev\arming_bot\models\entry,
|
||||||
|
mirzaev\arming_bot\models\category,
|
||||||
|
mirzaev\arming_bot\models\product;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller of catalog
|
||||||
|
*
|
||||||
|
* @package mirzaev\arming_bot\controllers
|
||||||
|
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||||
|
*/
|
||||||
|
final class catalog extends core
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Registry of errors
|
||||||
|
*/
|
||||||
|
protected array $errors = [
|
||||||
|
'session' => [],
|
||||||
|
'account' => [],
|
||||||
|
'catalog' => []
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Catalog
|
||||||
|
*
|
||||||
|
* @param array $parameters Parameters of the request (POST + GET)
|
||||||
|
*/
|
||||||
|
public function index(array $parameters = []): ?string
|
||||||
|
{
|
||||||
|
if (!empty($parameters['category'])) {
|
||||||
|
// Передана категория (идентификатор)
|
||||||
|
|
||||||
|
// Инициализация актуальной категории
|
||||||
|
$category = new category(document: category::_read('d.identifier == @identifier', parameters: ['identifier' => $parameters['category']]));
|
||||||
|
|
||||||
|
if ($category instanceof category) {
|
||||||
|
// Found the category
|
||||||
|
|
||||||
|
// Поиск категорий или товаров входящих в актуальную категорию
|
||||||
|
$entries = entry::search(
|
||||||
|
document: $category,
|
||||||
|
amount: 30,
|
||||||
|
errors: $this->errors['catalog']
|
||||||
|
);
|
||||||
|
|
||||||
|
// Объявление буферов категорий и товаров (важно - в единственном числе, по параметру из базы данных)
|
||||||
|
$category = $product = [];
|
||||||
|
|
||||||
|
foreach ($entries as $entry) {
|
||||||
|
// Перебор вхождений
|
||||||
|
|
||||||
|
// Запись массивов категорий и товаров ($category и $product) в буфер глобальной переменной шаблонизатора
|
||||||
|
${$entry->_type}[] = $entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запись категорий из буфера в глобальную переменную шаблонизатора
|
||||||
|
$this->view->categories = $category;
|
||||||
|
|
||||||
|
// Запись товаров из буфера в глобальную переменную шаблонизатора
|
||||||
|
$this->view->products = $product;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Не передана категория
|
||||||
|
|
||||||
|
// Поиск категорий: "categories" (самый верхний уровень)
|
||||||
|
$this->view->categories = entry::ascendants(descendant: new category, errors: $this->errors['catalog']);
|
||||||
|
|
||||||
|
// Search for products
|
||||||
|
/* $this->view->products = product::read(
|
||||||
|
filter: 'd.deleted != true && d.hidden != true',
|
||||||
|
sort: 'd.promoting ASC, d.position ASC, d.created DESC',
|
||||||
|
amount: 6,
|
||||||
|
errors: $this->errors['catalog']
|
||||||
|
); */
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generating filters
|
||||||
|
$this->view->filters = [
|
||||||
|
'brands' => product::collect('d.brand.ru', $this->errors['catalog'])
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
|
||||||
|
// GET request
|
||||||
|
|
||||||
|
// Exit (success)
|
||||||
|
return $this->view->render('catalog/page.html');
|
||||||
|
} else if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
// POST request
|
||||||
|
|
||||||
|
// Initializing a response headers
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
header('Content-Encoding: none');
|
||||||
|
header('X-Accel-Buffering: no');
|
||||||
|
|
||||||
|
// Initializing of the output buffer
|
||||||
|
ob_start();
|
||||||
|
|
||||||
|
// Generating the reponse
|
||||||
|
echo json_encode(
|
||||||
|
[
|
||||||
|
'title' => $title ?? '',
|
||||||
|
'html' => [
|
||||||
|
'categories' => $this->view->render('catalog/elements/categories.html'),
|
||||||
|
'products' => $this->view->render('catalog/elements/products/2columns.html'),
|
||||||
|
'filters' => $this->view->render('catalog/elemments/filters.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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,
|
||||||
|
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.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 of text fore search
|
||||||
|
preg_match('/[\d]+/', $parameters['id'] ?? '', $matches);
|
||||||
|
$_key = $matches[0] ?? null;
|
||||||
|
|
||||||
|
if (!empty($_key)) {
|
||||||
|
// Received id of prouct (_key)
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
// POST request
|
||||||
|
|
||||||
|
// Search for products
|
||||||
|
$product = product::read(
|
||||||
|
filter: "d._key == \"$_key\" && d.deleted != true && d.hidden != true",
|
||||||
|
sort: 'd.created DESC',
|
||||||
|
amount: 1,
|
||||||
|
return: '{id: d._key, title: d.title.ru, description: d.description.ru, cost: d.cost, weight: d.weight, dimensions: d.dimensions, brand: d.brand.ru, compatibility: d.compatibility.ru}',
|
||||||
|
errors: $this->errors['catalog']
|
||||||
|
)[0]?->getAll();
|
||||||
|
|
||||||
|
if (!empty($product)) {
|
||||||
|
// Found the product
|
||||||
|
|
||||||
|
// Initializing buffer of images
|
||||||
|
$images = [];
|
||||||
|
|
||||||
|
foreach (
|
||||||
|
glob(INDEX .
|
||||||
|
DIRECTORY_SEPARATOR .
|
||||||
|
'themes' .
|
||||||
|
DIRECTORY_SEPARATOR .
|
||||||
|
(THEME ?? 'default') .
|
||||||
|
DIRECTORY_SEPARATOR .
|
||||||
|
'images' .
|
||||||
|
DIRECTORY_SEPARATOR .
|
||||||
|
$_key .
|
||||||
|
DIRECTORY_SEPARATOR .
|
||||||
|
"*.{jpg,png,gif}", GLOB_BRACE) as $file
|
||||||
|
) {
|
||||||
|
// Iterate over images of the product
|
||||||
|
|
||||||
|
// Write to buffer of images
|
||||||
|
$images[] = "/images/$_key/" . basename($file);
|
||||||
|
}
|
||||||
|
|
||||||
|
$product = $product + ['images' => $images];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,104 +2,49 @@
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace mirzaev\huesos\controllers;
|
namespace mirzaev\arming_bot\controllers;
|
||||||
|
|
||||||
// Files of the project
|
// Files of the project
|
||||||
use mirzaev\huesos\views\templater,
|
use mirzaev\arming_bot\views\templater,
|
||||||
mirzaev\huesos\models\core as models,
|
mirzaev\arming_bot\models\core as models,
|
||||||
mirzaev\huesos\models\account,
|
mirzaev\arming_bot\models\account,
|
||||||
mirzaev\huesos\models\session,
|
mirzaev\arming_bot\models\session,
|
||||||
mirzaev\huesos\models\settings,
|
mirzaev\arming_bot\models\settings,
|
||||||
mirzaev\huesos\models\cart,
|
mirzaev\arming_bot\models\suspension;
|
||||||
mirzaev\huesos\models\suspension,
|
|
||||||
mirzaev\huesos\models\enumerations\language,
|
|
||||||
mirzaev\huesos\models\enumerations\currency;
|
|
||||||
|
|
||||||
// Framework for PHP
|
// Framework for PHP
|
||||||
use mirzaev\minimal\core as minimal,
|
use mirzaev\minimal\controller;
|
||||||
mirzaev\minimal\controller,
|
|
||||||
mirzaev\minimal\http\response,
|
|
||||||
mirzaev\minimal\http\enumerations\status;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Core of controllers
|
* Core of controllers
|
||||||
*
|
*
|
||||||
* @package mirzaev\huesos\controllers
|
* @package mirzaev\arming_bot\controllers
|
||||||
*
|
|
||||||
* @param settings $settings Instance of the settings
|
|
||||||
* @param session $session Instance of the session
|
|
||||||
* @param account|null $account Instance of the account
|
|
||||||
* @param cart|null $cart Instance of the cart
|
|
||||||
* @param language $language Language
|
|
||||||
* @param currency $currency Currency
|
|
||||||
* @param response $response Response
|
|
||||||
* @param array $errors Registry of errors
|
|
||||||
*
|
|
||||||
* @method void __construct(minimal $core, bool $initialize) Constructor
|
|
||||||
*
|
|
||||||
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
|
||||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||||
*/
|
*/
|
||||||
class core extends controller
|
class core extends controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Settings
|
* Postfix for name of controllers files
|
||||||
*
|
|
||||||
* @var settings $settings Instance of the settings
|
|
||||||
*/
|
*/
|
||||||
protected readonly settings $settings;
|
final public const string POSTFIX = '';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Session
|
* Instance of the settings
|
||||||
*
|
*/
|
||||||
* @var session|null $session Instance of the session
|
public static settings $settings;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instance of a session
|
||||||
*/
|
*/
|
||||||
protected readonly session $session;
|
protected readonly session $session;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Account
|
* Instance of an account
|
||||||
*
|
|
||||||
* @var account|null $account Instance of the account
|
|
||||||
*/
|
*/
|
||||||
protected readonly ?account $account;
|
protected readonly ?account $account;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cart
|
* Registry of errors
|
||||||
*
|
|
||||||
* @var cart|null $cart Instance of the cart
|
|
||||||
*/
|
|
||||||
protected readonly ?cart $cart;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Language
|
|
||||||
*
|
|
||||||
* @var language $language Language
|
|
||||||
*/
|
|
||||||
protected language $language = language::en;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Currency
|
|
||||||
*
|
|
||||||
* @var currency $currency Currency
|
|
||||||
*/
|
|
||||||
protected currency $currency = currency::usd;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Response
|
|
||||||
*
|
|
||||||
* @see https://wiki.php.net/rfc/property-hooks (find a table about backed and virtual hooks)
|
|
||||||
*
|
|
||||||
* @var response $response Response
|
|
||||||
*/
|
|
||||||
protected response $response {
|
|
||||||
// Read
|
|
||||||
get => $this->response ??= $this->request->response();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Errors
|
|
||||||
*
|
|
||||||
* @var array $errors Registry of errors
|
|
||||||
*/
|
*/
|
||||||
protected array $errors = [
|
protected array $errors = [
|
||||||
'session' => [],
|
'session' => [],
|
||||||
|
@ -107,30 +52,25 @@ class core extends controller
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor of an instance
|
||||||
*
|
*
|
||||||
* @param minimal $core Initialize a controller?
|
|
||||||
* @param bool $initialize Initialize a controller?
|
* @param bool $initialize Initialize a controller?
|
||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*
|
|
||||||
* @todo
|
|
||||||
* 1. settings account и session не имеют проверок на возврат null
|
|
||||||
* 2. TRANSIT EVERYTHING TO MIDDLEWARES
|
|
||||||
*/
|
*/
|
||||||
public function __construct(minimal $core, bool $initialize = true)
|
public function __construct(bool $initialize = true)
|
||||||
{
|
{
|
||||||
// Blocking requests from CloudFlare (better to write this blocking into nginx config file)
|
// Blocking requests from CloudFlare (better to write this blocking into nginx config file)
|
||||||
if (isset($_SERVER['HTTP_USER_AGENT']) && $_SERVER['HTTP_USER_AGENT'] === 'nginx-ssl early hints') return status::bruh->label;
|
if (isset($_SERVER['HTTP_USER_AGENT']) && $_SERVER['HTTP_USER_AGENT'] === 'nginx-ssl early hints') return;
|
||||||
|
|
||||||
// For the extends system
|
// For the extends system
|
||||||
parent::__construct(core: $core);
|
parent::__construct($initialize);
|
||||||
|
|
||||||
if ($initialize) {
|
if ($initialize) {
|
||||||
// Initializing is requested
|
// Initializing is requested
|
||||||
|
|
||||||
// Initializing of models core (connect to ArangoDB...)
|
// Initializing of models core (connect to ArangoDB...)
|
||||||
new models(true);
|
new models();
|
||||||
|
|
||||||
// Initializing of the date until which the session will be active
|
// Initializing of the date until which the session will be active
|
||||||
$expires = strtotime('+1 week');
|
$expires = strtotime('+1 week');
|
||||||
|
@ -143,7 +83,9 @@ class core extends controller
|
||||||
|
|
||||||
// Handle a problems with initializing a session
|
// Handle a problems with initializing a session
|
||||||
if (!empty($this->errors['session'])) exit(1);
|
if (!empty($this->errors['session'])) exit(1);
|
||||||
else if ($_COOKIE["session"] !== $this->session->hash) {
|
|
||||||
|
// телеграм не сохраняет куки
|
||||||
|
/* else if ($_COOKIE["session"] !== $this->session->hash) {
|
||||||
// Hash of the session is changed (implies that the session has expired and recreated)
|
// Hash of the session is changed (implies that the session has expired and recreated)
|
||||||
|
|
||||||
// Write a new hash of the session to cookies
|
// Write a new hash of the session to cookies
|
||||||
|
@ -158,29 +100,16 @@ class core extends controller
|
||||||
'samesite' => 'strict'
|
'samesite' => 'strict'
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
} */
|
||||||
|
|
||||||
// Initializing registry of account errors
|
|
||||||
$this->errors['account'];
|
|
||||||
|
|
||||||
// Initializing of the account
|
// Initializing of the account
|
||||||
$this->account = $this->session->account($this->errors['account']);
|
$this->account = $this->session->account($this->errors['account']);
|
||||||
|
|
||||||
// Initializing of the settings
|
// Initializing of the settings
|
||||||
$this->settings = settings::active(create: SETTINGS_PROJECT);
|
self::$settings = settings::active();
|
||||||
|
|
||||||
// Initializing of the language
|
|
||||||
$this->language = $this->account?->language ?? $this->session?->buffer['language'] ?? $this->settings?->language ?? language::en;
|
|
||||||
|
|
||||||
// Initializing of the currency
|
|
||||||
$this->currency = $this->account?->currency ?? $this->session?->buffer['currency'] ?? $this->settings?->currency ?? currency::usd;
|
|
||||||
|
|
||||||
// Initializing of preprocessor of views
|
// Initializing of preprocessor of views
|
||||||
$this->view = new templater(
|
$this->view = new templater($this->session, $this->account);
|
||||||
session: $this->session,
|
|
||||||
account: $this->account,
|
|
||||||
settings: $this->settings
|
|
||||||
);
|
|
||||||
|
|
||||||
// @todo перенести в middleware
|
// @todo перенести в middleware
|
||||||
|
|
||||||
|
@ -213,20 +142,20 @@ class core extends controller
|
||||||
suspension:
|
suspension:
|
||||||
|
|
||||||
// Write title of the page to templater global variables
|
// Write title of the page to templater global variables
|
||||||
$this->view->title = match ($this->language) {
|
$this->view->title = match ($account?->language ?? self::$settings?->language) {
|
||||||
language::en => 'Suspended',
|
'ru' => 'Приостановлено',
|
||||||
language::ru => 'Приостановлено',
|
'en' => 'Suspended',
|
||||||
default => 'Suspended'
|
default => 'Suspended'
|
||||||
};
|
};
|
||||||
|
|
||||||
// Write description of the suspension to templater global variables
|
// Write description of the suspension to templater global variables
|
||||||
$this->view->description = $suspension->description[$this->language] ?? array_values($suspension->description)[0];
|
$this->view->description = $suspension->description[$account?->language ?? self::$settings?->language] ?? array_values($suspension->description)[0];
|
||||||
|
|
||||||
// Write message of remaining time of the suspension to templater global variables
|
// Write message of remaining time of the suspension to templater global variables
|
||||||
$this->view->remain = [
|
$this->view->remain = [
|
||||||
'title' => match ($this->language) {
|
'title' => match ($account?->language ?? self::$settings?->language) {
|
||||||
language::en => 'Time remaining: ',
|
'ru' => 'Осталось времени: ',
|
||||||
language::ru => 'Осталось времени: ',
|
'en' => 'Time remaining: ',
|
||||||
default => 'Time remaining: '
|
default => 'Time remaining: '
|
||||||
},
|
},
|
||||||
'value' => $suspension?->message()
|
'value' => $suspension?->message()
|
||||||
|
@ -241,4 +170,21 @@ class core extends controller
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check of initialization
|
||||||
|
*
|
||||||
|
* Checks whether a property is initialized in a document instance from ArangoDB
|
||||||
|
*
|
||||||
|
* @param string $name Name of the property from ArangoDB
|
||||||
|
*
|
||||||
|
* @return bool The property is initialized?
|
||||||
|
*/
|
||||||
|
public function __isset(string $name): bool
|
||||||
|
{
|
||||||
|
// Check of initialization of the property and exit (success)
|
||||||
|
return match ($name) {
|
||||||
|
default => isset($this->{$name})
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
<?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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Index controller
|
||||||
|
*
|
||||||
|
* @package mirzaev\arming_bot\controllers
|
||||||
|
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||||
|
*/
|
||||||
|
final class index extends core
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the main page
|
||||||
|
*
|
||||||
|
* @param array $parameters Parameters of the request (POST + GET)
|
||||||
|
*/
|
||||||
|
public function index(array $parameters = []): ?string
|
||||||
|
{
|
||||||
|
// Exit (success)
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'GET') return $this->view->render('index.html');
|
||||||
|
|
||||||
|
// Exit (fail)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,134 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace mirzaev\arming_bot\controllers;
|
||||||
|
|
||||||
|
// Files of the project
|
||||||
|
use mirzaev\arming_bot\controllers\core,
|
||||||
|
mirzaev\arming_bot\models\account;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller of session
|
||||||
|
*
|
||||||
|
* @package mirzaev\arming_bot\controllers
|
||||||
|
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||||
|
*/
|
||||||
|
final class session extends core
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Registry of errors
|
||||||
|
*/
|
||||||
|
protected array $errors = [
|
||||||
|
'session' => [],
|
||||||
|
'account' => []
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connect session to the telegram account
|
||||||
|
*
|
||||||
|
* @param array $parameters Parameters of the request (POST + GET)
|
||||||
|
*/
|
||||||
|
public function telegram(array $parameters = []): ?string
|
||||||
|
{
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
// POST request
|
||||||
|
|
||||||
|
if ($connected = isset($this->account)) {
|
||||||
|
// Found the account
|
||||||
|
|
||||||
|
// Initializing language of the account
|
||||||
|
$language = $this->account->language;
|
||||||
|
} else {
|
||||||
|
// Not found the account
|
||||||
|
|
||||||
|
if (count($parameters) > 1 && isset($parameters['hash'])) {
|
||||||
|
|
||||||
|
$buffer = $parameters;
|
||||||
|
|
||||||
|
unset($buffer['authentication'], $buffer['hash']);
|
||||||
|
ksort($buffer);
|
||||||
|
|
||||||
|
$prepared = [];
|
||||||
|
foreach ($buffer as $key => $value) {
|
||||||
|
if (is_array($value)) {
|
||||||
|
$prepared[] = $key . '=' . json_encode($value, JSON_UNESCAPED_UNICODE);
|
||||||
|
} else {
|
||||||
|
$prepared[] = $key . '=' . $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$key = hash_hmac('sha256', require(SETTINGS . DIRECTORY_SEPARATOR . 'key.php'), 'WebAppData', true);
|
||||||
|
$hash = bin2hex(hash_hmac('sha256', implode(PHP_EOL, $prepared), $key, true));
|
||||||
|
|
||||||
|
if (hash_equals($hash, $parameters['hash'])) {
|
||||||
|
// Data confirmed (according to telegram documentation)
|
||||||
|
|
||||||
|
if (time() - $parameters['auth_date'] < 86400) {
|
||||||
|
// Authorization date less than 1 day ago
|
||||||
|
|
||||||
|
// Initializing data of the account
|
||||||
|
$data = json_decode($parameters['user']);
|
||||||
|
|
||||||
|
// Initializing of the account
|
||||||
|
$account = account::initialization(
|
||||||
|
$data->id,
|
||||||
|
[
|
||||||
|
'id' => $data->id,
|
||||||
|
'name' => [
|
||||||
|
'first' => $data->first_name,
|
||||||
|
'last' => $data->last_name
|
||||||
|
],
|
||||||
|
'domain' => $data->username,
|
||||||
|
'language' => $data->language_code,
|
||||||
|
'messages' => $data->allows_write_to_pm
|
||||||
|
],
|
||||||
|
$this->errors['account']
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($account instanceof account) {
|
||||||
|
// Initialized the account
|
||||||
|
|
||||||
|
// Connecting the account to the session
|
||||||
|
$connected = $this->session->connect($account, $this->errors['session']);
|
||||||
|
|
||||||
|
// Initializing language of the account
|
||||||
|
$language = $account->language;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(
|
||||||
|
[
|
||||||
|
'connected' => (bool) $connected,
|
||||||
|
'language' => $language ?? null,
|
||||||
|
'errors' => $this->errors
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initializing a response headers
|
||||||
|
header('Content-Length: ' . ob_get_length());
|
||||||
|
|
||||||
|
// Sending and deinitializing of the output buffer
|
||||||
|
ob_end_flush();
|
||||||
|
flush();
|
||||||
|
|
||||||
|
// Exit (success)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit (fail)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,136 @@
|
||||||
|
<?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\status,
|
||||||
|
mirzaev\arming_bot\models\traits\document as arangodb_document_trait,
|
||||||
|
mirzaev\arming_bot\models\interfaces\document as arangodb_document_interface;
|
||||||
|
|
||||||
|
// Framework for ArangoDB
|
||||||
|
use mirzaev\arangodb\collection,
|
||||||
|
mirzaev\arangodb\document;
|
||||||
|
|
||||||
|
// Framework for Telegram
|
||||||
|
use Zanzara\Telegram\Type\User as telegram;
|
||||||
|
|
||||||
|
// Built-in libraries
|
||||||
|
use exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model of account
|
||||||
|
*
|
||||||
|
* @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 account extends core implements arangodb_document_interface
|
||||||
|
{
|
||||||
|
use status, arangodb_document_trait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name of the collection in ArangoDB
|
||||||
|
*/
|
||||||
|
final public const string COLLECTION = 'account';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize
|
||||||
|
*
|
||||||
|
* @param int $identifier Identifier of the account
|
||||||
|
* @param telegram|array|null $registration Данные для регистрация, если аккаунт не найден
|
||||||
|
* @param array &$errors Registry of errors
|
||||||
|
*
|
||||||
|
* @return static|null Объект аккаунта, если найден
|
||||||
|
*/
|
||||||
|
public static function initialize(int $identifier, telegram|array|null $registration = null, array &$errors = []): static|null
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
|
||||||
|
// Initialized the collection
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Initializing the account and exit (success)
|
||||||
|
return new static(
|
||||||
|
document: collection::execute(
|
||||||
|
<<<'AQL'
|
||||||
|
FOR d IN @@collection
|
||||||
|
FILTER d.identifier == @identifier
|
||||||
|
RETURN d
|
||||||
|
AQL,
|
||||||
|
[
|
||||||
|
'@collection' => static::COLLECTION,
|
||||||
|
'identifier' => $identifier
|
||||||
|
],
|
||||||
|
errors: $errors
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} catch (exception $e) {
|
||||||
|
if ($registration) {
|
||||||
|
// Not found the account and registration is requested
|
||||||
|
|
||||||
|
// Creating account
|
||||||
|
$account = document::write(
|
||||||
|
static::COLLECTION,
|
||||||
|
(is_array($registration)
|
||||||
|
? $registration :
|
||||||
|
[
|
||||||
|
'identifier' => $registration->getId(),
|
||||||
|
'name' => [
|
||||||
|
'first' => $registration->getFirstName(),
|
||||||
|
'last' => $registration->getLastName()
|
||||||
|
],
|
||||||
|
'domain' => $registration->getUsername(),
|
||||||
|
'robot' => $registration->isBot(),
|
||||||
|
'banned' => false,
|
||||||
|
'tester' => false,
|
||||||
|
'developer' => false,
|
||||||
|
'access' => [
|
||||||
|
'settings' => false
|
||||||
|
],
|
||||||
|
'menus' => [
|
||||||
|
'attachments' => $registration->getAddedToAttachmentMenu()
|
||||||
|
],
|
||||||
|
'messages' => true,
|
||||||
|
'groups' => [
|
||||||
|
'join' => $registration->getCanJoinGroups(),
|
||||||
|
'messages' => $registration->getCanReadAllGroupMessages()
|
||||||
|
],
|
||||||
|
'premium' => $registration->isPremium(),
|
||||||
|
'language' => $registration->getLanguageCode(),
|
||||||
|
'queries' => [
|
||||||
|
'inline' => $registration->getSupportsInlineQueries()
|
||||||
|
]
|
||||||
|
]) + [
|
||||||
|
'version' => ROBOT_VERSION,
|
||||||
|
'active' => true
|
||||||
|
],
|
||||||
|
errors: $errors
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($account) {
|
||||||
|
// Created account
|
||||||
|
|
||||||
|
// Initializing of the account (without registration request)
|
||||||
|
return static::initialize($identifier, errors: $errors);
|
||||||
|
} else throw new exception('Failed to register account');
|
||||||
|
} else throw new exception('Failed to find account');
|
||||||
|
}
|
||||||
|
} 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,487 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace mirzaev\arming_bot\models;
|
||||||
|
|
||||||
|
// Files of the project
|
||||||
|
use mirzaev\arming_bot\models\core,
|
||||||
|
mirzaev\arming_bot\models\product,
|
||||||
|
mirzaev\arming_bot\models\category,
|
||||||
|
mirzaev\arming_bot\models\entry,
|
||||||
|
mirzaev\arming_bot\models\traits\files,
|
||||||
|
mirzaev\arming_bot\models\traits\yandex\disk as yandex;
|
||||||
|
|
||||||
|
// Framework for ArangoDB
|
||||||
|
use mirzaev\arangodb\collection,
|
||||||
|
mirzaev\arangodb\document;
|
||||||
|
|
||||||
|
// Framework for Excel
|
||||||
|
use avadim\FastExcelReader\Excel as excel;
|
||||||
|
|
||||||
|
// Built-in libraries
|
||||||
|
use exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model of the catalog
|
||||||
|
*
|
||||||
|
* @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 catalog extends core
|
||||||
|
{
|
||||||
|
use yandex, files {
|
||||||
|
yandex::download as yandex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collect parameter from all products
|
||||||
|
*
|
||||||
|
* @param string $documment Path to the EXCEL-document
|
||||||
|
* @param int &$categories_loaded Counter of loaded categories
|
||||||
|
* @param int &$categories_created Counter of created categories
|
||||||
|
* @param int &$categories_updated Counter of updated categories
|
||||||
|
* @param int &$categories_deleted Counter of deleted categories
|
||||||
|
* @param int &$categories_new Counter of new categories
|
||||||
|
* @param int &$categories_old Counter of old categories
|
||||||
|
* @param int &$products_loaded Counter of loaded products
|
||||||
|
* @param int &$products_created Counter of created products
|
||||||
|
* @param int &$products_updated Counter of updated products
|
||||||
|
* @param int &$products_deleted Counter of deleted products
|
||||||
|
* @param int &$products_new Counter of new products
|
||||||
|
* @param int &$products_old Counter of old products
|
||||||
|
* @param string &$language Language (en, ru...)
|
||||||
|
* @param array &$errors Registry of errors
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*
|
||||||
|
* @todo
|
||||||
|
* 1. Сначала создать все категории и затем снова по циклу пройтись уже создавать entry между ними
|
||||||
|
* 2. Сжимать изображения
|
||||||
|
*/
|
||||||
|
public static function import(
|
||||||
|
string $document,
|
||||||
|
int &$categories_loaded = 0,
|
||||||
|
int &$categories_created = 0,
|
||||||
|
int &$categories_updated = 0,
|
||||||
|
int &$categories_deleted = 0,
|
||||||
|
int &$categories_old = 0,
|
||||||
|
int &$categories_new = 0,
|
||||||
|
int &$products_loaded = 0,
|
||||||
|
int &$products_created = 0,
|
||||||
|
int &$products_updated = 0,
|
||||||
|
int &$products_deleted = 0,
|
||||||
|
int &$products_old = 0,
|
||||||
|
int &$products_new = 0,
|
||||||
|
string $language = 'en',
|
||||||
|
array &$errors = []
|
||||||
|
): void {
|
||||||
|
try {
|
||||||
|
// Initializing the spreadsheet
|
||||||
|
$spreadsheet = excel::open($document);
|
||||||
|
|
||||||
|
// Inititalizing worksheets
|
||||||
|
$categories = $spreadsheet->getSheet('Категории');
|
||||||
|
$products = $spreadsheet->getSheet('Товары');
|
||||||
|
|
||||||
|
// Counting old documents
|
||||||
|
$categories_old = collection::count(category::COLLECTION, errors: $errors);
|
||||||
|
$products_old = collection::count(product::COLLECTION, errors: $errors);
|
||||||
|
|
||||||
|
// Initializing the buffer of handler categories and products
|
||||||
|
$handled = [
|
||||||
|
'categories' => [],
|
||||||
|
'products' => []
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach (
|
||||||
|
$categories->nextRow(
|
||||||
|
[
|
||||||
|
'A' => 'identifier',
|
||||||
|
'B' => 'name',
|
||||||
|
'C' => 'category',
|
||||||
|
'D' => 'images',
|
||||||
|
'E' => 'position'
|
||||||
|
],
|
||||||
|
excel::KEYS_FIRST_ROW
|
||||||
|
) as $number => $row
|
||||||
|
) {
|
||||||
|
// Iterate over categories
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!empty($row['identifier']) && !empty($row['name'])) {
|
||||||
|
// All required cells are filled in
|
||||||
|
|
||||||
|
// Incrementing the counter of loaded categories
|
||||||
|
++$categories_loaded;
|
||||||
|
|
||||||
|
// Declaring the variable with the status that a new category has been created
|
||||||
|
$created = false;
|
||||||
|
|
||||||
|
// Declaring the variable with the category
|
||||||
|
$category = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Initializing the category
|
||||||
|
$category = new category(document: category::_read('d.identifier == @identifier', parameters: ['identifier' => (int) $row['identifier']], errors: $errors)[0] ?? null);
|
||||||
|
|
||||||
|
// Initializing name of the category
|
||||||
|
$category->name[$language] === $row['name'] || $category->name = [[$language => $row['name']]] + $category->name ?? [];
|
||||||
|
|
||||||
|
// Initializing position of the category
|
||||||
|
$category->position === $row['position'] || $category->position = $row['position'];
|
||||||
|
} catch (exception $e) {
|
||||||
|
// Not found the category
|
||||||
|
|
||||||
|
// Creating the category
|
||||||
|
$_id = category::write((int) $row['identifier'], [$language => $row['name']], $row['position'] ?? null, $errors);
|
||||||
|
|
||||||
|
// Initializing the category
|
||||||
|
$category = new category(document: $created = category::_read('d._id == @_id', parameters: ['_id' => $_id], errors: $errors)[0] ?? null);
|
||||||
|
|
||||||
|
// Incrementing the counter of created categories
|
||||||
|
if ($created) ++$categories_created;
|
||||||
|
};
|
||||||
|
|
||||||
|
if ($category instanceof category) {
|
||||||
|
// Found the category
|
||||||
|
|
||||||
|
if (!empty($row['category'])) {
|
||||||
|
// Received the ascendant category
|
||||||
|
|
||||||
|
// Initializing the ascendant category
|
||||||
|
$ascendant = new category(document: category::_read('d.identifier == @identifier', parameters: ['identifier' => (int) $row['category']], errors: $errors)[0] ?? null);
|
||||||
|
|
||||||
|
if ($ascendant instanceof category) {
|
||||||
|
// Found the ascendant category
|
||||||
|
|
||||||
|
// Deleting entries of the category in ArangoDB
|
||||||
|
entry::banish($category, $errors);
|
||||||
|
|
||||||
|
// Writing the category as an entry to the ascendant category in ArangoDB
|
||||||
|
entry::write($category, $ascendant, $errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($row['images'])) {
|
||||||
|
// Received images
|
||||||
|
|
||||||
|
// Initializing new images of the category
|
||||||
|
$images = explode(' ', trim($row['images']));
|
||||||
|
|
||||||
|
// Reinitialize images? (true, if no images found or their amount does not match)
|
||||||
|
$reinitialize = !$category->images || count($category->images) !== count($images);
|
||||||
|
|
||||||
|
// Checking the identity of existing images with new images (if reinitialization of images has not yet been requested)
|
||||||
|
if (!$reinitialize) foreach ($category->images as $key => $image) if ($reinitialize = $image['source'] !== $images[$key]) break;
|
||||||
|
|
||||||
|
if ($reinitialize) {
|
||||||
|
// Requested reinitialization of images
|
||||||
|
|
||||||
|
// Initializing the buffer of images
|
||||||
|
$buffer = [];
|
||||||
|
|
||||||
|
foreach ($images as $index => $image) {
|
||||||
|
// Iterating over new images
|
||||||
|
|
||||||
|
// Skipping empty URI`s
|
||||||
|
if (empty($image = trim($image))) continue;
|
||||||
|
|
||||||
|
// Initializing path to directory of the image in storage
|
||||||
|
$directory = DIRECTORY_SEPARATOR . 'categories' . DIRECTORY_SEPARATOR . $row['identifier'];
|
||||||
|
|
||||||
|
// Initializing URL of the image in storage
|
||||||
|
$url = STORAGE . $directory;
|
||||||
|
|
||||||
|
// Initializing URN of the image in storage
|
||||||
|
$urn = $index . '.jpg';
|
||||||
|
|
||||||
|
// Initializing URI of the image in storage
|
||||||
|
$uri = $url . DIRECTORY_SEPARATOR . $urn;
|
||||||
|
|
||||||
|
// Initializing the directory in storage
|
||||||
|
if (!file_exists($url)) mkdir($url, 0775, true);
|
||||||
|
|
||||||
|
if (static::yandex($image, $uri, errors: $errors)) {
|
||||||
|
// The image is downloaded
|
||||||
|
|
||||||
|
// Writing the image to the buffer if images
|
||||||
|
$buffer[] = [
|
||||||
|
'source' => $image,
|
||||||
|
'storage' => $directory . DIRECTORY_SEPARATOR . $urn
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initializing images of the category
|
||||||
|
$category->images = $buffer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writing in ArangoDB
|
||||||
|
$updated = document::update($category->__document(), errors: $errors);
|
||||||
|
|
||||||
|
// Incrementing the counter of updated categories
|
||||||
|
if ($updated && !$created) ++$categories_updated;
|
||||||
|
} else throw new exception("Failed to initialize category: {$row['name']} ($number)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writing to the registry of handled categories and products
|
||||||
|
$handled['categories'][] = $row['identifier'];
|
||||||
|
} catch (exception $e) {
|
||||||
|
// Writing to the registry of errors
|
||||||
|
$errors[] = [
|
||||||
|
'text' => $e->getMessage(),
|
||||||
|
'file' => $e->getFile(),
|
||||||
|
'line' => $e->getLine(),
|
||||||
|
'stack' => $e->getTrace()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (
|
||||||
|
$products->nextRow(
|
||||||
|
[
|
||||||
|
'A' => 'identifier',
|
||||||
|
'B' => 'name',
|
||||||
|
'C' => 'category',
|
||||||
|
'D' => 'description',
|
||||||
|
'E' => 'cost',
|
||||||
|
'F' => 'weight',
|
||||||
|
'G' => 'x',
|
||||||
|
'H' => 'y',
|
||||||
|
'I' => 'z',
|
||||||
|
'J' => 'brand',
|
||||||
|
'K' => 'compatibility',
|
||||||
|
'L' => 'images',
|
||||||
|
'M' => 'position'
|
||||||
|
],
|
||||||
|
excel::KEYS_FIRST_ROW
|
||||||
|
) as $number => $row
|
||||||
|
) {
|
||||||
|
// Iterate over products
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!empty($row['identifier']) && !empty($row['name'])) {
|
||||||
|
// All required cells are filled in
|
||||||
|
|
||||||
|
// Incrementing the counter of loaded products
|
||||||
|
++$products_loaded;
|
||||||
|
|
||||||
|
// Declaring the variable with the status that a new product has been created
|
||||||
|
$created = false;
|
||||||
|
|
||||||
|
// Declaring the variable with the product
|
||||||
|
$product = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Initializing the product
|
||||||
|
$product = new product(document: product::_read('d.identifier == %u', parameters: ['identifier' => (int) $row['identifier']], errors: $errors)[0] ?? null);
|
||||||
|
|
||||||
|
// Initializing name of the category
|
||||||
|
$product->name[$language] === $row['name'] || $product->name = [[$language => $row['name']]] + $product->name ?? [];
|
||||||
|
|
||||||
|
// Initializing position of the product
|
||||||
|
$product->position === $row['position'] || $product->position = $row['position'];
|
||||||
|
} catch (exception $e) {
|
||||||
|
// Not found the product
|
||||||
|
|
||||||
|
// Creating the product
|
||||||
|
$_id = product::write(
|
||||||
|
(int) $row['identifier'],
|
||||||
|
[$language => $row['name']],
|
||||||
|
[$language => $row['description']],
|
||||||
|
(float) $row['cost'],
|
||||||
|
(float) $row['weight'],
|
||||||
|
['x' => $row['x'], 'y' => $row['y'], 'z' => $row['z']],
|
||||||
|
$row['position'] ?? null,
|
||||||
|
errors: $errors
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initializing the product
|
||||||
|
$product = new product(document: $created = product::_read(sprintf('d._id == "%s"', $_id), errors: $errors)[0] ?? null);
|
||||||
|
|
||||||
|
// Incrementing the counter of created products
|
||||||
|
if ($created) ++$products_created;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($product instanceof product) {
|
||||||
|
// Found the product
|
||||||
|
|
||||||
|
if (!empty($row['category'])) {
|
||||||
|
// Received the category
|
||||||
|
|
||||||
|
// Initializing the category
|
||||||
|
$category = new category(document: category::_read(sprintf('d.identifier == %u', (int) $row['category']), errors: $errors)[0] ?? null);
|
||||||
|
|
||||||
|
if ($category instanceof category) {
|
||||||
|
// Found the ascendant category
|
||||||
|
|
||||||
|
// Deleting entries of the product in ArangoDB
|
||||||
|
entry::banish($product, $errors);
|
||||||
|
|
||||||
|
// Writing the product as an entry to the ascendant category in ArangoDB
|
||||||
|
entry::write($product, $category, $errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($row['images'])) {
|
||||||
|
// Received images
|
||||||
|
|
||||||
|
// Initializing new images of the category
|
||||||
|
$images = explode(' ', trim($row['images']));
|
||||||
|
|
||||||
|
// Reinitialize images? (true, if no images found or their amount does not match)
|
||||||
|
$reinitialize = !$product->images || count($product->images) !== count($images);
|
||||||
|
|
||||||
|
// Checking the identity of existing images with new images (if reinitialization of images has not yet been requested)
|
||||||
|
if (!$reinitialize) foreach ($product->images as $key => $image) if ($reinitialize = $image['source'] !== $images[$key]) break;
|
||||||
|
|
||||||
|
if ($reinitialize) {
|
||||||
|
// Requested reinitialization of images
|
||||||
|
|
||||||
|
// Initializing the buffer of images
|
||||||
|
$buffer = [];
|
||||||
|
|
||||||
|
foreach ($images as $index => $image) {
|
||||||
|
// Iterating over new images
|
||||||
|
|
||||||
|
// Skipping empty URI`s
|
||||||
|
if (empty($image = trim($image))) continue;
|
||||||
|
|
||||||
|
// Initializing path to directory of the image in storage
|
||||||
|
$directory = DIRECTORY_SEPARATOR . 'products' . DIRECTORY_SEPARATOR . $row['identifier'];
|
||||||
|
|
||||||
|
// Initializing URL of the image in storage
|
||||||
|
$url = STORAGE . $directory;
|
||||||
|
|
||||||
|
// Initializing URN of the image in storage
|
||||||
|
$urn = $index . '.jpg';
|
||||||
|
|
||||||
|
// Initializing URI of the image in storage
|
||||||
|
$uri = $url . DIRECTORY_SEPARATOR . $urn;
|
||||||
|
|
||||||
|
// Initializing the directory in storage
|
||||||
|
if (!file_exists($url)) mkdir($url, 0775, true);
|
||||||
|
|
||||||
|
if (static::yandex($image, $uri, errors: $errors)) {
|
||||||
|
// The image is downloaded
|
||||||
|
|
||||||
|
// Writing the image to the buffer if images
|
||||||
|
$buffer[] = [
|
||||||
|
'source' => $image,
|
||||||
|
'storage' => $directory . DIRECTORY_SEPARATOR . $urn
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initializing images of the category
|
||||||
|
$product->images = $buffer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writing in ArangoDB
|
||||||
|
$updated = document::update($product->__document(), errors: $errors);
|
||||||
|
|
||||||
|
// Incrementing the counter of updated categories
|
||||||
|
if ($updated && !$created) ++$products_updated;
|
||||||
|
} else throw new exception("Failed to initialize product: {$row['name']} ($number)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writing to the registry of handled categories and products
|
||||||
|
$handled['products'][] = $row['identifier'];
|
||||||
|
} catch (exception $e) {
|
||||||
|
// Writing to the registry of errors
|
||||||
|
$errors[] = [
|
||||||
|
'text' => $e->getMessage(),
|
||||||
|
'file' => $e->getFile(),
|
||||||
|
'line' => $e->getLine(),
|
||||||
|
'stack' => $e->getTrace()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deleting old categories
|
||||||
|
foreach (
|
||||||
|
category::_read(
|
||||||
|
/* filter: sprintf('%s - d.updated > 3600', time()), */
|
||||||
|
sort: 'd.updated DESC',
|
||||||
|
amount: 100000,
|
||||||
|
errors: $errors
|
||||||
|
) as $document
|
||||||
|
) {
|
||||||
|
// Iterating over categories
|
||||||
|
|
||||||
|
// Initializing the category
|
||||||
|
$category = new category(document: $document);
|
||||||
|
|
||||||
|
if (
|
||||||
|
$category instanceof category
|
||||||
|
&& array_search($category->identifier, $handled['categories']) === false
|
||||||
|
) {
|
||||||
|
// Not found identifier of the product in the buffer of handled categories and products
|
||||||
|
|
||||||
|
// Deleting images of the category from storage
|
||||||
|
static::delete(STORAGE . DIRECTORY_SEPARATOR . 'categories' . DIRECTORY_SEPARATOR . $category->identifier, errors: $errors);
|
||||||
|
|
||||||
|
// Deleting entries of the category in ArangoDB
|
||||||
|
entry::banish($category, errors: $errors);
|
||||||
|
|
||||||
|
// Deleting the category in ArangoDB
|
||||||
|
document::delete($category->__document(), errors: $errors);
|
||||||
|
|
||||||
|
// Incrementing the counter of deleted categories
|
||||||
|
++$categories_deleted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deleting old products
|
||||||
|
foreach (
|
||||||
|
product::_read(
|
||||||
|
/* filter: sprintf('%s - d.updated > 3600', time()), */
|
||||||
|
sort: 'd.updated DESC',
|
||||||
|
amount: 100000,
|
||||||
|
errors: $errors
|
||||||
|
) as $document
|
||||||
|
) {
|
||||||
|
// Iterating over products
|
||||||
|
|
||||||
|
// Initializing the category
|
||||||
|
$product = new product(document: $document);
|
||||||
|
|
||||||
|
if (
|
||||||
|
$product instanceof product
|
||||||
|
&& array_search($product->identifier, $handled['products']) === false
|
||||||
|
) {
|
||||||
|
// Not found identifier of the product in the buffer of handled categories and products
|
||||||
|
|
||||||
|
// Deleting images of the product from storage
|
||||||
|
static::delete(STORAGE . DIRECTORY_SEPARATOR . 'products' . DIRECTORY_SEPARATOR . $product->identifier, errors: $errors);
|
||||||
|
|
||||||
|
// Deleting entries of the product in ArangoDB
|
||||||
|
entry::banish($product, errors: $errors);
|
||||||
|
|
||||||
|
// Deleting the product in ArangoDB
|
||||||
|
document::delete($product->__document(), errors: $errors);
|
||||||
|
|
||||||
|
// Incrementing the counter of deleted products
|
||||||
|
++$products_deleted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Counting new documents
|
||||||
|
$categories_new = collection::count(category::COLLECTION, errors: $errors);
|
||||||
|
$products_new = collection::count(product::COLLECTION, errors: $errors);
|
||||||
|
} catch (exception $e) {
|
||||||
|
// Writing to the registry of errors
|
||||||
|
$errors[] = [
|
||||||
|
'text' => $e->getMessage(),
|
||||||
|
'file' => $e->getFile(),
|
||||||
|
'line' => $e->getLine(),
|
||||||
|
'stack' => $e->getTrace()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,13 +2,12 @@
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace mirzaev\huesos\models;
|
namespace mirzaev\arming_bot\models;
|
||||||
|
|
||||||
// Files of the project
|
// Files of the project
|
||||||
use mirzaev\huesos\models\core,
|
use mirzaev\arming_bot\models\core,
|
||||||
mirzaev\huesos\models\traits\document as document_trait,
|
mirzaev\arming_bot\models\traits\document as arangodb_document_trait,
|
||||||
mirzaev\huesos\models\interfaces\document as document_interface,
|
mirzaev\arming_bot\models\interfaces\document as arangodb_document_interface;
|
||||||
mirzaev\huesos\models\interfaces\collection as collection_interface;
|
|
||||||
|
|
||||||
// Framework for ArangoDB
|
// Framework for ArangoDB
|
||||||
use mirzaev\arangodb\collection,
|
use mirzaev\arangodb\collection,
|
||||||
|
@ -21,14 +20,14 @@ use exception;
|
||||||
/**
|
/**
|
||||||
* Model of category
|
* Model of category
|
||||||
*
|
*
|
||||||
* @package mirzaev\huesos\models
|
* @package mirzaev\arming_bot\models
|
||||||
*
|
*
|
||||||
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
||||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||||
*/
|
*/
|
||||||
final class category extends core implements document_interface, collection_interface
|
final class category extends core implements arangodb_document_interface
|
||||||
{
|
{
|
||||||
use document_trait;
|
use arangodb_document_trait;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Name of the collection in ArangoDB
|
* Name of the collection in ArangoDB
|
||||||
|
@ -69,7 +68,7 @@ final class category extends core implements document_interface, collection_inte
|
||||||
],
|
],
|
||||||
errors: $errors
|
errors: $errors
|
||||||
);
|
);
|
||||||
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::COLLECTION);
|
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
|
||||||
} catch (exception $e) {
|
} catch (exception $e) {
|
||||||
// Writing to the registry of errors
|
// Writing to the registry of errors
|
||||||
$errors[] = [
|
$errors[] = [
|
|
@ -2,13 +2,12 @@
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace mirzaev\huesos\models;
|
namespace mirzaev\arming_bot\models;
|
||||||
|
|
||||||
// Files of the project
|
// Files of the project
|
||||||
use mirzaev\huesos\models\core,
|
use mirzaev\arming_bot\models\core,
|
||||||
mirzaev\huesos\models\traits\document as document_trait,
|
mirzaev\arming_bot\models\traits\document as arangodb_document_trait,
|
||||||
mirzaev\huesos\models\interfaces\document as document_interface,
|
mirzaev\arming_bot\models\interfaces\document as arangodb_document_interface;
|
||||||
mirzaev\huesos\models\interfaces\collection as collection_interface;
|
|
||||||
|
|
||||||
// Framework for ArangoDB
|
// Framework for ArangoDB
|
||||||
use mirzaev\arangodb\enumerations\collection\type;
|
use mirzaev\arangodb\enumerations\collection\type;
|
||||||
|
@ -16,14 +15,14 @@ use mirzaev\arangodb\enumerations\collection\type;
|
||||||
/**
|
/**
|
||||||
* Model of connect
|
* Model of connect
|
||||||
*
|
*
|
||||||
* @package mirzaev\huesos\models
|
* @package mirzaev\arming_bot\models
|
||||||
*
|
*
|
||||||
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
||||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||||
*/
|
*/
|
||||||
final class connect extends core implements document_interface, collection_interface
|
final class connect extends core implements arangodb_document_interface
|
||||||
{
|
{
|
||||||
use document_trait;
|
use arangodb_document_trait;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Name of the collection in ArangoDB
|
* Name of the collection in ArangoDB
|
|
@ -2,17 +2,18 @@
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace mirzaev\huesos\models;
|
namespace mirzaev\arming_bot\models;
|
||||||
|
|
||||||
// Framework for PHP
|
// Framework for PHP
|
||||||
use mirzaev\minimal\model;
|
use mirzaev\minimal\model;
|
||||||
|
|
||||||
// Library for ArangoDB
|
|
||||||
use ArangoDBClient\Document as _document;
|
|
||||||
|
|
||||||
// Framework for ArangoDB
|
// Framework for ArangoDB
|
||||||
use mirzaev\arangodb\connection as arangodb,
|
use mirzaev\arangodb\connection as arangodb,
|
||||||
mirzaev\arangodb\collection;
|
mirzaev\arangodb\collection,
|
||||||
|
mirzaev\arangodb\enumerations\collection\type;
|
||||||
|
|
||||||
|
// Libraries for ArangoDB
|
||||||
|
use ArangoDBClient\Document as _document;
|
||||||
|
|
||||||
// Built-in libraries
|
// Built-in libraries
|
||||||
use exception;
|
use exception;
|
||||||
|
@ -20,13 +21,18 @@ use exception;
|
||||||
/**
|
/**
|
||||||
* Core of models
|
* Core of models
|
||||||
*
|
*
|
||||||
* @package mirzaev\huesos\models
|
* @package mirzaev\arming_bot\models
|
||||||
*
|
*
|
||||||
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
||||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||||
*/
|
*/
|
||||||
class core extends model
|
class core extends model
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* Postfix for name of models files
|
||||||
|
*/
|
||||||
|
final public const string POSTFIX = '';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Path to the file with settings of connecting to the ArangoDB
|
* Path to the file with settings of connecting to the ArangoDB
|
||||||
*/
|
*/
|
||||||
|
@ -34,20 +40,28 @@ class core extends model
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instance of the session of ArangoDB
|
* Instance of the session of ArangoDB
|
||||||
*
|
|
||||||
* @todo ПЕРЕДЕЛАТЬ В php 8.4
|
|
||||||
*/
|
*/
|
||||||
protected static arangodb $arangodb;
|
protected static arangodb $arangodb;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name of the collection in ArangoDB
|
||||||
|
*/
|
||||||
|
public const string COLLECTION = 'THIS_COLLECTION_SHOULD_NOT_EXIST';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type of the collection in ArangoDB
|
||||||
|
*/
|
||||||
|
public const type TYPE = type::document;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor of an instance
|
* Constructor of an instance
|
||||||
*
|
*
|
||||||
* @param bool $initialize Initialize ...?
|
* @param bool $initialize Initialize a model?
|
||||||
* @param ?arangodb $arangodb Instance of a session of ArangoDB
|
* @param ?arangodb $arangodb Instance of a session of ArangoDB
|
||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function __construct(bool $initialize = false, ?arangodb $arangodb = null)
|
public function __construct(bool $initialize = true, ?arangodb $arangodb = null)
|
||||||
{
|
{
|
||||||
// For the extends system
|
// For the extends system
|
||||||
parent::__construct($initialize);
|
parent::__construct($initialize);
|
||||||
|
@ -55,8 +69,17 @@ class core extends model
|
||||||
if ($initialize) {
|
if ($initialize) {
|
||||||
// Initializing is requested
|
// Initializing is requested
|
||||||
|
|
||||||
// Writing an instance of a session of ArangoDB to the property
|
if (isset($arangodb)) {
|
||||||
self::$arangodb = $arangodb ?? new arangodb(require static::ARANGODB);
|
// Recieved an instance of a session of ArangoDB
|
||||||
|
|
||||||
|
// Writing an instance of a session of ArangoDB to the property
|
||||||
|
$this->__set('arangodb', $arangodb);
|
||||||
|
} else {
|
||||||
|
// Not recieved an instance of a session of ArangoDB
|
||||||
|
|
||||||
|
// Initializing of an instance of a session of ArangoDB
|
||||||
|
$this->__get('arangodb');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,7 +104,7 @@ class core extends model
|
||||||
string $return = 'd',
|
string $return = 'd',
|
||||||
array $parameters = [],
|
array $parameters = [],
|
||||||
array &$errors = []
|
array &$errors = []
|
||||||
): _document|static|array|null {
|
): _document|array|null {
|
||||||
try {
|
try {
|
||||||
if (collection::initialize(static::COLLECTION, static::TYPE)) {
|
if (collection::initialize(static::COLLECTION, static::TYPE)) {
|
||||||
// Initialized the collection
|
// Initialized the collection
|
||||||
|
@ -94,40 +117,23 @@ class core extends model
|
||||||
%s
|
%s
|
||||||
%s
|
%s
|
||||||
LIMIT @offset, @amount
|
LIMIT @offset, @amount
|
||||||
RETURN %s
|
RETURN @return
|
||||||
AQL,
|
AQL,
|
||||||
empty($filter) ? '' : "FILTER $filter",
|
empty($filter) ? '' : "FILTER $filter",
|
||||||
empty($sort) ? '' : "SORT $sort",
|
empty($sort) ? '' : "SORT $sort",
|
||||||
empty($return) ? 'd' : $return
|
|
||||||
),
|
),
|
||||||
[
|
[
|
||||||
'@collection' => static::COLLECTION,
|
'@collection' => static::COLLECTION,
|
||||||
'offset' => --$page <= 0 ? 0 : $page * $amount,
|
'offset' => --$page <= 0 ? 0 : $page * $amount,
|
||||||
'amount' => $amount
|
'amount' => $amount,
|
||||||
|
'return' => $return
|
||||||
] + $parameters,
|
] + $parameters,
|
||||||
errors: $errors
|
errors: $errors
|
||||||
);
|
);
|
||||||
|
|
||||||
if ($amount === 1 && $result instanceof _document) {
|
|
||||||
// Received only 1 document and @todo rebuild
|
|
||||||
|
|
||||||
// Initializing the object
|
|
||||||
$object = new static;
|
|
||||||
|
|
||||||
if (method_exists($object, '__document')) {
|
|
||||||
// Object can implement a document from ArangoDB
|
|
||||||
|
|
||||||
// Writing the instance of document from ArangoDB to the implement object
|
|
||||||
$object->__document($result);
|
|
||||||
|
|
||||||
// Exit (success)
|
|
||||||
return $object;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exit (success)
|
// Exit (success)
|
||||||
return $result;
|
return is_array($result) ? $result : [$result];
|
||||||
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::COLLECTION);
|
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
|
||||||
} catch (exception $e) {
|
} catch (exception $e) {
|
||||||
// Writing to registry of errors
|
// Writing to registry of errors
|
||||||
$errors[] = [
|
$errors[] = [
|
||||||
|
@ -154,9 +160,26 @@ class core extends model
|
||||||
{
|
{
|
||||||
match ($name) {
|
match ($name) {
|
||||||
'arangodb' => (function () use ($value) {
|
'arangodb' => (function () use ($value) {
|
||||||
if (isset(static::$arangodb)) throw new exception('Forbidden to reinitialize the session of ArangoDB ($this::$arangodb)', 500);
|
if ($this->__isset('arangodb')) {
|
||||||
else if ($value instanceof arangodb) self::$arangodb = $value;
|
// Is alredy initialized
|
||||||
else throw new exception('Session of connection to ArangoDB ($this::$arangodb) is need to be mirzaev\arangodb\connection', 500);
|
|
||||||
|
// Exit (fail)
|
||||||
|
throw new exception('Forbidden to reinitialize the session of ArangoDB ($this::$arangodb)', 500);
|
||||||
|
} else {
|
||||||
|
// Is not already initialized
|
||||||
|
|
||||||
|
if ($value instanceof arangodb) {
|
||||||
|
// Recieved an appropriate value
|
||||||
|
|
||||||
|
// Writing the property and exit (success)
|
||||||
|
self::$arangodb = $value;
|
||||||
|
} else {
|
||||||
|
// Recieved an inappropriate value
|
||||||
|
|
||||||
|
// Exit (fail)
|
||||||
|
throw new exception('Session of ArangoDB ($this::$arangodb) is need to be mirzaev\arangodb\connection', 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
})(),
|
})(),
|
||||||
default => parent::__set($name, $value)
|
default => parent::__set($name, $value)
|
||||||
};
|
};
|
||||||
|
@ -172,6 +195,22 @@ class core extends model
|
||||||
public function __get(string $name): mixed
|
public function __get(string $name): mixed
|
||||||
{
|
{
|
||||||
return match ($name) {
|
return match ($name) {
|
||||||
|
'arangodb' => (function () {
|
||||||
|
try {
|
||||||
|
if (!$this->__isset('arangodb')) {
|
||||||
|
// Is not initialized
|
||||||
|
|
||||||
|
// Initializing of a default value from settings
|
||||||
|
$this->__set('arangodb', new arangodb(require static::ARANGODB));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit (success)
|
||||||
|
return self::$arangodb;
|
||||||
|
} catch (exception) {
|
||||||
|
// Exit (fail)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})(),
|
||||||
default => parent::__get($name)
|
default => parent::__get($name)
|
||||||
};
|
};
|
||||||
}
|
}
|
|
@ -2,13 +2,12 @@
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace mirzaev\huesos\models;
|
namespace mirzaev\arming_bot\models;
|
||||||
|
|
||||||
// Files of the project
|
// Files of the project
|
||||||
use mirzaev\huesos\models\core,
|
use mirzaev\arming_bot\models\core,
|
||||||
mirzaev\huesos\models\traits\document as document_trait,
|
mirzaev\arming_bot\models\traits\document as arangodb_document_trait,
|
||||||
mirzaev\huesos\models\interfaces\document as document_interface,
|
mirzaev\arming_bot\models\interfaces\document as arangodb_document_interface;
|
||||||
mirzaev\huesos\models\interfaces\collection as collection_interface;
|
|
||||||
|
|
||||||
// Library for ArangoDB
|
// Library for ArangoDB
|
||||||
use ArangoDBClient\Document as _document;
|
use ArangoDBClient\Document as _document;
|
||||||
|
@ -24,14 +23,14 @@ use exception;
|
||||||
/**
|
/**
|
||||||
* Model of entry
|
* Model of entry
|
||||||
*
|
*
|
||||||
* @package mirzaev\huesos\models
|
* @package mirzaev\arming_bot\models
|
||||||
*
|
*
|
||||||
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
||||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||||
*/
|
*/
|
||||||
final class entry extends core implements document_interface, collection_interface
|
final class entry extends core implements arangodb_document_interface
|
||||||
{
|
{
|
||||||
use document_trait;
|
use arangodb_document_trait;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Name of the collection in ArangoDB
|
* Name of the collection in ArangoDB
|
||||||
|
@ -73,9 +72,9 @@ final class entry extends core implements document_interface, collection_interfa
|
||||||
],
|
],
|
||||||
errors: $errors
|
errors: $errors
|
||||||
);
|
);
|
||||||
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::COLLECTION);
|
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
|
||||||
} else throw new exception('Failed to initialize ' . $to::TYPE->name . ' collection: ' . $to::COLLECTION);
|
} else throw new exception('Failed to initialize ' . $to::TYPE . ' collection: ' . $to::COLLECTION);
|
||||||
} else throw new exception('Failed to initialize ' . $from::TYPE->name . ' collection: ' . $from::COLLECTION);
|
} else throw new exception('Failed to initialize ' . $from::TYPE . ' collection: ' . $from::COLLECTION);
|
||||||
} catch (exception $e) {
|
} catch (exception $e) {
|
||||||
// Writing to the registry of errors
|
// Writing to the registry of errors
|
||||||
$errors[] = [
|
$errors[] = [
|
||||||
|
@ -91,54 +90,40 @@ final class entry extends core implements document_interface, collection_interfa
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ascendants
|
* Find ascendants
|
||||||
*
|
*
|
||||||
* Search for ascendants that are not descendants for anyone
|
* Find ascendants that are not descendants for anyone
|
||||||
*
|
*
|
||||||
* @param category|product $descendant Descendant document
|
* @param category|product $descendant Descendant document
|
||||||
* @param string|null $return Return (AQL)
|
|
||||||
* @param array $parameters Binded parameters for placeholders ['placeholder' => parameter]
|
|
||||||
* @param array &$errors Registry of errors
|
* @param array &$errors Registry of errors
|
||||||
*
|
*
|
||||||
* @return array|null Ascendants that are not descendants for anyone, if found
|
* @return array|null Ascendants that are not descendants for anyone, if found
|
||||||
*/
|
*/
|
||||||
public static function ascendants(
|
public static function ascendants(
|
||||||
category|product $descendant,
|
category|product $descendant,
|
||||||
?string $return = 'DISTINCT ascendant',
|
|
||||||
array $parameters = [],
|
|
||||||
array &$errors = []
|
array &$errors = []
|
||||||
): ?array {
|
): ?array {
|
||||||
try {
|
try {
|
||||||
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
|
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
|
||||||
// Initialized the collection
|
// Initialized the collection
|
||||||
|
if ($ascendants = collection::execute(
|
||||||
// Search for ascendants
|
<<<'AQL'
|
||||||
if ($result = collection::execute(
|
FOR d IN @@collection
|
||||||
sprintf(
|
FOR ascendant IN OUTBOUND d @@edge
|
||||||
<<<'AQL'
|
RETURN DISTINCT ascendant
|
||||||
let from = (
|
AQL,
|
||||||
FOR e IN @@edge
|
|
||||||
RETURN DISTINCT e._from
|
|
||||||
)
|
|
||||||
|
|
||||||
FOR d in @@collection
|
|
||||||
FILTER !POSITION(from, d._id)
|
|
||||||
RETURN %s
|
|
||||||
AQL,
|
|
||||||
empty($return) ? 'DISTINCT d' : $return
|
|
||||||
),
|
|
||||||
[
|
[
|
||||||
'@collection' => $descendant::COLLECTION,
|
'@collection' => $descendant::COLLECTION,
|
||||||
'@edge' => static::COLLECTION
|
'@edge' => static::COLLECTION
|
||||||
] + $parameters,
|
],
|
||||||
errors: $errors
|
errors: $errors
|
||||||
)) {
|
)) {
|
||||||
// Found ascendants
|
// Found ascendants
|
||||||
|
|
||||||
// Exit (success)
|
// Exit (success)
|
||||||
return is_array($result) ? $result : [$result];
|
return is_array($ascendants) ? $ascendants : [$ascendants];
|
||||||
} else return [];
|
} else return [];
|
||||||
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::COLLECTION);
|
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
|
||||||
} catch (exception $e) {
|
} catch (exception $e) {
|
||||||
// Writing to the registry of errors
|
// Writing to the registry of errors
|
||||||
$errors[] = [
|
$errors[] = [
|
||||||
|
@ -194,9 +179,9 @@ final class entry extends core implements document_interface, collection_interfa
|
||||||
// Exit (success)
|
// Exit (success)
|
||||||
return is_array($entry) ? $entry[0] : $entry;
|
return is_array($entry) ? $entry[0] : $entry;
|
||||||
} else return null;
|
} else return null;
|
||||||
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::COLLECTION);
|
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
|
||||||
} else throw new exception('Failed to initialize ' . $to::TYPE->name . ' collection: ' . $to::COLLECTION);
|
} else throw new exception('Failed to initialize ' . $to::TYPE . ' collection: ' . $to::COLLECTION);
|
||||||
} else throw new exception('Failed to initialize ' . $from::TYPE->name . ' collection: ' . $from::COLLECTION);
|
} else throw new exception('Failed to initialize ' . $from::TYPE . ' collection: ' . $from::COLLECTION);
|
||||||
} catch (exception $e) {
|
} catch (exception $e) {
|
||||||
// Writing to the registry of errors
|
// Writing to the registry of errors
|
||||||
$errors[] = [
|
$errors[] = [
|
||||||
|
@ -223,12 +208,8 @@ final class entry extends core implements document_interface, collection_interfa
|
||||||
* @param category|product $document Ascendant document
|
* @param category|product $document Ascendant document
|
||||||
* @param string|null $filter Expression for filtering (AQL)
|
* @param string|null $filter Expression for filtering (AQL)
|
||||||
* @param string|null $sort Expression for sorting (AQL)
|
* @param string|null $sort Expression for sorting (AQL)
|
||||||
* @param int $depth Amount of nodes for traversal search (subcategories/products inside subcategories...)
|
* @param int $page Страница
|
||||||
* @param int $page Page
|
* @param int $amount Количество товаров на странице
|
||||||
* @param int $amount Amount of documents per page
|
|
||||||
* @param string|null $categories_merge Expression with paremeters to return for categories (AQL)
|
|
||||||
* @param string|null $products_merge Expression with paremeters to return for products (AQL)
|
|
||||||
* @param array $parameters Binded parameters for placeholders ['placeholder' => parameter]
|
|
||||||
* @param array &$errors Registry of errors
|
* @param array &$errors Registry of errors
|
||||||
*
|
*
|
||||||
* @return array Массив с найденными вхождениями (может быть пустым)
|
* @return array Массив с найденными вхождениями (может быть пустым)
|
||||||
|
@ -237,12 +218,8 @@ final class entry extends core implements document_interface, collection_interfa
|
||||||
category|product $document,
|
category|product $document,
|
||||||
?string $filter = 'v.deleted != true && v.hidden != true',
|
?string $filter = 'v.deleted != true && v.hidden != true',
|
||||||
?string $sort = 'v.position ASC, v.created DESC',
|
?string $sort = 'v.position ASC, v.created DESC',
|
||||||
int $depth = 1,
|
|
||||||
int $page = 1,
|
int $page = 1,
|
||||||
int $amount = 100,
|
int $amount = 100,
|
||||||
?string $categories_merge = null,
|
|
||||||
?string $products_merge = null,
|
|
||||||
array $parameters = [],
|
|
||||||
array &$errors = []
|
array &$errors = []
|
||||||
): array {
|
): array {
|
||||||
try {
|
try {
|
||||||
|
@ -250,34 +227,34 @@ final class entry extends core implements document_interface, collection_interfa
|
||||||
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
|
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
|
||||||
// Initialized collections
|
// Initialized collections
|
||||||
|
|
||||||
// Execute and exit (success)
|
if ($documents = collection::execute(
|
||||||
return is_array($result = collection::execute(
|
|
||||||
sprintf(
|
sprintf(
|
||||||
<<<'AQL'
|
<<<'AQL'
|
||||||
FOR v IN 1..%u INBOUND @document GRAPH @graph
|
FOR v IN 1..1 INBOUND @document GRAPH @graph
|
||||||
%s
|
%s
|
||||||
%s
|
%s
|
||||||
LIMIT @offset, @amount
|
LIMIT @offset, @amount
|
||||||
RETURN DISTINCT IS_SAME_COLLECTION(@category, v._id) ? MERGE(v, {_type: @category%s}) : MERGE(v, {_type: @product%s})
|
LET _type = (FOR v2 IN INBOUND v._id GRAPH @graph RETURN v2)[0] ? "category" : "product"
|
||||||
|
RETURN MERGE(v, {_type})
|
||||||
AQL,
|
AQL,
|
||||||
$depth,
|
|
||||||
empty($filter) ? '' : "FILTER $filter",
|
empty($filter) ? '' : "FILTER $filter",
|
||||||
empty($sort) ? '' : "SORT $sort",
|
empty($sort) ? '' : "SORT $sort",
|
||||||
empty($categories_merge) ? '' : ", $categories_merge",
|
|
||||||
empty($products_merge) ? '' : ", $products_merge"
|
|
||||||
),
|
),
|
||||||
[
|
[
|
||||||
'category' => category::COLLECTION,
|
'grapth' => 'catalog',
|
||||||
'product' => product::COLLECTION,
|
|
||||||
'graph' => 'catalog',
|
|
||||||
'document' => $document->getId(),
|
'document' => $document->getId(),
|
||||||
'offset' => --$page <= 0 ? $page = 0 : $page * $amount,
|
'offset' => --$page <= 0 ? $page = 0 : $page * $amount,
|
||||||
'amount' => $amount
|
'amount' => $amount
|
||||||
] + $parameters,
|
],
|
||||||
errors: $errors
|
errors: $errors
|
||||||
)) ? $result : [$result];
|
)) {
|
||||||
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::COLLECTION);
|
// Fount entries
|
||||||
} else throw new exception('Failed to initialize ' . $document::TYPE->name . ' collection: ' . $document::COLLECTION);
|
|
||||||
|
// Возврат (успех)
|
||||||
|
return is_array($documents) ? $documents : [$documents];
|
||||||
|
} else return [];
|
||||||
|
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
|
||||||
|
} else throw new exception('Failed to initialize ' . $document::TYPE . ' collection: ' . $document::COLLECTION);
|
||||||
} catch (exception $e) {
|
} catch (exception $e) {
|
||||||
// Writing to the registry of errors
|
// Writing to the registry of errors
|
||||||
$errors[] = [
|
$errors[] = [
|
||||||
|
@ -323,8 +300,8 @@ final class entry extends core implements document_interface, collection_interfa
|
||||||
],
|
],
|
||||||
errors: $errors
|
errors: $errors
|
||||||
);
|
);
|
||||||
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::COLLECTION);
|
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
|
||||||
} else throw new exception('Failed to initialize ' . $document::TYPE->name . ' collection: ' . $document::COLLECTION);
|
} else throw new exception('Failed to initialize ' . $document::TYPE . ' collection: ' . $document::COLLECTION);
|
||||||
} catch (exception $e) {
|
} catch (exception $e) {
|
||||||
// Writing to the registry of errors
|
// Writing to the registry of errors
|
||||||
$errors[] = [
|
$errors[] = [
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace mirzaev\arming_bot\models\enumerations;
|
||||||
|
|
||||||
|
enum session
|
||||||
|
{
|
||||||
|
case hash_only;
|
||||||
|
case hash_else_address;
|
||||||
|
}
|
|
@ -2,19 +2,20 @@
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace mirzaev\huesos\models\interfaces;
|
namespace mirzaev\arming_bot\models\interfaces;
|
||||||
|
|
||||||
// Library для ArangoDB
|
// Library для ArangoDB
|
||||||
use ArangoDBClient\Document as _document;
|
use ArangoDBClient\Document as _document;
|
||||||
|
|
||||||
|
// Framework for ArangoDB
|
||||||
|
use mirzaev\arangodb\connection as arangodb;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for implementing a document instance from ArangoDB
|
* Interface for implementing a document instance from ArangoDB
|
||||||
*
|
*
|
||||||
* @param _document $document An instance of the ArangoDB document from ArangoDB (protected readonly)
|
* @param _document $document An instance of the ArangoDB document from ArangoDB (protected readonly)
|
||||||
*
|
*
|
||||||
* @package mirzaev\huesos\models\traits
|
* @package mirzaev\arming_bot\models\traits
|
||||||
*
|
|
||||||
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
|
||||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||||
*/
|
*/
|
||||||
interface document
|
interface document
|
|
@ -2,37 +2,30 @@
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace mirzaev\huesos\models;
|
namespace mirzaev\arming_bot\models;
|
||||||
|
|
||||||
// Files of the project
|
// Files of the project
|
||||||
use mirzaev\huesos\models\core,
|
use mirzaev\arming_bot\models\core,
|
||||||
mirzaev\huesos\models\traits\document as document_trait,
|
mirzaev\arming_bot\models\traits\document as arangodb_document_trait;
|
||||||
mirzaev\huesos\models\interfaces\document as document_interface,
|
|
||||||
mirzaev\huesos\models\interfaces\collection as collection_interface,
|
|
||||||
mirzaev\huesos\models\enumerations\language,
|
|
||||||
mirzaev\huesos\models\enumerations\currency;
|
|
||||||
|
|
||||||
// Framework for ArangoDB
|
// Framework for ArangoDB
|
||||||
use mirzaev\arangodb\collection,
|
use mirzaev\arangodb\collection,
|
||||||
mirzaev\arangodb\document;
|
mirzaev\arangodb\document;
|
||||||
|
|
||||||
// Library for ArangoDB
|
|
||||||
use ArangoDBClient\Document as _document;
|
|
||||||
|
|
||||||
// Built-in libraries
|
// Built-in libraries
|
||||||
use exception;
|
use exception;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Model of a product
|
* Model of a product
|
||||||
*
|
*
|
||||||
* @package mirzaev\huesos\models
|
* @package mirzaev\arming_bot\models
|
||||||
*
|
*
|
||||||
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
||||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||||
*/
|
*/
|
||||||
final class product extends core implements document_interface, collection_interface
|
final class product extends core
|
||||||
{
|
{
|
||||||
use document_trait;
|
use arangodb_document_trait;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Name of the collection in ArangoDB
|
* Name of the collection in ArangoDB
|
||||||
|
@ -48,8 +41,6 @@ final class product extends core implements document_interface, collection_inter
|
||||||
* @param float $cost Cost
|
* @param float $cost Cost
|
||||||
* @param float $weight Weight
|
* @param float $weight Weight
|
||||||
* @param array $dimensions Dimensions ['x' => 0.0, 'y' => 0.0, 'z' => 0.0]
|
* @param array $dimensions Dimensions ['x' => 0.0, 'y' => 0.0, 'z' => 0.0]
|
||||||
* @param array|null $brand Brand [['en' => value], ['ru' => значение]]
|
|
||||||
* @param array|null $compatibility Compatibility [['en' => value], ['ru' => значение]]
|
|
||||||
* @param array $images Images (first will be thumbnail)
|
* @param array $images Images (first will be thumbnail)
|
||||||
* @param int|null $position Position for sorting in the catalog (ASC)
|
* @param int|null $position Position for sorting in the catalog (ASC)
|
||||||
* @param array $data Data
|
* @param array $data Data
|
||||||
|
@ -64,11 +55,9 @@ final class product extends core implements document_interface, collection_inter
|
||||||
int $identifier,
|
int $identifier,
|
||||||
array $name = [['en' => 'ERROR']],
|
array $name = [['en' => 'ERROR']],
|
||||||
?array $description = [['en' => 'ERROR']],
|
?array $description = [['en' => 'ERROR']],
|
||||||
array $cost = [['usd' => 0]],
|
float $cost = 0,
|
||||||
float $weight = 0,
|
float $weight = 0,
|
||||||
array $dimensions = ['x' => 0, 'y' => 0, 'z' => 0],
|
array $dimensions = ['x' => 0, 'y' => 0, 'z' => 0],
|
||||||
?array $brand = [['en' => 'ERROR']],
|
|
||||||
?array $compatibility = [['en' => 'ERROR']],
|
|
||||||
array $images = [],
|
array $images = [],
|
||||||
?int $position = null,
|
?int $position = null,
|
||||||
array $data = [],
|
array $data = [],
|
||||||
|
@ -92,15 +81,13 @@ final class product extends core implements document_interface, collection_inter
|
||||||
'y' => $dimensions['y'] ?? 0,
|
'y' => $dimensions['y'] ?? 0,
|
||||||
'z' => $dimensions['z'] ?? 0,
|
'z' => $dimensions['z'] ?? 0,
|
||||||
],
|
],
|
||||||
'brand' => $brand,
|
|
||||||
'compatibility' => $compatibility,
|
|
||||||
'images' => $images,
|
'images' => $images,
|
||||||
'position' => $position,
|
'position' => $position,
|
||||||
'version' => ROBOT_VERSION
|
'version' => ROBOT_VERSION
|
||||||
] + $data,
|
] + $data,
|
||||||
errors: $errors
|
errors: $errors
|
||||||
);
|
);
|
||||||
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::COLLECTION);
|
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
|
||||||
} catch (exception $e) {
|
} catch (exception $e) {
|
||||||
// Writing to the registry of errors
|
// Writing to the registry of errors
|
||||||
$errors[] = [
|
$errors[] = [
|
||||||
|
@ -124,14 +111,10 @@ final class product extends core implements document_interface, collection_inter
|
||||||
* @param int $page Page
|
* @param int $page Page
|
||||||
* @param int $amount Amount per page
|
* @param int $amount Amount per page
|
||||||
* @param string|null $return Return (AQL)
|
* @param string|null $return Return (AQL)
|
||||||
* @param language|null $language Language
|
* @param string $language Language code (en, ru...)
|
||||||
* @param currency|null $currency Currency
|
|
||||||
* @param array $parameters Binded parameters for placeholders ['placeholder' => parameter]
|
|
||||||
* @param array &$errors Registry of errors
|
* @param array &$errors Registry of errors
|
||||||
*
|
*
|
||||||
* @return array|static Found products or instance of the product from ArangoDB (can be empty)
|
* @return array Массив с найденными товарами (может быть пустым)
|
||||||
*
|
|
||||||
* @todo убрать language и currency
|
|
||||||
*/
|
*/
|
||||||
public static function read(
|
public static function read(
|
||||||
?string $search = null,
|
?string $search = null,
|
||||||
|
@ -139,93 +122,81 @@ final class product extends core implements document_interface, collection_inter
|
||||||
?string $sort = 'd.position ASC, d.created DESC',
|
?string $sort = 'd.position ASC, d.created DESC',
|
||||||
int $page = 1,
|
int $page = 1,
|
||||||
int $amount = 100,
|
int $amount = 100,
|
||||||
?string $return = 'DISTINCT d',
|
?string $return = 'd',
|
||||||
?language $language = null,
|
string $language = 'en',
|
||||||
?currency $currency = null,
|
|
||||||
array $parameters = [],
|
|
||||||
array &$errors = []
|
array &$errors = []
|
||||||
): array|static {
|
): array {
|
||||||
try {
|
try {
|
||||||
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
|
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
|
||||||
// Initialized the collection
|
// Initialized the collection
|
||||||
|
|
||||||
// Initializing of the language parameter
|
// Initializing the query
|
||||||
if ($language instanceof language) $parameters['language'] = $language->name;
|
$aql = <<<'AQL'
|
||||||
|
FOR d IN @@collection
|
||||||
|
AQL;
|
||||||
|
|
||||||
// Initializing of the currency parameter
|
if ($search) {
|
||||||
if ($currency instanceof currency) $parameters['currency'] = $currency->name;
|
// Requested search
|
||||||
|
|
||||||
// Initializing parameters for search
|
// Writing to the query
|
||||||
if ($search) $parameters += [
|
$aql .= <<<'AQL'
|
||||||
'search' => $search,
|
SEARCH
|
||||||
'analyzer' => 'text_' . $language->name ?? language::en->name
|
|
||||||
];
|
|
||||||
|
|
||||||
// Search for products
|
|
||||||
$result = collection::execute(
|
|
||||||
sprintf(
|
|
||||||
<<<'AQL'
|
|
||||||
FOR d IN @@collection %s
|
|
||||||
%s
|
|
||||||
%s
|
|
||||||
LIMIT @offset, @amount
|
|
||||||
RETURN %s
|
|
||||||
AQL,
|
|
||||||
empty($search) ? '' : <<<'AQL'
|
|
||||||
SEARCH
|
|
||||||
LEVENSHTEIN_MATCH(
|
LEVENSHTEIN_MATCH(
|
||||||
d.name.@language,
|
d.name.@language,
|
||||||
TOKENS(@search, @analyzer)[0],
|
TOKENS(@search, @analyzer)[0],
|
||||||
1,
|
1,
|
||||||
false
|
false
|
||||||
) OR
|
) OR
|
||||||
LEVENSHTEIN_MATCH(
|
levenshtein_match(
|
||||||
d.description.@language,
|
d.description.@language,
|
||||||
TOKENS(@search, @analyzer)[0],
|
tokens(@search, @analyzer)[0],
|
||||||
1,
|
1,
|
||||||
false
|
false
|
||||||
) OR
|
) OR
|
||||||
LEVENSHTEIN_MATCH(
|
levenshtein_match(
|
||||||
d.compatibility.@language,
|
d.compatibility.@language,
|
||||||
TOKENS(@search, @analyzer)[0],
|
tokens(@search, @analyzer)[0],
|
||||||
1,
|
1,
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
|
AQL;
|
||||||
|
|
||||||
|
// Adding sorting
|
||||||
|
if ($sort) $sort = "BM25(d) DESC, $sort";
|
||||||
|
else $sort = "BM25(d) DESC";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reading products
|
||||||
|
$documents = collection::execute(
|
||||||
|
sprintf(
|
||||||
|
$aql . <<<'AQL'
|
||||||
|
%s
|
||||||
|
%s
|
||||||
|
LIMIT @offset, @amount
|
||||||
|
RETURN $s
|
||||||
AQL,
|
AQL,
|
||||||
empty($filter) ? '' : "FILTER $filter",
|
empty($filter) ? '' : "FILTER $filter",
|
||||||
empty($search) ? (empty($sort) ? '' : "SORT $sort") : (empty($sort) ? "SORT BM25(d) DESC" : "SORT BM25(d) DESC, $sort"),
|
empty($sort) ? '' : "SORT $sort",
|
||||||
empty($return) ? 'DISTINCT d' : $return
|
empty($return) ? 'd' : $return
|
||||||
),
|
),
|
||||||
[
|
[
|
||||||
'@collection' => empty($search) ? static::COLLECTION : static::COLLECTION . 's_search',
|
'@collection' => $search ? static::COLLECTION . 's_search' : static::COLLECTION,
|
||||||
|
'search' => $search,
|
||||||
|
'language' => $language,
|
||||||
|
'analyzer' => "text_$language",
|
||||||
'offset' => --$page <= 0 ? $page = 0 : $page * $amount,
|
'offset' => --$page <= 0 ? $page = 0 : $page * $amount,
|
||||||
'amount' => $amount,
|
'amount' => $amount,
|
||||||
] + $parameters,
|
],
|
||||||
errors: $errors
|
errors: $errors
|
||||||
);
|
);
|
||||||
|
|
||||||
if ($amount === 1 && $result instanceof _document) {
|
if ($documents) {
|
||||||
// Found the product @todo need to rebuild this
|
|
||||||
|
|
||||||
// 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');
|
|
||||||
} else if (!empty($result)) {
|
|
||||||
// Found products
|
// Found products
|
||||||
|
|
||||||
// Exit (success)
|
// Exit (success)
|
||||||
return is_array($result) ? $result : [$result];
|
return is_array($documents) ? $documents : [$documents];
|
||||||
}
|
} else return [];
|
||||||
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::COLLECTION);
|
} else throw new exception('Failed to initialize ' . static::COLLECTION . ' collection: ' . static::COLLECTION);
|
||||||
} catch (exception $e) {
|
} catch (exception $e) {
|
||||||
// Writing to the registry of errors
|
// Writing to the registry of errors
|
||||||
$errors[] = [
|
$errors[] = [
|
||||||
|
@ -243,45 +214,36 @@ final class product extends core implements document_interface, collection_inter
|
||||||
/**
|
/**
|
||||||
* Collect parameter from all products
|
* Collect parameter from all products
|
||||||
*
|
*
|
||||||
* @param string $return Return (AQL path)
|
* @param string $name Name of the parameter (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 $name = 'd._key',
|
||||||
array $products = [],
|
|
||||||
language $language = language::en,
|
|
||||||
array $parameters = [],
|
|
||||||
array &$errors = []
|
array &$errors = []
|
||||||
): array {
|
): array {
|
||||||
try {
|
try {
|
||||||
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
|
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
|
||||||
// Initialized the collection
|
// Initialized the collection
|
||||||
|
|
||||||
if ($result = collection::execute(
|
if ($values = collection::execute(
|
||||||
sprintf(
|
<<<'AQL'
|
||||||
<<<'AQL'
|
FOR d IN @@collecton
|
||||||
FOR d IN @@collection
|
RETURN DISTINCT @parameter
|
||||||
%s
|
AQL,
|
||||||
RETURN DISTINCT %s
|
|
||||||
AQL,
|
|
||||||
empty($products) ? '' : 'FILTER POSITION(["' . implode('", "', $products) . '"], d._id)',
|
|
||||||
empty($return) ? 'd._key' : $return
|
|
||||||
),
|
|
||||||
[
|
[
|
||||||
'@collection' => static::COLLECTION,
|
'@collection' => static::COLLECTION,
|
||||||
'language' => $language->name,
|
'parameter' => $name
|
||||||
] + $parameters,
|
],
|
||||||
errors: $errors
|
errors: $errors
|
||||||
)) {
|
)) {
|
||||||
// Found parameters
|
// Found parameters
|
||||||
|
|
||||||
// Exit (success)
|
// Exit (success)
|
||||||
return is_array($result) ? $result : [$result];
|
return $values;
|
||||||
} else return [];
|
} else return [];
|
||||||
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::COLLECTION);
|
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
|
||||||
} catch (exception $e) {
|
} catch (exception $e) {
|
||||||
// Writing to the registry of errors
|
// Writing to the registry of errors
|
||||||
$errors[] = [
|
$errors[] = [
|
|
@ -2,26 +2,21 @@
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace mirzaev\huesos\models;
|
namespace mirzaev\arming_bot\models;
|
||||||
|
|
||||||
// Files of the project
|
// Files of the project
|
||||||
use mirzaev\huesos\models\account,
|
use mirzaev\arming_bot\models\account,
|
||||||
mirzaev\huesos\models\connect,
|
mirzaev\arming_bot\models\connect,
|
||||||
mirzaev\huesos\models\enumerations\session as verification,
|
mirzaev\arming_bot\models\enumerations\session as verification,
|
||||||
mirzaev\huesos\models\traits\status,
|
mirzaev\arming_bot\models\traits\status,
|
||||||
mirzaev\huesos\models\traits\buffer,
|
mirzaev\arming_bot\models\traits\document as arangodb_document_trait,
|
||||||
mirzaev\huesos\models\traits\cart,
|
mirzaev\arming_bot\models\interfaces\document as arangodb_document_interface;
|
||||||
mirzaev\huesos\models\traits\document as document_trait,
|
|
||||||
mirzaev\huesos\models\interfaces\document as document_interface,
|
|
||||||
mirzaev\huesos\models\interfaces\collection as collection_interface,
|
|
||||||
mirzaev\huesos\models\enumerations\language,
|
|
||||||
mirzaev\huesos\models\enumerations\currency;
|
|
||||||
|
|
||||||
// Framework for ArangoDB
|
// Framework for ArangoDB
|
||||||
use mirzaev\arangodb\collection,
|
use mirzaev\arangodb\collection,
|
||||||
mirzaev\arangodb\document;
|
mirzaev\arangodb\document;
|
||||||
|
|
||||||
// Library for ArangoDB
|
// Library для ArangoDB
|
||||||
use ArangoDBClient\Document as _document;
|
use ArangoDBClient\Document as _document;
|
||||||
|
|
||||||
// Built-in libraries
|
// Built-in libraries
|
||||||
|
@ -30,17 +25,14 @@ use exception;
|
||||||
/**
|
/**
|
||||||
* Model of a session
|
* Model of a session
|
||||||
*
|
*
|
||||||
* @package mirzaev\huesos\models
|
* @package mirzaev\arming_bot\models
|
||||||
*
|
*
|
||||||
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
||||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||||
*/
|
*/
|
||||||
final class session extends core implements document_interface, collection_interface
|
final class session extends core implements arangodb_document_interface
|
||||||
{
|
{
|
||||||
use status, document_trait, buffer, cart {
|
use status, arangodb_document_trait;
|
||||||
buffer::write as write;
|
|
||||||
cart::initialize as cart;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Name of the collection in ArangoDB
|
* Name of the collection in ArangoDB
|
||||||
|
@ -50,18 +42,18 @@ final class session extends core implements document_interface, collection_inter
|
||||||
/**
|
/**
|
||||||
* Type of sessions verification
|
* Type of sessions verification
|
||||||
*/
|
*/
|
||||||
final public const verification VERIFICATION = verification::hash_else_address;
|
public const verification VERIFICATION = verification::hash_else_address;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor of instance
|
||||||
*
|
*
|
||||||
* Initialize session and write into the $this->document property
|
* Initialize of a session and write them to the $this->document property
|
||||||
*
|
*
|
||||||
* @param ?string $hash Hash of the session in ArangoDB
|
* @param ?string $hash Hash of the session in ArangoDB
|
||||||
* @param ?int $expires Date of expiring of the session (used for creating a new session)
|
* @param ?int $expires Date of expiring of the session (used for creating a new session)
|
||||||
* @param array &$errors Registry of errors
|
* @param array &$errors Registry of errors
|
||||||
*
|
*
|
||||||
* @return void
|
* @return static
|
||||||
*/
|
*/
|
||||||
public function __construct(?string $hash = null, ?int $expires = null, array &$errors = [])
|
public function __construct(?string $hash = null, ?int $expires = null, array &$errors = [])
|
||||||
{
|
{
|
||||||
|
@ -102,7 +94,7 @@ final class session extends core implements document_interface, collection_inter
|
||||||
RETURN d
|
RETURN d
|
||||||
AQL,
|
AQL,
|
||||||
[
|
[
|
||||||
'@collection' => static::COLLECTION,
|
'@collection' => static::COLLECTION,
|
||||||
'_id' => $_id,
|
'_id' => $_id,
|
||||||
'time' => time()
|
'time' => time()
|
||||||
],
|
],
|
||||||
|
@ -114,14 +106,14 @@ final class session extends core implements document_interface, collection_inter
|
||||||
$session->hash = sodium_bin2hex(sodium_crypto_generichash($_id));
|
$session->hash = sodium_bin2hex(sodium_crypto_generichash($_id));
|
||||||
|
|
||||||
if (document::update($session, errors: $errors)) {
|
if (document::update($session, errors: $errors)) {
|
||||||
// Writed into ArangoDB
|
// Update is writed to ArangoDB
|
||||||
|
|
||||||
// Writing instance of the session document from ArangoDB to the property of the implementing object
|
// Writing instance of the session document from ArangoDB to the property of the implementing object
|
||||||
$this->__document($session);
|
$this->__document($session);
|
||||||
} else throw new exception('Failed to write the session data');
|
} else throw new exception('Failed to write the session data');
|
||||||
} else throw new exception('Failed to create or find just created session');
|
} else throw new exception('Failed to create or find just created session');
|
||||||
}
|
}
|
||||||
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::COLLECTION);
|
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
|
||||||
} catch (exception $e) {
|
} catch (exception $e) {
|
||||||
// Writing to the registry of errors
|
// Writing to the registry of errors
|
||||||
$errors[] = [
|
$errors[] = [
|
||||||
|
@ -149,45 +141,79 @@ final class session extends core implements document_interface, collection_inter
|
||||||
// Initialized collections
|
// Initialized collections
|
||||||
|
|
||||||
// Search for connected account
|
// Search for connected account
|
||||||
$result = collection::execute(
|
$document = collection::execute(
|
||||||
<<<AQL
|
<<<AQL
|
||||||
FOR v IN INBOUND @session GRAPH @graph
|
FOR v IN INBOUND @session GRAPH sessions
|
||||||
FILTER IS_SAME_COLLECTION(@collection, v._id)
|
|
||||||
SORT v.created DESC
|
SORT v.created DESC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
RETURN v
|
RETURN v
|
||||||
AQL,
|
AQL,
|
||||||
[
|
[
|
||||||
'graph' => 'users',
|
|
||||||
'collection' => account::COLLECTION,
|
|
||||||
'session' => $this->getId()
|
'session' => $this->getId()
|
||||||
],
|
],
|
||||||
errors: $errors
|
errors: $errors
|
||||||
);
|
);
|
||||||
|
|
||||||
if ($result instanceof _document) {
|
if ($document instanceof _document) {
|
||||||
// Found the account
|
// Found connected account
|
||||||
|
|
||||||
// Initializing the object
|
// Initializing the implement object of the instance of sesson document from ArangoDB
|
||||||
$account = new account;
|
$account = new account;
|
||||||
|
|
||||||
if (method_exists($account, '__document')) {
|
// Writing the instance of session document from ArangoDB to the implement object
|
||||||
// Object can implement a document from ArangoDB
|
$account->__document($document);
|
||||||
|
|
||||||
// Abstractioning of parameters
|
// Exit (success)
|
||||||
if (isset($result->language)) $result->language = language::{$result->language};
|
return $account;
|
||||||
if (isset($result->currency)) $result->currency = currency::{$result->currency};
|
|
||||||
|
|
||||||
// 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 return null;
|
||||||
} else throw new exception('Failed to initialize ' . account::TYPE->name . ' collection: ' . account::COLLECTION);
|
} else throw new exception('Failed to initialize ' . account::TYPE . ' collection: ' . account::COLLECTION);
|
||||||
} else throw new exception('Failed to initialize ' . connect::TYPE->name . ' collection: ' . connect::COLLECTION);
|
} else throw new exception('Failed to initialize ' . connect::TYPE . ' collection: ' . connect::COLLECTION);
|
||||||
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connect account to session
|
||||||
|
*
|
||||||
|
* @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
|
||||||
|
*/
|
||||||
|
public function connect(account $account, 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(account::COLLECTION, account::TYPE, errors: $errors)) {
|
||||||
|
// Collections initialized
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
[
|
||||||
|
'_from' => $account->getId(),
|
||||||
|
'_to' => $this->document->getId()
|
||||||
|
],
|
||||||
|
errors: $errors
|
||||||
|
);
|
||||||
|
} else throw new exception('Failed to initialize ' . account::TYPE . ' collection: ' . account::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) {
|
} catch (exception $e) {
|
||||||
// Writing to the registry of errors
|
// Writing to the registry of errors
|
||||||
$errors[] = [
|
$errors[] = [
|
||||||
|
@ -222,7 +248,7 @@ final class session extends core implements document_interface, collection_inter
|
||||||
return collection::execute(
|
return collection::execute(
|
||||||
<<<'AQL'
|
<<<'AQL'
|
||||||
FOR d IN @@collection
|
FOR d IN @@collection
|
||||||
FILTER d.hash == @hash && d.expires > @time && d.active == true
|
FILTER d.hash == @hash && d.expires > $time && d.active == true
|
||||||
RETURN d
|
RETURN d
|
||||||
AQL,
|
AQL,
|
||||||
[
|
[
|
||||||
|
@ -232,7 +258,7 @@ final class session extends core implements document_interface, collection_inter
|
||||||
],
|
],
|
||||||
errors: $errors
|
errors: $errors
|
||||||
);
|
);
|
||||||
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::COLLECTION);
|
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
|
||||||
} catch (exception $e) {
|
} catch (exception $e) {
|
||||||
// Writing to the registry of errors
|
// Writing to the registry of errors
|
||||||
$errors[] = [
|
$errors[] = [
|
||||||
|
@ -268,18 +294,16 @@ final class session extends core implements document_interface, collection_inter
|
||||||
<<<'AQL'
|
<<<'AQL'
|
||||||
FOR d IN @@collection
|
FOR d IN @@collection
|
||||||
FILTER d.address == @address && d.expires > @time && d.active == true
|
FILTER d.address == @address && d.expires > @time && d.active == true
|
||||||
SORT d.updated DESC
|
|
||||||
LIMIT 1
|
|
||||||
RETURN d
|
RETURN d
|
||||||
AQL,
|
AQL,
|
||||||
[
|
[
|
||||||
'@collection' => static::COLLECTION,
|
'@collection' => static::COLLECTION,
|
||||||
'address' => $address,
|
'address' => $address,
|
||||||
'time' => time()
|
'time' => time()
|
||||||
],
|
],
|
||||||
errors: $errors
|
errors: $errors
|
||||||
);
|
);
|
||||||
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::COLLECTION);
|
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
|
||||||
} catch (exception $e) {
|
} catch (exception $e) {
|
||||||
// Writing to the registry of errors
|
// Writing to the registry of errors
|
||||||
$errors[] = [
|
$errors[] = [
|
||||||
|
@ -293,4 +317,44 @@ final class session extends core implements document_interface, collection_inter
|
||||||
// Exit (fail)
|
// Exit (fail)
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write to buffer of the session
|
||||||
|
*
|
||||||
|
* @param array $data Data for merging
|
||||||
|
* @param array &$errors Registry of errors
|
||||||
|
*
|
||||||
|
* @return bool Is data has written into the session document from ArangoDB?
|
||||||
|
*/
|
||||||
|
public function write(array $data, array &$errors = []): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
|
||||||
|
// Initialized the collection
|
||||||
|
|
||||||
|
// 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 data into buffer of the instance of the session document from ArangoDB
|
||||||
|
$this->document->buffer = array_replace_recursive(
|
||||||
|
$this->document->buffer ?? [],
|
||||||
|
[$_SERVER['INTERFACE'] => array_replace_recursive($this->document->buffer[$_SERVER['INTERFACE']] ?? [], $data)]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Writing to ArangoDB and exit (success)
|
||||||
|
return document::update($this->document, errors: $errors);
|
||||||
|
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
|
||||||
|
} catch (exception $e) {
|
||||||
|
// Writing to the registry of errors
|
||||||
|
$errors[] = [
|
||||||
|
'text' => $e->getMessage(),
|
||||||
|
'file' => $e->getFile(),
|
||||||
|
'line' => $e->getLine(),
|
||||||
|
'stack' => $e->getTrace()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit (fail)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -2,21 +2,18 @@
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace mirzaev\huesos\models;
|
namespace mirzaev\arming_bot\models;
|
||||||
|
|
||||||
// Files of the project
|
// Files of the project
|
||||||
use mirzaev\huesos\models\core,
|
use mirzaev\arming_bot\models\core,
|
||||||
mirzaev\huesos\models\traits\document as document_trait,
|
mirzaev\arming_bot\models\traits\document as arangodb_document_trait,
|
||||||
mirzaev\huesos\models\interfaces\document as document_interface,
|
mirzaev\arming_bot\models\interfaces\document as arangodb_document_interface;
|
||||||
mirzaev\huesos\models\interfaces\collection as collection_interface,
|
|
||||||
mirzaev\huesos\models\enumerations\language,
|
|
||||||
mirzaev\huesos\models\enumerations\currency;
|
|
||||||
|
|
||||||
// Framework for ArangoDB
|
// Framework for ArangoDB
|
||||||
use mirzaev\arangodb\collection,
|
use mirzaev\arangodb\collection,
|
||||||
mirzaev\arangodb\document;
|
mirzaev\arangodb\document;
|
||||||
|
|
||||||
// Library for ArangoDB
|
// Library для ArangoDB
|
||||||
use ArangoDBClient\Document as _document;
|
use ArangoDBClient\Document as _document;
|
||||||
|
|
||||||
// Built-in libraries
|
// Built-in libraries
|
||||||
|
@ -25,14 +22,14 @@ use exception;
|
||||||
/**
|
/**
|
||||||
* Model of settings
|
* Model of settings
|
||||||
*
|
*
|
||||||
* @package mirzaev\huesos\models
|
* @package mirzaev\arming_bot\models
|
||||||
*
|
*
|
||||||
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
||||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||||
*/
|
*/
|
||||||
final class settings extends core implements document_interface, collection_interface
|
final class settings extends core implements arangodb_document_interface
|
||||||
{
|
{
|
||||||
use document_trait;
|
use arangodb_document_trait;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Name of the collection in ArangoDB
|
* Name of the collection in ArangoDB
|
||||||
|
@ -54,7 +51,7 @@ final class settings extends core implements document_interface, collection_inte
|
||||||
// Initialized the collection
|
// Initialized the collection
|
||||||
|
|
||||||
// Search for active settings
|
// Search for active settings
|
||||||
$result = collection::execute(
|
$document = collection::execute(
|
||||||
<<<'AQL'
|
<<<'AQL'
|
||||||
FOR d IN @@collection
|
FOR d IN @@collection
|
||||||
FILTER d.status == 'active'
|
FILTER d.status == 'active'
|
||||||
|
@ -68,25 +65,17 @@ final class settings extends core implements document_interface, collection_inte
|
||||||
errors: $errors
|
errors: $errors
|
||||||
);
|
);
|
||||||
|
|
||||||
if ($result instanceof _document) {
|
if ($document instanceof _document) {
|
||||||
// Found active settings
|
// Found active settings
|
||||||
|
|
||||||
// Initializing the object
|
// Initializing the implement object of the instance of settings document from ArangoDB
|
||||||
$settings = new static;
|
$settings = new static;
|
||||||
|
|
||||||
if (method_exists($settings, '__document')) {
|
// Writing the instance of settings document from ArangoDB to the implement object
|
||||||
// Object can implement a document from ArangoDB
|
$settings->__document($document);
|
||||||
|
|
||||||
// Abstractioning of parameters
|
// Exit (success)
|
||||||
if (isset($result->language)) $result->language = language::{$result->language};
|
return $settings;
|
||||||
if (isset($result->currency)) $result->currency = currency::{$result->currency};
|
|
||||||
|
|
||||||
// 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) {
|
} else if ($create) {
|
||||||
// Not found active settings and requested their creating
|
// Not found active settings and requested their creating
|
||||||
|
|
||||||
|
@ -96,7 +85,7 @@ final class settings extends core implements document_interface, collection_inte
|
||||||
// Re-search (without creating) and exit (success || fail)
|
// Re-search (without creating) and exit (success || fail)
|
||||||
return static::active(errors: $errors);
|
return static::active(errors: $errors);
|
||||||
} else throw new exception('Active settings not found');
|
} else throw new exception('Active settings not found');
|
||||||
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::COLLECTION);
|
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
|
||||||
} catch (exception $e) {
|
} catch (exception $e) {
|
||||||
// Writing to the registry of errors
|
// Writing to the registry of errors
|
||||||
$errors[] = [
|
$errors[] = [
|
|
@ -2,14 +2,14 @@
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace mirzaev\huesos\models;
|
namespace mirzaev\arming_bot\models;
|
||||||
|
|
||||||
// Files of the project
|
// Files of the project
|
||||||
use mirzaev\huesos\models\core,
|
use mirzaev\arming_bot\models\core,
|
||||||
mirzaev\huesos\models\traits\document as document_trait,
|
mirzaev\arming_bot\controllers\core as controller,
|
||||||
mirzaev\huesos\models\interfaces\document as document_interface,
|
mirzaev\arming_bot\models\settings,
|
||||||
mirzaev\huesos\models\interfaces\collection as collection_interface,
|
mirzaev\arming_bot\models\traits\document as arangodb_document_trait,
|
||||||
mirzaev\huesos\models\enumerations\language;
|
mirzaev\arming_bot\models\interfaces\document as arangodb_document_interface;
|
||||||
|
|
||||||
// Framework for ArangoDB
|
// Framework for ArangoDB
|
||||||
use mirzaev\arangodb\collection,
|
use mirzaev\arangodb\collection,
|
||||||
|
@ -25,14 +25,14 @@ use exception,
|
||||||
/**
|
/**
|
||||||
* Model of a suspension
|
* Model of a suspension
|
||||||
*
|
*
|
||||||
* @package mirzaev\huesos\models
|
* @package mirzaev\arming_bot\models
|
||||||
*
|
*
|
||||||
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
||||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||||
*/
|
*/
|
||||||
final class suspension extends core implements document_interface, collection_interface
|
final class suspension extends core implements arangodb_document_interface
|
||||||
{
|
{
|
||||||
use document_trait;
|
use arangodb_document_trait;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Name of the collection in ArangoDB
|
* Name of the collection in ArangoDB
|
||||||
|
@ -53,7 +53,7 @@ final class suspension extends core implements document_interface, collection_in
|
||||||
// Initialized the collection
|
// Initialized the collection
|
||||||
|
|
||||||
// Search for active suspension
|
// Search for active suspension
|
||||||
$result = collection::execute(
|
$document = collection::execute(
|
||||||
<<<'AQL'
|
<<<'AQL'
|
||||||
FOR d IN @@collection
|
FOR d IN @@collection
|
||||||
FILTER d.end > @time
|
FILTER d.end > @time
|
||||||
|
@ -68,23 +68,19 @@ final class suspension extends core implements document_interface, collection_in
|
||||||
errors: $errors
|
errors: $errors
|
||||||
);
|
);
|
||||||
|
|
||||||
if ($result instanceof _document) {
|
if ($document instanceof _document) {
|
||||||
// Found active settings
|
// Found active suspension
|
||||||
|
|
||||||
// Initializing the object
|
// Initializing the implement object of the instance of suspension document from ArangoDB
|
||||||
$suspension = new static;
|
$suspension = new static;
|
||||||
|
|
||||||
if (method_exists($suspension, '__document')) {
|
// Writing the instance of suspension document from ArangoDB to the implement object
|
||||||
// Object can implement a document from ArangoDB
|
$suspension->__document($document);
|
||||||
|
|
||||||
// Writing the instance of suspension document from ArangoDB to the implement object
|
// Exit (success)
|
||||||
$suspension->__document($result);
|
return $suspension;
|
||||||
|
|
||||||
// Exit (success)
|
|
||||||
return $suspension;
|
|
||||||
} else throw new exception('Class ' . static::class . ' does not implement a document from ArangoDB');
|
|
||||||
} else return null;
|
} else return null;
|
||||||
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::COLLECTION);
|
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
|
||||||
} catch (exception $e) {
|
} catch (exception $e) {
|
||||||
// Writing to the registry of errors
|
// Writing to the registry of errors
|
||||||
$errors[] = [
|
$errors[] = [
|
||||||
|
@ -102,14 +98,17 @@ final class suspension extends core implements document_interface, collection_in
|
||||||
/**
|
/**
|
||||||
* Generate message about remaining time
|
* Generate message about remaining time
|
||||||
*
|
*
|
||||||
* @param language|null $language Language of the generated text (otherwise used from settings.language)
|
* @param string|null $language Language of the generated text (otherwise used from settings.language)
|
||||||
* @param array &$errors Registry of errors
|
* @param array &$errors Registry of errors
|
||||||
*
|
*
|
||||||
* @return string|null Text: "? days, ? hours and ? minutes"
|
* @return string|null Text: "? days, ? hours and ? minutes"
|
||||||
*/
|
*/
|
||||||
public function message(?language $language = language::en, array &$errors = []): ?string
|
public function message(?string $language = null, array &$errors = []): ?string
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
// Initializing default value
|
||||||
|
$language ??= controller::$settings?->language ?? 'en';
|
||||||
|
|
||||||
// Initializing the time until the suspension ends
|
// Initializing the time until the suspension ends
|
||||||
$difference = date_diff(new datetime('@' . $this->document->end), new datetime());
|
$difference = date_diff(new datetime('@' . $this->document->end), new datetime());
|
||||||
|
|
||||||
|
@ -119,54 +118,54 @@ final class suspension extends core implements document_interface, collection_in
|
||||||
$difference->d,
|
$difference->d,
|
||||||
match ($difference->d > 20 ? $difference->d % 10 : $difference->d % 100) {
|
match ($difference->d > 20 ? $difference->d % 10 : $difference->d % 100) {
|
||||||
1 => match ($language) {
|
1 => match ($language) {
|
||||||
language::en => 'day',
|
'ru' => 'день',
|
||||||
language::ru => 'день',
|
'en' => 'day',
|
||||||
default => 'day'
|
default => 'day'
|
||||||
},
|
},
|
||||||
2, 3, 4 => match ($language) {
|
2, 3, 4 => match ($language) {
|
||||||
language::en => 'days',
|
'ru' => 'дня',
|
||||||
language::ru => 'дня',
|
'en' => 'days',
|
||||||
default => 'days'
|
default => 'days'
|
||||||
},
|
},
|
||||||
default => match ($language) {
|
default => match ($language) {
|
||||||
language::en => 'days',
|
'ru' => 'дней',
|
||||||
language::ru => 'дней',
|
'en' => 'days',
|
||||||
default => 'days'
|
default => 'days'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
$difference->h,
|
$difference->h,
|
||||||
match ($difference->h > 20 ? $difference->h % 10 : $difference->h % 100) {
|
match ($difference->h > 20 ? $difference->h % 10 : $difference->h % 100) {
|
||||||
1 => match ($language) {
|
1 => match ($language) {
|
||||||
language::en => 'hours',
|
'ru' => 'час',
|
||||||
language::ru => 'час',
|
'en' => 'hours',
|
||||||
default => 'hour'
|
default => 'hour'
|
||||||
},
|
},
|
||||||
2, 3, 4 => match ($language) {
|
2, 3, 4 => match ($language) {
|
||||||
language::en => 'hours',
|
'ru' => 'часа',
|
||||||
language::ru => 'часа',
|
'en' => 'hours',
|
||||||
default => 'hours'
|
default => 'hours'
|
||||||
},
|
},
|
||||||
default => match ($language) {
|
default => match ($language) {
|
||||||
language::en => 'hours',
|
'ru' => 'часов',
|
||||||
language::ru => 'часов',
|
'en' => 'hours',
|
||||||
default => 'hours'
|
default => 'hours'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
$difference->i,
|
$difference->i,
|
||||||
match ($difference->i > 20 ? $difference->i % 10 : $difference->i % 100) {
|
match ($difference->i > 20 ? $difference->i % 10 : $difference->i % 100) {
|
||||||
1 => match ($language) {
|
1 => match ($language) {
|
||||||
language::en => 'minute',
|
'ru' => 'минута',
|
||||||
language::ru => 'минута',
|
'en' => 'minute',
|
||||||
default => 'minute'
|
default => 'minute'
|
||||||
},
|
},
|
||||||
2, 3, 4 => match ($language) {
|
2, 3, 4 => match ($language) {
|
||||||
language::en => 'minutes',
|
'ru' => 'минуты',
|
||||||
language::ru => 'минуты',
|
'en' => 'minutes',
|
||||||
default => 'minutes'
|
default => 'minutes'
|
||||||
},
|
},
|
||||||
default => match ($language) {
|
default => match ($language) {
|
||||||
language::en => 'minutes',
|
'ru' => 'минут',
|
||||||
language::ru => 'минут',
|
'en' => 'minutes',
|
||||||
default => 'minutes'
|
default => 'minutes'
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,586 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace mirzaev\arming_bot\models;
|
||||||
|
|
||||||
|
// Files of the project
|
||||||
|
use mirzaev\arming_bot\models\core,
|
||||||
|
mirzaev\arming_bot\controllers\core as controller,
|
||||||
|
mirzaev\arming_bot\models\catalog,
|
||||||
|
mirzaev\arming_bot\models\suspension,
|
||||||
|
mirzaev\arming_bot\models\account;
|
||||||
|
|
||||||
|
// Framework for Telegram
|
||||||
|
use Zanzara\Zanzara,
|
||||||
|
Zanzara\Context,
|
||||||
|
Zanzara\Telegram\Type\Input\InputFile,
|
||||||
|
Zanzara\Telegram\Type\File\Document as telegram_document,
|
||||||
|
Zanzara\Middleware\MiddlewareNode as Node;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model of chat (telegram)
|
||||||
|
*
|
||||||
|
* @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 telegram extends core
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Экранирование символов для Markdown
|
||||||
|
*
|
||||||
|
* @param string $text Текст для экранирования
|
||||||
|
* @param array $exception Символы которые будут исключены из списка для экранирования
|
||||||
|
*
|
||||||
|
* @return string Экранированный текст
|
||||||
|
*/
|
||||||
|
public static function unmarkdown(string $text, array $exceptions = []): string
|
||||||
|
{
|
||||||
|
// Инициализация реестра символом для конвертации
|
||||||
|
$from = array_diff(
|
||||||
|
[
|
||||||
|
'#',
|
||||||
|
'*',
|
||||||
|
'_',
|
||||||
|
'=',
|
||||||
|
'.',
|
||||||
|
'[',
|
||||||
|
']',
|
||||||
|
'(',
|
||||||
|
')',
|
||||||
|
'-',
|
||||||
|
'>',
|
||||||
|
'<',
|
||||||
|
'!',
|
||||||
|
'`'
|
||||||
|
],
|
||||||
|
$exceptions
|
||||||
|
);
|
||||||
|
|
||||||
|
// Инициализация реестра целей для конвертации
|
||||||
|
$to = [];
|
||||||
|
foreach ($from as $symbol) $to[] = "\\$symbol";
|
||||||
|
|
||||||
|
// Конвертация и выход (успех)
|
||||||
|
return str_replace($from, $to, $text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Инициализация запчасти
|
||||||
|
*
|
||||||
|
* Проверяет существование запчасти
|
||||||
|
*
|
||||||
|
* @param string $spare Запчасть
|
||||||
|
*
|
||||||
|
* @return string|bool Запчасть, если найдена, иначе false
|
||||||
|
*/
|
||||||
|
public static function spares(string $spare): string|bool
|
||||||
|
{
|
||||||
|
// Поиск запчастей и выход (успех)
|
||||||
|
return match (mb_strtolower($spare)) {
|
||||||
|
'цевьё' => 'Цевьё',
|
||||||
|
default => false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Главное меню
|
||||||
|
*
|
||||||
|
* Команда: /start
|
||||||
|
*
|
||||||
|
* @param Context $ctx
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function menu(Context $ctx): void
|
||||||
|
{
|
||||||
|
// Инициализация клавиатуры
|
||||||
|
$keyboard = [
|
||||||
|
[
|
||||||
|
['text' => '🛒 Каталог', 'web_app' => ['url' => 'https://arming.dev.mirzaev.sexy']]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
['text' => '🏛️ О компании'],
|
||||||
|
['text' => '💬 Контакты']
|
||||||
|
],
|
||||||
|
[
|
||||||
|
['text' => '🎯 Сообщество']
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($ctx->get('account')?->access['settings']) $keyboard[] = [['text' => '⚙️ Настройки']];
|
||||||
|
|
||||||
|
// Отправка сообщения
|
||||||
|
$ctx->sendMessage(
|
||||||
|
static::unmarkdown(<<<TXT
|
||||||
|
Это сообщение будет отображаться (оно должно быть обязательно) при вызове главного меню командой /start (создаёт кнопки меню снизу)
|
||||||
|
TXT),
|
||||||
|
[
|
||||||
|
'reply_markup' => [
|
||||||
|
'keyboard' => $keyboard,
|
||||||
|
'resize_keyboard' => true
|
||||||
|
],
|
||||||
|
'disable_notification' => true
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Начало работы с чат-роботом
|
||||||
|
*
|
||||||
|
* Команда: /start
|
||||||
|
*
|
||||||
|
* @param Context $ctx
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function start(Context $ctx): void
|
||||||
|
{
|
||||||
|
// Главное меню
|
||||||
|
static::menu($ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Контакты
|
||||||
|
*
|
||||||
|
* Команда: /contacts
|
||||||
|
*
|
||||||
|
* @param Context $ctx
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function contacts(Context $ctx): void
|
||||||
|
{
|
||||||
|
// Отправка сообщения
|
||||||
|
$ctx->sendMessage(static::unmarkdown(<<<TXT
|
||||||
|
Здесь придумать текст для раздела "Контакты"
|
||||||
|
TXT), [
|
||||||
|
'reply_markup' => [
|
||||||
|
'inline_keyboard' => [
|
||||||
|
[
|
||||||
|
['text' => '⚡ Связь с менеджером', 'url' => 'https://t.me/iarming'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
['text' => '📨 Почта', 'callback_data' => 'mail']
|
||||||
|
],
|
||||||
|
[
|
||||||
|
['text' => '🪖 Сайт', 'url' => 'https://arming.ru'],
|
||||||
|
['text' => '🛒 Wildberries', 'url' => 'https://www.wildberries.ru/seller/137386'],
|
||||||
|
['text' => '🛒 Ozon', 'url' => 'https://www.ozon.ru/seller/arming-1086587/products/?miniapp=seller_1086587'],
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'link_preview_options' => [
|
||||||
|
'is_disabled' => true
|
||||||
|
],
|
||||||
|
'disable_notification' => true
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Почта
|
||||||
|
*
|
||||||
|
* @param Context $ctx
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function _mail(Context $ctx): void
|
||||||
|
{
|
||||||
|
// Отправка сообщения
|
||||||
|
$ctx->sendMessage(static::unmarkdown(<<<TXT
|
||||||
|
[info@arming.ru](mailto::info@arming.ru)
|
||||||
|
TXT, ['[', ']', '(', ')']), [
|
||||||
|
'link_preview_options' => [
|
||||||
|
'is_disabled' => true
|
||||||
|
],
|
||||||
|
'disable_notification' => true
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Компания
|
||||||
|
*
|
||||||
|
* Команда: /company
|
||||||
|
*
|
||||||
|
* @param Context $ctx
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function company(Context $ctx): void
|
||||||
|
{
|
||||||
|
// Отправка сообщения
|
||||||
|
$ctx->sendMessage(
|
||||||
|
static::unmarkdown(<<<TXT
|
||||||
|
Здесь придумать текст для раздела "Компания"
|
||||||
|
TXT),
|
||||||
|
/* [
|
||||||
|
'reply_markup' => [
|
||||||
|
'inline_keyboard' => [
|
||||||
|
[
|
||||||
|
['text' => '⚡ Связь с менеджером', 'url' => 'https://git.mirzaev.sexy/mirzaev/mashtrash'],
|
||||||
|
['text' => '📨 Почта', 'text' => ''],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
['text' => '🪖 Сайт', 'url' => '']
|
||||||
|
['text' => '🛒 Wildberries', 'url' => '']
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'link_preview_options' => [
|
||||||
|
'is_disabled' => true
|
||||||
|
]
|
||||||
|
] */
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Сообщество
|
||||||
|
*
|
||||||
|
* Команда: /community
|
||||||
|
*
|
||||||
|
* @param Context $ctx
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function community(Context $ctx): void
|
||||||
|
{
|
||||||
|
// Отправка сообщения
|
||||||
|
$ctx->sendMessage(static::unmarkdown(<<<TXT
|
||||||
|
Здесь придумать текст для раздела "Сообщество"
|
||||||
|
TXT), [
|
||||||
|
'reply_markup' => [
|
||||||
|
'inline_keyboard' => [
|
||||||
|
[
|
||||||
|
['text' => '💬 Основной чат', 'url' => 'https://t.me/arming_zone'],
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'link_preview_options' => [
|
||||||
|
'is_disabled' => true
|
||||||
|
],
|
||||||
|
'disable_notification' => true
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Настройки (доступ только авторизованным)
|
||||||
|
*
|
||||||
|
* Команда: /settings
|
||||||
|
*
|
||||||
|
* @param Context $ctx
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function settings(Context $ctx): void
|
||||||
|
{
|
||||||
|
if ($ctx->get('account')?->access['settings']) {
|
||||||
|
// Авторизован доступ к настройкам
|
||||||
|
|
||||||
|
// Отправка сообщения
|
||||||
|
$ctx->sendMessage(
|
||||||
|
static::unmarkdown(<<<TXT
|
||||||
|
Панель управления чат-роботом ARMING
|
||||||
|
TXT),
|
||||||
|
[
|
||||||
|
'reply_markup' => [
|
||||||
|
'inline_keyboard' => [
|
||||||
|
[
|
||||||
|
['text' => '📦 Импорт товаров', 'callback_data' => 'import_request'],
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'link_preview_options' => [
|
||||||
|
'is_disabled' => true
|
||||||
|
],
|
||||||
|
'disable_notification' => true
|
||||||
|
]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Не авторизован доступ к настройкам
|
||||||
|
|
||||||
|
// Отправка сообщения
|
||||||
|
$ctx->sendMessage('⛔ *Нет доступа*');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Запросить файл для импорта товаров (доступ только авторизованным)
|
||||||
|
*
|
||||||
|
* @param Context $ctx
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function import_request(Context $ctx): void
|
||||||
|
{
|
||||||
|
if ($ctx->get('account')?->access['settings']) {
|
||||||
|
// Авторизован доступ к настройкам
|
||||||
|
|
||||||
|
// Отправка сообщения
|
||||||
|
$ctx->sendMessage(static::unmarkdown('Отправьте документ в формате xlsx со списком товаров'))
|
||||||
|
->then(function ($message) use ($ctx) {
|
||||||
|
// Отправка файла
|
||||||
|
$ctx->sendDocument(new InputFile(CATALOG_EXAMPLE), ['disable_notification' => true]);
|
||||||
|
|
||||||
|
// Импорт файла
|
||||||
|
$ctx->nextStep([static::class, 'import'], true);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Не авторизован доступ к настройкам
|
||||||
|
|
||||||
|
// Отправка сообщения
|
||||||
|
$ctx->sendMessage('⛔ *Нет доступа*');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Импорт товаров (доступ только авторизованным)
|
||||||
|
*
|
||||||
|
* @param Context $ctx
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function import(Context $ctx): void
|
||||||
|
{
|
||||||
|
if (($account = $ctx->get('account'))?->access['settings']) {
|
||||||
|
// Авторизован доступ к настройкам
|
||||||
|
|
||||||
|
// Инициализация документа
|
||||||
|
$document = $ctx->getMessage()?->getDocument();
|
||||||
|
|
||||||
|
if ($document instanceof telegram_document) {
|
||||||
|
// Инициализирован документ
|
||||||
|
|
||||||
|
// Инициализация файла
|
||||||
|
$ctx->getFile($document->getFileId())->then(function ($file) use ($ctx, $document, $account) {
|
||||||
|
|
||||||
|
if ($file->getFileSize() <= 50000000) {
|
||||||
|
// Не превышает 50 мегабайт (50 000 000 байт) размер файла
|
||||||
|
|
||||||
|
if (pathinfo(parse_url($file->getFilePath())['path'], PATHINFO_EXTENSION) === 'xlsx') {
|
||||||
|
// Имеет расширение xlsx файл
|
||||||
|
|
||||||
|
// Initializing the directory in the storage
|
||||||
|
if (!file_exists($storage = STORAGE . DIRECTORY_SEPARATOR . 'import' . DIRECTORY_SEPARATOR . $account->getKey() . DIRECTORY_SEPARATOR . time()))
|
||||||
|
mkdir($storage, 0775, true);
|
||||||
|
|
||||||
|
// Сохранение файла
|
||||||
|
file_put_contents(
|
||||||
|
$import = $storage . DIRECTORY_SEPARATOR . 'import.xlsx',
|
||||||
|
file_get_contents('https://api.telegram.org/file/bot' . KEY . '/' . parse_url($file->getFilePath())['path'])
|
||||||
|
);
|
||||||
|
|
||||||
|
// Отправка сообщения
|
||||||
|
$ctx->sendMessage(sprintf(
|
||||||
|
<<<'TXT'
|
||||||
|
🔬 *Выполняется анализ:* %s \(%s байт\)
|
||||||
|
TXT,
|
||||||
|
static::unmarkdown($document->getFileName()),
|
||||||
|
static::unmarkdown((string) $file->getFileSize())
|
||||||
|
))
|
||||||
|
->then(function ($message) use ($ctx, $import) {
|
||||||
|
// Инициализация счётчика загруженных товаров
|
||||||
|
$categories_loaded
|
||||||
|
= $products_loaded
|
||||||
|
= $categories_created
|
||||||
|
= $products_created
|
||||||
|
= $categories_updated
|
||||||
|
= $products_updated
|
||||||
|
= $categories_deleted
|
||||||
|
= $products_deleted
|
||||||
|
= $categories_old
|
||||||
|
= $products_old
|
||||||
|
= $categories_new
|
||||||
|
= $products_new
|
||||||
|
= 0;
|
||||||
|
|
||||||
|
// Import
|
||||||
|
catalog::import(
|
||||||
|
$import,
|
||||||
|
$categories_loaded,
|
||||||
|
$categories_created,
|
||||||
|
$categories_updated,
|
||||||
|
$categories_deleted,
|
||||||
|
$categories_old,
|
||||||
|
$categories_new,
|
||||||
|
$products_loaded,
|
||||||
|
$products_created,
|
||||||
|
$products_updated,
|
||||||
|
$products_deleted,
|
||||||
|
$products_old,
|
||||||
|
$products_new,
|
||||||
|
language: 'ru'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Отправка сообщения
|
||||||
|
$ctx->sendMessage(<<<TXT
|
||||||
|
🏷 *Категории*
|
||||||
|
|
||||||
|
*Загружено:* $categories_loaded
|
||||||
|
|
||||||
|
*Добавлено:* $categories_created
|
||||||
|
*Обновлено:* $categories_updated
|
||||||
|
*Удалено:* $categories_deleted
|
||||||
|
|
||||||
|
*Было:* $categories_old
|
||||||
|
*Стало:* $categories_new
|
||||||
|
TXT)
|
||||||
|
->then(function ($message) use ($ctx, $products_loaded, $products_created, $products_updated, $products_deleted, $products_old, $products_new) {
|
||||||
|
$ctx->sendMessage(<<<TXT
|
||||||
|
📦 *Товары*
|
||||||
|
|
||||||
|
*Загружено:* $products_loaded
|
||||||
|
|
||||||
|
*Добавлено:* $products_created
|
||||||
|
*Обновлено:* $products_updated
|
||||||
|
*Удалено:* $products_deleted
|
||||||
|
|
||||||
|
*Было:* $products_old
|
||||||
|
*Стало:* $products_new
|
||||||
|
TXT)
|
||||||
|
->then(function ($message) use ($ctx) {
|
||||||
|
// Завершение диалога
|
||||||
|
$ctx->endConversation();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Не имеет расширение xlsx файл
|
||||||
|
|
||||||
|
// Отправка сообщения
|
||||||
|
$ctx->sendMessage(static::unmarkdown('Файл должен иметь расширение xlsx'));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Превышает 50 мегабайт (50000000 байт) размер файла
|
||||||
|
|
||||||
|
// Отправка сообщения
|
||||||
|
$ctx->sendMessage(static::unmarkdown('Размер файла не должен превышать 50 мегабайт'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Не инициализирован документ
|
||||||
|
|
||||||
|
// Отправка сообщения
|
||||||
|
$ctx->sendMessage(static::unmarkdown('Отправьте документ в формате xlsx со списком товаров'));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Не авторизован доступ к настройкам
|
||||||
|
|
||||||
|
// Отправка сообщения
|
||||||
|
$ctx->sendMessage('⛔ *Нет доступа*');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Инициализация аккаунта (middleware)
|
||||||
|
*
|
||||||
|
* @param Context $ctx
|
||||||
|
* @param Node $next
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function account(Context $ctx, Node $next): void
|
||||||
|
{
|
||||||
|
// Выполнение заблокировано?
|
||||||
|
if ($ctx->get('stop')) return;
|
||||||
|
|
||||||
|
// Инициализация аккаунта Telegram
|
||||||
|
$telegram = $ctx->getEffectiveUser();
|
||||||
|
|
||||||
|
// Инициализация аккаунта
|
||||||
|
$account = account::initialization($telegram->getId(), $telegram);
|
||||||
|
|
||||||
|
if ($account) {
|
||||||
|
// Инициализирован аккаунт
|
||||||
|
|
||||||
|
if ($account->banned) {
|
||||||
|
// Заблокирован аккаунт
|
||||||
|
|
||||||
|
// Отправка сообщения
|
||||||
|
$ctx->sendMessage('⛔ *Ты заблокирован*')
|
||||||
|
->then(function ($message) use ($ctx) {
|
||||||
|
// Завершение диалога
|
||||||
|
$ctx->endConversation();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Блокировка дальнейшего выполнения
|
||||||
|
$ctx->set('stop', true);
|
||||||
|
} else {
|
||||||
|
// Не заблокирован аккаунт
|
||||||
|
|
||||||
|
// Запись в буфер
|
||||||
|
$ctx->set('account', $account);
|
||||||
|
|
||||||
|
// Продолжение выполнения
|
||||||
|
$next($ctx);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Не инициализирован аккаунт
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Инициализация статуса технических работ (middleware)
|
||||||
|
*
|
||||||
|
* @param Context $ctx
|
||||||
|
* @param Node $next
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function suspension(Context $ctx, Node $next): void
|
||||||
|
{
|
||||||
|
// Выполнение заблокировано?
|
||||||
|
if ($ctx->get('stop')) return;
|
||||||
|
|
||||||
|
// Поиск технических работ
|
||||||
|
$suspension = suspension::search();
|
||||||
|
|
||||||
|
if ($suspension && $suspension->targets['telegram-robot']) {
|
||||||
|
// Найдена активная приостановка
|
||||||
|
|
||||||
|
// Инициализация аккаунта
|
||||||
|
$account = $ctx->get('account');
|
||||||
|
|
||||||
|
if ($account) {
|
||||||
|
// Инициализирован аккаунт
|
||||||
|
|
||||||
|
foreach ($suspension->access as $type => $status) {
|
||||||
|
// Перебор статусов доступа
|
||||||
|
|
||||||
|
if ($status && $account->{$type}) {
|
||||||
|
// Авторизован аккаунт
|
||||||
|
|
||||||
|
// Продолжение выполнения
|
||||||
|
$next($ctx);
|
||||||
|
|
||||||
|
// Выход (успех)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Инициализация сообщения
|
||||||
|
$message = "⚠️ *Работа приостановлена*\n*Оставшееся время\:* " . $suspension->message($account->language ?? controller::$settings?->language);
|
||||||
|
|
||||||
|
// Добавление описания причины приостановки, если найдена
|
||||||
|
if (!empty($suspension->description))
|
||||||
|
$message .= "\n\n" . $suspension->description[$account->language ?? controller::$settings?->language] ?? array_values($suspension->description)[0];
|
||||||
|
|
||||||
|
// Отправка сообщения
|
||||||
|
$ctx->sendMessage($message)
|
||||||
|
->then(function ($message) use ($ctx) {
|
||||||
|
// Завершение диалога
|
||||||
|
$ctx->endConversation();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Блокировка дальнейшего выполнения
|
||||||
|
$ctx->set('stop', true);
|
||||||
|
} else {
|
||||||
|
// Не найдена активная приостановка
|
||||||
|
|
||||||
|
// Продолжение выполнения
|
||||||
|
$next($ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,20 +2,16 @@
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace mirzaev\huesos\models\traits;
|
namespace mirzaev\arming_bot\models\traits;
|
||||||
|
|
||||||
// Files of the project
|
// Files of the project
|
||||||
use mirzaev\huesos\models\interfaces\document as document_interface,
|
use mirzaev\arming_bot\models\core;
|
||||||
mirzaev\huesos\models\interfaces\collection as collection_interface,
|
|
||||||
mirzaev\huesos\models\connect;
|
|
||||||
|
|
||||||
// Library для ArangoDB
|
// Library для ArangoDB
|
||||||
use ArangoDBClient\Document as _document;
|
use ArangoDBClient\Document as _document;
|
||||||
|
|
||||||
// Framework for ArangoDB
|
// 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
|
// Built-in libraries
|
||||||
use exception;
|
use exception;
|
||||||
|
@ -25,10 +21,7 @@ use exception;
|
||||||
*
|
*
|
||||||
* @var protected readonly _document|null $document An instance of the ArangoDB document
|
* @var protected readonly _document|null $document An instance of the ArangoDB document
|
||||||
*
|
*
|
||||||
* @uses document_interface
|
* @package mirzaev\arming_bot\models\traits
|
||||||
* @package mirzaev\huesos\models\traits
|
|
||||||
*
|
|
||||||
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
|
||||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||||
*/
|
*/
|
||||||
trait document
|
trait document
|
||||||
|
@ -56,7 +49,7 @@ trait document
|
||||||
parent::__construct($initialize, $arangodb);
|
parent::__construct($initialize, $arangodb);
|
||||||
|
|
||||||
// Writing to the property
|
// Writing to the property
|
||||||
if ($document instanceof _document) $this->__document($document);
|
if ($document instanceof _document) $this->document = $document;
|
||||||
else if ($document === null) throw new exception('Failed to initialize an instance of the document from ArangoDB');
|
else if ($document === null) throw new exception('Failed to initialize an instance of the document from ArangoDB');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,85 +69,6 @@ trait document
|
||||||
return $this->document ?? null;
|
return $this->document ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Connect
|
|
||||||
*
|
|
||||||
* Searches for a connection document, otherwise creates one
|
|
||||||
*
|
|
||||||
* @param collecton_interface $document Document
|
|
||||||
* @param array &$errors Registry of errors
|
|
||||||
*
|
|
||||||
* @return string|null The identifier of the "connect" edge collection, if created or found
|
|
||||||
*/
|
|
||||||
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
|
|
||||||
|
|
||||||
// Searching for a connection
|
|
||||||
$found = collection::execute(
|
|
||||||
<<<'AQL'
|
|
||||||
FOR d IN @@collection
|
|
||||||
FILTER d._from == @_from && d._to == @_to && d.active == true
|
|
||||||
RETURN d
|
|
||||||
AQL,
|
|
||||||
[
|
|
||||||
'@collection' => connect::COLLECTION,
|
|
||||||
'_from' => $document->getId(),
|
|
||||||
'_to' => $this->document->getId()
|
|
||||||
],
|
|
||||||
errors: $errors
|
|
||||||
);
|
|
||||||
|
|
||||||
if ($found) {
|
|
||||||
// Found the connection document
|
|
||||||
|
|
||||||
// Exit (success)
|
|
||||||
return $found->getId();
|
|
||||||
} else {
|
|
||||||
// Not found the connection document
|
|
||||||
|
|
||||||
// Creting the connection document
|
|
||||||
$created = framework_document::write(
|
|
||||||
connect::COLLECTION,
|
|
||||||
[
|
|
||||||
'_from' => $document->getId(),
|
|
||||||
'_to' => $this->document->getId()
|
|
||||||
],
|
|
||||||
errors: $errors
|
|
||||||
);
|
|
||||||
|
|
||||||
if ($created) {
|
|
||||||
// Created the connection document
|
|
||||||
|
|
||||||
// Exit (success)
|
|
||||||
return $created;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else throw new exception('The instance of the document from ArangoDB is not initialized');
|
|
||||||
} else throw new exception('Failed to initialize ' . $document::TYPE->name . ' collection: ' . $document::COLLECTION);
|
|
||||||
} else throw new exception('Failed to initialize ' . connect::TYPE->name . ' collection: ' . connect::COLLECTION);
|
|
||||||
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::COLLECTION);
|
|
||||||
} catch (exception $e) {
|
|
||||||
// Writing to the registry of errors
|
|
||||||
$errors[] = [
|
|
||||||
'text' => $e->getMessage(),
|
|
||||||
'file' => $e->getFile(),
|
|
||||||
'line' => $e->getLine(),
|
|
||||||
'stack' => $e->getTrace()
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exit (fail)
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Write
|
* Write
|
||||||
*
|
*
|
||||||
|
@ -184,6 +98,7 @@ trait document
|
||||||
{
|
{
|
||||||
// Read a property from an instance of the ArangoDB document and exit (success)
|
// Read a property from an instance of the ArangoDB document and exit (success)
|
||||||
return match ($name) {
|
return match ($name) {
|
||||||
|
'arangodb' => core::$arangodb,
|
||||||
default => $this->document->{$name}
|
default => $this->document->{$name}
|
||||||
};
|
};
|
||||||
}
|
}
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace mirzaev\huesos\models\traits;
|
namespace mirzaev\arming_bot\models\traits;
|
||||||
|
|
||||||
// Built-in libraries
|
// Built-in libraries
|
||||||
use exception;
|
use exception;
|
||||||
|
@ -10,9 +10,7 @@ use exception;
|
||||||
/**
|
/**
|
||||||
* Trait for initialization of files handlers
|
* Trait for initialization of files handlers
|
||||||
*
|
*
|
||||||
* @package mirzaev\huesos\models\traits
|
* @package mirzaev\arming_bot\models\traits
|
||||||
*
|
|
||||||
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
|
||||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||||
*/
|
*/
|
||||||
trait files
|
trait files
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace mirzaev\huesos\models\traits;
|
namespace mirzaev\arming_bot\models\traits;
|
||||||
|
|
||||||
// Built-in libraries
|
// Built-in libraries
|
||||||
use exception;
|
use exception;
|
||||||
|
@ -10,9 +10,7 @@ use exception;
|
||||||
/**
|
/**
|
||||||
* Trait for initialization of a status
|
* Trait for initialization of a status
|
||||||
*
|
*
|
||||||
* @package mirzaev\huesos\models\traits
|
* @package mirzaev\arming_bot\models\traits
|
||||||
*
|
|
||||||
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
|
||||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||||
*/
|
*/
|
||||||
trait status
|
trait status
|
|
@ -0,0 +1,70 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace mirzaev\arming_bot\models\traits\yandex;
|
||||||
|
|
||||||
|
// Built-in libraries
|
||||||
|
use exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trait for "Yandex Disk"
|
||||||
|
*
|
||||||
|
* @package mirzaev\arming_bot\models\traits\yandex
|
||||||
|
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||||
|
*/
|
||||||
|
trait disk
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Download file from "Yandex Disk"
|
||||||
|
*
|
||||||
|
* @param string $uri URI of the file from "Yandex Disk"
|
||||||
|
* @param string $destination Destination to write the file
|
||||||
|
* @param array &$errors Registry of errors
|
||||||
|
*
|
||||||
|
* @return bool The file is downloaded?
|
||||||
|
*/
|
||||||
|
private static function download(
|
||||||
|
string $uri,
|
||||||
|
string $destination,
|
||||||
|
array &$errors = []
|
||||||
|
): bool {
|
||||||
|
try {
|
||||||
|
if (!empty($uri)) {
|
||||||
|
// Not empty URI
|
||||||
|
|
||||||
|
if (!empty($destination)) {
|
||||||
|
// Not empty destination
|
||||||
|
|
||||||
|
// Initializing URL of the file
|
||||||
|
$url = "https://cloud-api.yandex.net/v1/disk/public/resources/download?public_key=$uri";
|
||||||
|
|
||||||
|
// Checking if the file is available for download
|
||||||
|
$ch = curl_init($url);
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_exec($ch);
|
||||||
|
$code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($code === 200) {
|
||||||
|
// The file is available for download
|
||||||
|
|
||||||
|
// Downloading the file and exit (success)
|
||||||
|
return file_put_contents($destination, file_get_contents(json_decode(file_get_contents($url))?->href)) > 0;
|
||||||
|
} else throw new exception("File not available for download: $uri");
|
||||||
|
} else throw new exception("Empty destination");
|
||||||
|
} else throw new exception("Empty URI");
|
||||||
|
} catch (exception $e) {
|
||||||
|
// Writing to the registry of errors
|
||||||
|
$errors[] = [
|
||||||
|
'text' => $e->getMessage(),
|
||||||
|
'file' => $e->getFile(),
|
||||||
|
'line' => $e->getLine(),
|
||||||
|
'stack' => $e->getTrace()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit (fail)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace mirzaev\arming_bot;
|
||||||
|
|
||||||
|
// Files of the project
|
||||||
|
use mirzaev\arming_bot\controllers\core as controller,
|
||||||
|
mirzaev\arming_bot\models\core as model;
|
||||||
|
|
||||||
|
// Framework for PHP
|
||||||
|
use mirzaev\minimal\core,
|
||||||
|
mirzaev\minimal\router;
|
||||||
|
|
||||||
|
ini_set('error_reporting', E_ALL);
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
ini_set('display_startup_errors', 1);
|
||||||
|
|
||||||
|
// Версия робота
|
||||||
|
define('ROBOT_VERSION', '1.0.0');
|
||||||
|
define('VIEWS', realpath('..' . DIRECTORY_SEPARATOR . 'views'));
|
||||||
|
define('STORAGE', realpath('..' . DIRECTORY_SEPARATOR . 'storage'));
|
||||||
|
define('SETTINGS', realpath('..' . DIRECTORY_SEPARATOR . 'settings'));
|
||||||
|
define('INDEX', __DIR__);
|
||||||
|
define('THEME', 'default');
|
||||||
|
|
||||||
|
// Инициализация библиотек
|
||||||
|
require __DIR__ . DIRECTORY_SEPARATOR
|
||||||
|
. '..' . DIRECTORY_SEPARATOR
|
||||||
|
. '..' . DIRECTORY_SEPARATOR
|
||||||
|
. '..' . DIRECTORY_SEPARATOR
|
||||||
|
. '..' . DIRECTORY_SEPARATOR
|
||||||
|
. 'vendor' . DIRECTORY_SEPARATOR
|
||||||
|
. 'autoload.php';
|
||||||
|
|
||||||
|
// Инициализация маршрутизатора
|
||||||
|
$router = new router;
|
||||||
|
|
||||||
|
// Initializing of routes
|
||||||
|
$router
|
||||||
|
->write('/', 'catalog', 'index', 'GET')
|
||||||
|
->write('/search', 'catalog', 'search', 'POST')
|
||||||
|
->write('/session/connect/telegram', 'session', 'telegram', 'POST')
|
||||||
|
->write('/product/$id', 'catalog', 'product', 'POST')
|
||||||
|
->write('/$categories...', 'catalog', 'index', 'POST');
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
// Initializing of routes
|
||||||
|
$router
|
||||||
|
->write('/', 'catalog', 'index', 'GET')
|
||||||
|
->write('/$sex', 'catalog', 'search', 'POST')
|
||||||
|
->write('/$search', 'catalog', 'search', 'POST')
|
||||||
|
->write('/search', 'catalog', 'search', 'POST')
|
||||||
|
->write('/search/$asdasdasd', 'catalog', 'search', 'POST')
|
||||||
|
->write('/ebala/$sex/$categories...', 'catalog', 'index', 'POST')
|
||||||
|
->write('/$sex/$categories...', 'catalog', 'index', 'POST')
|
||||||
|
->write('/$categories...', 'catalog', 'index', 'POST')
|
||||||
|
->write('/ebala/$categories...', 'catalog', 'index', 'POST');
|
||||||
|
|
||||||
|
var_dump($router->routes);
|
||||||
|
echo "\n\n\n\n\n\n";
|
||||||
|
$router
|
||||||
|
->sort();
|
||||||
|
var_dump($router->routes); */
|
||||||
|
|
||||||
|
// Инициализация ядра
|
||||||
|
$core = new core(namespace: __NAMESPACE__, router: $router, controller: new controller(false), model: new model(false));
|
||||||
|
|
||||||
|
// Обработка запроса
|
||||||
|
echo $core->start();
|
|
@ -0,0 +1,65 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
// Import dependencies
|
||||||
|
import("/js/core.js").then(() =>
|
||||||
|
import("/js/damper.js").then(() => {
|
||||||
|
import("/js/telegram.js").then(() => {
|
||||||
|
const dependencies = setInterval(() => {
|
||||||
|
if (
|
||||||
|
typeof core === "function" &&
|
||||||
|
typeof core.damper === "function" &&
|
||||||
|
typeof core.telegram === "function"
|
||||||
|
) {
|
||||||
|
clearInterval(dependencies);
|
||||||
|
clearTimeout(timeout);
|
||||||
|
initialization();
|
||||||
|
}
|
||||||
|
}, 10);
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
clearInterval(dependencies);
|
||||||
|
initialization();
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
function initialization() {
|
||||||
|
const timer_for_response = setTimeout(() => {
|
||||||
|
core.loading.setAttribute("disabled", true);
|
||||||
|
|
||||||
|
const p = document.createElement("p");
|
||||||
|
p.innerText = "Not authenticated";
|
||||||
|
|
||||||
|
core.footer.appendChild(p);
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
core.request(
|
||||||
|
"/session/connect/telegram",
|
||||||
|
"authentication=telegram&" + core.telegram.api.initData,
|
||||||
|
)
|
||||||
|
.then((json) => {
|
||||||
|
if (
|
||||||
|
json.errors !== null &&
|
||||||
|
typeof json.errors === "object" &&
|
||||||
|
json.errors.length > 0
|
||||||
|
) {
|
||||||
|
// Errors received
|
||||||
|
} else {
|
||||||
|
// Errors not received
|
||||||
|
|
||||||
|
if (json.connected === true) {
|
||||||
|
core.loading.setAttribute("disabled", true);
|
||||||
|
|
||||||
|
clearTimeout(timer_for_response);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
json.language !== null &&
|
||||||
|
typeof json.language === "string" &&
|
||||||
|
json.langiage.length === 2
|
||||||
|
) {
|
||||||
|
core.language = json.language;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
|
@ -0,0 +1,35 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
// Import dependencies
|
||||||
|
import("/js/core.js").then(() =>
|
||||||
|
import("/js/damper.js").then(() => {
|
||||||
|
const dependencies = setInterval(() => {
|
||||||
|
if (
|
||||||
|
typeof core === "function" &&
|
||||||
|
typeof core.damper === "function"
|
||||||
|
) {
|
||||||
|
clearInterval(dependencies);
|
||||||
|
clearTimeout(timeout);
|
||||||
|
initialization();
|
||||||
|
}
|
||||||
|
}, 10);
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
clearInterval(dependencies);
|
||||||
|
initialization();
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
function initialization() {
|
||||||
|
if (typeof core.cart === "undefined") {
|
||||||
|
// Not initialized
|
||||||
|
|
||||||
|
// Write to the core
|
||||||
|
core.cart = class cart {
|
||||||
|
/**
|
||||||
|
* Products in cart ["product/148181", "product/148181", "product/148181"...]
|
||||||
|
*/
|
||||||
|
static cart = [];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
|
@ -0,0 +1,643 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
// Import dependencies
|
||||||
|
import("/js/core.js").then(() =>
|
||||||
|
import("/js/damper.js").then(() => {
|
||||||
|
import("/js/telegram.js").then(() => {
|
||||||
|
import("/js/hotline.js").then(() => {
|
||||||
|
const dependencies = setInterval(() => {
|
||||||
|
console.log(
|
||||||
|
typeof core,
|
||||||
|
typeof core.damper,
|
||||||
|
typeof core.telegram,
|
||||||
|
typeof core.hotline,
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
typeof core === "function" &&
|
||||||
|
typeof core.damper === "function" &&
|
||||||
|
typeof core.telegram === "function" &&
|
||||||
|
typeof core.hotline === "function"
|
||||||
|
) {
|
||||||
|
clearInterval(dependencies);
|
||||||
|
clearTimeout(timeout);
|
||||||
|
initialization();
|
||||||
|
}
|
||||||
|
}, 10);
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
clearInterval(dependencies);
|
||||||
|
initialization();
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
function initialization() {
|
||||||
|
if (typeof core.catalog === "undefined") {
|
||||||
|
// Not initialized
|
||||||
|
|
||||||
|
// Write to the core
|
||||||
|
core.catalog = class catalog {
|
||||||
|
/**
|
||||||
|
* Current position in hierarchy of the categories
|
||||||
|
*/
|
||||||
|
static categories = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registry of filters (instead of cookies)
|
||||||
|
*/
|
||||||
|
static filters = new Map([
|
||||||
|
['brand', null]
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select a category (interface)
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} button Button of category <a>
|
||||||
|
* @param {bool} clean Clear search bar?
|
||||||
|
* @param {bool} force Ignore the damper?
|
||||||
|
*
|
||||||
|
* @return {void}
|
||||||
|
*/
|
||||||
|
static category(button, clean = true, force = false) {
|
||||||
|
// Initialize of the new category name
|
||||||
|
const category = button.getAttribute("data-category-name");
|
||||||
|
|
||||||
|
this._category(category, clean, force);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select a category (damper)
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} button Button of category <a>
|
||||||
|
* @param {bool} clean Clear search bar?
|
||||||
|
* @param {bool} force Ignore the damper?
|
||||||
|
*
|
||||||
|
* @return {void}
|
||||||
|
*/
|
||||||
|
static _category = core.damper(
|
||||||
|
(...variables) => this.__category(...variables),
|
||||||
|
400,
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select a category (system)
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} button Button of category <a>
|
||||||
|
* @param {bool} clean Clear search bar?
|
||||||
|
*
|
||||||
|
* @return {Promise} Request to the server
|
||||||
|
*/
|
||||||
|
static __category(category = "", clean = true) {
|
||||||
|
if (typeof category === "string") {
|
||||||
|
//
|
||||||
|
|
||||||
|
let urn;
|
||||||
|
if (category === "/" || category === "") urn = "/";
|
||||||
|
else {urn = this.categories.length > 0
|
||||||
|
? `/${this.categories.join("/")}/${category}`
|
||||||
|
: `/${category}`;}
|
||||||
|
|
||||||
|
return core.request(urn)
|
||||||
|
.then((json) => {
|
||||||
|
if (
|
||||||
|
json.errors !== null &&
|
||||||
|
typeof json.errors === "object" &&
|
||||||
|
json.errors.length > 0
|
||||||
|
) {
|
||||||
|
// Errors received
|
||||||
|
} else {
|
||||||
|
// Errors not received
|
||||||
|
|
||||||
|
if (clean) {
|
||||||
|
// Clearing the search bar
|
||||||
|
const search = core.main.querySelector(
|
||||||
|
'search[data-section="search"]>input',
|
||||||
|
);
|
||||||
|
if (search instanceof HTMLElement) search.value = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the category to position in the categories hierarchy
|
||||||
|
if (category !== "/" && category !== "") {
|
||||||
|
this.categories.push(category);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof json.html.categories === "string" &&
|
||||||
|
json.html.categories.length > 0
|
||||||
|
) {
|
||||||
|
// Received categories (reinitialization of the categories)
|
||||||
|
|
||||||
|
const categories = core.main.querySelector(
|
||||||
|
'section[data-catalog-type="categories"]',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (categories instanceof HTMLElement) {
|
||||||
|
// Found list of categories
|
||||||
|
|
||||||
|
categories.outerHTML = json.html.categories;
|
||||||
|
} else {
|
||||||
|
// Not found list of categories
|
||||||
|
|
||||||
|
const element = document.createElement("section");
|
||||||
|
|
||||||
|
const search = core.main.querySelector(
|
||||||
|
'search[data-section="search"]',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (search instanceof HTMLElement) {
|
||||||
|
core.main.insertBefore(
|
||||||
|
element,
|
||||||
|
search.nextSibling,
|
||||||
|
);
|
||||||
|
|
||||||
|
element.outerHTML = json.html.categories;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Not received categories (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
|
||||||
|
) {
|
||||||
|
// 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");
|
||||||
|
|
||||||
|
const categories = core.main.querySelector(
|
||||||
|
'section[data-catalog-type="categories"]',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (categories instanceof HTMLElement) {
|
||||||
|
core.main.insertBefore(
|
||||||
|
element,
|
||||||
|
categories.nextSibling,
|
||||||
|
);
|
||||||
|
|
||||||
|
element.outerHTML = json.html.products;
|
||||||
|
} else {
|
||||||
|
const search = core.main.querySelector(
|
||||||
|
'search[data-section="search"]',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (search instanceof HTMLElement) {
|
||||||
|
core.main.insertBefore(
|
||||||
|
element,
|
||||||
|
search.nextSibling,
|
||||||
|
);
|
||||||
|
|
||||||
|
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.innerText.length === 1) {
|
||||||
|
return;
|
||||||
|
} else if (event.keyCode === 13) {
|
||||||
|
// Button: "enter"
|
||||||
|
|
||||||
|
element.setAttribute("disabled", true);
|
||||||
|
|
||||||
|
this.__search(element);
|
||||||
|
} 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) {
|
||||||
|
// Deinitialization of position in the categories hierarchy
|
||||||
|
this.categories = [];
|
||||||
|
|
||||||
|
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
|
||||||
|
) {
|
||||||
|
// 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");
|
||||||
|
|
||||||
|
const categories = core.main.querySelector(
|
||||||
|
'section[data-catalog-type="categories"',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (categories instanceof HTMLElement) {
|
||||||
|
core.main.insertBefore(
|
||||||
|
element,
|
||||||
|
categories.nextSibling,
|
||||||
|
);
|
||||||
|
|
||||||
|
element.outerHTML = json.html.products;
|
||||||
|
} else {
|
||||||
|
const search = core.main.querySelector(
|
||||||
|
'search[data-section="search"]',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (search instanceof HTMLElement) {
|
||||||
|
core.main.insertBefore(
|
||||||
|
element,
|
||||||
|
search.nextSibling,
|
||||||
|
);
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open product card (interface)
|
||||||
|
*
|
||||||
|
* @param {string} id Identifier of a product
|
||||||
|
* @param {bool} force Ignore the damper?
|
||||||
|
*
|
||||||
|
* @return {void}
|
||||||
|
*/
|
||||||
|
static product(id, force = false) {
|
||||||
|
this._product(id, force);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open product card (damper)
|
||||||
|
*
|
||||||
|
* @param {string} id Identifier of a product
|
||||||
|
* @param {bool} force Ignore the damper?
|
||||||
|
*
|
||||||
|
* @return {void}
|
||||||
|
*/
|
||||||
|
static _product = core.damper(
|
||||||
|
(...variables) => this.__product(...variables),
|
||||||
|
400,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open product card (system)
|
||||||
|
*
|
||||||
|
* @param {string} id Identifier of a product
|
||||||
|
*
|
||||||
|
* @return {Promise} Request to the server
|
||||||
|
*/
|
||||||
|
static __product(id) {
|
||||||
|
if (typeof id === "number") {
|
||||||
|
//
|
||||||
|
|
||||||
|
return core.request(`/product/${id}`)
|
||||||
|
.then((json) => {
|
||||||
|
if (
|
||||||
|
json.errors !== null &&
|
||||||
|
typeof json.errors === "object" &&
|
||||||
|
json.errors.length > 0
|
||||||
|
) {
|
||||||
|
// Errors received
|
||||||
|
} else {
|
||||||
|
// Errors not received
|
||||||
|
|
||||||
|
if (
|
||||||
|
json.product !== null &&
|
||||||
|
typeof json.product === "object"
|
||||||
|
) {
|
||||||
|
// Received data of the product
|
||||||
|
|
||||||
|
// Deinitializing of the old winow
|
||||||
|
const old = document.getElementById("window");
|
||||||
|
if (old instanceof HTMLElement) old.remove();
|
||||||
|
|
||||||
|
const wrap = document.createElement("section");
|
||||||
|
wrap.setAttribute("id", "window");
|
||||||
|
|
||||||
|
const card = document.createElement("div");
|
||||||
|
// card.classList.add("product", "card");
|
||||||
|
card.classList.add("card", "unselectable");
|
||||||
|
|
||||||
|
const h3 = document.createElement("h3");
|
||||||
|
h3.setAttribute("title", json.product.id);
|
||||||
|
|
||||||
|
const title = document.createElement("span");
|
||||||
|
title.classList.add("title");
|
||||||
|
title.innerText = json.product.title;
|
||||||
|
|
||||||
|
const brand = document.createElement("small");
|
||||||
|
brand.classList.add("brand");
|
||||||
|
brand.innerText = json.product.brand;
|
||||||
|
|
||||||
|
const images = document.createElement("div");
|
||||||
|
images.classList.add("images", "unselectable");
|
||||||
|
|
||||||
|
const button = core.telegram.api.isVisible;
|
||||||
|
|
||||||
|
const _open = (event) => {
|
||||||
|
if (event.target === from) {
|
||||||
|
if (typeof images.hotline === "object") {
|
||||||
|
if (images.hotline.moving) return;
|
||||||
|
images.hotline.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
images.classList.add("extend");
|
||||||
|
|
||||||
|
if (button) core.telegram.api.MainButton.hide();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
images.addEventListener("click", _close);
|
||||||
|
images.addEventListener("touch", _close);
|
||||||
|
}, 300);
|
||||||
|
images.removeEventListener("mouseup", _open);
|
||||||
|
images.removeEventListener("touchend", _open);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const _close = () => {
|
||||||
|
if (typeof images.hotline === "object") {
|
||||||
|
images.hotline.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
images.classList.remove("extend");
|
||||||
|
|
||||||
|
if (button) core.telegram.api.MainButton.show();
|
||||||
|
|
||||||
|
images.removeEventListener("click", _close);
|
||||||
|
images.removeEventListener("touch", _close);
|
||||||
|
images.addEventListener("mousedown", _start);
|
||||||
|
images.addEventListener("touchstart", _start);
|
||||||
|
};
|
||||||
|
|
||||||
|
const _start = (event) => {
|
||||||
|
if (
|
||||||
|
event.type === "touchstart" ||
|
||||||
|
event.button === 0
|
||||||
|
) {
|
||||||
|
images.removeEventListener("mousedown", _start);
|
||||||
|
images.removeEventListener("touchstart", _start);
|
||||||
|
images.addEventListener("mouseup", _open);
|
||||||
|
images.addEventListener("touchend", _open);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
images.addEventListener("mousedown", _start);
|
||||||
|
images.addEventListener("touchstart", _start);
|
||||||
|
|
||||||
|
for (const uri of json.product.images) {
|
||||||
|
const image = document.createElement("img");
|
||||||
|
image.setAttribute("src", uri);
|
||||||
|
image.setAttribute("ondragstart", "return false;");
|
||||||
|
|
||||||
|
images.append(image);
|
||||||
|
}
|
||||||
|
|
||||||
|
const description = document.createElement("p");
|
||||||
|
description.classList.add("description");
|
||||||
|
description.innerText = json.product.description;
|
||||||
|
|
||||||
|
const compatibility = document.createElement("p");
|
||||||
|
compatibility.classList.add("compatibility");
|
||||||
|
compatibility.innerText = json.product.compatibility;
|
||||||
|
|
||||||
|
const footer = document.createElement("div");
|
||||||
|
footer.classList.add("footer");
|
||||||
|
footer.classList.add("footer");
|
||||||
|
|
||||||
|
const dimensions = document.createElement("small");
|
||||||
|
dimensions.classList.add("dimensions");
|
||||||
|
dimensions.innerText = json.product.dimensions.x +
|
||||||
|
"x" +
|
||||||
|
json.product.dimensions.y + "x" +
|
||||||
|
json.product.dimensions.z;
|
||||||
|
|
||||||
|
const weight = document.createElement("small");
|
||||||
|
weight.classList.add("weight");
|
||||||
|
weight.innerText = json.product.weight + "г";
|
||||||
|
|
||||||
|
const cost = document.createElement("p");
|
||||||
|
cost.classList.add("cost");
|
||||||
|
cost.innerText = json.product.cost + "р";
|
||||||
|
|
||||||
|
h3.append(title);
|
||||||
|
h3.append(brand);
|
||||||
|
card.append(h3);
|
||||||
|
card.append(images);
|
||||||
|
card.append(description);
|
||||||
|
card.append(compatibility);
|
||||||
|
footer.append(dimensions);
|
||||||
|
footer.append(weight);
|
||||||
|
footer.append(cost);
|
||||||
|
card.append(footer);
|
||||||
|
wrap.append(card);
|
||||||
|
core.main.append(wrap);
|
||||||
|
|
||||||
|
let width = 0;
|
||||||
|
let buffer;
|
||||||
|
[...images.children].forEach((child) =>
|
||||||
|
width += child.offsetWidth + (isNaN(
|
||||||
|
buffer = parseFloat(
|
||||||
|
getComputedStyle(child).marginRight,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
? 0
|
||||||
|
: buffer)
|
||||||
|
);
|
||||||
|
|
||||||
|
history.pushState(
|
||||||
|
{ product_card: json.product.id },
|
||||||
|
json.product.title,
|
||||||
|
);
|
||||||
|
|
||||||
|
// блокировка закрытия карточки
|
||||||
|
let from;
|
||||||
|
const _from = (event) => from = event.target;
|
||||||
|
wrap.addEventListener("mousedown", _from);
|
||||||
|
wrap.addEventListener("touchstart", _from);
|
||||||
|
|
||||||
|
const remove = () => {
|
||||||
|
wrap.remove();
|
||||||
|
wrap.removeEventListener("mousedown", _from);
|
||||||
|
wrap.removeEventListener("touchstart", _from);
|
||||||
|
document.removeEventListener("click", close);
|
||||||
|
document.removeEventListener("touch", close);
|
||||||
|
window.removeEventListener("popstate", remove);
|
||||||
|
};
|
||||||
|
|
||||||
|
const close = (event) => {
|
||||||
|
if (
|
||||||
|
from === wrap &&
|
||||||
|
!card.contains(event.target) &&
|
||||||
|
!!card &&
|
||||||
|
!!(card.offsetWidth ||
|
||||||
|
card.offsetHeight ||
|
||||||
|
card.getClientRects().length)
|
||||||
|
) {
|
||||||
|
remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
from = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("click", close);
|
||||||
|
document.addEventListener("touch", close);
|
||||||
|
window.addEventListener("popstate", remove);
|
||||||
|
|
||||||
|
if (width > card.offsetWidth) {
|
||||||
|
images.hotline = new core.hotline(
|
||||||
|
json.product.id,
|
||||||
|
images,
|
||||||
|
);
|
||||||
|
images.hotline.step = -0.3;
|
||||||
|
images.hotline.wheel = true;
|
||||||
|
images.hotline.touch = true;
|
||||||
|
images.hotline.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
|
@ -0,0 +1,52 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
// Initialize of the class in global namespace
|
||||||
|
const core = class core {
|
||||||
|
// Domain
|
||||||
|
static domain = window.location.hostname;
|
||||||
|
|
||||||
|
// Language
|
||||||
|
static language = "ru";
|
||||||
|
|
||||||
|
// Label for the "loding" element
|
||||||
|
static loading = document.getElementById("loading");
|
||||||
|
|
||||||
|
// Label for the <header> element
|
||||||
|
static header = document.body.getElementsByTagName("header")[0];
|
||||||
|
|
||||||
|
// Label for the <aside> element
|
||||||
|
static aside = document.body.getElementsByTagName("aside")[0];
|
||||||
|
|
||||||
|
// Label for the "menu" element
|
||||||
|
static menu = document.body.querySelector("section[data-section='menu']");
|
||||||
|
|
||||||
|
// Label for the <main> element
|
||||||
|
static main = document.body.getElementsByTagName("main")[0];
|
||||||
|
|
||||||
|
// Label for the <footer> element
|
||||||
|
static footer = document.body.getElementsByTagName("footer")[0];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request
|
||||||
|
*
|
||||||
|
* @param {string} address
|
||||||
|
* @param {string} body
|
||||||
|
* @param {string} method POST, GET...
|
||||||
|
* @param {object} headers
|
||||||
|
* @param {string} type Format of response (json, text...)
|
||||||
|
*
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
static async request(
|
||||||
|
address = "/",
|
||||||
|
body,
|
||||||
|
method = "POST",
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
},
|
||||||
|
type = "json",
|
||||||
|
) {
|
||||||
|
return await fetch(encodeURI(address), { method, headers, body })
|
||||||
|
.then((response) => response[type]());
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,71 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
// Import dependencies
|
||||||
|
import("/js/core.js").then(() => {
|
||||||
|
const dependencies = setInterval(() => {
|
||||||
|
if (typeof core === "function") {
|
||||||
|
clearInterval(dependencies);
|
||||||
|
clearTimeout(timeout);
|
||||||
|
initialization();
|
||||||
|
}
|
||||||
|
}, 10);
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
clearInterval(dependencies);
|
||||||
|
initialization();
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
function initialization() {
|
||||||
|
if (typeof core.damper === "undefined") {
|
||||||
|
// Not initialized
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Damper
|
||||||
|
*
|
||||||
|
* @param {function} function Function to execute after damping
|
||||||
|
* @param {number} timeout Timer in milliseconds (ms)
|
||||||
|
* @param {number} force Argument number storing the status of enforcement execution (see @example)
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* $a = damper(
|
||||||
|
* async (
|
||||||
|
* a, // 0
|
||||||
|
* b, // 1
|
||||||
|
* c, // 2
|
||||||
|
* force = false, // 3
|
||||||
|
* d // 4
|
||||||
|
* ) => {
|
||||||
|
* // Body of function
|
||||||
|
* },
|
||||||
|
* 500,
|
||||||
|
* 3, // 3 -> "force" argument
|
||||||
|
* );
|
||||||
|
*
|
||||||
|
* $a('for a', 'for b', 'for c', true, 'for d'); // Force execute is enabled
|
||||||
|
*
|
||||||
|
* @return {void}
|
||||||
|
*/
|
||||||
|
core.damper = (func, timeout = 300, force) => {
|
||||||
|
// Initializing of the timer
|
||||||
|
let timer;
|
||||||
|
|
||||||
|
return (...args) => {
|
||||||
|
// Deinitializing of the timer
|
||||||
|
clearTimeout(timer);
|
||||||
|
|
||||||
|
if (typeof force === "number" && args[force]) {
|
||||||
|
// Force execution (ignoring the timer)
|
||||||
|
|
||||||
|
func.apply(this, args);
|
||||||
|
} else {
|
||||||
|
// Normal execution
|
||||||
|
|
||||||
|
// Execute the handled function (entry into recursion)
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
func.apply(this, args);
|
||||||
|
}, timeout);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,775 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
// Import dependencies
|
||||||
|
import("/js/core.js").then(() => {
|
||||||
|
const dependencies = setInterval(() => {
|
||||||
|
if (typeof core === "function") {
|
||||||
|
clearInterval(dependencies);
|
||||||
|
clearTimeout(timeout);
|
||||||
|
initialization();
|
||||||
|
}
|
||||||
|
}, 10);
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
clearInterval(dependencies);
|
||||||
|
initialization();
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
function initialization() {
|
||||||
|
if (typeof core.hotline === "undefined") {
|
||||||
|
// Not initialized
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Бегущая строка
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Простой, но мощный класс для создания бегущих строк. Поддерживает
|
||||||
|
* перемещение мышью и прокрутку колесом, полностью настраивается очень гибок
|
||||||
|
* для настроек в CSS и подразумевается, что отлично индексируется поисковыми роботами.
|
||||||
|
* Имеет свой препроцессор, благодаря которому можно создавать бегущие строки
|
||||||
|
* без программирования - с помощью HTML-аттрибутов, а так же возможность
|
||||||
|
* изменять параметры (data-hotline-* аттрибуты) на лету. Есть возможность вызывать
|
||||||
|
* события при выбранных действиях для того, чтобы пользователь имел возможность
|
||||||
|
* дорабатывать функционал без изучения и изменения моего кода
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* сonst hotline = new hotline();
|
||||||
|
* hotline.step = '-5';
|
||||||
|
* hotline.start();
|
||||||
|
*
|
||||||
|
* @todo
|
||||||
|
* 1. Бесконечный режим - элементы не удаляются если видны на экране (будут дубликаты).
|
||||||
|
* Сейчас при БЫСТРОМ прокручивании можно заметит как элементы "появляются" в начале и конце строки.
|
||||||
|
* 2. "gap" and "padding" in wrap should be removed! or added here to the calculations
|
||||||
|
*
|
||||||
|
* @copyright WTFPL
|
||||||
|
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||||
|
*/
|
||||||
|
core.hotline = class hotline {
|
||||||
|
// Идентификатор
|
||||||
|
#id = 0;
|
||||||
|
|
||||||
|
// Оболочка (instanceof HTMLElement)
|
||||||
|
#shell = document.getElementById("hotline");
|
||||||
|
|
||||||
|
// Инстанция горячей строки
|
||||||
|
#instance = null;
|
||||||
|
|
||||||
|
// Перемещение
|
||||||
|
#transfer = true;
|
||||||
|
|
||||||
|
// Движение
|
||||||
|
#move = true;
|
||||||
|
|
||||||
|
// Наблюдатель
|
||||||
|
#observer = null;
|
||||||
|
|
||||||
|
// Реестр запрещённых к изменению параметров
|
||||||
|
#block = new Set(["events"]);
|
||||||
|
|
||||||
|
// Status (null, active, inactive)
|
||||||
|
#status = null;
|
||||||
|
|
||||||
|
// Настраиваемые параметры
|
||||||
|
transfer = null;
|
||||||
|
move = null;
|
||||||
|
delay = 10;
|
||||||
|
step = 1;
|
||||||
|
hover = true;
|
||||||
|
movable = true;
|
||||||
|
sticky = false;
|
||||||
|
wheel = false;
|
||||||
|
delta = null;
|
||||||
|
vertical = false;
|
||||||
|
button = 0; // button for grabbing. 0 is main mouse button (left)
|
||||||
|
observe = false;
|
||||||
|
events = new Map([
|
||||||
|
["start", false],
|
||||||
|
["stop", false],
|
||||||
|
["move", false],
|
||||||
|
["move.block", false],
|
||||||
|
["move.unblock", false],
|
||||||
|
["offset", false],
|
||||||
|
["transfer.start", true],
|
||||||
|
["transfer.end", true],
|
||||||
|
["mousemove", false],
|
||||||
|
["touchmove", false],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Is hotline currently moving due to "onmousemove" or "ontouchmove"?
|
||||||
|
moving = false;
|
||||||
|
|
||||||
|
constructor(id, shell) {
|
||||||
|
// Запись идентификатора
|
||||||
|
if (typeof id === "string" || typeof id === "number") this.#id = id;
|
||||||
|
|
||||||
|
// Запись оболочки
|
||||||
|
if (shell instanceof HTMLElement) this.#shell = shell;
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
if (this.#instance === null) {
|
||||||
|
// Нет запущенной инстанции бегущей строки
|
||||||
|
|
||||||
|
// Инициализация ссылки на ядро
|
||||||
|
const _this = this;
|
||||||
|
|
||||||
|
// Запуск движения
|
||||||
|
this.#instance = setInterval(function () {
|
||||||
|
if (_this.#shell.childElementCount > 1) {
|
||||||
|
// Найдено содержимое бегущей строки (2 и более)
|
||||||
|
|
||||||
|
// Инициализация буфера для временных данных
|
||||||
|
let buffer;
|
||||||
|
|
||||||
|
// Инициализация данных первого элемента в строке
|
||||||
|
const first = {
|
||||||
|
element: (buffer = _this.#shell.firstElementChild),
|
||||||
|
coords: buffer.getBoundingClientRect(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (_this.vertical) {
|
||||||
|
// Вертикальная бегущая строка
|
||||||
|
|
||||||
|
// Инициализация сдвига у первого элемента (движение)
|
||||||
|
first.offset = isNaN(
|
||||||
|
buffer = parseFloat(first.element.style.marginTop),
|
||||||
|
)
|
||||||
|
? 0
|
||||||
|
: buffer;
|
||||||
|
|
||||||
|
// Инициализация отступа до второго элемента у первого элемента (разделение)
|
||||||
|
first.separator = isNaN(
|
||||||
|
buffer = parseFloat(
|
||||||
|
getComputedStyle(first.element).marginBottom,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
? 0
|
||||||
|
: buffer;
|
||||||
|
|
||||||
|
// Инициализация крайнего с конца ребра первого элемента в строке
|
||||||
|
first.end = first.coords.y + first.coords.height +
|
||||||
|
first.separator;
|
||||||
|
} else {
|
||||||
|
// Горизонтальная бегущая строка
|
||||||
|
|
||||||
|
// Инициализация отступа у первого элемента (движение)
|
||||||
|
first.offset = isNaN(
|
||||||
|
buffer = parseFloat(first.element.style.marginLeft),
|
||||||
|
)
|
||||||
|
? 0
|
||||||
|
: buffer;
|
||||||
|
|
||||||
|
// Инициализация отступа до второго элемента у первого элемента (разделение)
|
||||||
|
first.separator = isNaN(
|
||||||
|
buffer = parseFloat(
|
||||||
|
getComputedStyle(first.element).marginRight,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
? 0
|
||||||
|
: buffer;
|
||||||
|
|
||||||
|
// Инициализация крайнего с конца ребра первого элемента в строке
|
||||||
|
first.end = first.coords.x + first.coords.width +
|
||||||
|
first.separator;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(_this.vertical &&
|
||||||
|
Math.round(first.end) < _this.#shell.offsetTop) ||
|
||||||
|
(!_this.vertical &&
|
||||||
|
Math.round(first.end) < _this.#shell.offsetLeft)
|
||||||
|
) {
|
||||||
|
// Элемент (вместе с отступом до второго элемента) вышел из области видимости (строки)
|
||||||
|
|
||||||
|
if (
|
||||||
|
(_this.transfer === null && _this.#transfer) ||
|
||||||
|
_this.transfer === true
|
||||||
|
) {
|
||||||
|
// Перенос разрешен
|
||||||
|
|
||||||
|
if (_this.vertical) {
|
||||||
|
// Вертикальная бегущая строка
|
||||||
|
|
||||||
|
// Удаление отступов (движения)
|
||||||
|
first.element.style.marginTop = null;
|
||||||
|
} else {
|
||||||
|
// Горизонтальная бегущая строка
|
||||||
|
|
||||||
|
// Удаление отступов (движения)
|
||||||
|
first.element.style.marginLeft = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Копирование первого элемента в конец строки
|
||||||
|
_this.#shell.appendChild(first.element);
|
||||||
|
|
||||||
|
if (_this.events.get("transfer.end")) {
|
||||||
|
// Запрошен вызов события: "перемещение в конец"
|
||||||
|
|
||||||
|
// Вызов события: "перемещение в конец"
|
||||||
|
document.dispatchEvent(
|
||||||
|
new CustomEvent(`hotline.${_this.#id}.transfer.end`, {
|
||||||
|
detail: {
|
||||||
|
element: first.element,
|
||||||
|
offset: -(
|
||||||
|
(_this.vertical
|
||||||
|
? first.coords.height
|
||||||
|
: first.coords.width) + first.separator
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
(_this.vertical &&
|
||||||
|
Math.round(first.coords.y) > _this.#shell.offsetTop) ||
|
||||||
|
(!_this.vertical &&
|
||||||
|
Math.round(first.coords.x) > _this.#shell.offsetLeft)
|
||||||
|
) {
|
||||||
|
// Передняя (движущая) граница первого элемента вышла из области видимости
|
||||||
|
|
||||||
|
if (
|
||||||
|
(_this.transfer === null && _this.#transfer) ||
|
||||||
|
_this.transfer === true
|
||||||
|
) {
|
||||||
|
// Перенос разрешен
|
||||||
|
|
||||||
|
// Инициализация отступа у последнего элемента (разделение)
|
||||||
|
const separator = (buffer = isNaN(
|
||||||
|
buffer = parseFloat(
|
||||||
|
getComputedStyle(_this.#shell.lastElementChild)[
|
||||||
|
_this.vertical ? "marginBottom" : "marginRight"
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
? 0
|
||||||
|
: buffer) === 0
|
||||||
|
? first.separator
|
||||||
|
: buffer;
|
||||||
|
|
||||||
|
// Инициализация координат первого элемента в строке
|
||||||
|
const coords = _this.#shell.lastElementChild
|
||||||
|
.getBoundingClientRect();
|
||||||
|
|
||||||
|
if (_this.vertical) {
|
||||||
|
// Вертикальная бегущая строка
|
||||||
|
|
||||||
|
// Удаление отступов (движения)
|
||||||
|
_this.#shell.lastElementChild.style.marginTop =
|
||||||
|
-coords.height - separator + "px";
|
||||||
|
} else {
|
||||||
|
// Горизонтальная бегущая строка
|
||||||
|
|
||||||
|
// Удаление отступов (движения)
|
||||||
|
_this.#shell.lastElementChild.style.marginLeft =
|
||||||
|
-coords.width - separator + "px";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Копирование последнего элемента в начало строки
|
||||||
|
_this.#shell.insertBefore(
|
||||||
|
_this.#shell.lastElementChild,
|
||||||
|
first.element,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Удаление отступов у второго элемента в строке (движения)
|
||||||
|
_this.#shell.children[1].style[
|
||||||
|
_this.vertical ? "marginTop" : "marginLeft"
|
||||||
|
] = null;
|
||||||
|
|
||||||
|
if (_this.events.get("transfer.start")) {
|
||||||
|
// Запрошен вызов события: "перемещение в начало"
|
||||||
|
|
||||||
|
// Вызов события: "перемещение в начало"
|
||||||
|
document.dispatchEvent(
|
||||||
|
new CustomEvent(`hotline.${_this.#id}.transfer.start`, {
|
||||||
|
detail: {
|
||||||
|
element: _this.#shell.lastElementChild,
|
||||||
|
offset:
|
||||||
|
(_this.vertical ? coords.height : coords.width) +
|
||||||
|
separator,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Элемент в области видимости
|
||||||
|
|
||||||
|
if (
|
||||||
|
(_this.move === null && _this.#move) || _this.move === true
|
||||||
|
) {
|
||||||
|
// Движение разрешено
|
||||||
|
|
||||||
|
// Запись новых координат сдвига
|
||||||
|
const offset = first.offset + _this.step;
|
||||||
|
|
||||||
|
// Запись сдвига (движение)
|
||||||
|
_this.offset(offset);
|
||||||
|
|
||||||
|
if (_this.events.get("move")) {
|
||||||
|
// Запрошен вызов события: "движение"
|
||||||
|
|
||||||
|
// Вызов события: "движение"
|
||||||
|
document.dispatchEvent(
|
||||||
|
new CustomEvent(`hotline.${_this.#id}.move`, {
|
||||||
|
detail: {
|
||||||
|
from: first.offset,
|
||||||
|
to: offset,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, _this.delay);
|
||||||
|
|
||||||
|
if (this.hover) {
|
||||||
|
// Запрошена возможность останавливать бегущую строку
|
||||||
|
|
||||||
|
// Инициализация сдвига
|
||||||
|
let offset = 0;
|
||||||
|
|
||||||
|
// Инициализация слушателя события при перемещении элемента в бегущей строке
|
||||||
|
const listener = function (e) {
|
||||||
|
// Увеличение сдвига
|
||||||
|
offset += e.detail.offset ?? 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Объявление переменной в области видимости обработки остановки бегущей строки
|
||||||
|
let move;
|
||||||
|
|
||||||
|
// Инициализация обработчика наведения курсора (остановка движения)
|
||||||
|
this.#shell.onmouseover = function (e) {
|
||||||
|
// Курсор наведён на бегущую строку
|
||||||
|
|
||||||
|
// Блокировка движения
|
||||||
|
_this.#move = false;
|
||||||
|
|
||||||
|
if (_this.events.get("move.block")) {
|
||||||
|
// Запрошен вызов события: "блокировка движения"
|
||||||
|
|
||||||
|
// Вызов события: "блокировка движения"
|
||||||
|
document.dispatchEvent(
|
||||||
|
new CustomEvent(`hotline.${_this.#id}.move.block`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.movable) {
|
||||||
|
// Запрошена возможность двигать бегущую строку
|
||||||
|
|
||||||
|
_this.#shell.onmousedown =
|
||||||
|
_this.#shell.ontouchstart =
|
||||||
|
function (
|
||||||
|
start,
|
||||||
|
) {
|
||||||
|
// Handling a "mousedown" and a "touchstart" on hotline
|
||||||
|
|
||||||
|
if (
|
||||||
|
start.type === "touchstart" ||
|
||||||
|
start.button === _this.button
|
||||||
|
) {
|
||||||
|
const x = start.pageX || start.touches[0].pageX;
|
||||||
|
const y = start.pageY || start.touches[0].pageY;
|
||||||
|
|
||||||
|
// Блокировка движения
|
||||||
|
_this.#move = false;
|
||||||
|
|
||||||
|
if (_this.events.get("move.block")) {
|
||||||
|
// Запрошен вызов события: "блокировка движения"
|
||||||
|
|
||||||
|
// Вызов события: "блокировка движения"
|
||||||
|
document.dispatchEvent(
|
||||||
|
new CustomEvent(`hotline.${_this.#id}.move.block`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Инициализация слушателей события перемещения элемента в бегущей строке
|
||||||
|
document.addEventListener(
|
||||||
|
`hotline.${_this.#id}.transfer.start`,
|
||||||
|
listener,
|
||||||
|
);
|
||||||
|
document.addEventListener(
|
||||||
|
`hotline.${_this.#id}.transfer.end`,
|
||||||
|
listener,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Инициализация буфера для временных данных
|
||||||
|
let buffer;
|
||||||
|
|
||||||
|
// Инициализация данных первого элемента в строке
|
||||||
|
const first = {
|
||||||
|
offset: isNaN(
|
||||||
|
buffer = parseFloat(
|
||||||
|
_this.vertical
|
||||||
|
? _this.#shell.firstElementChild.style
|
||||||
|
.marginTop
|
||||||
|
: _this.#shell.firstElementChild.style
|
||||||
|
.marginLeft,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
? 0
|
||||||
|
: buffer,
|
||||||
|
};
|
||||||
|
|
||||||
|
move = (move) => {
|
||||||
|
// Обработка движения курсора
|
||||||
|
|
||||||
|
if (_this.#status === "active") {
|
||||||
|
// Запись статуса ручного перемещения
|
||||||
|
_this.moving = true;
|
||||||
|
|
||||||
|
const _x = move.pageX || move.touches[0].pageX;
|
||||||
|
const _y = move.pageY || move.touches[0].pageY;
|
||||||
|
|
||||||
|
if (_this.vertical) {
|
||||||
|
// Вертикальная бегущая строка
|
||||||
|
|
||||||
|
// Инициализация буфера местоположения
|
||||||
|
const from =
|
||||||
|
_this.#shell.firstElementChild.style.marginTop;
|
||||||
|
const to = _y - (y + offset - first.offset);
|
||||||
|
|
||||||
|
// Движение
|
||||||
|
_this.#shell.firstElementChild.style.marginTop =
|
||||||
|
to +
|
||||||
|
"px";
|
||||||
|
} else {
|
||||||
|
// Горизонтальная бегущая строка
|
||||||
|
|
||||||
|
// Инициализация буфера местоположения
|
||||||
|
const from =
|
||||||
|
_this.#shell.firstElementChild.style.marginLeft;
|
||||||
|
const to = _x - (x + offset - first.offset);
|
||||||
|
|
||||||
|
// Движение
|
||||||
|
_this.#shell.firstElementChild.style.marginLeft =
|
||||||
|
to +
|
||||||
|
"px";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_this.events.get(move.type)) {
|
||||||
|
// Запрошен вызов события: "перемещение" (мышью или касанием)
|
||||||
|
|
||||||
|
// Вызов события: "перемещение" (мышью или касанием)
|
||||||
|
document.dispatchEvent(
|
||||||
|
new CustomEvent(
|
||||||
|
`hotline.${_this.#id}.${move.type}`,
|
||||||
|
{
|
||||||
|
detail: { from, to },
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запись курсора
|
||||||
|
_this.#shell.style.cursor = "grabbing";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Запуск обработки движения
|
||||||
|
document.addEventListener("mousemove", move);
|
||||||
|
document.addEventListener("touchmove", move);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Перещапись событий браузера (чтобы не дёргалось)
|
||||||
|
_this.#shell.ondragstart = null;
|
||||||
|
|
||||||
|
_this.#shell.onmouseup = _this.#shell.ontouchend = function () {
|
||||||
|
// Курсор деактивирован
|
||||||
|
|
||||||
|
// Запись статуса ручного перемещения
|
||||||
|
_this.moving = false;
|
||||||
|
|
||||||
|
// Остановка обработки движения
|
||||||
|
document.removeEventListener("mousemove", move);
|
||||||
|
document.removeEventListener("touchmove", move);
|
||||||
|
|
||||||
|
// Сброс сдвига
|
||||||
|
offset = 0;
|
||||||
|
|
||||||
|
document.removeEventListener(
|
||||||
|
`hotline.${_this.#id}.transfer.start`,
|
||||||
|
listener,
|
||||||
|
);
|
||||||
|
document.removeEventListener(
|
||||||
|
`hotline.${_this.#id}.transfer.end`,
|
||||||
|
listener,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Разблокировка движения
|
||||||
|
_this.#move = true;
|
||||||
|
|
||||||
|
if (_this.events.get("move.unblock")) {
|
||||||
|
// Запрошен вызов события: "разблокировка движения"
|
||||||
|
|
||||||
|
// Вызов события: "разблокировка движения"
|
||||||
|
document.dispatchEvent(
|
||||||
|
new CustomEvent(`hotline.${_this.#id}.move.unblock`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Восстановление курсора
|
||||||
|
_this.#shell.style.cursor = null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Инициализация обработчика отведения курсора (остановка движения)
|
||||||
|
this.#shell.onmouseleave = function (onmouseleave) {
|
||||||
|
// Курсор отведён от бегущей строки
|
||||||
|
|
||||||
|
if (!_this.sticky) {
|
||||||
|
// Отключено прилипание
|
||||||
|
|
||||||
|
// Запись статуса ручного перемещения
|
||||||
|
_this.moving = false;
|
||||||
|
|
||||||
|
// Остановка обработки движения
|
||||||
|
document.removeEventListener("mousemove", move);
|
||||||
|
document.removeEventListener("touchmove", move);
|
||||||
|
|
||||||
|
document.removeEventListener(
|
||||||
|
`hotline.${_this.#id}.transfer.start`,
|
||||||
|
listener,
|
||||||
|
);
|
||||||
|
document.removeEventListener(
|
||||||
|
`hotline.${_this.#id}.transfer.end`,
|
||||||
|
listener,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Восстановление курсора
|
||||||
|
_this.#shell.style.cursor = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сброс сдвига
|
||||||
|
offset = 0;
|
||||||
|
|
||||||
|
// Разблокировка движения
|
||||||
|
_this.#move = true;
|
||||||
|
|
||||||
|
if (_this.events.get("move.unblock")) {
|
||||||
|
// Запрошен вызов события: "разблокировка движения"
|
||||||
|
|
||||||
|
// Вызов события: "разблокировка движения"
|
||||||
|
document.dispatchEvent(
|
||||||
|
new CustomEvent(`hotline.${_this.#id}.move.unblock`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.wheel) {
|
||||||
|
// Запрошена возможность прокручивать колесом мыши
|
||||||
|
|
||||||
|
// Инициализация обработчика наведения курсора (остановка движения)
|
||||||
|
this.#shell.onwheel = function (e) {
|
||||||
|
// Курсор наведён на бегущую
|
||||||
|
|
||||||
|
// Инициализация буфера для временных данных
|
||||||
|
let buffer;
|
||||||
|
|
||||||
|
// Перемещение
|
||||||
|
_this.offset(
|
||||||
|
(isNaN(
|
||||||
|
buffer = parseFloat(
|
||||||
|
_this.#shell.firstElementChild.style[
|
||||||
|
_this.vertical ? "marginTop" : "marginLeft"
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
? 0
|
||||||
|
: buffer) +
|
||||||
|
(_this.delta === null
|
||||||
|
? e.wheelDelta
|
||||||
|
: e.wheelDelta > 0
|
||||||
|
? _this.delta
|
||||||
|
: -_this.delta),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#status = "active";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.observe) {
|
||||||
|
// Запрошено наблюдение за изменениями аттрибутов элемента бегущей строки
|
||||||
|
|
||||||
|
if (this.#observer === null) {
|
||||||
|
// Отсутствует наблюдатель
|
||||||
|
|
||||||
|
// Инициализация ссылки на ядро
|
||||||
|
const _this = this;
|
||||||
|
|
||||||
|
// Инициализация наблюдателя
|
||||||
|
this.#observer = new MutationObserver(function (mutations) {
|
||||||
|
for (const mutation of mutations) {
|
||||||
|
if (mutation.type === "attributes") {
|
||||||
|
// Запись параметра в инстанцию бегущей строки
|
||||||
|
_this.configure(mutation.attributeName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Перезапуск бегущей строки
|
||||||
|
_this.restart();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Активация наблюдения
|
||||||
|
this.#observer.observe(this.#shell, {
|
||||||
|
attributes: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (this.#observer instanceof MutationObserver) {
|
||||||
|
// Запрошено отключение наблюдения
|
||||||
|
|
||||||
|
// Деактивация наблюдения
|
||||||
|
this.#observer.disconnect();
|
||||||
|
|
||||||
|
// Удаление наблюдателя
|
||||||
|
this.#observer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.events.get("start")) {
|
||||||
|
// Запрошен вызов события: "запуск"
|
||||||
|
|
||||||
|
// Вызов события: "запуск"
|
||||||
|
document.dispatchEvent(
|
||||||
|
new CustomEvent(`hotline.${this.#id}.start`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
this.#status = "inactive";
|
||||||
|
|
||||||
|
// Остановка бегущей строки
|
||||||
|
clearInterval(this.#instance);
|
||||||
|
|
||||||
|
// Удаление инстанции интервала
|
||||||
|
this.#instance = null;
|
||||||
|
|
||||||
|
if (this.events.get("stop")) {
|
||||||
|
// Запрошен вызов события: "остановка"
|
||||||
|
|
||||||
|
// Вызов события: "остановка"
|
||||||
|
document.dispatchEvent(new CustomEvent(`hotline.${this.#id}.stop`));
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
restart() {
|
||||||
|
// Остановка бегущей строки
|
||||||
|
this.stop();
|
||||||
|
|
||||||
|
// Запуск бегущей строки
|
||||||
|
this.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
configure(attribute) {
|
||||||
|
// Инициализация названия параметра
|
||||||
|
const parameter =
|
||||||
|
(/^data-hotline-(\w+)$/.exec(attribute) ?? [, null])[1];
|
||||||
|
|
||||||
|
if (typeof parameter === "string") {
|
||||||
|
// Параметр найден
|
||||||
|
|
||||||
|
// Проверка на разрешение изменения
|
||||||
|
if (this.#block.has(parameter)) return;
|
||||||
|
|
||||||
|
// Инициализация значения параметра
|
||||||
|
const value = this.#shell.getAttribute(attribute);
|
||||||
|
|
||||||
|
if (typeof value !== undefined || typeof value !== null) {
|
||||||
|
// Найдено значение
|
||||||
|
|
||||||
|
// Инициализация буфера для временных данных
|
||||||
|
let buffer;
|
||||||
|
|
||||||
|
// Запись параметра
|
||||||
|
this[parameter] = isNaN(buffer = parseFloat(value))
|
||||||
|
? value === "true" ? true : value === "false" ? false : value
|
||||||
|
: buffer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
offset(value) {
|
||||||
|
// Запись отступа
|
||||||
|
this.#shell.firstElementChild.style[
|
||||||
|
this.vertical ? "marginTop" : "marginLeft"
|
||||||
|
] = value + "px";
|
||||||
|
|
||||||
|
if (this.events.get("offset")) {
|
||||||
|
// Запрошен вызов события: "сдвиг"
|
||||||
|
|
||||||
|
// Вызов события: "сдвиг"
|
||||||
|
document.dispatchEvent(
|
||||||
|
new CustomEvent(`hotline.${this.#id}.offset`, {
|
||||||
|
detail: {
|
||||||
|
to: value,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
static preprocessing(event = false) {
|
||||||
|
// Инициализация счётчиков инстанций горячей строки
|
||||||
|
const success = new Set();
|
||||||
|
let error = 0;
|
||||||
|
|
||||||
|
for (
|
||||||
|
const element of document.querySelectorAll('*[data-hotline="true"]')
|
||||||
|
) {
|
||||||
|
// Перебор элементов для инициализации бегущих строк
|
||||||
|
|
||||||
|
if (typeof element.id === "string") {
|
||||||
|
// Найден идентификатор
|
||||||
|
|
||||||
|
// Инициализация инстанции бегущей строки
|
||||||
|
const hotline = new this(element.id, element);
|
||||||
|
|
||||||
|
for (const attribute of element.getAttributeNames()) {
|
||||||
|
// Перебор аттрибутов
|
||||||
|
|
||||||
|
// Запись параметра в инстанцию бегущей строки
|
||||||
|
hotline.configure(attribute);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запуск бегущей строки
|
||||||
|
hotline.start();
|
||||||
|
|
||||||
|
// Запись инстанции бегущей строки в элемент
|
||||||
|
element.hotline = hotline;
|
||||||
|
|
||||||
|
// Запись в счётчик успешных инициализаций
|
||||||
|
success.add(hotline);
|
||||||
|
} else ++error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event) {
|
||||||
|
// Запрошен вызов события: "предварительная подготовка"
|
||||||
|
|
||||||
|
// Вызов события: "предварительная подготовка"
|
||||||
|
document.dispatchEvent(
|
||||||
|
new CustomEvent(`hotline.preprocessed`, {
|
||||||
|
detail: {
|
||||||
|
success,
|
||||||
|
error,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,45 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
// Import dependencies
|
||||||
|
import("/js/core.js").then(() =>
|
||||||
|
import("/js/damper.js").then(() => {
|
||||||
|
const dependencies = setInterval(() => {
|
||||||
|
if (
|
||||||
|
typeof core === "function" &&
|
||||||
|
typeof core.damper === "function"
|
||||||
|
) {
|
||||||
|
clearInterval(dependencies);
|
||||||
|
clearTimeout(timeout);
|
||||||
|
initialization();
|
||||||
|
}
|
||||||
|
}, 10);
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
clearInterval(dependencies);
|
||||||
|
initialization();
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
function initialization() {
|
||||||
|
if (typeof core.session === "undefined") {
|
||||||
|
// Not initialized
|
||||||
|
|
||||||
|
// Write to the core
|
||||||
|
core.session = class session {
|
||||||
|
/**
|
||||||
|
* Current position in hierarchy of the categories
|
||||||
|
*/
|
||||||
|
static categories = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {void}
|
||||||
|
*/
|
||||||
|
static connect() {
|
||||||
|
core.request(
|
||||||
|
"/session/connect/telegram",
|
||||||
|
window.Telegram.WebApp.initData,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
|
@ -0,0 +1,43 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
// Import dependencies
|
||||||
|
import("/js/core.js").then(() =>
|
||||||
|
import("/js/damper.js").then(() => {
|
||||||
|
const dependencies = setInterval(() => {
|
||||||
|
if (
|
||||||
|
typeof core === "function" &&
|
||||||
|
typeof core.damper === "function"
|
||||||
|
) {
|
||||||
|
clearInterval(dependencies);
|
||||||
|
clearTimeout(timeout);
|
||||||
|
initialization();
|
||||||
|
}
|
||||||
|
}, 10);
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
clearInterval(dependencies);
|
||||||
|
initialization();
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
function initialization() {
|
||||||
|
if (typeof core.telegram === "undefined") {
|
||||||
|
// Not initialized
|
||||||
|
|
||||||
|
// Write to the core
|
||||||
|
core.telegram = class telegram {
|
||||||
|
/**
|
||||||
|
* Telegram WebApp API
|
||||||
|
*
|
||||||
|
* @see {@link https://core.telegram.org/bots/webapps#initializing-mini-apps}
|
||||||
|
*/
|
||||||
|
static api = window.Telegram.WebApp;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* telegram.MainButton.text =
|
||||||
|
typeof core === "object" && core.language === "ru"
|
||||||
|
? "Корзина"
|
||||||
|
: "Cart";
|
||||||
|
telegram.MainButton.show(); */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
|
@ -0,0 +1,88 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace mirzaev\arming_bot;
|
||||||
|
|
||||||
|
// Files of the project
|
||||||
|
use mirzaev\arming_bot\controllers\core as controller,
|
||||||
|
mirzaev\arming_bot\models\core as model,
|
||||||
|
mirzaev\arming_bot\models\telegram;
|
||||||
|
|
||||||
|
// Фреймворк Telegram
|
||||||
|
use Zanzara\Zanzara,
|
||||||
|
Zanzara\Context,
|
||||||
|
Zanzara\Config;
|
||||||
|
|
||||||
|
ini_set('error_reporting', E_ALL);
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
ini_set('display_startup_errors', 1);
|
||||||
|
|
||||||
|
// Версия робота
|
||||||
|
define('ROBOT_VERSION', '1.0.0');
|
||||||
|
|
||||||
|
// Путь до настроек
|
||||||
|
define('SETTINGS', __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'settings');
|
||||||
|
|
||||||
|
// Путь до хранилища
|
||||||
|
define('STORAGE', __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'storage');
|
||||||
|
|
||||||
|
// Файл в формате xlsx с примером excel-документа для импорта каталога
|
||||||
|
define('CATALOG_EXAMPLE', STORAGE . DIRECTORY_SEPARATOR . 'example.xlsx');
|
||||||
|
|
||||||
|
// Файл в формате xlsx для импорта каталога
|
||||||
|
define('CATALOG_IMPORT', STORAGE . DIRECTORY_SEPARATOR . 'import.xlsx');
|
||||||
|
|
||||||
|
// Ключ чат-робота Telegram
|
||||||
|
define('KEY', require(SETTINGS . DIRECTORY_SEPARATOR . 'key.php'));
|
||||||
|
|
||||||
|
// Инициализация библиотек
|
||||||
|
require __DIR__ . DIRECTORY_SEPARATOR
|
||||||
|
. '..' . DIRECTORY_SEPARATOR
|
||||||
|
. '..' . DIRECTORY_SEPARATOR
|
||||||
|
. '..' . DIRECTORY_SEPARATOR
|
||||||
|
. '..' . DIRECTORY_SEPARATOR
|
||||||
|
. 'vendor' . DIRECTORY_SEPARATOR
|
||||||
|
. 'autoload.php';
|
||||||
|
|
||||||
|
// Инициализация ядра контроллеров MINIMAL
|
||||||
|
new controller(false);
|
||||||
|
|
||||||
|
// Инициализация ядра моделей MINIMAL
|
||||||
|
new model(true);
|
||||||
|
|
||||||
|
$config = new Config();
|
||||||
|
$config->setParseMode(Config::PARSE_MODE_MARKDOWN);
|
||||||
|
$config->useReactFileSystem(true);
|
||||||
|
|
||||||
|
$bot = new Zanzara(KEY, $config);
|
||||||
|
|
||||||
|
/* $bot->onUpdate(function (Context $ctx): void {
|
||||||
|
var_dump($ctx->getMessage()->getWebAppData());
|
||||||
|
var_dump($ctx->getEffectiveUser() );
|
||||||
|
}); */
|
||||||
|
|
||||||
|
$bot->onCommand('start', fn($ctx) => telegram::start($ctx));
|
||||||
|
$bot->onCommand('contacts', fn($ctx) => telegram::contacts($ctx));
|
||||||
|
$bot->onCommand('company', fn($ctx) => telegram::company($ctx));
|
||||||
|
$bot->onCommand('community', fn($ctx) => telegram::community($ctx));
|
||||||
|
$bot->onCommand('settings', fn($ctx) => telegram::settings($ctx));
|
||||||
|
|
||||||
|
$bot->onText('💬 Контакты', fn($ctx) => telegram::contacts($ctx));
|
||||||
|
$bot->onText('🏛️ О компании', fn($ctx) => telegram::company($ctx));
|
||||||
|
$bot->onText('🎯 Сообщество', fn($ctx) => telegram::community($ctx));
|
||||||
|
$bot->onText('⚙️ Настройки', fn($ctx) => telegram::settings($ctx));
|
||||||
|
|
||||||
|
$bot->onCbQueryData(['mail'], fn($ctx) => telegram::_mail($ctx));
|
||||||
|
$bot->onCbQueryData(['import_request'], fn($ctx) => telegram::import_request($ctx));
|
||||||
|
$bot->onCbQueryData(['tuning'], fn($ctx) => telegram::tuning($ctx));
|
||||||
|
$bot->onCbQueryData(['brands'], fn($ctx) => telegram::brands($ctx));
|
||||||
|
|
||||||
|
// Инициализация middleware с обработкой аккаунта
|
||||||
|
$bot->middleware([telegram::class, "account"]);
|
||||||
|
|
||||||
|
// Инициализация middleware с обработкой технических работ разных уровней
|
||||||
|
$bot->middleware([telegram::class, "suspension"]);
|
||||||
|
|
||||||
|
// Запуск чат-робота
|
||||||
|
$bot->run();
|
|
@ -0,0 +1,160 @@
|
||||||
|
@charset "UTF-8";
|
||||||
|
|
||||||
|
main>section[data-section="catalog"] {
|
||||||
|
width: var(--width);
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
gap: var(--gap, 5px);
|
||||||
|
/* justify-content: space-between; */
|
||||||
|
/* justify-content: space-evenly; */
|
||||||
|
/* background-color: var(--tg-theme-secondary-bg-color); */
|
||||||
|
}
|
||||||
|
|
||||||
|
main>section[data-section="catalog"]:last-child {
|
||||||
|
margin-bottom: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
main>section[data-section="catalog"]>a.category[type="button"] {
|
||||||
|
height: 23px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-grow: 1;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
color: var(--tg-theme-button-text-color);
|
||||||
|
background-color: var(--tg-theme-button-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
main>section[data-section="catalog"]>article.product {
|
||||||
|
/* --product-height: 200px; */
|
||||||
|
--product-height: 220px;
|
||||||
|
--title-font-size: 0.9rem;
|
||||||
|
--title-height: 1.5rem;
|
||||||
|
--button-height: 33px;
|
||||||
|
position: relative;
|
||||||
|
/* width: calc((100% - var(--gap) * 2) / 3); */
|
||||||
|
width: calc((100% - var(--gap)) / 2);
|
||||||
|
height: var(--product-height);
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
white-space: nowrap;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
overflow: clip;
|
||||||
|
backdrop-filter: brightness(0.7);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
main>section[data-section="catalog"]>article.product:hover {
|
||||||
|
/* flex-grow: 0.1; */
|
||||||
|
/* background-color: var(--tg-theme-secondary-bg-color); */
|
||||||
|
}
|
||||||
|
|
||||||
|
main>section[data-section="catalog"]>article.product:hover>* {
|
||||||
|
transition: 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
main>section[data-section="catalog"]>article.product:not(:hover)>* {
|
||||||
|
transition: 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
main>section[data-section="catalog"]>article.product>img:first-of-type {
|
||||||
|
z-index: -50;
|
||||||
|
position: absolute;
|
||||||
|
bottom: var(--button-height);
|
||||||
|
/* bottom: calc(var(--button-height) + var(--title-height)); */
|
||||||
|
width: 100%;
|
||||||
|
/* height: 100%; */
|
||||||
|
height: calc(var(--product-height) - var(--button-height));
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
main>section[data-section="catalog"]>article.product>img:first-of-type+* {
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
main>section[data-section="catalog"]>article.product>a {
|
||||||
|
padding: 4px 8px 4px 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
word-break: break-all;
|
||||||
|
font-weight: bold;
|
||||||
|
backdrop-filter: brightness(0.4) contrast(1.2);
|
||||||
|
color: var(--text-light, var(--tg-theme-text-color));
|
||||||
|
}
|
||||||
|
|
||||||
|
main>section[data-section="catalog"]>article.product>a.title {
|
||||||
|
padding: 0 8px 0 8px;
|
||||||
|
height: var(--title-height);
|
||||||
|
min-height: var(--title-height);
|
||||||
|
line-height: var(--title-height);
|
||||||
|
font-size: var(--title-font-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
main>section[data-section="catalog"]>article.product:hover>a.title {
|
||||||
|
backdrop-filter: brightness(0.35) contrast(1.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
main>section[data-section="catalog"]>article.product>a+ :is(a, small) {
|
||||||
|
padding: 0px 8px 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
main>section[data-section="catalog"]>article.product>small {
|
||||||
|
padding: 3px 8px 0 8px;
|
||||||
|
white-space: normal;
|
||||||
|
word-break: break-all;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
backdrop-filter: brightness(0.4) contrast(1.2);
|
||||||
|
color: var(--text-light, var(--tg-theme-text-color));
|
||||||
|
}
|
||||||
|
|
||||||
|
main>section[data-section="catalog"]>article.product>small.description {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
main>section[data-section="catalog"]>article.product:hover>small.description {
|
||||||
|
height: calc(var(--product-height) - var(--title-height) - var(--button-height) - 6px);
|
||||||
|
}
|
||||||
|
|
||||||
|
main>section[data-section="catalog"]>article.product:not(:hover)>small.description {
|
||||||
|
height: 0;
|
||||||
|
padding: 0 8px 0px 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
main>section[data-section="catalog"]>article.product>*:has(+ button:last-of-type) {
|
||||||
|
--offset-before-button: 9px;
|
||||||
|
padding: 4px 8px 13px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
main>section[data-section="catalog"]>article.product>button:last-of-type {
|
||||||
|
height: var(--button-height);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--tg-theme-button-text-color);
|
||||||
|
background-color: var(--tg-theme-button-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
main>section[data-section="cart"] {
|
||||||
|
--diameter: 4rem;
|
||||||
|
z-index: 999;
|
||||||
|
right: 5vw;
|
||||||
|
bottom: 5vw;
|
||||||
|
position: fixed;
|
||||||
|
width: var(--diameter);
|
||||||
|
height: var(--diameter);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 100%;
|
||||||
|
background-color: var(--tg-theme-button-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
main>section[data-section="cart"]>i.icon.shopping.cart {
|
||||||
|
top: -1px;
|
||||||
|
color: var(--tg-theme-button-text-color);
|
||||||
|
}
|
|
@ -0,0 +1,179 @@
|
||||||
|
@charset "UTF-8";
|
||||||
|
|
||||||
|
main>section[data-section="catalog"] {
|
||||||
|
width: var(--width);
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
gap: var(--gap, 5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
main>section[data-section="catalog"][data-catalog-type="categories"]>a.category[type="button"] {
|
||||||
|
position: relative;
|
||||||
|
height: 23px;
|
||||||
|
padding: unset;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
color: var(--tg-theme-button-text-color);
|
||||||
|
background-color: var(--tg-theme-button-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
main>section[data-section="catalog"][data-catalog-type="categories"]:last-child {
|
||||||
|
/* margin-bottom: unset; */
|
||||||
|
}
|
||||||
|
|
||||||
|
main>section[data-section="catalog"][data-catalog-type="categories"]>a.category[type="button"]:has(>img) {
|
||||||
|
height: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
main>section[data-section="catalog"][data-catalog-type="categories"]>a.category[type="button"]>img {
|
||||||
|
position: absolute;
|
||||||
|
left: -5%;
|
||||||
|
top: -5%;
|
||||||
|
width: 110%;
|
||||||
|
height: 110%;
|
||||||
|
object-fit: cover;
|
||||||
|
filter: blur(1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
main>section[data-section="catalog"][data-catalog-type="categories"]>a.category[type="button"]:hover>img {
|
||||||
|
filter: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
main>section[data-section="catalog"][data-catalog-type="categories"]>a.category[type="button"]:has(>img)>p {
|
||||||
|
--padding: 0.7rem;
|
||||||
|
position: absolute;
|
||||||
|
left: var(--padding);
|
||||||
|
bottom: var(--padding);
|
||||||
|
right: var(--padding);
|
||||||
|
margin: unset;
|
||||||
|
width: min-content;
|
||||||
|
padding: var(--padding);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
background: var(--tg-theme-secondary-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
main>section[data-section="catalog"][data-catalog-type="categories"]>a.category[type="button"]>p {
|
||||||
|
z-index: 100;
|
||||||
|
padding: 8px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
main>section[data-section="catalog"][data-catalog-type="products"] {
|
||||||
|
--column: calc((100% - var(--gap)) / 2);
|
||||||
|
width: var(--width);
|
||||||
|
display: grid;
|
||||||
|
grid-gap: var(--gap);
|
||||||
|
grid-template-columns: repeat(2, var(--column));
|
||||||
|
grid-auto-flow: row dense;
|
||||||
|
}
|
||||||
|
|
||||||
|
main>section[data-section="catalog"][data-catalog-type="products"]>div.column {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--gap);
|
||||||
|
}
|
||||||
|
|
||||||
|
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
overflow: clip;
|
||||||
|
backdrop-filter: brightness(0.7);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product:hover {
|
||||||
|
/* flex-grow: 0.1; */
|
||||||
|
/* background-color: var(--tg-theme-secondary-bg-color); */
|
||||||
|
}
|
||||||
|
|
||||||
|
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product:hover>* {
|
||||||
|
transition: 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product:not(:hover)>* {
|
||||||
|
transition: 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product>a {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product>a>img:first-of-type {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product>a>img:first-of-type+* {
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product>a>p.title {
|
||||||
|
z-index: 50;
|
||||||
|
margin: unset;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: bold;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
hyphens: auto;
|
||||||
|
color: var(--tg-theme-text-color);
|
||||||
|
background-color: var(--tg-theme-secondary-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product>button:last-of-type {
|
||||||
|
z-index: 100;
|
||||||
|
height: 33px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--tg-theme-button-text-color);
|
||||||
|
background-color: var(--tg-theme-button-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
main>section[data-section="cart"] {
|
||||||
|
--diameter: 4rem;
|
||||||
|
z-index: 999;
|
||||||
|
right: 5vw;
|
||||||
|
bottom: 5vw;
|
||||||
|
position: fixed;
|
||||||
|
width: var(--diameter);
|
||||||
|
height: var(--diameter);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 100%;
|
||||||
|
background-color: var(--tg-theme-button-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
main>section[data-section="cart"]>i.icon.shopping.cart {
|
||||||
|
top: -1px;
|
||||||
|
color: var(--tg-theme-button-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
main>section[data-section="filters"] {
|
||||||
|
--diameter: 4rem;
|
||||||
|
z-index: 999;
|
||||||
|
right: 5vw;
|
||||||
|
bottom: 5vw;
|
||||||
|
position: fixed;
|
||||||
|
width: var(--diameter);
|
||||||
|
height: var(--diameter);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 100%;
|
||||||
|
background-color: var(--tg-theme-button-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
main>section[data-section="filters"][data-filter="brand"] {}
|
|
@ -0,0 +1,117 @@
|
||||||
|
@charset "UTF-8";
|
||||||
|
|
||||||
|
main>section[data-section="catalog"] {
|
||||||
|
width: var(--width);
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
gap: var(--gap, 5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
main>section[data-section="catalog"][data-catalog-type="categories"]>a.category[type="button"] {
|
||||||
|
height: 23px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-grow: 1;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
color: var(--tg-theme-button-text-color);
|
||||||
|
background-color: var(--tg-theme-button-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
main>section[data-section="catalog"][data-catalog-type="categories"]:last-child {
|
||||||
|
/* margin-bottom: unset; */
|
||||||
|
}
|
||||||
|
|
||||||
|
main>section[data-section="catalog"][data-catalog-type="products"] {
|
||||||
|
--column: calc((100% - var(--gap) * 2) / 3);
|
||||||
|
width: var(--width);
|
||||||
|
display: grid;
|
||||||
|
grid-gap: var(--gap);
|
||||||
|
grid-template-columns: repeat(3, var(--column));
|
||||||
|
grid-auto-flow: row dense;
|
||||||
|
}
|
||||||
|
|
||||||
|
main>section[data-section="catalog"][data-catalog-type="products"]>div.column {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--gap);
|
||||||
|
}
|
||||||
|
|
||||||
|
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
overflow: clip;
|
||||||
|
backdrop-filter: brightness(0.7);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product:hover {
|
||||||
|
/* flex-grow: 0.1; */
|
||||||
|
/* background-color: var(--tg-theme-secondary-bg-color); */
|
||||||
|
}
|
||||||
|
|
||||||
|
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product:hover>* {
|
||||||
|
transition: 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product:not(:hover)>* {
|
||||||
|
transition: 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product>img:first-of-type {
|
||||||
|
z-index: -50;
|
||||||
|
width: 100%;
|
||||||
|
max-height: 120px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product>img:first-of-type+* {
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product>a.title {
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: bold;
|
||||||
|
word-break: break-word;
|
||||||
|
hyphens: auto;
|
||||||
|
color: var(--tg-theme-text-color);
|
||||||
|
background-color: var(--tg-theme-secondary-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product>button:last-of-type {
|
||||||
|
height: 33px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--tg-theme-button-text-color);
|
||||||
|
background-color: var(--tg-theme-button-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
main>section[data-section="cart"] {
|
||||||
|
--diameter: 4rem;
|
||||||
|
z-index: 999;
|
||||||
|
right: 5vw;
|
||||||
|
bottom: 5vw;
|
||||||
|
position: fixed;
|
||||||
|
width: var(--diameter);
|
||||||
|
height: var(--diameter);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 100%;
|
||||||
|
background-color: var(--tg-theme-button-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
main>section[data-section="cart"]>i.icon.shopping.cart {
|
||||||
|
top: -1px;
|
||||||
|
color: var(--tg-theme-button-text-color);
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ 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:
|
|
@ -0,0 +1,152 @@
|
||||||
|
@charset "UTF-8";
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--text-light: #fafaff;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
text-decoration: none;
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
font-family: "DejaVu";
|
||||||
|
color: var(--tg-theme-text-color);
|
||||||
|
transition: 0.1s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--tg-theme-link-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow-x: clip;
|
||||||
|
background-color: var(--tg-theme-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
aside {}
|
||||||
|
|
||||||
|
header {}
|
||||||
|
|
||||||
|
main {
|
||||||
|
--offset-x: 2%;
|
||||||
|
padding: 0 var(--offset-x);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 26px;
|
||||||
|
transition: 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
main>*[data-section] {
|
||||||
|
--gap: 16px;
|
||||||
|
--width: calc(100% - var(--gap) * 2);
|
||||||
|
width: var(--width);
|
||||||
|
}
|
||||||
|
|
||||||
|
main>section[data-section]>p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
main>search {
|
||||||
|
--gap: 16px;
|
||||||
|
--border-width: 1px;
|
||||||
|
width: var(--width);
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row;
|
||||||
|
border-radius: 1.375rem;
|
||||||
|
backdrop-filter: contrast(0.8);
|
||||||
|
border: 2px solid transparent;
|
||||||
|
overflow: clip;
|
||||||
|
}
|
||||||
|
|
||||||
|
search:has(input:is(:focus, :active)) {
|
||||||
|
border-color: var(--tg-theme-accent-text-color);
|
||||||
|
transition: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
search>label {
|
||||||
|
margin-inline-start: 0.75rem;
|
||||||
|
width: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
search>label>i.icon {
|
||||||
|
color: var(--tg-theme-subtitle-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
search:has(input:is(:focus, :active))>label>i.icon {
|
||||||
|
color: var(--tg-theme-accent-text-color);
|
||||||
|
transition: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
search>input {
|
||||||
|
width: 100%;
|
||||||
|
max-width: calc(100% - 3.25rem);
|
||||||
|
height: 2.5rem;
|
||||||
|
touch-action: manipulation;
|
||||||
|
padding: calc(.4375rem - var(--border-width)) calc(.625rem - var(--border-width)) calc(.5rem - var(--border-width)) calc(.75rem - var(--border-width));
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
search>input:disabled {
|
||||||
|
cursor: progress;
|
||||||
|
color: var(--tg-theme-subtitle-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
search:has(input:disabled) {
|
||||||
|
backdrop-filter: contrast(0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
:is(button, a[type="button"]) {
|
||||||
|
padding: 8px 16px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--tg-theme-button-text-color);
|
||||||
|
background-color: var(--tg-theme-button-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
height: 33px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a[type="button"] {
|
||||||
|
height: 23px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:is(button, a[type="button"]):is(:hover) {
|
||||||
|
filter: brightness(120%);
|
||||||
|
}
|
||||||
|
|
||||||
|
:is(button, a[type="button"]):active {
|
||||||
|
filter: brightness(80%);
|
||||||
|
transition: 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2 {
|
||||||
|
margin: 28px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {}
|
||||||
|
|
||||||
|
.unselectable {
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-khtml-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
|
@ -2,24 +2,13 @@
|
||||||
|
|
||||||
section#window {
|
section#window {
|
||||||
z-index: 1500;
|
z-index: 1500;
|
||||||
position: fixed;
|
position: absolute;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
/* backdrop-filter: brightness(40%) contrast(120%) grayscale(60%) blur(1.2px); */
|
backdrop-filter: brightness(50%) contrast(120%) grayscale(60%) blur(1.2px);
|
||||||
}
|
|
||||||
|
|
||||||
section#window:before {
|
|
||||||
content: '';
|
|
||||||
z-index: -100;
|
|
||||||
position: absolute;
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
opacity: 0.7;
|
|
||||||
background: #000;
|
|
||||||
filter: brightness(50%);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
section#window>div.card {
|
section#window>div.card {
|
||||||
|
@ -35,7 +24,7 @@ section#window>div.card {
|
||||||
section#window>div.card>h3 {
|
section#window>div.card>h3 {
|
||||||
margin: 0 0 0.5rem 0;
|
margin: 0 0 0.5rem 0;
|
||||||
height: 23px;
|
height: 23px;
|
||||||
padding: 1rem 0;
|
padding: 1rem 1.5rem;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
@ -43,86 +32,50 @@ section#window>div.card>h3 {
|
||||||
background-color: var(--tg-theme-header-bg-color);
|
background-color: var(--tg-theme-header-bg-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
section#window>div.card>h3>span.name {
|
section#window>div.card>h3>span.title {
|
||||||
margin-left: 1.5rem;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* section#window>div.card>h3>small.brand {
|
section#window>div.card>h3>small.brand {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
color: var(--tg-theme-section-header-text-color);
|
color: var(--tg-theme-section-header-text-color);
|
||||||
} */
|
|
||||||
|
|
||||||
section#window>div.card>h3>a.exit[type="button"] {
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
padding: 0.6rem;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
font-weight: bold;
|
|
||||||
color: var(--tg-theme-section-header-text-color);
|
|
||||||
background-color: unset;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
section#window>div.card>div.images {
|
section#window>div.card>div.images {
|
||||||
height: 10rem;
|
height: 10rem;
|
||||||
min-height: 10rem;
|
|
||||||
max-height: 10rem;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
overflow: hidden;
|
overflow: clip;
|
||||||
cursor: zoom-in;
|
|
||||||
transition: 0s;
|
|
||||||
}
|
|
||||||
|
|
||||||
section#window>div.card>div.images:not(.extend):has(> img:last-child:nth-child(2)) {
|
|
||||||
padding: 0rem 1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
section#window>div.card>div.images.extend {
|
section#window>div.card>div.images.extend {
|
||||||
z-index: 9999999;
|
z-index: 9999999;
|
||||||
left: 10vw;
|
left: 0;
|
||||||
top: 10vh;
|
top: 0;
|
||||||
margin: unset !important;
|
margin: unset !important;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
max-width: unset;
|
max-width: unset;
|
||||||
height: 80vh;
|
height: 100vh;
|
||||||
min-height: 80vh;
|
object-fit: contain;
|
||||||
max-height: 80vh;
|
border-radius: unset;
|
||||||
align-items: center;
|
|
||||||
cursor: default;
|
|
||||||
overflow: unset;
|
|
||||||
transition: 0s;
|
transition: 0s;
|
||||||
}
|
cursor: zoom-out;
|
||||||
|
|
||||||
section#window>div.card:has(>div.images.extend)>:not(div.images.extend) {
|
|
||||||
filter: brightness(50%);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
section#window>div.card>div.images>img {
|
section#window>div.card>div.images>img {
|
||||||
margin-right: 0.5rem;
|
margin-right: 0.5rem;
|
||||||
min-width: 150px;
|
width: 10rem;
|
||||||
width: auto;
|
max-width: 10rem;
|
||||||
height: 10rem;
|
height: 100%;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
image-rendering: auto;
|
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
transition: 0s;
|
transition: 0s;
|
||||||
}
|
cursor: zoom-in;
|
||||||
|
|
||||||
section#window>div.card>div.images.extend>img {
|
|
||||||
margin-right: 3vw;
|
|
||||||
width: 80vw;
|
|
||||||
height: auto;
|
|
||||||
max-height: 100%;
|
|
||||||
object-fit: contain;
|
|
||||||
border-radius: 0.75rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
section#window>div.card>div.images>img:last-child {
|
section#window>div.card>div.images>img:last-child {
|
||||||
|
@ -149,7 +102,7 @@ section#window>div.card>p {
|
||||||
min-height: 1rem;
|
min-height: 1rem;
|
||||||
padding: 0 1rem;
|
padding: 0 1rem;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
-webkit-overflow-scrolling: touch;
|
-webkit-overflow-scrolling: touch;
|
||||||
}
|
}
|
||||||
|
|
||||||
section#window>div.card>p:last-of-type {
|
section#window>div.card>p:last-of-type {
|
||||||
|
@ -157,17 +110,15 @@ section#window>div.card>p:last-of-type {
|
||||||
}
|
}
|
||||||
|
|
||||||
section#window>div.card>div.footer {
|
section#window>div.card>div.footer {
|
||||||
container-type: inline-size;
|
|
||||||
container-name: window-footer;
|
|
||||||
padding: 0.8rem 2.3rem;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: row wrap;
|
flex-flow: row wrap;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
gap: 0.8rem;
|
gap: 0 0.8rem;
|
||||||
background-color: var(--tg-theme-header-bg-color);
|
background-color: var(--tg-theme-header-bg-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
section#window>div.card>div.footer>small.dimensions {
|
section#window>div.card>div.footer>small.dimensions {
|
||||||
|
margin-left: 1.5rem;
|
||||||
color: var(--tg-theme-section-header-text-color);
|
color: var(--tg-theme-section-header-text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -177,6 +128,7 @@ section#window>div.card>div.footer>small.weight {
|
||||||
|
|
||||||
section#window>div.card>div.footer>p.cost {
|
section#window>div.card>div.footer>p.cost {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
|
margin-right: 1.5rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -184,19 +136,3 @@ section#window>div.card>div.footer>button.buy {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 3.5rem;
|
height: 3.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@container window-footer (max-width: 350px) {
|
|
||||||
section#window>div.card>div.footer>small:first-of-type {
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
section#window>div.card>div.footer>small:last-of-type {
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
section#window>div.card>div.footer>p.cost {
|
|
||||||
margin: unset;
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue