Compare commits

..

12 Commits

Author SHA1 Message Date
Alisa 256afcbb80 доставка в плавающем 2026-06-08 10:58:47 +03:00
Alisa 3a065b34d6 главная добита по визуалу 2026-06-07 23:13:03 +03:00
Alisa 66e931e0d6 js css 2026-06-07 21:14:30 +03:00
Alisa 76bf5d8191 корзина кеш 2026-06-07 17:43:45 +03:00
Alisa 170bea76d5 переключатель цены убран где не нужно 2026-06-07 14:59:33 +03:00
Alisa ee753429e2 поиск+доставка 2026-06-07 13:12:42 +03:00
Alisa 65d182b4af главная сырой слайдер форма запроса цен 2026-06-06 16:59:36 +03:00
Alisa 0b6b1f5d5e лого и фавикон готово 2026-06-06 00:46:10 +03:00
Alisa 4776769782 отображение ненужных цен за кг 2026-06-05 22:50:13 +03:00
Alisa 8f9bc92271 баги с превью 2026-06-05 19:57:23 +03:00
Alisa 98987b5355 превью относительно готово 2026-06-05 19:24:57 +03:00
Alisa c08519263c тут очень много с карточкой товара и импортом. 2026-06-05 18:50:38 +03:00
88 changed files with 10663 additions and 401 deletions
+1
View File
@@ -2,6 +2,7 @@ APP_NAME=RybStock
APP_ENV=local
APP_DEBUG=true
APP_URL=http://localhost
YANDEX_MAPS_API_KEY=9ab8ad56-c350-4a2c-8f23-35acb102ee7a
DB_HOST=localhost
DB_PORT=3306
+101
View File
@@ -0,0 +1,101 @@
# Рыбсток: наработки для брендбука
## Основа бренда
Название: Рыбсток.
Смысл названия: соединение двух идей - рыбная и продуктовая специализация плюс стоковые цены интернет-склада.
Рабочий слоган: "Качество со склада. Стоковые цены."
Короткое позиционирование: интернет-склад качественной рыбы, морепродуктов, мяса, сыров, овощей, ягод и бакалеи по стоковым ценам с доставкой по Москве и Московской области.
## Логотип
Выбранное направление: жирный графичный wordmark без отдельной рисованной рыбы.
Принцип:
- "Рыб" - темно-зеленый блок.
- "сток" - красный блок.
- Стык букв "б/с" - фирменный акцент, который цепляет взгляд и визуально связывает две части названия.
- Подчеркивание разделено по цветам: красная линия под первой частью и зеленая линия под второй.
- Слоган расположен под знаком и является частью фирменного образа.
Рабочий логотип на сайте сейчас встроен inline SVG в `views/layouts/main.php`.
PNG-версии логотипа:
- `public/assets/images/brand/logo-rybstock-320.png`
- `public/assets/images/brand/logo-rybstock-640.png`
- `public/assets/images/brand/logo-rybstock-1280.png`
- `public/assets/images/brand/logo-rybstock-1920.png`
## Favicon
Текущий favicon: монограммный знак на основе выбранного стыка логотипа.
Подключенные файлы:
- `public/favicon-16x16.png`
- `public/favicon-32x32.png`
- `public/apple-touch-icon.png`
- `public/site.webmanifest`
Дополнительные варианты:
- `public/assets/images/brand/favicon-rybstock-monogram-*.png`
- `public/assets/images/brand/favicon-rybstock-fish-*.png`
## Цвета
Основной зеленый: `#173d2c`.
Дополнительный зеленый: `#27634a`.
Акцентный красный: `#c43d31`.
Теплый белый фон: `#fffefa`.
Светлый фон интерфейса: `#f4f6ef`.
Линии и границы: `#dfe5da`.
Приглушенный текст: `#657469`.
## Характер визуала
Стиль: лаконичный, складской, уверенный, современный, без случайной декоративности.
Нельзя:
- рисованная от руки рыба;
- мультяшные элементы;
- слишком тонкие иконки рядом с жирным логотипом;
- перегруженные градиенты;
- ощущение дешевого оптового прайса.
Можно:
- плотная типографика;
- аккуратные тени;
- чистые карточки;
- графичные разделители;
- понятные ценовые акценты;
- фото товара без лишней постановочности.
## Тон коммуникации
Голос бренда: спокойный, уверенный, заботливый, без давления.
Ключевые смыслы:
- качество проверено;
- цена выгодная;
- заказ подтверждается человеком;
- оплатить можно при получении;
- клиенту не нужно самому считать выгоду, сайт пересчитывает цены автоматически.
Пример формулировки: "Добавьте товары в корзину, а сайт сам покажет лучшую цену при выбранном способе оплаты."
## Что доработать позже
- Утвердить финальную favicon-иконку: монограмма или рыба-склад.
- Сделать правила использования логотипа на светлом, темном и фотофоне.
- Подобрать основную шрифтовую пару для сайта и печатных материалов.
- Описать стиль товарных фото и баннеров.
- Сформировать набор фирменных иконок категорий.
- Подготовить шаблон прайс-листа и коммерческого предложения в фирменном стиле.
+36
View File
@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace App\Controllers;
use App\Repositories\ProductRepository;
final class CartController
{
public function sync(): void
{
header('Content-Type: application/json; charset=utf-8');
$payload = json_decode((string) file_get_contents('php://input'), true);
$items = is_array($payload) && isset($payload['items']) && is_array($payload['items'])
? $payload['items']
: [];
if ($items === []) {
echo json_encode(['items' => []], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
return;
}
try {
$snapshots = (new ProductRepository())->cartSnapshots($items);
echo json_encode(['items' => $snapshots], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
} catch (\Throwable) {
http_response_code(500);
echo json_encode([
'items' => [],
'error' => 'cart_sync_failed',
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}
}
}
+204 -4
View File
@@ -9,10 +9,25 @@ use App\Repositories\ProductRepository;
final class CatalogController
{
private ?string $searchBasePath = null;
public function index(): void
{
$categoryRepository = new CategoryRepository();
$productRepository = new ProductRepository();
$pagination = $this->paginationInput();
$searchQuery = trim((string) ($_GET['q'] ?? $this->queryParameter('q')));
$basePath = '/catalog';
if ($searchQuery !== '') {
$searchResult = $this->searchProducts($productRepository, $searchQuery, $pagination['limit'], $pagination['offset']);
$totalProducts = $searchResult['total'];
$products = $searchResult['products'];
$basePath = $this->searchBasePath ?? ($basePath . '?q=' . rawurlencode($searchQuery));
} else {
$totalProducts = $productRepository->catalogTotal(true);
$products = $productRepository->catalogPreview($pagination['limit'], $pagination['offset'], true);
}
view('catalog/index', [
'title' => 'Каталог Рыбсток - рыба, морепродукты, мясо, сыры и продукты',
@@ -24,10 +39,69 @@ final class CatalogController
],
'categories' => $categoryRepository->tree(),
'currentCategory' => null,
'products' => $productRepository->catalogPreview(48),
'products' => $products,
'searchQuery' => $searchQuery,
'pagination' => $this->paginationData($basePath, $totalProducts, count($products), $pagination),
]);
}
public function promo(): void
{
$categoryRepository = new CategoryRepository();
$productRepository = new ProductRepository();
$pagination = $this->paginationInput();
$listing = $productRepository->promoListing($pagination['limit'], $pagination['offset']);
view('catalog/index', [
'title' => 'Акции Рыбсток - продукты по стоковым ценам с доставкой',
'metaDescription' => 'Акционные товары Рыбсток: рыба, морепродукты, мясо, сыры, икра, полуфабрикаты и бакалея по стоковым ценам с доставкой по Москве и Московской области.',
'metaKeywords' => 'акции Рыбсток, скидки на рыбу, продукты по акции Москва, рыба морепродукты мясо сыры по стоковым ценам',
'breadcrumbs' => [
['title' => 'Главная', 'url' => '/'],
['title' => 'Акции'],
],
'categories' => $categoryRepository->tree(),
'currentCategory' => null,
'catalogHeading' => 'Акции',
'catalogEyebrow' => 'Подборка',
'catalogLead' => 'Товары с акционной витриной и перечеркнутой ценой. Итоговая цена в корзине дополнительно зависит от выбранного способа оплаты.',
'products' => $listing['products'],
'pagination' => $this->paginationData('/promo', $listing['total'], count($listing['products']), $pagination),
]);
}
public function newest(): void
{
$categoryRepository = new CategoryRepository();
$productRepository = new ProductRepository();
$pagination = $this->paginationInput();
$listing = $productRepository->newListing($pagination['limit'], $pagination['offset']);
view('catalog/index', [
'title' => 'Новинки Рыбсток - последние поступления на склад',
'metaDescription' => 'Новые поступления Рыбсток: свежие позиции каталога, новые фасовки и товары, недавно добавленные на склад с доставкой по Москве и Московской области.',
'metaKeywords' => 'новинки Рыбсток, новые поступления рыбы, свежие поступления морепродуктов, новые товары склад продуктов Москва',
'breadcrumbs' => [
['title' => 'Главная', 'url' => '/'],
['title' => 'Новинки'],
],
'categories' => $categoryRepository->tree(),
'currentCategory' => null,
'catalogHeading' => 'Новинки',
'catalogEyebrow' => 'Последние поступления',
'catalogLead' => 'Последние добавленные позиции и товары, у которых изменилась фасовка. Новинки помогают быстро увидеть, что недавно появилось на складе.',
'products' => $listing['products'],
'pagination' => $this->paginationData('/new', $listing['total'], count($listing['products']), $pagination),
]);
}
public function search(string $query): void
{
$_GET['q'] = trim($query);
$this->searchBasePath = '/search/' . rawurlencode(trim($query));
$this->index();
}
public function category(string $slug): void
{
$categoryRepository = new CategoryRepository();
@@ -48,6 +122,9 @@ final class CatalogController
}
$categoryName = (string) $category['name'];
$pagination = $this->paginationInput();
$totalProducts = $productRepository->categoryTotal((int) $category['id'], true);
$products = $productRepository->forCategory((int) $category['id'], $pagination['limit'], $pagination['offset'], true);
view('catalog/index', [
'title' => $category['seo_title'] ?: $categoryName . ' - купить в Рыбсток с доставкой по Москве',
@@ -56,7 +133,8 @@ final class CatalogController
'breadcrumbs' => $categoryRepository->breadcrumbs((int) $category['id']),
'categories' => $categoryRepository->tree(),
'currentCategory' => $category,
'products' => $productRepository->forCategory((int) $category['id'], 48),
'products' => $products,
'pagination' => $this->paginationData('/catalog/' . (string) $category['slug'], $totalProducts, count($products), $pagination),
]);
}
@@ -84,8 +162,8 @@ final class CatalogController
['title' => 'Каталог', 'url' => '/catalog'],
];
if (!empty($product['category_id'])) {
$breadcrumbs = $categoryRepository->breadcrumbs((int) $product['category_id']);
if (!empty($product['display_category_id'] ?? $product['category_id'] ?? null)) {
$breadcrumbs = $categoryRepository->breadcrumbs((int) ($product['display_category_id'] ?? $product['category_id']));
}
$breadcrumbs[] = ['title' => (string) $product['name']];
@@ -100,4 +178,126 @@ final class CatalogController
'images' => $productRepository->imagesForProduct((int) $product['id']),
]);
}
/**
* @return array{page:int, limit:int, offset:int, per_page:int, cumulative:bool}
*/
private function paginationInput(): array
{
$perPage = 48;
$cumulativeLimit = isset($_GET['limit']) ? (int) $_GET['limit'] : 0;
if ($cumulativeLimit > 0) {
$limit = max($perPage, min($cumulativeLimit, 5000));
return [
'page' => 1,
'limit' => $limit,
'offset' => 0,
'per_page' => $perPage,
'cumulative' => true,
];
}
$page = max(1, (int) ($_GET['page'] ?? 1));
return [
'page' => $page,
'limit' => $perPage,
'offset' => ($page - 1) * $perPage,
'per_page' => $perPage,
'cumulative' => false,
];
}
private function queryParameter(string $name): string
{
$query = (string) (parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_QUERY) ?? '');
if ($query === '') {
return '';
}
parse_str($query, $parameters);
return (string) ($parameters[$name] ?? '');
}
/**
* @return array{products: array<int, array<string, mixed>>, total: int}
*/
private function searchProducts(ProductRepository $productRepository, string $query, int $limit, int $offset): array
{
if (method_exists($productRepository, 'searchCatalog')) {
return $productRepository->searchCatalog($query, $limit, $offset, true);
}
$needle = $this->lower($query);
$words = array_values(array_filter(preg_split('/\s+/u', $needle) ?: []));
$ranked = [];
foreach ($productRepository->catalogPreview(5000, 0, true) as $product) {
$haystack = $this->lower(implode(' ', [
(string) ($product['name'] ?? ''),
(string) ($product['short_description'] ?? ''),
(string) ($product['category_name'] ?? ''),
(string) ($product['category_display_name'] ?? ''),
]));
$score = 0;
if ($needle !== '' && str_contains($haystack, $needle)) {
$score += 100;
}
foreach ($words as $word) {
if ($word !== '' && str_contains($haystack, $word)) {
$score += 20;
}
}
if ($score > 0) {
$product['_search_score'] = $score;
$ranked[] = $product;
}
}
usort($ranked, static fn (array $left, array $right): int => ($right['_search_score'] ?? 0) <=> ($left['_search_score'] ?? 0));
foreach ($ranked as &$product) {
unset($product['_search_score']);
}
unset($product);
return [
'products' => array_slice($ranked, $offset, $limit),
'total' => count($ranked),
];
}
private function lower(string $value): string
{
return function_exists('mb_strtolower') ? mb_strtolower($value, 'UTF-8') : strtolower($value);
}
/**
* @param array{page:int, limit:int, offset:int, per_page:int, cumulative:bool} $input
* @return array<string, mixed>
*/
private function paginationData(string $basePath, int $total, int $currentCount, array $input): array
{
$shown = min($total, $input['offset'] + $currentCount);
$nextLimit = min($total, max($shown, $input['limit']) + $input['per_page']);
return [
'base_path' => $basePath,
'total' => $total,
'shown' => $shown,
'page' => $input['page'],
'pages' => max(1, (int) ceil($total / $input['per_page'])),
'per_page' => $input['per_page'],
'limit' => $input['limit'],
'has_more' => $shown < $total,
'next_limit' => $nextLimit,
'cumulative' => $input['cumulative'],
];
}
}
+100 -1
View File
@@ -18,6 +18,7 @@ final class HomeController
$categories = [];
$catalogProducts = [];
$popularProducts = [];
$homeCatalogSections = [];
$settings = [];
$notice = null;
@@ -25,10 +26,27 @@ final class HomeController
Database::pdo()->query('SELECT 1');
$dbStatus = 'connected';
$categories = (new CategoryRepository())->tree();
$categoryRepository = new CategoryRepository();
$categories = $categoryRepository->tree();
$productRepository = new ProductRepository();
$catalogProducts = $productRepository->catalogPreview(24);
$popularProducts = $productRepository->popularPreview(10);
$homeCatalogSections[] = [
'key' => 'promos',
'title' => 'Акции',
'url' => '/promo',
'count' => 50,
'kind' => 'promo',
'products' => $productRepository->promoPreview(10),
];
$homeCatalogSections[] = [
'key' => 'new',
'title' => 'Новинки',
'url' => '/new',
'count' => 50,
'kind' => 'new',
'products' => $productRepository->newPreview(10),
];
$settings = (new SettingRepository())->allKeyed();
} catch (Throwable $exception) {
$dbStatus = app_env('APP_DEBUG', 'false') === 'true'
@@ -49,9 +67,90 @@ final class HomeController
'categories' => $categories,
'catalogProducts' => $catalogProducts,
'popularProducts' => $popularProducts,
'homeCatalogSections' => $homeCatalogSections,
'settings' => $settings,
'notice' => $notice,
'buildVersion' => '2026-06-04-02',
]);
}
/**
* @param array<int, array<string, mixed>> $categoryTree
* @param array<int, string> $slugs
* @return array{category: array<string, mixed>, showcase: array{products: array<int, array<string, mixed>>, total: int}}|null
*/
private function homeCategoryShowcase(
CategoryRepository $categoryRepository,
ProductRepository $productRepository,
array $categoryTree,
array $slugs,
string $title
): ?array {
$candidates = [];
$seenIds = [];
foreach ($slugs as $slug) {
$category = $categoryRepository->findBySlug($slug);
if ($category !== null) {
$categoryId = (int) $category['id'];
$seenIds[$categoryId] = true;
$candidates[] = $category;
}
}
foreach ($this->findCategoriesByName($categoryTree, $title) as $category) {
$categoryId = (int) $category['id'];
if (isset($seenIds[$categoryId])) {
continue;
}
$seenIds[$categoryId] = true;
$candidates[] = $category;
}
$fallback = null;
foreach ($candidates as $category) {
$showcase = $productRepository->categoryRetailShowcase((int) $category['id'], 10);
$match = [
'category' => $category,
'showcase' => $showcase,
];
if ($showcase['products'] !== []) {
return $match;
}
$fallback ??= $match;
}
return $fallback;
}
/**
* @param array<int, array<string, mixed>> $categories
* @return array<int, array<string, mixed>>
*/
private function findCategoriesByName(array $categories, string $name): array
{
$matches = [];
$normalizedName = $this->normalizeCategoryName($name);
foreach ($categories as $category) {
if ($this->normalizeCategoryName((string) ($category['name'] ?? '')) === $normalizedName) {
$matches[] = $category;
}
$matches = array_merge(
$matches,
$this->findCategoriesByName($category['children'] ?? [], $name)
);
}
return $matches;
}
private function normalizeCategoryName(string $name): string
{
return trim(str_replace('ё', 'е', mb_strtolower($name)));
}
}
+181
View File
@@ -0,0 +1,181 @@
<?php
declare(strict_types=1);
namespace App\Controllers;
use App\Core\Database;
use App\Repositories\SettingRepository;
use Throwable;
final class PriceRequestController
{
public function store(): void
{
$companyName = trim((string) ($_POST['company_name'] ?? ''));
$paymentType = trim((string) ($_POST['payment_type'] ?? ''));
$phone = trim((string) ($_POST['phone'] ?? ''));
$email = trim((string) ($_POST['email'] ?? ''));
$activityType = trim((string) ($_POST['activity_type'] ?? ''));
$requestText = trim((string) ($_POST['request_text'] ?? ''));
$errors = [];
if ($companyName === '') {
$errors[] = 'company';
}
if (!in_array($paymentType, ['cash', 'invoice'], true)) {
$errors[] = 'payment';
}
if ($phone === '' && $email === '') {
$errors[] = 'contact';
}
if ($requestText === '') {
$errors[] = 'request';
}
if ($errors !== []) {
$this->redirect('error');
}
try {
$uploadedFiles = $this->storeUploadedFiles();
$fullRequestText = $this->buildRequestText($paymentType, $activityType, $requestText, $uploadedFiles);
$statement = Database::pdo()->prepare(
'INSERT INTO price_requests
(customer_name, phone, email, company_name, request_text, file_path, status, client_ip, user_agent)
VALUES
(:customer_name, :phone, :email, :company_name, :request_text, :file_path, :status, :client_ip, :user_agent)'
);
$statement->execute([
'customer_name' => $companyName,
'phone' => $phone !== '' ? $phone : null,
'email' => $email !== '' ? $email : null,
'company_name' => $companyName,
'request_text' => $fullRequestText,
'file_path' => $uploadedFiles[0] ?? null,
'status' => 'new',
'client_ip' => $_SERVER['REMOTE_ADDR'] ?? null,
'user_agent' => substr((string) ($_SERVER['HTTP_USER_AGENT'] ?? ''), 0, 255),
]);
$requestId = (int) Database::pdo()->lastInsertId();
$this->sendNotification($requestId, $companyName, $phone, $email, $fullRequestText);
} catch (Throwable) {
$this->redirect('error');
}
$this->redirect('sent');
}
/**
* @return array<int, string>
*/
private function storeUploadedFiles(): array
{
if (!isset($_FILES['request_files']) || !is_array($_FILES['request_files']['name'])) {
return [];
}
$stored = [];
$uploadDir = base_path('storage/price_requests/' . date('Y/m'));
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0775, true);
}
foreach ($_FILES['request_files']['name'] as $index => $originalName) {
$error = (int) ($_FILES['request_files']['error'][$index] ?? UPLOAD_ERR_NO_FILE);
if ($error === UPLOAD_ERR_NO_FILE) {
continue;
}
if ($error !== UPLOAD_ERR_OK) {
continue;
}
$tmpName = (string) ($_FILES['request_files']['tmp_name'][$index] ?? '');
if ($tmpName === '' || !is_uploaded_file($tmpName)) {
continue;
}
$extension = strtolower(pathinfo((string) $originalName, PATHINFO_EXTENSION));
$safeExtension = preg_replace('/[^a-z0-9]/', '', $extension) ?: 'file';
$fileName = date('Ymd-His') . '-' . bin2hex(random_bytes(4)) . '.' . $safeExtension;
$targetPath = $uploadDir . '/' . $fileName;
if (move_uploaded_file($tmpName, $targetPath)) {
$stored[] = str_replace('\\', '/', substr($targetPath, strlen(base_path()) + 1));
}
}
return $stored;
}
/**
* @param array<int, string> $uploadedFiles
*/
private function buildRequestText(string $paymentType, string $activityType, string $requestText, array $uploadedFiles): string
{
$paymentLabel = $paymentType === 'invoice' ? 'Расчетный счет' : 'Наличные';
$lines = [
'Форма оплаты: ' . $paymentLabel,
'Тип деятельности: ' . ($activityType !== '' ? $activityType : 'не указан'),
'',
'Позиции, объем и нужная цена:',
$requestText,
];
if ($uploadedFiles !== []) {
$lines[] = '';
$lines[] = 'Файлы:';
foreach ($uploadedFiles as $path) {
$lines[] = '- ' . $path;
}
}
return implode("\n", $lines);
}
private function sendNotification(int $requestId, string $companyName, string $phone, string $email, string $requestText): void
{
$settings = (new SettingRepository())->allKeyed();
$to = trim((string) (($settings['price_request_email'] ?? '') ?: ($settings['admin_email'] ?? '') ?: 'zakaz@rybstock.ru'));
if ($to === '') {
return;
}
$subject = 'Запрос проходных цен #' . $requestId . ' - ' . $companyName;
$body = implode("\n", [
'Новая бизнес-заявка на проходные цены.',
'',
'Номер заявки: ' . $requestId,
'Организация: ' . $companyName,
'Телефон: ' . ($phone !== '' ? $phone : 'не указан'),
'Email: ' . ($email !== '' ? $email : 'не указан'),
'',
$requestText,
]);
$headers = [
'Content-Type: text/plain; charset=UTF-8',
'From: Рыбсток <no-reply@rybstock.ru>',
];
@mail($to, $subject, $body, implode("\r\n", $headers));
}
private function redirect(string $status): never
{
header('Location: /?price_request=' . rawurlencode($status) . '#price-request', true, 303);
exit;
}
}
@@ -0,0 +1,154 @@
<?php
declare(strict_types=1);
namespace App\Controllers;
use App\Core\Database;
use App\Repositories\SettingRepository;
use Throwable;
final class WishlistRequestController
{
public function store(): void
{
$customerName = trim((string) ($_POST['customer_name'] ?? ''));
$phone = trim((string) ($_POST['phone'] ?? ''));
$email = trim((string) ($_POST['email'] ?? ''));
$requestText = trim((string) ($_POST['request_text'] ?? ''));
if ($customerName === '' || $requestText === '' || ($phone === '' && $email === '')) {
$this->redirect('error');
}
try {
$uploadedFiles = $this->storeUploadedFiles();
$fullRequestText = $this->buildRequestText($requestText, $uploadedFiles);
$statement = Database::pdo()->prepare(
'INSERT INTO price_requests
(customer_name, phone, email, company_name, request_text, file_path, status, client_ip, user_agent)
VALUES
(:customer_name, :phone, :email, :company_name, :request_text, :file_path, :status, :client_ip, :user_agent)'
);
$statement->execute([
'customer_name' => $customerName,
'phone' => $phone !== '' ? $phone : null,
'email' => $email !== '' ? $email : null,
'company_name' => 'Запрос позиции',
'request_text' => $fullRequestText,
'file_path' => $uploadedFiles[0] ?? null,
'status' => 'new',
'client_ip' => $_SERVER['REMOTE_ADDR'] ?? null,
'user_agent' => substr((string) ($_SERVER['HTTP_USER_AGENT'] ?? ''), 0, 255),
]);
$requestId = (int) Database::pdo()->lastInsertId();
$this->sendNotification($requestId, $customerName, $phone, $email, $fullRequestText);
} catch (Throwable $exception) {
$this->redirect('error');
}
$this->redirect('sent');
}
/**
* @return array<int, string>
*/
private function storeUploadedFiles(): array
{
if (!isset($_FILES['wishlist_files']) || !is_array($_FILES['wishlist_files']['name'])) {
return [];
}
$stored = [];
$uploadDir = base_path('storage/wishlist_requests/' . date('Y/m'));
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0775, true);
}
foreach ($_FILES['wishlist_files']['name'] as $index => $originalName) {
$error = (int) ($_FILES['wishlist_files']['error'][$index] ?? UPLOAD_ERR_NO_FILE);
if ($error === UPLOAD_ERR_NO_FILE || $error !== UPLOAD_ERR_OK) {
continue;
}
$tmpName = (string) ($_FILES['wishlist_files']['tmp_name'][$index] ?? '');
if ($tmpName === '' || !is_uploaded_file($tmpName)) {
continue;
}
$extension = strtolower(pathinfo((string) $originalName, PATHINFO_EXTENSION));
$safeExtension = preg_replace('/[^a-z0-9]/', '', $extension) ?: 'file';
$fileName = date('Ymd-His') . '-' . bin2hex(random_bytes(4)) . '.' . $safeExtension;
$targetPath = $uploadDir . '/' . $fileName;
if (move_uploaded_file($tmpName, $targetPath)) {
$stored[] = str_replace('\\', '/', substr($targetPath, strlen(base_path()) + 1));
}
}
return $stored;
}
/**
* @param array<int, string> $uploadedFiles
*/
private function buildRequestText(string $requestText, array $uploadedFiles): string
{
$lines = [
'Тип заявки: поиск позиции / виш-лист.',
'',
'Что нужно найти:',
$requestText,
];
if ($uploadedFiles !== []) {
$lines[] = '';
$lines[] = 'Файлы:';
foreach ($uploadedFiles as $path) {
$lines[] = '- ' . $path;
}
}
return implode("\n", $lines);
}
private function sendNotification(int $requestId, string $customerName, string $phone, string $email, string $requestText): void
{
$settings = (new SettingRepository())->allKeyed();
$to = trim((string) (($settings['price_request_email'] ?? '') ?: ($settings['admin_email'] ?? '') ?: 'zakaz@rybstock.ru'));
if ($to === '') {
return;
}
$subject = 'Запрос позиции #' . $requestId . ' - ' . $customerName;
$body = implode("\n", [
'Новый запрос на поиск позиции.',
'',
'Номер заявки: ' . $requestId,
'Имя: ' . $customerName,
'Телефон: ' . ($phone !== '' ? $phone : 'не указан'),
'Email: ' . ($email !== '' ? $email : 'не указан'),
'',
$requestText,
]);
$headers = [
'Content-Type: text/plain; charset=UTF-8',
'From: Рыбсток <no-reply@rybstock.ru>',
];
@mail($to, $subject, $body, implode("\r\n", $headers));
}
private function redirect(string $status): never
{
header('Location: /?wishlist_request=' . rawurlencode($status) . '#wishlist-request', true, 303);
exit;
}
}
+5
View File
@@ -18,6 +18,11 @@ final class Router
$this->routes[] = ['method' => 'GET', 'pattern' => $pattern, 'handler' => $handler];
}
public function post(string $pattern, callable $handler): void
{
$this->routes[] = ['method' => 'POST', 'pattern' => $pattern, 'handler' => $handler];
}
public function dispatch(string $method, string $uri): void
{
$path = parse_url($uri, PHP_URL_PATH) ?: '/';
+42
View File
@@ -49,9 +49,51 @@ final class CategoryRepository extends BaseRepository
$childrenByParent[$parentKey][] = $item;
}
$childrenByParent[0] = $this->sortRootCategories($childrenByParent[0] ?? []);
return $this->buildTree($childrenByParent, 0);
}
/**
* @param array<int, array<string, mixed>> $categories
* @return array<int, array<string, mixed>>
*/
private function sortRootCategories(array $categories): array
{
usort($categories, function (array $left, array $right): int {
return [
$this->rootCategoryWeight((string) ($left['name'] ?? '')),
(int) ($left['sort_order'] ?? 100),
(string) ($left['name'] ?? ''),
] <=> [
$this->rootCategoryWeight((string) ($right['name'] ?? '')),
(int) ($right['sort_order'] ?? 100),
(string) ($right['name'] ?? ''),
];
});
return $categories;
}
private function rootCategoryWeight(string $name): int
{
$lowerName = trim(mb_strtolower($name));
return match (true) {
str_contains($lowerName, 'рыб') => 10,
str_contains($lowerName, 'морепроду') => 20,
str_contains($lowerName, 'икра') => 30,
$lowerName === 'мясо' => 40,
str_contains($lowerName, 'сыр') => 50,
str_contains($lowerName, 'полуфаб') => 60,
str_contains($lowerName, 'фрукт') || str_contains($lowerName, 'овощ') || str_contains($lowerName, 'ягод') || str_contains($lowerName, 'гриб') => 70,
str_contains($lowerName, 'бакале') => 80,
str_contains($lowerName, 'мясная гастрономия') => 90,
str_contains($lowerName, 'молоч') => 100,
default => 1000,
};
}
/**
* @param array<int, array<int, array<string, mixed>>> $childrenByParent
* @return array<int, array<string, mixed>>
+571 -17
View File
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Repositories;
use App\Services\PriceTierService;
use App\Services\SmartSearchService;
final class ProductRepository extends BaseRepository
{
@@ -27,12 +28,150 @@ final class ProductRepository extends BaseRepository
/**
* @return array<int, array<string, mixed>>
*/
public function catalogPreview(int $limit = 24): array
public function promoPreview(int $limit = 10): array
{
$limit = max(1, min($limit, 60));
return array_slice($this->promoProducts(max(1, min($limit, 10))), 0, max(1, min($limit, 10)));
}
/**
* @return array<int, array<string, mixed>>
*/
private function promoProducts(int $limit = 50): array
{
$limit = max(1, min($limit, 50));
$products = $this->manualPromoPreview($limit);
$usedIds = array_flip(array_map(static fn (array $product): int => (int) ($product['id'] ?? 0), $products));
$fallbackProducts = $this->catalogPreview(5000);
$fallbackProducts = array_values(array_filter(array_map(
fn (array $product): ?array => $this->withRetailPreviewVariant($product, 6.0),
$fallbackProducts
)));
usort($fallbackProducts, static function (array $left, array $right): int {
return (int) sprintf('%u', crc32('promo|' . ($left['id'] ?? 0)))
<=> (int) sprintf('%u', crc32('promo|' . ($right['id'] ?? 0)));
});
foreach ($fallbackProducts as $fallbackProduct) {
$fallbackId = (int) ($fallbackProduct['id'] ?? 0);
if (isset($usedIds[$fallbackId])) {
continue;
}
$products[] = $fallbackProduct;
if (count($products) >= $limit) {
break;
}
}
return array_map(
fn (array $product): array => $this->applyPromoPricing($product),
array_slice($products, 0, $limit)
);
}
/**
* @return array{products: array<int, array<string, mixed>>, total: int}
*/
public function promoListing(int $limit = 48, int $offset = 0): array
{
$products = $this->promoProducts(50);
$total = count($products);
return [
'products' => array_slice($products, max(0, $offset), max(1, min($limit, 48))),
'total' => $total,
];
}
/**
* @return array<int, array<string, mixed>>
*/
public function newPreview(int $limit = 10): array
{
$limit = max(1, min($limit, 50));
$sql = $this->previewSelectSql(
'ORDER BY p.sort_order, p.name',
$limit
'ORDER BY p.created_at DESC, p.id DESC',
$limit,
0,
false
);
$statement = $this->pdo()->query($sql);
$products = $this->preparePreviewProducts($statement->fetchAll());
foreach ($products as &$product) {
$product['badge_label'] = 'Новинка';
$product['badge_kind'] = 'new';
}
unset($product);
return $products;
}
/**
* @return array{products: array<int, array<string, mixed>>, total: int}
*/
public function newListing(int $limit = 48, int $offset = 0): array
{
$products = $this->newPreview(50);
$total = count($products);
return [
'products' => array_slice($products, max(0, $offset), max(1, min($limit, 48))),
'total' => $total,
];
}
/**
* @return array<int, array<string, mixed>>
*/
public function categoryRetailPreview(int $categoryId, int $limit = 10, float $maxPackageQuantity = 6.0): array
{
return $this->categoryRetailShowcase($categoryId, $limit, $maxPackageQuantity)['products'];
}
/**
* @return array{products: array<int, array<string, mixed>>, total: int}
*/
public function categoryRetailShowcase(int $categoryId, int $limit = 10, float $maxPackageQuantity = 6.0): array
{
$products = $this->forCategory($categoryId, 5000, 0, false);
$total = count($products);
$products = array_values(array_filter(array_map(
fn (array $product): ?array => $this->withRetailPreviewVariant($product, $maxPackageQuantity),
$products
)));
usort($products, static function (array $left, array $right): int {
return [
(int) sprintf('%u', crc32('home|' . ($left['id'] ?? 0))),
(string) ($left['name'] ?? ''),
] <=> [
(int) sprintf('%u', crc32('home|' . ($right['id'] ?? 0))),
(string) ($right['name'] ?? ''),
];
});
return [
'products' => array_slice($products, 0, max(1, min($limit, 10))),
'total' => $total,
];
}
/**
* @return array<int, array<string, mixed>>
*/
public function catalogPreview(int $limit = 24, int $offset = 0, bool $includeHidden = false): array
{
$limit = max(1, min($limit, 5000));
$offset = max(0, $offset);
$sql = $this->previewSelectSql(
'ORDER BY (p.is_published = 0 OR p.is_available = 0), p.sort_order, p.name',
$limit,
$offset,
$includeHidden
);
$statement = $this->pdo()->query($sql);
@@ -40,32 +179,120 @@ final class ProductRepository extends BaseRepository
return $this->preparePreviewProducts($statement->fetchAll());
}
public function catalogTotal(bool $includeHidden = false): int
{
$statusSql = $includeHidden ? '' : ' AND p.is_published = 1 AND p.is_available = 1';
$statement = $this->pdo()->query('SELECT COUNT(*) FROM products p WHERE 1=1' . $statusSql);
return (int) $statement->fetchColumn();
}
/**
* @return array<int, array{slug: string, updated_at: string|null, published_at: string|null, created_at: string|null}>
*/
public function sitemapProducts(int $limit = 10000): array
{
$statement = $this->pdo()->query(
'SELECT slug, updated_at, published_at, created_at
FROM products
WHERE is_published = 1
AND is_available = 1
ORDER BY updated_at DESC, published_at DESC, created_at DESC, id DESC
LIMIT ' . max(1, min($limit, 10000))
);
return $statement->fetchAll();
}
/**
* @return array{products: array<int, array<string, mixed>>, total: int}
*/
public function searchCatalog(string $query, int $limit = 48, int $offset = 0, bool $includeHidden = false): array
{
$limit = max(1, min($limit, 5000));
$offset = max(0, $offset);
$products = $this->catalogPreview(5000, 0, $includeHidden);
$rankedProducts = (new SmartSearchService())->rankProducts($query, $products);
return [
'products' => array_slice($rankedProducts, $offset, $limit),
'total' => count($rankedProducts),
];
}
/**
* @return array<int, array<string, mixed>>
*/
public function forCategory(int $categoryId, int $limit = 48): array
public function forCategory(int $categoryId, int $limit = 48, int $offset = 0, bool $includeHidden = false): array
{
$limit = max(1, min($limit, 96));
$limit = max(1, min($limit, 5000));
$offset = max(0, $offset);
$categoryIds = (new CategoryRepository())->descendantIds($categoryId);
$categoryParents = $this->categoryParentMap();
$requestedRootId = $this->rootCategoryId($categoryId, $categoryParents);
$isRootCategory = ($categoryParents[$categoryId] ?? null) === null;
$placeholders = implode(',', array_fill(0, count($categoryIds), '?'));
$sql = $this->previewSelectSql(
'AND (
p.category_id IN (' . $placeholders . ')
OR EXISTS (
EXISTS (
SELECT 1
FROM product_category_links pcl
WHERE pcl.product_id = p.id
AND pcl.category_id IN (' . $placeholders . ')
)
OR (
p.category_id IN (' . $placeholders . ')
AND (p.legacy_path IS NULL OR p.legacy_path NOT LIKE \'/katalog/product/view/1/%\')
AND NOT EXISTS (
SELECT 1
FROM product_category_links pcl_any
WHERE pcl_any.product_id = p.id
)
)
)
ORDER BY p.sort_order, p.name',
$limit
ORDER BY (p.is_published = 0 OR p.is_available = 0), p.sort_order, p.name',
5000,
0,
$includeHidden
);
$statement = $this->pdo()->prepare($sql);
$statement->execute(array_merge($categoryIds, $categoryIds));
return $this->preparePreviewProducts($statement->fetchAll());
$categoryLookup = array_flip($categoryIds);
$products = $this->preparePreviewProducts($statement->fetchAll());
$filtered = [];
foreach ($products as $product) {
$leafCategoryIds = $product['category_display_ids'] ?? [];
$leafRootIds = $product['category_display_root_ids'] ?? [];
if ($leafCategoryIds === []) {
if (isset($categoryLookup[(int) ($product['category_id'] ?? 0)])) {
$filtered[] = $product;
}
} elseif (
array_intersect($leafCategoryIds, $categoryIds) !== []
&& (!$isRootCategory || array_values(array_unique($leafRootIds)) === [$requestedRootId])
) {
$filtered[] = $product;
}
}
usort($filtered, static function (array $left, array $right): int {
$leftUnavailable = ((int) ($left['is_published'] ?? 1) === 0 || (int) ($left['is_available'] ?? 1) === 0) ? 1 : 0;
$rightUnavailable = ((int) ($right['is_published'] ?? 1) === 0 || (int) ($right['is_available'] ?? 1) === 0) ? 1 : 0;
return [$leftUnavailable, (int) ($left['sort_order'] ?? 0), (string) ($left['name'] ?? '')]
<=> [$rightUnavailable, (int) ($right['sort_order'] ?? 0), (string) ($right['name'] ?? '')];
});
return array_slice($filtered, $offset, $limit);
}
public function categoryTotal(int $categoryId, bool $includeHidden = false): int
{
return count($this->forCategory($categoryId, 5000, 0, $includeHidden));
}
/**
@@ -74,9 +301,23 @@ final class ProductRepository extends BaseRepository
public function findBySlug(string $slug): ?array
{
$statement = $this->pdo()->prepare(
'SELECT p.*, c.name AS category_name, c.slug AS category_slug
'SELECT
p.*,
COALESCE(linked_c.id, c.id) AS display_category_id,
COALESCE(linked_c.name, c.name) AS category_name,
COALESCE(linked_c.slug, c.slug) AS category_slug
FROM products p
LEFT JOIN categories c ON c.id = p.category_id
LEFT JOIN categories linked_c
ON linked_c.id = (
SELECT pcl.category_id
FROM product_category_links pcl
INNER JOIN categories lc ON lc.id = pcl.category_id
WHERE pcl.product_id = p.id
AND lc.is_active = 1
ORDER BY lc.parent_id IS NULL, pcl.sort_order, pcl.category_id
LIMIT 1
)
WHERE p.slug = :slug
AND p.is_published = 1
AND p.is_available = 1
@@ -103,7 +344,17 @@ final class ProductRepository extends BaseRepository
);
$statement->execute(['product_id' => $productId]);
return $statement->fetchAll();
return array_map(
static function (array $image): array {
$path = (string) ($image['path'] ?? '');
$image['preview_path'] = \function_exists('optimized_image_path')
? \optimized_image_path($path, 'preview')
: $path;
return $image;
},
$statement->fetchAll()
);
}
/**
@@ -122,6 +373,147 @@ final class ProductRepository extends BaseRepository
return $statement->fetchAll();
}
/**
* @param array<int, array<string, mixed>> $items
* @return array<int, array<string, mixed>>
*/
public function cartSnapshots(array $items): array
{
$result = [];
foreach ($items as $item) {
$key = (string) ($item['key'] ?? '');
$productId = (int) ($item['productId'] ?? 0);
$variantId = (int) ($item['variantId'] ?? 0);
$desiredPackageQuantity = (float) ($item['packageQuantity'] ?? 0);
if ($key === '' || $productId <= 0) {
continue;
}
$variantJoin = $variantId > 0
? 'v.product_id = p.id AND v.id = :variant_id'
: 'v.id = (
SELECT pv.id
FROM product_variants pv
WHERE pv.product_id = p.id
ORDER BY pv.is_default DESC, pv.sort_order, pv.id
LIMIT 1
)';
$statement = $this->pdo()->prepare(
'SELECT
p.id AS product_id,
p.name,
p.slug,
p.main_image_path,
p.is_published AS product_is_published,
p.is_available AS product_is_available,
v.id AS variant_id,
v.name AS variant_name,
v.unit,
v.package_quantity,
v.price_per_unit,
v.package_price,
v.is_published AS variant_is_published,
v.is_available AS variant_is_available
FROM products p
LEFT JOIN product_variants v ON ' . $variantJoin . '
WHERE p.id = :product_id
LIMIT 1'
);
$params = ['product_id' => $productId];
if ($variantId > 0) {
$params['variant_id'] = $variantId;
}
$statement->execute($params);
$row = $statement->fetch();
if (!$row) {
$result[] = [
'key' => $key,
'isAvailable' => false,
'reason' => 'Товар больше не найден в каталоге.',
];
continue;
}
if (empty($row['variant_id'])) {
$fallbackStatement = $this->pdo()->prepare(
'SELECT
p.id AS product_id,
p.name,
p.slug,
p.main_image_path,
p.is_published AS product_is_published,
p.is_available AS product_is_available,
v.id AS variant_id,
v.name AS variant_name,
v.unit,
v.package_quantity,
v.price_per_unit,
v.package_price,
v.is_published AS variant_is_published,
v.is_available AS variant_is_available
FROM products p
INNER JOIN product_variants v ON v.product_id = p.id
WHERE p.id = :product_id
ORDER BY
(v.is_published = 1 AND v.is_available = 1 AND v.package_price > 0) DESC,
CASE WHEN :package_quantity_check > 0 THEN ABS(v.package_quantity - :package_quantity) ELSE 0 END,
v.is_default DESC,
v.sort_order,
v.id
LIMIT 1'
);
$fallbackStatement->execute([
'product_id' => $productId,
'package_quantity' => $desiredPackageQuantity,
'package_quantity_check' => $desiredPackageQuantity,
]);
$fallbackRow = $fallbackStatement->fetch();
if ($fallbackRow) {
$row = $fallbackRow;
}
}
$productIsAvailable = (int) ($row['product_is_published'] ?? 0) === 1
&& (int) ($row['product_is_available'] ?? 0) === 1;
$variantIsAvailable = !empty($row['variant_id'])
&& (int) ($row['variant_is_published'] ?? 0) === 1
&& (int) ($row['variant_is_available'] ?? 0) === 1
&& (float) ($row['package_price'] ?? 0) > 0;
$reason = '';
if (!$productIsAvailable) {
$reason = 'Товар сейчас не опубликован или отсутствует в наличии.';
} elseif (!$variantIsAvailable) {
$reason = 'Выбранная фасовка сейчас недоступна.';
}
$result[] = [
'key' => $key,
'productId' => (int) $row['product_id'],
'variantId' => !empty($row['variant_id']) ? (int) $row['variant_id'] : $variantId,
'name' => (string) ($row['name'] ?? ''),
'slug' => (string) ($row['slug'] ?? ''),
'image' => \function_exists('optimized_image_path')
? \optimized_image_path((string) ($row['main_image_path'] ?? ''), 'preview')
: (string) ($row['main_image_path'] ?? ''),
'variantName' => (string) ($row['variant_name'] ?? 'Фасовка'),
'unit' => (string) ($row['unit'] ?? ''),
'packageQuantity' => (float) ($row['package_quantity'] ?? 0),
'baseUnitPrice' => (float) ($row['price_per_unit'] ?? 0),
'basePackagePrice' => (float) ($row['package_price'] ?? 0),
'isAvailable' => $productIsAvailable && $variantIsAvailable,
'reason' => $reason,
];
}
return $result;
}
/**
* @return array<int, array<string, mixed>>
*/
@@ -203,7 +595,7 @@ final class ProductRepository extends BaseRepository
}
}
private function previewSelectSql(string $tailSql, int $limit): string
private function previewSelectSql(string $tailSql, int $limit, int $offset = 0, bool $includeHidden = false): string
{
return 'SELECT
p.id,
@@ -214,12 +606,16 @@ final class ProductRepository extends BaseRepository
p.seo_title,
p.seo_description,
p.seo_keywords,
p.category_id,
p.is_published,
p.is_available,
c.name AS category_name,
c.slug AS category_slug,
p.main_image_path,
p.base_unit,
p.package_display_mode,
p.sales_count,
p.sort_order,
v.id AS variant_id,
v.name AS variant_name,
v.unit,
@@ -241,10 +637,131 @@ final class ProductRepository extends BaseRepository
ORDER BY pv.is_default DESC, pv.sort_order, pv.id
LIMIT 1
)
WHERE p.is_published = 1
AND p.is_available = 1
WHERE 1=1
' . ($includeHidden ? '' : 'AND p.is_published = 1 AND p.is_available = 1') . '
' . $tailSql . '
LIMIT ' . $limit;
LIMIT ' . $limit . ' OFFSET ' . $offset;
}
/**
* @param array<string, mixed> $product
* @return array<string, mixed>|null
*/
private function withRetailPreviewVariant(array $product, float $maxPackageQuantity): ?array
{
$variants = array_values(array_filter(
$product['preview_variants'] ?? [],
static function (array $variant) use ($maxPackageQuantity): bool {
$packagePrice = (float) ($variant['package_price'] ?? 0);
$packageQuantity = (float) ($variant['package_quantity'] ?? 0);
return $packagePrice > 0
&& $packageQuantity > 0
&& $packageQuantity <= $maxPackageQuantity;
}
));
if ($variants === []) {
return null;
}
$product['preview_variants'] = $variants;
return $this->syncProductVariantFields($product, $variants[0]);
}
/**
* @return array<int, array<string, mixed>>
*/
private function manualPromoPreview(int $limit): array
{
$statement = $this->pdo()->query(
'SELECT product_id
FROM promo_block_items
WHERE is_active = 1
ORDER BY sort_order, id
LIMIT ' . max(1, min($limit, 10))
);
$productIds = array_map('intval', $statement->fetchAll(\PDO::FETCH_COLUMN));
if ($productIds === []) {
return [];
}
$placeholders = implode(',', array_fill(0, count($productIds), '?'));
$sql = $this->previewSelectSql(
'AND p.id IN (' . $placeholders . ')
ORDER BY FIELD(p.id, ' . $placeholders . ')',
count($productIds),
0,
false
);
$productsStatement = $this->pdo()->prepare($sql);
$productsStatement->execute(array_merge($productIds, $productIds));
$products = $this->preparePreviewProducts($productsStatement->fetchAll());
$productsById = [];
foreach ($products as $product) {
$retailProduct = $this->withRetailPreviewVariant($product, 6.0);
if ($retailProduct !== null) {
$productsById[(int) ($product['id'] ?? 0)] = $retailProduct;
}
}
$result = [];
foreach ($productIds as $productId) {
if (isset($productsById[$productId])) {
$result[] = $productsById[$productId];
}
}
return $result;
}
/**
* @param array<string, mixed> $product
* @param array<string, mixed> $variant
* @return array<string, mixed>
*/
private function syncProductVariantFields(array $product, array $variant): array
{
$product['variant_id'] = $variant['id'] ?? $product['variant_id'] ?? null;
$product['variant_name'] = $variant['name'] ?? $product['variant_name'] ?? null;
$product['unit'] = $variant['unit'] ?? $product['unit'] ?? null;
$product['package_quantity'] = $variant['package_quantity'] ?? $product['package_quantity'] ?? null;
$product['price_per_unit'] = $variant['price_per_unit'] ?? $product['price_per_unit'] ?? null;
$product['package_price'] = $variant['package_price'] ?? $product['package_price'] ?? null;
$product['old_package_price'] = $variant['old_package_price'] ?? $product['old_package_price'] ?? null;
return $product;
}
/**
* @param array<string, mixed> $product
* @return array<string, mixed>
*/
private function applyPromoPricing(array $product): array
{
$discountPercent = 5 + ((int) sprintf('%u', crc32('discount|' . ($product['id'] ?? 0))) % 31);
$divider = max(0.01, 1 - ($discountPercent / 100));
$variants = [];
foreach (($product['preview_variants'] ?? []) as $variant) {
$packagePrice = (float) ($variant['package_price'] ?? 0);
if ($packagePrice > 0) {
$variant['old_package_price'] = (string) ceil($packagePrice / $divider);
}
$variants[] = $variant;
}
$product['preview_variants'] = $variants;
$product = $variants === [] ? $product : $this->syncProductVariantFields($product, $variants[0]);
$product['badge_label'] = '-' . $discountPercent . '%';
$product['badge_kind'] = 'promo';
$product['promo_discount_percent'] = $discountPercent;
return $product;
}
/**
@@ -329,6 +846,7 @@ final class ProductRepository extends BaseRepository
}
$categoryParents = $this->categoryParentMap();
$categoryRoots = $this->categoryRootMap($categoryParents);
foreach ($products as &$product) {
$linkedCategories = $categoriesByProduct[(int) $product['id']] ?? [];
@@ -351,6 +869,11 @@ final class ProductRepository extends BaseRepository
});
$names = array_values(array_unique(array_map(static fn (array $category): string => $category['name'], $leafCategories)));
$product['category_display_ids'] = array_values(array_map(static fn (array $category): int => (int) $category['id'], $leafCategories));
$product['category_display_root_ids'] = array_values(array_unique(array_map(
static fn (array $category): int => (int) ($categoryRoots[(int) $category['id']] ?? $category['id']),
$leafCategories
)));
$product['category_display_name'] = $names === []
? ($product['category_name'] ?? 'Каталог')
: implode(' / ', $names);
@@ -376,6 +899,37 @@ final class ProductRepository extends BaseRepository
return $parents;
}
/**
* @param array<int, int|null> $categoryParents
* @return array<int, int>
*/
private function categoryRootMap(array $categoryParents): array
{
$roots = [];
foreach (array_keys($categoryParents) as $categoryId) {
$roots[$categoryId] = $this->rootCategoryId((int) $categoryId, $categoryParents);
}
return $roots;
}
/**
* @param array<int, int|null> $categoryParents
*/
private function rootCategoryId(int $categoryId, array $categoryParents): int
{
$currentId = $categoryId;
$parentId = $categoryParents[$currentId] ?? null;
while ($parentId !== null) {
$currentId = $parentId;
$parentId = $categoryParents[$currentId] ?? null;
}
return $currentId;
}
/**
* @param array<int, int> $linkedCategoryIds
* @param array<int, int|null> $categoryParents
File diff suppressed because it is too large Load Diff
+107
View File
@@ -22,6 +22,58 @@ function base_path(string $path = ''): string
return $path === '' ? $base : $base . '/' . ltrim($path, '/');
}
function versioned_asset(string $path): string
{
$assetPath = '/' . ltrim($path, '/');
$pathWithoutQuery = strtok($assetPath, '?') ?: $assetPath;
$querySeparator = str_contains($assetPath, '?') ? '&' : '?';
$publicPath = base_path('public' . $pathWithoutQuery);
if (!is_file($publicPath)) {
return $assetPath;
}
return $assetPath . $querySeparator . filemtime($publicPath);
}
function public_asset_exists(string $path): bool
{
$assetPath = '/' . ltrim(strtok($path, '?') ?: $path, '/');
return is_file(base_path('public' . $assetPath));
}
function optimized_image_path(?string $path, string $mode = 'preview'): string
{
$assetPath = trim((string) $path);
if ($assetPath === '' || !str_starts_with($assetPath, '/assets/uploads/old/products/')) {
return $assetPath;
}
$assetPath = '/' . ltrim(strtok($assetPath, '?') ?: $assetPath, '/');
$directory = rtrim(str_replace('\\', '/', dirname($assetPath)), '/');
$filename = basename($assetPath);
$isFull = str_starts_with($filename, 'full_');
$baseFilename = $isFull ? substr($filename, 5) : $filename;
$candidates = $mode === 'detail'
? [$baseFilename, 'thumb_' . $baseFilename]
: ['thumb_' . $baseFilename, $baseFilename];
foreach ($candidates as $candidate) {
if ($candidate === '' || $candidate === $filename) {
continue;
}
$candidatePath = $directory . '/' . $candidate;
if (public_asset_exists($candidatePath)) {
return $candidatePath;
}
}
return $assetPath;
}
function view(string $template, array $data = []): void
{
View::render($template, $data);
@@ -44,6 +96,61 @@ function unit_label(?string $unit): string
};
}
function text_lower(string $value): string
{
if (function_exists('mb_strtolower')) {
return mb_strtolower($value, 'UTF-8');
}
return strtolower(strtr($value, [
'А' => 'а', 'Б' => 'б', 'В' => 'в', 'Г' => 'г', 'Д' => 'д',
'Е' => 'е', 'Ё' => 'ё', 'Ж' => 'ж', 'З' => 'з', 'И' => 'и',
'Й' => 'й', 'К' => 'к', 'Л' => 'л', 'М' => 'м', 'Н' => 'н',
'О' => 'о', 'П' => 'п', 'Р' => 'р', 'С' => 'с', 'Т' => 'т',
'У' => 'у', 'Ф' => 'ф', 'Х' => 'х', 'Ц' => 'ц', 'Ч' => 'ч',
'Ш' => 'ш', 'Щ' => 'щ', 'Ъ' => 'ъ', 'Ы' => 'ы', 'Ь' => 'ь',
'Э' => 'э', 'Ю' => 'ю', 'Я' => 'я',
]));
}
/**
* Price per kg helps with boxes and weight products, but confuses customers on
* caviar, cans, sauces, spices and small consumer packs. In those cases the
* storefront should sell by package and avoid derived kg prices.
*
* @param array<string, mixed> $product
* @param array<string, mixed> $variant
*/
function product_should_show_unit_price(array $product, array $variant): bool
{
$unit = (string) ($variant['unit'] ?? $product['base_unit'] ?? '');
$unitLabel = unit_label($unit);
if (!in_array($unit, ['kg', 'кг'], true) && $unitLabel !== 'кг') {
return false;
}
if (($variant['price_per_unit'] ?? null) === null || (float) $variant['price_per_unit'] <= 0) {
return false;
}
$packageQuantity = (float) ($variant['package_quantity'] ?? 0);
if ($packageQuantity <= 0) {
return false;
}
if ($packageQuantity < 1) {
return false;
}
$variantName = text_lower((string) ($variant['name'] ?? ''));
if (preg_match('/(^|[\s,.;])\d+(?:[.,]\d+)?\s*(мл|шт)\.?($|[\s,.;])/u', $variantName) === 1) {
return false;
}
return true;
}
function money_label(mixed $value): string
{
if ($value === null || $value === '') {
+24 -1
View File
@@ -170,6 +170,28 @@ function execute_old_site_import_batch(string $sqlPath, string $progressPath, ar
];
}
function normalize_old_site_product_categories(PDO $pdo): void
{
$pdo->exec(
"UPDATE products p
SET p.category_id = (
SELECT pcl.category_id
FROM product_category_links pcl
INNER JOIN categories c ON c.id = pcl.category_id
WHERE pcl.product_id = p.id
AND c.is_active = 1
ORDER BY c.parent_id IS NULL, pcl.sort_order, pcl.category_id
LIMIT 1
)
WHERE p.legacy_path LIKE '/katalog/product/view/1/%'
AND EXISTS (
SELECT 1
FROM product_category_links pcl_exists
WHERE pcl_exists.product_id = p.id
)"
);
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
$action = (string) ($_POST['action'] ?? 'start');
@@ -186,6 +208,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$progress = execute_old_site_import_batch($sqlPath, $progressPath, $progress);
if (!empty($progress['done'])) {
normalize_old_site_product_categories(Database::pdo());
$message = 'Импорт выполнен. SQL-команд выполнено: ' . (int) $progress['executed'] . '.';
} else {
$message = 'Импорт продолжается: ' . (float) $progress['percent'] . '%, команд за этот шаг: ' . (int) $progress['batch_executed'] . '.';
@@ -206,7 +229,7 @@ $progressPercent = !empty($progress['done'])
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Импорт старого сайта - Рыбсток</title>
<link rel="stylesheet" href="/assets/css/app.css">
<link rel="stylesheet" href="<?= e(versioned_asset('/assets/css/app.css')) ?>">
</head>
<body>
<main class="page">
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

+3221 -110
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 425 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 799 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<rect width="512" height="512" rx="112" fill="#173d2c"/>
<path d="M120 176 C222 181 292 248 292 256 C292 264 222 331 120 336 C96 315 76 286 68 256 C76 226 96 197 120 176Z" fill="#fffefa"/>
<circle cx="145" cy="256" r="17" fill="#173d2c"/>
<path d="M292 256 L390 190 L390 322Z" fill="#fffefa"/>
<path d="M90 118 H344" fill="none" stroke="#c43d31" stroke-width="26" stroke-linecap="round"/>
<path d="M90 394 H344" fill="none" stroke="#c43d31" stroke-width="26" stroke-linecap="round"/>
<path d="M146 440 H295" fill="none" stroke="#27634a" stroke-width="20" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 663 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 924 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

@@ -0,0 +1,13 @@
<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>

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

@@ -0,0 +1,21 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 650 220">
<defs>
<style>
.green{fill:#173d2c}.green2{fill:#27634a}.red{fill:#c43d31}.paper{fill:#fffefa}.shadow-green{fill:#173d2c;opacity:.13}
.word{font-family:Arial Black, Arial, Helvetica, sans-serif;font-size:104px;font-weight:950;letter-spacing:-7px}
.tag{font-family:Arial, Helvetica, sans-serif;font-size:25px;font-weight:900;letter-spacing:-.25px;fill:#173d2c}
.rule-red{fill:none;stroke:#c43d31;stroke-width:7;stroke-linecap:round}.rule-green{fill:none;stroke:#173d2c;stroke-width:5;stroke-linecap:round}.rule-shadow{fill:none;stroke:#173d2c;stroke-width:7;stroke-linecap:round;opacity:.12}.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>

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 540 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 524 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 555 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

@@ -0,0 +1,84 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1400 980" role="img" aria-labelledby="title desc">
<title id="title">Рыбсток - варианты стилизации буквы Б</title>
<desc id="desc">Четыре читаемых варианта логотипа Рыбсток со стилизацией верхней части буквы б под рыбий плавник.</desc>
<defs>
<style>
.bg{fill:#f4f6ef}
.panel{fill:#fffefa;stroke:#dfe5da;stroke-width:2}
.green{fill:#173d2c}
.green2{fill:#27634a}
.red{fill:#c43d31}
.ice{fill:#d9efef}
.label{font-family:Arial, Helvetica, sans-serif;font-size:22px;font-weight:900;fill:#657469}
.tag{font-family:Arial, Helvetica, sans-serif;font-size:22px;font-weight:800;letter-spacing:-.3px;fill:#173d2c}
.word{font-family:Arial Black, Arial, Helvetica, sans-serif;font-size:104px;font-weight:950;letter-spacing:-6px}
.ryb{fill:#173d2c}
.stock{fill:#c43d31}
.rule-red{fill:none;stroke:#c43d31;stroke-width:7;stroke-linecap:round}
.rule-green{fill:none;stroke:#173d2c;stroke-width:5;stroke-linecap:round}
.ice-line{fill:none;stroke:#d9efef;stroke-width:5;stroke-linecap:round;stroke-linejoin:round}
.paper{fill:#fffefa}
</style>
</defs>
<rect class="bg" width="1400" height="980"/>
<g transform="translate(48 46)">
<rect class="panel" width="620" height="400" rx="18"/>
<text class="label" x="32" y="48">01 / плавник над Б, максимально читаемо</text>
<g transform="translate(42 134)">
<text class="word ryb" x="0" y="80">Рыб</text>
<text class="word stock" x="188" y="80">сток</text>
<path class="red" d="M145 4 C164 10 178 24 184 44 C165 38 151 27 145 4Z"/>
<path class="paper" d="M151 12 C164 18 173 28 178 39 C165 34 156 25 151 12Z"/>
<path class="rule-red" d="M4 110 H178"/>
<path class="rule-green" d="M188 110 H430"/>
<text class="tag" x="4" y="156">Качественные продукты по стоковым ценам</text>
</g>
</g>
<g transform="translate(732 46)">
<rect class="panel" width="620" height="400" rx="18"/>
<text class="label" x="32" y="48">02 / плавник как холодный срез</text>
<g transform="translate(42 134)">
<text class="word ryb" x="0" y="80">Рыб</text>
<text class="word stock" x="188" y="80">сток</text>
<path class="ice" d="M141 4 L194 24 L156 43Z"/>
<path class="red" d="M146 9 L184 24 L157 36Z"/>
<path class="ice-line" d="M151 3 L165 18 M181 22 L194 16"/>
<path class="rule-red" d="M4 110 H178"/>
<path class="rule-green" d="M188 110 H430"/>
<text class="tag" x="4" y="156">Качественные продукты по стоковым ценам</text>
</g>
</g>
<g transform="translate(48 516)">
<rect class="panel" width="620" height="400" rx="18"/>
<text class="label" x="32" y="48">03 / плавник внутри верхней чаши Б</text>
<g transform="translate(42 134)">
<text class="word ryb" x="0" y="80">Рыб</text>
<text class="word stock" x="188" y="80">сток</text>
<path class="paper" d="M142 25 C160 17 181 22 194 38 C174 39 157 35 142 25Z"/>
<path class="green2" d="M147 26 C161 21 177 24 187 35 C171 35 158 32 147 26Z"/>
<path class="red" d="M184 35 L201 24 L200 48Z"/>
<path class="rule-red" d="M4 110 H178"/>
<path class="rule-green" d="M188 110 H430"/>
<text class="tag" x="4" y="156">Качественные продукты по стоковым ценам</text>
</g>
</g>
<g transform="translate(732 516)">
<rect class="panel" width="620" height="400" rx="18"/>
<text class="label" x="32" y="48">04 / плавник + легкое соприкосновение</text>
<g transform="translate(42 134)">
<text class="word ryb" x="0" y="80">Рыб</text>
<text class="word stock" x="188" y="80">сток</text>
<path class="red" d="M142 8 C161 8 181 19 192 38 C172 38 154 27 142 8Z"/>
<circle class="green2" cx="184" cy="62" r="7"/>
<path class="paper" d="M181 58 H190 V66 H181Z"/>
<path class="rule-red" d="M4 110 H178"/>
<path class="rule-green" d="M188 110 H430"/>
<text class="tag" x="4" y="156">Качественные продукты по стоковым ценам</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

@@ -0,0 +1,125 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1400 980" role="img" aria-labelledby="title desc">
<title id="title">Рыбсток - варианты хвоста в букве б</title>
<desc id="desc">Четыре варианта логотипа Рыбсток, где верх буквы б стилизован под рыбий хвост.</desc>
<defs>
<style>
.bg{fill:#f4f6ef}
.panel{fill:#fffefa;stroke:#dfe5da;stroke-width:2}
.green{fill:#173d2c}
.green2{fill:#27634a}
.red{fill:#c43d31}
.paper{fill:#fffefa}
.shadow-green{fill:#173d2c;opacity:.13}
.shadow-red{fill:#c43d31;opacity:.18}
.label{font-family:Arial, Helvetica, sans-serif;font-size:22px;font-weight:900;fill:#657469}
.tag{font-family:Arial, Helvetica, sans-serif;font-size:22px;font-weight:900;letter-spacing:-.3px;fill:#173d2c}
.word{font-family:Arial Black, Arial, Helvetica, sans-serif;font-size:104px;font-weight:950;letter-spacing:-7px}
.rule-red{fill:none;stroke:#c43d31;stroke-width:7;stroke-linecap:round}
.rule-green{fill:none;stroke:#173d2c;stroke-width:5;stroke-linecap:round}
.rule-shadow{fill:none;stroke:#173d2c;stroke-width:7;stroke-linecap:round;opacity:.12}
.stroke{fill:none;stroke-linecap:round;stroke-linejoin:round}
</style>
</defs>
<rect class="bg" width="1400" height="980"/>
<g transform="translate(48 46)">
<rect class="panel" width="620" height="400" rx="18"/>
<text class="label" x="32" y="48">01 / хвост как аккуратный надрез</text>
<g transform="translate(40 132)">
<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 green" x="116" y="80">б</text>
<path class="green" d="M168 2 C182 3 194 0 205 -8 C203 8 194 19 178 24 C188 30 197 42 199 57 C185 50 174 43 164 32 C160 21 161 10 168 2Z"/>
<path class="paper" d="M175 14 C184 14 191 11 197 6 C195 14 189 19 181 22 C188 25 193 31 196 39 C187 36 179 32 172 25 C171 21 172 17 175 14Z"/>
<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="158">Качественные продукты по стоковым ценам</text>
</g>
</g>
<g transform="translate(732 46)">
<rect class="panel" width="620" height="400" rx="18"/>
<text class="label" x="32" y="48">02 / хвост-плавник выше буквы</text>
<g transform="translate(40 132)">
<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 green" x="116" y="80">б</text>
<path class="green" d="M156 5 C170 -18 198 -20 224 -13 C210 -1 202 12 201 31 C181 29 166 21 156 5Z"/>
<path class="paper" d="M174 -2 C186 -7 199 -7 211 -4 C202 2 196 9 194 19 C186 16 179 10 174 -2Z"/>
<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="158">Качественные продукты по стоковым ценам</text>
</g>
</g>
<g transform="translate(48 516)">
<rect class="panel" width="620" height="400" rx="18"/>
<text class="label" x="32" y="48">03 / хвост как фирменная засечка</text>
<g transform="translate(40 132)">
<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 green" x="116" y="80">б</text>
<path class="green" d="M162 6 C177 2 190 -5 202 -18 C204 -2 199 12 188 21 C201 23 212 30 221 42 C202 44 184 39 170 28 C162 22 159 14 162 6Z"/>
<path class="paper" d="M178 11 C185 8 192 4 198 -2 C199 6 196 12 190 17 C198 19 204 23 209 29 C199 29 189 26 181 21 C177 18 176 14 178 11Z"/>
<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="158">Качественные продукты по стоковым ценам</text>
</g>
</g>
<g transform="translate(732 516)">
<rect class="panel" width="620" height="400" rx="18"/>
<text class="label" x="32" y="48">04 / хвост-стрела в сторону стока</text>
<g transform="translate(40 132)">
<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 green" x="116" y="80">б</text>
<path class="green" d="M157 7 C176 -4 199 -4 226 5 C207 12 194 24 185 41 C176 27 167 17 157 7Z"/>
<path class="paper" d="M178 6 C188 3 199 4 211 8 C201 12 194 18 188 27 C185 18 182 12 178 6Z"/>
<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="158">Качественные продукты по стоковым ценам</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.3 KiB

@@ -0,0 +1,91 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1400 980" role="img" aria-labelledby="title desc">
<title id="title">Рыбсток - варианты соприкосновения Б и С</title>
<desc id="desc">Четыре варианта типографического логотипа Рыбсток с фирменным акцентом на стыке букв Б и С.</desc>
<defs>
<style>
.bg{fill:#f4f6ef}
.panel{fill:#fffefa;stroke:#dfe5da;stroke-width:2}
.green{fill:#173d2c}
.green2{fill:#27634a}
.red{fill:#c43d31}
.ice{fill:#d9efef}
.muted{fill:#657469}
.label{font-family:Arial, Helvetica, sans-serif;font-size:22px;font-weight:900;fill:#657469}
.tag{font-family:Arial, Helvetica, sans-serif;font-size:22px;font-weight:800;letter-spacing:-.3px;fill:#173d2c}
.word{font-family:Arial Black, Arial, Helvetica, sans-serif;font-weight:950;letter-spacing:-6px}
.ryb{font-size:104px;fill:#173d2c}
.stock{font-size:104px;fill:#c43d31}
.rule-red{fill:none;stroke:#c43d31;stroke-width:7;stroke-linecap:round}
.rule-green{fill:none;stroke:#173d2c;stroke-width:5;stroke-linecap:round}
.cut{fill:#fffefa}
.ice-line{fill:none;stroke:#d9efef;stroke-width:5;stroke-linecap:round;stroke-linejoin:round}
</style>
</defs>
<rect class="bg" width="1400" height="980"/>
<g transform="translate(48 46)">
<rect class="panel" width="620" height="400" rx="18"/>
<text class="label" x="32" y="48">01 / С обнимает хвост Б</text>
<g transform="translate(42 134)">
<text class="word ryb" x="0" y="80">Рыб</text>
<text class="word stock" x="188" y="80">сток</text>
<path class="cut" d="M181 18 C205 18 225 37 225 61 C225 83 207 102 184 104 L184 91 C200 89 211 77 211 62 C211 45 198 32 181 32Z"/>
<path class="red" d="M188 21 C212 21 231 39 231 62 C231 86 212 104 188 104 L188 89 C203 89 216 77 216 62 C216 47 203 35 188 35Z"/>
<path class="green2" d="M160 42 H186 V58 H160Z"/>
<path class="green2" d="M160 75 H186 V91 H160Z"/>
<path class="rule-red" d="M4 110 H178"/>
<path class="rule-green" d="M188 110 H430"/>
<text class="tag" x="4" y="156">Качественные продукты по стоковым ценам</text>
</g>
</g>
<g transform="translate(732 46)">
<rect class="panel" width="620" height="400" rx="18"/>
<text class="label" x="32" y="48">02 / холодный зазор между Б и С</text>
<g transform="translate(42 134)">
<text class="word ryb" x="0" y="80">Рыб</text>
<text class="word stock" x="192" y="80">сток</text>
<path class="ice" d="M174 23 H187 V96 H174Z"/>
<path class="ice-line" d="M165 28 H196 M181 13 V42 M166 93 H196"/>
<path class="red" d="M194 34 C210 20 239 20 256 37 L245 48 C234 38 218 38 207 48 C196 58 196 75 207 85 C218 96 235 96 246 86 L257 97 C239 114 211 114 194 100 C177 83 177 51 194 34Z"/>
<path class="rule-red" d="M4 110 H178"/>
<path class="rule-green" d="M192 110 H430"/>
<text class="tag" x="4" y="156">Качественные продукты по стоковым ценам</text>
</g>
</g>
<g transform="translate(48 516)">
<rect class="panel" width="620" height="400" rx="18"/>
<text class="label" x="32" y="48">03 / складской паз Б-С</text>
<g transform="translate(42 134)">
<text class="word ryb" x="0" y="80">Рыб</text>
<text class="word stock" x="190" y="80">сток</text>
<path class="cut" d="M160 31 H191 V48 H176 V57 H191 V74 H176 V83 H191 V100 H160Z"/>
<path class="green2" d="M160 31 H181 V47 H160Z"/>
<path class="green2" d="M160 57 H181 V73 H160Z"/>
<path class="green2" d="M160 83 H181 V99 H160Z"/>
<path class="red" d="M190 27 C216 27 237 45 237 66 C237 88 216 106 190 106 L190 88 C204 88 216 78 216 66 C216 54 204 45 190 45Z"/>
<path class="rule-red" d="M4 110 H178"/>
<path class="rule-green" d="M190 110 H430"/>
<text class="tag" x="4" y="156">Качественные продукты по стоковым ценам</text>
</g>
</g>
<g transform="translate(732 516)">
<rect class="panel" width="620" height="400" rx="18"/>
<text class="label" x="32" y="48">04 / ценовой маркер на стыке</text>
<g transform="translate(42 134)">
<text class="word ryb" x="0" y="80">Рыб</text>
<text class="word stock" x="190" y="80">сток</text>
<circle class="red" cx="187" cy="61" r="18"/>
<path class="cut" d="M179 52 H195 V59 H188 V70 H179Z"/>
<path class="ice-line" d="M174 34 L200 88 M201 34 L175 88"/>
<path class="green2" d="M160 53 H182 V69 H160Z"/>
<path class="red" d="M198 34 C214 21 241 22 257 38 L246 49 C235 39 220 39 209 49 C198 60 198 75 209 86 C220 96 236 96 247 86 L258 97 C241 114 214 114 198 100 C181 83 181 51 198 34Z"/>
<path class="rule-red" d="M4 110 H178"/>
<path class="rule-green" d="M190 110 H430"/>
<text class="tag" x="4" y="156">Качественные продукты по стоковым ценам</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.1 KiB

@@ -0,0 +1,91 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1400 980" role="img" aria-labelledby="title desc">
<title id="title">Новые концепты логотипа Рыбсток</title>
<desc id="desc">Четыре альтернативных графичных направления логотипа Рыбсток.</desc>
<defs>
<style>
.bg{fill:#f4f6ef}
.panel{fill:#fffefa;stroke:#dfe5da;stroke-width:2}
.green{fill:#173d2c}
.green2{fill:#27634a}
.red{fill:#c43d31}
.muted{fill:#657469}
.paper{fill:#fffefa}
.stroke-g{fill:none;stroke:#173d2c;stroke-width:9;stroke-linecap:round;stroke-linejoin:round}
.stroke-r{fill:none;stroke:#c43d31;stroke-width:9;stroke-linecap:round;stroke-linejoin:round}
.word-ryb{font-family:Arial Black, Arial, Helvetica, sans-serif;font-size:94px;font-weight:950;letter-spacing:-7px;fill:#173d2c}
.word-stock{font-family:Arial Black, Arial, Helvetica, sans-serif;font-size:73px;font-weight:950;letter-spacing:-5px;fill:#c43d31}
.word-stock-big{font-family:Arial Black, Arial, Helvetica, sans-serif;font-size:86px;font-weight:950;letter-spacing:-6px;fill:#c43d31}
.tag{font-family:Arial, Helvetica, sans-serif;font-size:21px;font-weight:800;letter-spacing:-.2px;fill:#173d2c}
.label{font-family:Arial, Helvetica, sans-serif;font-size:22px;font-weight:900;fill:#657469}
</style>
</defs>
<rect class="bg" width="1400" height="980"/>
<g transform="translate(48 46)">
<rect class="panel" width="620" height="400" rx="18"/>
<text class="label" x="32" y="48">01 / монограмма: Р + складская полка</text>
<g transform="translate(48 112)">
<g transform="translate(0 0)">
<path class="green" d="M0 0 H92 C125 0 142 18 142 46 C142 71 128 88 104 94 L150 142 H103 L65 101 H42 V142 H0Z"/>
<path class="paper" d="M42 34 H86 C97 34 103 39 103 49 C103 59 97 64 86 64 H42Z"/>
<path class="red" d="M30 112 H135 V123 H30Z"/>
<path class="red" d="M151 112 L181 94 L181 141Z"/>
</g>
<text class="word-ryb" x="185" y="80">РЫБ</text>
<text class="word-stock" x="330" y="80">СТОК</text>
<text class="tag" x="188" y="128">Качественные продукты по стоковым ценам</text>
</g>
</g>
<g transform="translate(732 46)">
<rect class="panel" width="620" height="400" rx="18"/>
<text class="label" x="32" y="48">02 / промышленная маркировка склада</text>
<g transform="translate(48 110)">
<path class="stroke-g" d="M4 16 H480"/>
<text class="word-ryb" x="0" y="96">РЫБ</text>
<text class="word-stock-big" x="146" y="96">СТОК</text>
<path class="stroke-r" d="M410 76 H514 M410 112 H514"/>
<path class="red" d="M502 94 L548 66 L548 122Z"/>
<path class="green" d="M410 94 C432 71 472 71 502 94 C472 117 432 117 410 94Z"/>
<circle class="paper" cx="431" cy="91" r="5"/>
<text class="tag" x="3" y="145">Качественные продукты по стоковым ценам</text>
</g>
</g>
<g transform="translate(48 516)">
<rect class="panel" width="620" height="400" rx="18"/>
<text class="label" x="32" y="48">03 / премиальная печать без рисованности</text>
<g transform="translate(48 94)">
<circle class="green" cx="86" cy="86" r="82"/>
<circle class="paper" cx="86" cy="86" r="58"/>
<path class="red" d="M41 86 C60 62 97 62 123 86 C97 110 60 110 41 86Z"/>
<path class="red" d="M119 86 L146 66 L146 106Z"/>
<circle class="green" cx="61" cy="83" r="5"/>
<path class="green2" d="M49 122 H122 V134 H49Z"/>
<text class="word-ryb" x="196" y="94">РЫБ</text>
<text class="word-stock" x="342" y="94">СТОК</text>
<text class="tag" x="198" y="139">Качественные продукты по стоковым ценам</text>
</g>
</g>
<g transform="translate(732 516)">
<rect class="panel" width="620" height="400" rx="18"/>
<text class="label" x="32" y="48">04 / минимальный знак “склад-хвост”</text>
<g transform="translate(48 108)">
<text class="word-ryb" x="0" y="86">РЫБ</text>
<text class="word-stock" x="144" y="86">СТОК</text>
<g transform="translate(425 10)">
<path class="green" d="M0 0 H38 V94 H0Z"/>
<path class="green2" d="M46 0 H84 V94 H46Z"/>
<path class="green" d="M92 0 H130 V94 H92Z"/>
<path class="red" d="M12 112 H108"/>
<path class="red" d="M106 112 L146 86 L146 138Z"/>
<circle class="paper" cx="28" cy="34" r="5"/>
</g>
<rect class="green" x="3" y="112" width="138" height="8" rx="4"/>
<rect class="red" x="142" y="112" width="164" height="8" rx="4"/>
<text class="tag" x="4" y="156">Качественные продукты по стоковым ценам</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

@@ -0,0 +1,101 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1400 980" role="img" aria-labelledby="title desc">
<title id="title">Концепты логотипа Рыбсток</title>
<desc id="desc">Четыре векторных варианта логотипа Рыбсток с графичными знаками рыбы и склада.</desc>
<defs>
<style>
.bg{fill:#f4f6ef}
.panel{fill:#fffefa;stroke:#dfe5da;stroke-width:2}
.green{fill:#173d2c}
.green2{fill:#27634a}
.red{fill:#c43d31}
.muted{fill:#657469}
.white{fill:#fffefa}
.line-green{fill:none;stroke:#173d2c;stroke-width:8;stroke-linecap:round;stroke-linejoin:round}
.line-red{fill:none;stroke:#c43d31;stroke-width:8;stroke-linecap:round;stroke-linejoin:round}
.word-ryb{font-family:Arial, Helvetica, sans-serif;font-weight:950;font-size:92px;letter-spacing:-5px;fill:#173d2c}
.word-stock{font-family:Arial, Helvetica, sans-serif;font-weight:950;font-size:76px;letter-spacing:-4px;fill:#c43d31}
.word-stock-small{font-family:Arial, Helvetica, sans-serif;font-weight:950;font-size:66px;letter-spacing:-3px;fill:#c43d31}
.tag{font-family:Arial, Helvetica, sans-serif;font-weight:800;font-size:21px;letter-spacing:-.2px;fill:#173d2c}
.label{font-family:Arial, Helvetica, sans-serif;font-weight:900;font-size:22px;fill:#657469}
</style>
</defs>
<rect class="bg" width="1400" height="980"/>
<g transform="translate(48 46)">
<rect class="panel" width="620" height="400" rx="18"/>
<text class="label" x="32" y="48">01 / складской знак слева</text>
<g transform="translate(46 105)">
<g transform="translate(0 4)">
<rect class="green" x="0" y="0" width="118" height="118" rx="26"/>
<path class="red" d="M18 42 L59 16 L100 42" fill="none" stroke="#c43d31" stroke-width="10" stroke-linecap="round" stroke-linejoin="round"/>
<rect class="white" x="28" y="46" width="62" height="46" rx="8"/>
<path class="green" d="M33 69 C48 48 72 48 90 69 C72 89 48 89 33 69Z"/>
<circle class="white" cx="46" cy="67" r="4"/>
<path class="white" d="M84 69 L98 57 L98 81Z"/>
</g>
<text class="word-ryb" x="145" y="82">РЫБ</text>
<text class="word-stock" x="290" y="82">СТОК</text>
<rect class="green" x="148" y="104" width="137" height="8" rx="4"/>
<rect class="red" x="286" y="104" width="166" height="8" rx="4"/>
<text class="tag" x="148" y="148">Качественные продукты по стоковым ценам</text>
</g>
</g>
<g transform="translate(732 46)">
<rect class="panel" width="620" height="400" rx="18"/>
<text class="label" x="32" y="48">02 / знак как складская наклейка</text>
<g transform="translate(48 112)">
<text class="word-ryb" x="0" y="78">РЫБ</text>
<text class="word-stock-small" x="143" y="78">СТОК</text>
<g transform="translate(402 6)">
<rect class="green" x="0" y="0" width="128" height="96" rx="18"/>
<path class="white" d="M21 55 C42 28 78 28 104 55 C78 81 42 81 21 55Z"/>
<path class="white" d="M100 55 L121 38 L121 72Z"/>
<circle class="green" cx="40" cy="53" r="5"/>
<path class="red" d="M12 14 H92 M12 82 H92" stroke="#c43d31" stroke-width="8" stroke-linecap="round"/>
</g>
<rect class="green" x="4" y="104" width="135" height="8" rx="4"/>
<rect class="red" x="140" y="104" width="149" height="8" rx="4"/>
<text class="tag" x="4" y="148">Качественные продукты по стоковым ценам</text>
</g>
</g>
<g transform="translate(48 516)">
<rect class="panel" width="620" height="400" rx="18"/>
<text class="label" x="32" y="48">03 / знак-иконка для фавикона</text>
<g transform="translate(46 112)">
<g transform="translate(0 0)">
<rect class="green" x="0" y="0" width="136" height="136" rx="32"/>
<path class="red" d="M28 44 H108 M28 92 H108" stroke="#c43d31" stroke-width="10" stroke-linecap="round"/>
<path class="white" d="M27 68 C50 39 88 39 112 68 C88 97 50 97 27 68Z"/>
<path class="white" d="M108 68 L130 49 L130 87Z"/>
<circle class="green" cx="49" cy="65" r="5"/>
<rect class="green2" x="36" y="102" width="64" height="10" rx="5"/>
</g>
<text class="word-ryb" x="166" y="84">РЫБ</text>
<text class="word-stock" x="313" y="84">СТОК</text>
<text class="tag" x="169" y="132">Качественные продукты по стоковым ценам</text>
</g>
</g>
<g transform="translate(732 516)">
<rect class="panel" width="620" height="400" rx="18"/>
<text class="label" x="32" y="48">04 / более премиальный, знак справа</text>
<g transform="translate(48 112)">
<text class="word-ryb" x="0" y="82">РЫБ</text>
<text class="word-stock" x="146" y="82">СТОК</text>
<g transform="translate(430 5)">
<path class="green" d="M0 24 H112 V95 H0Z"/>
<path class="red" d="M0 24 L56 0 L112 24Z"/>
<path class="white" d="M18 62 C38 38 76 38 100 62 C76 86 38 86 18 62Z"/>
<path class="white" d="M98 62 L119 44 L119 80Z"/>
<circle class="green" cx="39" cy="60" r="5"/>
<rect class="green2" x="18" y="101" width="80" height="10" rx="5"/>
</g>
<rect class="green" x="3" y="105" width="138" height="8" rx="4"/>
<rect class="red" x="142" y="105" width="166" height="8" rx="4"/>
<text class="tag" x="4" y="150">Качественные продукты по стоковым ценам</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.6 KiB

@@ -0,0 +1,41 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 650 220" role="img" aria-labelledby="logo-title logo-desc">
<title id="logo-title">Рыбсток</title>
<desc id="logo-desc">Качество со склада. Стоковые цены.</desc>
<defs>
<style>
.green{fill:#173d2c}
.green2{fill:#27634a}
.red{fill:#c43d31}
.paper{fill:#fffefa}
.shadow-green{fill:#173d2c;opacity:.13}
.word{font-family:Arial Black, Arial, Helvetica, sans-serif;font-size:104px;font-weight:950;letter-spacing:-7px}
.tag{font-family:Arial, Helvetica, sans-serif;font-size:25px;font-weight:900;letter-spacing:-.25px;fill:#173d2c}
.rule-red{fill:none;stroke:#c43d31;stroke-width:7;stroke-linecap:round}
.rule-green{fill:none;stroke:#173d2c;stroke-width:5;stroke-linecap:round}
.rule-shadow{fill:none;stroke:#173d2c;stroke-width:7;stroke-linecap:round;opacity:.12}
.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>

After

Width:  |  Height:  |  Size: 2.2 KiB

@@ -0,0 +1,90 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1400 980" role="img" aria-labelledby="title desc">
<title id="title">Рыбсток - варианты фирменного стыка</title>
<desc id="desc">Четыре варианта логотипа Рыбсток с выразительным стыком букв б и с.</desc>
<defs>
<style>
.bg{fill:#f4f6ef}
.panel{fill:#fffefa;stroke:#dfe5da;stroke-width:2}
.green{fill:#173d2c}
.green2{fill:#27634a}
.red{fill:#c43d31}
.paper{fill:#fffefa}
.label{font-family:Arial, Helvetica, sans-serif;font-size:22px;font-weight:900;fill:#657469}
.tag{font-family:Arial, Helvetica, sans-serif;font-size:22px;font-weight:900;letter-spacing:-.3px;fill:#173d2c}
.word{font-family:Arial Black, Arial, Helvetica, sans-serif;font-size:104px;font-weight:950;letter-spacing:-7px}
.rule-red{fill:none;stroke:#c43d31;stroke-width:7;stroke-linecap:round}
.rule-green{fill:none;stroke:#173d2c;stroke-width:5;stroke-linecap:round}
.ice{fill:none;stroke:#d9efef;stroke-width:4;stroke-linecap:round}
</style>
</defs>
<rect class="bg" width="1400" height="980"/>
<g transform="translate(48 46)">
<rect class="panel" width="620" height="400" rx="18"/>
<text class="label" x="32" y="48">01 / ближе к выбранному, чище</text>
<g transform="translate(40 132)">
<text class="word green" x="0" y="80">Рыб</text>
<text class="word red" x="190" y="80">сток</text>
<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="green2" d="M147 51 H169 V66 H147Z"/>
<path class="green2" d="M147 77 H169 V92 H147Z"/>
<path class="rule-red" d="M4 112 H176"/>
<path class="rule-green" d="M178 112 H432"/>
<text class="tag" x="4" y="158">Качественные продукты по стоковым ценам</text>
</g>
</g>
<g transform="translate(732 46)">
<rect class="panel" width="620" height="400" rx="18"/>
<text class="label" x="32" y="48">02 / красный стык как хвост рыбы</text>
<g transform="translate(40 132)">
<text class="word green" x="0" y="80">Рыб</text>
<text class="word red" x="196" y="80">сток</text>
<path class="red" d="M160 20 C191 22 214 43 214 68 C214 93 191 114 160 116 L160 95 C177 93 190 82 190 68 C190 54 177 43 160 41Z"/>
<path class="red" d="M202 68 L244 42 L244 94Z"/>
<path class="paper" d="M164 42 C178 45 188 55 188 68 C188 81 178 91 164 94 L164 80 C170 78 174 74 174 68 C174 62 170 58 164 56Z"/>
<path class="green2" d="M145 50 H169 V66 H145Z"/>
<path class="green2" d="M145 78 H169 V94 H145Z"/>
<path class="rule-red" d="M4 112 H176"/>
<path class="rule-green" d="M178 112 H432"/>
<text class="tag" x="4" y="158">Качественные продукты по стоковым ценам</text>
</g>
</g>
<g transform="translate(48 516)">
<rect class="panel" width="620" height="400" rx="18"/>
<text class="label" x="32" y="48">03 / стык как ценовая метка</text>
<g transform="translate(40 132)">
<text class="word green" x="0" y="80">Рыб</text>
<text class="word red" x="195" y="80">сток</text>
<circle class="red" cx="172" cy="68" r="36"/>
<path class="paper" d="M156 51 H174 C185 51 192 58 192 68 C192 78 185 85 174 85 H156Z"/>
<circle class="paper" cx="159" cy="55" r="4"/>
<path class="green2" d="M142 50 H166 V66 H142Z"/>
<path class="green2" d="M142 78 H166 V94 H142Z"/>
<path class="red" d="M202 35 C220 35 235 49 235 68 C235 87 220 101 202 101 L202 84 C211 84 218 77 218 68 C218 59 211 52 202 52Z"/>
<path class="rule-red" d="M4 112 H176"/>
<path class="rule-green" d="M178 112 H432"/>
<text class="tag" x="4" y="158">Качественные продукты по стоковым ценам</text>
</g>
</g>
<g transform="translate(732 516)">
<rect class="panel" width="620" height="400" rx="18"/>
<text class="label" x="32" y="48">04 / холодный, более премиальный узел</text>
<g transform="translate(40 132)">
<text class="word green" x="0" y="80">Рыб</text>
<text class="word red" x="192" y="80">сток</text>
<path class="red" d="M164 23 C190 25 210 43 210 68 C210 93 190 111 164 113 L164 94 C179 92 190 82 190 68 C190 54 179 44 164 42Z"/>
<path class="paper" d="M165 45 C176 48 183 57 183 68 C183 79 176 88 165 91Z"/>
<path class="ice" d="M148 29 H180 M164 14 V43 M142 104 H177"/>
<path class="green2" d="M146 52 H169 V66 H146Z"/>
<path class="green2" d="M146 78 H169 V92 H146Z"/>
<path class="rule-red" d="M4 112 H176"/>
<path class="rule-green" d="M178 112 H432"/>
<text class="tag" x="4" y="158">Качественные продукты по стоковым ценам</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.1 KiB

@@ -0,0 +1,109 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1400 980" role="img" aria-labelledby="title desc">
<title id="title">Рыбсток - теневые варианты логотипа</title>
<desc id="desc">Четыре теневых варианта логотипа Рыбсток с выразительным стыком букв б и с.</desc>
<defs>
<style>
.bg{fill:#f4f6ef}
.panel{fill:#fffefa;stroke:#dfe5da;stroke-width:2}
.green{fill:#173d2c}
.green2{fill:#27634a}
.red{fill:#c43d31}
.paper{fill:#fffefa}
.ice-fill{fill:#d9efef}
.shadow-green{fill:#173d2c;opacity:.13}
.shadow-red{fill:#c43d31;opacity:.18}
.label{font-family:Arial, Helvetica, sans-serif;font-size:22px;font-weight:900;fill:#657469}
.tag{font-family:Arial, Helvetica, sans-serif;font-size:22px;font-weight:900;letter-spacing:-.3px;fill:#173d2c}
.word{font-family:Arial Black, Arial, Helvetica, sans-serif;font-size:104px;font-weight:950;letter-spacing:-7px}
.rule-red{fill:none;stroke:#c43d31;stroke-width:7;stroke-linecap:round}
.rule-green{fill:none;stroke:#173d2c;stroke-width:5;stroke-linecap:round}
.rule-shadow{fill:none;stroke:#173d2c;stroke-width:7;stroke-linecap:round;opacity:.12}
.frost{fill:none;stroke:#d9efef;stroke-width:5;stroke-linecap:round}
</style>
<filter id="soft-premium-shadow" x="-12%" y="-22%" width="128%" height="150%">
<feDropShadow dx="0" dy="9" stdDeviation="5" flood-color="#173d2c" flood-opacity=".18"/>
</filter>
<filter id="small-red-shadow" x="-20%" y="-20%" width="145%" height="145%">
<feDropShadow dx="5" dy="6" stdDeviation="1.8" flood-color="#173d2c" flood-opacity=".16"/>
</filter>
</defs>
<rect class="bg" width="1400" height="980"/>
<g transform="translate(48 46)">
<rect class="panel" width="620" height="400" rx="18"/>
<text class="label" x="32" y="48">01 / ледяная тень</text>
<g transform="translate(40 132)">
<text class="word ice-fill" x="7" y="88">Рыб</text>
<text class="word ice-fill" x="197" y="88">сток</text>
<path class="frost" d="M15 18 H70 M410 25 H460 M310 122 H398"/>
<text class="word green" x="0" y="80">Рыб</text>
<text class="word red" x="190" y="80">сток</text>
<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="green2" d="M147 51 H169 V66 H147Z"/>
<path class="green2" d="M147 77 H169 V92 H147Z"/>
<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="158">Качественные продукты по стоковым ценам</text>
</g>
</g>
<g transform="translate(732 46)">
<rect class="panel" width="620" height="400" rx="18"/>
<text class="label" x="32" y="48">02 / складская глубина</text>
<g transform="translate(40 132)">
<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="M173 31 C199 33 219 52 219 77 C219 102 199 121 173 123 L173 103 C188 101 199 91 199 77 C199 63 188 53 173 51Z"/>
<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="green2" d="M147 51 H169 V66 H147Z"/>
<path class="green2" d="M147 77 H169 V92 H147Z"/>
<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="158">Качественные продукты по стоковым ценам</text>
</g>
</g>
<g transform="translate(48 516)">
<rect class="panel" width="620" height="400" rx="18"/>
<text class="label" x="32" y="48">03 / фирменный красно-зеленый сдвиг</text>
<g transform="translate(40 132)">
<text class="word shadow-red" x="5" y="85">Рыб</text>
<text class="word shadow-green" x="184" y="86">сток</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="green2" d="M147 51 H169 V66 H147Z"/>
<path class="green2" d="M147 77 H169 V92 H147Z"/>
<path class="rule-red" d="M4 112 H176"/>
<path class="rule-green" d="M178 112 H432"/>
<text class="tag" x="4" y="158">Качественные продукты по стоковым ценам</text>
</g>
</g>
<g transform="translate(732 516)">
<rect class="panel" width="620" height="400" rx="18"/>
<text class="label" x="32" y="48">04 / мягкая премиальная тень</text>
<g transform="translate(40 132)" filter="url(#soft-premium-shadow)">
<text class="word green" x="0" y="80">Рыб</text>
<text class="word red" x="190" y="80">сток</text>
<g filter="url(#small-red-shadow)">
<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"/>
</g>
<path class="green2" d="M147 51 H169 V66 H147Z"/>
<path class="green2" d="M147 77 H169 V92 H147Z"/>
<path class="rule-red" d="M4 112 H176"/>
<path class="rule-green" d="M178 112 H432"/>
<text class="tag" x="4" y="158">Качественные продукты по стоковым ценам</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.5 KiB

@@ -0,0 +1,37 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 310" role="img" aria-labelledby="title desc">
<title id="title">Рыбсток - логотип без зеленых перемычек</title>
<desc id="desc">Теневой вариант логотипа Рыбсток с очищенным стыком букв б и с.</desc>
<defs>
<style>
.bg{fill:#fffefa}
.green{fill:#173d2c}
.red{fill:#c43d31}
.paper{fill:#fffefa}
.shadow-green{fill:#173d2c;opacity:.13}
.shadow-red{fill:#c43d31;opacity:.12}
.word{font-family:Arial Black, Arial, Helvetica, sans-serif;font-size:138px;font-weight:950;letter-spacing:-9px}
.tag{font-family:Arial, Helvetica, sans-serif;font-size:28px;font-weight:900;letter-spacing:-.35px;fill:#173d2c}
.rule-red{fill:none;stroke:#c43d31;stroke-width:8;stroke-linecap:round}
.rule-green{fill:none;stroke:#173d2c;stroke-width:6;stroke-linecap:round}
.rule-shadow{fill:none;stroke:#173d2c;stroke-width:8;stroke-linecap:round;opacity:.12}
</style>
</defs>
<rect class="bg" width="900" height="310"/>
<g transform="translate(8 40)">
<text class="word shadow-green" x="10" y="104">Рыб</text>
<text class="word shadow-green" x="262" y="104">сток</text>
<text class="word green" x="0" y="92">Рыб</text>
<text class="word red" x="252" y="92">сток</text>
<path class="shadow-red" d="M226 17 C262 20 289 48 289 83 C289 118 262 145 226 148 L226 120 C247 117 262 102 262 83 C262 64 247 49 226 46Z"/>
<path class="red" d="M216 6 C251 9 278 36 278 70 C278 104 251 131 216 134 L216 107 C236 104 251 89 251 70 C251 51 236 36 216 34Z"/>
<path class="paper" d="M217 30 C235 34 247 50 247 70 C247 90 235 106 217 110 L217 87 C226 84 232 78 232 70 C232 62 226 56 217 53Z"/>
<path class="rule-shadow" d="M6 147 H620"/>
<path class="rule-red" d="M0 139 H236"/>
<path class="rule-green" d="M238 139 H620"/>
<text class="tag" x="0" y="198">Качественные продукты по стоковым ценам</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

@@ -0,0 +1,111 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1400 980" role="img" aria-labelledby="title desc">
<title id="title">Рыбсток - варианты черт в стыке логотипа</title>
<desc id="desc">Четыре варианта логотипа Рыбсток с разными графическими чертами в стыке букв б и с.</desc>
<defs>
<style>
.bg{fill:#f4f6ef}
.panel{fill:#fffefa;stroke:#dfe5da;stroke-width:2}
.green{fill:#173d2c}
.green2{fill:#27634a}
.red{fill:#c43d31}
.paper{fill:#fffefa}
.ice{stroke:#d9efef}
.shadow-green{fill:#173d2c;opacity:.13}
.label{font-family:Arial, Helvetica, sans-serif;font-size:22px;font-weight:900;fill:#657469}
.tag{font-family:Arial, Helvetica, sans-serif;font-size:22px;font-weight:900;letter-spacing:-.3px;fill:#173d2c}
.word{font-family:Arial Black, Arial, Helvetica, sans-serif;font-size:104px;font-weight:950;letter-spacing:-7px}
.rule-red{fill:none;stroke:#c43d31;stroke-width:7;stroke-linecap:round}
.rule-green{fill:none;stroke:#173d2c;stroke-width:5;stroke-linecap:round}
.rule-shadow{fill:none;stroke:#173d2c;stroke-width:7;stroke-linecap:round;opacity:.12}
.stroke{fill:none;stroke-linecap:round;stroke-linejoin:round}
</style>
</defs>
<rect class="bg" width="1400" height="980"/>
<g transform="translate(48 46)">
<rect class="panel" width="620" height="400" rx="18"/>
<text class="label" x="32" y="48">01 / короткие складские пазы</text>
<g transform="translate(40 132)">
<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="M173 31 C199 33 219 52 219 77 C219 102 199 121 173 123 L173 103 C188 101 199 91 199 77 C199 63 188 53 173 51Z"/>
<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="11" d="M145 58 H166"/>
<path class="stroke green2" stroke-width="11" d="M145 84 H166"/>
<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="158">Качественные продукты по стоковым ценам</text>
</g>
</g>
<g transform="translate(732 46)">
<rect class="panel" width="620" height="400" rx="18"/>
<text class="label" x="32" y="48">02 / ледяные прорези</text>
<g transform="translate(40 132)">
<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="M173 31 C199 33 219 52 219 77 C219 102 199 121 173 123 L173 103 C188 101 199 91 199 77 C199 63 188 53 173 51Z"/>
<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 ice" stroke-width="7" d="M143 55 H171"/>
<path class="stroke green2" stroke-width="5" d="M150 64 H168"/>
<path class="stroke ice" stroke-width="7" d="M143 83 H171"/>
<path class="stroke green2" stroke-width="5" d="M150 92 H168"/>
<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="158">Качественные продукты по стоковым ценам</text>
</g>
</g>
<g transform="translate(48 516)">
<rect class="panel" width="620" height="400" rx="18"/>
<text class="label" x="32" y="48">03 / ценовые штрихи</text>
<g transform="translate(40 132)">
<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="M173 31 C199 33 219 52 219 77 C219 102 199 121 173 123 L173 103 C188 101 199 91 199 77 C199 63 188 53 173 51Z"/>
<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="158">Качественные продукты по стоковым ценам</text>
</g>
</g>
<g transform="translate(732 516)">
<rect class="panel" width="620" height="400" rx="18"/>
<text class="label" x="32" y="48">04 / мягкие волокна склада</text>
<g transform="translate(40 132)">
<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="M173 31 C199 33 219 52 219 77 C219 102 199 121 173 123 L173 103 C188 101 199 91 199 77 C199 63 188 53 173 51Z"/>
<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="7" d="M142 57 C151 52 160 52 171 57"/>
<path class="stroke green2" stroke-width="7" d="M142 85 C151 90 160 90 171 85"/>
<path class="stroke paper" stroke-width="3" d="M145 57 C153 55 161 55 168 57"/>
<path class="stroke paper" stroke-width="3" d="M145 85 C153 87 161 87 168 85"/>
<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="158">Качественные продукты по стоковым ценам</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.0 KiB

@@ -0,0 +1,50 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 980 260" role="img" aria-labelledby="logo-title logo-desc">
<title id="logo-title">Рыбсток - доработка логотипа</title>
<desc id="logo-desc">Графичный логотип Рыбсток: рыба, склад, заморозка и стоковые цены.</desc>
<defs>
<style>
.green{fill:#143f2d}
.green-soft{fill:#2a6a50}
.red{fill:#c83d32}
.ice{fill:#dff2f2}
.ice-line{fill:none;stroke:#dff2f2;stroke-width:4;stroke-linecap:round;stroke-linejoin:round}
.white{fill:#fffefa}
.word-ryb{font-family:Arial Black, Arial, Helvetica, sans-serif;font-size:112px;font-weight:950;letter-spacing:-8px;fill:#143f2d}
.word-stock{font-family:Arial Black, Arial, Helvetica, sans-serif;font-size:96px;font-weight:950;letter-spacing:-6px;fill:#c83d32}
.word-wire{font-family:Arial Black, Arial, Helvetica, sans-serif;font-weight:950;letter-spacing:-7px;fill:none;stroke:#dff2f2;stroke-width:2.2;stroke-linejoin:round;opacity:.92}
.tag{font-family:Arial, Helvetica, sans-serif;font-size:26px;font-weight:900;letter-spacing:-.5px;fill:#143f2d}
</style>
</defs>
<rect width="980" height="260" rx="0" fill="#fffefa"/>
<g transform="translate(28 30)">
<rect class="green" x="0" y="0" width="170" height="170" rx="34"/>
<path class="red" d="M31 52 H139" stroke="#c83d32" stroke-width="11" stroke-linecap="round"/>
<path class="red" d="M31 112 H139" stroke="#c83d32" stroke-width="11" stroke-linecap="round"/>
<path class="green-soft" d="M50 136 H116" stroke="#2a6a50" stroke-width="12" stroke-linecap="round"/>
<path class="white" d="M34 84 C58 55 101 55 132 84 C101 113 58 113 34 84Z"/>
<path class="white" d="M128 84 L158 59 L158 109Z"/>
<circle class="green" cx="62" cy="81" r="7"/>
<path class="ice-line" d="M26 26 H52 M39 13 V39 M24 151 H45 M143 28 H154 M145 22 L154 31 M145 34 L154 25"/>
<path class="ice-line" d="M118 135 L137 151 M137 135 L118 151"/>
</g>
<g transform="translate(222 24)">
<text class="word-wire" x="0" y="115" font-size="112">РЫБ</text>
<text class="word-wire" x="175" y="115" font-size="96" letter-spacing="-6">СТОК</text>
<text class="word-ryb" x="0" y="115">РЫБ</text>
<text class="word-stock" x="175" y="115">СТОК</text>
<path class="ice-line" d="M16 20 H54 M35 2 V38 M424 18 H456 M440 3 V35 M596 32 H620"/>
<path class="ice-line" d="M112 140 L132 160 M132 140 L112 160 M332 143 L350 161 M350 143 L332 161"/>
<path class="red" d="M4 134 H164" stroke="#c83d32" stroke-width="7" stroke-linecap="round"/>
<path class="green-soft" d="M166 134 H326" stroke="#2a6a50" stroke-width="7" stroke-linecap="round"/>
<text class="tag" x="0" y="178">Качественные продукты по стоковым ценам</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

@@ -0,0 +1,101 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1400 980" role="img" aria-labelledby="title desc">
<title id="title">Рыбсток - теневые варианты логотипа</title>
<desc id="desc">Четыре варианта логотипа Рыбсток с теневым фирменным слоем.</desc>
<defs>
<style>
.bg{fill:#f4f6ef}
.panel{fill:#fffefa;stroke:#dfe5da;stroke-width:2}
.green{fill:#173d2c}
.green2{fill:#27634a}
.red{fill:#c43d31}
.paper{fill:#fffefa}
.ice{fill:#d9efef}
.shadow-green{fill:#b8cdc1}
.shadow-red{fill:#efc3bd}
.deep{fill:#092418;opacity:.18}
.label{font-family:Arial, Helvetica, sans-serif;font-size:22px;font-weight:900;fill:#657469}
.tag{font-family:Arial, Helvetica, sans-serif;font-size:22px;font-weight:900;letter-spacing:-.3px;fill:#173d2c}
.word{font-family:Arial Black, Arial, Helvetica, sans-serif;font-size:104px;font-weight:950;letter-spacing:-7px}
.rule-red{fill:none;stroke:#c43d31;stroke-width:7;stroke-linecap:round}
.rule-green{fill:none;stroke:#173d2c;stroke-width:5;stroke-linecap:round}
.rule-shadow{fill:none;stroke:#d9efef;stroke-width:8;stroke-linecap:round}
</style>
</defs>
<rect class="bg" width="1400" height="980"/>
<g transform="translate(48 46)">
<rect class="panel" width="620" height="400" rx="18"/>
<text class="label" x="32" y="48">01 / ледяной теневой слой</text>
<g transform="translate(40 132)">
<text class="word ice" x="6" y="87">Рыб</text>
<text class="word ice" x="196" y="87">сток</text>
<text class="word green" x="0" y="80">Рыб</text>
<text class="word red" x="190" y="80">сток</text>
<path class="ice" d="M170 29 C197 31 216 49 216 74 C216 99 197 118 170 120 L170 101 C185 99 196 89 196 75 C196 61 185 51 170 49Z"/>
<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="green2" d="M147 51 H169 V66 H147Z"/>
<path class="green2" d="M147 77 H169 V92 H147Z"/>
<path class="rule-shadow" d="M8 118 H432"/>
<path class="rule-red" d="M4 112 H176"/>
<path class="rule-green" d="M178 112 H432"/>
<text class="tag" x="4" y="158">Качественные продукты по стоковым ценам</text>
</g>
</g>
<g transform="translate(732 46)">
<rect class="panel" width="620" height="400" rx="18"/>
<text class="label" x="32" y="48">02 / складская глубина</text>
<g transform="translate(40 132)">
<text class="word deep" x="8" y="89">Рыб</text>
<text class="word deep" x="198" y="89">сток</text>
<text class="word green" x="0" y="80">Рыб</text>
<text class="word red" x="190" y="80">сток</text>
<path class="deep" d="M172 31 C198 33 218 51 218 76 C218 101 198 120 172 122 L172 102 C187 100 198 90 198 76 C198 62 187 52 172 50Z"/>
<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="green2" d="M147 51 H169 V66 H147Z"/>
<path class="green2" d="M147 77 H169 V92 H147Z"/>
<path class="rule-red" d="M4 112 H176"/>
<path class="rule-green" d="M178 112 H432"/>
<text class="tag" x="4" y="158">Качественные продукты по стоковым ценам</text>
</g>
</g>
<g transform="translate(48 516)">
<rect class="panel" width="620" height="400" rx="18"/>
<text class="label" x="32" y="48">03 / цветовой сдвиг, современно</text>
<g transform="translate(40 132)">
<text class="word shadow-red" x="5" y="82">Рыб</text>
<text class="word shadow-green" x="195" y="82">сток</text>
<text class="word green" x="0" y="80">Рыб</text>
<text class="word red" x="190" y="80">сток</text>
<path class="shadow-green" d="M169 24 C195 26 215 45 215 70 C215 95 195 114 169 116 L169 96 C184 94 195 84 195 70 C195 56 184 46 169 44Z"/>
<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="green2" d="M147 51 H169 V66 H147Z"/>
<path class="green2" d="M147 77 H169 V92 H147Z"/>
<path class="rule-red" d="M4 112 H176"/>
<path class="rule-green" d="M178 112 H432"/>
<text class="tag" x="4" y="158">Качественные продукты по стоковым ценам</text>
</g>
</g>
<g transform="translate(732 516)">
<rect class="panel" width="620" height="400" rx="18"/>
<text class="label" x="32" y="48">04 / мягкая премиальная тень</text>
<g transform="translate(40 132)">
<ellipse class="deep" cx="220" cy="128" rx="224" ry="14"/>
<text class="word green" x="0" y="80">Рыб</text>
<text class="word red" x="190" y="80">сток</text>
<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="green2" d="M147 51 H169 V66 H147Z"/>
<path class="green2" d="M147 77 H169 V92 H147Z"/>
<path class="rule-red" d="M4 112 H176"/>
<path class="rule-green" d="M178 112 H432"/>
<text class="tag" x="4" y="158">Качественные продукты по стоковым ценам</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.9 KiB

@@ -0,0 +1,85 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1400 980" role="img" aria-labelledby="title desc">
<title id="title">Рыбсток - варианты слогана</title>
<desc id="desc">Четыре варианта слогана и типографики для выбранного логотипа Рыбсток.</desc>
<defs>
<style>
.bg{fill:#f4f6ef}
.panel{fill:#fffefa;stroke:#dfe5da;stroke-width:2}
.green{fill:#173d2c}
.green2{fill:#27634a}
.red{fill:#c43d31}
.paper{fill:#fffefa}
.shadow-green{fill:#173d2c;opacity:.13}
.label{font-family:Arial, Helvetica, sans-serif;font-size:22px;font-weight:900;fill:#657469}
.word{font-family:Arial Black, Arial, Helvetica, sans-serif;font-size:104px;font-weight:950;letter-spacing:-7px}
.rule-red{fill:none;stroke:#c43d31;stroke-width:7;stroke-linecap:round}
.rule-green{fill:none;stroke:#173d2c;stroke-width:5;stroke-linecap:round}
.rule-shadow{fill:none;stroke:#173d2c;stroke-width:7;stroke-linecap:round;opacity:.12}
.stroke{fill:none;stroke-linecap:round;stroke-linejoin:round}
.tag-bold{font-family:Arial, Helvetica, sans-serif;font-size:25px;font-weight:900;letter-spacing:-.25px;fill:#173d2c}
.tag-caps{font-family:Arial, Helvetica, sans-serif;font-size:19px;font-weight:900;letter-spacing:2.4px;fill:#173d2c}
.tag-serif{font-family:Georgia, 'Times New Roman', serif;font-size:26px;font-weight:700;letter-spacing:.1px;fill:#173d2c}
.tag-small{font-family:Arial, Helvetica, sans-serif;font-size:18px;font-weight:800;letter-spacing:.8px;fill:#657469}
.pill{fill:#f4f6ef;stroke:#dfe5da;stroke-width:2}
</style>
</defs>
<rect class="bg" width="1400" height="980"/>
<g id="mark">
<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"/>
</g>
<g transform="translate(48 46)">
<rect class="panel" width="620" height="400" rx="18"/>
<text class="label" x="32" y="48">01 / самый прямой и сильный</text>
<g transform="translate(40 132)">
<use href="#mark"/>
<text class="tag-bold" x="4" y="162">Качество со склада. <tspan class="red">Стоковые цены.</tspan></text>
</g>
</g>
<g transform="translate(732 46)">
<rect class="panel" width="620" height="400" rx="18"/>
<text class="label" x="32" y="48">02 / премиальнее, с паузой</text>
<g transform="translate(40 132)">
<use href="#mark"/>
<text class="tag-serif" x="4" y="160">Качественные продукты</text>
<text class="tag-caps red" x="4" y="190">ПО СТОКОВЫМ ЦЕНАМ</text>
</g>
</g>
<g transform="translate(48 516)">
<rect class="panel" width="620" height="400" rx="18"/>
<text class="label" x="32" y="48">03 / знак как ценовая система</text>
<g transform="translate(40 132)">
<use href="#mark"/>
<rect class="pill" x="4" y="139" width="426" height="45" rx="22"/>
<text class="tag-small" x="27" y="168">ОТБОРНОЕ КАЧЕСТВО</text>
<text class="tag-small red" x="258" y="168">СТОКОВЫЕ ЦЕНЫ</text>
</g>
</g>
<g transform="translate(732 516)">
<rect class="panel" width="620" height="400" rx="18"/>
<text class="label" x="32" y="48">04 / мягче и человечнее</text>
<g transform="translate(40 132)">
<use href="#mark"/>
<text class="tag-bold" x="4" y="160">Продукты для своих</text>
<text class="tag-bold red" x="242" y="160">по стоковым ценам</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

@@ -0,0 +1,75 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1400 980" role="img" aria-labelledby="title desc">
<title id="title">Типографические варианты логотипа Рыбсток</title>
<desc id="desc">Четыре чистых wordmark-варианта Рыбсток без иконок и наложений.</desc>
<defs>
<style>
.bg{fill:#f4f6ef}
.panel{fill:#fffefa;stroke:#dfe5da;stroke-width:2}
.green{fill:#173d2c}
.green2{fill:#27634a}
.red{fill:#c43d31}
.ice{fill:#d9efef}
.muted{fill:#657469}
.label{font-family:Arial, Helvetica, sans-serif;font-size:22px;font-weight:900;fill:#657469}
.tag{font-family:Arial, Helvetica, sans-serif;font-size:23px;font-weight:800;letter-spacing:-.3px;fill:#173d2c}
.wm-a{font-family:Arial Black, Arial, Helvetica, sans-serif;font-size:104px;font-weight:950;letter-spacing:-6px}
.wm-b{font-family:Georgia, 'Times New Roman', serif;font-size:108px;font-weight:800;letter-spacing:-4px}
.wm-c{font-family:Trebuchet MS, Arial, sans-serif;font-size:112px;font-weight:900;letter-spacing:-5px}
.wm-d{font-family:Verdana, Arial, sans-serif;font-size:92px;font-weight:900;letter-spacing:-4px}
.wire{fill:none;stroke:#d9efef;stroke-width:2;stroke-linejoin:round}
.thin{fill:none;stroke:#173d2c;stroke-width:5;stroke-linecap:round}
.redline{fill:none;stroke:#c43d31;stroke-width:7;stroke-linecap:round}
</style>
</defs>
<rect class="bg" width="1400" height="980"/>
<g transform="translate(48 46)">
<rect class="panel" width="620" height="400" rx="18"/>
<text class="label" x="32" y="48">01 / уверенный складской wordmark</text>
<g transform="translate(42 132)">
<text class="wm-a green" x="0" y="80">Рыб</text>
<text class="wm-a red" x="188" y="80">сток</text>
<path class="redline" d="M4 108 H178"/>
<path class="thin" d="M188 108 H430"/>
<text class="tag" x="4" y="155">Качественные продукты по стоковым ценам</text>
</g>
</g>
<g transform="translate(732 46)">
<rect class="panel" width="620" height="400" rx="18"/>
<text class="label" x="32" y="48">02 / более дорогой, спокойный</text>
<g transform="translate(48 128)">
<text class="wm-b green" x="0" y="86">Рыб</text>
<text class="wm-b red" x="180" y="86">сток</text>
<circle class="red" cx="172" cy="56" r="5"/>
<path class="thin" d="M8 114 H430"/>
<text class="tag" x="8" y="160">Качественные продукты по стоковым ценам</text>
</g>
</g>
<g transform="translate(48 516)">
<rect class="panel" width="620" height="400" rx="18"/>
<text class="label" x="32" y="48">03 / холодный, технологичный</text>
<g transform="translate(42 130)">
<text class="wm-c wire" x="0" y="84">Рыбсток</text>
<text class="wm-c green" x="0" y="84">Рыб</text>
<text class="wm-c red" x="191" y="84">сток</text>
<path class="redline" d="M12 112 H126 M358 112 H466"/>
<path fill="none" stroke="#d9efef" stroke-width="6" stroke-linecap="round" d="M144 112 H337"/>
<text class="tag" x="8" y="160">Качественные продукты по стоковым ценам</text>
</g>
</g>
<g transform="translate(732 516)">
<rect class="panel" width="620" height="400" rx="18"/>
<text class="label" x="32" y="48">04 / современный маркет, мягче</text>
<g transform="translate(48 136)">
<rect x="0" y="0" width="476" height="112" rx="28" fill="#edf4ea"/>
<text class="wm-d green" x="26" y="79">Рыб</text>
<text class="wm-d red" x="196" y="79">сток</text>
<path class="redline" d="M28 95 H150"/>
<text class="tag" x="3" y="162">Качественные продукты по стоковым ценам</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

+34 -13
View File
@@ -1,20 +1,41 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 390 108" role="img" aria-labelledby="logo-title logo-desc">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 650 220" role="img" aria-labelledby="logo-title logo-desc">
<title id="logo-title">Рыбсток</title>
<desc id="logo-desc">РЫБСТОК - качественные продукты по стоковым ценам</desc>
<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}
.green{fill:#173d2c}
.green2{fill:#27634a}
.red{fill:#c43d31}
.paper{fill:#fffefa}
.shadow-green{fill:#173d2c;opacity:.13}
.word{font-family:Arial Black, Arial, Helvetica, sans-serif;font-size:104px;font-weight:950;letter-spacing:-7px}
.tag{font-family:Arial, Helvetica, sans-serif;font-size:25px;font-weight:900;letter-spacing:-.25px;fill:#173d2c}
.rule-red{fill:none;stroke:#c43d31;stroke-width:7;stroke-linecap:round}
.rule-green{fill:none;stroke:#173d2c;stroke-width:5;stroke-linecap:round}
.rule-shadow{fill:none;stroke:#173d2c;stroke-width:7;stroke-linecap:round;opacity:.12}
.stroke{fill:none;stroke-linecap:round;stroke-linejoin:round}
</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 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>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

+44
View File
@@ -0,0 +1,44 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 520" role="img" aria-labelledby="title desc">
<title id="title">Рефрижератор Рыбсток в Москве</title>
<desc id="desc">Фирменная машина Рыбсток для доставки внутри МКАД</desc>
<defs>
<linearGradient id="sky" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#d9edf4"/>
<stop offset="1" stop-color="#f7f2e9"/>
</linearGradient>
<linearGradient id="van" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#fffdf7"/>
<stop offset="1" stop-color="#dfe8e2"/>
</linearGradient>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="18" stdDeviation="18" flood-color="#073322" flood-opacity=".25"/>
</filter>
</defs>
<rect width="960" height="520" rx="34" fill="url(#sky)"/>
<path d="M0 377c158-42 306-49 444-20 131 27 272 22 516-42v205H0z" fill="#cddfd3"/>
<path d="M0 417c210-41 386-38 530 8 130 42 247 38 430-12v107H0z" fill="#f4efe6"/>
<g opacity=".42" fill="#ffffff">
<rect x="62" y="100" width="76" height="185" rx="7"/>
<rect x="152" y="75" width="105" height="220" rx="9"/>
<rect x="282" y="118" width="92" height="177" rx="8"/>
<rect x="748" y="90" width="132" height="214" rx="10"/>
</g>
<g filter="url(#shadow)" transform="translate(130 150)">
<path d="M54 124c0-34 28-62 62-62h378c25 0 48 14 59 36l35 69h57c26 0 47 21 47 47v62H54z" fill="url(#van)"/>
<path d="M79 55c0-27 22-49 49-49h332c27 0 49 22 49 49v202H79z" fill="#f9fbf5"/>
<path d="M511 107h38c18 0 34 10 43 26l27 51H511z" fill="#d7ecf0"/>
<path d="M111 42h360v51H111z" fill="#144631"/>
<text x="130" y="78" font-family="Arial, sans-serif" font-size="42" font-weight="900" fill="#fff">РЫБ</text>
<text x="233" y="78" font-family="Arial, sans-serif" font-size="42" font-weight="900" fill="#d23b30">СТОК</text>
<text x="112" y="134" font-family="Arial, sans-serif" font-size="20" font-weight="700" fill="#144631">Качество со склада. Стоковые цены.</text>
<path d="M86 174h396" stroke="#d23b30" stroke-width="8" stroke-linecap="round"/>
<path d="M86 194h396" stroke="#144631" stroke-width="8" stroke-linecap="round"/>
<rect x="102" y="218" width="104" height="22" rx="11" fill="#eaf3ed"/>
<rect x="230" y="218" width="145" height="22" rx="11" fill="#eaf3ed"/>
<circle cx="170" cy="287" r="44" fill="#10261d"/>
<circle cx="170" cy="287" r="22" fill="#dfe9e1"/>
<circle cx="568" cy="287" r="44" fill="#10261d"/>
<circle cx="568" cy="287" r="22" fill="#dfe9e1"/>
<path d="M36 257h676" stroke="#154631" stroke-width="18" stroke-linecap="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

+42
View File
@@ -0,0 +1,42 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 520" role="img" aria-labelledby="title desc">
<title id="title">Рефрижератор Рыбсток за МКАД</title>
<desc id="desc">Фирменная машина Рыбсток для доставки по Московской области</desc>
<defs>
<linearGradient id="sunset" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#f3d9b6"/>
<stop offset=".58" stop-color="#cfe7df"/>
<stop offset="1" stop-color="#6d9ab1"/>
</linearGradient>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="20" stdDeviation="20" flood-color="#0a3323" flood-opacity=".28"/>
</filter>
</defs>
<rect width="960" height="520" rx="34" fill="url(#sunset)"/>
<circle cx="792" cy="92" r="42" fill="#fff2bf" opacity=".82"/>
<path d="M0 318c100-64 208-84 333-48 122 35 205 35 318 0 109-34 208-20 309 38v212H0z" fill="#80a884" opacity=".7"/>
<path d="M0 395c150-39 319-33 510 17 139 36 283 33 450-8v116H0z" fill="#f6efe4"/>
<g fill="#184631" opacity=".38">
<path d="M80 294c24-65 55-65 80 0z"/>
<path d="M154 306c31-83 71-83 102 0z"/>
<path d="M704 288c28-76 64-76 92 0z"/>
<path d="M778 306c35-94 80-94 115 0z"/>
</g>
<g filter="url(#shadow)" transform="translate(110 152)">
<path d="M48 118c0-35 28-63 63-63h391c27 0 51 16 62 41l28 65h77c25 0 45 20 45 45v74H48z" fill="#fffdf5"/>
<path d="M76 48c0-27 22-48 48-48h343c27 0 48 22 48 48v208H76z" fill="#f7faf2"/>
<path d="M520 103h44c18 0 35 11 42 28l23 53H520z" fill="#cde7ec"/>
<path d="M108 38h371v57H108z" fill="#143f2d"/>
<text x="129" y="80" font-family="Arial, sans-serif" font-size="43" font-weight="900" fill="#fff">РЫБ</text>
<text x="235" y="80" font-family="Arial, sans-serif" font-size="43" font-weight="900" fill="#d23b30">СТОК</text>
<text x="110" y="135" font-family="Arial, sans-serif" font-size="20" font-weight="700" fill="#143f2d">Доставка по Москве и области</text>
<path d="M94 172h405" stroke="#d23b30" stroke-width="8" stroke-linecap="round"/>
<path d="M94 193h405" stroke="#143f2d" stroke-width="8" stroke-linecap="round"/>
<rect x="104" y="221" width="124" height="23" rx="12" fill="#e8f0e8"/>
<rect x="250" y="221" width="168" height="23" rx="12" fill="#e8f0e8"/>
<circle cx="174" cy="292" r="45" fill="#10261d"/>
<circle cx="174" cy="292" r="22" fill="#dfe9e1"/>
<circle cx="590" cy="292" r="45" fill="#10261d"/>
<circle cx="590" cy="292" r="22" fill="#dfe9e1"/>
<path d="M31 261h702" stroke="#144631" stroke-width="18" stroke-linecap="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 924 B

+189 -1
View File
@@ -3,16 +3,204 @@
declare(strict_types=1);
use App\Controllers\CatalogController;
use App\Controllers\CartController;
use App\Controllers\HomeController;
use App\Controllers\PageController;
use App\Controllers\PriceRequestController;
use App\Controllers\WishlistRequestController;
use App\Core\Router;
use App\Repositories\CategoryRepository;
use App\Repositories\ProductRepository;
require_once dirname(__DIR__) . '/app/bootstrap.php';
$router = new Router();
$absoluteSiteUrl = static function (): string {
$siteUrl = rtrim((string) app_env('SITE_URL', ''), '/');
if ($siteUrl !== '') {
return $siteUrl;
}
$host = preg_replace('/[^a-z0-9.\-:]/i', '', (string) ($_SERVER['HTTP_HOST'] ?? 'new.rybstock.ru'));
return 'https://' . $host;
};
$xmlEscape = static fn (string $value): string => htmlspecialchars($value, ENT_XML1 | ENT_COMPAT, 'UTF-8');
$sitemapHandler = static function () use ($absoluteSiteUrl, $xmlEscape): void {
$baseUrl = $absoluteSiteUrl();
$today = date('Y-m-d');
$urls = [];
$addUrl = static function (
string $path,
string $priority,
string $changefreq = 'weekly',
?string $lastmod = null
) use (&$urls, $baseUrl, $today): void {
$urls[] = [
'loc' => $baseUrl . ($path === '/' ? '/' : '/' . ltrim($path, '/')),
'lastmod' => $lastmod ?: $today,
'changefreq' => $changefreq,
'priority' => $priority,
];
};
$addUrl('/', '1.0', 'daily');
$addUrl('/catalog', '0.9', 'daily');
$addUrl('/promo', '0.8', 'daily');
$addUrl('/new', '0.8', 'daily');
$addUrl('/delivery', '0.6', 'monthly');
$addUrl('/payment', '0.5', 'monthly');
$addUrl('/contacts', '0.5', 'monthly');
$addUrl('/returns', '0.4', 'monthly');
try {
foreach ((new CategoryRepository())->active() as $category) {
$addUrl('/catalog/' . (string) $category['slug'], '0.7', 'weekly');
}
foreach ((new ProductRepository())->sitemapProducts() as $product) {
$dateSource = $product['updated_at'] ?: ($product['published_at'] ?: $product['created_at']);
$lastmod = $dateSource ? date('Y-m-d', strtotime((string) $dateSource)) : $today;
$addUrl('/product/' . (string) $product['slug'], '0.6', 'weekly', $lastmod);
}
} catch (Throwable) {
// Keep a valid sitemap available even during database maintenance.
}
header('Content-Type: application/xml; charset=UTF-8');
echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
echo "<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n";
foreach ($urls as $url) {
echo " <url>\n";
echo ' <loc>' . $xmlEscape($url['loc']) . "</loc>\n";
echo ' <lastmod>' . $xmlEscape($url['lastmod']) . "</lastmod>\n";
echo ' <changefreq>' . $xmlEscape($url['changefreq']) . "</changefreq>\n";
echo ' <priority>' . $xmlEscape($url['priority']) . "</priority>\n";
echo " </url>\n";
}
echo "</urlset>\n";
};
$robotsHandler = static function () use ($absoluteSiteUrl): void {
header('Content-Type: text/plain; charset=UTF-8');
echo "User-agent: *\n";
echo "Allow: /\n";
echo "Disallow: /admin/\n";
echo "Disallow: /public/admin/\n";
echo "Disallow: /cart\n";
echo "\n";
echo 'Sitemap: ' . $absoluteSiteUrl() . "/sitemap.xml\n";
};
$catalogSearchHandler = static function (?string $rawQuery = null): void {
$queryParameters = [];
parse_str((string) (parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_QUERY) ?? ''), $queryParameters);
$query = trim((string) ($rawQuery ?? $_GET['q'] ?? $queryParameters['q'] ?? ''));
if ($query === '') {
(new CatalogController())->index();
return;
}
$_GET['q'] = $query;
$perPage = 48;
$limit = isset($_GET['limit']) ? max($perPage, min((int) $_GET['limit'], 5000)) : $perPage;
$page = max(1, (int) ($_GET['page'] ?? 1));
$offset = isset($_GET['limit']) ? 0 : (($page - 1) * $perPage);
$categoryRepository = new CategoryRepository();
$productRepository = new ProductRepository();
if (method_exists($productRepository, 'searchCatalog')) {
$searchResult = $productRepository->searchCatalog($query, $limit, $offset, true);
$products = $searchResult['products'];
$total = $searchResult['total'];
} else {
$allProducts = $productRepository->catalogPreview(5000, 0, true);
$needle = function_exists('mb_strtolower') ? mb_strtolower($query, 'UTF-8') : strtolower($query);
$words = array_values(array_filter(preg_split('/\s+/u', $needle) ?: []));
$ranked = [];
foreach ($allProducts as $product) {
$haystack = implode(' ', [
(string) ($product['name'] ?? ''),
(string) ($product['short_description'] ?? ''),
(string) ($product['category_name'] ?? ''),
(string) ($product['display_category_name'] ?? ''),
(string) ($product['categories_path'] ?? ''),
]);
$haystack = function_exists('mb_strtolower') ? mb_strtolower($haystack, 'UTF-8') : strtolower($haystack);
$score = 0;
if ($needle !== '' && str_contains($haystack, $needle)) {
$score += 100;
}
foreach ($words as $word) {
if ($word !== '' && str_contains($haystack, $word)) {
$score += 20;
}
}
if ($score > 0) {
$product['_search_score'] = $score;
$ranked[] = $product;
}
}
usort($ranked, static fn (array $a, array $b): int => ($b['_search_score'] ?? 0) <=> ($a['_search_score'] ?? 0));
$total = count($ranked);
$products = array_slice($ranked, $offset, $limit);
}
$shown = min($total, $offset + count($products));
$basePath = '/search/' . rawurlencode($query);
view('catalog/index', [
'title' => 'Поиск по каталогу Рыбсток - ' . $query,
'metaDescription' => 'Результаты поиска по каталогу Рыбсток: ' . $query,
'metaKeywords' => $query . ', Рыбсток, каталог продуктов',
'breadcrumbs' => [
['title' => 'Главная', 'url' => '/'],
['title' => 'Каталог', 'url' => '/catalog'],
['title' => 'Поиск'],
],
'categories' => $categoryRepository->tree(),
'currentCategory' => null,
'products' => $products,
'searchQuery' => $query,
'pagination' => [
'base_path' => $basePath,
'total' => $total,
'shown' => $shown,
'page' => $page,
'pages' => max(1, (int) ceil($total / $perPage)),
'per_page' => $perPage,
'limit' => $limit,
'has_more' => $shown < $total,
'next_limit' => min($total, max($shown, $limit) + $perPage),
'cumulative' => isset($_GET['limit']),
],
]);
};
$router->get('/', static fn () => (new HomeController())->index());
$router->get('/catalog', static fn () => (new CatalogController())->index());
$router->get('/robots.txt', $robotsHandler);
$router->get('/sitemap.xml', $sitemapHandler);
$router->post('/price-request', static fn () => (new PriceRequestController())->store());
$router->post('/wishlist-request', static fn () => (new WishlistRequestController())->store());
$router->post('/api/cart/sync', static fn () => (new CartController())->sync());
$router->get('/search', static fn () => $catalogSearchHandler());
$router->get('/search/{query}', static fn (string $query) => $catalogSearchHandler($query));
$router->get('/promo', static fn () => (new CatalogController())->promo());
$router->get('/new', static fn () => (new CatalogController())->newest());
$router->get('/catalog', static fn () => trim((string) ($_GET['q'] ?? '')) !== '' ? $catalogSearchHandler() : (new CatalogController())->index());
$router->get('/delivery', static fn () => (new PageController())->delivery());
$router->get('/payment', static fn () => (new PageController())->payment());
$router->get('/contacts', static fn () => (new PageController())->contacts());
+1 -1
View File
@@ -82,7 +82,7 @@ function run_sql_file(PDO $pdo, string $path): void
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Установка базы Рыбсток</title>
<link rel="stylesheet" href="/assets/css/app.css">
<link rel="stylesheet" href="<?= e(versioned_asset('/assets/css/app.css')) ?>">
</head>
<body>
<main class="page">
+55
View File
@@ -0,0 +1,55 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Концепты логотипа Рыбсток</title>
<style>
* { box-sizing: border-box; }
body {
margin: 0;
font-family: "Segoe UI", Arial, sans-serif;
color: #17211c;
background: #f4f6ef;
}
main {
width: min(1280px, calc(100% - 32px));
margin: 0 auto;
padding: 28px 0 44px;
}
h1 {
margin: 0 0 8px;
color: #173d2c;
font-size: clamp(28px, 4vw, 48px);
}
p {
max-width: 820px;
margin: 0 0 24px;
color: #657469;
font-size: 17px;
line-height: 1.55;
}
.board {
overflow: hidden;
border: 1px solid #dfe5da;
border-radius: 12px;
background: #fffefa;
box-shadow: 0 20px 70px rgba(23, 61, 44, .09);
}
img {
display: block;
width: 100%;
height: auto;
}
</style>
</head>
<body>
<main>
<h1>Концепты логотипа Рыбсток</h1>
<p>Берем выбранный знак и подбираем к нему слоган: сохраняем смысл про качество и стоковые цены, но пробуем более фирменную подачу.</p>
<section class="board">
<img src="/assets/images/logo-rybstock-slogan-01.svg" alt="Варианты слогана логотипа Рыбсток">
</section>
</main>
</body>
</html>
+20
View File
@@ -0,0 +1,20 @@
{
"name": "Рыбсток",
"short_name": "Рыбсток",
"description": "Качество со склада. Стоковые цены.",
"icons": [
{
"src": "/assets/images/brand/favicon-rybstock-monogram-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/assets/images/brand/favicon-rybstock-monogram-512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#173d2c",
"background_color": "#fffefa",
"display": "standalone"
}
+107
View File
@@ -0,0 +1,107 @@
const fs = require('fs');
const path = require('path');
const { chromium } = require('playwright');
const outDir = path.join('public', 'assets', 'images', 'brand');
fs.mkdirSync(outDir, { recursive: true });
const fullLogoSvg = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 650 220">
<defs>
<style>
.green{fill:#173d2c}.green2{fill:#27634a}.red{fill:#c43d31}.paper{fill:#fffefa}.shadow-green{fill:#173d2c;opacity:.13}
.word{font-family:Arial Black, Arial, Helvetica, sans-serif;font-size:104px;font-weight:950;letter-spacing:-7px}
.tag{font-family:Arial, Helvetica, sans-serif;font-size:25px;font-weight:900;letter-spacing:-.25px;fill:#173d2c}
.rule-red{fill:none;stroke:#c43d31;stroke-width:7;stroke-linecap:round}.rule-green{fill:none;stroke:#173d2c;stroke-width:5;stroke-linecap:round}.rule-shadow{fill:none;stroke:#173d2c;stroke-width:7;stroke-linecap:round;opacity:.12}.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>`;
const iconMonogramSvg = `
<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>`;
const iconFishSvg = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<rect width="512" height="512" rx="112" fill="#173d2c"/>
<path d="M120 176 C222 181 292 248 292 256 C292 264 222 331 120 336 C96 315 76 286 68 256 C76 226 96 197 120 176Z" fill="#fffefa"/>
<circle cx="145" cy="256" r="17" fill="#173d2c"/>
<path d="M292 256 L390 190 L390 322Z" fill="#fffefa"/>
<path d="M90 118 H344" fill="none" stroke="#c43d31" stroke-width="26" stroke-linecap="round"/>
<path d="M90 394 H344" fill="none" stroke="#c43d31" stroke-width="26" stroke-linecap="round"/>
<path d="M146 440 H295" fill="none" stroke="#27634a" stroke-width="20" stroke-linecap="round"/>
</svg>`;
async function pngFromSvg(page, svg, file, width, height = null) {
const targetHeight = height ?? Math.round((width / 650) * 220);
await page.setViewportSize({ width, height: targetHeight });
await page.setContent(`
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<style>
html, body { margin: 0; width: ${width}px; height: ${targetHeight}px; background: transparent; overflow: hidden; }
svg { display: block; width: ${width}px; height: ${targetHeight}px; }
</style>
</head>
<body>${svg}</body>
</html>
`);
await page.locator('svg').screenshot({ path: file, omitBackground: true });
}
async function main() {
fs.writeFileSync(path.join(outDir, 'logo-rybstock-source.svg'), fullLogoSvg.trim(), 'utf8');
fs.writeFileSync(path.join(outDir, 'favicon-rybstock-monogram-source.svg'), iconMonogramSvg.trim(), 'utf8');
fs.writeFileSync(path.join(outDir, 'favicon-rybstock-fish-source.svg'), iconFishSvg.trim(), 'utf8');
const edgePath = 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe';
const browser = await chromium.launch(fs.existsSync(edgePath) ? { executablePath: edgePath } : {});
const page = await browser.newPage({ deviceScaleFactor: 1 });
try {
for (const width of [320, 640, 1280, 1920]) {
await pngFromSvg(page, fullLogoSvg, path.join(outDir, `logo-rybstock-${width}.png`), width);
}
for (const width of [16, 32, 48, 64, 180, 192, 512]) {
await pngFromSvg(page, iconMonogramSvg, path.join(outDir, `favicon-rybstock-monogram-${width}.png`), width, width);
await pngFromSvg(page, iconFishSvg, path.join(outDir, `favicon-rybstock-fish-${width}.png`), width, width);
}
await pngFromSvg(page, iconMonogramSvg, path.join('public', 'favicon-16x16.png'), 16, 16);
await pngFromSvg(page, iconMonogramSvg, path.join('public', 'favicon-32x32.png'), 32, 32);
await pngFromSvg(page, iconMonogramSvg, path.join('public', 'apple-touch-icon.png'), 180, 180);
} finally {
await browser.close();
}
}
main().catch((error) => {
console.error(error);
process.exit(1);
});
+144 -15
View File
@@ -5,43 +5,172 @@ declare(strict_types=1);
/** @var array<int, array<string, mixed>> $categories */
/** @var array<string, mixed>|null $currentCategory */
/** @var array<int, array<string, mixed>> $products */
/** @var array<string, mixed> $pagination */
/** @var string|null $searchQuery */
/** @var string|null $catalogHeading */
/** @var string|null $catalogEyebrow */
/** @var string|null $catalogLead */
$heading = $currentCategory['h1'] ?? $currentCategory['name'] ?? 'Каталог продукции';
$heading = $catalogHeading ?? $currentCategory['h1'] ?? $currentCategory['name'] ?? 'Каталог продукции';
$eyebrow = $catalogEyebrow ?? ($currentCategory === null ? 'Полный каталог' : 'Категория');
$catalogLead = trim((string) ($catalogLead ?? ''));
$categoryDescription = trim((string) ($currentCategory['description'] ?? ''));
$searchQuery = trim((string) ($searchQuery ?? ''));
$pagination = $pagination ?? [
'base_path' => '/catalog',
'total' => count($products),
'shown' => count($products),
'page' => 1,
'pages' => 1,
'per_page' => 48,
'has_more' => false,
'next_limit' => count($products),
];
$paginationGlue = str_contains((string) ($pagination['base_path'] ?? ''), '?') ? '&' : '?';
?>
<section class="catalog-layout">
<aside class="catalog-sidebar">
<div class="panel sticky-panel">
<h2>Категории</h2>
<?php
$items = $categories;
$depth = 0;
$currentSlug = $currentCategory['slug'] ?? '';
require base_path('views/partials/category-tree.php');
?>
<div class="catalog-side-stack">
<div class="panel catalog-menu-panel">
<div class="catalog-side-heading">
<span>Каталог</span>
<small><?= e((string) $pagination['total']) ?> позиций</small>
</div>
<?php
$items = $categories;
$depth = 0;
$currentSlug = $currentCategory['slug'] ?? '';
require base_path('views/partials/category-tree.php');
?>
</div>
<div class="panel catalog-mini-cart" data-mini-cart>
<div>
<span>Корзина</span>
<strong data-mini-cart-total>0 руб.</strong>
</div>
<p><span data-mini-cart-count>0</span> позиций</p>
<a href="/cart">Перейти</a>
</div>
</div>
</aside>
<section class="catalog-content">
<div class="catalog-heading">
<p class="eyebrow"><?= $currentCategory === null ? 'Полный каталог' : 'Категория' ?></p>
<p class="eyebrow"><?= e($eyebrow) ?></p>
<h1><?= e($heading) ?></h1>
<?php if (!empty($currentCategory['description'])): ?>
<p><?= e($currentCategory['description']) ?></p>
<?php else: ?>
<p class="muted">Выберите категорию слева или смотрите товары сразу в каталоге.</p>
<?php if ($catalogLead !== ''): ?>
<p class="catalog-heading-lead"><?= e($catalogLead) ?></p>
<?php endif; ?>
</div>
<?php if ($searchQuery !== ''): ?>
<section class="catalog-search-result-panel" aria-label="Результаты умного поиска">
<div>
<p class="eyebrow">Умный поиск</p>
<h2>Поиск по запросу: <?= e($searchQuery) ?></h2>
<p>Учитываем названия, категории, фасовки, похожие формулировки и бытовую логику покупки.</p>
</div>
<a href="/catalog">Сбросить поиск</a>
</section>
<?php endif; ?>
<section class="payment-benefit-banner cash-benefit-banner" aria-label="Выгода при оплате наличными">
<div class="cash-benefit-copy">
<p class="eyebrow">Выгода считается автоматически</p>
<h2>Оплачивайте наличными при получении и получайте лучшие условия</h2>
<p>Добавляйте товары в корзину, а сайт сам пересчитает итог от суммы заказа. Ничего считать вручную не нужно.</p>
</div>
<div class="cash-receipt" aria-hidden="true">
<div class="cash-receipt-top">
<span>Чек выгоды</span>
<strong>Рыбсток</strong>
</div>
<dl>
<div>
<dt>от 7 000р.</dt>
<dd>-300р.</dd>
</div>
<div>
<dt>от 20 000р.</dt>
<dd>-5%</dd>
</div>
<div>
<dt>от 50 000р.</dt>
<dd>-7%</dd>
</div>
</dl>
<small>Пересчет появится в корзине сразу</small>
</div>
</section>
<section class="payment-benefit-banner qr-benefit-banner" aria-label="Преимущество оплаты QR-кодом">
<div class="cash-benefit-copy">
<p class="eyebrow">Удобная оплата при получении</p>
<h2>Принимаем оплату QR-кодом без комиссии даже с кредитной карты</h2>
<p>После подтверждения заказа менеджер согласует получение, а при передаче товара можно оплатить QR-кодом без лишних комиссий.</p>
</div>
<div class="payment-benefit-card" aria-hidden="true">
<span>QR-оплата</span>
<strong>0%</strong>
<p>комиссия для покупателя</p>
<small>Наличными пользоваться необязательно</small>
</div>
</section>
<section class="payment-benefit-banner invoice-benefit-banner" aria-label="Оплата по расчетному счету">
<div class="cash-benefit-copy">
<p class="eyebrow">Для организаций и крупных заказов</p>
<h2>Счет на оплату выставляем день в день</h2>
<p>После подтверждения заказа менеджер подготовит счет и согласует документы. Безналичный расчет доступен для заказов от 30 000р.</p>
</div>
<div class="payment-benefit-card" aria-hidden="true">
<span>Расчетный счет</span>
<strong>день в день</strong>
<p>после подтверждения заказа</p>
<small>Документы согласуем с менеджером</small>
</div>
</section>
<?php if ($products === []): ?>
<div class="panel">
<p class="muted">В этой категории товары появятся после переноса каталога со старого сайта.</p>
<p class="muted"><?= $searchQuery !== '' ? 'По этому запросу пока ничего не найдено. Попробуйте другое название, фасовку или назначение.' : 'В этой категории товары появятся после переноса каталога со старого сайта.' ?></p>
</div>
<?php else: ?>
<div class="catalog-count-panel">
<span>Показано <?= e((string) $pagination['shown']) ?> из <?= e((string) $pagination['total']) ?> товаров</span>
<?php if (!empty($pagination['has_more'])): ?>
<a href="<?= e((string) $pagination['base_path']) ?><?= e($paginationGlue) ?>limit=<?= e((string) $pagination['next_limit']) ?>">Показать еще</a>
<?php endif; ?>
</div>
<div class="product-grid catalog-product-grid">
<?php foreach ($products as $product): ?>
<?php require base_path('views/partials/product-card.php'); ?>
<?php endforeach; ?>
</div>
<?php if ((int) $pagination['pages'] > 1): ?>
<nav class="catalog-pagination" aria-label="Страницы каталога">
<?php for ($pageNumber = 1; $pageNumber <= (int) $pagination['pages']; $pageNumber++): ?>
<a
class="<?= $pageNumber === (int) $pagination['page'] && empty($pagination['cumulative']) ? 'is-current' : '' ?>"
href="<?= e((string) $pagination['base_path']) ?><?= e($paginationGlue) ?>page=<?= e((string) $pageNumber) ?>"
>
<?= e((string) $pageNumber) ?>
</a>
<?php endfor; ?>
</nav>
<?php endif; ?>
<?php endif; ?>
<?php if ($categoryDescription !== ''): ?>
<section class="catalog-seo-panel">
<h2><?= e($heading) ?></h2>
<div class="catalog-seo-content">
<?= (string) $categoryDescription ?>
</div>
</section>
<?php endif; ?>
</section>
</section>
+113 -19
View File
@@ -6,14 +6,45 @@ declare(strict_types=1);
/** @var array<int, array<string, mixed>> $variants */
/** @var array<int, array<string, mixed>> $images */
$mainImage = $product['main_image_path'] ?? '';
$sourceMainImage = (string) ($product['main_image_path'] ?? '');
$mainImage = function_exists('optimized_image_path')
? optimized_image_path($sourceMainImage, 'detail')
: $sourceMainImage;
$cartImage = function_exists('optimized_image_path')
? optimized_image_path($sourceMainImage, 'preview')
: $sourceMainImage;
$showUnitPriceForVariant = static function (array $variant) use ($product): bool {
if (function_exists('product_should_show_unit_price')) {
return product_should_show_unit_price($product, $variant);
}
$unit = (string) ($variant['unit'] ?? $product['base_unit'] ?? '');
if (!in_array($unit, ['kg', 'кг'], true)) {
return false;
}
if (($variant['price_per_unit'] ?? null) === null || (float) $variant['price_per_unit'] <= 0) {
return false;
}
$packageQuantity = (float) ($variant['package_quantity'] ?? 0);
if ($packageQuantity < 1) {
return false;
}
$lower = static function (string $value): string {
return function_exists('mb_strtolower') ? mb_strtolower($value, 'UTF-8') : strtolower($value);
};
return preg_match('/(^|[\s,.;])\d+(?:[.,]\d+)?\s*(мл|шт)\.?($|[\s,.;])/u', $lower((string) ($variant['name'] ?? ''))) !== 1;
};
?>
<section
class="product-detail price-aware"
data-product-id="<?= e((string) ($product['id'] ?? '')) ?>"
data-product-name="<?= e((string) ($product['name'] ?? '')) ?>"
data-product-slug="<?= e((string) ($product['slug'] ?? '')) ?>"
data-product-image="<?= e((string) ($product['main_image_path'] ?? '')) ?>"
data-product-image="<?= e($cartImage) ?>"
>
<div class="product-gallery panel">
<?php if ($mainImage !== ''): ?>
@@ -25,7 +56,12 @@ $mainImage = $product['main_image_path'] ?? '';
<?php if ($images !== []): ?>
<div class="product-thumbs">
<?php foreach ($images as $image): ?>
<img src="<?= e((string) $image['path']) ?>" alt="<?= e((string) ($image['alt'] ?: $product['name'])) ?>">
<?php
$thumbPath = function_exists('optimized_image_path')
? optimized_image_path((string) $image['path'], 'preview')
: (string) $image['path'];
?>
<img src="<?= e($thumbPath) ?>" alt="<?= e((string) ($image['alt'] ?: $product['name'])) ?>" loading="lazy" decoding="async">
<?php endforeach; ?>
</div>
<?php endif; ?>
@@ -39,34 +75,92 @@ $mainImage = $product['main_image_path'] ?? '';
<p class="product-lead"><?= e((string) $product['short_description']) ?></p>
<?php endif; ?>
<div class="product-detail-notes" aria-label="Условия заказа">
<span>Оплата при получении</span>
<span>Весовые позиции пересчитываем при сборке</span>
<span>Доставка внутри МКАД бесплатно от 5000р.</span>
</div>
<?php if ($variants !== []): ?>
<section class="variant-list" aria-label="Фасовки товара">
<h2>Фасовки</h2>
<section class="variant-list product-detail-variants" aria-label="Фасовки товара">
<h2>Доступные фасовки</h2>
<?php foreach ($variants as $variant): ?>
<?php
$showUnitPrice = $showUnitPriceForVariant($variant);
$unitLabel = unit_label((string) ($variant['unit'] ?? $product['base_unit'] ?? 'кг'));
$variantName = (string) ($variant['name'] ?? '');
$packageQuantity = (float) ($variant['package_quantity'] ?? 0);
$unitPrice = $variant['price_per_unit'] ?? null;
$packagePrice = $variant['package_price'] ?? null;
$oldPackagePrice = $variant['old_package_price'] ?? null;
$oldUnitPrice = null;
if ($showUnitPrice && $oldPackagePrice !== null && $packageQuantity > 0) {
$oldUnitPrice = (float) $oldPackagePrice / $packageQuantity;
}
$tierBase = $showUnitPrice && $unitPrice !== null ? (float) $unitPrice : (float) ($packagePrice ?? 0);
$tierUnit = $showUnitPrice ? $unitLabel : 'фасовку';
?>
<div
class="variant-row"
data-variant-id="<?= e((string) ($variant['id'] ?? '')) ?>"
data-variant-name="<?= e((string) ($variant['name'] ?? '')) ?>"
data-variant-name="<?= e($variantName) ?>"
data-unit="<?= e($unitLabel) ?>"
data-package-quantity="<?= e((string) ($variant['package_quantity'] ?? '')) ?>"
data-base-unit-price="<?= e((string) ($variant['price_per_unit'] ?? '')) ?>"
data-base-package-price="<?= e((string) ($variant['package_price'] ?? '')) ?>"
>
<strong><?= e((string) $variant['name']) ?></strong>
<span>
<?php if ($variant['price_per_unit'] !== null): ?>
<span data-dynamic-unit-price data-base-unit-price="<?= e((string) $variant['price_per_unit']) ?>"><?= e(money_label($variant['price_per_unit'])) ?></span> / <?= e(unit_label((string) $variant['unit'])) ?>
<?php endif; ?>
</span>
<b>
<?php if (!empty($variant['old_package_price'])): ?>
<span class="old-price"><?= e(money_label($variant['old_package_price'])) ?></span>
<?php endif; ?>
<span data-dynamic-package-price data-base-package-price="<?= e((string) $variant['package_price']) ?>"><?= e(money_label($variant['package_price'])) ?></span>
</b>
<button class="add-to-cart-button" type="button">В корзину</button>
<div class="variant-row-head">
<span>Фасовка</span>
<strong><?= e($variantName) ?></strong>
</div>
<div class="variant-row-prices">
<span class="price-line<?= $showUnitPrice ? '' : ' is-price-placeholder' ?>" data-unit-price-row aria-hidden="<?= $showUnitPrice ? 'false' : 'true' ?>">
<span class="price-unit-label">Цена за <span data-unit-label><?= e($unitLabel) ?></span>:</span>
<span class="price-unit-values">
<span class="old-price" data-old-unit-price-display<?= $oldUnitPrice === null ? ' hidden' : '' ?>><?= e($oldUnitPrice === null ? '' : money_label($oldUnitPrice)) ?></span>
<b data-price-unit data-dynamic-unit-price data-base-unit-price="<?= e((string) $unitPrice) ?>"><?= e($unitPrice === null ? '' : money_label($unitPrice)) ?></b>
<button class="price-tier-trigger" type="button" aria-label="Показать варианты цены за наличный расчет">?</button>
</span>
</span>
<strong class="package-price-line">
<span class="package-price-label">За фасовку <span data-package-label-display><?= e($variantName) ?></span>:</span>
<span class="package-price-values">
<span class="old-price" data-old-package-price-display<?= $oldPackagePrice === null ? ' hidden' : '' ?>><?= e($oldPackagePrice === null ? '' : money_label($oldPackagePrice)) ?></span>
<span data-package-price data-dynamic-package-price data-base-package-price="<?= e((string) $packagePrice) ?>"><?= e($packagePrice === null ? '' : money_label($packagePrice)) ?></span>
<button class="price-tier-trigger" data-package-tier-trigger type="button" aria-label="Показать варианты цены за наличный расчет"<?= $showUnitPrice ? ' hidden' : '' ?>>?</button>
</span>
</strong>
<span class="package-price-note<?= $showUnitPrice ? '' : ' is-price-placeholder' ?>" data-package-price-note aria-hidden="<?= $showUnitPrice ? 'false' : 'true' ?>">Цена за <span data-unit-label-note><?= e($unitLabel) ?></span> x <span data-package-label-note><?= e($variantName) ?></span></span>
<div class="price-tier-popover" role="tooltip">
<strong>Варианты цены за <span data-tier-unit-label><?= e($tierUnit) ?></span> при наличном расчете:</strong>
<dl>
<div><dt>Базовая цена</dt><dd data-tier-base><?= e(money_label($tierBase)) ?></dd></div>
<div><dt>При заказе от 20 000р.</dt><dd data-tier-cash-20><?= e(money_label(floor($tierBase * 0.95))) ?></dd></div>
<div><dt>При заказе от 50 000р.</dt><dd data-tier-cash-50><?= e(money_label(floor($tierBase * 0.93))) ?></dd></div>
</dl>
</div>
</div>
<div class="product-card-actions">
<div class="quantity-stepper" data-quantity-control aria-label="Количество фасовок">
<button type="button" data-quantity-minus aria-label="Уменьшить количество">-</button>
<input data-quantity-input type="text" inputmode="numeric" value="1" aria-label="Количество">
<button type="button" data-quantity-plus aria-label="Увеличить количество">+</button>
</div>
<button class="add-to-cart-button" type="button">В корзину</button>
</div>
</div>
<?php endforeach; ?>
</section>
<?php endif; ?>
<p class="product-detail-preorder">На сайте оформляется предзаказ. Мы перезвоним, подтвердим наличие, фактический вес и удобное получение.</p>
</article>
<?php if (!empty($product['description'])): ?>
+1332 -63
View File
File diff suppressed because it is too large Load Diff
+134 -8
View File
@@ -2,21 +2,135 @@
<div class="cart-hero">
<p class="eyebrow">Ваш заказ</p>
<h1>Корзина</h1>
<p>Цены пересчитываются автоматически: наличными от 20 000р. действует -5%, от 50 000р. -7%, QR-код показывает базовую цену, расчетный счет показывает +8%.</p>
<p>Проверьте фасовки, выберите способ оплаты и получение. Сайт сразу пересчитает выгоду, доставку и итоговую сумму.</p>
<div class="cart-weight-notice">
Весовые позиции пересчитываются при сборке по фактическому весу. Если Вам нужна итоговая сумма до получения товара, отметьте это в форме заказа: после сборки менеджер перезвонит и сообщит сумму.
</div>
<div class="cart-weight-notice cart-live-notice">
При каждом открытии корзины сайт сверяет наличие, актуальные цены и фасовки. Если позиция закончилась, она не попадет в сумму заказа; если изменилась фасовка, мы покажем это рядом с товаром.
</div>
</div>
<div class="cart-layout">
<div class="cart-layout cart-layout-checkout">
<section class="cart-items-panel">
<div class="section-heading">
<div>
<p class="eyebrow">Товары</p>
<h2>Выбранные позиции</h2>
</div>
<a class="subtle-link" href="/catalog">Добавить товары</a>
<div class="cart-heading-actions">
<button class="subtle-button" type="button" data-cart-clear>Очистить корзину</button>
<a class="subtle-link" href="/catalog">Добавить товары</a>
</div>
</div>
<p class="cart-empty" data-cart-empty>Корзина пока пустая. Перейдите в каталог и добавьте нужные фасовки.</p>
<div class="cart-sync-note" data-cart-sync-note hidden></div>
<div class="cart-items" data-cart-items></div>
<section class="cart-checkout-panel">
<div class="cart-payment-trust">Оплата только при получении</div>
<div class="section-heading">
<div>
<p class="eyebrow">Оплата</p>
<h2>Выберите способ оплаты</h2>
</div>
</div>
<div class="cart-payment-options" role="group" aria-label="Способ оплаты">
<button type="button" class="price-mode-button price-mode-button-cash" 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 class="cart-invoice-details" data-invoice-details>
<p>Для счета можно указать реквизиты текстом или прикрепить файл. Счет подготовим день в день после подтверждения заказа.</p>
<label>
<span>Реквизиты организации</span>
<textarea name="invoice_requisites" data-order-invoice-requisites rows="4" placeholder="ИНН, название организации, email для счета"></textarea>
</label>
<label>
<span>Файл с реквизитами</span>
<input type="file" name="invoice_files[]" data-order-invoice-file accept=".pdf,.doc,.docx,.xls,.xlsx,.jpg,.jpeg,.png" multiple>
</label>
</div>
</section>
<section class="cart-checkout-panel">
<div class="section-heading">
<div>
<p class="eyebrow">Получение</p>
<h2>Доставка или самовывоз</h2>
</div>
</div>
<div class="cart-delivery-options" role="radiogroup" aria-label="Способ получения">
<label>
<input type="radio" name="cart_delivery_type" value="pickup" data-cart-delivery-type>
<span>Самовывоз</span>
</label>
<label>
<input type="radio" name="cart_delivery_type" value="inside_mkad" data-cart-delivery-type>
<span>Доставка внутри МКАД</span>
</label>
<label>
<input type="radio" name="cart_delivery_type" value="outside_mkad" data-cart-delivery-type>
<span>Доставка за МКАД</span>
</label>
</div>
<p class="cart-minimum-warning" data-cart-minimum-warning hidden></p>
<div class="cart-delivery-fields">
<label>
<span>Адрес доставки</span>
<input type="text" data-cart-address placeholder="Например: Москва, ул. Привольная, 13к1">
<small class="cart-address-status" data-cart-address-status hidden></small>
</label>
<label class="cart-lift-label" data-lift-field hidden>
<input type="checkbox" data-cart-lift>
<span>Нужен подъем до квартиры/офиса +390 руб.</span>
</label>
</div>
</section>
<section class="cart-checkout-panel">
<div class="section-heading">
<div>
<p class="eyebrow">Контакты</p>
<h2>Куда перезвонить</h2>
</div>
</div>
<form class="cart-order-form" data-cart-order-form>
<div class="cart-form-grid">
<label>
<span>Имя *</span>
<input type="text" name="customer_name" data-order-name required>
</label>
<label>
<span>Телефон *</span>
<input type="tel" name="phone" data-order-phone required placeholder="+7">
</label>
<label>
<span>Email (необязательно)</span>
<input type="email" name="email" data-order-email placeholder="Если хотите получить копию заказа">
</label>
</div>
<label class="cart-final-sum-label">
<input type="checkbox" name="final_sum_needed" data-order-final-sum-needed>
<span>Нужна итоговая сумма до получения товара. После сборки менеджер перезвонит и сообщит сумму.</span>
</label>
<label>
<span>Комментарий</span>
<textarea name="comment" data-order-comment rows="4" placeholder="Например: буду дома после 19.00 или нужен номер машины для заказа пропуска."></textarea>
</label>
<button class="primary-button cart-confirm-button" type="submit">Подтвердить заказ</button>
<p class="cart-form-note">На сайте Вы оформляете предзаказ. Мы перезвоним Вам и утвердим наличие, оплата производится при получении товара.</p>
</form>
</section>
</section>
<aside class="cart-summary">
@@ -24,20 +138,32 @@
<h2>Итого</h2>
<dl>
<div>
<dt>Базовая сумма</dt>
<dt>Базовая сумма товаров</dt>
<dd data-cart-base-total>0 руб.</dd>
</div>
<div>
<dt>Текущий режим</dt>
<dd data-cart-current-mode>Наличные</dd>
</div>
<div class="cart-summary-total">
<dt>Сумма к расчету</dt>
<div class="cart-summary-saving" data-cart-saving-row hidden>
<dt>Ваша выгода</dt>
<dd data-cart-saving-total>0 руб.</dd>
</div>
<div>
<dt>Товары с учетом оплаты</dt>
<dd data-cart-current-total>0 руб.</dd>
</div>
<div>
<dt>Доставка</dt>
<dd data-cart-delivery-total>Выберите получение</dd>
</div>
<div class="cart-summary-total">
<dt>К оплате при получении</dt>
<dd data-cart-grand-total>0 руб.</dd>
</div>
</dl>
<p>Окончательная сумма весовых товаров подтверждается менеджером после проверки фактического веса и наличия.</p>
<a class="primary-button cart-summary-action" href="/contacts">Связаться по заказу</a>
<p data-cart-delivery-note>Выберите способ получения, чтобы увидеть расчет доставки.</p>
<div class="cart-order-success" data-cart-order-success hidden></div>
</aside>
</div>
</section>
+5 -2
View File
@@ -45,7 +45,10 @@
<p class="eyebrow">Как пройти</p>
<h2>Маршрут к пункту самовывоза</h2>
</div>
<a class="subtle-link" href="https://yandex.ru/maps/?text=Москва%2C%20улица%20Привольная%2C%2013%D0%BA1" target="_blank" rel="noopener">Открыть в Яндекс.Картах</a>
<div class="route-actions">
<a class="subtle-link" href="https://yandex.ru/maps/?text=Москва%2C%20улица%20Привольная%2C%2013%D0%BA1" target="_blank" rel="noopener">Открыть в Яндекс.Картах</a>
<a class="secondary-button" href="https://yandex.ru/maps/?rtext=%D0%BC%D0%B5%D1%82%D1%80%D0%BE%20%D0%9B%D0%B5%D1%80%D0%BC%D0%BE%D0%BD%D1%82%D0%BE%D0%B2%D1%81%D0%BA%D0%B8%D0%B9%20%D0%BF%D1%80%D0%BE%D1%81%D0%BF%D0%B5%D0%BA%D1%82~%D0%9C%D0%BE%D1%81%D0%BA%D0%B2%D0%B0%2C%20%D1%83%D0%BB.%20%D0%9F%D1%80%D0%B8%D0%B2%D0%BE%D0%BB%D1%8C%D0%BD%D0%B0%D1%8F%2C%2013%D0%BA1&rtt=pd" target="_blank" rel="noopener">Маршрут от метро</a>
</div>
</div>
<div class="route-layout">
@@ -60,7 +63,7 @@
<div class="route-steps">
<ol>
<li>Доезжайте до станции метро “Лермонтовский проспект”.</li>
<li>Идите к адресу: ул. Привольная, д. 13к1.</li>
<li>Выходите в сторону ул. Привольная и идите к дому 13к1.</li>
<li>Зайдите во двор дома.</li>
<li>Ориентир - крыльцо Wildberries, вход со двора.</li>
<li>Перед самовывозом дождитесь подтверждения заказа по телефону.</li>
+117 -41
View File
@@ -25,6 +25,7 @@
<label>
Адрес доставки
<input type="text" name="address" placeholder="Например: Москва, ул. Привольная, 13к1">
<small class="cart-address-status" id="delivery-address-status" hidden></small>
</label>
<label>
@@ -35,21 +36,18 @@
<label>
Способ получения
<select name="zone">
<option value="inside">Доставка внутри МКАД</option>
<option value="outside">Доставка за МКАД</option>
<option value="inside_mkad">Доставка внутри МКАД</option>
<option value="outside_mkad">Доставка за МКАД</option>
<option value="pickup">Самовывоз</option>
</select>
</label>
<label class="km-field">
Км от МКАД
<input type="number" name="km" min="0" step="1" value="0">
</label>
<label class="checkbox-label">
<input type="checkbox" name="lift">
Нужен подъем до квартиры/офиса
</label>
<button class="secondary-button calculator-submit" type="button" data-delivery-calc-button>Рассчитать</button>
</form>
<div class="calculator-result" id="delivery-result">
@@ -60,7 +58,9 @@
<div class="info-grid">
<section class="info-card">
<div class="info-card-visual delivery-city"></div>
<figure class="delivery-vehicle-visual delivery-city">
<img src="/assets/img/delivery-van-city.svg" alt="Рефрижератор Рыбсток для доставки внутри МКАД">
</figure>
<h2>Доставка внутри МКАД</h2>
<ul class="clean-list">
<li>Заказы от 5000 руб. - бесплатно до подъезда.</li>
@@ -72,7 +72,9 @@
</section>
<section class="info-card">
<div class="info-card-visual delivery-region"></div>
<figure class="delivery-vehicle-visual delivery-region">
<img src="/assets/img/delivery-van-region.svg" alt="Рефрижератор Рыбсток для доставки за МКАД">
</figure>
<h2>Доставка за МКАД</h2>
<ul class="clean-list">
<li>До 10 км от МКАД бесплатная доставка от 10000 руб.</li>
@@ -113,57 +115,131 @@
</section>
<script>
(() => {
window.addEventListener('load', () => {
const form = document.getElementById('delivery-calculator');
const result = document.getElementById('delivery-result');
const addressStatus = document.getElementById('delivery-address-status');
if (!form || !result) {
return;
}
const formatRub = (value) => new Intl.NumberFormat('ru-RU').format(Math.max(0, Math.round(value))) + ' руб.';
const formatRub = (value) => window.RybStockDelivery?.formatRub
? window.RybStockDelivery.formatRub(value)
: new Intl.NumberFormat('ru-RU').format(Math.max(0, Math.round(value))) + ' руб.';
let resolvedDistance = 0;
let resolvedZone = '';
let addressTimer = null;
let addressRequestId = 0;
const setAddressStatus = (message) => {
if (!addressStatus) {
return;
}
addressStatus.hidden = message === '';
addressStatus.textContent = message;
};
const calculate = () => {
const data = new FormData(form);
const orderSum = Number(data.get('order_sum') || 0);
const zone = String(data.get('zone') || 'inside');
const km = Math.max(0, Number(data.get('km') || 0));
const zone = String(data.get('zone') || 'inside_mkad');
const lift = data.get('lift') === 'on';
const liftPrice = lift ? 390 : 0;
let delivery = 0;
let title = '';
let note = '';
const effectiveZone = resolvedZone === 'outside'
? 'outside_mkad'
: (resolvedZone === 'inside' ? 'inside_mkad' : zone);
const distance = effectiveZone === 'outside_mkad' ? resolvedDistance : 0;
const quote = window.RybStockDelivery?.quote
? window.RybStockDelivery.quote(effectiveZone, orderSum, distance, lift, resolvedZone)
: { label: 'Расчет доставки', note: 'Обновите страницу, чтобы загрузить калькулятор.', total: 0, minimumMessage: '' };
const title = quote.selected ? `${quote.label}: ${formatRub(quote.total)}` : quote.label;
const liftNote = lift && effectiveZone !== 'pickup' ? ` Подъем включен: 390 руб.` : '';
const minimumNote = quote.minimumMessage ? ` ${quote.minimumMessage}` : '';
form.querySelector('.km-field').style.display = zone === 'outside' ? 'grid' : 'none';
result.innerHTML = `<strong>${title}</strong><span>${quote.note}${liftNote}${minimumNote}</span>`;
};
const resolveAddress = () => {
const address = String(new FormData(form).get('address') || '').trim();
const zone = String(new FormData(form).get('zone') || 'inside_mkad');
const requestId = ++addressRequestId;
if (zone === 'pickup') {
delivery = 0;
title = 'Самовывоз: бесплатно';
note = orderSum < 1
? 'Минимальная сумма заказа на самовывоз - 1 руб.'
: 'Самовывоз возможен после подтверждения заказа.';
} else if (zone === 'inside') {
delivery = orderSum >= 5000 ? 0 : 599;
title = delivery === 0 ? 'Доставка внутри МКАД: бесплатно' : 'Доставка внутри МКАД: ' + formatRub(delivery);
note = orderSum < 2000
? 'Минимальная сумма заказа на доставку внутри МКАД - 2000 руб.'
: 'Бесплатная доставка внутри МКАД от 5000 руб.';
} else {
const freeThreshold = km <= 10 ? 10000 : Math.ceil(km) * 1000;
delivery = orderSum >= freeThreshold ? 0 : 199 + km * 60;
title = delivery === 0 ? 'Доставка за МКАД: бесплатно' : 'Доставка за МКАД: ' + formatRub(delivery);
note = 'Порог бесплатной доставки для этого расстояния: ' + formatRub(freeThreshold) + '. Платная доставка: 199 руб. + 60 руб./км.';
if (orderSum < 5000) {
note += ' Минимальная сумма заказа за МКАД - 5000 руб.';
}
resolvedDistance = 0;
resolvedZone = '';
setAddressStatus('');
calculate();
return;
}
const total = delivery + liftPrice;
result.innerHTML = '<strong>' + title + '</strong><span>' + note + (lift ? ' Подъем: 390 руб. Итого с подъемом: ' + formatRub(total) + '.' : '') + '</span>';
if (address.length < 6) {
resolvedDistance = 0;
resolvedZone = '';
setAddressStatus('Введите адрес и выберите вариант из подсказки, чтобы сайт рассчитал доставку автоматически.');
calculate();
return;
}
setAddressStatus('Рассчитываем расстояние по адресу...');
if (!window.RybStockDelivery?.resolveAddress) {
resolvedDistance = 0;
resolvedZone = '';
setAddressStatus('Калькулятор еще загружается. Если расчет не появился, обновите страницу.');
calculate();
return;
}
window.RybStockDelivery.resolveAddress(address)
.then((resolved) => {
if (requestId !== addressRequestId) {
return;
}
resolvedDistance = Number(resolved.distance || 0);
resolvedZone = String(resolved.zone || '');
if (resolvedZone === 'outside') {
form.querySelector('[name="zone"]').value = 'outside_mkad';
} else if (resolvedZone === 'inside') {
form.querySelector('[name="zone"]').value = 'inside_mkad';
}
setAddressStatus(resolved.status || '');
calculate();
})
.catch(() => {
if (requestId !== addressRequestId) {
return;
}
resolvedDistance = 0;
resolvedZone = '';
setAddressStatus('Не удалось автоматически рассчитать адрес. Менеджер уточнит доставку по телефону.');
calculate();
});
};
const scheduleResolveAddress = () => {
window.clearTimeout(addressTimer);
addressTimer = window.setTimeout(resolveAddress, 700);
};
form.addEventListener('input', calculate);
form.addEventListener('change', calculate);
form.addEventListener('input', (event) => {
if (event.target.matches('[name="address"]')) {
scheduleResolveAddress();
}
});
form.addEventListener('change', () => {
resolveAddress();
calculate();
});
form.querySelector('[data-delivery-calc-button]')?.addEventListener('click', () => {
resolveAddress();
calculate();
});
window.RybStockDelivery?.setupAddressSuggestions?.();
resolveAddress();
calculate();
})();
});
</script>
+279 -69
View File
@@ -6,6 +6,7 @@ declare(strict_types=1);
/** @var array<int, array<string, mixed>> $categories */
/** @var array<int, array<string, mixed>> $catalogProducts */
/** @var array<int, array<string, mixed>> $popularProducts */
/** @var array<int, array<string, mixed>> $homeCatalogSections */
/** @var array<string, mixed> $settings */
/** @var string|null $notice */
/** @var string $buildVersion */
@@ -54,101 +55,309 @@ $months = [
$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>
<section class="home-value-system" aria-label="Преимущества Рыбсток">
<div class="home-value-head">
<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>
<p>Рыба, морепродукты, мясо, сыры, овощи, ягоды и бакалея по стоковым ценам для большого домашнего стола, дачи, магазина и кухни.</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>
</div>
<div class="hero-media">
<img src="/assets/images/brand-hero-products.svg" alt="Ассортимент Рыбсток">
<div class="home-benefit-grid">
<article class="home-benefit-card is-wide" style="--benefit-image: var(--home-hero-picnic)">
<div>
<span>Дом, дача, большой стол</span>
<h2>Запас продуктов без переплаты</h2>
<p>Берете коробками и фасовками со склада, а не с витрины. Удобно набрать рыбу, мясо, морепродукты и полуфабрикаты для семьи и гостей.</p>
</div>
</article>
<article class="home-benefit-card" style="--benefit-image: var(--home-warehouse-fish)">
<div>
<span>Выгода за наличные</span>
<h2>Корзина считает сама</h2>
<p>Минус 300 руб. от 7000 руб., -5% от 20000 руб. и -7% от 50000 руб. Покупателю не нужно считать вручную.</p>
</div>
</article>
<article class="home-benefit-card" style="--benefit-image: var(--home-banner-meat)">
<div>
<span>Доставка и оплата</span>
<h2>Без предоплаты</h2>
<p>Внутри МКАД бесплатно от 5000 руб., за МКАД - от 10000 руб. до 10 км. Заказ оплачивается только при получении.</p>
</div>
</article>
</div>
</section>
<section class="storage-status" aria-label="Температура хранения и поступление товара">
<div class="arrival-card">
<span>Последнее поступление</span>
<strong><?= e((string) $arrivalDate) ?></strong>
<p>Свежие позиции принимаем по будням утром. В выходные склад не принимает новый товар.</p>
<section class="audience-system" aria-label="Для кого работает Рыбсток">
<div class="audience-heading">
<p class="eyebrow">Для кого мы работаем</p>
<h2>Закрываем разные задачи одной поставкой</h2>
<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 class="audience-grid">
<article class="audience-card" style="--audience-image: var(--home-warehouse-cold)">
<span>Частным покупателям</span>
<h3>Наполнить морозильник и спокойно кормить семью</h3>
<p>Решаем главную боль: чем накормить домочадцев, гостей и дачу без ежедневных закупок и лишней наценки.</p>
</article>
<article class="audience-card" style="--audience-image: var(--home-warehouse-fish)">
<span>Магазинам</span>
<h3>Собрать ходовой рыбный прилавок</h3>
<p>Не люкс-витрина ради картинки, а понятные позиции, за которыми покупатель возвращается и встает в очередь.</p>
</article>
<article class="audience-card" style="--audience-image: var(--home-banner-seafood)">
<span>HoReCa</span>
<h3>Поддержать полную посадку кухни</h3>
<p>Рыба, мясо, морепродукты, полуфабрикаты и бакалея в одной закупке, чтобы меню не зависело от разрозненных поставок.</p>
</article>
</div>
</section>
<section class="home-banners" aria-label="Преимущества Рыбсток">
<div class="home-banner delivery-image">
<strong>Бесплатная доставка внутри МКАД от 5000 руб.</strong>
<span>Привезем заказ после подтверждения по телефону.</span>
<section class="warehouse-showcase" aria-label="Склад Рыбсток">
<div class="warehouse-copy">
<p class="eyebrow">Склад и хранение</p>
<h2>Реальный склад, холодовая цепь и свежие поступления</h2>
<p>Мы показываем склад без лишней витринности: коробки, паллеты, камеры хранения и продукция, которая дальше уезжает к домашнему столу.</p>
</div>
<div class="home-banner region-image">
<strong>Бесплатная доставка за МКАД от 10000 руб.</strong>
<span>Для адресов до 10 км от МКАД, дальше расчет по расстоянию.</span>
<div 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>
</div>
<div class="home-banner day-image">
<strong>Можно приобрести день в день</strong>
<span>Привезем заказ день в день, если он оформлен в будний день до 14:00.</span>
</div>
<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>
<small>5% при наличном расчете от 20 000 руб.</small>
</div>
<div>
<span>Промокод</span>
<strong>Рыбсток50000</strong>
<small>7% при наличном расчете от 50 000 руб.</small>
</div>
<div>
<span>Безналичный расчет</span>
<strong>+8%</strong>
<small>Отображается отдельной ценовой градацией.</small>
<div class="warehouse-photo-grid">
<figure class="warehouse-photo-card large" style="--warehouse-image: var(--home-warehouse-main)">
<figcaption>
<span>Склад Рыбсток</span>
<strong>Паллеты, коробки и рабочий порядок</strong>
</figcaption>
</figure>
<figure class="warehouse-photo-card" style="--warehouse-image: var(--home-warehouse-arrival)">
<figcaption>
<span>Поступления</span>
<strong>Рыба и готовая продукция</strong>
</figcaption>
</figure>
<figure class="warehouse-photo-card" style="--warehouse-image: var(--home-warehouse-cold)">
<figcaption>
<span>Хранение</span>
<strong>Камеры и фасовки</strong>
</figcaption>
</figure>
</div>
</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>
<p class="muted">Можно вставить список текстом или приложить файл. Заявка будет уходить администратору на почту.</p>
<div class="price-request-cover" aria-hidden="true">
<span>Для магазинов, кафе, ресторанов и закупщиков</span>
</div>
<div class="price-request-content">
<div class="price-request-copy">
<p class="eyebrow">Запрос проходных цен</p>
<h2>Для бизнеса</h2>
<p>Запросите у нас цену на продукцию. Просто напишите позиции, которые Вас интересуют, необходимый объем и цену, которая Вам нужна. Наш менеджер оптового отдела постарается помочь Вам.</p>
<div class="business-only-badge">Только для юридических лиц, ИП, HoReCa и закупщиков</div>
</div>
<?php if (($_GET['price_request'] ?? '') === 'sent'): ?>
<div class="status-box success">Спасибо, запрос принят. Менеджер оптового отдела свяжется с Вами после обработки заявки.</div>
<?php elseif (($_GET['price_request'] ?? '') === 'error'): ?>
<div class="status-box danger">Не удалось отправить запрос. Проверьте обязательные поля и попробуйте еще раз.</div>
<?php endif; ?>
<form class="price-request-form" action="/price-request" method="post" enctype="multipart/form-data">
<label>
<span>Название организации</span>
<input type="text" name="company_name" placeholder="Например: кафе, магазин, ИП или ООО" required>
</label>
<fieldset>
<legend>Форма оплаты</legend>
<label class="choice-pill">
<input type="radio" name="payment_type" value="cash" checked>
<span>Нал</span>
</label>
<label class="choice-pill">
<input type="radio" name="payment_type" value="invoice">
<span>Расчетный счет</span>
</label>
</fieldset>
<label>
<span>Телефон</span>
<input type="tel" name="phone" placeholder="+7 ___ ___-__-__">
</label>
<label>
<span>Email</span>
<input type="email" name="email" placeholder="mail@example.ru">
</label>
<label>
<span>Тип деятельности</span>
<input type="text" name="activity_type" placeholder="Ресторан, магазин, производство, кейтеринг">
</label>
<label class="wide">
<span>Позиции, объем и нужная цена</span>
<textarea name="request_text" rows="6" placeholder="Например: форель 10 кг до 1200 руб./кг, креветки 5 кг до 900 руб./кг" required></textarea>
</label>
<label class="file-field wide">
<span>Прикрепить файлы</span>
<input type="file" name="request_files[]" multiple>
<small>Можно приложить список, прайс, таблицу или фото заявки.</small>
</label>
<button class="primary-button wide" type="submit">Отправить запрос</button>
</form>
</div>
<a class="secondary-button" href="#price-request">Подготовить форму</a>
</section>
<?php if ($notice !== null): ?>
<div class="status-box danger"><?= e($notice) ?></div>
<?php endif; ?>
<section class="home-catalog-showcase" aria-label="Витрина каталога Рыбсток">
<div class="home-catalog-heading">
<div>
<p class="eyebrow">Каталог продукции</p>
<h2>Акции и новые поступления</h2>
</div>
<a class="home-catalog-main-link" href="/catalog">Перейти во весь каталог</a>
</div>
<?php if (($homeCatalogSections ?? []) === []): ?>
<div class="panel">
<p class="muted">После наполнения каталога товары появятся здесь по категориям.</p>
</div>
<?php else: ?>
<?php foreach ($homeCatalogSections as $section): ?>
<?php
$sectionProducts = $section['products'] ?? [];
if ($sectionProducts === []) {
continue;
}
$sectionKind = (string) ($section['kind'] ?? 'category');
?>
<section class="home-product-section is-<?= e($sectionKind) ?>" id="home-<?= e((string) ($section['key'] ?? 'section')) ?>">
<div class="home-product-section-head">
<h3><?= e((string) ($section['title'] ?? 'Категория')) ?></h3>
<div class="home-product-section-actions">
<?php if (!empty($section['url'])): ?>
<a href="<?= e((string) $section['url']) ?>">Показать <?= e((string) ($section['count'] ?? count($sectionProducts))) ?> товаров</a>
<?php endif; ?>
<div class="home-strip-controls" aria-label="Прокрутка подборки">
<button type="button" data-home-strip-prev aria-label="Назад">&larr;</button>
<button type="button" data-home-strip-next aria-label="Вперед">&rarr;</button>
</div>
</div>
</div>
<div class="home-products-strip" aria-label="<?= e((string) ($section['title'] ?? 'Категория')) ?>">
<?php foreach ($sectionProducts as $product): ?>
<?php require base_path('views/partials/product-card.php'); ?>
<?php endforeach; ?>
</div>
</section>
<?php endforeach; ?>
<?php endif; ?>
</section>
<script>
document.querySelectorAll('[data-home-strip-prev], [data-home-strip-next]').forEach((button) => {
button.addEventListener('click', () => {
const section = button.closest('.home-product-section');
const strip = section ? section.querySelector('.home-products-strip') : null;
if (!strip) {
return;
}
const direction = button.hasAttribute('data-home-strip-prev') ? -1 : 1;
strip.scrollBy({
left: direction * Math.max(260, strip.clientWidth * 0.86),
behavior: 'smooth',
});
});
});
</script>
<section class="wishlist-request-panel" id="wishlist-request">
<div class="wishlist-request-copy">
<p class="eyebrow">Запрос позиции</p>
<h2>Одна голова хорошо, две лучше</h2>
<p>Нужна позиция, но нигде не можете ее найти? Отправьте нам наименование и объем. Если сможем помочь с поиском, менеджер свяжется с Вами.</p>
<span>Для частных покупателей и бизнеса</span>
</div>
<div class="wishlist-request-card">
<form class="wishlist-request-form" action="/wishlist-request" method="post" enctype="multipart/form-data">
<label>
<span>Имя</span>
<input type="text" name="customer_name" placeholder="Как к Вам обращаться" required>
</label>
<label>
<span>Телефон</span>
<input type="tel" name="phone" placeholder="+7 ___ ___-__-__">
</label>
<label>
<span>Email</span>
<input type="email" name="email" placeholder="mail@example.ru">
</label>
<label class="wide">
<span>Что нужно найти</span>
<textarea name="request_text" rows="5" placeholder="Например: филе судака 10 кг, икра в стеклянной банке, креветки нужного размера" required></textarea>
</label>
<label class="file-field wide">
<span>Прикрепить файлы</span>
<input type="file" name="wishlist_files[]" multiple>
<small>Можно приложить фото, список или пример товара.</small>
</label>
<p class="form-hint wide">Телефон или email - достаточно одного контакта.</p>
<button class="primary-button wide" type="submit">Отправить запрос</button>
</form>
</div>
</section>
<section class="home-delivery-summary" aria-label="Доставка и выгода Рыбсток">
<div>
<p class="eyebrow">Перед оформлением заказа</p>
<h2>Доставка, оплата при получении и подарок за наличный расчет</h2>
<p>Мы привозим заказы после подтверждения по телефону, а оплату принимаем только при получении. При оплате наличными сайт сам пересчитает выгоду в корзине: считать вручную ничего не нужно.</p>
</div>
<div class="home-delivery-benefits">
<article>
<span>от 7 000 руб.</span>
<strong>-300 руб.</strong>
<p>Подарок при оплате наличными.</p>
</article>
<article>
<span>внутри МКАД</span>
<strong>0 руб.</strong>
<p>Бесплатно от 5000 руб., иначе 599 руб.</p>
</article>
<article>
<span>за МКАД</span>
<strong>по адресу</strong>
<p>Сайт рассчитает предварительную стоимость доставки.</p>
</article>
</div>
<a class="primary-button" href="/delivery">Посмотреть условия доставки</a>
</section>
<?php if (false): ?>
<div class="content-grid">
<section class="panel">
<h2>Каталог</h2>
@@ -219,4 +428,5 @@ $arrivalDate = $lastArrival->format('j') . ' ' . $months[(int) $lastArrival->for
</div>
<?php endif; ?>
</section>
<?php endif; ?>
</section>
+41
View File
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
if (!function_exists('category_tree_icon')) {
function category_tree_icon(string $name): string
{
$lowerName = mb_strtolower($name);
return match (true) {
str_contains($lowerName, 'рыб') => 'fish',
str_contains($lowerName, 'морепроду') => 'shrimp',
str_contains($lowerName, 'икра') => 'caviar',
str_contains($lowerName, 'мяс') => 'meat',
str_contains($lowerName, 'сыр') => 'cheese',
str_contains($lowerName, 'полуфаб') => 'box',
str_contains($lowerName, 'овощ') || str_contains($lowerName, 'фрукт') || str_contains($lowerName, 'ягод') || str_contains($lowerName, 'гриб') => 'leaf',
str_contains($lowerName, 'бакале') => 'jar',
str_contains($lowerName, 'молоч') => 'milk',
default => 'grid',
};
}
}
if (!function_exists('category_tree_icon_svg')) {
function category_tree_icon_svg(string $icon): string
{
return match ($icon) {
'fish' => '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 12c3.2-4.2 7.1-6.2 11.6-6 2.3.1 4.2 1.1 5.8 3l1.6-1.5v9L20.4 15c-1.6 1.9-3.6 2.9-5.8 3-4.5.2-8.4-1.8-11.6-6Z"/><path d="M7.5 12h.1"/><path d="M14 7.5c-1.4 1.2-2.1 2.7-2.1 4.5s.7 3.3 2.1 4.5"/></svg>',
'shrimp' => '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M18.5 7.5c-2.8-2.3-7.5-1.6-10.2.9-2.7 2.4-2.8 6-.3 8.1 2.3 2 6 1.9 8.2-.2 1.6-1.5 1.8-3.8.4-5.2-1.2-1.2-3.3-1.3-4.7-.2"/><path d="M19 8l2-2"/><path d="M18.8 8.2l2.4.8"/><path d="M8.8 16.8 7 19"/><path d="M11.4 17.7 10.5 20"/><path d="M14 17.2l.3 2.4"/></svg>',
'leaf' => '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M20 4c-7.8.1-12.8 3-15 8.6C3.7 16 6 20 9.8 20c5.7 0 9.2-5.6 10.2-16Z"/><path d="M5 19c3.2-4.4 7.2-7.5 12-9.2"/></svg>',
'caviar' => '<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="8" cy="8" r="2.4"/><circle cx="15.5" cy="7.5" r="2"/><circle cx="12" cy="14" r="2.5"/><circle cx="6.8" cy="16.2" r="1.8"/><circle cx="17.5" cy="16.5" r="2.1"/></svg>',
'jar' => '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 3h8"/><path d="M9 6h6"/><path d="M7 9c0-1.7 1.4-3 3-3h4c1.7 0 3 1.3 3 3v9c0 1.7-1.3 3-3 3h-4c-1.6 0-3-1.3-3-3V9Z"/><path d="M7 13h10"/></svg>',
'box' => '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m4 8 8-4 8 4-8 4-8-4Z"/><path d="M4 8v8l8 4 8-4V8"/><path d="M12 12v8"/></svg>',
'meat' => '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5.5 15.5c-2.8-3.9.5-9.1 5.6-9.6 4.1-.4 7.9 1.9 8.7 5.2.8 3.5-2.1 6.8-6.2 7.2-3.4.4-6.3-.7-8.1-2.8Z"/><circle cx="11" cy="12" r="2.4"/></svg>',
'cheese' => '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 16 19 7v10H4v-1Z"/><path d="M4 16v3h15v-2"/><circle cx="14.5" cy="13" r="1"/><circle cx="17" cy="10.5" r=".8"/><circle cx="9.5" cy="15" r=".8"/></svg>',
'milk' => '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 3h6v4l2 3v10H7V10l2-3V3Z"/><path d="M9 7h6"/><path d="M7 13h10"/></svg>',
default => '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 5h6v6H5z"/><path d="M13 5h6v6h-6z"/><path d="M5 13h6v6H5z"/><path d="M13 13h6v6h-6z"/></svg>',
};
}
}
+24 -2
View File
@@ -6,6 +6,8 @@ declare(strict_types=1);
/** @var int $depth */
/** @var string $currentSlug */
require_once __DIR__ . '/category-icons.php';
if (!function_exists('category_tree_has_current')) {
/**
* @param array<string, mixed> $category
@@ -31,14 +33,29 @@ if (!function_exists('category_tree_has_current')) {
}
?>
<ul class="category-list depth-<?= e((string) $depth) ?>">
<?php if ($depth === 0): ?>
<li class="category-special-link is-all">
<a href="/catalog"><span>Весь каталог</span></a>
</li>
<li class="category-special-link">
<a href="/promo"><span>Акции</span></a>
</li>
<li class="category-special-link">
<a href="/new"><span>Новинки</span></a>
</li>
<?php endif; ?>
<?php foreach ($items as $category): ?>
<?php $isCurrent = ($currentSlug ?? '') === $category['slug']; ?>
<?php $hasChildren = !empty($category['children']); ?>
<?php $shouldOpen = category_tree_has_current($category, $currentSlug ?? ''); ?>
<li class="<?= $isCurrent ? 'is-current' : '' ?>">
<?php $iconName = $depth === 0 ? category_tree_icon((string) $category['name']) : ''; ?>
<li class="<?= $isCurrent ? 'is-current' : '' ?><?= $hasChildren ? ' has-children' : '' ?>">
<?php if ($hasChildren): ?>
<details class="category-details" <?= $shouldOpen ? 'open' : '' ?>>
<summary>
<?php if ($depth === 0): ?>
<span class="category-icon is-<?= e($iconName) ?>" aria-hidden="true"><?= category_tree_icon_svg($iconName) ?></span>
<?php endif; ?>
<a class="<?= $isCurrent ? 'is-current-link' : '' ?>" href="/catalog/<?= e($category['slug']) ?>"><?= e($category['name']) ?></a>
</summary>
<?php
@@ -51,7 +68,12 @@ if (!function_exists('category_tree_has_current')) {
?>
</details>
<?php else: ?>
<a href="/catalog/<?= e($category['slug']) ?>"><?= e($category['name']) ?></a>
<a href="/catalog/<?= e($category['slug']) ?>">
<?php if ($depth === 0): ?>
<span class="category-icon is-<?= e($iconName) ?>" aria-hidden="true"><?= category_tree_icon_svg($iconName) ?></span>
<?php endif; ?>
<span><?= e($category['name']) ?></span>
</a>
<?php endif; ?>
</li>
<?php endforeach; ?>
+20 -1
View File
@@ -4,6 +4,8 @@ declare(strict_types=1);
/** @var array<int, array<string, mixed>> $megaCategories */
require_once __DIR__ . '/category-icons.php';
if (!function_exists('render_mega_category_children')) {
/**
* @param array<int, array<string, mixed>> $children
@@ -29,9 +31,26 @@ if (!function_exists('render_mega_category_children')) {
<?php if ($megaCategories !== []): ?>
<nav class="category-mega-bar" aria-label="Категории товаров">
<div class="category-mega-inner">
<a class="mega-root mega-all-link" href="/catalog">Весь каталог</a>
<a class="mega-root mega-virtual-link is-promo" href="/promo">Акции</a>
<a class="mega-root mega-virtual-link is-new" href="/new">Новинки</a>
<?php foreach ($megaCategories as $category): ?>
<?php
$hiddenTopCategorySlugs = ['myasnaya-gastronomiya', 'molochnaya-produkciya'];
$hiddenTopCategoryNames = ['Мясная гастрономия', 'Молочная продукция'];
if (
in_array((string) ($category['slug'] ?? ''), $hiddenTopCategorySlugs, true)
|| in_array((string) ($category['name'] ?? ''), $hiddenTopCategoryNames, true)
) {
continue;
} ?>
<?php $iconName = category_tree_icon((string) $category['name']); ?>
<div class="mega-item">
<a class="mega-root" href="/catalog/<?= e((string) $category['slug']) ?>"><?= e((string) $category['name']) ?></a>
<a class="mega-root" href="/catalog/<?= e((string) $category['slug']) ?>">
<span class="mega-category-icon category-icon is-<?= e($iconName) ?>" aria-hidden="true"><?= category_tree_icon_svg($iconName) ?></span>
<span><?= e((string) $category['name']) ?></span>
</a>
<?php render_mega_category_children($category['children'] ?? []); ?>
</div>
<?php endforeach; ?>
+159 -32
View File
@@ -9,56 +9,128 @@ $activeVariant = $variants[0] ?? [
'unit' => $product['unit'] ?? '',
'price_per_unit' => $product['price_per_unit'] ?? null,
'package_price' => $product['package_price'] ?? null,
'package_quantity' => $product['package_quantity'] ?? 0,
'old_package_price' => $product['old_package_price'] ?? null,
];
$showUnitPriceForVariant = static function (array $variant) use ($product): bool {
if (function_exists('product_should_show_unit_price')) {
return product_should_show_unit_price($product, $variant);
}
$unit = (string) ($variant['unit'] ?? $product['base_unit'] ?? '');
if (!in_array($unit, ['kg', 'кг'], true)) {
return false;
}
if (($variant['price_per_unit'] ?? null) === null || (float) $variant['price_per_unit'] <= 0) {
return false;
}
$packageQuantity = (float) ($variant['package_quantity'] ?? 0);
if ($packageQuantity < 1) {
return false;
}
$lower = static function (string $value): string {
return function_exists('mb_strtolower') ? mb_strtolower($value, 'UTF-8') : strtolower($value);
};
return preg_match('/(^|[\s,.;])\d+(?:[.,]\d+)?\s*(мл|шт)\.?($|[\s,.;])/u', $lower((string) ($variant['name'] ?? ''))) !== 1;
};
$cardId = 'product-card-' . (int) ($product['id'] ?? 0);
$activePackagePrice = (float) ($activeVariant['package_price'] ?? 0);
$activeUnitPrice = $activeVariant['price_per_unit'] === null ? null : (float) $activeVariant['price_per_unit'];
$activePackageQuantity = (float) ($activeVariant['package_quantity'] ?? 0);
$activeVariantOldPackagePrice = $activeVariant['old_package_price'] ?? null;
$activeOldPackagePrice = (
$activeVariantOldPackagePrice !== null
&& (float) $activeVariantOldPackagePrice > $activePackagePrice
)
? (float) $activeVariantOldPackagePrice
: null;
$activeOldUnitPrice = ($activeOldPackagePrice !== null && $activePackageQuantity > 0 && $activeUnitPrice !== null)
? $activeOldPackagePrice / $activePackageQuantity
: null;
if ($activeOldUnitPrice !== null && $activeOldUnitPrice <= $activeUnitPrice) {
$activeOldUnitPrice = null;
}
$activeShowsUnitPrice = $showUnitPriceForVariant($activeVariant);
$activeTierBase = $activeShowsUnitPrice ? ($activeUnitPrice ?? $activePackagePrice) : $activePackagePrice;
$activeTierUnit = $activeShowsUnitPrice ? unit_label((string) ($activeVariant['unit'] ?? '')) : 'фасовку';
$isPublished = (int) ($product['is_published'] ?? 1) === 1;
$isAvailable = (int) ($product['is_available'] ?? 1) === 1;
$isPurchasable = $isPublished && $isAvailable && $activePackagePrice > 0;
$sourceImagePath = (string) ($product['main_image_path'] ?? '');
$previewImagePath = function_exists('optimized_image_path')
? optimized_image_path($sourceImagePath, 'preview')
: $sourceImagePath;
?>
<article
class="product-card price-aware"
class="product-card price-aware<?= $isPurchasable ? '' : ' is-unavailable' ?>"
id="<?= e($cardId) ?>"
data-product-id="<?= e((string) ($product['id'] ?? '')) ?>"
data-product-name="<?= e((string) ($product['name'] ?? '')) ?>"
data-product-slug="<?= e((string) ($product['slug'] ?? '')) ?>"
data-product-image="<?= e((string) ($product['main_image_path'] ?? '')) ?>"
data-product-image="<?= e($previewImagePath) ?>"
data-show-unit-price="<?= $activeShowsUnitPrice ? '1' : '0' ?>"
>
<?php if (!empty($product['main_image_path'])): ?>
<?php if (!$isPurchasable): ?>
<span class="product-unavailable-badge"><?= !$isPublished ? 'Ожидается поступление' : 'Нет в наличии' ?></span>
<?php endif; ?>
<?php if (!empty($product['badge_label']) && $isPurchasable): ?>
<span class="product-card-badge is-<?= e((string) ($product['badge_kind'] ?? 'default')) ?>"><?= e((string) $product['badge_label']) ?></span>
<?php endif; ?>
<?php if ($previewImagePath !== '' && $isPurchasable): ?>
<a class="product-card-image" href="/product/<?= e($product['slug']) ?>" aria-label="<?= e($product['name']) ?>">
<img src="<?= e((string) $product['main_image_path']) ?>" alt="<?= e((string) $product['name']) ?>">
<img src="<?= e($previewImagePath) ?>" alt="<?= e((string) $product['name']) ?>" loading="lazy" decoding="async">
</a>
<?php elseif ($previewImagePath !== ''): ?>
<div class="product-card-image" aria-label="<?= e($product['name']) ?>">
<img src="<?= e($previewImagePath) ?>" alt="<?= e((string) $product['name']) ?>" loading="lazy" decoding="async">
</div>
<?php elseif ($isPurchasable): ?>
<a class="product-card-image product-card-image-placeholder" href="/product/<?= e($product['slug']) ?>" aria-label="<?= e($product['name']) ?>">
<span>Рыбсток</span>
</a>
<?php else: ?>
<div class="product-card-image product-card-image-placeholder" aria-label="<?= e($product['name']) ?>">
<span>Рыбсток</span>
</div>
<?php endif; ?>
<p class="product-category"><?= e($product['category_display_name'] ?? $product['category_name'] ?? 'Каталог') ?></p>
<h3><a href="/product/<?= e($product['slug']) ?>"><?= e($product['name']) ?></a></h3>
<?php if ($activeVariant['price_per_unit'] !== null): ?>
<span class="price-line">Цена за <span data-unit-label><?= e(unit_label((string) $activeVariant['unit'])) ?></span>: <b data-price-unit data-dynamic-unit-price data-base-unit-price="<?= e((string) $activeUnitPrice) ?>"><?= e(money_label($activeVariant['price_per_unit'])) ?></b></span>
<?php endif; ?>
<?php if ($activeVariant['package_price'] !== null): ?>
<strong class="package-price-line">
<span class="old-price" data-old-price<?= empty($activeVariant['old_package_price']) ? ' hidden' : '' ?>><?= e(money_label($activeVariant['old_package_price'] ?? null)) ?></span>
За упаковку: <span data-package-price data-dynamic-package-price data-base-package-price="<?= e((string) $activePackagePrice) ?>"><?= e(money_label($activeVariant['package_price'])) ?></span>
<button class="price-tier-trigger" type="button" aria-label="Показать градацию цены">?</button>
</strong>
<?php endif; ?>
<div class="price-tier-popover" role="tooltip">
<strong>Варианты цены</strong>
<dl>
<div><dt>Базовая цена</dt><dd data-tier-base><?= e(money_label($activePackagePrice)) ?></dd></div>
<div><dt>Цена при сумме заказа от 20 000р. наличными</dt><dd data-tier-cash-20><?= e(money_label(floor($activePackagePrice * 0.95))) ?></dd></div>
<div><dt>Цена при сумме заказа от 50 000р. наличными</dt><dd data-tier-cash-50><?= e(money_label(floor($activePackagePrice * 0.93))) ?></dd></div>
<div><dt>Оплата по расчетному счету</dt><dd data-tier-invoice><?= e(money_label(floor($activePackagePrice * 1.08))) ?></dd></div>
</dl>
</div>
<h3>
<?php if ($isPurchasable): ?>
<a href="/product/<?= e($product['slug']) ?>"><?= e($product['name']) ?></a>
<?php else: ?>
<span><?= e($product['name']) ?></span>
<?php endif; ?>
</h3>
<?php if ($variants !== []): ?>
<div class="variant-choice" aria-label="Выбор фасовки">
<span>Фасовка:</span>
<span>Доступные фасовки:</span>
<div class="variant-choice-list">
<?php foreach ($variants as $index => $variant): ?>
<?php
$variantPackagePrice = (float) ($variant['package_price'] ?? 0);
$variantUnitPrice = $variant['price_per_unit'] === null ? null : (float) $variant['price_per_unit'];
$variantPackageQuantity = (float) ($variant['package_quantity'] ?? 0);
$variantOldPackagePrice = (
!empty($variant['old_package_price'])
&& (float) $variant['old_package_price'] > $variantPackagePrice
)
? (float) $variant['old_package_price']
: null;
$variantOldUnitPrice = ($variantOldPackagePrice !== null && $variantPackageQuantity > 0 && $variantUnitPrice !== null)
? $variantOldPackagePrice / $variantPackageQuantity
: null;
if ($variantOldUnitPrice !== null && $variantOldUnitPrice <= $variantUnitPrice) {
$variantOldUnitPrice = null;
}
$variantShowsUnitPrice = $showUnitPriceForVariant($variant);
?>
<button
class="variant-choice-button<?= $index === 0 ? ' is-active' : '' ?>"
type="button"
@@ -66,11 +138,15 @@ $activeUnitPrice = $activeVariant['price_per_unit'] === null ? null : (float) $a
data-variant-id="<?= e((string) ($variant['id'] ?? $index)) ?>"
data-variant-name="<?= e((string) ($variant['name'] ?? '')) ?>"
data-unit="<?= e(unit_label((string) $variant['unit'])) ?>"
data-package-quantity="<?= e((string) ($variant['package_quantity'] ?? '')) ?>"
data-base-unit-price="<?= e((string) ($variant['price_per_unit'] ?? '')) ?>"
data-old-unit-price="<?= e($variantOldUnitPrice === null ? '' : (string) $variantOldUnitPrice) ?>"
data-base-package-price="<?= e((string) ($variant['package_price'] ?? '')) ?>"
data-price-unit="<?= e(money_label($variant['price_per_unit'])) ?>"
data-package-price="<?= e(money_label($variant['package_price'])) ?>"
data-old-price="<?= e(money_label($variant['old_package_price'] ?? null)) ?>"
data-variant-package-label="<?= e((string) ($variant['name'] ?? '')) ?>"
data-variant-unit-price-label="<?= e(money_label($variant['price_per_unit'])) ?>"
data-variant-package-price-label="<?= e(money_label($variant['package_price'])) ?>"
data-old-price="<?= e($variantOldPackagePrice === null ? '' : money_label($variantOldPackagePrice)) ?>"
data-show-unit-price="<?= $variantShowsUnitPrice ? '1' : '0' ?>"
>
<?= e((string) $variant['name']) ?>
</button>
@@ -79,5 +155,56 @@ $activeUnitPrice = $activeVariant['price_per_unit'] === null ? null : (float) $a
</div>
<?php endif; ?>
<button class="add-to-cart-button" type="button">В корзину</button>
<?php if ($isPurchasable): ?>
<span class="price-line<?= $activeShowsUnitPrice ? '' : ' is-price-placeholder' ?>" data-unit-price-row aria-hidden="<?= $activeShowsUnitPrice ? 'false' : 'true' ?>">
<span class="price-unit-label">Цена за <span data-unit-label><?= e(unit_label((string) $activeVariant['unit'])) ?></span>:</span>
<span class="price-unit-values">
<span class="old-price" data-old-unit-price-display<?= $activeOldUnitPrice === null ? ' hidden' : '' ?>><?= e(money_label($activeOldUnitPrice)) ?></span>
<b data-price-unit data-dynamic-unit-price data-base-unit-price="<?= e((string) $activeUnitPrice) ?>"><?= e(money_label($activeVariant['price_per_unit'])) ?></b>
<?php if ($isPurchasable): ?>
<button class="price-tier-trigger" type="button" aria-label="Показать градацию цены">?</button>
<?php endif; ?>
</span>
</span>
<?php endif; ?>
<?php if ($isPurchasable && $activeVariant['package_price'] !== null): ?>
<strong class="package-price-line">
<span class="package-price-label">За фасовку <span data-package-label-display><?= e((string) $activeVariant['name']) ?></span>:</span>
<span class="package-price-values">
<span class="old-price" data-old-package-price-display<?= $activeOldPackagePrice === null ? ' hidden' : '' ?>><?= e(money_label($activeOldPackagePrice)) ?></span>
<span data-package-price data-dynamic-package-price data-base-package-price="<?= e((string) $activePackagePrice) ?>"><?= e(money_label($activeVariant['package_price'])) ?></span>
<button class="price-tier-trigger" data-package-tier-trigger type="button" aria-label="Показать градацию цены"<?= $activeShowsUnitPrice ? ' hidden' : '' ?>>?</button>
</span>
</strong>
<?php if ($isPurchasable): ?>
<span class="package-price-note<?= $activeShowsUnitPrice ? '' : ' is-price-placeholder' ?>" data-package-price-note aria-hidden="<?= $activeShowsUnitPrice ? 'false' : 'true' ?>">Цена за <span data-unit-label-note><?= e(unit_label((string) $activeVariant['unit'])) ?></span> × <span data-package-label-note><?= e((string) $activeVariant['name']) ?></span></span>
<?php endif; ?>
<?php elseif (!$isPurchasable): ?>
<div class="product-card-unavailable-note">Цену уточним после поступления товара.</div>
<?php endif; ?>
<?php if ($isPurchasable): ?>
<div class="price-tier-popover" role="tooltip">
<strong>Варианты цены за <span data-tier-unit-label><?= e($activeTierUnit) ?></span> при наличном расчете:</strong>
<dl>
<div><dt>Базовая цена</dt><dd data-tier-base><?= e(money_label($activeTierBase)) ?></dd></div>
<div><dt>При заказе от 20 000р.</dt><dd data-tier-cash-20><?= e(money_label(floor($activeTierBase * 0.95))) ?></dd></div>
<div><dt>При заказе от 50 000р.</dt><dd data-tier-cash-50><?= e(money_label(floor($activeTierBase * 0.93))) ?></dd></div>
</dl>
</div>
<div class="product-card-actions">
<div class="quantity-stepper" data-quantity-control aria-label="Количество фасовок">
<button type="button" data-quantity-minus aria-label="Уменьшить количество">-</button>
<input data-quantity-input type="text" inputmode="numeric" value="1" aria-label="Количество">
<button type="button" data-quantity-plus aria-label="Увеличить количество">+</button>
</div>
<button class="add-to-cart-button" type="button">В корзину</button>
</div>
<?php else: ?>
<div class="product-card-actions">
<button class="notify-arrival-button" type="button" data-notify-arrival>Сообщить о поступлении</button>
</div>
<?php endif; ?>
</article>
+42
View File
@@ -0,0 +1,42 @@
<section class="wishlist-request-panel" id="wishlist-request">
<div class="wishlist-request-copy">
<p class="eyebrow">Запрос позиции</p>
<h2>Одна голова хорошо, две лучше</h2>
<p>Нужна позиция, но нигде не можете ее найти? Отправьте нам наименование и объем. Наши менеджеры постараются помочь Вам в поиске.</p>
<span>Для частных покупателей и бизнеса</span>
</div>
<div class="wishlist-request-card">
<?php if (($_GET['wishlist_request'] ?? '') === 'sent'): ?>
<div class="status-box success">Ваш запрос принят. Если найдем подходящий вариант, обязательно свяжемся с вами.</div>
<?php elseif (($_GET['wishlist_request'] ?? '') === 'error'): ?>
<div class="status-box danger">Не удалось отправить запрос. Укажите имя, что нужно найти, и телефон или email.</div>
<?php endif; ?>
<form class="wishlist-request-form" action="/wishlist-request" method="post" enctype="multipart/form-data">
<label>
<span>Имя</span>
<input type="text" name="customer_name" placeholder="Как к Вам обращаться" required>
</label>
<label>
<span>Телефон</span>
<input type="tel" name="phone" placeholder="+7 ___ ___-__-__">
</label>
<label>
<span>Email</span>
<input type="email" name="email" placeholder="mail@example.ru">
</label>
<label class="wide">
<span>Что нужно найти</span>
<textarea name="request_text" rows="5" placeholder="Например: филе судака 10 кг, икра в стеклянной банке, креветки под нужный размер" required></textarea>
</label>
<label class="file-field wide">
<span>Прикрепить файлы</span>
<input type="file" name="wishlist_files[]" multiple>
<small>Можно приложить фото, список или пример товара.</small>
</label>
<p class="form-hint wide">Телефон или email - достаточно одного контакта. Если сможем помочь с поиском, менеджер свяжется с Вами.</p>
<button class="primary-button wide" type="submit">Отправить запрос</button>
</form>
</div>
</section>
+1 -1
View File
@@ -383,7 +383,7 @@
15. Нужно ли сохранять расчет доставки в заказе или только показывать клиенту предварительно?
16. Нужен ли ручной пересчет доставки администратором в заказе?
17. Как учитывать подъем 390 руб.: отдельной галочкой в корзине или только текстом?
18. Нужен ли выбор даты доставки в календаре?
18. Нужен ли выбор даты доставки в календаре..S?
19. Нужно ли запрещать выбор выходных и праздников в календаре?
20. Где брать производственный календарь РФ: встроенный список, API или ручная настройка в админке?
21. Какой должен быть внешний стиль: более премиальный, складской/оптовый, домашний семейный или нейтральный продуктовый?