From 6216c1ee4fd94c78cc60c9f4c9132b93efbec75b Mon Sep 17 00:00:00 2001 From: Alisa Date: Thu, 4 Jun 2026 19:19:35 +0300 Subject: [PATCH] =?UTF-8?q?=D1=88=D0=B0=D0=B3=205=20=D0=B7=D0=B0=D0=B2?= =?UTF-8?q?=D0=B5=D1=80=D1=88=D0=B5=D0=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/categories.php | 5 + admin/index.php | 6 + admin/login.php | 5 + admin/logout.php | 5 + admin/products.php | 5 + admin/setup.php | 5 + app/Controllers/CatalogController.php | 14 +- app/Controllers/HomeController.php | 6 +- app/Controllers/PageController.php | 60 ++ app/Core/Auth.php | 86 +++ app/Repositories/AdminRepository.php | 87 +++ app/Services/PriceTierService.php | 9 + database/seed.sql | 1 + docs/SEO_ТЕГИ.md | 55 ++ docs/ЛОГОТИП_И_БРЕНД_РЫБСТОК.md | 43 ++ docs/ШАГ_4_КАТАЛОГ_И_АДМИНКА.md | 19 + public/admin/categories.php | 3 + public/admin/login.php | 50 ++ public/admin/logout.php | 12 + public/admin/products.php | 3 + public/admin/setup.php | 51 ++ public/assets/css/app.css | 802 +++++++++++++++++++++++++- public/assets/images/logo_300.png | Bin 0 -> 7580 bytes public/check.php | 11 + public/index.php | 5 + views/admin/login.php | 29 + views/admin/setup.php | 47 ++ views/layouts/main.php | 78 +++ views/pages/contacts.php | 72 +++ views/pages/delivery.php | 169 ++++++ views/pages/home.php | 72 ++- views/pages/payment.php | 43 ++ views/pages/returns.php | 35 ++ ПЛАН_ДЕЙСТВИЙ_РЫБСТОК.md | 2 + 34 files changed, 1859 insertions(+), 36 deletions(-) create mode 100644 admin/categories.php create mode 100644 admin/login.php create mode 100644 admin/logout.php create mode 100644 admin/products.php create mode 100644 admin/setup.php create mode 100644 app/Controllers/PageController.php create mode 100644 app/Core/Auth.php create mode 100644 app/Repositories/AdminRepository.php create mode 100644 docs/SEO_ТЕГИ.md create mode 100644 docs/ЛОГОТИП_И_БРЕНД_РЫБСТОК.md create mode 100644 public/admin/login.php create mode 100644 public/admin/logout.php create mode 100644 public/admin/setup.php create mode 100644 public/assets/images/logo_300.png create mode 100644 views/admin/login.php create mode 100644 views/admin/setup.php create mode 100644 views/pages/contacts.php create mode 100644 views/pages/delivery.php create mode 100644 views/pages/payment.php create mode 100644 views/pages/returns.php diff --git a/admin/categories.php b/admin/categories.php new file mode 100644 index 0000000..1ac996b --- /dev/null +++ b/admin/categories.php @@ -0,0 +1,5 @@ + 'Товары', 'url' => '/admin/products.php'], ['title' => 'Категории', 'url' => '/admin/categories.php'], @@ -31,6 +34,9 @@ $sections = [

Админка

Рыбсток

Подготовлены основные разделы будущей панели управления.

+ +

Вы вошли как . Выйти

+
diff --git a/admin/login.php b/admin/login.php new file mode 100644 index 0000000..1ddb53f --- /dev/null +++ b/admin/login.php @@ -0,0 +1,5 @@ + 'Каталог продукции - Рыбсток', - 'metaDescription' => 'Полный каталог Рыбсток: рыба, морепродукты, икра, мясо, сыры, полуфабрикаты и продукты с доставкой.', - 'metaKeywords' => 'каталог Рыбсток, рыба, морепродукты, мясо, сыры, полуфабрикаты', + 'title' => 'Каталог Рыбсток - рыба, морепродукты, мясо, сыры и продукты', + 'metaDescription' => 'Полный каталог Рыбсток: рыба, морепродукты, икра, мясо, сыры, полуфабрикаты, овощи, ягоды и бакалея по стоковым ценам с доставкой по Москве и Московской области.', + 'metaKeywords' => 'каталог Рыбсток, купить рыбу Москва, морепродукты Москва, мясо доставка Москва, сыры доставка, икра, полуфабрикаты, овощи ягоды заморозка, бакалея, продукты по стоковым ценам', 'breadcrumbs' => [ ['title' => 'Главная', 'url' => '/'], ['title' => 'Каталог'], @@ -47,10 +47,12 @@ final class CatalogController return; } + $categoryName = (string) $category['name']; + view('catalog/index', [ - 'title' => ($category['seo_title'] ?: $category['name']) . ' - Рыбсток', - 'metaDescription' => $category['seo_description'] ?: '', - 'metaKeywords' => $category['seo_keywords'] ?: '', + 'title' => $category['seo_title'] ?: $categoryName . ' - купить в Рыбсток с доставкой по Москве', + 'metaDescription' => $category['seo_description'] ?: $categoryName . ' в интернет-складе Рыбсток: качественная продукция по стоковым ценам, фасовки для дома и бизнеса, доставка по Москве и Московской области.', + 'metaKeywords' => $category['seo_keywords'] ?: $categoryName . ', купить ' . $categoryName . ' Москва, ' . $categoryName . ' доставка, Рыбсток, продукты по стоковым ценам', 'breadcrumbs' => $categoryRepository->breadcrumbs((int) $category['id']), 'categories' => $categoryRepository->tree(), 'currentCategory' => $category, diff --git a/app/Controllers/HomeController.php b/app/Controllers/HomeController.php index 925c693..a4738c6 100644 --- a/app/Controllers/HomeController.php +++ b/app/Controllers/HomeController.php @@ -38,9 +38,9 @@ final class HomeController } view('pages/home', [ - 'title' => 'Рыбсток - продукты и рыба с доставкой по Москве и Московской области', - 'metaDescription' => 'Рыбсток - интернет-магазин рыбы, морепродуктов, мяса, сыров, полуфабрикатов и продуктов с доставкой по Москве и Московской области.', - 'metaKeywords' => 'Рыбсток, рыба, морепродукты, мясо, сыры, полуфабрикаты, доставка продуктов Москва', + 'title' => 'Рыбсток - качественные продукты по стоковым ценам с доставкой', + 'metaDescription' => 'Рыбсток - интернет-склад качественной рыбы, морепродуктов, мяса, сыров, овощей, ягод и бакалеи по стоковым ценам. Бесплатная доставка по Москве и Московской области, самовывоз с ул. Привольная.', + 'metaKeywords' => 'Рыбсток, рыба доставка Москва, морепродукты доставка Москва, мясо доставка Москва, сыры доставка, икра доставка Москва, овощи ягоды заморозка, бакалея с доставкой, продукты по стоковым ценам, интернет склад продуктов, бесплатная доставка продуктов Москва, доставка продуктов Московская область', 'breadcrumbs' => [ ['title' => 'Главная'], ], diff --git a/app/Controllers/PageController.php b/app/Controllers/PageController.php new file mode 100644 index 0000000..7c0d6dd --- /dev/null +++ b/app/Controllers/PageController.php @@ -0,0 +1,60 @@ + 'Доставка Рыбсток - Москва и Московская область', + 'metaDescription' => 'Условия доставки Рыбсток по Москве и Московской области: бесплатная доставка внутри МКАД от 5000 рублей, за МКАД от 10000 рублей, самовывоз и расчет доставки.', + 'metaKeywords' => 'Рыбсток доставка, доставка рыбы Москва, доставка продуктов Москва, доставка за МКАД, самовывоз Рыбсток, расчет доставки', + 'breadcrumbs' => [ + ['title' => 'Главная', 'url' => '/'], + ['title' => 'Доставка'], + ], + ]); + } + + public function payment(): void + { + view('pages/payment', [ + 'title' => 'Оплата заказа - Рыбсток', + 'metaDescription' => 'Способы оплаты заказа в Рыбсток: оплата при получении наличными или QR-кодом, безналичный расчет от 30000 рублей.', + 'metaKeywords' => 'Рыбсток оплата, оплата при получении, оплата QR кодом, безналичный расчет продукты', + 'breadcrumbs' => [ + ['title' => 'Главная', 'url' => '/'], + ['title' => 'Оплата'], + ], + ]); + } + + public function contacts(): void + { + view('pages/contacts', [ + 'title' => 'Контакты Рыбсток - телефон, адрес самовывоза, email', + 'metaDescription' => 'Контакты Рыбсток: телефоны для заказа и консультации, адрес пункта самовывоза в Москве на ул. Привольная, д. 13к1, email для заявок.', + 'metaKeywords' => 'Рыбсток контакты, телефон Рыбсток, адрес самовывоза Рыбсток, zakaz@rybstock.ru', + 'breadcrumbs' => [ + ['title' => 'Главная', 'url' => '/'], + ['title' => 'Контакты'], + ], + ]); + } + + public function returns(): void + { + view('pages/returns', [ + 'title' => 'Правила возврата товара - Рыбсток', + 'metaDescription' => 'Правила возврата товара в интернет-складе Рыбсток: условия возврата, сохранность упаковки, температурный режим и порядок обращения.', + 'metaKeywords' => 'Рыбсток возврат товара, правила возврата товара, возврат продуктов, условия возврата Рыбсток', + 'breadcrumbs' => [ + ['title' => 'Главная', 'url' => '/'], + ['title' => 'Правила возврата товара'], + ], + ]); + } +} diff --git a/app/Core/Auth.php b/app/Core/Auth.php new file mode 100644 index 0000000..f3d3275 --- /dev/null +++ b/app/Core/Auth.php @@ -0,0 +1,86 @@ +|null + */ + public static function admin(): ?array + { + self::start(); + $adminId = (int) ($_SESSION[self::SESSION_ADMIN_ID] ?? 0); + + if ($adminId <= 0) { + return null; + } + + $admin = (new AdminRepository())->findActiveById($adminId); + + if ($admin === null) { + unset($_SESSION[self::SESSION_ADMIN_ID]); + } + + return $admin; + } + + public static function attempt(string $email, string $password): bool + { + self::start(); + $repository = new AdminRepository(); + $admin = $repository->findActiveByEmail($email); + + if ($admin === null || !password_verify($password, (string) $admin['password_hash'])) { + return false; + } + + session_regenerate_id(true); + $_SESSION[self::SESSION_ADMIN_ID] = (int) $admin['id']; + $repository->touchLastLogin((int) $admin['id']); + + return true; + } + + public static function logout(): void + { + self::start(); + $_SESSION = []; + + if (ini_get('session.use_cookies')) { + $params = session_get_cookie_params(); + setcookie(session_name(), '', time() - 42000, $params['path'], $params['domain'], (bool) $params['secure'], (bool) $params['httponly']); + } + + session_destroy(); + } +} diff --git a/app/Repositories/AdminRepository.php b/app/Repositories/AdminRepository.php new file mode 100644 index 0000000..d8a9a92 --- /dev/null +++ b/app/Repositories/AdminRepository.php @@ -0,0 +1,87 @@ +pdo() + ->query('SELECT COUNT(*) FROM admins WHERE is_active = 1') + ->fetchColumn(); + } + + /** + * @return array|null + */ + public function findActiveByEmail(string $email): ?array + { + $statement = $this->pdo()->prepare( + 'SELECT id, name, email, password_hash, role + FROM admins + WHERE email = :email AND is_active = 1 + LIMIT 1' + ); + $statement->execute(['email' => mb_strtolower(trim($email))]); + $admin = $statement->fetch(); + + return $admin === false ? null : $admin; + } + + /** + * @return array|null + */ + public function findActiveById(int $id): ?array + { + $statement = $this->pdo()->prepare( + 'SELECT id, name, email, role + FROM admins + WHERE id = :id AND is_active = 1 + LIMIT 1' + ); + $statement->execute(['id' => $id]); + $admin = $statement->fetch(); + + return $admin === false ? null : $admin; + } + + public function createOwner(string $name, string $email, string $password): int + { + $name = trim($name); + $email = mb_strtolower(trim($email)); + + if ($name === '' || $email === '' || $password === '') { + throw new \InvalidArgumentException('Имя, email и пароль обязательны.'); + } + + if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { + throw new \InvalidArgumentException('Укажите корректный email.'); + } + + if (mb_strlen($password) < 8) { + throw new \InvalidArgumentException('Пароль должен быть не короче 8 символов.'); + } + + $statement = $this->pdo()->prepare( + 'INSERT INTO admins (name, email, password_hash, role, is_active) + VALUES (:name, :email, :password_hash, "owner", 1)' + ); + $statement->execute([ + 'name' => $name, + 'email' => $email, + 'password_hash' => password_hash($password, PASSWORD_DEFAULT), + ]); + + return (int) $this->pdo()->lastInsertId(); + } + + public function touchLastLogin(int $id): void + { + $statement = $this->pdo()->prepare( + 'UPDATE admins SET last_login_at = NOW() WHERE id = :id' + ); + $statement->execute(['id' => $id]); + } +} diff --git a/app/Services/PriceTierService.php b/app/Services/PriceTierService.php index 03ce157..a3ee5c1 100644 --- a/app/Services/PriceTierService.php +++ b/app/Services/PriceTierService.php @@ -38,6 +38,15 @@ final class PriceTierService ]; } + $tiers[] = [ + 'label' => 'Безналичный расчет', + 'code' => null, + 'min_order_amount' => 0.0, + 'discount_percent' => -8.0, + 'price' => round($basePrice * 1.08, 2), + 'note' => '+8% к базовой цене при безналичном расчете', + ]; + return $tiers; } diff --git a/database/seed.sql b/database/seed.sql index 670bdea..ba2d1b4 100644 --- a/database/seed.sql +++ b/database/seed.sql @@ -404,6 +404,7 @@ VALUES INSERT INTO site_settings (setting_key, setting_value, value_type, group_name) VALUES ('site_phone', '+7-977-736-33-63', 'string', 'contacts'), +('pickup_address', 'г. Москва, ул. Привольная, д. 13к1', 'string', 'contacts'), ('admin_email', '', 'string', 'mail'), ('price_request_email', '', 'string', 'mail'), ('inside_mkad_free_from', '5000', 'number', 'delivery'), diff --git a/docs/SEO_ТЕГИ.md b/docs/SEO_ТЕГИ.md new file mode 100644 index 0000000..9cfcad9 --- /dev/null +++ b/docs/SEO_ТЕГИ.md @@ -0,0 +1,55 @@ +# SEO-теги Рыбсток + +Документ фиксирует базовые теги для главных страниц и шаблоны для дальнейшего заполнения категорий и товаров. Теги со старого сайта можно переносить в админке точечно: они будут иметь приоритет над автоматическими шаблонами. + +## Главная страница + +Title: +Рыбсток - качественные продукты по стоковым ценам с доставкой + +Description: +Рыбсток - интернет-склад качественной рыбы, морепродуктов, мяса, сыров, овощей, ягод и бакалеи по стоковым ценам. Бесплатная доставка по Москве и Московской области, самовывоз с ул. Привольная. + +Keywords: +Рыбсток, рыба доставка Москва, морепродукты доставка Москва, мясо доставка Москва, сыры доставка, икра доставка Москва, овощи ягоды заморозка, бакалея с доставкой, продукты по стоковым ценам, интернет склад продуктов, бесплатная доставка продуктов Москва, доставка продуктов Московская область + +## Каталог + +Title: +Каталог Рыбсток - рыба, морепродукты, мясо, сыры и продукты + +Description: +Полный каталог Рыбсток: рыба, морепродукты, икра, мясо, сыры, полуфабрикаты, овощи, ягоды и бакалея по стоковым ценам с доставкой по Москве и Московской области. + +Keywords: +каталог Рыбсток, купить рыбу Москва, морепродукты Москва, мясо доставка Москва, сыры доставка, икра, полуфабрикаты, овощи ягоды заморозка, бакалея, продукты по стоковым ценам + +## Шаблон категории + +Title: +{Название категории} - купить в Рыбсток с доставкой по Москве + +Description: +{Название категории} в интернет-складе Рыбсток: качественная продукция по стоковым ценам, фасовки для дома и бизнеса, доставка по Москве и Московской области. + +Keywords: +{Название категории}, купить {Название категории} Москва, {Название категории} доставка, Рыбсток, продукты по стоковым ценам + +## Шаблон товара + +Title: +{Название товара} - купить в Рыбсток + +Description: +{Название товара}: фасовки, цены и наличие в Рыбсток. Качественная продукция по стоковым ценам с доставкой по Москве и Московской области. + +Keywords: +{Название товара}, купить {Название товара}, Рыбсток, доставка Москва, продукты по стоковым ценам + +## Правила заполнения + +1. Title держим коротким и понятным: сначала главный запрос, затем бренд. +2. Description пишем человеческим языком, без набора ключей подряд. +3. Keywords используем как вспомогательное поле для внутренней структуры и старых поисковых систем, но не перегружаем. +4. Для важных категорий после переноса товаров лучше написать уникальные описания вручную. +5. Для товаров используем точное название, фасовку, тип заморозки или обработки, если это важно для поиска. diff --git a/docs/ЛОГОТИП_И_БРЕНД_РЫБСТОК.md b/docs/ЛОГОТИП_И_БРЕНД_РЫБСТОК.md new file mode 100644 index 0000000..5aab9a8 --- /dev/null +++ b/docs/ЛОГОТИП_И_БРЕНД_РЫБСТОК.md @@ -0,0 +1,43 @@ +# Логотип и бренд Рыбсток + +## Смысл названия + +Рыбсток состоит из двух смыслов: + +- "Рыб" - рыба, морепродукты, свежесть, холод, складская надежность; +- "Сток" - выгодные стоковые цены, большой ассортимент, возможность купить качественную продукцию дешевле. + +Основной слоган: + +**Качественные продукты по стоковым ценам** + +## Что важно сохранить + +- Название должно читаться быстро: Рыбсток. +- Логотип должен быть уникальным, не похожим на стандартную рыбку из клипарта. +- Знак должен работать в шапке сайта, на мобильном экране, в прайс-листе, накладной и аватарке мессенджера. +- Логотип не должен выглядеть слишком ресторанным или премиально-холодным: Рыбсток - это интернет-склад качественных продуктов по выгодной цене. + +## Направление модернизации + +Подходящий образ: + +- спокойный сильный wordmark "Рыбсток"; +- небольшой знак рыбы, встроенный в букву или стоящий рядом; +- ощущение складской надежности и честной цены; +- цвета: глубокий зеленый/графитовый как база, красный можно оставить только как акцент для цены или знака. + +## Не делать + +- не использовать мультяшную рыбу; +- не делать логотип слишком премиальным, как ресторан высокой кухни; +- не перегружать знак волнами, чешуей и декоративными деталями; +- не использовать много оттенков синего, чтобы бренд не стал обычным рыбным магазином. + +## Текущий статус + +Старый логотип сохранен как временный файл: + +`public/assets/images/logo_300.png` + +Он подключен в шапке сайта. Позже его можно заменить новым файлом с тем же именем или добавить новый SVG/PNG и обновить ссылку в layout. diff --git a/docs/ШАГ_4_КАТАЛОГ_И_АДМИНКА.md b/docs/ШАГ_4_КАТАЛОГ_И_АДМИНКА.md index c38b288..ebe704b 100644 --- a/docs/ШАГ_4_КАТАЛОГ_И_АДМИНКА.md +++ b/docs/ШАГ_4_КАТАЛОГ_И_АДМИНКА.md @@ -91,6 +91,25 @@ В схему добавлены поля `discount_type`, `discount_percent`, используется `old_package_price`, а также таблица `price_requests` для будущих заявок проходных цен. +## Дополнение: главная страница и ценовые градации + +На главную добавлены требования к рабочей витрине: + +- видимое главное меню: Главная, Каталог, Доставка, Контакты, Корзина; +- контактный телефон: +7-977-736-33-63; +- пункт самовывоза: г. Москва, ул. Привольная, д. 13к1; +- первый баннер: "Рыбсток - рыба, морепродукты, мясо, сыры, бакалея. Интернет-склад качественной продукции по стоковым ценам"; +- баннеры: бесплатная доставка внутри МКАД от 5000 руб., бесплатная доставка за МКАД от 10000 руб., можно приобрести день в день, без предоплаты; +- заметное размещение промокодов; +- блок будущей заявки "Запрос проходных цен". + +Ценовых градаций теперь четыре: + +- базовая цена; +- наличный расчет от 20 000 руб. - скидка 5%; +- наличный расчет от 50 000 руб. - скидка 7%; +- безналичный расчет - +8%. + ## Следующий шаг Шаг 5: diff --git a/public/admin/categories.php b/public/admin/categories.php index d4bdbe8..11ca9fb 100644 --- a/public/admin/categories.php +++ b/public/admin/categories.php @@ -3,7 +3,10 @@ declare(strict_types=1); use App\Controllers\Admin\CategoryAdminController; +use App\Core\Auth; require_once dirname(__DIR__, 2) . '/app/bootstrap.php'; +Auth::requireAdmin(); + (new CategoryAdminController())->index(); diff --git a/public/admin/login.php b/public/admin/login.php new file mode 100644 index 0000000..f9052d5 --- /dev/null +++ b/public/admin/login.php @@ -0,0 +1,50 @@ +activeCount() === 0; + +if ($needsSetup) { + header('Location: /admin/setup.php?token=' . rawurlencode((string) app_env('INSTALL_TOKEN', ''))); + exit; +} + +$error = null; +$next = admin_safe_next((string) ($_GET['next'] ?? '/admin/')); + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $next = admin_safe_next((string) ($_POST['next'] ?? '/admin/')); + + if (Auth::attempt((string) ($_POST['email'] ?? ''), (string) ($_POST['password'] ?? ''))) { + header('Location: ' . ($next !== '' ? $next : '/admin/')); + exit; + } + + $error = 'Неверный email или пароль.'; +} + +view('admin/login', [ + 'title' => 'Вход в админку - Рыбсток', + 'breadcrumbs' => [ + ['title' => 'Главная', 'url' => '/'], + ['title' => 'Админка', 'url' => '/admin/'], + ['title' => 'Вход'], + ], + 'error' => $error, + 'next' => $next, +]); + +function admin_safe_next(string $next): string +{ + if ($next === '' || !str_starts_with($next, '/') || str_starts_with($next, '//')) { + return '/admin/'; + } + + return $next; +} diff --git a/public/admin/logout.php b/public/admin/logout.php new file mode 100644 index 0000000..7638ab3 --- /dev/null +++ b/public/admin/logout.php @@ -0,0 +1,12 @@ +index(); diff --git a/public/admin/setup.php b/public/admin/setup.php new file mode 100644 index 0000000..7aee076 --- /dev/null +++ b/public/admin/setup.php @@ -0,0 +1,51 @@ +activeCount() > 0; +$message = null; +$error = null; + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + if (!$canRun || $hasAdmin) { + http_response_code(403); + $error = 'Создание администратора недоступно.'; + } else { + try { + $repository->createOwner( + (string) ($_POST['name'] ?? ''), + (string) ($_POST['email'] ?? ''), + (string) ($_POST['password'] ?? '') + ); + $message = 'Администратор создан. Теперь можно войти.'; + $hasAdmin = true; + } catch (Throwable $exception) { + $error = app_env('APP_DEBUG', 'false') === 'true' + ? $exception->getMessage() + : 'Не удалось создать администратора.'; + } + } +} + +view('admin/setup', [ + 'title' => 'Создание администратора - Рыбсток', + 'breadcrumbs' => [ + ['title' => 'Главная', 'url' => '/'], + ['title' => 'Админка', 'url' => '/admin/'], + ['title' => 'Создание администратора'], + ], + 'canRun' => $canRun, + 'hasAdmin' => $hasAdmin, + 'token' => $actualToken, + 'message' => $message, + 'error' => $error, +]); diff --git a/public/assets/css/app.css b/public/assets/css/app.css index 9370b1a..2d12f3e 100644 --- a/public/assets/css/app.css +++ b/public/assets/css/app.css @@ -10,8 +10,92 @@ body { background: #f7f8f4; } +.site-topbar { + position: sticky; + top: 0; + z-index: 20; + border-bottom: 1px solid #dfe5da; + background: #ffffff; + box-shadow: 0 6px 22px rgba(32, 45, 35, 0.06); +} + +.site-topbar-inner { + width: min(1180px, calc(100% - 48px)); + display: grid; + grid-template-columns: 220px minmax(0, 1fr) 290px; + gap: 24px; + align-items: center; + margin: 0 auto; + padding: 12px 0; +} + +.brand-link { + display: flex; + gap: 10px; + align-items: center; + font-size: 22px; + font-weight: 800; + color: #18201b; +} + +.brand-link:hover { + text-decoration: none; +} + +.brand-link img { + display: block; + width: 126px; + height: auto; +} + +.brand-link small { + max-width: 86px; + font-size: 10px; + font-weight: 700; + line-height: 1.25; + color: #1d5d3b; +} + +.main-nav { + display: flex; + flex-wrap: wrap; + gap: 8px; + justify-content: center; + font-size: 14px; + font-weight: 700; +} + +.main-nav a { + display: inline-flex; + align-items: center; + min-height: 34px; + border-radius: 8px; + padding: 8px 10px; + color: #243229; +} + +.main-nav a:hover { + background: #f0f5eb; + text-decoration: none; +} + +.topbar-contacts { + display: grid; + gap: 4px; + justify-items: end; + font-size: 12px; + line-height: 1.35; + color: #687466; +} + +.topbar-contacts a { + font-size: 17px; + font-weight: 800; + color: #1d5d3b; +} + .site-utility { - width: min(1080px, calc(100% - 48px)); + width: min(1180px, calc(100% - 48px)); display: flex; gap: 14px; align-items: center; @@ -56,9 +140,7 @@ body { .page { min-height: 100vh; - display: grid; - place-items: center; - padding: 24px; + padding: 24px 24px 56px; } .primary-link { @@ -74,7 +156,11 @@ body { } .start-screen.wide { - width: min(1080px, 100%); + width: min(1180px, 100%); + margin: 0 auto; + border: 0; + padding: 0; + background: transparent; } .eyebrow { @@ -150,6 +236,125 @@ a:hover { background: #16492e; } +.secondary-button { + appearance: none; + display: inline-flex; + align-items: center; + justify-content: center; + border: 1px solid #b9c6b1; + border-radius: 8px; + padding: 13px 16px; + font-size: 16px; + font-weight: 800; + color: #1d5d3b; + background: #ffffff; +} + +.secondary-button:hover { + background: #f5f8f1; + text-decoration: none; +} + +.home-hero { + position: relative; + overflow: hidden; + padding: 44px; + border-radius: 8px; + color: #ffffff; + background: #1f5e46; +} + +.home-hero h1 { + margin-bottom: 12px; + color: #ffffff; + font-size: 52px; +} + +.home-hero .eyebrow { + color: #d8eadc; +} + +.hero-lead { + max-width: 760px; + margin: 0; + font-size: 25px; + font-weight: 700; +} + +.hero-actions { + display: flex; + flex-wrap: wrap; + gap: 12px; + margin-top: 24px; +} + +.hero-button { + display: inline-flex; + margin-top: 0; + background: #ffffff; + color: #1d5d3b; +} + +.hero-button:hover { + background: #eef4ea; +} + +.home-banners, +.promo-strip { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px; + margin-top: 18px; +} + +.home-banner, +.promo-strip div, +.price-request-panel { + padding: 18px; + border: 1px solid #dfe5da; + border-radius: 8px; + background: #ffffff; +} + +.home-banner { + display: grid; + gap: 8px; + min-height: 116px; +} + +.home-banner strong, +.promo-strip strong { + color: #18201b; +} + +.home-banner span, +.promo-strip span, +.promo-strip small { + color: #687466; +} + +.promo-strip { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.promo-strip div { + display: grid; + gap: 5px; +} + +.promo-strip strong { + font-size: 22px; +} + +.price-request-panel { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 18px; + align-items: center; + margin-top: 18px; + background: #ffffff; +} + .content-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); @@ -161,7 +366,7 @@ a:hover { padding: 20px; border: 1px solid #dfe5da; border-radius: 8px; - background: #fbfcf8; + background: #ffffff; } .products-panel { @@ -195,7 +400,7 @@ a:hover { .category-list { display: grid; - gap: 8px; + gap: 7px; margin: 0; padding: 0; list-style: none; @@ -210,10 +415,14 @@ a:hover { } .category-list li { - font-size: 16px; + font-size: 15px; font-weight: 700; } +.category-list a { + overflow-wrap: anywhere; +} + .category-list li.is-current > a, .category-list a.is-current-link { display: inline-block; @@ -224,16 +433,16 @@ a:hover { } .category-list.depth-1 li { - font-size: 14px; + font-size: 13px; font-weight: 600; } .category-list.depth-2 { - padding-left: 10px; + padding-left: 8px; } .category-list.depth-2 li { - font-size: 13px; + font-size: 12px; font-weight: 400; } @@ -241,9 +450,16 @@ a:hover { display: flex; gap: 8px; align-items: center; + min-height: 28px; + border-radius: 7px; + padding: 2px 4px; cursor: pointer; } +.category-details summary:hover { + background: #eef4ea; +} + .category-details summary a { min-width: 0; } @@ -357,7 +573,7 @@ a:hover { .admin-layout { width: min(1180px, 100%); display: grid; - grid-template-columns: 320px minmax(0, 1fr); + grid-template-columns: 300px minmax(0, 1fr); gap: 20px; align-items: start; } @@ -369,8 +585,8 @@ a:hover { .sticky-panel { position: sticky; - top: 18px; - max-height: calc(100vh - 36px); + top: 98px; + max-height: calc(100vh - 116px); overflow: auto; } @@ -390,6 +606,371 @@ a:hover { grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); } +.text-page { + width: min(920px, 100%); + margin: 0 auto; +} + +.info-page { + width: min(1180px, 100%); + display: grid; + gap: 18px; + margin: 0 auto; +} + +.info-hero { + display: grid; + grid-template-columns: minmax(0, 1fr) 260px; + gap: 28px; + align-items: center; + border-radius: 8px; + padding: 38px; + color: #ffffff; + background: #1f5e46; +} + +.info-hero h1 { + margin-bottom: 14px; + color: #ffffff; + font-size: 48px; +} + +.info-hero p { + max-width: 760px; + margin: 0; +} + +.info-hero .eyebrow { + color: #d8eadc; +} + +.payment-hero { + background: #263b55; +} + +.contacts-hero { + background: #4d392a; +} + +.info-visual { + display: grid; + place-items: center; + min-height: 210px; + border: 1px solid rgba(255, 255, 255, 0.25); + border-radius: 8px; + background: rgba(255, 255, 255, 0.08); + text-align: center; +} + +.info-visual span { + display: block; + width: 82px; + height: 52px; + border: 4px solid #ffffff; + border-radius: 8px; + box-shadow: 32px 36px 0 -18px #ffffff, -34px 34px 0 -20px #ffffff; +} + +.info-visual strong { + display: block; + margin-top: 18px; + font-size: 32px; + line-height: 1; +} + +.info-visual small { + display: block; + margin-top: 6px; + color: #dfe8dc; +} + +.info-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 18px; +} + +.info-grid.three { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.info-card { + border: 1px solid #dfe5da; + border-radius: 8px; + padding: 24px; + background: #ffffff; +} + +.info-card.compact { + min-height: 190px; +} + +.info-card p { + margin: 0 0 12px; + font-size: 16px; +} + +.info-card p:last-child { + margin-bottom: 0; +} + +.info-card-visual { + height: 118px; + margin-bottom: 18px; + border-radius: 8px; + background: #eef4ea; +} + +.delivery-city { + background: + linear-gradient(90deg, rgba(29, 93, 59, 0.2), rgba(29, 93, 59, 0.04)), + repeating-linear-gradient(90deg, #dfe8dc 0 22px, transparent 22px 44px); +} + +.delivery-region { + background: + linear-gradient(135deg, rgba(38, 59, 85, 0.2), rgba(38, 59, 85, 0.04)), + repeating-linear-gradient(135deg, #dfe8dc 0 20px, transparent 20px 46px); +} + +.payment-cash { + background: + linear-gradient(90deg, rgba(29, 93, 59, 0.16), rgba(29, 93, 59, 0.04)), + radial-gradient(circle at 22% 50%, #1d5d3b 0 24px, transparent 25px); +} + +.payment-scale { + background: + linear-gradient(90deg, rgba(77, 57, 42, 0.16), rgba(77, 57, 42, 0.04)), + linear-gradient(#dfe8dc 0 0) center 68% / 58% 10px no-repeat; +} + +.clean-list { + display: grid; + gap: 10px; + margin: 0 0 14px; + padding: 0; + list-style: none; +} + +.clean-list li { + position: relative; + padding-left: 20px; + font-size: 16px; + line-height: 1.45; +} + +.clean-list li::before { + content: ""; + position: absolute; + top: 0.65em; + left: 0; + width: 7px; + height: 7px; + border-radius: 50%; + background: #1d5d3b; +} + +.delivery-calculator { + display: grid; + gap: 18px; +} + +.calculator-note { + max-width: 260px; + font-size: 13px; + line-height: 1.45; + color: #687466; + text-align: right; +} + +.calculator-form { + display: grid; + grid-template-columns: 1.4fr 0.75fr 1fr 0.6fr; + gap: 14px; + align-items: end; +} + +.calculator-form label { + display: grid; + gap: 7px; + font-size: 14px; + font-weight: 800; + color: #394437; +} + +.calculator-form input, +.calculator-form select { + width: 100%; + border: 1px solid #cfd8c9; + border-radius: 8px; + padding: 12px; + font: inherit; + background: #ffffff; +} + +.calculator-form .checkbox-label { + align-self: center; + margin-top: 24px; +} + +.calculator-result { + display: grid; + gap: 5px; + border-radius: 8px; + padding: 18px; + color: #ffffff; + background: #1d5d3b; +} + +.calculator-result strong { + font-size: 22px; +} + +.calculator-result span { + line-height: 1.45; + color: #d8eadc; +} + +.contact-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 18px; +} + +.contact-card.wide { + grid-column: 1 / -1; +} + +.contact-list { + display: grid; + gap: 16px; + margin: 0; +} + +.contact-list div { + display: grid; + gap: 4px; +} + +.contact-list dt { + color: #687466; +} + +.contact-list dd { + margin: 0; + font-size: 24px; + font-weight: 800; +} + +.route-card { + display: grid; + gap: 18px; +} + +.route-layout { + display: grid; + grid-template-columns: minmax(0, 1.35fr) minmax(300px, 0.65fr); + gap: 20px; + align-items: stretch; +} + +.map-frame { + overflow: hidden; + min-height: 360px; + border: 1px solid #dfe5da; + border-radius: 8px; + background: #eef4ea; +} + +.map-frame iframe { + display: block; + width: 100%; + height: 100%; + min-height: 360px; + border: 0; +} + +.route-steps { + display: grid; + align-content: start; + gap: 16px; + border-radius: 8px; + padding: 22px; + background: #f7f8f4; +} + +.route-steps ol { + display: grid; + gap: 12px; + margin: 0; + padding-left: 20px; +} + +.route-steps li { + font-size: 16px; + line-height: 1.45; +} + +.text-page-header, +.text-page-body { + border: 1px solid #dfe5da; + border-radius: 8px; + background: #ffffff; +} + +.text-page-header { + padding: 30px; + margin-bottom: 18px; +} + +.text-page-header h1 { + margin-bottom: 12px; +} + +.text-page-header p:last-child { + max-width: 760px; + margin-bottom: 0; +} + +.text-page-body { + display: grid; + gap: 0; + padding: 8px 30px 30px; +} + +.text-page-body section { + padding: 24px 0; + border-bottom: 1px solid #e5eadf; +} + +.text-page-body section:last-of-type { + border-bottom: 0; +} + +.text-page-body p { + margin: 0 0 12px; + font-size: 16px; +} + +.text-page-body p:last-child { + margin-bottom: 0; +} + +.notice-panel { + margin-top: 10px; + border: 1px solid #ead8c4 !important; + border-radius: 8px; + padding: 22px !important; + background: #fffaf2; +} + +.thanks-text { + margin: 22px 0 0 !important; + font-weight: 800; + color: #1d5d3b; +} + .admin-form { display: grid; gap: 14px; @@ -450,6 +1031,129 @@ a:hover { color: #687466; } +.site-footer { + margin-top: 28px; + color: #dfe8dc; + background: #18201b; +} + +.site-footer-inner { + width: min(1180px, calc(100% - 48px)); + display: grid; + grid-template-columns: 1.15fr 1.25fr 1fr 1.2fr; + gap: 34px; + margin: 0 auto; + padding: 42px 0 34px; +} + +.footer-logo { + display: inline-flex; + align-items: center; + min-height: 42px; +} + +.footer-logo img { + display: block; + width: 150px; + height: auto; + filter: brightness(0) invert(1); +} + +.footer-logo span { + font-size: 24px; + font-weight: 800; + color: #ffffff; +} + +.footer-brand p, +.footer-column p { + margin: 12px 0 0; + font-size: 14px; + line-height: 1.55; + color: #b8c5b5; +} + +.footer-column h2 { + margin: 0 0 14px; + font-size: 15px; + color: #ffffff; +} + +.footer-contacts { + display: grid; + gap: 12px; + margin: 0; +} + +.footer-contacts div { + display: grid; + gap: 3px; +} + +.footer-contacts dt { + font-size: 12px; + color: #91a08c; +} + +.footer-contacts dd { + margin: 0; + font-size: 14px; + line-height: 1.45; +} + +.site-footer a { + color: #ffffff; +} + +.site-footer a:hover { + color: #d8eadc; +} + +.footer-links { + display: grid; + align-content: start; + gap: 9px; +} + +.footer-links a { + font-size: 14px; +} + +.footer-action { + display: inline-flex; + width: fit-content; + margin-top: 18px; + border: 1px solid #526457; + border-radius: 8px; + padding: 11px 14px; + font-size: 14px; + font-weight: 800; +} + +.footer-action:hover { + border-color: #d8eadc; + text-decoration: none; +} + +.site-footer-bottom { + border-top: 1px solid #2b362f; + padding: 16px 24px 18px; + text-align: center; +} + +.site-footer-bottom p { + max-width: 1180px; + margin: 6px auto 0; + font-size: 12px; + line-height: 1.5; + color: #9eaa9a; +} + +.site-footer-bottom p:first-child { + margin-top: 0; + color: #c7d2c3; +} + @media (max-width: 760px) { .site-utility { width: calc(100% - 32px); @@ -461,6 +1165,44 @@ a:hover { grid-template-columns: 1fr; } + .site-topbar-inner, + .home-banners, + .promo-strip, + .price-request-panel { + grid-template-columns: 1fr; + } + + .site-topbar-inner { + width: calc(100% - 32px); + justify-items: start; + gap: 12px; + } + + .brand-link { + width: 100%; + justify-content: space-between; + } + + .main-nav { + justify-content: flex-start; + } + + .topbar-contacts { + justify-items: start; + } + + .hero-lead { + font-size: 20px; + } + + .home-hero { + padding: 28px; + } + + .home-hero h1 { + font-size: 40px; + } + .settings-list div { grid-template-columns: 1fr; gap: 2px; @@ -475,6 +1217,38 @@ a:hover { grid-template-columns: 1fr; } + .info-hero, + .info-grid, + .info-grid.three, + .calculator-form, + .contact-grid, + .route-layout { + grid-template-columns: 1fr; + } + + .info-hero { + padding: 28px; + } + + .info-hero h1 { + font-size: 38px; + } + + .info-visual { + min-height: 160px; + } + + .calculator-note { + max-width: none; + text-align: left; + } + + .site-footer-inner { + width: calc(100% - 32px); + grid-template-columns: 1fr; + gap: 26px; + } + .sticky-panel { position: static; max-height: none; diff --git a/public/assets/images/logo_300.png b/public/assets/images/logo_300.png new file mode 100644 index 0000000000000000000000000000000000000000..6b22ef563aedefb9ef9077b4d423930022020462 GIT binary patch literal 7580 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 (3IOfT006CQwB^ z_qLr7a#Pbrdrng|mrX-FnQRv=Jj?Fp-wvdj?!~r=QIhy>ov=B$x6&R;To`PS(Z;{$octii_hZ; zD*n)BR=%$xnAI&d^dP|QGP_@wg85Kh*08qZZ)5chK}(xSot5n}F;FJ!d{av)A<1l3aR5ugRc2s$XPq zi+M_7~zhY5JUl=;TJdbc+C{h zdT_l>O;ncJdkZ}F{*9%CSp{YPpT2$sGQgS1e8nbcR>K`KxO;-WhBsG1yS2y)sL&gW z9!mwpx{HBj9w?V3yH>bTXSa z2?(i)Jpe09{2N19wR{9Ne^w6!raf(hxLpQ10CPea#$FGl)6a=h+5`jucVlh>n#+?y zgRj)h-$N;aX+PR)lQ2p4pij<12C?`(&+9{>_^%!0cg|>n+r*^Kd~-HAud<*Zz}xk1 zmus9E%9CXhEpO;KHP@NC4QdQ!L82>Da_`HBOm*2Lger;DC#=+MWgG;7H3!~0GU&M6 z2jGJ`B6->f_$h83a!8JWs=vyrD4&7U9=#Q>;{k(@*0c=E>c+IN^MBKROM3`=adkp? z=B`T9f}AU!@PrI($@pyHqJc7vX~S00u9E>SAJ$UjI4jd8d0G@_zKOvks_ggA|D|Y} ziZS1p09#t$l~VR2ChGi6t0R~!{?P1hn)}};?5%>2VvfKn&b<8yD^D6{C==ygu#S2& zQ3WP0^Ut*KmSZpR*y{#zxx{FwbN9k+>e(~*3FWyPy9sp~$`oh&+`3q~?X7O|Elp1O zo54)GDjVX9fMM49P525^kbmd)p_Rdvk>d$hrWaQQO^r5w<@`Ri|1CwX>5bI{u7!bT zrC##JUn`{5*_py@=7Zx{A6iUWiZOvTv>LdnPCIAy(QF}Y4K>|MEX44biH6Xd0Xd*o zsneWY?*xMx-@<^-qI^Xqp9;c?S#ZB_itoX!$miqD&-`h7akLKgmvyWhGpG5g4AxZz zDzlal=FbxgI0e1Fu*yD7i$GRfTR69bB}@SaIe{xsRK00N7E1VfPRiiEQ8;9VE=A7$ zNcgPqVd0a)p_sselD?=HTB4mnrqv#3iqi53H!*qWceFaD>78@$+lG7E`#3gG`G^Fv zY0v`4bA&E?`N15Br!gDU_ZtFpMrT!djW-q^P=mlQ3zmDRj}FK@8_SXIIIB2jk5x=5 zsJs|>NX&(_?OJE>w?u! z@kTnn%%L{InlRMOvo`1q1_ODn31~oCZLRwPVLtXsvI5KSR=VoGs_mI;lO6uiLTDcr zNBEQom}L(2jUi>Rc)%&H43q%=QP11HU1q8s8n;Us&-b;}$oZa?7uwo-Mu;;7pP1v< z;@}?>SX&${=o~a4gQ(|YImkH7b=!A{(2-c6ylmG5q20^wiqj)}%3F7iIp+AVw%DIA zBQhxtC$@Fy;78xfE#8whKu@X~E{WVXI+nvBegHSeUEi#0MkDY7=;i zEwWM3lRuv>9rS_>E#fD`;=W-ycDxV zTQ0}SpwK4H(-v(mbxXa17RUB>2~6}R;R4rp&a!CZ8tx;4T8bKhg@XEqf;1h|Znzc- zAb77%xhp_daAY0i=y?d#6ztjy@{^KRIRpkOzi96^z*6v}z(c{ZZ5;)EI^XZBz6@4y z=SiW!Mb8iGzM_AqOB-pgjeE%twT&`Z`O=rY^{I`jcP$b3<-Lj8MNqK_?sl`VVCQxd zzP_aXjvvJtC!V1ELIngb+xz;;lgCpdPsZXNf_;qzoAuSt79ZJ8&dmMhq>>&*?&+Bb zY!;-g9o0#ndPVMMlm6Oa`-j10ZLv8w|2&8qt|tn<(52-|cDgR+1XB9oFUYX>hcA5v6c(oN^Zu z1SOf;5OhZ85vZjdU0-0{DiBdkpu0winAf?7UREn}gk#+c4MxY;LK6%Q*(GWaCnyZIc;hmgQM&|FgR+6?Qvf3P0B62Si&AU$J5 zqDjbgE*#!Be0NyiqvqM+X<_@UW+B(Pe)#gRcdx>;Lc17dVJL9a?*_VuDHo5KL|8t7 zRVT|cqsrxe-B4%pUP8y3#d?|t0bxd>vG}y1R|i@K3Ec?Mf?o`a5X2f!4sE2Br%HKf zM)E@mgtPCghS}5krCcb}g8^DMs(RWvuPu}vUAsgB4?#QX^n_|@N4w;ee=PM3*lJ@l zsq))`LwLF8L&67!z1M_^2q)mw!@cDGR>u|wW2@C_2fwxLsqF}GLfbW!gVw7}?BA;s z*EyHg<;^#>(9k~~wEOj_>$kACQ!X#}@Z8NEuxEmFLv%`_vz_`%^;K zLVcSsSBG}MYDejRv^On*wm@69Ri%de(-!5IDt$wFSA8)B&EHt@jzg2Odl4YshC>US zg`BJ0wq%)nJRg&jj2Kej7;}^fi6u>d1mx@XB19nP>zB4Q$yz~+l99xQbkE+W&DX3U zZTX@c|FC?n4K8jbtyOJ_-)K+u3l8o4*8z2yF%T{Wg9kbVW>7P`97tv^o!CnlXetD# z-)aquDz7|u+4<{8m|^{|K3>L3<_tn8%)G%WqRPSafK zV4|o&s4>rnCjLptAihTU+K?vlhEVw?A?KVFzH4!f7l(vt7x~{M!pAI*#gasDW#pU- zg->0a|NhYP8$|wp>+n5`oR+zK}_RV2JJ{N@%Ly)n~=R($m}+hw61_gQQD~m)6c}wP5|yzZv=0BI|T&3 z{O(M0&qJN~9cc&JsK9`+`M8$>qxgW7vxaA)zO)gut`sMPHm!qj>}h;8Y_cUB>h>Rk zzTp|tvI^Skn~a!py*jv$%(U)V!z8IMsE08?kh@p=$~wZ5wpQ9un~Y@(8O6yTK1ZTL zWs$jp@9w1;(7p89UF2B)!?8ojTWx2 zO30^g4kG_IQBp*>6mAnfJJ&fPjq@0W1eS+xkWwqnOeHn3R~^-F2+=t`Gy`)Cr4vb; zA(6kdzx+(An1jwW@QM$@K-b7U$?P{}sRvzw->^PUX9qKjR}2+j4UMrjKeUrUr`*W2 zSS10Uvb$$#M|qz-VE1bMF)ftSGw7lGPJ1ZOc?g(QcEvl-aphS^d1FH0RId9(5qLw{ z3zVZSLxKF`u4i)L=PD^ih6HGhQtw&_4S~?s|M=F1;GU&F_t2|@HsG}9CzTO`#zx3{ z?)4jx9H-6HRU2wglk#k2Kz}@B!kUzsxi8CrUS`1%9yA-mN;ZX4a*cb0rGJki$Eg=< zNn1Q2*E%&Mpa+rvGlAXDd9>Oa$(pni3#-eQWk&SEB^LDqs0t5Y|d`Itw&m*IL2 z{B!jJ1(VQ(LLdVnT3$U~jxYZk0!>#v22Gg?`Y9h$Ekv|!Z4xSjV|w*1<*l{lViJ0G zHHy!)HdVD%>sMPFGKfzJi@Emn@Veo}Lk{(egqI3$5*?Q0d#a>h~xds{52ZGZ`RC@`4KSk(KTpfF2A zPcthZbP7w^TS3%A-HvG1WTp&hEd>S`t%L?tzrOtzA%~{=)QQxn@>aK)knRJ@2t94I z{I9?<&MVIfXb;SB&imS@=f$D437XVz`e6XHlBvt)vQnBXY2dYQiu-_>RKPhe*FOP8 zD;|O}$Zt*qk*BG^Ga=a2h51xN8xVA$owQ4FCA8LrKH%88U$kvI3(-UfWZ6RH^tXt_ zd?g^6dFVThj#Jy;H_SPMZ_`|u zs3O20|JHLQOs3&W;&euNl7c? zXo11rd)1e6wWa`vv6X_NJgZU9kGPMo&Nj0k>>0(|B$k5-6^10^F2}R*sGU?Ue_tW=F zztA=&@?M{vj0|dKwZ3N|$?7TLK*(wzeZ>C>LJNxv21ngq>cVO3x6 zYtxwsh)0H&uZyWf5V}@v-g|BYI^jr2v``NN++Z=|^2N1%fuJyWn9ptCBmwDvCXb~- zm4N}NPgxZPWwIcmuP~E2Y?Z$aMD*Ng;)GHQeOev376>ABXrF`j(%uGWdEWXEf#iPm zgr{sYlDc1LMc&LV-_OncR`EOU3F?^pEVDqi yIKVIq(jt1$(ag%dP>+L%S~zU+8HEd6;Qs+FGm7kR_$F8Y0000get('/', static fn () => (new HomeController())->index()); $router->get('/catalog', static fn () => (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()); +$router->get('/returns', static fn () => (new PageController())->returns()); $router->get('/catalog/{slug}', static fn (string $slug) => (new CatalogController())->category($slug)); $router->dispatch($_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI']); diff --git a/views/admin/login.php b/views/admin/login.php new file mode 100644 index 0000000..80fbb81 --- /dev/null +++ b/views/admin/login.php @@ -0,0 +1,29 @@ + +
+

Админка

+

Вход

+

Введите email и пароль администратора.

+ + +
+ + +
+ + + + +
+
diff --git a/views/admin/setup.php b/views/admin/setup.php new file mode 100644 index 0000000..74e4bad --- /dev/null +++ b/views/admin/setup.php @@ -0,0 +1,47 @@ + +
+

Админка

+

Создание администратора

+ + + + + + +
+ + + +

Администратор уже создан. Создание через setup закрыто.

+

Войти в админку

+ +
Нужен корректный токен установки.
+ +

Создайте первого администратора. После этого страница станет недоступна для повторного создания.

+
+ + + + + +
+ +
diff --git a/views/layouts/main.php b/views/layouts/main.php index 8f85cac..e767ebc 100644 --- a/views/layouts/main.php +++ b/views/layouts/main.php @@ -24,6 +24,30 @@ declare(strict_types=1); +
+ +
+
@@ -35,5 +59,59 @@ declare(strict_types=1);
+ + diff --git a/views/pages/contacts.php b/views/pages/contacts.php new file mode 100644 index 0000000..de8572c --- /dev/null +++ b/views/pages/contacts.php @@ -0,0 +1,72 @@ +
+
+
+

Связь с Рыбсток

+

Контакты

+

Позвоните по вопросам заказа или продукции. Самовывоз доступен после подтверждения заказа.

+
+ +
+ +
+
+

Телефоны

+
+
+
По вопросам вашего заказа
+
+7-977-736-33-63
+
+
+
Консультация по продукции
+
+7-967-198-10-94
+
+
+
+ +
+

Email

+

zakaz@rybstock.ru

+
+ +
+

Адрес пункта самовывоза

+

г. Москва, м. Лермонтовский проспект, ул. Привольная, д. 13к1

+

Вход со двора через крыльцо Wildberries. Самовывоз возможен только после подтверждения заказа.

+
+
+ +
+
+
+

Как пройти

+

Маршрут к пункту самовывоза

+
+ Открыть в Яндекс.Картах +
+ +
+
+ +
+ +
+
    +
  1. Доезжайте до станции метро “Лермонтовский проспект”.
  2. +
  3. Идите к адресу: ул. Привольная, д. 13к1.
  4. +
  5. Зайдите во двор дома.
  6. +
  7. Ориентир - крыльцо Wildberries, вход со двора.
  8. +
  9. Перед самовывозом дождитесь подтверждения заказа по телефону.
  10. +
+

Если сложно найти вход, позвоните нам: +7-977-736-33-63. Подскажем, как пройти.

+
+
+
+
diff --git a/views/pages/delivery.php b/views/pages/delivery.php new file mode 100644 index 0000000..1ede44c --- /dev/null +++ b/views/pages/delivery.php @@ -0,0 +1,169 @@ +
+
+
+

Москва и Московская область

+

Доставка и самовывоз

+

Привозим заказы после подтверждения по телефону. В день заказа можно получить товар, если оформить заказ в будний день до 14:00.

+
+ +
+ +
+
+
+

Калькулятор

+

Рассчитать доставку

+
+ Расчет предварительный, финально подтвердит менеджер. +
+ +
+ + + + + + + + + +
+ +
+ Доставка: 0 руб. + Введите сумму заказа и условия доставки. +
+
+ +
+
+
+

Доставка внутри МКАД

+
    +
  • Заказы от 5000 руб. - бесплатно до подъезда.
  • +
  • В остальных случаях доставка 599 руб.
  • +
  • Минимальная сумма заказа на доставку внутри МКАД - 2000 руб.
  • +
  • Подъем до квартиры или офиса - 390 руб.
  • +
+

Водитель может оставить заказ у консьержа, соседа и т. д. при полной оплате по QR-коду и оплате подъема.

+
+ +
+
+

Доставка за МКАД

+
    +
  • До 10 км от МКАД бесплатная доставка от 10000 руб.
  • +
  • От 10 км и дальше: каждый километр от МКАД = 1000 руб. к сумме заказа для бесплатной доставки.
  • +
  • Если заказ не попадает под бесплатную доставку: 199 руб. + 60 руб./км.
  • +
  • Минимальная сумма заказа - 5000 руб.
  • +
  • Подъем до квартиры или офиса - 390 руб.
  • +
+

Например, 15 км от МКАД - бесплатная доставка от 15000 руб.; 17 км - от 17000 руб.

+
+
+ +
+
+

Как мы развозим

+

Понедельник-пятница: 18:00-24:00 без ограничения по точному времени.

+

При построении маршрута учитываем комментарии к заказу, например: “буду дома после 19:00”.

+
+ +
+

Самовывоз

+

Понедельник-пятница: 18:00-21:00.

+

Минимальная сумма заказа на самовывоз - 1 руб. Самовывоз возможен только после подтверждения заказа.

+
+ +
+

Когда получить заказ

+

Заказы до 14:00 в будние дни можно получить день в день.

+

Заказы после 14:00 - на следующий или другой удобный для вас день.

+
+
+ +
+

Оплата при получении

+

На сайте ничего оплачивать не нужно. Вы оформляете заказ, чтобы мы его увидели, проверили наличие и собрали с учетом ваших пожеланий. Оплата товара производится только при получении.

+

При доставке и самовывозе можно оплатить наличными или QR-кодом. Безналичный расчет и “Меркурий” обсуждаются индивидуально для заказов от 30000 руб.

+
+
+ + diff --git a/views/pages/home.php b/views/pages/home.php index ce5cc83..e0a8931 100644 --- a/views/pages/home.php +++ b/views/pages/home.php @@ -11,15 +11,61 @@ declare(strict_types=1); /** @var string $buildVersion */ ?>
-

Шаг 3

-

Рыбсток

-

Каталог открывается уже на главной странице, чтобы покупатель сразу видел ассортимент и не делал лишних переходов.

-

Версия сборки:

+
+

Интернет-склад качественной продукции

+

Рыбсток

+

Рыба, морепродукты, мясо, сыры, овощи, ягоды и бакалея по стоковым ценам.

+ +
-
- MariaDB: - -
+
+
+ Бесплатная доставка внутри МКАД от 5000 руб. + Привезем заказ после подтверждения по телефону. +
+
+ Бесплатная доставка за МКАД от 10000 руб. + Для адресов до 10 км от МКАД, дальше расчет по расстоянию. +
+
+ Можно приобрести день в день + Привезем заказ день в день, если он оформлен в будний день до 14:00. +
+
+ Без предоплаты + Оплата после подтверждения заказа и получения товара. +
+
+ +
+
+ Промокод + Рыбсток20000 + 5% при наличном расчете от 20 000 руб. +
+
+ Промокод + Рыбсток50000 + 7% при наличном расчете от 50 000 руб. +
+
+ Безналичный расчет + +8% + Отображается отдельной ценовой градацией. +
+
+ +
+
+

Запрос проходных цен

+

Пришлите список позиций, а мы сверим цены

+

Можно вставить список текстом или приложить файл. Заявка будет уходить администратору на почту.

+
+ Подготовить форму +
@@ -27,7 +73,7 @@ declare(strict_types=1);
-

Категории

+

Каталог

Категории пока не найдены. Если схема уже применена, проверьте `database/seed.sql`.

@@ -41,18 +87,18 @@ declare(strict_types=1);
-

Настройки доставки

+

Доставка

-
Внутри МКАД бесплатно от
+
Бесплатно внутри МКАД от
руб.
-
Внутри МКАД платно
+
Доставка внутри МКАД
руб.
-
За МКАД расчет
+
За МКАД
руб. + руб./км
diff --git a/views/pages/payment.php b/views/pages/payment.php new file mode 100644 index 0000000..c435ed6 --- /dev/null +++ b/views/pages/payment.php @@ -0,0 +1,43 @@ +
+
+
+

Без оплаты на сайте

+

Оплата заказа

+

На сайте вы только оформляете заказ. Оплата производится в момент получения товара после подтверждения и проверки.

+
+ +
+ +
+
+
+

Оплата при получении

+

Оплатить товар при доставке или самовывозе можно наличными или QR-кодом.

+

Кнопки “Оплатить” нет ни в одном разделе сайта Рыбсток: мы сначала подтверждаем заказ, наличие и фактический вес товара.

+
+ +
+
+

Весовая продукция

+

Весовая продукция на сайте рассчитывается по приблизительному весу товара.

+

Перед отправкой или выдачей заказ пересчитывается по фактическому весу.

+
+
+ +
+
+

Безналичный расчет

+

Выставляем счета на безналичный расчет от 30000 руб.

+

Для безналичного расчета действует отдельная ценовая градация: +8% на весь чек.

+
+ +
+

Проверка качества

+

Мы бережно демонстрируем товар, который привезли. Если качество товара вас не устроит, вы можете не приобретать этот товар.

+
+
+
diff --git a/views/pages/returns.php b/views/pages/returns.php new file mode 100644 index 0000000..1482a44 --- /dev/null +++ b/views/pages/returns.php @@ -0,0 +1,35 @@ +
+
+

Покупателям

+

Правила возврата товара

+

Процедура возврата товара регламентируется статьей 26.1 федерального закона «О защите прав потребителей».

+
+ +
+
+

Сроки возврата

+

Потребитель вправе отказаться от товара в любое время до его передачи, а после передачи товара - в течение семи дней.

+

Покупатель вправе вернуть товар при соблюдении правил возврата в течение 7 дней после покупки путем доставки товара в наш магазин по адресу: г. Москва, ул. Привольная, д. 13к1.

+
+ +
+

Условия возврата товара надлежащего качества

+

Возврат товара надлежащего качества возможен, если сохранены его товарный вид, потребительские свойства, а также документ, подтверждающий факт и условия покупки указанного товара.

+

Возврат возможен при условии правильного хранения товара с соблюдением температурного режима. Также должны быть сохранены фирменная тара, упаковка, маркировка, внешний вид и комплектность товара.

+
+ +
+

Возврат денежных средств

+

При отказе потребителя от товара продавец возвращает денежную сумму, уплаченную потребителем по договору, за исключением расходов продавца на доставку от потребителя возвращенного товара.

+

Возврат денежных средств производится не позднее чем через десять дней со дня предъявления потребителем соответствующего требования.

+
+ +
+

Готовая продукция

+

Обращаем ваше внимание, что возвраты на готовую продукцию не принимаются. Пожалуйста, осматривайте внешний вид товара при получении.

+

Если вас не устроил посол, это не является причиной для возврата. Количество соли - субъективное восприятие каждого покупателя. Вся продукция изготавливается в соответствии с техническими условиями завода-производителя.

+
+ +

Спасибо за понимание.

+
+
diff --git a/ПЛАН_ДЕЙСТВИЙ_РЫБСТОК.md b/ПЛАН_ДЕЙСТВИЙ_РЫБСТОК.md index 278d921..f4c9367 100644 --- a/ПЛАН_ДЕЙСТВИЙ_РЫБСТОК.md +++ b/ПЛАН_ДЕЙСТВИЙ_РЫБСТОК.md @@ -642,4 +642,6 @@ - Шаг 3 выполнен: созданы первые PHP-классы, репозитории, контроллер главной страницы, вывод категорий/настроек/популярных товаров, каталог на главной, категории старого сайта в `database/seed.sql`, SEO-поля, хлебные крошки, кнопка назад, градации выгодных цен по промокодам и документ `docs/ШАГ_3_PHP_КЛАССЫ.md`. - Перенос товаров выделен в отдельную задачу: `docs/ПЛАН_ПЕРЕНОСА_ТОВАРОВ.md`. - Шаг 4 выполнен: добавлен роутинг, страницы полного каталога и категорий, боковая навигация, вывод товаров по вложенным категориям, первые формы админки для категорий/товаров, расширенная модель атрибутов/фасовок как в старой админке, анализ первых товаров, сценарные сегменты каталога, бейджи, правила рекомендаций, события для будущего обучения и документы `docs/ШАГ_4_КАТАЛОГ_И_АДМИНКА.md`, `docs/АНАЛИЗ_ПЕРВЫХ_ТОВАРОВ_И_АТРИБУТОВ.md`, `docs/АНАЛИЗ_КАТАЛОГА_И_РЕКОМЕНДАЦИЙ.md`. +- Главная страница дополнена требованиями к первому экрану: видимые контакты, пункт самовывоза, меню `Главная / Каталог / Доставка / Контакты / Корзина`, баннеры про доставку, день-в-день и отсутствие предоплаты, видимые промокоды и блок будущего запроса проходных цен. +- Ценовые градации должны быть четыре: базовая цена, наличный расчет от 20 000 руб. со скидкой 5%, наличный расчет от 50 000 руб. со скидкой 7%, безналичный расчет с наценкой 8%. - Следующий шаг: сделать авторизацию администратора, защитить админку, добавить редактирование категорий/товаров и заготовку страницы товара.