feat(filters): add filters for the main page
This commit is contained in:
@@ -14,6 +14,12 @@ services:
|
||||
resources:
|
||||
limits:
|
||||
memory: 512M
|
||||
healthcheck:
|
||||
test: [ "CMD", "curl" ,"-f", "http://localhost/index.php?route=extension/tgshop/handle&api_action=health" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 5s
|
||||
environment:
|
||||
- WEB_DOCUMENT_ROOT=/web/upload
|
||||
- PHP_DISPLAY_ERRORS=1
|
||||
@@ -25,6 +31,8 @@ services:
|
||||
- PHP_IDE_CONFIG=serverName=orbstack
|
||||
- php.session.gc_maxlifetime=28800
|
||||
- php.session.cookie_lifetime=0
|
||||
depends_on:
|
||||
- mysql
|
||||
|
||||
mysql:
|
||||
image: mariadb:10.2.7
|
||||
@@ -34,7 +42,7 @@ services:
|
||||
- MYSQL_ROOT_PASSWORD=secret
|
||||
- MYSQL_DATABASE=ocstore3
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"]
|
||||
test: [ "CMD", "mysqladmin" ,"ping", "-h", "localhost", "-u", "root", "-psecret" ]
|
||||
timeout: 20s
|
||||
retries: 10
|
||||
ports:
|
||||
|
||||
@@ -11,6 +11,7 @@ abstract class BaseRule
|
||||
public const CRITERIA_OPTION_NUMBER = 'number';
|
||||
public const CRITERIA_OPTION_BOOLEAN = 'boolean';
|
||||
public const CRITERIA_OPTION_PRODUCT_CATEGORIES = 'product_categories';
|
||||
public const CRITERIA_OPTION_PRODUCT_CATEGORY = 'product_category';
|
||||
public const CRITERIA_OPTION_PRODUCT_MANUFACTURER = 'product_manufacturer';
|
||||
public const CRITERIA_OPTION_PRODUCT_ATTRIBUTE = 'product_attribute';
|
||||
public const CRITERIA_OPTION_PRODUCT_MODEL = 'product_model';
|
||||
|
||||
@@ -22,6 +22,7 @@ class Builder
|
||||
public $joins = [];
|
||||
public $wheres = [];
|
||||
public $orders = [];
|
||||
public $groupBy = [];
|
||||
public $limit;
|
||||
public $offset;
|
||||
public $distinct = false;
|
||||
@@ -447,4 +448,11 @@ class Builder
|
||||
|
||||
return count($join) > 0;
|
||||
}
|
||||
|
||||
public function groupBy(array $columns): Builder
|
||||
{
|
||||
$this->groupBy = $columns;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ abstract class Grammar
|
||||
'orders' => [],
|
||||
'limit' => [],
|
||||
'offset' => [],
|
||||
'groupBy' => [],
|
||||
];
|
||||
|
||||
private function resetCompiled(): void
|
||||
@@ -29,6 +30,7 @@ abstract class Grammar
|
||||
'orders' => [],
|
||||
'limit' => [],
|
||||
'offset' => [],
|
||||
'groupBy' => [],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -194,4 +196,9 @@ abstract class Grammar
|
||||
|
||||
return $this->getRawValue($condition['column']) . " $inOperator (" . $inValues . ')';
|
||||
}
|
||||
|
||||
public function compileGroupBy(Builder $builder, array $groupBy): string
|
||||
{
|
||||
return 'GROUP BY ' . implode(', ', $groupBy);
|
||||
}
|
||||
}
|
||||
|
||||
0
module/oc_telegram_shop/upload/oc_telegram_shop/framework/QueryBuilder/Table.php
Normal file → Executable file
0
module/oc_telegram_shop/upload/oc_telegram_shop/framework/QueryBuilder/Table.php
Normal file → Executable file
0
module/oc_telegram_shop/upload/oc_telegram_shop/framework/Telegram/Commands/StartCommand.php
Normal file → Executable file
0
module/oc_telegram_shop/upload/oc_telegram_shop/framework/Telegram/Commands/StartCommand.php
Normal file → Executable file
@@ -12,6 +12,7 @@ class TelegramValidateInitDataMiddleware
|
||||
'testTgMessage',
|
||||
'manifest',
|
||||
'webhook',
|
||||
'health',
|
||||
];
|
||||
|
||||
public function __construct(SignatureValidator $signatureValidator)
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filters;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Openguru\OpenCartFramework\CriteriaBuilder\Criterion;
|
||||
use Openguru\OpenCartFramework\CriteriaBuilder\Rules\BaseRule;
|
||||
use Openguru\OpenCartFramework\QueryBuilder\Builder;
|
||||
use Openguru\OpenCartFramework\QueryBuilder\JoinClause;
|
||||
|
||||
class ProductCategory extends BaseRule
|
||||
{
|
||||
public const NAME = 'RULE_PRODUCT_CATEGORY';
|
||||
|
||||
public static function initWithDefaults(): BaseRule
|
||||
{
|
||||
return new static(static::NAME, [
|
||||
'product_category_id' => new Criterion(static::CRITERIA_OPTION_PRODUCT_CATEGORIES, [
|
||||
'operator' => static::CRITERIA_OPERATOR_CONTAINS,
|
||||
'value' => null,
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
public function apply(Builder $builder, $operand): void
|
||||
{
|
||||
/** @var Criterion $criterion */
|
||||
foreach ($this->criteria as $criterion) {
|
||||
if ($criterion->type === static::CRITERIA_OPTION_PRODUCT_CATEGORY) {
|
||||
$operator = $criterion->params['operator'];
|
||||
$categoryId = $criterion->params['value'];
|
||||
|
||||
if (! $categoryId) {
|
||||
return;
|
||||
}
|
||||
|
||||
$uniqHash = md5(serialize($criterion));
|
||||
$joinAlias = 'product_category_' . $uniqHash;
|
||||
if ($builder->hasJoinAlias($joinAlias)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$builder->join(
|
||||
db_table('product_to_category') . " AS $joinAlias",
|
||||
function (JoinClause $join) use ($joinAlias, $categoryId) {
|
||||
$join
|
||||
->on('products.product_id', '=', "$joinAlias.product_id")
|
||||
->where("$joinAlias.category_id", '=', $categoryId);
|
||||
},
|
||||
'left'
|
||||
);
|
||||
|
||||
if ($operator === 'contains') {
|
||||
$builder->whereNotNull("$joinAlias.product_id", $operand);
|
||||
} elseif ($operator === 'not_contains') {
|
||||
$builder->whereNull("$joinAlias.product_id", $operand);
|
||||
} else {
|
||||
throw new InvalidArgumentException('Invalid operator: ' . $operator);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,8 +42,7 @@ class ProductPrice extends BaseRule
|
||||
*/
|
||||
public function apply(Builder $builder, $operand)
|
||||
{
|
||||
$includeSpecials = Arr::get($this->criteria, 'include_specials.value', true);
|
||||
$includeDiscounts = Arr::get($this->criteria, 'include_discounts.value', false);
|
||||
$includeSpecials = $this->criteria['include_specials']->params['value'] ?? true;
|
||||
|
||||
/** @var Criterion|null $productPriceCriterion */
|
||||
$productPriceCriterion = $this->criteria['product_price'] ?? null;
|
||||
@@ -67,10 +66,10 @@ class ProductPrice extends BaseRule
|
||||
|
||||
$customerGroupId = config('oc_customer_group_id', 1);
|
||||
|
||||
$sub = $builder->newQuery()
|
||||
$sub2 = $builder->newQuery()
|
||||
->select([
|
||||
'ps.product_id',
|
||||
'ps.price',
|
||||
'product_id',
|
||||
new RawExpression("MIN(CONCAT(LPAD(priority, 5, '0'), LPAD(price, 10, '0'))) AS sort_key"),
|
||||
])
|
||||
->from(db_table('product_special'), 'ps')
|
||||
->where("ps.customer_group_id", '=', $customerGroupId)
|
||||
@@ -80,9 +79,18 @@ class ProductPrice extends BaseRule
|
||||
AND (ps.date_end = '0000-00-00' OR ps.date_end > NOW())
|
||||
"
|
||||
)
|
||||
->orderBy('ps.priority', 'ASC')
|
||||
->orderBy('ps.price', 'ASC')
|
||||
->limit(1);
|
||||
->groupBy(['product_id']);
|
||||
|
||||
$sub = $builder->newQuery()
|
||||
->select([
|
||||
'ps1.product_id',
|
||||
'ps1.price',
|
||||
])
|
||||
->from(db_table('product_special'), 'ps1')
|
||||
->join(new Table($sub2, 'ps2'), function (JoinClause $join) {
|
||||
$join->on('ps1.product_id', '=', 'ps2.product_id')
|
||||
->whereRaw("CONCAT(LPAD(ps1.priority, 5, '0'), LPAD(ps1.price, 10, '0')) = ps2.sort_key");
|
||||
});
|
||||
|
||||
$builder->join(new Table($sub, $joinAlias), function (JoinClause $join) use ($joinAlias) {
|
||||
$join->on('products.product_id', '=', "$joinAlias.product_id");
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Handlers;
|
||||
|
||||
use App\Filters\ProductCategory;
|
||||
use App\Filters\ProductForMainPage;
|
||||
use App\Filters\ProductPrice;
|
||||
use Openguru\OpenCartFramework\Http\JsonResponse;
|
||||
|
||||
class FiltersHandler
|
||||
{
|
||||
public function getFiltersForMainPage(): JsonResponse
|
||||
{
|
||||
$filters = [
|
||||
'operand' => 'AND',
|
||||
'rules' => [
|
||||
ProductPrice::NAME => [
|
||||
'criteria' => [
|
||||
'product_price' => [
|
||||
'type' => 'number',
|
||||
'params' => [
|
||||
'operator' => 'between',
|
||||
'value' => [
|
||||
'from' => 0,
|
||||
'to' => null,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
ProductForMainPage::NAME => [
|
||||
'criteria' => [
|
||||
'product_for_main_page' => [
|
||||
'type' => 'boolean',
|
||||
'params' => [
|
||||
'operator' => 'equals',
|
||||
'value' => true,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
ProductCategory::NAME => [
|
||||
'criteria' => [
|
||||
'product_category_id' => [
|
||||
'type' => 'product_category',
|
||||
'params' => [
|
||||
'operator' => 'contains',
|
||||
'value' => null,
|
||||
],
|
||||
],
|
||||
]
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
return new JsonResponse([
|
||||
'data' => $filters,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Handlers;
|
||||
|
||||
use Openguru\OpenCartFramework\Http\JsonResponse;
|
||||
use Openguru\OpenCartFramework\Http\Response;
|
||||
|
||||
class HealthCheckHandler
|
||||
{
|
||||
public function handle(): JsonResponse
|
||||
{
|
||||
return new JsonResponse([
|
||||
'status' => 'ok',
|
||||
], Response::HTTP_OK);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ namespace App\ServiceProviders;
|
||||
use App\Exceptions\CustomExceptionHandler;
|
||||
use App\Filters\ProductAttribute;
|
||||
use App\Filters\ProductCategories;
|
||||
use App\Filters\ProductCategory;
|
||||
use App\Filters\ProductForMainPage;
|
||||
use App\Filters\ProductManufacturer;
|
||||
use App\Filters\ProductModel;
|
||||
@@ -64,6 +65,7 @@ class AppServiceProvider extends ServiceProvider
|
||||
ProductQuantity::NAME => ProductQuantity::class,
|
||||
ProductStatus::NAME => ProductStatus::class,
|
||||
ProductForMainPage::NAME => ProductForMainPage::class,
|
||||
ProductCategory::NAME => ProductCategory::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ class ProductsService
|
||||
$maxPages = $params['maxPages'] ?? 50;
|
||||
$filters = $params['filters'] ?? [];
|
||||
|
||||
$customerGroupId = (int) $this->oc->config->get('config_customer_group_id');
|
||||
$customerGroupId = (int) $this->settings->get('oc_customer_group_id');
|
||||
$specialPriceSql = "(SELECT price
|
||||
FROM oc_product_special ps
|
||||
WHERE ps.product_id = products.product_id
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
<?php
|
||||
|
||||
use App\Handlers\CategoriesHandler;
|
||||
use App\Handlers\CartHandler;
|
||||
use App\Handlers\CategoriesHandler;
|
||||
use App\Handlers\FiltersHandler;
|
||||
use App\Handlers\HealthCheckHandler;
|
||||
use App\Handlers\OrderHandler;
|
||||
use App\Handlers\ProductsHandler;
|
||||
use App\Handlers\SettingsHandler;
|
||||
use App\Handlers\TelegramHandler;
|
||||
|
||||
return [
|
||||
'health' => [HealthCheckHandler::class, 'handle'],
|
||||
|
||||
'products' => [ProductsHandler::class, 'index'],
|
||||
'product_show' => [ProductsHandler::class, 'show'],
|
||||
'storeOrder' => [OrderHandler::class, 'store'],
|
||||
'filtersForMainPage' => [FiltersHandler::class, 'getFiltersForMainPage'],
|
||||
|
||||
'categoriesList' => [CategoriesHandler::class, 'index'],
|
||||
|
||||
|
||||
@@ -498,4 +498,18 @@ class BuilderTest extends TestCase
|
||||
|
||||
$this->assertEquals($expected, $query->toRawSql());
|
||||
}
|
||||
|
||||
public function testGroupBy(): void
|
||||
{
|
||||
$query = $this->builder->newQuery()
|
||||
->select(['foo', 'bar'])
|
||||
->from('table_1')
|
||||
->groupBy(['foo', 'bar']);
|
||||
|
||||
$this->assertEquals(
|
||||
'SELECT foo, bar FROM table_1 GROUP BY foo, bar',
|
||||
$query->toRawSql(),
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ namespace Tests\Unit;
|
||||
|
||||
use App\Filters\ProductAttribute;
|
||||
use App\Filters\ProductCategories;
|
||||
use App\Filters\ProductCategory;
|
||||
use App\Filters\ProductForMainPage;
|
||||
use App\Filters\ProductManufacturer;
|
||||
use App\Filters\ProductModel;
|
||||
@@ -89,6 +90,7 @@ class CriteriaBuilderTest extends TestCase
|
||||
$rulesRegistry->register(ProductQuantity::NAME, ProductQuantity::class);
|
||||
$rulesRegistry->register(ProductAttribute::NAME, ProductAttribute::class);
|
||||
$rulesRegistry->register(ProductForMainPage::NAME, ProductForMainPage::class);
|
||||
$rulesRegistry->register(ProductCategory::NAME, ProductCategory::class);
|
||||
|
||||
$this->builder = $application->get(Builder::class);
|
||||
$this->criteriaBuilder = $application->get(CriteriaBuilder::class);
|
||||
|
||||
@@ -219,4 +219,15 @@ class MySqlGrammarTest extends TestCase
|
||||
['column' => 'bar', 'as' => null],
|
||||
]));
|
||||
}
|
||||
|
||||
public function testCompileGroupBy(): void
|
||||
{
|
||||
$mock = m::mock(Builder::class);
|
||||
$mock->groupBy = ['foo', 'bar'];
|
||||
|
||||
$this->assertEquals(
|
||||
'GROUP BY foo, bar',
|
||||
$this->grammar->compileGroupBy($mock, ['foo', 'bar'])
|
||||
);
|
||||
}
|
||||
}
|
||||
0
module/oc_telegram_shop/upload/oc_telegram_shop/tests/Unit/TableTest.php
Normal file → Executable file
0
module/oc_telegram_shop/upload/oc_telegram_shop/tests/Unit/TableTest.php
Normal file → Executable file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"operand": "AND",
|
||||
"rules": {
|
||||
"RULE_PRODUCT_CATEGORY": {
|
||||
"criteria": {
|
||||
"product_category_id": {
|
||||
"type": "product_category",
|
||||
"params": {
|
||||
"operator": "contains",
|
||||
"value": 58
|
||||
}
|
||||
}
|
||||
},
|
||||
"__meta": {
|
||||
"group": "other"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
SELECT
|
||||
products.product_id AS product_id,
|
||||
products.image AS image,
|
||||
product_description.name AS name,
|
||||
products.model AS model,
|
||||
products.price AS price,
|
||||
products.quantity AS quantity,
|
||||
products.status AS STATUS,
|
||||
products.noindex AS noindex
|
||||
FROM
|
||||
oc_product AS products
|
||||
INNER JOIN oc_product_description AS product_description ON products.product_id = product_description.product_id
|
||||
AND product_description.language_id = 1
|
||||
LEFT JOIN oc_product_to_category AS product_category_a46414dc254a7328c63a5de8a760fa8f ON products.product_id = product_category_a46414dc254a7328c63a5de8a760fa8f.product_id
|
||||
AND product_category_a46414dc254a7328c63a5de8a760fa8f.category_id = 58
|
||||
WHERE
|
||||
product_category_a46414dc254a7328c63a5de8a760fa8f.product_id IS NOT NULL
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"operand": "AND",
|
||||
"rules": {
|
||||
"RULE_PRODUCT_CATEGORY": {
|
||||
"criteria": {
|
||||
"product_category_id": {
|
||||
"type": "product_category",
|
||||
"params": {
|
||||
"operator": "not_contains",
|
||||
"value": 58
|
||||
}
|
||||
}
|
||||
},
|
||||
"__meta": {
|
||||
"group": "other"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
SELECT
|
||||
products.product_id AS product_id,
|
||||
products.image AS image,
|
||||
product_description.name AS name,
|
||||
products.model AS model,
|
||||
products.price AS price,
|
||||
products.quantity AS quantity,
|
||||
products.status AS STATUS,
|
||||
products.noindex AS noindex
|
||||
FROM
|
||||
oc_product AS products
|
||||
INNER JOIN oc_product_description AS product_description ON products.product_id = product_description.product_id
|
||||
AND product_description.language_id = 1
|
||||
LEFT JOIN oc_product_to_category AS product_category_fa9affd75942255178dc16d1ec8b424f ON products.product_id = product_category_fa9affd75942255178dc16d1ec8b424f.product_id
|
||||
AND product_category_fa9affd75942255178dc16d1ec8b424f.category_id = 58
|
||||
WHERE
|
||||
product_category_fa9affd75942255178dc16d1ec8b424f.product_id IS NULL
|
||||
0
module/oc_telegram_shop/upload/oc_telegram_shop/tests/fixtures/criteria_builder/product_for_main_page/featured/output.sql
vendored
Normal file → Executable file
0
module/oc_telegram_shop/upload/oc_telegram_shop/tests/fixtures/criteria_builder/product_for_main_page/featured/output.sql
vendored
Normal file → Executable file
0
module/oc_telegram_shop/upload/oc_telegram_shop/tests/fixtures/criteria_builder/product_for_main_page/featured/settings_override.json
vendored
Normal file → Executable file
0
module/oc_telegram_shop/upload/oc_telegram_shop/tests/fixtures/criteria_builder/product_for_main_page/featured/settings_override.json
vendored
Normal file → Executable file
0
module/oc_telegram_shop/upload/oc_telegram_shop/tests/fixtures/criteria_builder/product_for_main_page/featured_empty_products/output.sql
vendored
Normal file → Executable file
0
module/oc_telegram_shop/upload/oc_telegram_shop/tests/fixtures/criteria_builder/product_for_main_page/featured_empty_products/output.sql
vendored
Normal file → Executable file
0
module/oc_telegram_shop/upload/oc_telegram_shop/tests/fixtures/criteria_builder/product_for_main_page/featured_empty_products/settings_override.json
vendored
Normal file → Executable file
0
module/oc_telegram_shop/upload/oc_telegram_shop/tests/fixtures/criteria_builder/product_for_main_page/featured_empty_products/settings_override.json
vendored
Normal file → Executable file
0
module/oc_telegram_shop/upload/oc_telegram_shop/tests/fixtures/criteria_builder/product_for_main_page/latests/output.sql
vendored
Normal file → Executable file
0
module/oc_telegram_shop/upload/oc_telegram_shop/tests/fixtures/criteria_builder/product_for_main_page/latests/output.sql
vendored
Normal file → Executable file
0
module/oc_telegram_shop/upload/oc_telegram_shop/tests/fixtures/criteria_builder/product_for_main_page/latests/settings_override.json
vendored
Normal file → Executable file
0
module/oc_telegram_shop/upload/oc_telegram_shop/tests/fixtures/criteria_builder/product_for_main_page/latests/settings_override.json
vendored
Normal file → Executable file
0
module/oc_telegram_shop/upload/oc_telegram_shop/tests/fixtures/criteria_builder/product_for_main_page/most_viewed/output.sql
vendored
Normal file → Executable file
0
module/oc_telegram_shop/upload/oc_telegram_shop/tests/fixtures/criteria_builder/product_for_main_page/most_viewed/output.sql
vendored
Normal file → Executable file
0
module/oc_telegram_shop/upload/oc_telegram_shop/tests/fixtures/criteria_builder/product_for_main_page/most_viewed/settings_override.json
vendored
Normal file → Executable file
0
module/oc_telegram_shop/upload/oc_telegram_shop/tests/fixtures/criteria_builder/product_for_main_page/most_viewed/settings_override.json
vendored
Normal file → Executable file
@@ -13,8 +13,19 @@ FROM
|
||||
AND product_description.language_id = 1
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
ps.product_id,
|
||||
ps.price
|
||||
ps1.product_id,
|
||||
ps1.price
|
||||
FROM
|
||||
oc_product_special AS ps1
|
||||
INNER JOIN (
|
||||
SELECT
|
||||
product_id,
|
||||
MIN(
|
||||
CONCAT(
|
||||
LPAD(priority, 5, '0'),
|
||||
LPAD(price, 10, '0')
|
||||
)
|
||||
) AS sort_key
|
||||
FROM
|
||||
oc_product_special AS ps
|
||||
WHERE
|
||||
@@ -27,11 +38,13 @@ FROM
|
||||
ps.date_end = '0000-00-00'
|
||||
OR ps.date_end > NOW()
|
||||
)
|
||||
ORDER BY
|
||||
ps.priority ASC,
|
||||
ps.price ASC
|
||||
LIMIT
|
||||
1
|
||||
GROUP BY
|
||||
product_id
|
||||
) AS ps2 ON ps1.product_id = ps2.product_id
|
||||
AND CONCAT(
|
||||
LPAD(ps1.priority, 5, '0'),
|
||||
LPAD(ps1.price, 10, '0')
|
||||
) = ps2.sort_key
|
||||
) AS product_specials_4bf5415bae7c037731801559d0410697 ON products.product_id = product_specials_4bf5415bae7c037731801559d0410697.product_id
|
||||
WHERE
|
||||
COALESCE(
|
||||
|
||||
@@ -13,8 +13,19 @@ FROM
|
||||
AND product_description.language_id = 1
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
ps.product_id,
|
||||
ps.price
|
||||
ps1.product_id,
|
||||
ps1.price
|
||||
FROM
|
||||
oc_product_special AS ps1
|
||||
INNER JOIN (
|
||||
SELECT
|
||||
product_id,
|
||||
MIN(
|
||||
CONCAT(
|
||||
LPAD(priority, 5, '0'),
|
||||
LPAD(price, 10, '0')
|
||||
)
|
||||
) AS sort_key
|
||||
FROM
|
||||
oc_product_special AS ps
|
||||
WHERE
|
||||
@@ -27,11 +38,13 @@ FROM
|
||||
ps.date_end = '0000-00-00'
|
||||
OR ps.date_end > NOW()
|
||||
)
|
||||
ORDER BY
|
||||
ps.priority ASC,
|
||||
ps.price ASC
|
||||
LIMIT
|
||||
1
|
||||
GROUP BY
|
||||
product_id
|
||||
) AS ps2 ON ps1.product_id = ps2.product_id
|
||||
AND CONCAT(
|
||||
LPAD(ps1.priority, 5, '0'),
|
||||
LPAD(ps1.price, 10, '0')
|
||||
) = ps2.sort_key
|
||||
) AS product_specials_f1ca65a396fc2e41a23a3661b84d1519 ON products.product_id = product_specials_f1ca65a396fc2e41a23a3661b84d1519.product_id
|
||||
WHERE
|
||||
COALESCE(
|
||||
|
||||
@@ -13,8 +13,19 @@ FROM
|
||||
AND product_description.language_id = 1
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
ps.product_id,
|
||||
ps.price
|
||||
ps1.product_id,
|
||||
ps1.price
|
||||
FROM
|
||||
oc_product_special AS ps1
|
||||
INNER JOIN (
|
||||
SELECT
|
||||
product_id,
|
||||
MIN(
|
||||
CONCAT(
|
||||
LPAD(priority, 5, '0'),
|
||||
LPAD(price, 10, '0')
|
||||
)
|
||||
) AS sort_key
|
||||
FROM
|
||||
oc_product_special AS ps
|
||||
WHERE
|
||||
@@ -27,11 +38,13 @@ FROM
|
||||
ps.date_end = '0000-00-00'
|
||||
OR ps.date_end > NOW()
|
||||
)
|
||||
ORDER BY
|
||||
ps.priority ASC,
|
||||
ps.price ASC
|
||||
LIMIT
|
||||
1
|
||||
GROUP BY
|
||||
product_id
|
||||
) AS ps2 ON ps1.product_id = ps2.product_id
|
||||
AND CONCAT(
|
||||
LPAD(ps1.priority, 5, '0'),
|
||||
LPAD(ps1.price, 10, '0')
|
||||
) = ps2.sort_key
|
||||
) AS product_specials_f0c4ece67502916e844720b12abc54a9 ON products.product_id = product_specials_f0c4ece67502916e844720b12abc54a9.product_id
|
||||
WHERE
|
||||
COALESCE(
|
||||
|
||||
@@ -13,8 +13,19 @@ FROM
|
||||
AND product_description.language_id = 1
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
ps.product_id,
|
||||
ps.price
|
||||
ps1.product_id,
|
||||
ps1.price
|
||||
FROM
|
||||
oc_product_special AS ps1
|
||||
INNER JOIN (
|
||||
SELECT
|
||||
product_id,
|
||||
MIN(
|
||||
CONCAT(
|
||||
LPAD(priority, 5, '0'),
|
||||
LPAD(price, 10, '0')
|
||||
)
|
||||
) AS sort_key
|
||||
FROM
|
||||
oc_product_special AS ps
|
||||
WHERE
|
||||
@@ -27,11 +38,13 @@ FROM
|
||||
ps.date_end = '0000-00-00'
|
||||
OR ps.date_end > NOW()
|
||||
)
|
||||
ORDER BY
|
||||
ps.priority ASC,
|
||||
ps.price ASC
|
||||
LIMIT
|
||||
1
|
||||
GROUP BY
|
||||
product_id
|
||||
) AS ps2 ON ps1.product_id = ps2.product_id
|
||||
AND CONCAT(
|
||||
LPAD(ps1.priority, 5, '0'),
|
||||
LPAD(ps1.price, 10, '0')
|
||||
) = ps2.sort_key
|
||||
) AS product_specials_5173bd7b8335ab5a9f4e37227d0c317f ON products.product_id = product_specials_5173bd7b8335ab5a9f4e37227d0c317f.product_id
|
||||
WHERE
|
||||
COALESCE(
|
||||
|
||||
@@ -13,8 +13,19 @@ FROM
|
||||
AND product_description.language_id = 1
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
ps.product_id,
|
||||
ps.price
|
||||
ps1.product_id,
|
||||
ps1.price
|
||||
FROM
|
||||
oc_product_special AS ps1
|
||||
INNER JOIN (
|
||||
SELECT
|
||||
product_id,
|
||||
MIN(
|
||||
CONCAT(
|
||||
LPAD(priority, 5, '0'),
|
||||
LPAD(price, 10, '0')
|
||||
)
|
||||
) AS sort_key
|
||||
FROM
|
||||
oc_product_special AS ps
|
||||
WHERE
|
||||
@@ -27,11 +38,13 @@ FROM
|
||||
ps.date_end = '0000-00-00'
|
||||
OR ps.date_end > NOW()
|
||||
)
|
||||
ORDER BY
|
||||
ps.priority ASC,
|
||||
ps.price ASC
|
||||
LIMIT
|
||||
1
|
||||
GROUP BY
|
||||
product_id
|
||||
) AS ps2 ON ps1.product_id = ps2.product_id
|
||||
AND CONCAT(
|
||||
LPAD(ps1.priority, 5, '0'),
|
||||
LPAD(ps1.price, 10, '0')
|
||||
) = ps2.sort_key
|
||||
) AS product_specials_7b934dfdfc5b809270875934c10acc06 ON products.product_id = product_specials_7b934dfdfc5b809270875934c10acc06.product_id
|
||||
WHERE
|
||||
COALESCE(
|
||||
|
||||
@@ -13,8 +13,19 @@ FROM
|
||||
AND product_description.language_id = 1
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
ps.product_id,
|
||||
ps.price
|
||||
ps1.product_id,
|
||||
ps1.price
|
||||
FROM
|
||||
oc_product_special AS ps1
|
||||
INNER JOIN (
|
||||
SELECT
|
||||
product_id,
|
||||
MIN(
|
||||
CONCAT(
|
||||
LPAD(priority, 5, '0'),
|
||||
LPAD(price, 10, '0')
|
||||
)
|
||||
) AS sort_key
|
||||
FROM
|
||||
oc_product_special AS ps
|
||||
WHERE
|
||||
@@ -27,11 +38,13 @@ FROM
|
||||
ps.date_end = '0000-00-00'
|
||||
OR ps.date_end > NOW()
|
||||
)
|
||||
ORDER BY
|
||||
ps.priority ASC,
|
||||
ps.price ASC
|
||||
LIMIT
|
||||
1
|
||||
GROUP BY
|
||||
product_id
|
||||
) AS ps2 ON ps1.product_id = ps2.product_id
|
||||
AND CONCAT(
|
||||
LPAD(ps1.priority, 5, '0'),
|
||||
LPAD(ps1.price, 10, '0')
|
||||
) = ps2.sort_key
|
||||
) AS product_specials_51a33d8bcfeece60de797c18e3800075 ON products.product_id = product_specials_51a33d8bcfeece60de797c18e3800075.product_id
|
||||
WHERE
|
||||
COALESCE(
|
||||
|
||||
@@ -13,8 +13,19 @@ FROM
|
||||
AND product_description.language_id = 1
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
ps.product_id,
|
||||
ps.price
|
||||
ps1.product_id,
|
||||
ps1.price
|
||||
FROM
|
||||
oc_product_special AS ps1
|
||||
INNER JOIN (
|
||||
SELECT
|
||||
product_id,
|
||||
MIN(
|
||||
CONCAT(
|
||||
LPAD(priority, 5, '0'),
|
||||
LPAD(price, 10, '0')
|
||||
)
|
||||
) AS sort_key
|
||||
FROM
|
||||
oc_product_special AS ps
|
||||
WHERE
|
||||
@@ -27,11 +38,13 @@ FROM
|
||||
ps.date_end = '0000-00-00'
|
||||
OR ps.date_end > NOW()
|
||||
)
|
||||
ORDER BY
|
||||
ps.priority ASC,
|
||||
ps.price ASC
|
||||
LIMIT
|
||||
1
|
||||
GROUP BY
|
||||
product_id
|
||||
) AS ps2 ON ps1.product_id = ps2.product_id
|
||||
AND CONCAT(
|
||||
LPAD(ps1.priority, 5, '0'),
|
||||
LPAD(ps1.price, 10, '0')
|
||||
) = ps2.sort_key
|
||||
) AS product_specials_7de8997be994ada47d62cdfe2b7369c9 ON products.product_id = product_specials_7de8997be994ada47d62cdfe2b7369c9.product_id
|
||||
WHERE
|
||||
COALESCE(
|
||||
|
||||
@@ -13,8 +13,19 @@ FROM
|
||||
AND product_description.language_id = 1
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
ps.product_id,
|
||||
ps.price
|
||||
ps1.product_id,
|
||||
ps1.price
|
||||
FROM
|
||||
oc_product_special AS ps1
|
||||
INNER JOIN (
|
||||
SELECT
|
||||
product_id,
|
||||
MIN(
|
||||
CONCAT(
|
||||
LPAD(priority, 5, '0'),
|
||||
LPAD(price, 10, '0')
|
||||
)
|
||||
) AS sort_key
|
||||
FROM
|
||||
oc_product_special AS ps
|
||||
WHERE
|
||||
@@ -27,11 +38,13 @@ FROM
|
||||
ps.date_end = '0000-00-00'
|
||||
OR ps.date_end > NOW()
|
||||
)
|
||||
ORDER BY
|
||||
ps.priority ASC,
|
||||
ps.price ASC
|
||||
LIMIT
|
||||
1
|
||||
GROUP BY
|
||||
product_id
|
||||
) AS ps2 ON ps1.product_id = ps2.product_id
|
||||
AND CONCAT(
|
||||
LPAD(ps1.priority, 5, '0'),
|
||||
LPAD(ps1.price, 10, '0')
|
||||
) = ps2.sort_key
|
||||
) AS product_specials_78ccac1dc1d86997e6eac0613f2eb5f3 ON products.product_id = product_specials_78ccac1dc1d86997e6eac0613f2eb5f3.product_id
|
||||
WHERE
|
||||
COALESCE(
|
||||
|
||||
@@ -13,8 +13,19 @@ FROM
|
||||
AND product_description.language_id = 1
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
ps.product_id,
|
||||
ps.price
|
||||
ps1.product_id,
|
||||
ps1.price
|
||||
FROM
|
||||
oc_product_special AS ps1
|
||||
INNER JOIN (
|
||||
SELECT
|
||||
product_id,
|
||||
MIN(
|
||||
CONCAT(
|
||||
LPAD(priority, 5, '0'),
|
||||
LPAD(price, 10, '0')
|
||||
)
|
||||
) AS sort_key
|
||||
FROM
|
||||
oc_product_special AS ps
|
||||
WHERE
|
||||
@@ -27,11 +38,13 @@ FROM
|
||||
ps.date_end = '0000-00-00'
|
||||
OR ps.date_end > NOW()
|
||||
)
|
||||
ORDER BY
|
||||
ps.priority ASC,
|
||||
ps.price ASC
|
||||
LIMIT
|
||||
1
|
||||
GROUP BY
|
||||
product_id
|
||||
) AS ps2 ON ps1.product_id = ps2.product_id
|
||||
AND CONCAT(
|
||||
LPAD(ps1.priority, 5, '0'),
|
||||
LPAD(ps1.price, 10, '0')
|
||||
) = ps2.sort_key
|
||||
) AS product_specials_5816f1e65a3d296a26635f070d0439bc ON products.product_id = product_specials_5816f1e65a3d296a26635f070d0439bc.product_id
|
||||
WHERE
|
||||
COALESCE(
|
||||
|
||||
@@ -13,8 +13,19 @@ FROM
|
||||
AND product_description.language_id = 1
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
ps.product_id,
|
||||
ps.price
|
||||
ps1.product_id,
|
||||
ps1.price
|
||||
FROM
|
||||
oc_product_special AS ps1
|
||||
INNER JOIN (
|
||||
SELECT
|
||||
product_id,
|
||||
MIN(
|
||||
CONCAT(
|
||||
LPAD(priority, 5, '0'),
|
||||
LPAD(price, 10, '0')
|
||||
)
|
||||
) AS sort_key
|
||||
FROM
|
||||
oc_product_special AS ps
|
||||
WHERE
|
||||
@@ -27,11 +38,13 @@ FROM
|
||||
ps.date_end = '0000-00-00'
|
||||
OR ps.date_end > NOW()
|
||||
)
|
||||
ORDER BY
|
||||
ps.priority ASC,
|
||||
ps.price ASC
|
||||
LIMIT
|
||||
1
|
||||
GROUP BY
|
||||
product_id
|
||||
) AS ps2 ON ps1.product_id = ps2.product_id
|
||||
AND CONCAT(
|
||||
LPAD(ps1.priority, 5, '0'),
|
||||
LPAD(ps1.price, 10, '0')
|
||||
) = ps2.sort_key
|
||||
) AS product_specials_51a33d8bcfeece60de797c18e3800075 ON products.product_id = product_specials_51a33d8bcfeece60de797c18e3800075.product_id
|
||||
WHERE
|
||||
COALESCE(
|
||||
|
||||
@@ -13,8 +13,19 @@ FROM
|
||||
AND product_description.language_id = 1
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
ps.product_id,
|
||||
ps.price
|
||||
ps1.product_id,
|
||||
ps1.price
|
||||
FROM
|
||||
oc_product_special AS ps1
|
||||
INNER JOIN (
|
||||
SELECT
|
||||
product_id,
|
||||
MIN(
|
||||
CONCAT(
|
||||
LPAD(priority, 5, '0'),
|
||||
LPAD(price, 10, '0')
|
||||
)
|
||||
) AS sort_key
|
||||
FROM
|
||||
oc_product_special AS ps
|
||||
WHERE
|
||||
@@ -27,11 +38,13 @@ FROM
|
||||
ps.date_end = '0000-00-00'
|
||||
OR ps.date_end > NOW()
|
||||
)
|
||||
ORDER BY
|
||||
ps.priority ASC,
|
||||
ps.price ASC
|
||||
LIMIT
|
||||
1
|
||||
GROUP BY
|
||||
product_id
|
||||
) AS ps2 ON ps1.product_id = ps2.product_id
|
||||
AND CONCAT(
|
||||
LPAD(ps1.priority, 5, '0'),
|
||||
LPAD(ps1.price, 10, '0')
|
||||
) = ps2.sort_key
|
||||
) AS product_specials_51a33d8bcfeece60de797c18e3800075 ON products.product_id = product_specials_51a33d8bcfeece60de797c18e3800075.product_id
|
||||
WHERE
|
||||
COALESCE(
|
||||
|
||||
@@ -13,8 +13,19 @@ FROM
|
||||
AND product_description.language_id = 1
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
ps.product_id,
|
||||
ps.price
|
||||
ps1.product_id,
|
||||
ps1.price
|
||||
FROM
|
||||
oc_product_special AS ps1
|
||||
INNER JOIN (
|
||||
SELECT
|
||||
product_id,
|
||||
MIN(
|
||||
CONCAT(
|
||||
LPAD(priority, 5, '0'),
|
||||
LPAD(price, 10, '0')
|
||||
)
|
||||
) AS sort_key
|
||||
FROM
|
||||
oc_product_special AS ps
|
||||
WHERE
|
||||
@@ -27,11 +38,13 @@ FROM
|
||||
ps.date_end = '0000-00-00'
|
||||
OR ps.date_end > NOW()
|
||||
)
|
||||
ORDER BY
|
||||
ps.priority ASC,
|
||||
ps.price ASC
|
||||
LIMIT
|
||||
1
|
||||
GROUP BY
|
||||
product_id
|
||||
) AS ps2 ON ps1.product_id = ps2.product_id
|
||||
AND CONCAT(
|
||||
LPAD(ps1.priority, 5, '0'),
|
||||
LPAD(ps1.price, 10, '0')
|
||||
) = ps2.sort_key
|
||||
) AS product_specials_cf0f856fa900f18f5c8c0a57357532f5 ON products.product_id = product_specials_cf0f856fa900f18f5c8c0a57357532f5.product_id
|
||||
WHERE
|
||||
COALESCE(
|
||||
|
||||
@@ -13,8 +13,19 @@ FROM
|
||||
AND product_description.language_id = 1
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
ps.product_id,
|
||||
ps.price
|
||||
ps1.product_id,
|
||||
ps1.price
|
||||
FROM
|
||||
oc_product_special AS ps1
|
||||
INNER JOIN (
|
||||
SELECT
|
||||
product_id,
|
||||
MIN(
|
||||
CONCAT(
|
||||
LPAD(priority, 5, '0'),
|
||||
LPAD(price, 10, '0')
|
||||
)
|
||||
) AS sort_key
|
||||
FROM
|
||||
oc_product_special AS ps
|
||||
WHERE
|
||||
@@ -27,11 +38,13 @@ FROM
|
||||
ps.date_end = '0000-00-00'
|
||||
OR ps.date_end > NOW()
|
||||
)
|
||||
ORDER BY
|
||||
ps.priority ASC,
|
||||
ps.price ASC
|
||||
LIMIT
|
||||
1
|
||||
GROUP BY
|
||||
product_id
|
||||
) AS ps2 ON ps1.product_id = ps2.product_id
|
||||
AND CONCAT(
|
||||
LPAD(ps1.priority, 5, '0'),
|
||||
LPAD(ps1.price, 10, '0')
|
||||
) = ps2.sort_key
|
||||
) AS product_specials_08e0a0248dc92591ae55fdd966728ba0 ON products.product_id = product_specials_08e0a0248dc92591ae55fdd966728ba0.product_id
|
||||
WHERE
|
||||
COALESCE(
|
||||
|
||||
@@ -13,8 +13,19 @@ FROM
|
||||
AND product_description.language_id = 1
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
ps.product_id,
|
||||
ps.price
|
||||
ps1.product_id,
|
||||
ps1.price
|
||||
FROM
|
||||
oc_product_special AS ps1
|
||||
INNER JOIN (
|
||||
SELECT
|
||||
product_id,
|
||||
MIN(
|
||||
CONCAT(
|
||||
LPAD(priority, 5, '0'),
|
||||
LPAD(price, 10, '0')
|
||||
)
|
||||
) AS sort_key
|
||||
FROM
|
||||
oc_product_special AS ps
|
||||
WHERE
|
||||
@@ -27,11 +38,13 @@ FROM
|
||||
ps.date_end = '0000-00-00'
|
||||
OR ps.date_end > NOW()
|
||||
)
|
||||
ORDER BY
|
||||
ps.priority ASC,
|
||||
ps.price ASC
|
||||
LIMIT
|
||||
1
|
||||
GROUP BY
|
||||
product_id
|
||||
) AS ps2 ON ps1.product_id = ps2.product_id
|
||||
AND CONCAT(
|
||||
LPAD(ps1.priority, 5, '0'),
|
||||
LPAD(ps1.price, 10, '0')
|
||||
) = ps2.sort_key
|
||||
) AS product_specials_47d71067324729b89b13414a6ea869e0 ON products.product_id = product_specials_47d71067324729b89b13414a6ea869e0.product_id
|
||||
WHERE
|
||||
COALESCE(
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"operand": "AND",
|
||||
"rules": {
|
||||
"RULE_PRODUCT_PRICE": {
|
||||
"criteria": {
|
||||
"product_price": {
|
||||
"type": "number",
|
||||
"params": {
|
||||
"operator": "equals",
|
||||
"value": {
|
||||
"from": 100,
|
||||
"to": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"include_specials": {
|
||||
"type": "boolean",
|
||||
"params": {
|
||||
"value": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
SELECT
|
||||
products.product_id AS product_id,
|
||||
products.image AS image,
|
||||
product_description.name AS name,
|
||||
products.model AS model,
|
||||
products.price AS price,
|
||||
products.quantity AS quantity,
|
||||
products.status AS STATUS,
|
||||
products.noindex AS noindex
|
||||
FROM
|
||||
oc_product AS products
|
||||
INNER JOIN oc_product_description AS product_description ON products.product_id = product_description.product_id
|
||||
AND product_description.language_id = 1
|
||||
WHERE
|
||||
products.price = 100
|
||||
@@ -5,9 +5,9 @@
|
||||
<section class="safe-top">
|
||||
<FullscreenViewport v-if="platform === 'ios' || platform === 'android'"/>
|
||||
<RouterView v-slot="{ Component, route }">
|
||||
<Transition name="route" appear>
|
||||
<KeepAlive include="Home" :key="filtersStore.paramsHashForRouter">
|
||||
<component :is="Component" :key="route.fullPath"/>
|
||||
</Transition>
|
||||
</KeepAlive>
|
||||
</RouterView>
|
||||
<CartButton v-if="settings.store_enabled"/>
|
||||
</section>
|
||||
@@ -21,6 +21,7 @@ import {useMiniApp, FullscreenViewport} from 'vue-tg';
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
import CartButton from "@/components/CartButton.vue";
|
||||
import {useSettingsStore} from "@/stores/SettingsStore.js";
|
||||
import {useProductFiltersStore} from "@/stores/ProductFiltersStore.js";
|
||||
|
||||
const tg = useMiniApp();
|
||||
const platform = ref();
|
||||
@@ -32,6 +33,7 @@ disableVerticalSwipes();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const settings = useSettingsStore();
|
||||
const filtersStore = useProductFiltersStore();
|
||||
const backButton = window.Telegram.WebApp.BackButton;
|
||||
const haptic = window.Telegram.WebApp.HapticFeedback;
|
||||
|
||||
@@ -54,21 +56,3 @@ watch(
|
||||
{immediate: true}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* route transitions */
|
||||
.route-enter-active,
|
||||
.route-leave-active {
|
||||
transition: opacity 0.25s ease, transform 0.25s ease;
|
||||
}
|
||||
|
||||
.route-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
.route-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
</style>
|
||||
@@ -1,14 +1,10 @@
|
||||
<template>
|
||||
<div class="flex flex-col items-center justify-center text-center py-16">
|
||||
<span class="text-5xl mb-4">🛒</span>
|
||||
<h2 class="text-xl font-semibold mb-2">Здесь пока пусто</h2>
|
||||
<p class="text-sm mb-4">Мы уже выехали на склад, чтобы найти что-нибудь подходящее.</p>
|
||||
<button class="btn btn-primary" @click="goBack">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5 8.25 12l7.5-7.5" />
|
||||
</svg>
|
||||
Назад
|
||||
</button>
|
||||
<h2 class="text-xl font-semibold mb-2">Здесь нет товаров</h2>
|
||||
<p class="text-sm mb-4">
|
||||
Попробуйте изменить настройки фильтров
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<template>
|
||||
<div class="border-b mb-5 border-b-base-200 flex justify-between">
|
||||
<div class="mb-2 text-base-content/40">Товары для главной страницы</div>
|
||||
<div class="mb-2">Рекомендуемые товары</div>
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle"
|
||||
disabled
|
||||
:checked="filter.criteria.product_for_main_page.params.value"
|
||||
v-model="filter.criteria.product_for_main_page.params.value"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<div class="border-b mb-5 border-b-base-200">
|
||||
<div class="mb-2">Категория</div>
|
||||
<div v-if="categoriesStore.isLoading" class="skeleton h-10 w-full"></div>
|
||||
<select
|
||||
v-else
|
||||
v-model.number="props.filter.criteria.product_category_id.params.value"
|
||||
class="select w-full"
|
||||
>
|
||||
<option :value="null">Любая категория</option>
|
||||
<SelectOption
|
||||
v-for="category in categoriesStore.categories"
|
||||
:key="category.id"
|
||||
:level="0"
|
||||
:category="category"
|
||||
/>
|
||||
</select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {useCategoriesStore} from "@/stores/CategoriesStore.js";
|
||||
import {onMounted} from "vue";
|
||||
import SelectOption from "@/components/ProductFilters/Components/ProductCategory/SelectOption.vue";
|
||||
|
||||
const props = defineProps({
|
||||
filter: {
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
const categoriesStore = useCategoriesStore();
|
||||
|
||||
onMounted(() => {
|
||||
categoriesStore.fetchCategories();
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<option :value="category.id">
|
||||
{{ "-".repeat(level) }} {{ category.name }}
|
||||
</option>
|
||||
|
||||
<SelectOption
|
||||
v-if="category.children"
|
||||
v-for="child in category.children"
|
||||
:key="child.id"
|
||||
:category="child"
|
||||
:level="level + 1"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
category: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
level: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
@@ -1,105 +0,0 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div
|
||||
class="fixed top-0 left-0 z-50 w-full h-full bg-base-200 flex flex-col safe-top"
|
||||
>
|
||||
<header class="text-center shrink-0 p-3 font-bold text-xl">
|
||||
Фильтры
|
||||
</header>
|
||||
|
||||
<main class="mt-5 px-5 bg-base-200">
|
||||
<div
|
||||
v-if="draft?.rules && Object.keys(draft.rules).length > 0"
|
||||
v-for="(filter, filterId) in draft.rules"
|
||||
>
|
||||
<component
|
||||
v-if="componentMap[filterId]"
|
||||
:is="componentMap[filterId]"
|
||||
:filter="filter"
|
||||
/>
|
||||
|
||||
<p v-else>Not supported {{ filter.type }}</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="bg-base-100 rounded-2xl p-5">
|
||||
Нет фильтров
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ProductPrice from "@/components/ProductFilters/Components/ProductPrice.vue";
|
||||
import {onMounted, onUnmounted, ref} from "vue";
|
||||
import ForMainPage from "@/components/ProductFilters/Components/ForMainPage.vue";
|
||||
|
||||
const componentMap = {
|
||||
RULE_PRODUCT_PRICE: ProductPrice,
|
||||
RULE_PRODUCT_FOR_MAIN_PAGE: ForMainPage,
|
||||
};
|
||||
|
||||
const props = defineProps({
|
||||
closeOnOverlay: {type: Boolean, default: true},
|
||||
filters: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close', 'apply', 'reset']);
|
||||
|
||||
const mainButton = window.Telegram.WebApp.MainButton;
|
||||
const secondaryButton = window.Telegram.WebApp.SecondaryButton;
|
||||
const backButton = window.Telegram.WebApp.BackButton;
|
||||
const draft = ref({});
|
||||
|
||||
const applyFilters = () => {
|
||||
emit('apply', draft);
|
||||
}
|
||||
|
||||
const resetFilters = () => {
|
||||
emit('reset');
|
||||
}
|
||||
|
||||
function closeFiltersWithoutApply() {
|
||||
emit('close');
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.document.body.style.overflow = 'hidden';
|
||||
|
||||
// Crete draft of the filters.
|
||||
draft.value = JSON.parse(JSON.stringify(props.filters));
|
||||
|
||||
mainButton.setParams({
|
||||
text: 'Применить',
|
||||
is_active: true,
|
||||
is_visible: true,
|
||||
});
|
||||
mainButton.show();
|
||||
mainButton.onClick(applyFilters);
|
||||
|
||||
secondaryButton.setParams({
|
||||
text: 'Сбросить фильтры',
|
||||
is_active: true,
|
||||
is_visible: true,
|
||||
position: 'top',
|
||||
});
|
||||
secondaryButton.show();
|
||||
secondaryButton.onClick(resetFilters);
|
||||
|
||||
backButton.show();
|
||||
backButton.onClick(closeFiltersWithoutApply);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
mainButton.hide();
|
||||
secondaryButton.hide();
|
||||
mainButton.offClick(applyFilters);
|
||||
secondaryButton.offClick(resetFilters);
|
||||
backButton.hide();
|
||||
backButton.offClick(closeFiltersWithoutApply);
|
||||
window.document.body.style.overflow = '';
|
||||
});
|
||||
</script>
|
||||
@@ -1,31 +0,0 @@
|
||||
export const FILTERS_MAIN_PAGE_DEFAULT = {
|
||||
operand: "AND",
|
||||
rules: {
|
||||
RULE_PRODUCT_PRICE: {
|
||||
criteria: {
|
||||
"product_price":
|
||||
{
|
||||
"type": "number",
|
||||
"params": {
|
||||
"operator": "between",
|
||||
"value": {
|
||||
"from": "",
|
||||
"to": ""
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
RULE_PRODUCT_FOR_MAIN_PAGE: {
|
||||
"criteria": {
|
||||
"product_for_main_page": {
|
||||
"type": "boolean",
|
||||
"params": {
|
||||
"operator": "equals",
|
||||
"value": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -2,7 +2,7 @@
|
||||
<swiper-container ref="swiperEl" init="false" pagination-dynamic-bullets="true">
|
||||
<swiper-slide
|
||||
v-for="image in images"
|
||||
lazy
|
||||
:key="image.url"
|
||||
class="bg-base-100 overflow-hidden"
|
||||
style="aspect-ratio:1/1; border-radius:12px;"
|
||||
>
|
||||
@@ -18,7 +18,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {onMounted, onUnmounted, ref} from "vue";
|
||||
import {onActivated, onMounted, onUnmounted, ref} from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
images: {
|
||||
@@ -45,12 +45,43 @@ onUnmounted(() => {
|
||||
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
swiperEl.value?.addEventListener('swiperactiveindexchange', (event) => {
|
||||
window.Telegram.WebApp.HapticFeedback.selectionChanged();
|
||||
onMounted(() => {
|
||||
const el = swiperEl.value;
|
||||
if (!el) return;
|
||||
|
||||
el.addEventListener('swiperactiveindexchange', () => {
|
||||
window.Telegram?.WebApp?.HapticFeedback?.selectionChanged();
|
||||
});
|
||||
|
||||
Object.assign(swiperEl.value, params);
|
||||
swiperEl.value.initialize();
|
||||
Object.assign(el, params);
|
||||
el.initialize();
|
||||
|
||||
// 👇 важно, особенно если картинки подгружаются не сразу
|
||||
el.addEventListener('swiperinit', () => {
|
||||
el.swiper.update();
|
||||
});
|
||||
});
|
||||
|
||||
onActivated(() => {
|
||||
const el = swiperEl.value
|
||||
if (!el) return;
|
||||
|
||||
// Если swiper есть, но pagination потерялся — уничтожаем
|
||||
if (el.swiper) {
|
||||
try {
|
||||
el.swiper.destroy(true, true)
|
||||
} catch (e) {
|
||||
console.warn('Failed to destroy swiper', e)
|
||||
}
|
||||
}
|
||||
|
||||
// Переинициализация с параметрами
|
||||
Object.assign(el, params)
|
||||
el.initialize()
|
||||
|
||||
// Пересчёт пагинации после инициализации
|
||||
el.addEventListener('swiperinit', () => {
|
||||
el.swiper.update()
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<div class="mx-auto max-w-2xl px-4 py-4 sm:px-6 sm:py-6 lg:max-w-7xl lg:px-8">
|
||||
<h2 class="text-lg font-bold mb-5 text-center">{{ productsStore.products.meta.currentCategoryName }}</h2>
|
||||
<div class="mx-auto max-w-2xl px-4 py-4 pb-14">
|
||||
<h2 v-if="categoryName" class="text-lg font-bold mb-5 text-center">{{ categoryName }}</h2>
|
||||
|
||||
<div v-if="productsStore.products.data.length > 0">
|
||||
<template v-if="products.length > 0">
|
||||
<div
|
||||
class="products-grid grid grid-cols-2 gap-x-5 gap-y-5 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 xl:gap-x-8"
|
||||
>
|
||||
<RouterLink
|
||||
v-for="product in productsStore.products.data"
|
||||
v-for="product in products"
|
||||
:key="product.id"
|
||||
class="product-grid-card group"
|
||||
:to="`/product/${product.id}`"
|
||||
@@ -26,21 +26,16 @@
|
||||
<div ref="bottom" style="height: 1px;"></div>
|
||||
</div>
|
||||
|
||||
<div v-if="productsStore.isLoading" class="text-center mt-5">
|
||||
<span class="loading loading-spinner loading-md"></span> Загрузка...
|
||||
<div v-if="isLoadingMore" class="text-center mt-5">
|
||||
<span class="loading loading-spinner loading-md"></span> Загрузка товаров...
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="productsStore.products.meta.hasMore === false"
|
||||
class="text-xs text-center mt-4 pt-4 mb-2 border-t"
|
||||
>
|
||||
<div v-else-if="hasMore === false" class="text-xs text-center mt-4 pt-4 mb-2 border-t">
|
||||
{{ settings.noMoreProductsMessage }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<NoProducts v-else-if="productsStore.loadFinished"/>
|
||||
|
||||
<div v-else
|
||||
<div v-else-if="isLoading === true"
|
||||
class="grid grid-cols-2 gap-x-6 gap-y-10 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 xl:gap-x-8">
|
||||
<div v-for="n in 8" :key="n" class="animate-pulse space-y-2">
|
||||
<div class="aspect-square bg-gray-200 rounded-md"></div>
|
||||
@@ -48,32 +43,71 @@
|
||||
<div class="h-4 bg-gray-200 rounded w-1/2"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<NoProducts v-else/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import NoProducts from "@/components/NoProducts.vue";
|
||||
import ProductImageSwiper from "@/components/ProductImageSwiper.vue";
|
||||
import {useProductsStore} from "@/stores/ProductsStore.js";
|
||||
import {useInfiniteScroll} from '@vueuse/core';
|
||||
import {useSettingsStore} from "@/stores/SettingsStore.js";
|
||||
import {ref} from "vue";
|
||||
import {useIntersectionObserver} from '@vueuse/core';
|
||||
|
||||
const productsStore = useProductsStore();
|
||||
const settings = useSettingsStore();
|
||||
const bottom = ref(null);
|
||||
|
||||
const emits = defineEmits(['loadMore']);
|
||||
|
||||
const props = defineProps({
|
||||
products: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
|
||||
hasMore: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
categoryName: {
|
||||
type: String,
|
||||
default: () => '',
|
||||
},
|
||||
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
isLoadingMore: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
});
|
||||
|
||||
function haptic() {
|
||||
window.Telegram.WebApp.HapticFeedback.selectionChanged();
|
||||
// productsStore.savedScrollY = window.scrollY;
|
||||
// console.log("Store scrollY: ", productsStore.savedScrollY);
|
||||
}
|
||||
|
||||
useInfiniteScroll(
|
||||
useIntersectionObserver(
|
||||
bottom,
|
||||
async () => await productsStore.loadMore(),
|
||||
{distance: 1000}
|
||||
)
|
||||
([entry]) => {
|
||||
console.debug('Check Intersection');
|
||||
if (entry?.isIntersecting === true
|
||||
&& props.hasMore === true
|
||||
&& props.isLoading === false
|
||||
&& props.isLoadingMore === false
|
||||
) {
|
||||
emits('loadMore');
|
||||
}
|
||||
},
|
||||
{
|
||||
root: null,
|
||||
rootMargin: '400px 0',
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -15,6 +15,7 @@ import { register } from 'swiper/element/bundle';
|
||||
import 'swiper/element/bundle';
|
||||
import 'swiper/css/bundle';
|
||||
import AppLoading from "@/AppLoading.vue";
|
||||
import {useProductFiltersStore} from "@/stores/ProductFiltersStore.js";
|
||||
register();
|
||||
|
||||
const pinia = createPinia();
|
||||
@@ -30,6 +31,11 @@ const appLoading = createApp(AppLoading);
|
||||
appLoading.mount('#app');
|
||||
|
||||
settings.load()
|
||||
.then(async () => {
|
||||
console.debug('Load default filters for the main page');
|
||||
const filtersStore = useProductFiltersStore();
|
||||
filtersStore.applied = await filtersStore.fetchFiltersForMainPage();
|
||||
})
|
||||
.then(() => {
|
||||
if (settings.app_enabled === false) {
|
||||
throw new Error('App disabled (maintenance mode)');
|
||||
|
||||
@@ -7,6 +7,7 @@ import Products from "@/views/Products.vue";
|
||||
import Checkout from "@/views/Checkout.vue";
|
||||
import OrderCreated from "@/views/OrderCreated.vue";
|
||||
import Search from "@/views/Search.vue";
|
||||
import Filters from "@/views/Filters.vue";
|
||||
|
||||
const routes = [
|
||||
{
|
||||
@@ -14,6 +15,7 @@ const routes = [
|
||||
name: 'home',
|
||||
component: Home,
|
||||
},
|
||||
{path: '/filters', name: 'filters', component: Filters},
|
||||
{path: '/product/:id', name: 'product.show', component: Product},
|
||||
{
|
||||
path: '/products/:category_id',
|
||||
|
||||
@@ -1,19 +1,33 @@
|
||||
import {defineStore} from "pinia";
|
||||
|
||||
import {getFiltersForMainPage} from "@/utils/ftch.js";
|
||||
import {md5} from "js-md5";
|
||||
import {toRaw} from "vue";
|
||||
|
||||
export const useProductFiltersStore = defineStore('product_filters', {
|
||||
state: () => ({
|
||||
filters: {},
|
||||
isLoading: false,
|
||||
draft: {},
|
||||
applied: {},
|
||||
fullPath: '',
|
||||
}),
|
||||
|
||||
getters: {
|
||||
hasFilters: (state) => state.filters?.rules && Object.keys(state.filters.rules).length > 0,
|
||||
paramsHashForRouter: (state) => md5(JSON.stringify({ filters: state.applied })),
|
||||
},
|
||||
|
||||
actions: {
|
||||
reset() {
|
||||
async fetchFiltersForMainPage() {
|
||||
if (this.isLoading) return;
|
||||
|
||||
try {
|
||||
this.isLoading = true;
|
||||
const response = await getFiltersForMainPage();
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
clear() {
|
||||
|
||||
@@ -16,6 +16,7 @@ export const useProductsStore = defineStore('products', {
|
||||
search: '',
|
||||
page: 1,
|
||||
isLoading: false,
|
||||
isLoadingMore: false,
|
||||
loadFinished: false,
|
||||
savedScrollY: 0,
|
||||
currentLoadedParamsHash: null,
|
||||
@@ -23,6 +24,10 @@ export const useProductsStore = defineStore('products', {
|
||||
|
||||
getters: {
|
||||
paramsHash: (state) => md5(JSON.stringify(toRaw(state.getParams()))),
|
||||
paramsHashForRouter: (state) => md5(JSON.stringify({
|
||||
search: state.search,
|
||||
filters: toRaw(state.filters),
|
||||
})),
|
||||
},
|
||||
|
||||
actions: {
|
||||
@@ -36,8 +41,6 @@ export const useProductsStore = defineStore('products', {
|
||||
|
||||
async fetchProducts() {
|
||||
try {
|
||||
this.isLoading = true;
|
||||
|
||||
console.debug('Current params hash: ', this.currentLoadedParamsHash);
|
||||
if (this.products.data.length > 0 && this.paramsHash === this.currentLoadedParamsHash) {
|
||||
console.debug('Loading products from cache');
|
||||
@@ -61,7 +64,6 @@ export const useProductsStore = defineStore('products', {
|
||||
console.error("Failed to load products");
|
||||
console.error(error);
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -70,8 +72,8 @@ export const useProductsStore = defineStore('products', {
|
||||
|
||||
try {
|
||||
console.debug('Load products with filters', filters);
|
||||
console.debug('Filters for URL: ', this.filtersFullUrl);
|
||||
this.isLoading = false;
|
||||
this.reset();
|
||||
this.isLoading = true;
|
||||
this.page = 1;
|
||||
this.loadFinished = false;
|
||||
this.search = '';
|
||||
@@ -86,10 +88,10 @@ export const useProductsStore = defineStore('products', {
|
||||
},
|
||||
|
||||
async loadMore() {
|
||||
if (this.isLoading || this.products.meta.hasMore === false) return;
|
||||
if (this.isLoading || this.isLoadingMore || this.products.meta.hasMore === false) return;
|
||||
|
||||
try {
|
||||
this.isLoading = true;
|
||||
this.isLoadingMore = true;
|
||||
this.page++;
|
||||
console.debug('Load more products for page: ', this.page);
|
||||
const response = await this.fetchProducts();
|
||||
@@ -98,8 +100,9 @@ export const useProductsStore = defineStore('products', {
|
||||
} catch (e) {
|
||||
console.error('Ошибка загрузки', e);
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
this.isLoadingMore = false;
|
||||
this.loadFinished = true;
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import {defineStore} from "pinia";
|
||||
import ftch from "@/utils/ftch.js";
|
||||
|
||||
export const useSearchStore = defineStore('search', {
|
||||
|
||||
state: () => ({
|
||||
search: '',
|
||||
page: 1,
|
||||
|
||||
@@ -68,4 +68,8 @@ export async function fetchSettings() {
|
||||
return await ftch('settings');
|
||||
}
|
||||
|
||||
export async function getFiltersForMainPage() {
|
||||
return await ftch('filtersForMainPage');
|
||||
}
|
||||
|
||||
export default ftch;
|
||||
|
||||
108
spa/src/views/Filters.vue
Normal file
108
spa/src/views/Filters.vue
Normal file
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<div ref="goodsRef" class="pb-10">
|
||||
<div class="flex flex-col">
|
||||
<header class="text-center shrink-0 p-3 font-bold text-xl">
|
||||
Фильтры
|
||||
</header>
|
||||
|
||||
<main class="mt-5 px-5 bg-base-200">
|
||||
<div
|
||||
v-if="filtersStore.draft?.rules && Object.keys(filtersStore.draft.rules).length > 0"
|
||||
v-for="(filter, filterId) in filtersStore.draft.rules"
|
||||
>
|
||||
<component
|
||||
v-if="componentMap[filterId]"
|
||||
:is="componentMap[filterId]"
|
||||
:filter="filter"
|
||||
/>
|
||||
|
||||
<p v-else>Not supported: {{ filterId }}</p>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
Нет фильтров
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {nextTick, onMounted, onUnmounted} from "vue";
|
||||
import {useProductFiltersStore} from "@/stores/ProductFiltersStore.js";
|
||||
import ProductPrice from "@/components/ProductFilters/Components/ProductPrice.vue";
|
||||
import ForMainPage from "@/components/ProductFilters/Components/ForMainPage.vue";
|
||||
import {useRouter} from "vue-router";
|
||||
import ProductCategory from "@/components/ProductFilters/Components/ProductCategory/ProductCategory.vue";
|
||||
|
||||
defineOptions({
|
||||
name: 'Filters'
|
||||
});
|
||||
|
||||
const componentMap = {
|
||||
RULE_PRODUCT_PRICE: ProductPrice,
|
||||
RULE_PRODUCT_FOR_MAIN_PAGE: ForMainPage,
|
||||
RULE_PRODUCT_CATEGORY: ProductCategory,
|
||||
};
|
||||
|
||||
const router = useRouter();
|
||||
const emit = defineEmits(['close', 'apply']);
|
||||
|
||||
const filtersStore = useProductFiltersStore();
|
||||
|
||||
const mainButton = window.Telegram.WebApp.MainButton;
|
||||
const secondaryButton = window.Telegram.WebApp.SecondaryButton;
|
||||
const haptic = window.Telegram.WebApp.HapticFeedback;
|
||||
|
||||
const applyFilters = async () => {
|
||||
filtersStore.applied = JSON.parse(JSON.stringify(filtersStore.draft));
|
||||
console.debug('Filters: apply filters. Hash for router: ', filtersStore.paramsHashForRouter);
|
||||
haptic.impactOccurred('soft');
|
||||
await nextTick();
|
||||
router.back();
|
||||
}
|
||||
|
||||
const resetFilters = async () => {
|
||||
filtersStore.applied = await filtersStore.fetchFiltersForMainPage();
|
||||
console.debug('Filters: reset filters. Hash for router: ', filtersStore.paramsHashForRouter);
|
||||
haptic.notificationOccurred('success');
|
||||
await nextTick();
|
||||
window.scrollTo(0, 0);
|
||||
router.back();
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
console.debug('Filters: OnMounted');
|
||||
mainButton.setParams({
|
||||
text: 'Применить',
|
||||
is_active: true,
|
||||
is_visible: true,
|
||||
});
|
||||
mainButton.show();
|
||||
mainButton.onClick(applyFilters);
|
||||
|
||||
secondaryButton.setParams({
|
||||
text: 'Сбросить фильтры',
|
||||
is_active: true,
|
||||
is_visible: true,
|
||||
position: 'top',
|
||||
});
|
||||
secondaryButton.show();
|
||||
secondaryButton.onClick(resetFilters);
|
||||
|
||||
if (filtersStore.applied?.rules) {
|
||||
console.debug('Filters: Found applied filters.');
|
||||
filtersStore.draft = JSON.parse(JSON.stringify(filtersStore.applied));
|
||||
} else {
|
||||
console.debug('No filters. Load filters from server');
|
||||
filtersStore.draft = await filtersStore.fetchFiltersForMainPage();
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
mainButton.hide();
|
||||
secondaryButton.hide();
|
||||
mainButton.offClick(applyFilters);
|
||||
secondaryButton.offClick(resetFilters);
|
||||
});
|
||||
</script>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div ref="goodsRef">
|
||||
<div ref="goodsRef" class="pb-10">
|
||||
<CategoriesInline/>
|
||||
|
||||
<div class="px-5 fixed z-50 w-full opacity-90" style="bottom: var(--tg-safe-area-inset-bottom);">
|
||||
@@ -11,13 +11,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ProductsList/>
|
||||
<Filters
|
||||
v-if="isFiltersShow"
|
||||
:filters="productsStore.filters"
|
||||
@apply="applyFilters"
|
||||
@reset="resetFilters"
|
||||
@close="closeFilters"
|
||||
<ProductsList
|
||||
:products="products"
|
||||
:hasMore="hasMore"
|
||||
:isLoading="isLoading"
|
||||
:isLoadingMore="isLoadingMore"
|
||||
@loadMore="onLoadMore"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -26,59 +25,78 @@
|
||||
import ProductsList from "@/components/ProductsList.vue";
|
||||
import CategoriesInline from "../components/CategoriesInline.vue";
|
||||
import SearchInput from "@/components/SearchInput.vue";
|
||||
import Filters from "@/components/ProductFilters/Filters.vue";
|
||||
import {onMounted, onUnmounted, ref} from "vue";
|
||||
import {useProductsStore} from "@/stores/ProductsStore.js";
|
||||
import {onActivated, onMounted, ref, toRaw} from "vue";
|
||||
import IconFunnel from "@/components/Icons/IconFunnel.vue";
|
||||
import {FILTERS_MAIN_PAGE_DEFAULT} from "@/components/ProductFilters/filters.js";
|
||||
import {useRoute} from "vue-router";
|
||||
import {useRouter} from "vue-router";
|
||||
import ftch from "@/utils/ftch.js";
|
||||
import {useProductFiltersStore} from "@/stores/ProductFiltersStore.js";
|
||||
|
||||
const route = useRoute();
|
||||
const productsStore = useProductsStore();
|
||||
defineOptions({
|
||||
name: 'Home'
|
||||
});
|
||||
|
||||
const isFiltersShow = ref(false);
|
||||
const backButton = window.Telegram.WebApp.BackButton;
|
||||
const router = useRouter();
|
||||
const filtersStore = useProductFiltersStore();
|
||||
const haptic = window.Telegram.WebApp.HapticFeedback;
|
||||
|
||||
const products = ref([]);
|
||||
const hasMore = ref(false);
|
||||
const isLoading = ref(false);
|
||||
const isLoadingMore = ref(false);
|
||||
const page = ref(1);
|
||||
|
||||
function showFilters() {
|
||||
window.Telegram.WebApp.HapticFeedback.impactOccurred('soft');
|
||||
isFiltersShow.value = true;
|
||||
backButton.show();
|
||||
haptic.impactOccurred('soft');
|
||||
router.push({name: 'filters'});
|
||||
}
|
||||
|
||||
function closeFilters() {
|
||||
window.Telegram.WebApp.HapticFeedback.impactOccurred('rigid');
|
||||
isFiltersShow.value = false;
|
||||
}
|
||||
|
||||
async function applyFilters(newFilters) {
|
||||
closeFilters();
|
||||
console.log("Load products with new filters: ", newFilters);
|
||||
productsStore.page = 1;
|
||||
await productsStore.loadProducts(newFilters);
|
||||
}
|
||||
|
||||
async function resetFilters() {
|
||||
closeFilters();
|
||||
productsStore.reset();
|
||||
await productsStore.loadProducts(FILTERS_MAIN_PAGE_DEFAULT);
|
||||
}
|
||||
|
||||
function handleClickOutside(e) {
|
||||
if (!e.target.closest('input, textarea')) {
|
||||
document.activeElement?.blur()
|
||||
async function fetchProducts() {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
console.debug('Home: Load products for Main Page.');
|
||||
console.debug('Home: Fetch products from server using filters: ', toRaw(filtersStore.applied));
|
||||
const response = await ftch('products', null, toRaw({
|
||||
page: page.value,
|
||||
filters: filtersStore.applied,
|
||||
}));
|
||||
products.value = response.data;
|
||||
hasMore.value = response.meta.hasMore;
|
||||
console.debug('Home: Products for main page loaded.');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onUnmounted(() => document.removeEventListener('click', handleClickOutside));
|
||||
async function onLoadMore() {
|
||||
try {
|
||||
console.debug('Home: onLoadMore');
|
||||
if (isLoading.value === true || isLoadingMore.value === true || hasMore.value === false) return;
|
||||
isLoadingMore.value = true;
|
||||
page.value++;
|
||||
console.debug('Home: Load more for page ', page.value, ' using filters: ', toRaw(filtersStore.applied));
|
||||
const response = await ftch('products', null, toRaw({
|
||||
page: page.value,
|
||||
filters: filtersStore.applied,
|
||||
}));
|
||||
products.value.push(...response.data);
|
||||
hasMore.value = response.meta.hasMore;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
isLoadingMore.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
if (productsStore.filtersFullUrl !== route.fullPath) {
|
||||
productsStore.filtersFullUrl = route.fullPath;
|
||||
await productsStore.loadProducts(FILTERS_MAIN_PAGE_DEFAULT);
|
||||
} else {
|
||||
await productsStore.loadProducts(productsStore.filters ?? FILTERS_MAIN_PAGE_DEFAULT);
|
||||
}
|
||||
console.debug("Home: Home Mounted");
|
||||
console.debug("Home: Scroll top");
|
||||
await fetchProducts();
|
||||
window.scrollTo(0, 0);
|
||||
});
|
||||
|
||||
onActivated(async () => {
|
||||
console.debug('Home: Activated Home');
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
<template>
|
||||
<div ref="goodsRef">
|
||||
<div ref="goodsRef" class="pb-10">
|
||||
<div class="px-5 fixed z-50 w-full opacity-90" style="bottom: var(--tg-safe-area-inset-bottom);">
|
||||
<div class="bg-base-300 flex justify-between p-2 rounded-xl shadow-md">
|
||||
<SearchInput/>
|
||||
</div>
|
||||
</div>
|
||||
<ProductsList/>
|
||||
<ProductsList
|
||||
:products="productsStore.products.data"
|
||||
:hasMore="productsStore.products.meta.hasMore"
|
||||
:isLoading="productsStore.isLoading"
|
||||
:isLoadingMore="productsStore.isLoadingMore"
|
||||
@loadMore="productsStore.loadMore"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -17,12 +23,17 @@ import {useRoute} from "vue-router";
|
||||
import {useProductsStore} from "@/stores/ProductsStore.js";
|
||||
import IconFunnel from "@/components/Icons/IconFunnel.vue";
|
||||
|
||||
defineOptions({
|
||||
name: 'Products'
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
const productsStore = useProductsStore();
|
||||
|
||||
const categoryId = route.params.category_id ?? null;
|
||||
|
||||
onMounted(async () => {
|
||||
console.debug("Category Products Mounted");
|
||||
console.debug("Load products for category: ", categoryId);
|
||||
|
||||
if (productsStore.filtersFullUrl === route.fullPath) {
|
||||
|
||||
@@ -91,6 +91,5 @@ onUnmounted(() => {
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
nextTick(() => searchInput.value.focus());
|
||||
});
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user