diff --git a/app/Repositories/CategoryRepository.php b/app/Repositories/CategoryRepository.php index 853193e..b7b5173 100644 --- a/app/Repositories/CategoryRepository.php +++ b/app/Repositories/CategoryRepository.php @@ -49,9 +49,51 @@ final class CategoryRepository extends BaseRepository $childrenByParent[$parentKey][] = $item; } + $childrenByParent[0] = $this->sortRootCategories($childrenByParent[0] ?? []); + return $this->buildTree($childrenByParent, 0); } + /** + * @param array> $categories + * @return array> + */ + private function sortRootCategories(array $categories): array + { + usort($categories, function (array $left, array $right): int { + return [ + $this->rootCategoryWeight((string) ($left['name'] ?? '')), + (int) ($left['sort_order'] ?? 100), + (string) ($left['name'] ?? ''), + ] <=> [ + $this->rootCategoryWeight((string) ($right['name'] ?? '')), + (int) ($right['sort_order'] ?? 100), + (string) ($right['name'] ?? ''), + ]; + }); + + return $categories; + } + + private function rootCategoryWeight(string $name): int + { + $lowerName = trim(mb_strtolower($name)); + + return match (true) { + str_contains($lowerName, 'рыб') => 10, + str_contains($lowerName, 'морепроду') => 20, + str_contains($lowerName, 'икра') => 30, + $lowerName === 'мясо' => 40, + str_contains($lowerName, 'сыр') => 50, + str_contains($lowerName, 'полуфаб') => 60, + str_contains($lowerName, 'фрукт') || str_contains($lowerName, 'овощ') || str_contains($lowerName, 'ягод') || str_contains($lowerName, 'гриб') => 70, + str_contains($lowerName, 'бакале') => 80, + str_contains($lowerName, 'мясная гастрономия') => 90, + str_contains($lowerName, 'молоч') => 100, + default => 1000, + }; + } + /** * @param array>> $childrenByParent * @return array> diff --git a/app/helpers.php b/app/helpers.php index 4a92a98..2d35e94 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -44,6 +44,61 @@ function unit_label(?string $unit): string }; } +function text_lower(string $value): string +{ + if (function_exists('mb_strtolower')) { + return mb_strtolower($value, 'UTF-8'); + } + + return strtolower(strtr($value, [ + 'А' => 'а', 'Б' => 'б', 'В' => 'в', 'Г' => 'г', 'Д' => 'д', + 'Е' => 'е', 'Ё' => 'ё', 'Ж' => 'ж', 'З' => 'з', 'И' => 'и', + 'Й' => 'й', 'К' => 'к', 'Л' => 'л', 'М' => 'м', 'Н' => 'н', + 'О' => 'о', 'П' => 'п', 'Р' => 'р', 'С' => 'с', 'Т' => 'т', + 'У' => 'у', 'Ф' => 'ф', 'Х' => 'х', 'Ц' => 'ц', 'Ч' => 'ч', + 'Ш' => 'ш', 'Щ' => 'щ', 'Ъ' => 'ъ', 'Ы' => 'ы', 'Ь' => 'ь', + 'Э' => 'э', 'Ю' => 'ю', 'Я' => 'я', + ])); +} + +/** + * Price per kg helps with boxes and weight products, but confuses customers on + * caviar, cans, sauces, spices and small consumer packs. In those cases the + * storefront should sell by package and avoid derived kg prices. + * + * @param array $product + * @param array $variant + */ +function product_should_show_unit_price(array $product, array $variant): bool +{ + $unit = (string) ($variant['unit'] ?? $product['base_unit'] ?? ''); + $unitLabel = unit_label($unit); + + if (!in_array($unit, ['kg', 'кг'], true) && $unitLabel !== 'кг') { + return false; + } + + if (($variant['price_per_unit'] ?? null) === null || (float) $variant['price_per_unit'] <= 0) { + return false; + } + + $packageQuantity = (float) ($variant['package_quantity'] ?? 0); + if ($packageQuantity <= 0) { + return false; + } + + if ($packageQuantity < 1) { + return false; + } + + $variantName = text_lower((string) ($variant['name'] ?? '')); + if (preg_match('/(^|[\s,.;])\d+(?:[.,]\d+)?\s*(мл|шт)\.?($|[\s,.;])/u', $variantName) === 1) { + return false; + } + + return true; +} + function money_label(mixed $value): string { if ($value === null || $value === '') { diff --git a/public/assets/css/app.css b/public/assets/css/app.css index 85df6f0..dfc4dca 100644 --- a/public/assets/css/app.css +++ b/public/assets/css/app.css @@ -260,12 +260,13 @@ body { display: inline-flex; align-items: center; justify-content: center; + gap: 7px; width: auto; - min-width: 96px; - max-width: 190px; - min-height: 38px; + min-width: 104px; + max-width: 220px; + min-height: 42px; border-radius: 999px; - padding: 8px 13px; + padding: 8px 14px 8px 10px; font-size: 13px; line-height: 1.15; font-weight: 850; @@ -274,6 +275,10 @@ body { white-space: normal; } +.mega-root > span:last-child { + min-width: 0; +} + .mega-root:hover, .mega-item:focus-within .mega-root, .mega-item:hover .mega-root { @@ -434,21 +439,23 @@ body { .price-mode-bar { border-bottom: 1px solid var(--line); - background: rgba(244, 246, 239, 0.9); + background: + linear-gradient(90deg, rgba(25, 91, 63, 0.08), rgba(207, 56, 45, 0.05)), + rgba(244, 246, 239, 0.96); } .price-mode-inner { - width: min(var(--page-max), calc(100% - (var(--page-gap) * 2))); - display: flex; - flex-wrap: wrap; - gap: 10px 14px; + width: min(980px, calc(100% - (var(--page-gap) * 2))); + display: grid; + grid-template-columns: auto minmax(0, 1fr); + gap: 8px 16px; align-items: center; margin: 0 auto; - padding: 10px 0; + padding: 12px 0; } .price-mode-inner > span { - font-size: 13px; + font-size: 14px; font-weight: 900; color: var(--brand-green); } @@ -456,15 +463,20 @@ body { .price-mode-options { display: flex; flex-wrap: wrap; - gap: 6px; + gap: 8px; + justify-content: center; } .price-mode-button { appearance: none; - min-height: 32px; + display: inline-flex; + gap: 8px; + align-items: center; + justify-content: center; + min-height: 36px; border: 1px solid #cfd8c9; border-radius: 999px; - padding: 7px 12px; + padding: 8px 14px; font: inherit; font-size: 13px; font-weight: 850; @@ -473,6 +485,24 @@ body { cursor: pointer; } +.price-mode-button b { + display: inline-flex; + align-items: center; + min-height: 22px; + border-radius: 999px; + padding: 3px 8px; + font-size: 11px; + line-height: 1; + color: #ffffff; + background: var(--brand-red); + white-space: nowrap; +} + +.price-mode-button-cash { + border-color: rgba(25, 91, 63, 0.35); + box-shadow: 0 8px 26px rgba(25, 91, 63, 0.10); +} + .price-mode-button:hover, .price-mode-button.is-active { border-color: var(--brand-green); @@ -480,8 +510,9 @@ body { background: var(--brand-green); } -.price-mode-inner small { - color: var(--muted); +.price-mode-button.is-active b { + color: var(--brand-red); + background: #ffffff; } .page { @@ -949,7 +980,7 @@ a:hover { .category-list { display: grid; - gap: 7px; + gap: 2px; margin: 0; padding: 0; list-style: none; @@ -957,56 +988,189 @@ a:hover { .category-list .category-list { display: grid; - gap: 6px; - margin: 8px 0 0; - padding: 0 0 0 14px; + gap: 1px; + margin: 3px 0 6px; + padding: 0; list-style: none; } .category-list li { - font-size: 15px; - font-weight: 700; + min-width: 0; + font-size: 14px; + font-weight: 760; +} + +.category-list a, +.category-details summary { + box-sizing: border-box; + display: grid; + grid-template-columns: minmax(0, 1fr) 18px; + gap: 8px; + align-items: center; + width: 100%; + min-width: 0; + min-height: 36px; + border-radius: 8px; + padding: 7px 10px; + color: var(--ink); } .category-list a { - overflow-wrap: anywhere; + overflow: hidden; + text-decoration: none; } -.category-list li.is-current > a, -.category-list a.is-current-link { - display: inline-block; - border-radius: 6px; - padding: 3px 6px; - color: #ffffff; - background: var(--brand-green); +.category-list.depth-0 > li > a, +.category-list.depth-0 > li > .category-details > summary { + grid-template-columns: 30px minmax(0, 1fr) 18px; + padding-left: 8px; +} + +.category-list.depth-0 > li > a .category-icon, +.category-list.depth-0 > li > .category-details > summary .category-icon { + grid-column: 1; +} + +.category-list.depth-0 > li > a span:last-child, +.category-list.depth-0 > li > .category-details > summary a { + grid-column: 2; +} + +.category-list a span:last-child, +.category-details summary a { + overflow: hidden; + min-width: 0; + text-overflow: ellipsis; + white-space: nowrap; +} + +.category-list.depth-0 > li { + border-bottom: 1px solid #edf1e9; + padding-bottom: 4px; +} + +.category-list.depth-0 > li:last-child { + border-bottom: 0; } .category-list.depth-1 li { font-size: 13px; - font-weight: 600; + font-weight: 680; } -.category-list.depth-2 { - padding-left: 8px; +.category-list.depth-1 > li > a, +.category-list.depth-1 > li > .category-details > summary { + padding-left: 46px; } .category-list.depth-2 li { font-size: 12px; - font-weight: 400; + font-weight: 560; +} + +.category-list.depth-2 > li > a, +.category-list.depth-2 > li > .category-details > summary { + padding-left: 58px; +} + +.category-list.depth-3 > li > a, +.category-list.depth-3 > li > .category-details > summary { + padding-left: 70px; +} + +.category-list li.is-current > a, +.category-list li.is-current > .category-details > summary { + color: #ffffff; + background: var(--brand-green); +} + +.category-list a.is-current-link { + color: inherit; + background: transparent; +} + +.category-list li.is-current > a .category-icon, +.category-list li.is-current > .category-details > summary .category-icon { + color: #ffffff; + background: rgba(255, 255, 255, 0.14); +} + +.category-list a:hover, +.category-details summary:hover { + color: var(--brand-green); + background: #eef4ea; +} + +.category-list li.is-current > a:hover, +.category-list li.is-current > .category-details > summary:hover { + color: #ffffff; + background: var(--brand-green); } .category-details summary { - display: flex; - gap: 8px; - align-items: center; - min-height: 28px; - border-radius: 7px; - padding: 2px 4px; + position: relative; cursor: pointer; + list-style: none; } -.category-details summary:hover { - background: #eef4ea; +.category-details summary::-webkit-details-marker { + display: none; +} + +.category-details summary::after { + grid-column: 2; + justify-self: center; + color: #8a9587; + content: ">"; + transition: transform 0.16s ease; +} + +.category-list.depth-0 > li > .category-details > summary::after { + grid-column: 3; +} + +.category-details[open] > summary::after { + transform: rotate(90deg); +} + +.category-icon { + display: inline-grid; + place-items: center; + width: 28px; + height: 28px; + border-radius: 8px; + line-height: 1; + color: var(--brand-green); + background: #edf4ea; +} + +.category-icon svg { + width: 19px; + height: 19px; + fill: none; + stroke: currentColor; + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 1.9; +} + +.category-icon.is-caviar svg circle, +.category-icon.is-meat svg circle, +.category-icon.is-cheese svg circle { + fill: currentColor; + stroke-width: 0; +} + +.mega-category-icon { + flex: 0 0 26px; + width: 26px; + height: 26px; + border-radius: 9px; +} + +.mega-category-icon svg { + width: 17px; + height: 17px; } .category-details summary a { @@ -1190,6 +1354,12 @@ a:hover { color: #2d372f; } +.price-line.is-price-placeholder, +.package-price-note.is-price-placeholder { + visibility: hidden; + pointer-events: none; +} + .price-unit-label { overflow: hidden; min-width: 0; @@ -1239,6 +1409,7 @@ a:hover { .package-price-note { display: block; + min-height: 15px; margin: 3px 0 8px; font-size: 12px; line-height: 1.25; @@ -1606,10 +1777,18 @@ body[data-payment-mode="cash"] .price-tier-popover:hover { } .variant-row b { + display: grid; + gap: 3px; color: var(--brand-green); text-align: right; } +.variant-row b small { + font-size: 12px; + font-weight: 700; + color: var(--muted); +} + .variant-row .add-to-cart-button { margin-top: 0; } @@ -1825,6 +2004,102 @@ body[data-payment-mode="cash"] .price-tier-popover:hover { min-width: 0; } +.catalog-sidebar { + position: sticky; + top: 96px; + align-self: start; + max-height: calc(100vh - 108px); +} + +.catalog-side-stack { + display: grid; + gap: 12px; + max-height: inherit; +} + +.catalog-menu-panel { + display: grid; + gap: 12px; + min-height: 0; + padding: 14px; + overflow: hidden; +} + +.catalog-menu-panel > .category-list { + max-height: calc(100vh - 290px); + padding-right: 4px; + overflow: auto; + scrollbar-width: thin; +} + +.catalog-side-heading { + display: flex; + gap: 8px; + align-items: baseline; + justify-content: space-between; + border-bottom: 1px solid var(--line); + padding-bottom: 10px; +} + +.catalog-side-heading span { + font-size: 18px; + font-weight: 900; + color: var(--brand-green); +} + +.catalog-side-heading small { + color: var(--muted); + font-size: 12px; + font-weight: 800; +} + +.catalog-mini-cart { + display: grid; + gap: 8px; + padding: 14px; +} + +.catalog-mini-cart div { + display: flex; + gap: 8px; + align-items: baseline; + justify-content: space-between; +} + +.catalog-mini-cart span { + color: var(--muted); + font-size: 13px; + font-weight: 800; +} + +.catalog-mini-cart strong { + color: var(--brand-green); + font-size: 18px; +} + +.catalog-mini-cart p { + margin: 0; + color: var(--muted); + font-size: 13px; +} + +.catalog-mini-cart a { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 36px; + border-radius: 8px; + padding: 8px 12px; + font-weight: 900; + color: #ffffff; + background: var(--brand-green); +} + +.catalog-mini-cart a:hover { + text-decoration: none; + background: #102f21; +} + .sticky-panel { position: sticky; top: 98px; @@ -1842,7 +2117,216 @@ body[data-payment-mode="cash"] .price-tier-popover:hover { } .catalog-heading h1 { + margin-bottom: 0; +} + +.payment-benefit-banner { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(260px, 0.38fr); + gap: 22px; + align-items: center; + overflow: hidden; + border: 1px solid rgba(25, 91, 63, 0.16); + border-radius: 8px; + margin-bottom: 18px; + padding: 24px; + background: + radial-gradient(circle at 94% 16%, rgba(207, 56, 45, 0.12), transparent 26%), + linear-gradient(135deg, #fffdf8 0%, #eef5ea 100%); + box-shadow: var(--shadow-soft); +} + +.qr-benefit-banner, +.invoice-benefit-banner { + display: none; +} + +body[data-payment-mode="qr"] .cash-benefit-banner, +body[data-payment-mode="invoice"] .cash-benefit-banner { + display: none; +} + +body[data-payment-mode="qr"] .qr-benefit-banner, +body[data-payment-mode="invoice"] .invoice-benefit-banner { + display: grid; +} + +.qr-benefit-banner { + background: + radial-gradient(circle at 92% 18%, rgba(25, 91, 63, 0.14), transparent 28%), + linear-gradient(135deg, #fffdf8 0%, #eef6f4 100%); +} + +.invoice-benefit-banner { + background: + radial-gradient(circle at 92% 18%, rgba(23, 61, 44, 0.12), transparent 28%), + linear-gradient(135deg, #fffdf8 0%, #f4f1ea 100%); +} + +.cash-benefit-copy h2 { + max-width: 720px; + margin: 0 0 10px; + color: var(--brand-green); + font-size: clamp(24px, 3vw, 38px); +} + +.cash-benefit-copy p:not(.eyebrow) { + max-width: 660px; + margin: 0; + color: var(--muted); + font-size: 16px; +} + +.cash-receipt { + position: relative; + display: grid; + gap: 14px; + border: 1px solid #e2dfd2; + border-radius: 8px; + padding: 18px; + background: #ffffff; + box-shadow: 0 18px 44px rgba(22, 60, 43, 0.13); +} + +.cash-receipt::before, +.cash-receipt::after { + position: absolute; + right: 18px; + left: 18px; + height: 1px; + background-image: linear-gradient(90deg, #d7ded2 50%, transparent 0); + background-size: 10px 1px; + content: ""; +} + +.cash-receipt::before { + top: 54px; +} + +.cash-receipt::after { + bottom: 44px; +} + +.cash-receipt-top { + display: flex; + gap: 10px; + align-items: baseline; + justify-content: space-between; + padding-bottom: 10px; +} + +.cash-receipt-top span, +.cash-receipt small { + color: var(--muted); + font-size: 12px; + font-weight: 800; +} + +.cash-receipt-top strong { + color: var(--brand-green); + font-size: 18px; +} + +.cash-receipt dl { + display: grid; + gap: 9px; + margin: 0; + padding: 8px 0 10px; +} + +.cash-receipt dl div { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 12px; + align-items: baseline; +} + +.cash-receipt dt { + color: var(--ink); + font-weight: 850; +} + +.cash-receipt dd { + margin: 0; + border-radius: 999px; + padding: 4px 10px; + color: #ffffff; + font-size: 16px; + font-weight: 950; + background: var(--brand-red); +} + +.cash-receipt small { + justify-self: center; + padding-top: 8px; + text-align: center; +} + +.payment-benefit-card { + display: grid; + place-items: center; + gap: 8px; + min-height: 190px; + border: 1px solid #e2dfd2; + border-radius: 8px; + padding: 20px; + text-align: center; + background: #ffffff; + box-shadow: 0 18px 44px rgba(22, 60, 43, 0.13); +} + +.payment-benefit-card span, +.payment-benefit-card small { + color: var(--muted); + font-size: 12px; + font-weight: 850; +} + +.payment-benefit-card strong { + color: var(--brand-green); + font-size: clamp(46px, 6vw, 72px); + line-height: 0.95; +} + +.invoice-benefit-banner .payment-benefit-card strong { + max-width: 220px; + color: var(--brand-green); + font-size: clamp(32px, 4vw, 48px); + line-height: 1.02; +} + +.payment-benefit-card p { + margin: 0; + color: var(--ink); + font-weight: 900; +} + +.catalog-seo-panel { + margin-top: 26px; + border: 1px solid var(--line); + border-radius: 8px; + padding: 20px 22px; + color: #536055; + background: rgba(255, 254, 250, 0.72); +} + +.catalog-seo-panel h2 { margin-bottom: 10px; + color: var(--brand-green); + font-size: 22px; +} + +.catalog-seo-content { + display: grid; + gap: 10px; + font-size: 14px; + line-height: 1.65; +} + +.catalog-seo-content p, +.catalog-seo-content ul, +.catalog-seo-content ol { + margin: 0; } .catalog-product-grid { @@ -2518,6 +3002,19 @@ body[data-payment-mode="cash"] .price-tier-popover:hover { max-height: none; } + .catalog-sidebar { + position: static; + max-height: none; + } + + .catalog-side-stack { + max-height: none; + } + + .catalog-menu-panel > .category-list { + max-height: 420px; + } + .info-visual { min-height: 170px; } @@ -2526,6 +3023,10 @@ body[data-payment-mode="cash"] .price-tier-popover:hover { grid-template-columns: 1fr; } + .payment-benefit-banner { + grid-template-columns: 1fr; + } + .hero-media { min-height: 240px; } @@ -2556,6 +3057,20 @@ body[data-payment-mode="cash"] .price-tier-popover:hover { gap: 12px; } + .price-mode-inner { + grid-template-columns: 1fr; + justify-items: stretch; + } + + .price-mode-inner > span { + justify-self: center; + text-align: center; + } + + .price-mode-options { + justify-content: center; + } + .brand-link { width: 100%; justify-content: flex-start; diff --git a/views/catalog/index.php b/views/catalog/index.php index 90e33c8..49ffb9a 100644 --- a/views/catalog/index.php +++ b/views/catalog/index.php @@ -8,6 +8,7 @@ declare(strict_types=1); /** @var array $pagination */ $heading = $currentCategory['h1'] ?? $currentCategory['name'] ?? 'Каталог продукции'; +$categoryDescription = trim((string) ($currentCategory['description'] ?? '')); $pagination = $pagination ?? [ 'base_path' => '/catalog', 'total' => count($products), @@ -21,14 +22,28 @@ $pagination = $pagination ?? [ ?>
@@ -36,13 +51,65 @@ $pagination = $pagination ?? [

- -

- -

Выберите категорию слева или смотрите товары сразу в каталоге.

-
+
+
+

Выгода считается автоматически

+

Оплачивайте наличными при получении и получайте лучшие условия

+

Добавляйте товары в корзину, а сайт сам пересчитает итог от суммы заказа. Ничего считать вручную не нужно.

+
+ +
+ +
+
+

Удобная оплата при получении

+

Принимаем оплату QR-кодом без комиссии даже с кредитной карты

+

После подтверждения заказа менеджер согласует получение, а при передаче товара можно оплатить QR-кодом без лишних комиссий.

+
+ +
+ +
+
+

Для организаций и крупных заказов

+

Счет на оплату выставляем день в день

+

После подтверждения заказа менеджер подготовит счет и согласует документы. Безналичный расчет доступен для заказов от 30 000р.

+
+ +
+

В этой категории товары появятся после переноса каталога со старого сайта.

@@ -74,5 +141,14 @@ $pagination = $pagination ?? [ + + +
+

+
+ +
+
+
diff --git a/views/catalog/product.php b/views/catalog/product.php index a8fe4e5..83e2f60 100644 --- a/views/catalog/product.php +++ b/views/catalog/product.php @@ -7,6 +7,31 @@ declare(strict_types=1); /** @var array> $images */ $mainImage = $product['main_image_path'] ?? ''; +$showUnitPriceForVariant = static function (array $variant) use ($product): bool { + if (function_exists('product_should_show_unit_price')) { + return product_should_show_unit_price($product, $variant); + } + + $unit = (string) ($variant['unit'] ?? $product['base_unit'] ?? ''); + if (!in_array($unit, ['kg', 'кг'], true)) { + return false; + } + + if (($variant['price_per_unit'] ?? null) === null || (float) $variant['price_per_unit'] <= 0) { + return false; + } + + $packageQuantity = (float) ($variant['package_quantity'] ?? 0); + if ($packageQuantity < 1) { + return false; + } + + $lower = static function (string $value): string { + return function_exists('mb_strtolower') ? mb_strtolower($value, 'UTF-8') : strtolower($value); + }; + + return preg_match('/(^|[\s,.;])\d+(?:[.,]\d+)?\s*(мл|шт)\.?($|[\s,.;])/u', $lower((string) ($variant['name'] ?? ''))) !== 1; +}; ?>

Фасовки

+
- + / + За фасовку diff --git a/views/layouts/main.php b/views/layouts/main.php index 3d339b4..8d99ba0 100644 --- a/views/layouts/main.php +++ b/views/layouts/main.php @@ -71,15 +71,17 @@ try { -
+
- Показывать цены: + Выберите систему оплаты при получении:
- +
- Скидка наличными применится автоматически от суммы корзины.
@@ -245,22 +247,33 @@ try { const total = getCartTotal(); if (payment === 'invoice') { - return { multiplier: 1.08, label: 'Оплата по расчетному счету: +8%' }; + return { multiplier: 1.08, fixedDiscount: 0, label: 'Оплата по расчетному счету: +8%' }; } if (payment === 'qr') { - return { multiplier: 1, label: 'QR-код: базовая цена' }; + return { multiplier: 1, fixedDiscount: 0, label: 'QR-код: базовая цена' }; } if (total >= 50000) { - return { multiplier: 0.93, label: 'Наличные: цена от 50 000р.' }; + return { multiplier: 0.93, fixedDiscount: 0, label: 'Наличные: цена от 50 000р.' }; } if (total >= 20000) { - return { multiplier: 0.95, label: 'Наличные: цена от 20 000р.' }; + return { multiplier: 0.95, fixedDiscount: 0, label: 'Наличные: цена от 20 000р.' }; } - return { multiplier: 1, label: 'Наличные: базовая цена до 20 000р.' }; + if (total >= 7000) { + return { multiplier: 1, fixedDiscount: 300, label: 'Наличные: -300р. от 7000р.' }; + } + + return { multiplier: 1, fixedDiscount: 0, label: 'Наличные: базовая цена до 7000р.' }; + }; + + const cartCurrentTotal = (items = getCartItems()) => { + const mode = priceMode(); + const subtotal = Math.floor(cartBaseTotal(items) * mode.multiplier); + + return Math.max(0, subtotal - Number(mode.fixedDiscount || 0)); }; const tierValues = (base) => ({ @@ -293,7 +306,9 @@ try { if (unitNode || packageNode) { const unitBase = Number(unitNode?.dataset.baseUnitPrice || 0); const packageBase = Number(packageNode?.dataset.basePackagePrice || 0); - const base = unitBase > 0 ? unitBase : packageBase; + const unitPriceRow = card.querySelector('[data-unit-price-row]'); + const showUnitPrice = card.dataset.showUnitPrice === '1' && !unitPriceRow?.classList.contains('is-price-placeholder'); + const base = showUnitPrice && unitBase > 0 ? unitBase : packageBase; const tiers = tierValues(base); const tierBase = card.querySelector('[data-tier-base]'); @@ -304,7 +319,9 @@ try { if (tierBase) tierBase.textContent = formatRub(tiers.base); if (tierCash20) tierCash20.textContent = formatRub(tiers.cash20); if (tierCash50) tierCash50.textContent = formatRub(tiers.cash50); - if (tierUnitLabel) tierUnitLabel.textContent = card.querySelector('[data-unit-label]')?.textContent || 'шт.'; + if (tierUnitLabel) tierUnitLabel.textContent = showUnitPrice + ? (card.querySelector('[data-unit-label]')?.textContent || 'кг') + : 'фасовку'; } }; @@ -318,6 +335,20 @@ try { }); }; + const updateMiniCart = () => { + const items = getCartItems(); + const count = items.reduce((sum, item) => sum + Number(item.quantity || 0), 0); + const total = cartCurrentTotal(items); + + document.querySelectorAll('[data-mini-cart-count]').forEach((node) => { + node.textContent = String(count); + }); + + document.querySelectorAll('[data-mini-cart-total]').forEach((node) => { + node.textContent = formatRub(total); + }); + }; + const itemCurrentPrice = (item) => Math.floor(Number(item.basePackagePrice || 0) * priceMode().multiplier); const renderCartPage = () => { @@ -336,7 +367,7 @@ try { const savingNode = cartPage.querySelector('[data-cart-saving-total]'); const items = getCartItems(); const baseTotal = cartBaseTotal(items); - const currentTotal = Math.floor(baseTotal * priceMode().multiplier); + const currentTotal = cartCurrentTotal(items); const savingTotal = Math.max(0, baseTotal - currentTotal); if (emptyNode) { @@ -397,20 +428,12 @@ try { button.classList.toggle('is-active', button.dataset.paymentMode === payment); }); - const note = document.querySelector('[data-price-mode-note]'); - if (note) { - const total = getCartTotal(); - const mode = priceMode(); - const suffix = payment === 'cash' && total < 50000 - ? (total < 20000 ? ' До цены от 20 000р. осталось ' + formatRub(20000 - total) + '.' : ' До цены от 50 000р. осталось ' + formatRub(50000 - total) + '.') - : ''; - note.textContent = mode.label + '. Сумма корзины: ' + formatRub(total) + '.' + suffix; - } }; const updateAllPrices = () => { updateModeButtons(); updateCartCount(); + updateMiniCart(); updateDynamicPrices(document); document.querySelectorAll('.product-card.price-aware').forEach(updateCardPrices); renderCartPage(); @@ -427,9 +450,46 @@ try { }, currentMode: priceMode, cartBaseTotal, + cartCurrentTotal, update: updateAllPrices }; + const catalogScrollKey = 'rybstock.catalogScrollY'; + const catalogMenuScrollKey = 'rybstock.catalogMenuScrollY'; + + if (window.location.pathname.startsWith('/catalog')) { + const savedCatalogScroll = sessionStorage.getItem(catalogScrollKey); + const savedCatalogMenuScroll = sessionStorage.getItem(catalogMenuScrollKey); + const catalogMenu = document.querySelector('.catalog-menu-panel > .category-list'); + + if (savedCatalogScroll !== null) { + sessionStorage.removeItem(catalogScrollKey); + window.requestAnimationFrame(() => { + window.scrollTo({ + top: Math.max(0, parseInt(savedCatalogScroll, 10) || 0), + left: 0, + behavior: 'auto' + }); + }); + } + + if (savedCatalogMenuScroll !== null && catalogMenu) { + sessionStorage.removeItem(catalogMenuScrollKey); + catalogMenu.scrollTop = Math.max(0, parseInt(savedCatalogMenuScroll, 10) || 0); + } + } + + document.addEventListener('click', (event) => { + const categoryLink = event.target.closest('.catalog-menu-panel .category-list a[href^="/catalog/"]'); + + if (!categoryLink || event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) { + return; + } + + sessionStorage.setItem(catalogScrollKey, String(window.scrollY)); + sessionStorage.setItem(catalogMenuScrollKey, String(categoryLink.closest('.catalog-menu-panel > .category-list')?.scrollTop || 0)); + }); + document.addEventListener('click', (event) => { const modeButton = event.target.closest('[data-payment-mode]'); @@ -594,6 +654,26 @@ try { const unitLabelNoteNode = card.querySelector('[data-unit-label-note]'); const oldPriceNode = card.querySelector('[data-old-package-price-display]'); const oldUnitPriceNode = card.querySelector('[data-old-unit-price-display]'); + const unitPriceRow = card.querySelector('[data-unit-price-row]'); + const packageTierTrigger = card.querySelector('[data-package-tier-trigger]'); + const packagePriceNote = card.querySelector('[data-package-price-note]'); + const showUnitPrice = button.dataset.showUnitPrice === '1'; + + card.dataset.showUnitPrice = showUnitPrice ? '1' : '0'; + + if (unitPriceRow) { + unitPriceRow.classList.toggle('is-price-placeholder', !showUnitPrice); + unitPriceRow.setAttribute('aria-hidden', showUnitPrice ? 'false' : 'true'); + } + + if (packageTierTrigger) { + packageTierTrigger.hidden = showUnitPrice; + } + + if (packagePriceNote) { + packagePriceNote.classList.toggle('is-price-placeholder', !showUnitPrice); + packagePriceNote.setAttribute('aria-hidden', showUnitPrice ? 'false' : 'true'); + } if (unitNode) { unitNode.textContent = button.dataset.unit || 'ед.'; diff --git a/views/partials/category-icons.php b/views/partials/category-icons.php new file mode 100644 index 0000000..452d5d1 --- /dev/null +++ b/views/partials/category-icons.php @@ -0,0 +1,41 @@ + 'fish', + str_contains($lowerName, 'морепроду') => 'shrimp', + str_contains($lowerName, 'икра') => 'caviar', + str_contains($lowerName, 'мяс') => 'meat', + str_contains($lowerName, 'сыр') => 'cheese', + str_contains($lowerName, 'полуфаб') => 'box', + str_contains($lowerName, 'овощ') || str_contains($lowerName, 'фрукт') || str_contains($lowerName, 'ягод') || str_contains($lowerName, 'гриб') => 'leaf', + str_contains($lowerName, 'бакале') => 'jar', + str_contains($lowerName, 'молоч') => 'milk', + default => 'grid', + }; + } +} + +if (!function_exists('category_tree_icon_svg')) { + function category_tree_icon_svg(string $icon): string + { + return match ($icon) { + 'fish' => '', + 'shrimp' => '', + 'leaf' => '', + 'caviar' => '', + 'jar' => '', + 'box' => '', + 'meat' => '', + 'cheese' => '', + 'milk' => '', + default => '', + }; + } +} diff --git a/views/partials/category-tree.php b/views/partials/category-tree.php index df49702..9a8fa95 100644 --- a/views/partials/category-tree.php +++ b/views/partials/category-tree.php @@ -6,6 +6,8 @@ declare(strict_types=1); /** @var int $depth */ /** @var string $currentSlug */ +require_once __DIR__ . '/category-icons.php'; + if (!function_exists('category_tree_has_current')) { /** * @param array $category @@ -35,10 +37,14 @@ if (!function_exists('category_tree_has_current')) { -
  • + +
  • > + + +
    - + + + + + +
  • diff --git a/views/partials/mega-category-menu.php b/views/partials/mega-category-menu.php index a4ddc80..3e085bc 100644 --- a/views/partials/mega-category-menu.php +++ b/views/partials/mega-category-menu.php @@ -4,6 +4,8 @@ declare(strict_types=1); /** @var array> $megaCategories */ +require_once __DIR__ . '/category-icons.php'; + if (!function_exists('render_mega_category_children')) { /** * @param array> $children @@ -30,8 +32,12 @@ if (!function_exists('render_mega_category_children')) {