Интернет-магазин свежих букетов с доставкой за 2 часа. Laravel 12 + Blade + vanilla JS без тяжёлых фронтенд-фреймворков.
- О проекте
- Скриншоты
- Стек
- Функционал
- Архитектурные решения
- Чему я научился
- Статистика проекта
- Структура проекта
- Установка
- Переменные окружения
- Telegram-интеграция
- Маршруты
- База данных
- Мультивалютность
- Особенности реализации
- Админка
- Деплой
- Планы по развитию
- Тесты
- Лицензия
- Контакты
Flower Shop — полноценный интернет-магазин цветов с доставкой за 2 часа. Одностраничный лендинг на Laravel 12 + Blade + vanilla JS, без Vue/React и без сборщика. Заказы уходят напрямую в Telegram через Bot API. Есть простая админка для управления товарами.
Проект задуман как демонстрация того, что можно построить быстрый, лёгкий и функциональный магазин без тяжёлого фронтенд-стека — только PHP, Blade и чистый JavaScript.
Что реализовано:
- 🎠 Собственная hero-карусель (без Swiper) с бесконечным циклом, свайпами и адаптивным позиционированием
- 🛒 Корзина на
localStorageс промокодом и мультивалютностью (5 валют) - 🌓 Тёмная тема, кастомный курсор, прелоадер, анимации появления
- 📱 Адаптивная вёрстка (mobile-first) с touch-событиями
- 🤖 Отправка заказов в Telegram Bot API через HTTP-клиент Laravel
- ⚙️ Админка для CRUD товаров с загрузкой изображений
- 🎨 Кастомный скролл по секциям, скелетоны загрузки картинок
- 💱 Пересчёт цен на клиенте при смене страны (RU / US / EU / BY / KZ)
| Слой | Технология | Версия |
|---|---|---|
| Backend | PHP + Laravel | 8.2+ / 12 |
| Frontend | Blade + Vanilla JS | ES6+ |
| Стили | CSS3 (custom properties, grid, flexbox) | — |
| БД | SQLite (по умолчанию) / MySQL / PostgreSQL | — |
| HTTP-клиент | Laravel HTTP Client (Guzzle) | — |
| Уведомления | Telegram Bot API | — |
| Аналитика | Яндекс.Метрика (опционально) | — |
| Инструменты | Vite 7, Tailwind 4, Pint, PHPUnit, Pail | — |
Vite и Tailwind подключены «на будущее» — в
home.blade.phpне используются. Весь фронтенд — чистый CSS и JS без сборки.
- Hero-карусель — собственная реализация без Swiper: бесконечный цикл, 1 сосед слева / 1 справа, остальные скрыты. Работает с любым числом товаров (1, 2, 3, ...). Свайпы через Pointer Events.
- Каталог — сетка карточек с псевдо-скелетонами при загрузке картинок и анимацией появления.
- Фильтры — по цене (
до 3000,3000–5000,от 5000). - Сортировка — по цене ↑/↓.
- Карточка товара — модалка с зумом изображения, размером, стойкостью.
- Мультивалютность — RU / US / EU / BY / KZ, курсы заданы вручную в JS.
- Тёмная тема — переключатель в шапке, сохраняется в
localStorage. - Кастомный курсор — точка + кольцо, реагирует на интерактивные элементы (только desktop ≥ 1024px).
- Прелоадер — при первой загрузке.
- Кастомный скролл — перехват
wheel, плавная прокрутка по секциям.
- Корзина хранится в
localStorage(ключflowershop_cart). - Промокод
FLOWER10— скидка 10%. - Валидация формы: имя, телефон (маска по стране), адрес.
- Отправка заказа → Telegram. В БД заказы не сохраняются (by design — меньше оверхеда, real-time уведомления).
- Номер заказа генерируется случайно (
random_int(1000, 9999)). - Анимация «летящего букета» при добавлении товара.
- URL:
/admin/products - CRUD товаров: список (пагинация 15/стр.), создание, редактирование, удаление.
- Загрузка картинок в
public/products/(PNG/JPG до 4 МБ). - Открывается по 5 кликам на логотип на главной (в iframe-модалке).
- Валидация через
ProductController::validated().
| Решение | Почему |
|---|---|
| Vanilla JS вместо Vue/React | Лендинг не требует SPA-роутинга; меньше вес, быстрее загрузка, нет сборщика |
Корзина в localStorage |
Нет необходимости в серверных сессиях; работает без БД, мгновенный отклик |
| Заказы только в Telegram | Нет оверхеда на таблицу orders; уведомления приходят real-time менеджеру |
| Своя карусель вместо Swiper | Полный контроль над анимацией, нет зависимости (~30 КБ), уникальные позиции pos-center/left/right |
Один Blade-файл home.blade.php |
Быстрое прототипирование лендинга; всё в одном месте без разбиения на partials |
| Курсы валют в JS | Не требует внешних API, работает офлайн; при необходимости легко заменить на запрос к ЦБ |
| Админка на 5 кликов по логотипу | Скрытый вход без отдельной страницы логина — удобно для владельца-одиночки |
| **Аксессор image_url в модели | Единый путь через asset($this->image), файлы лежат в public/products/ или public/images/ |
- Laravel 12 — новый стиль конфигурации через
bootstrap/app.php(безKernel.php), сервис-контейнер, Eloquent-аксессоры, HTTP-клиент - Собственная карусель на Pointer Events без сторонних библиотек: расчёт позиций, бесконечный цикл через модульную арифметику, свайпы
- Работа с
localStorage— корзина, тёмная тема, выбранная валюта, промокод (несколько независимых ключей) - Интеграция с Telegram Bot API через HTTP-клиент Laravel: HTML-сообщения, эмодзи-флаги, форматирование заказа
- Загрузка файлов — валидация размера/типа,
storage:link, аксессоры для генерации URL - Мультивалютность — пересчёт цен на клиенте с сохранением выбора пользователя
- Кастомный скролл — перехват
wheel,easeInOutCubic, прокрутка по секциям - Адаптивность — mobile-first, медиа-запросы,
matchMedia, touch-события - Анимации CSS — скелетоны, пульсация бейджа, «летящий букет», прыгающая иконка корзины
- Кастомный курсор — точка + кольцо с задержкой через
requestAnimationFrame, реакция на:hoverинтерактивных элементов
- Строк кода: ~3500 (PHP + Blade + JS + CSS)
- Контроллеров: 3 (
HomeController,OrderController,Admin\ProductController) - Моделей: 2 (
Product,User) - Сервисов: 1 (
TelegramNotifier) - Миграций: 4
- Blade-шаблонов: 4 (
home,admin/layout,admin/products/index,admin/products/form) - Маршрутов: 11
- Время разработки: ~2 недели
.
├── app/
│ ├── Http/Controllers/
│ │ ├── Controller.php
│ │ ├── HomeController.php
│ │ ├── OrderController.php
│ │ └── Admin/ProductController.php
│ ├── Models/
│ │ ├── Product.php
│ │ └── User.php
│ ├── Providers/AppServiceProvider.php
│ └── Services/TelegramNotifier.php
├── bootstrap/app.php
├── config/services.php
├── database/migrations/
│ ├── 0001_01_01_000000_create_users_table.php
│ ├── 0001_01_01_000001_create_cache_table.php
│ ├── 0001_01_01_000002_create_jobs_table.php
│ └── 2026_09_21_130105_create_products_table.php
├── docs/screenshots/
│ ├── 1.png
│ ├── 2.png
│ ├── 3.png
│ ├── 4.png
│ └── 5.png
├── resources/views/
│ ├── home.blade.php
│ └── admin/
│ ├── layout.blade.php
│ └── products/
│ ├── index.blade.php
│ └── form.blade.php
├── routes/web.php
├── composer.json
├── package.json
└── .env.example
- PHP 8.2+ с расширениями:
pdo_sqlite,mbstring,openssl,tokenizer,xml,ctype,json,curl - Composer 2
- Node.js 18+ (опционально — только для Vite)
- SQLite (по умолчанию) или MySQL/PostgreSQL
git clone <repo-url> flowershop
cd flowershop
composer install
cp .env.example .env
php artisan key:generate
touch database/database.sqlite
php artisan migrate --seed
php artisan serveСайт: http://localhost:8000
composer setupСкрипт делает: composer install, копирует .env, генерит ключ, мигрирует БД, ставит npm-зависимости, собирает фронт.
composer devЗапускает параллельно: php artisan serve, queue:listen, pail, vite dev.
Скопируйте .env.example → .env и заполните.
| Переменная | Описание | Пример |
|---|---|---|
APP_KEY |
Ключ приложения | генерится php artisan key:generate |
APP_URL |
Базовый URL | http://localhost |
DB_CONNECTION |
Драйвер БД | sqlite |
TELEGRAM_BOT_TOKEN |
Токен бота | 123456:ABC-DEF... |
TELEGRAM_CHAT_ID |
ID чата/канала | -1001234567890 |
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=flowershop
DB_USERNAME=root
DB_PASSWORD=APP_ENV=production
APP_DEBUG=false
APP_URL=https://your-domain.comЗаказы уходят в Telegram через App\Services\TelegramNotifier.
- Создайте бота через @BotFather →
/newbot→ получите токен. - Узнайте chat_id:
- Напишите боту любое сообщение.
- Откройте
https://api.telegram.org/bot<TOKEN>/getUpdates. - Найдите
"chat":{"id": ...}— это ваш chat_id. - Для канала: добавьте бота админом и используйте
@usernameили ID вида-100....
- Пропишите в
.env:
TELEGRAM_BOT_TOKEN=123456:ABC-DEF...
TELEGRAM_CHAT_ID=-1001234567890- Очистите кэш конфига:
php artisan config:clearTelegramNotifier::sendOrder() отправляет HTML-сообщение с:
- Номером заказа, именем, телефоном, адресом, страной (с флагом)
- Валютой, в которой видел цены клиент (если не RUB)
- Комментарием
- Составом заказа (в рублях)
- Подытогом, скидкой, итогом
- Временем (МСК)
TelegramNotifier::send() логирует warning и возвращает false. OrderController отвечает 500, клиент видит ошибку.
| Метод | URI | Контроллер | Назначение |
|---|---|---|---|
| GET | / |
HomeController@index |
Главная страница |
| POST | /api/orders |
OrderController@store |
Приём заказа → Telegram |
| GET | /admin |
редирект | → /admin/products |
| GET | /admin/products |
Admin\ProductController@index |
Список товаров |
| GET | /admin/products/create |
Admin\ProductController@create |
Форма создания |
| POST | /admin/products |
Admin\ProductController@store |
Сохранить товар |
| GET | /admin/products/{id} |
Admin\ProductController@show |
→ редирект на edit |
| GET | /admin/products/{id}/edit |
Admin\ProductController@edit |
Форма редактирования |
| PUT/PATCH | /admin/products/{id} |
Admin\ProductController@update |
Обновить товар |
| DELETE | /admin/products/{id} |
Admin\ProductController@destroy |
Удалить товар |
| GET | /up |
— | Health check (Laravel 12) |
| Поле | Тип | Описание |
|---|---|---|
id |
bigint, PK | |
name |
string | Название |
description |
text, nullable | Описание |
price |
integer | Цена в рублях |
old_price |
integer, nullable | Старая цена (для зачёркивания) |
image |
string, nullable | Путь к картинке |
badge |
string, nullable | Бейдж: «Хит», «Новинка» и т.п. |
size |
string, nullable | Размер букета |
life |
string, nullable | Стойкость |
created_at, updated_at |
timestamp |
Аксессор Product::getImageUrlAttribute():
Аксессор Product::getImageUrlAttribute() возвращает asset($this->image). Все фото лежат в public/: public/products/ — новые загрузки через админку public/images/ — старые картинки из сидера
Таблицы orders нет — это осознанное решение. Заказы не сохраняются в БД, а сразу уходят в Telegram. Если нужна история — см. раздел «Планы по развитию».
Реализована на фронте в home.blade.php. Курсы заданы в JS:
const EXCHANGE_RATES = {
RUB: 1,
USD: 1 / 84.20,
EUR: 1 / 98.50,
BYN: 1 / 27.86,
KZT: 1 / 0.16
};Базовая валюта — RUB. При смене страны все цены на странице пересчитываются. Выбор сохраняется в localStorage под ключом flowershop_country.
Чтобы обновлять курсы автоматически, замените
EXCHANGE_RATESна запрос к API ЦБ или внешнему сервису.
- Ключ:
flowershop_cart - Формат:
[{ name, price, img, qty }, ...] - Промокод хранится в переменной
discount(не персистится). - Бейдж корзины пульсирует при добавлении, иконка «подпрыгивает».
- При добавлении — анимация «летящего букета» из кнопки в корзину.
Собственная реализация (IIFE в конце <script>):
- Классы позиций:
pos-center,pos-left,pos-right,pos-hidden-left,pos-hidden-right. - Активный индекс зацикливается:
(activeIndex + direction + total) % total. - Клик по центру → скролл к каталогу + открытие модалки товара.
- Клик по боковому → перелистывание.
- Свайпы через Pointer Events.
Если товаров с badge нет — берутся все товары. Если товар один — кнопки навигации скрываются.
Контейнер .scroll-container перехватывает wheel и скроллит по секциям (по одной за раз) с плавной анимацией через easeInOutCubic.
У карточек каталога — псевдо-скелетон через ::before с анимацией skeleton. Убирается после load картинки.
При window.matchMedia('(min-width: 1024px)') включается кастомный курсор: точка следует за мышью, кольцо догоняет с задержкой через requestAnimationFrame. На интерактивных элементах кольцо растёт.
На главной странице 5 раз подряд кликните на логотип (FlowerShop в левом верхнем углу) в течение 1.5 секунд. Откроется модалка с iframe на /admin/products.
- Список товаров с пагинацией (15 на страницу).
- Создание / редактирование / удаление.
- Загрузка картинок: PNG/JPG до 4 МБ, сохраняются в
public/products/. - Валидация через
ProductController::validated().
Поля: name*, description, price*, old_price, badge, size, life, image_file.
⚠️ Админка изолирована по URL. Для продакшна рекомендуется закрыть её middlewareauth— см. раздел «Планы по развитию».
composer install --no-dev --optimize-autoloader
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan migrate --force
chmod -R 775 storage bootstrap/cache
chown -R www-data:www-data storage bootstrap/cacheAPP_DEBUG=false— иначе утечка стектрейсов.TELEGRAM_BOT_TOKENиTELEGRAM_CHAT_IDзаданы.php artisan config:clearпосле изменения.env.- Не коммитить
.envиdatabase/database.sqlite. - Настроить HTTPS.
- Закрыть
/admin/*авторизацией.
- Авторизация в админке (
authmiddleware + Laravel Breeze) - Таблица
ordersдля истории заказов - Интеграция с API ЦБ РФ для актуальных курсов валют
- Таблица промокодов с гибкими правилами
- Онлайн-оплата (ЮKassa / Stripe)
- Личный кабинет пользователя
- Telegram-уведомления клиенту о статусе заказа
- Покрытие тестами бизнес-логики (Pest/PHPUnit)
- CI/CD через GitHub Actions
- Замена
home.blade.phpна набор partials
php artisan testИли через composer:
composer testMIT
- Email: ditlate0@gmail.com
- Instagram: @prod_23b
- GitHub: @23b




