201 lines
8.4 KiB
PHP
201 lines
8.4 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 = [];
|
|
|
|
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="stylesheet" href="/assets/css/app.css">
|
|
</head>
|
|
<body>
|
|
<header class="site-topbar">
|
|
<div class="site-topbar-inner">
|
|
<a class="brand-link" href="/">
|
|
<?php if (is_file(base_path('public/assets/images/logo-rybstock.svg'))): ?>
|
|
<img src="/assets/images/logo-rybstock.svg" alt="Рыбсток">
|
|
<?php else: ?>
|
|
<span>РЫБ<span>СТОК</span></span>
|
|
<small>Качественные продукты по стоковым ценам</small>
|
|
<?php endif; ?>
|
|
</a>
|
|
<nav class="main-nav" aria-label="Главное меню">
|
|
<a href="/">Главная</a>
|
|
<a href="/catalog">Каталог</a>
|
|
<a href="/delivery">Доставка</a>
|
|
<a href="/contacts">Контакты</a>
|
|
<a href="/cart">Корзина</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'); ?>
|
|
|
|
<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="/">
|
|
<?php if (is_file(base_path('public/assets/images/logo-rybstock.svg'))): ?>
|
|
<img src="/assets/images/logo-rybstock.svg" alt="Рыбсток">
|
|
<?php else: ?>
|
|
<span>Рыбсток</span>
|
|
<?php endif; ?>
|
|
</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);
|
|
});
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|