771 lines
35 KiB
PHP
771 lines
35 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
/** @var string $templatePath */
|
||
/** @var string $title */
|
||
/** @var string $metaDescription */
|
||
/** @var string $metaKeywords */
|
||
/** @var array<int, array{title: string, url?: string}> $breadcrumbs */
|
||
/** @var bool $showBackButton */
|
||
$megaCategories = [];
|
||
$brandLogoSvg = <<<'SVG'
|
||
<svg class="brand-logo-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 650 220" role="img" aria-labelledby="brand-logo-title brand-logo-desc">
|
||
<title id="brand-logo-title">Рыбсток</title>
|
||
<desc id="brand-logo-desc">Качество со склада. Стоковые цены.</desc>
|
||
<defs>
|
||
<style>
|
||
.brand-logo-svg .green{fill:#173d2c}
|
||
.brand-logo-svg .green2{fill:#27634a}
|
||
.brand-logo-svg .red{fill:#c43d31}
|
||
.brand-logo-svg .paper{fill:#fffefa}
|
||
.brand-logo-svg .shadow-green{fill:#173d2c;opacity:.13}
|
||
.brand-logo-svg .word{font-family:Arial Black, Arial, Helvetica, sans-serif;font-size:104px;font-weight:950;letter-spacing:-7px}
|
||
.brand-logo-svg .tag{font-family:Arial, Helvetica, sans-serif;font-size:25px;font-weight:900;letter-spacing:-.25px;fill:#173d2c}
|
||
.brand-logo-svg .rule-red{fill:none;stroke:#c43d31;stroke-width:7;stroke-linecap:round}
|
||
.brand-logo-svg .rule-green{fill:none;stroke:#173d2c;stroke-width:5;stroke-linecap:round}
|
||
.brand-logo-svg .rule-shadow{fill:none;stroke:#173d2c;stroke-width:7;stroke-linecap:round;opacity:.12}
|
||
.brand-logo-svg .stroke{fill:none;stroke-linecap:round;stroke-linejoin:round}
|
||
</style>
|
||
</defs>
|
||
<g transform="translate(8 26)">
|
||
<text class="word shadow-green" x="9" y="91">Рыб</text>
|
||
<text class="word shadow-green" x="199" y="91">сток</text>
|
||
<text class="word green" x="0" y="80">Рыб</text>
|
||
<text class="word red" x="190" y="80">сток</text>
|
||
<path class="shadow-green" d="M157 28 C183 30 203 49 203 74 C203 99 183 118 157 120 L157 100 C172 98 183 88 183 74 C183 60 172 50 157 48Z"/>
|
||
<path class="red" d="M164 22 C190 24 210 43 210 68 C210 93 190 112 164 114 L164 94 C179 92 190 82 190 68 C190 54 179 44 164 42Z"/>
|
||
<path class="paper" d="M164 39 C179 41 190 53 190 68 C190 83 179 95 164 97 L164 82 C171 80 176 75 176 68 C176 61 171 56 164 54Z"/>
|
||
<path class="stroke green2" stroke-width="6" d="M141 56 H168"/>
|
||
<path class="stroke red" stroke-width="5" d="M151 64 H174"/>
|
||
<path class="stroke green2" stroke-width="6" d="M141 84 H168"/>
|
||
<path class="stroke red" stroke-width="5" d="M151 92 H174"/>
|
||
<path class="rule-shadow" d="M8 119 H432"/>
|
||
<path class="rule-red" d="M4 112 H176"/>
|
||
<path class="rule-green" d="M178 112 H432"/>
|
||
<text class="tag" x="4" y="162">Качество со склада. <tspan class="red">Стоковые цены.</tspan></text>
|
||
</g>
|
||
</svg>
|
||
SVG;
|
||
$faviconSvg = <<<'SVG'
|
||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||
<rect width="512" height="512" rx="112" fill="#fffefa"/>
|
||
<path d="M76 362 H250" fill="none" stroke="#c43d31" stroke-width="26" stroke-linecap="round"/>
|
||
<path d="M252 362 H436" fill="none" stroke="#173d2c" stroke-width="20" stroke-linecap="round"/>
|
||
<text x="38" y="284" font-family="Arial Black, Arial, Helvetica, sans-serif" font-size="214" font-weight="950" letter-spacing="-18" fill="#173d2c">Ры</text>
|
||
<text x="214" y="284" font-family="Arial Black, Arial, Helvetica, sans-serif" font-size="214" font-weight="950" letter-spacing="-18" fill="#c43d31">с</text>
|
||
<path d="M247 105 C318 111 374 166 374 256 C374 346 318 401 247 407 L247 352 C290 346 320 307 320 256 C320 205 290 166 247 160Z" fill="#c43d31"/>
|
||
<path d="M248 165 C284 174 306 210 306 256 C306 302 284 338 248 347 L248 300 C266 293 278 277 278 256 C278 235 266 219 248 212Z" fill="#fffefa"/>
|
||
<path d="M202 225 H267" fill="none" stroke="#27634a" stroke-width="18" stroke-linecap="round"/>
|
||
<path d="M225 246 H282" fill="none" stroke="#c43d31" stroke-width="14" stroke-linecap="round"/>
|
||
<path d="M202 287 H267" fill="none" stroke="#27634a" stroke-width="18" stroke-linecap="round"/>
|
||
<path d="M225 308 H282" fill="none" stroke="#c43d31" stroke-width="14" stroke-linecap="round"/>
|
||
</svg>
|
||
SVG;
|
||
$faviconHref = 'data:image/svg+xml,' . rawurlencode($faviconSvg);
|
||
|
||
try {
|
||
$megaCategories = (new \App\Repositories\CategoryRepository())->tree();
|
||
} catch (\Throwable) {
|
||
$megaCategories = [];
|
||
}
|
||
?>
|
||
<!doctype html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title><?= e($title) ?></title>
|
||
<?php if ($metaDescription !== ''): ?>
|
||
<meta name="description" content="<?= e($metaDescription) ?>">
|
||
<?php endif; ?>
|
||
<?php if ($metaKeywords !== ''): ?>
|
||
<meta name="keywords" content="<?= e($metaKeywords) ?>">
|
||
<?php endif; ?>
|
||
<link rel="icon" type="image/svg+xml" href="<?= e($faviconHref) ?>">
|
||
<meta name="theme-color" content="#173d2c">
|
||
<link rel="stylesheet" href="/assets/css/app.css">
|
||
</head>
|
||
<body>
|
||
<header class="site-topbar">
|
||
<div class="site-topbar-inner">
|
||
<a class="brand-link" href="/">
|
||
<?= $brandLogoSvg ?>
|
||
</a>
|
||
<nav class="main-nav" aria-label="Главное меню">
|
||
<a href="/">Главная</a>
|
||
<a href="/catalog">Каталог</a>
|
||
<a href="/delivery">Доставка</a>
|
||
<a href="/contacts">Контакты</a>
|
||
<a class="cart-link" href="/cart">Корзина <span data-cart-count hidden>0</span></a>
|
||
</nav>
|
||
<div class="topbar-contacts">
|
||
<a href="tel:+79777363363">+7-977-736-33-63</a>
|
||
<span>Самовывоз: г. Москва, ул. Привольная, д. 13к1</span>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<section class="smart-search-bar" aria-label="Поиск по каталогу">
|
||
<div class="smart-search-inner">
|
||
<form class="smart-search-form" action="/catalog" method="get">
|
||
<label class="smart-search-label" for="site-search">Умный поиск</label>
|
||
<div class="smart-search-control">
|
||
<input id="site-search" name="q" type="search" placeholder="Например: форель стейки, креветки 1 кг, красная рыба, что взять на ужин">
|
||
<button class="voice-search-button" type="button" aria-label="Голосовой ввод" title="Голосовой ввод">Голос</button>
|
||
<button class="smart-search-submit" type="submit">Найти</button>
|
||
</div>
|
||
<p>Понимает склонения, похожие запросы и бытовые формулировки. Обучаемый поиск подключим к товарам после загрузки каталога.</p>
|
||
</form>
|
||
</div>
|
||
</section>
|
||
|
||
<?php require base_path('views/partials/mega-category-menu.php'); ?>
|
||
|
||
<section class="price-mode-bar" aria-label="Система оплаты при получении">
|
||
<div class="price-mode-inner">
|
||
<span>Выберите систему оплаты при получении:</span>
|
||
<div class="price-mode-options" role="group" aria-label="Способ оплаты">
|
||
<button type="button" class="price-mode-button price-mode-button-cash is-active" data-payment-mode="cash">
|
||
<span>Наличными</span>
|
||
<b>Выгодно от 7000р.</b>
|
||
</button>
|
||
<button type="button" class="price-mode-button" data-payment-mode="qr">QR-код</button>
|
||
<button type="button" class="price-mode-button" data-payment-mode="invoice">Расчетный счет</button>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<header class="site-utility">
|
||
<?php if ($showBackButton): ?>
|
||
<button class="back-button" type="button" onclick="history.length > 1 ? history.back() : location.href='/'">Вернуться назад</button>
|
||
<?php endif; ?>
|
||
|
||
<?php require base_path('views/partials/breadcrumbs.php'); ?>
|
||
</header>
|
||
|
||
<main class="page">
|
||
<?php require $templatePath; ?>
|
||
</main>
|
||
|
||
<footer class="site-footer">
|
||
<div class="site-footer-inner">
|
||
<section class="footer-brand">
|
||
<a class="footer-logo" href="/">
|
||
<?= $brandLogoSvg ?>
|
||
</a>
|
||
</section>
|
||
|
||
<section class="footer-column">
|
||
<h2>Контакты</h2>
|
||
<dl class="footer-contacts">
|
||
<div>
|
||
<dt>Телефон</dt>
|
||
<dd><a href="tel:+79777363363">+7-977-736-33-63</a></dd>
|
||
</div>
|
||
<div>
|
||
<dt>Email</dt>
|
||
<dd><a href="mailto:zakaz@rybstock.ru">zakaz@rybstock.ru</a></dd>
|
||
</div>
|
||
<div>
|
||
<dt>Адрес самовывоза</dt>
|
||
<dd>г. Москва, ул. Привольная, д. 13к1</dd>
|
||
</div>
|
||
</dl>
|
||
</section>
|
||
|
||
<nav class="footer-column footer-links" aria-label="Ссылки в подвале">
|
||
<h2>Покупателям</h2>
|
||
<a href="/">Главная</a>
|
||
<a href="/catalog">Каталог</a>
|
||
<a href="/delivery">Доставка</a>
|
||
<a href="/payment">Оплата</a>
|
||
<a href="/reviews">Отзывы</a>
|
||
<a href="/returns">Правила возврата товара</a>
|
||
</nav>
|
||
|
||
<section class="footer-column">
|
||
<h2>Заказы</h2>
|
||
<p>Доставка и самовывоз происходят после подтверждения заказа по телефону.</p>
|
||
<a class="footer-action" href="/catalog">Перейти в каталог</a>
|
||
</section>
|
||
</div>
|
||
|
||
<div class="site-footer-bottom">
|
||
<p>© 2026 Рыбсток</p>
|
||
<p>Информация на сайте носит справочный характер и не является публичной офертой. Наличие, фасовки, цены и условия доставки подтверждаются менеджером при обработке заказа.</p>
|
||
</div>
|
||
</footer>
|
||
<script>
|
||
(() => {
|
||
const button = document.querySelector('.voice-search-button');
|
||
const input = document.querySelector('#site-search');
|
||
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
||
|
||
if (!button || !input) {
|
||
return;
|
||
}
|
||
|
||
if (!SpeechRecognition) {
|
||
button.addEventListener('click', () => {
|
||
input.focus();
|
||
input.placeholder = 'Голосовой ввод не поддерживается этим браузером';
|
||
});
|
||
return;
|
||
}
|
||
|
||
const recognition = new SpeechRecognition();
|
||
recognition.lang = 'ru-RU';
|
||
recognition.interimResults = false;
|
||
|
||
button.addEventListener('click', () => {
|
||
button.classList.add('is-listening');
|
||
recognition.start();
|
||
});
|
||
|
||
recognition.addEventListener('result', (event) => {
|
||
const phrase = event.results[0]?.[0]?.transcript || '';
|
||
input.value = phrase;
|
||
input.focus();
|
||
});
|
||
|
||
recognition.addEventListener('end', () => {
|
||
button.classList.remove('is-listening');
|
||
});
|
||
})();
|
||
(() => {
|
||
const nodes = document.querySelectorAll('[data-temperature]');
|
||
|
||
if (nodes.length === 0) {
|
||
return;
|
||
}
|
||
|
||
const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
|
||
|
||
nodes.forEach((node) => {
|
||
let current = Number(node.dataset.temperature || node.textContent || 0);
|
||
const min = Number(node.dataset.min || current - 1);
|
||
const max = Number(node.dataset.max || current + 1);
|
||
|
||
const update = () => {
|
||
const drift = (Math.random() - 0.5) * 0.28;
|
||
current = clamp(current + drift, min, max);
|
||
node.textContent = current.toFixed(1);
|
||
};
|
||
|
||
window.setInterval(update, 4200 + Math.random() * 2400);
|
||
});
|
||
})();
|
||
(() => {
|
||
const formatRub = (value) => new Intl.NumberFormat('ru-RU').format(Math.max(0, Math.floor(Number(value) || 0))) + ' руб.';
|
||
const storageKeys = {
|
||
paymentMode: 'rybstock_payment_mode',
|
||
cartTotal: 'rybstock_cart_total',
|
||
cartItems: 'rybstock_cart_items'
|
||
};
|
||
|
||
const getPaymentMode = () => localStorage.getItem(storageKeys.paymentMode) || 'cash';
|
||
const getCartTotal = () => Number(localStorage.getItem(storageKeys.cartTotal) || 0);
|
||
const getCartItems = () => {
|
||
try {
|
||
const items = JSON.parse(localStorage.getItem(storageKeys.cartItems) || '[]');
|
||
return Array.isArray(items) ? items : [];
|
||
} catch (error) {
|
||
return [];
|
||
}
|
||
};
|
||
const cartBaseTotal = (items = getCartItems()) => items.reduce((sum, item) => {
|
||
return sum + (Number(item.basePackagePrice || 0) * Number(item.quantity || 0));
|
||
}, 0);
|
||
const saveCartItems = (items) => {
|
||
localStorage.setItem(storageKeys.cartItems, JSON.stringify(items));
|
||
localStorage.setItem(storageKeys.cartTotal, String(cartBaseTotal(items)));
|
||
};
|
||
const escapeHtml = (value) => String(value ?? '').replace(/[&<>"']/g, (char) => ({
|
||
'&': '&',
|
||
'<': '<',
|
||
'>': '>',
|
||
'"': '"',
|
||
"'": '''
|
||
})[char]);
|
||
|
||
const priceMode = () => {
|
||
const payment = getPaymentMode();
|
||
const total = getCartTotal();
|
||
|
||
if (payment === 'invoice') {
|
||
return { multiplier: 1.08, fixedDiscount: 0, label: 'Оплата по расчетному счету: +8%' };
|
||
}
|
||
|
||
if (payment === 'qr') {
|
||
return { multiplier: 1, fixedDiscount: 0, label: 'QR-код: базовая цена' };
|
||
}
|
||
|
||
if (total >= 50000) {
|
||
return { multiplier: 0.93, fixedDiscount: 0, label: 'Наличные: цена от 50 000р.' };
|
||
}
|
||
|
||
if (total >= 20000) {
|
||
return { multiplier: 0.95, fixedDiscount: 0, 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) => ({
|
||
base: base,
|
||
cash20: base * 0.95,
|
||
cash50: base * 0.93
|
||
});
|
||
|
||
const updateDynamicPrices = (scope) => {
|
||
const mode = priceMode();
|
||
|
||
scope.querySelectorAll('[data-dynamic-package-price]').forEach((node) => {
|
||
const base = Number(node.dataset.basePackagePrice || 0);
|
||
node.textContent = formatRub(base * mode.multiplier);
|
||
});
|
||
|
||
scope.querySelectorAll('[data-dynamic-unit-price]').forEach((node) => {
|
||
const base = Number(node.dataset.baseUnitPrice || 0);
|
||
if (base > 0) {
|
||
node.textContent = formatRub(base * mode.multiplier);
|
||
}
|
||
});
|
||
};
|
||
|
||
const updateCardPrices = (card) => {
|
||
const packageNode = card.querySelector('[data-dynamic-package-price]');
|
||
const unitNode = card.querySelector('[data-dynamic-unit-price]');
|
||
updateDynamicPrices(card);
|
||
|
||
if (unitNode || packageNode) {
|
||
const unitBase = Number(unitNode?.dataset.baseUnitPrice || 0);
|
||
const packageBase = Number(packageNode?.dataset.basePackagePrice || 0);
|
||
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]');
|
||
const tierCash20 = card.querySelector('[data-tier-cash-20]');
|
||
const tierCash50 = card.querySelector('[data-tier-cash-50]');
|
||
const tierUnitLabel = card.querySelector('[data-tier-unit-label]');
|
||
|
||
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 = showUnitPrice
|
||
? (card.querySelector('[data-unit-label]')?.textContent || 'кг')
|
||
: 'фасовку';
|
||
}
|
||
|
||
};
|
||
|
||
const updateCartCount = () => {
|
||
const count = getCartItems().reduce((sum, item) => sum + Number(item.quantity || 0), 0);
|
||
|
||
document.querySelectorAll('[data-cart-count]').forEach((node) => {
|
||
node.textContent = String(count);
|
||
node.hidden = count === 0;
|
||
});
|
||
};
|
||
|
||
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 = () => {
|
||
const cartPage = document.querySelector('[data-cart-page]');
|
||
|
||
if (!cartPage) {
|
||
return;
|
||
}
|
||
|
||
const itemsNode = cartPage.querySelector('[data-cart-items]');
|
||
const emptyNode = cartPage.querySelector('[data-cart-empty]');
|
||
const baseTotalNode = cartPage.querySelector('[data-cart-base-total]');
|
||
const totalNode = cartPage.querySelector('[data-cart-current-total]');
|
||
const modeNode = cartPage.querySelector('[data-cart-current-mode]');
|
||
const savingRow = cartPage.querySelector('[data-cart-saving-row]');
|
||
const savingNode = cartPage.querySelector('[data-cart-saving-total]');
|
||
const items = getCartItems();
|
||
const baseTotal = cartBaseTotal(items);
|
||
const currentTotal = cartCurrentTotal(items);
|
||
const savingTotal = Math.max(0, baseTotal - currentTotal);
|
||
|
||
if (emptyNode) {
|
||
emptyNode.hidden = items.length !== 0;
|
||
}
|
||
|
||
if (itemsNode) {
|
||
itemsNode.innerHTML = '';
|
||
|
||
items.forEach((item) => {
|
||
const row = document.createElement('article');
|
||
row.className = 'cart-row';
|
||
row.dataset.cartKey = item.key;
|
||
row.innerHTML = `
|
||
<div class="cart-row-image">${item.image ? `<img src="${escapeHtml(item.image)}" alt="">` : ''}</div>
|
||
<div class="cart-row-main">
|
||
<h2>${escapeHtml(item.name)}</h2>
|
||
<p>${escapeHtml(item.variantName || 'Фасовка')} · ${formatRub(itemCurrentPrice(item))} за фасовку</p>
|
||
</div>
|
||
<div class="cart-row-controls">
|
||
<button type="button" data-cart-minus>-</button>
|
||
<strong>${Number(item.quantity || 0)}</strong>
|
||
<button type="button" data-cart-plus>+</button>
|
||
</div>
|
||
<strong class="cart-row-total">${formatRub(itemCurrentPrice(item) * Number(item.quantity || 0))}</strong>
|
||
<button class="cart-row-remove" type="button" data-cart-remove>Убрать</button>
|
||
`;
|
||
itemsNode.append(row);
|
||
});
|
||
}
|
||
|
||
if (baseTotalNode) {
|
||
baseTotalNode.textContent = formatRub(baseTotal);
|
||
}
|
||
|
||
if (totalNode) {
|
||
totalNode.textContent = formatRub(currentTotal);
|
||
}
|
||
|
||
if (modeNode) {
|
||
modeNode.textContent = priceMode().label;
|
||
}
|
||
|
||
if (savingRow) {
|
||
savingRow.hidden = savingTotal <= 0;
|
||
}
|
||
|
||
if (savingNode) {
|
||
savingNode.textContent = formatRub(savingTotal);
|
||
}
|
||
};
|
||
|
||
const updateModeButtons = () => {
|
||
const payment = getPaymentMode();
|
||
document.body.dataset.paymentMode = payment;
|
||
|
||
document.querySelectorAll('[data-payment-mode]').forEach((button) => {
|
||
button.classList.toggle('is-active', button.dataset.paymentMode === payment);
|
||
});
|
||
|
||
};
|
||
|
||
const updateAllPrices = () => {
|
||
updateModeButtons();
|
||
updateCartCount();
|
||
updateMiniCart();
|
||
updateDynamicPrices(document);
|
||
document.querySelectorAll('.product-card.price-aware').forEach(updateCardPrices);
|
||
renderCartPage();
|
||
};
|
||
|
||
window.RybStockPricing = {
|
||
setCartTotal(total) {
|
||
localStorage.setItem(storageKeys.cartTotal, String(Math.max(0, Number(total) || 0)));
|
||
updateAllPrices();
|
||
},
|
||
setPaymentMode(mode) {
|
||
localStorage.setItem(storageKeys.paymentMode, mode);
|
||
updateAllPrices();
|
||
},
|
||
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]');
|
||
|
||
if (!modeButton) {
|
||
return;
|
||
}
|
||
|
||
window.RybStockPricing.setPaymentMode(modeButton.dataset.paymentMode || 'cash');
|
||
});
|
||
|
||
document.addEventListener('click', (event) => {
|
||
const quantityButton = event.target.closest('[data-quantity-minus], [data-quantity-plus]');
|
||
|
||
if (!quantityButton) {
|
||
return;
|
||
}
|
||
|
||
const control = quantityButton.closest('[data-quantity-control]');
|
||
const input = control?.querySelector('[data-quantity-input]');
|
||
|
||
if (!input) {
|
||
return;
|
||
}
|
||
|
||
const current = Math.max(1, parseInt(input.value || '1', 10) || 1);
|
||
const next = quantityButton.matches('[data-quantity-plus]')
|
||
? current + 1
|
||
: Math.max(1, current - 1);
|
||
input.value = String(next);
|
||
});
|
||
|
||
document.addEventListener('input', (event) => {
|
||
const input = event.target.closest('[data-quantity-input]');
|
||
|
||
if (!input) {
|
||
return;
|
||
}
|
||
|
||
input.value = String(Math.max(1, parseInt(input.value.replace(/\D/g, '') || '1', 10) || 1));
|
||
});
|
||
|
||
document.addEventListener('click', (event) => {
|
||
const addButton = event.target.closest('.add-to-cart-button');
|
||
|
||
if (!addButton) {
|
||
return;
|
||
}
|
||
|
||
const productScope = addButton.closest('.price-aware');
|
||
const row = addButton.closest('.variant-row');
|
||
const variantButton = productScope?.querySelector('.variant-choice-button.is-active');
|
||
const packagePriceNode = row?.querySelector('[data-dynamic-package-price]') || productScope?.querySelector('[data-dynamic-package-price]');
|
||
const quantityInput = row?.querySelector('[data-quantity-input]') || productScope?.querySelector('[data-quantity-input]');
|
||
|
||
if (!productScope || !packagePriceNode) {
|
||
return;
|
||
}
|
||
|
||
const addQuantity = Math.max(1, parseInt(quantityInput?.value || '1', 10) || 1);
|
||
const variantId = row?.dataset.variantId || variantButton?.dataset.variantId || 'default';
|
||
const key = String(productScope.dataset.productId || '') + ':' + variantId;
|
||
const items = getCartItems();
|
||
const existing = items.find((item) => item.key === key);
|
||
|
||
if (existing) {
|
||
existing.quantity = Number(existing.quantity || 0) + addQuantity;
|
||
} else {
|
||
items.push({
|
||
key,
|
||
productId: productScope.dataset.productId || '',
|
||
slug: productScope.dataset.productSlug || '',
|
||
name: productScope.dataset.productName || '',
|
||
image: productScope.dataset.productImage || '',
|
||
variantId,
|
||
variantName: row?.dataset.variantName || variantButton?.dataset.variantName || 'Фасовка',
|
||
basePackagePrice: Number(packagePriceNode.dataset.basePackagePrice || 0),
|
||
baseUnitPrice: Number(row?.dataset.baseUnitPrice || variantButton?.dataset.baseUnitPrice || 0),
|
||
quantity: addQuantity
|
||
});
|
||
}
|
||
|
||
saveCartItems(items);
|
||
updateAllPrices();
|
||
addButton.classList.add('is-added');
|
||
addButton.textContent = 'Добавлено';
|
||
window.setTimeout(() => {
|
||
addButton.classList.remove('is-added');
|
||
addButton.textContent = 'В корзину';
|
||
}, 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) => {
|
||
const control = event.target.closest('[data-cart-plus], [data-cart-minus], [data-cart-remove]');
|
||
|
||
if (!control) {
|
||
return;
|
||
}
|
||
|
||
const row = control.closest('[data-cart-key]');
|
||
const key = row?.dataset.cartKey;
|
||
|
||
if (!key) {
|
||
return;
|
||
}
|
||
|
||
let items = getCartItems();
|
||
const item = items.find((cartItem) => cartItem.key === key);
|
||
|
||
if (!item) {
|
||
return;
|
||
}
|
||
|
||
if (control.matches('[data-cart-plus]')) {
|
||
item.quantity = Number(item.quantity || 0) + 1;
|
||
}
|
||
|
||
if (control.matches('[data-cart-minus]')) {
|
||
item.quantity = Number(item.quantity || 0) - 1;
|
||
}
|
||
|
||
if (control.matches('[data-cart-remove]') || Number(item.quantity || 0) <= 0) {
|
||
items = items.filter((cartItem) => cartItem.key !== key);
|
||
}
|
||
|
||
saveCartItems(items);
|
||
updateAllPrices();
|
||
});
|
||
|
||
document.addEventListener('click', (event) => {
|
||
const button = event.target.closest('.variant-choice-button');
|
||
|
||
if (!button) {
|
||
return;
|
||
}
|
||
|
||
const card = document.getElementById(button.dataset.card || '');
|
||
|
||
if (!card) {
|
||
return;
|
||
}
|
||
|
||
card.querySelectorAll('.variant-choice-button').forEach((item) => {
|
||
item.classList.toggle('is-active', item === button);
|
||
});
|
||
|
||
const unitNode = card.querySelector('[data-unit-label]');
|
||
const unitPriceNode = card.querySelector('[data-dynamic-unit-price]');
|
||
const packagePriceNode = card.querySelector('[data-dynamic-package-price]');
|
||
const packageLabelNode = card.querySelector('[data-package-label-display]');
|
||
const packageLabelNoteNode = card.querySelector('[data-package-label-note]');
|
||
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 || 'ед.';
|
||
}
|
||
|
||
if (unitLabelNoteNode) {
|
||
unitLabelNoteNode.textContent = button.dataset.unit || 'ед.';
|
||
}
|
||
|
||
if (unitPriceNode) {
|
||
unitPriceNode.dataset.baseUnitPrice = button.dataset.baseUnitPrice || '';
|
||
}
|
||
|
||
if (packagePriceNode) {
|
||
packagePriceNode.dataset.basePackagePrice = button.dataset.basePackagePrice || '';
|
||
}
|
||
|
||
if (packageLabelNode) {
|
||
packageLabelNode.textContent = button.dataset.variantPackageLabel || button.dataset.variantName || 'Фасовка';
|
||
}
|
||
|
||
if (packageLabelNoteNode) {
|
||
packageLabelNoteNode.textContent = button.dataset.variantPackageLabel || button.dataset.variantName || 'Фасовка';
|
||
}
|
||
|
||
if (oldPriceNode) {
|
||
const oldPackagePrice = button.dataset.oldPrice || '';
|
||
oldPriceNode.textContent = oldPackagePrice;
|
||
oldPriceNode.hidden = oldPackagePrice === '';
|
||
}
|
||
|
||
if (oldUnitPriceNode) {
|
||
const oldUnitPrice = Number(button.dataset.oldUnitPrice || 0);
|
||
const baseUnitPrice = Number(button.dataset.baseUnitPrice || 0);
|
||
const hasOldUnitPrice = oldUnitPrice > baseUnitPrice;
|
||
oldUnitPriceNode.textContent = hasOldUnitPrice ? formatRub(oldUnitPrice) : '';
|
||
oldUnitPriceNode.hidden = !hasOldUnitPrice;
|
||
}
|
||
|
||
updateCardPrices(card);
|
||
});
|
||
|
||
updateAllPrices();
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|