черновая шапка и первый скрол с лого завершено

This commit is contained in:
Alisa
2026-06-04 21:05:10 +03:00
parent e1c66ae53c
commit 7d14383031
9 changed files with 940 additions and 97 deletions
+90 -7
View File
@@ -8,6 +8,13 @@ declare(strict_types=1);
/** @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">
@@ -27,12 +34,12 @@ declare(strict_types=1);
<header class="site-topbar">
<div class="site-topbar-inner">
<a class="brand-link" href="/">
<?php if (is_file(base_path('public/assets/images/logo_300.png'))): ?>
<img src="/assets/images/logo_300.png" alt="Рыбсток">
<?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>СТОК</span></span>
<small>Качественные продукты по стоковым ценам</small>
<?php endif; ?>
<small>Качественные продукты по стоковым ценам</small>
</a>
<nav class="main-nav" aria-label="Главное меню">
<a href="/">Главная</a>
@@ -48,6 +55,22 @@ declare(strict_types=1);
</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>
@@ -64,13 +87,12 @@ declare(strict_types=1);
<div class="site-footer-inner">
<section class="footer-brand">
<a class="footer-logo" href="/">
<?php if (is_file(base_path('public/assets/images/logo_300.png'))): ?>
<img src="/assets/images/logo_300.png" alt="Рыбсток">
<?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>
<p>Качественные продукты по стоковым ценам</p>
</section>
<section class="footer-column">
@@ -113,5 +135,66 @@ declare(strict_types=1);
<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>
+88 -10
View File
@@ -9,38 +9,115 @@ declare(strict_types=1);
/** @var array<string, mixed> $settings */
/** @var string|null $notice */
/** @var string $buildVersion */
$timezone = new DateTimeZone('Europe/Moscow');
$now = new DateTimeImmutable('now', $timezone);
$lastArrival = new DateTimeImmutable('today', $timezone);
$previousWorkingDay = static function (DateTimeImmutable $date): DateTimeImmutable {
do {
$date = $date->modify('-1 day');
} while ((int) $date->format('N') >= 6);
return $date;
};
while ((int) $lastArrival->format('N') >= 6) {
$lastArrival = $previousWorkingDay($lastArrival);
}
$arrivalSeed = (int) sprintf('%u', crc32($lastArrival->format('Y-m-d') . '|rybstock-arrival'));
$arrivalSecond = (4 * 3600) + ($arrivalSeed % (3 * 3600 + 1));
$arrivalRefreshAt = $lastArrival->setTime(
intdiv($arrivalSecond, 3600),
intdiv($arrivalSecond % 3600, 60),
$arrivalSecond % 60
);
if ($now < $arrivalRefreshAt) {
$lastArrival = $previousWorkingDay($lastArrival);
}
$months = [
1 => 'января',
2 => 'февраля',
3 => 'марта',
4 => 'апреля',
5 => 'мая',
6 => 'июня',
7 => 'июля',
8 => 'августа',
9 => 'сентября',
10 => 'октября',
11 => 'ноября',
12 => 'декабря',
];
$arrivalDate = $lastArrival->format('j') . ' ' . $months[(int) $lastArrival->format('n')] . ' ' . $lastArrival->format('Y');
?>
<section class="start-screen wide">
<section class="home-hero">
<p class="eyebrow">Интернет-склад качественной продукции</p>
<h1>Рыбсток</h1>
<p class="hero-lead">Рыба, морепродукты, мясо, сыры, овощи, ягоды и бакалея по стоковым ценам.</p>
<div class="hero-actions">
<a class="primary-button hero-button" href="/catalog">Смотреть каталог</a>
<a class="secondary-button" href="tel:+79777363363">+7-977-736-33-63</a>
<div class="hero-copy">
<p class="eyebrow">Интернет-склад качественной продукции</p>
<h1>Рыбсток</h1>
<p class="hero-lead">Рыба, морепродукты, мясо, сыры, овощи, ягоды и бакалея по стоковым ценам.</p>
<div class="hero-points" aria-label="Ключевые условия">
<span>Доставка день в день до 14:00</span>
<span>Без предоплаты</span>
<span>Самовывоз после подтверждения</span>
</div>
<div class="hero-actions">
<a class="primary-button hero-button" href="/catalog">Смотреть каталог</a>
<a class="secondary-button" href="tel:+79777363363">+7-977-736-33-63</a>
</div>
</div>
<div class="hero-media">
<img src="/assets/images/brand-hero-products.svg" alt="Ассортимент Рыбсток">
</div>
</section>
<section class="storage-status" aria-label="Температура хранения и поступление товара">
<div class="arrival-card">
<span>Последнее поступление</span>
<strong><?= e((string) $arrivalDate) ?></strong>
<p>Свежие позиции принимаем по будням утром. В выходные склад не принимает новый товар.</p>
</div>
<div class="temperature-card freezer">
<span>Морозильная камера</span>
<strong><span data-temperature="-18.6" data-min="-19.4" data-max="-17.8">-18.6</span> °C</strong>
<p>Рабочий режим глубокой заморозки.</p>
</div>
<div class="temperature-card chiller">
<span>Холодильная камера</span>
<strong><span data-temperature="2.4" data-min="1.6" data-max="3.8">2.4</span> °C</strong>
<p>Температура для охлажденной продукции.</p>
</div>
</section>
<section class="home-banners" aria-label="Преимущества Рыбсток">
<div class="home-banner">
<div class="home-banner delivery-image">
<strong>Бесплатная доставка внутри МКАД от 5000 руб.</strong>
<span>Привезем заказ после подтверждения по телефону.</span>
</div>
<div class="home-banner">
<div class="home-banner region-image">
<strong>Бесплатная доставка за МКАД от 10000 руб.</strong>
<span>Для адресов до 10 км от МКАД, дальше расчет по расстоянию.</span>
</div>
<div class="home-banner">
<div class="home-banner day-image">
<strong>Можно приобрести день в день</strong>
<span>Привезем заказ день в день, если он оформлен в будний день до 14:00.</span>
</div>
<div class="home-banner">
<div class="home-banner payment-image">
<strong>Без предоплаты</strong>
<span>Оплата после подтверждения заказа и получения товара.</span>
</div>
</section>
<section class="promo-strip" aria-label="Промокоды и цены">
<div>
<span>Промокод</span>
<strong>Рыбсток7000</strong>
<small>300 руб. скидка на заказы от 7 000 руб.</small>
</div>
<div>
<span>Промокод</span>
<strong>Рыбсток20000</strong>
@@ -59,6 +136,7 @@ declare(strict_types=1);
</section>
<section class="price-request-panel" id="price-request">
<div class="price-request-image" aria-hidden="true"></div>
<div>
<p class="eyebrow">Запрос проходных цен</p>
<h2>Пришлите список позиций, а мы сверим цены</h2>
+40
View File
@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
/** @var array<int, array<string, mixed>> $megaCategories */
if (!function_exists('render_mega_category_children')) {
/**
* @param array<int, array<string, mixed>> $children
*/
function render_mega_category_children(array $children, int $depth = 1): void
{
if ($children === [] || $depth > 3) {
return;
}
?>
<ul class="mega-submenu depth-<?= e((string) $depth) ?>">
<?php foreach ($children as $child): ?>
<li>
<a href="/catalog/<?= e((string) $child['slug']) ?>"><?= e((string) $child['name']) ?></a>
<?php render_mega_category_children($child['children'] ?? [], $depth + 1); ?>
</li>
<?php endforeach; ?>
</ul>
<?php
}
}
?>
<?php if ($megaCategories !== []): ?>
<nav class="category-mega-bar" aria-label="Категории товаров">
<div class="category-mega-inner">
<?php foreach ($megaCategories as $category): ?>
<div class="mega-item">
<a class="mega-root" href="/catalog/<?= e((string) $category['slug']) ?>"><?= e((string) $category['name']) ?></a>
<?php render_mega_category_children($category['children'] ?? []); ?>
</div>
<?php endforeach; ?>
</div>
</nav>
<?php endif; ?>