1289 lines
61 KiB
PHP
1289 lines
61 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 = [];
|
||
$currentPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/';
|
||
$isCartPage = $currentPath === '/cart';
|
||
$yandexMapsApiKey = (string) app_env('YANDEX_MAPS_API_KEY', '9ab8ad56-c350-4a2c-8f23-35acb102ee7a');
|
||
$queryParameters = [];
|
||
parse_str((string) (parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_QUERY) ?? ''), $queryParameters);
|
||
$currentSearchQuery = (string) ($_GET['q'] ?? $queryParameters['q'] ?? '');
|
||
$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);
|
||
$homeEmbeddedImages = [
|
||
'home-hero-picnic' => '/assets/images/home/hero-picnic-sunset.png',
|
||
'home-warehouse-fish' => '/assets/images/home/warehouse-fish.jpg',
|
||
'home-banner-meat' => '/assets/images/home/banner-meat-grill.png',
|
||
'home-banner-seafood' => '/assets/images/home/banner-seafood-fresh.png',
|
||
'home-warehouse-main' => '/assets/images/home/warehouse-main.jpg',
|
||
'home-warehouse-arrival' => '/assets/images/home/warehouse-arrival.jpg',
|
||
'home-warehouse-cold' => '/assets/images/home/warehouse-cold.jpg',
|
||
];
|
||
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) ?>">
|
||
<?php if ($yandexMapsApiKey !== ''): ?>
|
||
<meta name="yandex-maps-api-key" content="<?= e($yandexMapsApiKey) ?>">
|
||
<script>
|
||
window.RYBSTOCK_YANDEX_MAPS_API_KEY = <?= json_encode($yandexMapsApiKey, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
|
||
</script>
|
||
<?php endif; ?>
|
||
<style id="embedded-home-images">
|
||
:root {
|
||
<?php foreach ($homeEmbeddedImages as $homeImageKey => $homeImagePath): ?>
|
||
--<?= e($homeImageKey) ?>: url("<?= e($homeImagePath) ?>");
|
||
<?php endforeach; ?>
|
||
}
|
||
</style>
|
||
<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="/search" method="get" onsubmit="return window.rybstockSearchSubmit ? window.rybstockSearchSubmit(this) : true">
|
||
<label class="smart-search-label" for="site-search">Умный поиск</label>
|
||
<div class="smart-search-control">
|
||
<input id="site-search" name="q" type="search" value="<?= e($currentSearchQuery) ?>" 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'); ?>
|
||
|
||
<?php if (!$isCartPage): ?>
|
||
<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 price-mode-button-qr" data-payment-mode="qr">
|
||
<span>QR-код</span>
|
||
<b>Без комиссии даже с кредитки</b>
|
||
</button>
|
||
<button type="button" class="price-mode-button" data-payment-mode="invoice">Расчетный счет</button>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
<?php endif; ?>
|
||
|
||
<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 form = document.querySelector('.smart-search-form');
|
||
const input = document.querySelector('#site-search');
|
||
|
||
if (!form || !input) {
|
||
return;
|
||
}
|
||
|
||
window.rybstockSearchSubmit = (searchForm) => {
|
||
const searchInput = searchForm.querySelector('input[name="q"]');
|
||
const query = searchInput ? searchInput.value.trim() : '';
|
||
|
||
if (query === '') {
|
||
window.location.href = '/catalog';
|
||
return false;
|
||
}
|
||
|
||
window.location.href = '/search/' + encodeURIComponent(query);
|
||
return false;
|
||
};
|
||
|
||
form.addEventListener('submit', (event) => {
|
||
const query = input.value.trim();
|
||
|
||
if (query === '') {
|
||
event.preventDefault();
|
||
window.location.href = '/catalog';
|
||
return;
|
||
}
|
||
|
||
event.preventDefault();
|
||
window.location.href = '/search/' + encodeURIComponent(query);
|
||
});
|
||
})();
|
||
|
||
(() => {
|
||
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();
|
||
|
||
if (phrase.trim() !== '') {
|
||
window.setTimeout(() => {
|
||
input.form?.requestSubmit();
|
||
}, 120);
|
||
}
|
||
});
|
||
|
||
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',
|
||
deliveryType: 'rybstock_delivery_type',
|
||
deliveryDistance: 'rybstock_delivery_distance',
|
||
deliveryZone: 'rybstock_delivery_zone',
|
||
deliveryGeoStatus: 'rybstock_delivery_geo_status',
|
||
deliveryLift: 'rybstock_delivery_lift',
|
||
deliveryAddress: 'rybstock_delivery_address'
|
||
};
|
||
|
||
const getPaymentMode = () => localStorage.getItem(storageKeys.paymentMode) || 'cash';
|
||
const getDeliveryType = () => localStorage.getItem(storageKeys.deliveryType) || '';
|
||
const getDeliveryDistance = () => Math.max(0, Number(localStorage.getItem(storageKeys.deliveryDistance) || 0));
|
||
const getDeliveryZone = () => localStorage.getItem(storageKeys.deliveryZone) || '';
|
||
const getDeliveryGeoStatus = () => localStorage.getItem(storageKeys.deliveryGeoStatus) || '';
|
||
const getDeliveryLift = () => localStorage.getItem(storageKeys.deliveryLift) === '1';
|
||
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: 'Расчетный счет: счет день в день' };
|
||
}
|
||
|
||
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 deliveryLabels = {
|
||
pickup: 'самовывоз',
|
||
inside_mkad: 'доставку внутри МКАД',
|
||
outside_mkad: 'доставку за МКАД'
|
||
};
|
||
|
||
const deliveryMinimums = {
|
||
pickup: 1,
|
||
inside_mkad: 2000,
|
||
outside_mkad: 5000
|
||
};
|
||
|
||
const deliveryMinimumMessage = (type, amount) => {
|
||
const minimum = deliveryMinimums[type] || 0;
|
||
const missing = Math.max(0, minimum - amount);
|
||
|
||
if (missing <= 0) {
|
||
return '';
|
||
}
|
||
|
||
return `Уважаемый клиент, Вы выбрали ${deliveryLabels[type] || 'получение'}, минимальная сумма - ${formatRub(minimum)}, добавьте пожалуйста товары на сумму ${formatRub(missing)}.`;
|
||
};
|
||
|
||
const moscowCenter = [55.755864, 37.617698];
|
||
const mkadRadiusKm = 17.7;
|
||
let yandexMapsPromise = null;
|
||
let deliveryAddressTimer = null;
|
||
|
||
const setDeliveryGeo = (distance, zone, status) => {
|
||
localStorage.setItem(storageKeys.deliveryDistance, String(Math.max(0, Number(distance) || 0)));
|
||
localStorage.setItem(storageKeys.deliveryZone, zone || '');
|
||
localStorage.setItem(storageKeys.deliveryGeoStatus, status || '');
|
||
};
|
||
|
||
const loadYandexMapsApi = () => {
|
||
if (window.ymaps) {
|
||
return new Promise((resolve) => window.ymaps.ready(resolve));
|
||
}
|
||
|
||
if (yandexMapsPromise) {
|
||
return yandexMapsPromise;
|
||
}
|
||
|
||
const apiKey = window.RYBSTOCK_YANDEX_MAPS_API_KEY || document.querySelector('meta[name="yandex-maps-api-key"]')?.content || '';
|
||
|
||
yandexMapsPromise = new Promise((resolve, reject) => {
|
||
if (!apiKey) {
|
||
reject(new Error('Yandex Maps API key is empty'));
|
||
return;
|
||
}
|
||
|
||
const script = document.createElement('script');
|
||
script.src = `https://api-maps.yandex.ru/2.1/?apikey=${encodeURIComponent(apiKey)}&lang=ru_RU`;
|
||
script.async = true;
|
||
script.onload = () => window.ymaps ? window.ymaps.ready(resolve) : reject(new Error('Yandex Maps API is unavailable'));
|
||
script.onerror = () => reject(new Error('Yandex Maps API loading failed'));
|
||
document.head.append(script);
|
||
});
|
||
|
||
return yandexMapsPromise;
|
||
};
|
||
|
||
const haversineKm = (from, to) => {
|
||
const toRad = (value) => value * Math.PI / 180;
|
||
const earthRadiusKm = 6371;
|
||
const dLat = toRad(to[0] - from[0]);
|
||
const dLon = toRad(to[1] - from[1]);
|
||
const lat1 = toRad(from[0]);
|
||
const lat2 = toRad(to[0]);
|
||
const a = Math.sin(dLat / 2) ** 2 + Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLon / 2) ** 2;
|
||
|
||
return 2 * earthRadiusKm * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||
};
|
||
|
||
const resolveDeliveryAddress = async (address) => {
|
||
const normalizedAddress = String(address || '').trim();
|
||
|
||
if (normalizedAddress.length < 6) {
|
||
setDeliveryGeo(0, '', '');
|
||
updateAllPrices();
|
||
return;
|
||
}
|
||
|
||
setDeliveryGeo(0, '', 'Рассчитываем расстояние по адресу...');
|
||
updateAllPrices();
|
||
|
||
try {
|
||
await loadYandexMapsApi();
|
||
const result = await window.ymaps.geocode(normalizedAddress, { results: 1 });
|
||
const first = result.geoObjects.get(0);
|
||
|
||
if (!first) {
|
||
setDeliveryGeo(0, '', 'Адрес не найден. Проверьте написание, менеджер уточнит доставку по телефону.');
|
||
updateAllPrices();
|
||
return;
|
||
}
|
||
|
||
const coords = first.geometry.getCoordinates();
|
||
const distanceFromCenter = haversineKm(moscowCenter, coords);
|
||
const outsideKm = Math.max(0, Math.ceil(distanceFromCenter - mkadRadiusKm));
|
||
|
||
if (outsideKm <= 0) {
|
||
setDeliveryGeo(0, 'inside', 'Адрес найден: внутри МКАД.');
|
||
} else {
|
||
setDeliveryGeo(outsideKm, 'outside', `Адрес найден: примерно ${outsideKm} км от МКАД.`);
|
||
}
|
||
} catch (error) {
|
||
setDeliveryGeo(0, '', 'Не удалось автоматически рассчитать адрес. Менеджер уточнит доставку по телефону.');
|
||
}
|
||
|
||
updateAllPrices();
|
||
};
|
||
|
||
const scheduleDeliveryAddressResolve = (address) => {
|
||
window.clearTimeout(deliveryAddressTimer);
|
||
deliveryAddressTimer = window.setTimeout(() => resolveDeliveryAddress(address), 700);
|
||
};
|
||
|
||
const deliveryCalculation = (items = getCartItems()) => {
|
||
const type = getDeliveryType();
|
||
const amount = cartCurrentTotal(items);
|
||
const lift = getDeliveryLift() && type !== 'pickup' ? 390 : 0;
|
||
const distance = getDeliveryDistance();
|
||
const zone = getDeliveryZone();
|
||
const minimum = deliveryMinimums[type] || 0;
|
||
const minimumOk = type ? amount >= minimum : false;
|
||
const minimumMessage = type ? deliveryMinimumMessage(type, amount) : '';
|
||
|
||
if (!type) {
|
||
return {
|
||
selected: false,
|
||
minimumOk: false,
|
||
minimumMessage: '',
|
||
price: 0,
|
||
lift,
|
||
total: 0,
|
||
label: 'Выберите получение',
|
||
note: 'Выберите самовывоз, доставку внутри МКАД или доставку за МКАД.'
|
||
};
|
||
}
|
||
|
||
if (type === 'pickup') {
|
||
return {
|
||
selected: true,
|
||
minimumOk,
|
||
minimumMessage,
|
||
price: 0,
|
||
lift: 0,
|
||
total: 0,
|
||
label: 'Самовывоз бесплатно',
|
||
note: 'Самовывоз осуществляется после оформления заказа на сайте и подтверждения заказа по телефону.'
|
||
};
|
||
}
|
||
|
||
if (type === 'inside_mkad') {
|
||
const free = amount >= 5000;
|
||
const price = free ? 0 : 599;
|
||
|
||
return {
|
||
selected: true,
|
||
minimumOk,
|
||
minimumMessage,
|
||
price,
|
||
lift,
|
||
total: price + lift,
|
||
label: free ? 'Бесплатно внутри МКАД' : 'Доставка внутри МКАД',
|
||
note: free
|
||
? 'Заказ от 5000 руб. доставим внутри МКАД бесплатно до подъезда. При заказе от 2000 до 5000 руб. доставка внутри МКАД стоит 599 руб.'
|
||
: 'Доставка внутри МКАД доступна от 2000 руб. При заказе от 2000 до 5000 руб. доставка стоит 599 руб., от 5000 руб. - бесплатно до подъезда.'
|
||
};
|
||
}
|
||
|
||
if (type === 'outside_mkad' && distance <= 0) {
|
||
const zoneNote = zone === 'inside'
|
||
? 'Адрес похож на адрес внутри МКАД. Выберите доставку внутри МКАД, если это верно.'
|
||
: 'Введите адрес доставки, и сайт рассчитает примерное расстояние за МКАД автоматически.';
|
||
|
||
return {
|
||
selected: true,
|
||
minimumOk,
|
||
minimumMessage,
|
||
price: 0,
|
||
lift,
|
||
total: lift,
|
||
label: 'Доставка за МКАД',
|
||
note: `${zoneNote} Доставка за МКАД доступна от 5000 руб. До 10 км от МКАД бесплатная доставка от 10000 руб.; дальше минимум для бесплатной доставки считается как 1000 руб. за каждый км от МКАД. Если заказ не проходит на бесплатную доставку, расчет: 199 руб. + 60 руб. за км.`
|
||
};
|
||
}
|
||
|
||
const freeThreshold = distance <= 10 ? 10000 : distance * 1000;
|
||
const free = amount >= freeThreshold;
|
||
const price = free ? 0 : Math.floor(199 + distance * 60);
|
||
|
||
return {
|
||
selected: true,
|
||
minimumOk,
|
||
minimumMessage,
|
||
price,
|
||
lift,
|
||
total: price + lift,
|
||
label: free ? 'Бесплатно за МКАД' : 'Доставка за МКАД',
|
||
note: distance > 0
|
||
? (free
|
||
? `Заказ проходит на бесплатную доставку: расстояние ${distance} км от МКАД.`
|
||
: `Расчет доставки: 199 руб. + ${distance} км × 60 руб. Минимум для бесплатной доставки: ${formatRub(freeThreshold)}.`)
|
||
: 'Доставка за МКАД доступна от 5000 руб. До 10 км от МКАД бесплатная доставка от 10000 руб.; дальше минимальная сумма для бесплатной доставки считается как 1000 руб. за каждый км от МКАД. Если заказ не проходит на бесплатную доставку, расчет: 199 руб. + 60 руб. за км.'
|
||
};
|
||
};
|
||
|
||
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 deliveryTotalNode = cartPage.querySelector('[data-cart-delivery-total]');
|
||
const deliveryNoteNode = cartPage.querySelector('[data-cart-delivery-note]');
|
||
const minimumWarningNode = cartPage.querySelector('[data-cart-minimum-warning]');
|
||
const grandTotalNode = cartPage.querySelector('[data-cart-grand-total]');
|
||
const outsideDistanceField = cartPage.querySelector('[data-outside-distance-field]');
|
||
const liftField = cartPage.querySelector('[data-lift-field]');
|
||
const addressInput = cartPage.querySelector('[data-cart-address]');
|
||
const addressStatusNode = cartPage.querySelector('[data-cart-address-status]');
|
||
const distanceInput = cartPage.querySelector('[data-cart-distance]');
|
||
const liftInput = cartPage.querySelector('[data-cart-lift]');
|
||
const items = getCartItems();
|
||
const baseTotal = cartBaseTotal(items);
|
||
const currentTotal = cartCurrentTotal(items);
|
||
const savingTotal = Math.max(0, baseTotal - currentTotal);
|
||
const delivery = deliveryCalculation(items);
|
||
|
||
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);
|
||
}
|
||
|
||
cartPage.querySelectorAll('[data-cart-delivery-type]').forEach((input) => {
|
||
input.checked = input.value === getDeliveryType();
|
||
});
|
||
|
||
if (outsideDistanceField) {
|
||
outsideDistanceField.hidden = getDeliveryType() !== 'outside_mkad';
|
||
}
|
||
|
||
if (liftField) {
|
||
liftField.hidden = !['inside_mkad', 'outside_mkad'].includes(getDeliveryType());
|
||
}
|
||
|
||
if (addressInput && document.activeElement !== addressInput) {
|
||
addressInput.value = localStorage.getItem(storageKeys.deliveryAddress) || '';
|
||
}
|
||
|
||
if (distanceInput && document.activeElement !== distanceInput) {
|
||
distanceInput.value = getDeliveryDistance() > 0 ? String(getDeliveryDistance()) : '';
|
||
}
|
||
|
||
if (addressStatusNode) {
|
||
const status = getDeliveryGeoStatus();
|
||
addressStatusNode.hidden = status === '';
|
||
addressStatusNode.textContent = status;
|
||
}
|
||
|
||
if (liftInput) {
|
||
liftInput.checked = getDeliveryLift();
|
||
}
|
||
|
||
if (deliveryTotalNode) {
|
||
deliveryTotalNode.textContent = delivery.selected ? formatRub(delivery.total) : delivery.label;
|
||
}
|
||
|
||
if (deliveryNoteNode) {
|
||
deliveryNoteNode.textContent = delivery.note;
|
||
}
|
||
|
||
if (minimumWarningNode) {
|
||
minimumWarningNode.hidden = !delivery.minimumMessage;
|
||
minimumWarningNode.textContent = delivery.minimumMessage || '';
|
||
}
|
||
|
||
if (grandTotalNode) {
|
||
grandTotalNode.textContent = formatRub(currentTotal + delivery.total);
|
||
}
|
||
};
|
||
|
||
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 deliveryLabel = event.target.closest('.cart-delivery-options label');
|
||
|
||
if (deliveryLabel) {
|
||
const input = deliveryLabel.querySelector('[data-cart-delivery-type]');
|
||
|
||
if (input && !input.checked) {
|
||
event.preventDefault();
|
||
input.checked = true;
|
||
input.dispatchEvent(new Event('change', { bubbles: true }));
|
||
}
|
||
}
|
||
});
|
||
|
||
document.addEventListener('click', (event) => {
|
||
const liftLabel = event.target.closest('.cart-lift-label');
|
||
|
||
if (!liftLabel || event.target.closest('[data-cart-lift]')) {
|
||
return;
|
||
}
|
||
|
||
const input = liftLabel.querySelector('[data-cart-lift]');
|
||
|
||
if (input) {
|
||
event.preventDefault();
|
||
input.checked = !input.checked;
|
||
input.dispatchEvent(new Event('change', { bubbles: true }));
|
||
}
|
||
});
|
||
|
||
document.addEventListener('change', (event) => {
|
||
const deliveryType = event.target.closest('[data-cart-delivery-type]');
|
||
|
||
if (deliveryType) {
|
||
localStorage.setItem(storageKeys.deliveryType, deliveryType.value);
|
||
localStorage.setItem(storageKeys.deliveryDistance, '0');
|
||
localStorage.setItem(storageKeys.deliveryZone, '');
|
||
|
||
if (deliveryType.value === 'pickup') {
|
||
localStorage.setItem(storageKeys.deliveryLift, '0');
|
||
localStorage.setItem(storageKeys.deliveryGeoStatus, '');
|
||
} else {
|
||
const address = localStorage.getItem(storageKeys.deliveryAddress) || '';
|
||
if (address.trim() !== '') {
|
||
scheduleDeliveryAddressResolve(address);
|
||
}
|
||
}
|
||
|
||
updateAllPrices();
|
||
return;
|
||
}
|
||
|
||
const liftInput = event.target.closest('[data-cart-lift]');
|
||
|
||
if (liftInput) {
|
||
localStorage.setItem(storageKeys.deliveryLift, liftInput.checked ? '1' : '0');
|
||
updateAllPrices();
|
||
}
|
||
});
|
||
|
||
document.addEventListener('input', (event) => {
|
||
const distanceInput = event.target.closest('[data-cart-distance]');
|
||
|
||
if (distanceInput) {
|
||
localStorage.setItem(storageKeys.deliveryDistance, String(Math.max(0, Number(distanceInput.value || 0))));
|
||
updateAllPrices();
|
||
return;
|
||
}
|
||
|
||
const addressInput = event.target.closest('[data-cart-address]');
|
||
|
||
if (addressInput) {
|
||
localStorage.setItem(storageKeys.deliveryAddress, addressInput.value);
|
||
localStorage.setItem(storageKeys.deliveryDistance, '0');
|
||
localStorage.setItem(storageKeys.deliveryZone, '');
|
||
localStorage.setItem(storageKeys.deliveryGeoStatus, addressInput.value.trim() === '' ? '' : 'Рассчитываем расстояние по адресу...');
|
||
if (getDeliveryType() !== 'pickup') {
|
||
scheduleDeliveryAddressResolve(addressInput.value);
|
||
}
|
||
updateAllPrices();
|
||
}
|
||
});
|
||
|
||
document.addEventListener('submit', (event) => {
|
||
const form = event.target.closest('[data-cart-order-form]');
|
||
|
||
if (!form) {
|
||
return;
|
||
}
|
||
|
||
event.preventDefault();
|
||
|
||
const cartPage = form.closest('[data-cart-page]');
|
||
const successNode = cartPage?.querySelector('[data-cart-order-success]');
|
||
const items = getCartItems();
|
||
const delivery = deliveryCalculation(items);
|
||
const name = form.querySelector('[data-order-name]')?.value.trim() || '';
|
||
const phone = form.querySelector('[data-order-phone]')?.value.trim() || '';
|
||
const email = form.querySelector('[data-order-email]')?.value.trim() || '';
|
||
const comment = form.querySelector('[data-order-comment]')?.value.trim() || '';
|
||
const finalSumNeeded = form.querySelector('[data-order-final-sum-needed]')?.checked || false;
|
||
const invoiceRequisites = form.closest('[data-cart-page]')?.querySelector('[data-order-invoice-requisites]')?.value.trim() || '';
|
||
const invoiceFiles = [...(form.closest('[data-cart-page]')?.querySelector('[data-order-invoice-file]')?.files || [])].map((file) => file.name);
|
||
|
||
if (items.length === 0) {
|
||
window.alert('Добавьте товары в корзину.');
|
||
return;
|
||
}
|
||
|
||
if (!getDeliveryType()) {
|
||
window.alert('Выберите самовывоз, доставку внутри МКАД или доставку за МКАД.');
|
||
return;
|
||
}
|
||
|
||
if (!delivery.minimumOk) {
|
||
window.alert(delivery.minimumMessage);
|
||
return;
|
||
}
|
||
|
||
if (!name || !phone) {
|
||
window.alert('Укажите имя и телефон.');
|
||
return;
|
||
}
|
||
|
||
const orderNumber = 'RS-' + new Date().toISOString().replace(/\D/g, '').slice(0, 14);
|
||
const productsTotal = cartCurrentTotal(items);
|
||
const grandTotal = productsTotal + delivery.total;
|
||
const order = {
|
||
orderNumber,
|
||
createdAt: new Date().toISOString(),
|
||
name,
|
||
phone,
|
||
email,
|
||
comment,
|
||
finalSumNeeded,
|
||
invoiceRequisites,
|
||
invoiceFiles,
|
||
paymentMode: getPaymentMode(),
|
||
deliveryType: getDeliveryType(),
|
||
address: localStorage.getItem(storageKeys.deliveryAddress) || '',
|
||
distance: getDeliveryDistance(),
|
||
deliveryZone: getDeliveryZone(),
|
||
lift: getDeliveryLift(),
|
||
items,
|
||
productsTotal,
|
||
deliveryTotal: delivery.total,
|
||
grandTotal
|
||
};
|
||
|
||
localStorage.setItem('rybstock_last_order', JSON.stringify(order));
|
||
|
||
if (successNode) {
|
||
successNode.hidden = false;
|
||
successNode.innerHTML = `
|
||
<strong>Спасибо за Ваш заказ. Заказ ${escapeHtml(orderNumber)} принят в обработку.</strong>
|
||
<p>Спасибо, что оформили заказ на сайте Рыбсток. Мы благодарим Вас за доверие.</p>
|
||
<p>Доставка и самовывоз товара происходят только после подтверждения заказа по телефону. Сумма к оплате при получении: <b>${formatRub(grandTotal)}</b>.</p>
|
||
<p>Если заказ оформлен в будний день до 14.00, мы перезвоним Вам сегодня. Если заказ оформлен после 14.00, мы свяжемся на следующий рабочий день с 10.00 до 14.00.</p>
|
||
<p>Суббота и воскресенье - выходные. Заказы, оформленные в пятницу после 14.00, автоматически переносятся на понедельник.</p>
|
||
<p>Если у Вас срочный вопрос, свяжитесь с нами по телефону <a href="tel:+79777363363">+7-977-736-33-63</a>.</p>
|
||
`;
|
||
successNode.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||
}
|
||
});
|
||
|
||
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>
|
||
|
||
|