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

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
@@ -0,0 +1,56 @@
# Бренд-стиль Рыбсток
## Идея
Рыбсток - интернет-склад качественных продуктов по стоковым ценам. Визуально бренд должен соединять ощущение надежного продуктового склада, свежей рыбы и понятной выгоды без ощущения дешевого дискаунтера.
## Логотип
Название пишется в одно слово: `РЫБСТОК`.
Внутри слова визуально разделяются две части:
- `РЫБ` - качество, рыба, свежесть, холодная логистика.
- `СТОК` - выгодные стоковые цены, складской формат, рациональная покупка.
Слоган:
`Качественные продукты по стоковым ценам`
Старый слоган `Все море в одном слове` больше не используется.
## Цвета
Основной зеленый:
`#173d2c`
Дополнительный зеленый:
`#27634a`
Акцентный красный:
`#c43d31`
Фон:
`#f5f7f1`
Бумажный белый:
`#fffefa`
Основной текст:
`#17211c`
Вторичный текст:
`#657469`
## Визуальный тон
Сайт должен выглядеть спокойно, дорого и понятно. Без лишней декоративности, без мультяшности, без ощущения ресторана или маркетплейса. Главный характер: склад, надежность, качественная продукция, честная цена. Визуальный язык должен быть светлее и легче, чем классический темный “рыбный” сайт.
## Правила интерфейса
1. Основные действия выделяются зеленым.
2. Красный используется только как акцент выгоды, скидки или части `СТОК`.
3. Карточки остаются с радиусом 8px.
4. Текст должен быть крупным и хорошо читаемым для покупателей 18-75 лет.
5. Сайт строится на одной ширине и единой сетке, чтобы страницы не “плясали”.
6. Логотип не должен слипаться с декоративным знаком: слово и марка должны иметь воздух.
7. Крупные темные блоки используются осторожно; основной интерфейс держится на светлом фоне, тонких границах и спокойных акцентах.
+609 -80
View File
File diff suppressed because it is too large Load Diff
+11
View File
@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 520 260" role="img" aria-labelledby="delivery-title">
<title id="delivery-title">Тестовая иллюстрация доставки</title>
<rect width="520" height="260" rx="22" fill="#fffefa"/>
<rect x="42" y="72" width="300" height="104" rx="18" fill="#173d2c"/>
<rect x="82" y="38" width="150" height="76" rx="16" fill="#27634a"/>
<circle cx="126" cy="188" r="26" fill="#c43d31"/>
<circle cx="296" cy="188" r="26" fill="#c43d31"/>
<path d="M374 102h74l30 42v32H342v-74h32Z" fill="#f1e9dc" stroke="#173d2c" stroke-width="6" stroke-linejoin="round"/>
<path d="M82 126h190" stroke="#fffefa" stroke-width="12" stroke-linecap="round"/>
<path d="M82 152h128" stroke="#d8eadc" stroke-width="10" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 801 B

@@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 520" role="img" aria-labelledby="hero-products-title">
<title id="hero-products-title">Тестовая иллюстрация ассортимента Рыбсток</title>
<rect width="760" height="520" rx="28" fill="#f7f8f4"/>
<rect x="50" y="54" width="660" height="412" rx="26" fill="#fffefa" stroke="#dfe5da" stroke-width="2"/>
<rect x="92" y="94" width="246" height="170" rx="22" fill="#173d2c"/>
<path d="M130 180c44-52 120-52 184 0-64 52-140 52-184 0Z" fill="#fffefa" opacity=".94"/>
<circle cx="166" cy="173" r="7" fill="#c43d31"/>
<path d="M140 300c34 20 71 20 105 0s71-20 105 0" fill="none" stroke="#c43d31" stroke-width="12" stroke-linecap="round"/>
<rect x="382" y="98" width="236" height="88" rx="20" fill="#f1e9dc"/>
<rect x="382" y="212" width="236" height="88" rx="20" fill="#dce8df"/>
<rect x="382" y="326" width="236" height="88" rx="20" fill="#f5dcd6"/>
<circle cx="646" cy="142" r="30" fill="#c43d31" opacity=".9"/>
<circle cx="646" cy="256" r="30" fill="#173d2c" opacity=".9"/>
<circle cx="646" cy="370" r="30" fill="#c43d31" opacity=".9"/>
<path d="M108 390h226" stroke="#173d2c" stroke-width="10" stroke-linecap="round"/>
<path d="M108 422h154" stroke="#9ba99e" stroke-width="10" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+9
View File
@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 520 260" role="img" aria-labelledby="price-title">
<title id="price-title">Тестовая иллюстрация стоковых цен</title>
<rect width="520" height="260" rx="22" fill="#fffefa"/>
<path d="M76 62h250l96 96-78 78H76V62Z" fill="#f7f8f4" stroke="#dfe5da" stroke-width="4"/>
<circle cx="330" cy="120" r="18" fill="#c43d31"/>
<text x="112" y="138" fill="#173d2c" font-family="Arial, Helvetica, sans-serif" font-size="58" font-weight="900">СТОК</text>
<path d="M118 172h160" stroke="#c43d31" stroke-width="10" stroke-linecap="round"/>
<path d="M118 200h96" stroke="#9ba99e" stroke-width="10" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 712 B

+20
View File
@@ -0,0 +1,20 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 390 108" role="img" aria-labelledby="logo-title logo-desc">
<title id="logo-title">Рыбсток</title>
<desc id="logo-desc">РЫБСТОК - качественные продукты по стоковым ценам</desc>
<defs>
<style>
.root{fill:#173d2c;font-family:Arial, Helvetica, sans-serif;font-size:46px;font-weight:900;letter-spacing:.3px}
.stock{fill:#c43d31;font-family:Arial, Helvetica, sans-serif;font-size:46px;font-weight:900;letter-spacing:.3px}
.tag{fill:#173d2c;font-family:Arial, Helvetica, sans-serif;font-size:13px;font-weight:800}
.rule-a{fill:#173d2c}
.rule-b{fill:#c43d31}
</style>
</defs>
<g transform="translate(8 8)">
<text class="root" x="0" y="45">РЫБ</text>
<text class="stock" x="105" y="45">СТОК</text>
<rect class="rule-a" x="2" y="58" width="101" height="4" rx="2"/>
<rect class="rule-b" x="104" y="58" width="121" height="4" rx="2"/>
<text class="tag" x="2" y="86">Качественные продукты по стоковым ценам</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+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>
<?php endif; ?>
<span>РЫБ<span>СТОК</span></span>
<small>Качественные продукты по стоковым ценам</small>
<?php endif; ?>
</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>
+82 -4
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">
<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; ?>