баги с превью
This commit is contained in:
@@ -13,6 +13,9 @@ final class CatalogController
|
|||||||
{
|
{
|
||||||
$categoryRepository = new CategoryRepository();
|
$categoryRepository = new CategoryRepository();
|
||||||
$productRepository = new ProductRepository();
|
$productRepository = new ProductRepository();
|
||||||
|
$pagination = $this->paginationInput();
|
||||||
|
$totalProducts = $productRepository->catalogTotal(true);
|
||||||
|
$products = $productRepository->catalogPreview($pagination['limit'], $pagination['offset'], true);
|
||||||
|
|
||||||
view('catalog/index', [
|
view('catalog/index', [
|
||||||
'title' => 'Каталог Рыбсток - рыба, морепродукты, мясо, сыры и продукты',
|
'title' => 'Каталог Рыбсток - рыба, морепродукты, мясо, сыры и продукты',
|
||||||
@@ -24,7 +27,8 @@ final class CatalogController
|
|||||||
],
|
],
|
||||||
'categories' => $categoryRepository->tree(),
|
'categories' => $categoryRepository->tree(),
|
||||||
'currentCategory' => null,
|
'currentCategory' => null,
|
||||||
'products' => $productRepository->catalogPreview(48),
|
'products' => $products,
|
||||||
|
'pagination' => $this->paginationData('/catalog', $totalProducts, count($products), $pagination),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,6 +52,9 @@ final class CatalogController
|
|||||||
}
|
}
|
||||||
|
|
||||||
$categoryName = (string) $category['name'];
|
$categoryName = (string) $category['name'];
|
||||||
|
$pagination = $this->paginationInput();
|
||||||
|
$totalProducts = $productRepository->categoryTotal((int) $category['id'], true);
|
||||||
|
$products = $productRepository->forCategory((int) $category['id'], $pagination['limit'], $pagination['offset'], true);
|
||||||
|
|
||||||
view('catalog/index', [
|
view('catalog/index', [
|
||||||
'title' => $category['seo_title'] ?: $categoryName . ' - купить в Рыбсток с доставкой по Москве',
|
'title' => $category['seo_title'] ?: $categoryName . ' - купить в Рыбсток с доставкой по Москве',
|
||||||
@@ -56,7 +63,8 @@ final class CatalogController
|
|||||||
'breadcrumbs' => $categoryRepository->breadcrumbs((int) $category['id']),
|
'breadcrumbs' => $categoryRepository->breadcrumbs((int) $category['id']),
|
||||||
'categories' => $categoryRepository->tree(),
|
'categories' => $categoryRepository->tree(),
|
||||||
'currentCategory' => $category,
|
'currentCategory' => $category,
|
||||||
'products' => $productRepository->forCategory((int) $category['id'], 48),
|
'products' => $products,
|
||||||
|
'pagination' => $this->paginationData('/catalog/' . (string) $category['slug'], $totalProducts, count($products), $pagination),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,4 +108,58 @@ final class CatalogController
|
|||||||
'images' => $productRepository->imagesForProduct((int) $product['id']),
|
'images' => $productRepository->imagesForProduct((int) $product['id']),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{page:int, limit:int, offset:int, per_page:int, cumulative:bool}
|
||||||
|
*/
|
||||||
|
private function paginationInput(): array
|
||||||
|
{
|
||||||
|
$perPage = 48;
|
||||||
|
$cumulativeLimit = isset($_GET['limit']) ? (int) $_GET['limit'] : 0;
|
||||||
|
|
||||||
|
if ($cumulativeLimit > 0) {
|
||||||
|
$limit = max($perPage, min($cumulativeLimit, 5000));
|
||||||
|
|
||||||
|
return [
|
||||||
|
'page' => 1,
|
||||||
|
'limit' => $limit,
|
||||||
|
'offset' => 0,
|
||||||
|
'per_page' => $perPage,
|
||||||
|
'cumulative' => true,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$page = max(1, (int) ($_GET['page'] ?? 1));
|
||||||
|
|
||||||
|
return [
|
||||||
|
'page' => $page,
|
||||||
|
'limit' => $perPage,
|
||||||
|
'offset' => ($page - 1) * $perPage,
|
||||||
|
'per_page' => $perPage,
|
||||||
|
'cumulative' => false,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array{page:int, limit:int, offset:int, per_page:int, cumulative:bool} $input
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function paginationData(string $basePath, int $total, int $currentCount, array $input): array
|
||||||
|
{
|
||||||
|
$shown = min($total, $input['offset'] + $currentCount);
|
||||||
|
$nextLimit = min($total, max($shown, $input['limit']) + $input['per_page']);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'base_path' => $basePath,
|
||||||
|
'total' => $total,
|
||||||
|
'shown' => $shown,
|
||||||
|
'page' => $input['page'],
|
||||||
|
'pages' => max(1, (int) ceil($total / $input['per_page'])),
|
||||||
|
'per_page' => $input['per_page'],
|
||||||
|
'limit' => $input['limit'],
|
||||||
|
'has_more' => $shown < $total,
|
||||||
|
'next_limit' => $nextLimit,
|
||||||
|
'cumulative' => $input['cumulative'],
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,12 +27,15 @@ final class ProductRepository extends BaseRepository
|
|||||||
/**
|
/**
|
||||||
* @return array<int, array<string, mixed>>
|
* @return array<int, array<string, mixed>>
|
||||||
*/
|
*/
|
||||||
public function catalogPreview(int $limit = 24): array
|
public function catalogPreview(int $limit = 24, int $offset = 0, bool $includeHidden = false): array
|
||||||
{
|
{
|
||||||
$limit = max(1, min($limit, 60));
|
$limit = max(1, min($limit, 5000));
|
||||||
|
$offset = max(0, $offset);
|
||||||
$sql = $this->previewSelectSql(
|
$sql = $this->previewSelectSql(
|
||||||
'ORDER BY p.sort_order, p.name',
|
'ORDER BY (p.is_published = 0 OR p.is_available = 0), p.sort_order, p.name',
|
||||||
$limit
|
$limit,
|
||||||
|
$offset,
|
||||||
|
$includeHidden
|
||||||
);
|
);
|
||||||
|
|
||||||
$statement = $this->pdo()->query($sql);
|
$statement = $this->pdo()->query($sql);
|
||||||
@@ -40,12 +43,21 @@ final class ProductRepository extends BaseRepository
|
|||||||
return $this->preparePreviewProducts($statement->fetchAll());
|
return $this->preparePreviewProducts($statement->fetchAll());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function catalogTotal(bool $includeHidden = false): int
|
||||||
|
{
|
||||||
|
$statusSql = $includeHidden ? '' : ' AND p.is_published = 1 AND p.is_available = 1';
|
||||||
|
$statement = $this->pdo()->query('SELECT COUNT(*) FROM products p WHERE 1=1' . $statusSql);
|
||||||
|
|
||||||
|
return (int) $statement->fetchColumn();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<int, array<string, mixed>>
|
* @return array<int, array<string, mixed>>
|
||||||
*/
|
*/
|
||||||
public function forCategory(int $categoryId, int $limit = 48): array
|
public function forCategory(int $categoryId, int $limit = 48, int $offset = 0, bool $includeHidden = false): array
|
||||||
{
|
{
|
||||||
$limit = max(1, min($limit, 96));
|
$limit = max(1, min($limit, 5000));
|
||||||
|
$offset = max(0, $offset);
|
||||||
$categoryIds = (new CategoryRepository())->descendantIds($categoryId);
|
$categoryIds = (new CategoryRepository())->descendantIds($categoryId);
|
||||||
$categoryParents = $this->categoryParentMap();
|
$categoryParents = $this->categoryParentMap();
|
||||||
$requestedRootId = $this->rootCategoryId($categoryId, $categoryParents);
|
$requestedRootId = $this->rootCategoryId($categoryId, $categoryParents);
|
||||||
@@ -69,8 +81,10 @@ final class ProductRepository extends BaseRepository
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
ORDER BY p.sort_order, p.name',
|
ORDER BY (p.is_published = 0 OR p.is_available = 0), p.sort_order, p.name',
|
||||||
5000
|
5000,
|
||||||
|
0,
|
||||||
|
$includeHidden
|
||||||
);
|
);
|
||||||
|
|
||||||
$statement = $this->pdo()->prepare($sql);
|
$statement = $this->pdo()->prepare($sql);
|
||||||
@@ -94,13 +108,22 @@ final class ProductRepository extends BaseRepository
|
|||||||
) {
|
) {
|
||||||
$filtered[] = $product;
|
$filtered[] = $product;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (count($filtered) >= $limit) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $filtered;
|
usort($filtered, static function (array $left, array $right): int {
|
||||||
|
$leftUnavailable = ((int) ($left['is_published'] ?? 1) === 0 || (int) ($left['is_available'] ?? 1) === 0) ? 1 : 0;
|
||||||
|
$rightUnavailable = ((int) ($right['is_published'] ?? 1) === 0 || (int) ($right['is_available'] ?? 1) === 0) ? 1 : 0;
|
||||||
|
|
||||||
|
return [$leftUnavailable, (int) ($left['sort_order'] ?? 0), (string) ($left['name'] ?? '')]
|
||||||
|
<=> [$rightUnavailable, (int) ($right['sort_order'] ?? 0), (string) ($right['name'] ?? '')];
|
||||||
|
});
|
||||||
|
|
||||||
|
return array_slice($filtered, $offset, $limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function categoryTotal(int $categoryId, bool $includeHidden = false): int
|
||||||
|
{
|
||||||
|
return count($this->forCategory($categoryId, 5000, 0, $includeHidden));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -252,7 +275,7 @@ final class ProductRepository extends BaseRepository
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function previewSelectSql(string $tailSql, int $limit): string
|
private function previewSelectSql(string $tailSql, int $limit, int $offset = 0, bool $includeHidden = false): string
|
||||||
{
|
{
|
||||||
return 'SELECT
|
return 'SELECT
|
||||||
p.id,
|
p.id,
|
||||||
@@ -264,12 +287,15 @@ final class ProductRepository extends BaseRepository
|
|||||||
p.seo_description,
|
p.seo_description,
|
||||||
p.seo_keywords,
|
p.seo_keywords,
|
||||||
p.category_id,
|
p.category_id,
|
||||||
|
p.is_published,
|
||||||
|
p.is_available,
|
||||||
c.name AS category_name,
|
c.name AS category_name,
|
||||||
c.slug AS category_slug,
|
c.slug AS category_slug,
|
||||||
p.main_image_path,
|
p.main_image_path,
|
||||||
p.base_unit,
|
p.base_unit,
|
||||||
p.package_display_mode,
|
p.package_display_mode,
|
||||||
p.sales_count,
|
p.sales_count,
|
||||||
|
p.sort_order,
|
||||||
v.id AS variant_id,
|
v.id AS variant_id,
|
||||||
v.name AS variant_name,
|
v.name AS variant_name,
|
||||||
v.unit,
|
v.unit,
|
||||||
@@ -291,10 +317,10 @@ final class ProductRepository extends BaseRepository
|
|||||||
ORDER BY pv.is_default DESC, pv.sort_order, pv.id
|
ORDER BY pv.is_default DESC, pv.sort_order, pv.id
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
)
|
)
|
||||||
WHERE p.is_published = 1
|
WHERE 1=1
|
||||||
AND p.is_available = 1
|
' . ($includeHidden ? '' : 'AND p.is_published = 1 AND p.is_available = 1') . '
|
||||||
' . $tailSql . '
|
' . $tailSql . '
|
||||||
LIMIT ' . $limit;
|
LIMIT ' . $limit . ' OFFSET ' . $offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+132
-3
@@ -1069,6 +1069,49 @@ a:hover {
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.product-card.is-unavailable {
|
||||||
|
background: linear-gradient(180deg, rgba(245, 246, 242, 0.94), rgba(232, 236, 227, 0.94));
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card.is-unavailable .product-card-image img,
|
||||||
|
.product-card.is-unavailable .product-card-image-placeholder {
|
||||||
|
filter: grayscale(0.9) blur(0.4px);
|
||||||
|
opacity: 0.68;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card.is-unavailable h3,
|
||||||
|
.product-card.is-unavailable .price-line,
|
||||||
|
.product-card.is-unavailable .package-price-line,
|
||||||
|
.product-card.is-unavailable .variant-choice {
|
||||||
|
opacity: 0.72;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card-unavailable-note {
|
||||||
|
display: grid;
|
||||||
|
place-items: center start;
|
||||||
|
min-height: 72px;
|
||||||
|
border-top: 1px solid #d8ded2;
|
||||||
|
margin: 6px 0 10px;
|
||||||
|
padding-top: 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #687266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-unavailable-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 12px;
|
||||||
|
left: 12px;
|
||||||
|
z-index: 2;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 6px 9px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 900;
|
||||||
|
color: #ffffff;
|
||||||
|
background: rgba(80, 89, 78, 0.9);
|
||||||
|
box-shadow: 0 8px 20px rgba(24, 34, 24, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
.product-card-image {
|
.product-card-image {
|
||||||
display: block;
|
display: block;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -1138,7 +1181,7 @@ a:hover {
|
|||||||
|
|
||||||
.price-line {
|
.price-line {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr) auto 22px;
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
min-height: 30px;
|
min-height: 30px;
|
||||||
@@ -1156,7 +1199,7 @@ a:hover {
|
|||||||
|
|
||||||
.price-unit-values {
|
.price-unit-values {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
gap: 6px;
|
gap: 5px;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -1186,7 +1229,7 @@ a:hover {
|
|||||||
|
|
||||||
.package-price-values {
|
.package-price-values {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
gap: 6px;
|
gap: 5px;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -1342,6 +1385,28 @@ a:hover {
|
|||||||
padding: 0 3px;
|
padding: 0 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notify-arrival-button {
|
||||||
|
appearance: none;
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 40px;
|
||||||
|
border: 1px solid #c7d0c0;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 900;
|
||||||
|
color: #435044;
|
||||||
|
background: #ffffff;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notify-arrival-button:hover,
|
||||||
|
.notify-arrival-button.is-sent {
|
||||||
|
color: #ffffff;
|
||||||
|
background: #5e6a5d;
|
||||||
|
}
|
||||||
|
|
||||||
.price-tier-trigger {
|
.price-tier-trigger {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
display: none;
|
display: none;
|
||||||
@@ -1402,6 +1467,7 @@ body[data-payment-mode="cash"] .price-tier-trigger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body[data-payment-mode="cash"] .price-line:has(.price-tier-trigger:hover) ~ .price-tier-popover,
|
body[data-payment-mode="cash"] .price-line:has(.price-tier-trigger:hover) ~ .price-tier-popover,
|
||||||
|
body[data-payment-mode="cash"] .package-price-line:has(.price-tier-trigger:hover) ~ .price-tier-popover,
|
||||||
body[data-payment-mode="cash"] .price-tier-popover:hover {
|
body[data-payment-mode="cash"] .price-tier-popover:hover {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
@@ -1783,6 +1849,69 @@ body[data-payment-mode="cash"] .price-tier-popover:hover {
|
|||||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.catalog-count-panel {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
color: var(--muted);
|
||||||
|
background: var(--paper);
|
||||||
|
box-shadow: var(--shadow-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-count-panel span {
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-count-panel a {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 34px;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-weight: 900;
|
||||||
|
color: #ffffff;
|
||||||
|
background: var(--brand-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-count-panel a:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
background: #102f21;
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-pagination {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 7px;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-pagination a {
|
||||||
|
display: inline-grid;
|
||||||
|
place-items: center;
|
||||||
|
min-width: 36px;
|
||||||
|
min-height: 36px;
|
||||||
|
border: 1px solid #cfd8c9;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 6px 9px;
|
||||||
|
font-weight: 900;
|
||||||
|
color: var(--brand-green);
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-pagination a:hover,
|
||||||
|
.catalog-pagination a.is-current {
|
||||||
|
color: #ffffff;
|
||||||
|
text-decoration: none;
|
||||||
|
background: var(--brand-green);
|
||||||
|
}
|
||||||
|
|
||||||
.text-page {
|
.text-page {
|
||||||
width: min(var(--page-max), 100%);
|
width: min(var(--page-max), 100%);
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|||||||
@@ -5,8 +5,19 @@ declare(strict_types=1);
|
|||||||
/** @var array<int, array<string, mixed>> $categories */
|
/** @var array<int, array<string, mixed>> $categories */
|
||||||
/** @var array<string, mixed>|null $currentCategory */
|
/** @var array<string, mixed>|null $currentCategory */
|
||||||
/** @var array<int, array<string, mixed>> $products */
|
/** @var array<int, array<string, mixed>> $products */
|
||||||
|
/** @var array<string, mixed> $pagination */
|
||||||
|
|
||||||
$heading = $currentCategory['h1'] ?? $currentCategory['name'] ?? 'Каталог продукции';
|
$heading = $currentCategory['h1'] ?? $currentCategory['name'] ?? 'Каталог продукции';
|
||||||
|
$pagination = $pagination ?? [
|
||||||
|
'base_path' => '/catalog',
|
||||||
|
'total' => count($products),
|
||||||
|
'shown' => count($products),
|
||||||
|
'page' => 1,
|
||||||
|
'pages' => 1,
|
||||||
|
'per_page' => 48,
|
||||||
|
'has_more' => false,
|
||||||
|
'next_limit' => count($products),
|
||||||
|
];
|
||||||
?>
|
?>
|
||||||
<section class="catalog-layout">
|
<section class="catalog-layout">
|
||||||
<aside class="catalog-sidebar">
|
<aside class="catalog-sidebar">
|
||||||
@@ -37,11 +48,31 @@ $heading = $currentCategory['h1'] ?? $currentCategory['name'] ?? 'Каталог
|
|||||||
<p class="muted">В этой категории товары появятся после переноса каталога со старого сайта.</p>
|
<p class="muted">В этой категории товары появятся после переноса каталога со старого сайта.</p>
|
||||||
</div>
|
</div>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
|
<div class="catalog-count-panel">
|
||||||
|
<span>Показано <?= e((string) $pagination['shown']) ?> из <?= e((string) $pagination['total']) ?> товаров</span>
|
||||||
|
<?php if (!empty($pagination['has_more'])): ?>
|
||||||
|
<a href="<?= e((string) $pagination['base_path']) ?>?limit=<?= e((string) $pagination['next_limit']) ?>">Показать еще</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="product-grid catalog-product-grid">
|
<div class="product-grid catalog-product-grid">
|
||||||
<?php foreach ($products as $product): ?>
|
<?php foreach ($products as $product): ?>
|
||||||
<?php require base_path('views/partials/product-card.php'); ?>
|
<?php require base_path('views/partials/product-card.php'); ?>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<?php if ((int) $pagination['pages'] > 1): ?>
|
||||||
|
<nav class="catalog-pagination" aria-label="Страницы каталога">
|
||||||
|
<?php for ($pageNumber = 1; $pageNumber <= (int) $pagination['pages']; $pageNumber++): ?>
|
||||||
|
<a
|
||||||
|
class="<?= $pageNumber === (int) $pagination['page'] && empty($pagination['cumulative']) ? 'is-current' : '' ?>"
|
||||||
|
href="<?= e((string) $pagination['base_path']) ?>?page=<?= e((string) $pageNumber) ?>"
|
||||||
|
>
|
||||||
|
<?= e((string) $pageNumber) ?>
|
||||||
|
</a>
|
||||||
|
<?php endfor; ?>
|
||||||
|
</nav>
|
||||||
|
<?php endif; ?>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -521,6 +521,17 @@ try {
|
|||||||
}, 1200);
|
}, 1200);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.addEventListener('click', (event) => {
|
||||||
|
const notifyButton = event.target.closest('[data-notify-arrival]');
|
||||||
|
|
||||||
|
if (!notifyButton) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyButton.classList.add('is-sent');
|
||||||
|
notifyButton.textContent = 'Запрос принят';
|
||||||
|
});
|
||||||
|
|
||||||
document.addEventListener('click', (event) => {
|
document.addEventListener('click', (event) => {
|
||||||
const control = event.target.closest('[data-cart-plus], [data-cart-minus], [data-cart-remove]');
|
const control = event.target.closest('[data-cart-plus], [data-cart-minus], [data-cart-remove]');
|
||||||
|
|
||||||
|
|||||||
@@ -31,27 +31,48 @@ if ($activeOldUnitPrice !== null && $activeOldUnitPrice <= $activeUnitPrice) {
|
|||||||
}
|
}
|
||||||
$activeTierBase = $activeUnitPrice ?? $activePackagePrice;
|
$activeTierBase = $activeUnitPrice ?? $activePackagePrice;
|
||||||
$activeTierUnit = unit_label((string) ($activeVariant['unit'] ?? '')) ?: 'шт.';
|
$activeTierUnit = unit_label((string) ($activeVariant['unit'] ?? '')) ?: 'шт.';
|
||||||
|
$isPublished = (int) ($product['is_published'] ?? 1) === 1;
|
||||||
|
$isAvailable = (int) ($product['is_available'] ?? 1) === 1;
|
||||||
|
$isPurchasable = $isPublished && $isAvailable && $activePackagePrice > 0;
|
||||||
?>
|
?>
|
||||||
<article
|
<article
|
||||||
class="product-card price-aware"
|
class="product-card price-aware<?= $isPurchasable ? '' : ' is-unavailable' ?>"
|
||||||
id="<?= e($cardId) ?>"
|
id="<?= e($cardId) ?>"
|
||||||
data-product-id="<?= e((string) ($product['id'] ?? '')) ?>"
|
data-product-id="<?= e((string) ($product['id'] ?? '')) ?>"
|
||||||
data-product-name="<?= e((string) ($product['name'] ?? '')) ?>"
|
data-product-name="<?= e((string) ($product['name'] ?? '')) ?>"
|
||||||
data-product-slug="<?= e((string) ($product['slug'] ?? '')) ?>"
|
data-product-slug="<?= e((string) ($product['slug'] ?? '')) ?>"
|
||||||
data-product-image="<?= e((string) ($product['main_image_path'] ?? '')) ?>"
|
data-product-image="<?= e((string) ($product['main_image_path'] ?? '')) ?>"
|
||||||
>
|
>
|
||||||
<?php if (!empty($product['main_image_path'])): ?>
|
<?php if (!$isPurchasable): ?>
|
||||||
|
<span class="product-unavailable-badge"><?= !$isPublished ? 'Ожидается поступление' : 'Нет в наличии' ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (!empty($product['main_image_path']) && $isPurchasable): ?>
|
||||||
<a class="product-card-image" href="/product/<?= e($product['slug']) ?>" aria-label="<?= e($product['name']) ?>">
|
<a class="product-card-image" href="/product/<?= e($product['slug']) ?>" aria-label="<?= e($product['name']) ?>">
|
||||||
<img src="<?= e((string) $product['main_image_path']) ?>" alt="<?= e((string) $product['name']) ?>">
|
<img src="<?= e((string) $product['main_image_path']) ?>" alt="<?= e((string) $product['name']) ?>">
|
||||||
</a>
|
</a>
|
||||||
<?php else: ?>
|
<?php elseif (!empty($product['main_image_path'])): ?>
|
||||||
|
<div class="product-card-image" aria-label="<?= e($product['name']) ?>">
|
||||||
|
<img src="<?= e((string) $product['main_image_path']) ?>" alt="<?= e((string) $product['name']) ?>">
|
||||||
|
</div>
|
||||||
|
<?php elseif ($isPurchasable): ?>
|
||||||
<a class="product-card-image product-card-image-placeholder" href="/product/<?= e($product['slug']) ?>" aria-label="<?= e($product['name']) ?>">
|
<a class="product-card-image product-card-image-placeholder" href="/product/<?= e($product['slug']) ?>" aria-label="<?= e($product['name']) ?>">
|
||||||
<span>Рыбсток</span>
|
<span>Рыбсток</span>
|
||||||
</a>
|
</a>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="product-card-image product-card-image-placeholder" aria-label="<?= e($product['name']) ?>">
|
||||||
|
<span>Рыбсток</span>
|
||||||
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<p class="product-category"><?= e($product['category_display_name'] ?? $product['category_name'] ?? 'Каталог') ?></p>
|
<p class="product-category"><?= e($product['category_display_name'] ?? $product['category_name'] ?? 'Каталог') ?></p>
|
||||||
<h3><a href="/product/<?= e($product['slug']) ?>"><?= e($product['name']) ?></a></h3>
|
<h3>
|
||||||
|
<?php if ($isPurchasable): ?>
|
||||||
|
<a href="/product/<?= e($product['slug']) ?>"><?= e($product['name']) ?></a>
|
||||||
|
<?php else: ?>
|
||||||
|
<span><?= e($product['name']) ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</h3>
|
||||||
|
|
||||||
<?php if ($variants !== []): ?>
|
<?php if ($variants !== []): ?>
|
||||||
<div class="variant-choice" aria-label="Выбор фасовки">
|
<div class="variant-choice" aria-label="Выбор фасовки">
|
||||||
@@ -97,45 +118,58 @@ $activeTierUnit = unit_label((string) ($activeVariant['unit'] ?? '')) ?: 'шт.'
|
|||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php if ($activeVariant['price_per_unit'] !== null): ?>
|
<?php if ($isPurchasable && $activeVariant['price_per_unit'] !== null): ?>
|
||||||
<span class="price-line">
|
<span class="price-line">
|
||||||
<span class="price-unit-label">Цена за <span data-unit-label><?= e(unit_label((string) $activeVariant['unit'])) ?></span>:</span>
|
<span class="price-unit-label">Цена за <span data-unit-label><?= e(unit_label((string) $activeVariant['unit'])) ?></span>:</span>
|
||||||
<span class="price-unit-values">
|
<span class="price-unit-values">
|
||||||
<span class="old-price" data-old-unit-price-display<?= $activeOldUnitPrice === null ? ' hidden' : '' ?>><?= e(money_label($activeOldUnitPrice)) ?></span>
|
<span class="old-price" data-old-unit-price-display<?= $activeOldUnitPrice === null ? ' hidden' : '' ?>><?= e(money_label($activeOldUnitPrice)) ?></span>
|
||||||
<b data-price-unit data-dynamic-unit-price data-base-unit-price="<?= e((string) $activeUnitPrice) ?>"><?= e(money_label($activeVariant['price_per_unit'])) ?></b>
|
<b data-price-unit data-dynamic-unit-price data-base-unit-price="<?= e((string) $activeUnitPrice) ?>"><?= e(money_label($activeVariant['price_per_unit'])) ?></b>
|
||||||
|
<?php if ($isPurchasable): ?>
|
||||||
|
<button class="price-tier-trigger" type="button" aria-label="Показать градацию цены">?</button>
|
||||||
|
<?php endif; ?>
|
||||||
</span>
|
</span>
|
||||||
<button class="price-tier-trigger" type="button" aria-label="Показать градацию цены">?</button>
|
|
||||||
</span>
|
</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php if ($activeVariant['package_price'] !== null): ?>
|
<?php if ($isPurchasable && $activeVariant['package_price'] !== null): ?>
|
||||||
<strong class="package-price-line">
|
<strong class="package-price-line">
|
||||||
<span class="package-price-label">За фасовку <span data-package-label-display><?= e((string) $activeVariant['name']) ?></span>:</span>
|
<span class="package-price-label">За фасовку <span data-package-label-display><?= e((string) $activeVariant['name']) ?></span>:</span>
|
||||||
<span class="package-price-values">
|
<span class="package-price-values">
|
||||||
<span class="old-price" data-old-package-price-display<?= $activeOldPackagePrice === null ? ' hidden' : '' ?>><?= e(money_label($activeOldPackagePrice)) ?></span>
|
<span class="old-price" data-old-package-price-display<?= $activeOldPackagePrice === null ? ' hidden' : '' ?>><?= e(money_label($activeOldPackagePrice)) ?></span>
|
||||||
<span data-package-price data-dynamic-package-price data-base-package-price="<?= e((string) $activePackagePrice) ?>"><?= e(money_label($activeVariant['package_price'])) ?></span>
|
<span data-package-price data-dynamic-package-price data-base-package-price="<?= e((string) $activePackagePrice) ?>"><?= e(money_label($activeVariant['package_price'])) ?></span>
|
||||||
|
<?php if ($activeVariant['price_per_unit'] === null): ?>
|
||||||
|
<button class="price-tier-trigger" type="button" aria-label="Показать градацию цены">?</button>
|
||||||
|
<?php endif; ?>
|
||||||
</span>
|
</span>
|
||||||
</strong>
|
</strong>
|
||||||
<?php if ($activeVariant['price_per_unit'] !== null): ?>
|
<?php if ($activeVariant['price_per_unit'] !== null): ?>
|
||||||
<span class="package-price-note">Цена за <span data-unit-label-note><?= e(unit_label((string) $activeVariant['unit'])) ?></span> × <span data-package-label-note><?= e((string) $activeVariant['name']) ?></span></span>
|
<span class="package-price-note">Цена за <span data-unit-label-note><?= e(unit_label((string) $activeVariant['unit'])) ?></span> × <span data-package-label-note><?= e((string) $activeVariant['name']) ?></span></span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
<?php elseif (!$isPurchasable): ?>
|
||||||
|
<div class="product-card-unavailable-note">Цену уточним после поступления товара.</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<div class="price-tier-popover" role="tooltip">
|
<?php if ($isPurchasable): ?>
|
||||||
<strong>Варианты цены за <span data-tier-unit-label><?= e($activeTierUnit) ?></span> при наличном расчете:</strong>
|
<div class="price-tier-popover" role="tooltip">
|
||||||
<dl>
|
<strong>Варианты цены за <span data-tier-unit-label><?= e($activeTierUnit) ?></span> при наличном расчете:</strong>
|
||||||
<div><dt>Базовая цена</dt><dd data-tier-base><?= e(money_label($activeTierBase)) ?></dd></div>
|
<dl>
|
||||||
<div><dt>При заказе от 20 000р.</dt><dd data-tier-cash-20><?= e(money_label(floor($activeTierBase * 0.95))) ?></dd></div>
|
<div><dt>Базовая цена</dt><dd data-tier-base><?= e(money_label($activeTierBase)) ?></dd></div>
|
||||||
<div><dt>При заказе от 50 000р.</dt><dd data-tier-cash-50><?= e(money_label(floor($activeTierBase * 0.93))) ?></dd></div>
|
<div><dt>При заказе от 20 000р.</dt><dd data-tier-cash-20><?= e(money_label(floor($activeTierBase * 0.95))) ?></dd></div>
|
||||||
</dl>
|
<div><dt>При заказе от 50 000р.</dt><dd data-tier-cash-50><?= e(money_label(floor($activeTierBase * 0.93))) ?></dd></div>
|
||||||
</div>
|
</dl>
|
||||||
|
|
||||||
<div class="product-card-actions">
|
|
||||||
<div class="quantity-stepper" data-quantity-control aria-label="Количество фасовок">
|
|
||||||
<button type="button" data-quantity-minus aria-label="Уменьшить количество">-</button>
|
|
||||||
<input data-quantity-input type="text" inputmode="numeric" value="1" aria-label="Количество">
|
|
||||||
<button type="button" data-quantity-plus aria-label="Увеличить количество">+</button>
|
|
||||||
</div>
|
</div>
|
||||||
<button class="add-to-cart-button" type="button">В корзину</button>
|
|
||||||
</div>
|
<div class="product-card-actions">
|
||||||
|
<div class="quantity-stepper" data-quantity-control aria-label="Количество фасовок">
|
||||||
|
<button type="button" data-quantity-minus aria-label="Уменьшить количество">-</button>
|
||||||
|
<input data-quantity-input type="text" inputmode="numeric" value="1" aria-label="Количество">
|
||||||
|
<button type="button" data-quantity-plus aria-label="Увеличить количество">+</button>
|
||||||
|
</div>
|
||||||
|
<button class="add-to-cart-button" type="button">В корзину</button>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="product-card-actions">
|
||||||
|
<button class="notify-arrival-button" type="button" data-notify-arrival>Сообщить о поступлении</button>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
Reference in New Issue
Block a user