20 KiB
RideBus PWA
Возможности
- Города из RideBus API, поиск остановок без повторяющихся названий и выбор направления чипами в карточке отправлений.
- Ближайшие отправления и полное расписание остановки на выбранную дату, переключение направлений и фильтр по маршруту.
- Полное расписание в сетке по часам без скрытых рейсов; переход к ближайшему времени и открытие конкретного рейса нажатием на отправление.
- «Мои поездки»: город, остановка, направление и маршрут в одной карточке. Можно сохранить «Домой», «На работу» или своё название, переименовать и удалить. На мобильном несколько поездок листаются каруселью; на десктопе карусель включается от трёх поездок. Доступны свайпы, кнопки и управление с клавиатуры. Частично видимые карточки плавно затухают у края. На первом и последнем положении затухание остаётся только со стороны скрытых поездок. «Открыть поездку» выбирает нужный город/направление/маршрут и прокручивает страницу к карточке отправлений. При уменьшении движения переход происходит без анимации.
- Экран рейса с последовательностью остановок и временем на каждой из них, с учётом укороченных/обратных рейсов и перехода через полночь.
- Кнопка «Поделиться отправлением» у ближайших отправлений и на экране рейса. Ссылка сохраняет город, остановку, маршрут, дату и идентификатор конкретного рейса. Используется системное меню отправки, затем копирование ссылки, если меню недоступно. При недоступном буфере обмена можно скопировать ссылку вручную. Отмена системного меню не копирует ссылку и не показывает ошибку.
- Активные сообщения об отменах и изменениях рядом с затронутыми маршрутами. Полный текст сохраняется, сообщения нескольких маршрутов не дублируются, а закончившиеся сообщения исчезают даже без сети.
- Кнопка «Остановки рядом»: запрос местоположения только после нажатия, остановки в радиусе 1 км с расстояниями и группировкой одинаковых названий. Отказ в разрешении не мешает обычному поиску. Координаты и результаты поиска рядом не сохраняются в IndexedDB/localStorage и серверном кеше приложения. Обычный список и поиск рядом используют общие строки, состояния и скроллбары.
- Ожидание в часах и минутах: «через 2 ч 1 мин».
- Избранные остановки с тремя ближайшими отправлениями прямо в карточках, сохранением в браузере и переходом к расписанию.
- Короткие микроанимации карточек, кнопок, фильтров, закладки и счётчика избранного. Эффекты отключаются при системной настройке уменьшения движения.
- Просмотр ранее загруженных данных без интернета. Кеш разделён по городам, остановкам и режиму работы; время сохранения видно в интерфейсе.
- Установка PWA, локальные шрифты, иконки, офлайн-оболочка и предложение обновления.
- Отдельный демонстрационный режим
/?demo=1с явно помеченными примерами. Кнопка «Посмотреть пример» есть только приnpm run dev; в релизной сборке и Docker её нет, включая экран ошибки загрузки. Прямая ссылка на демонстрацию сохранена для ручных проверок и тестов. Она не подменяет реальные данные при ошибке API.
В режиме «Ближайшие» API ETA возвращает ограниченное количество отправлений для каждого маршрута. Историческая задержка исключается: показаны времена по расписанию, а не обещание реального прибытия. В режиме «На дату» сервер получает полное расписание маршрутов, учитывает праздничный тип дня, ограничения рейсов по дням недели, начальную/конечную остановку укороченных рейсов и время пути. В выбранную календарную дату включаются и рейсы предыдущего дня, достигающие остановки после полуночи. Частичные ошибки обозначаются в интерфейсе.
Просмотренное полное расписание сохраняется отдельно для каждой даты и доступно офлайн, включая прошедшие рейсы. Ближайшие отправления и карточки избранного удаляют прошедшие времена из показа. Это не офлайн-база всех городов и дат: сначала нужно открыть нужную остановку/дату в сети. Браузер может очищать локальное хранилище. Просмотренные рейсы и сообщения также доступны из сохранённых данных с предупреждением об устаревании. Поиск рядом требует сети и разрешения браузера на местоположение; в демонстрации расстояния явно помечены как примеры. Карты с движением транспорта, пушей и аккаунтов пока нет.
Расписание рейса показывает плановые времена, а не положение автобуса. Если несколько вариантов приходят одновременно, ссылка из полного расписания содержит идентификатор конкретного рейса. При изменившемся расписании приложение предлагает выбрать отправление заново. Сообщения об отменах информируют пользователя, но не удаляют времена из исходного расписания. На будущую дату рядом могут отображаться текущие сообщения; в каждом виден полный период действия.
Запуск
Требуется Node.js 22.12+ (проверено с Node.js 24).
npm install
cp .env.example .env
В .env задайте RIDEBUS_API_KEY. При создании этого проекта локальный файл
уже настроен по конфигурации лендинга. .env исключён из Git; ключ используется
только сервером и не попадает в JavaScript или запросы браузера.
npm run dev
Приложение: http://localhost:5173. Один Node-сервер обслуживает Vite и API, поэтому отдельный процесс прокси не нужен. В development service worker отключён.
Для проверки установки и офлайн-работы используйте production-сборку:
npm run build
npm start
Порт задаётся переменной PORT, адрес — HOST (по умолчанию 127.0.0.1).
При запуске в контейнере или на сервере используйте HOST=0.0.0.0 и HTTPS
на внешнем reverse proxy. localhost допускает service worker без HTTPS.
Раздачи одного dist/ недостаточно: нужен серверный обработчик /api/data.
Развёртывание через Docker Compose
Требуются Docker Engine с Docker Compose v2 и HTTPS-прокси на сервере.
В docker-compose.yml один сервис web: он собирает фронтенд и запускает
Node-сервер, который обслуживает приложение и запросы к RideBus.
Образ собирается в несколько этапов; в конечном контейнере только dist/
и сервер без зависимостей сборки. Схема описана в
документации Docker.
На сервере скопируйте проект и создайте .env:
cp .env.example .env
Задайте RIDEBUS_API_KEY; при необходимости измените RIDEBUS_API_BASE_URL.
Ключ передаётся только процессу сервера при запуске контейнера; .env исключён
из контекста сборки. Compose остановится с понятной ошибкой, если ключ пустой.
PWA_PORT=8080 и PWA_BIND_ADDRESS=127.0.0.1 задают адрес на хосте.
Внутри контейнера HOST=0.0.0.0 и PORT=5173 фиксированы отдельно от настроек
локального запуска.
docker compose config --quiet
docker compose up -d --build --wait
docker compose ps
curl --fail http://127.0.0.1:8080/healthz
/healthz возвращает {"status":"ok"} при работающем HTTP-сервере.
Проверка не обращается к RideBus: ошибки источника расписаний отображаются
в самом приложении. Включены перезапуск процесса после сбоя, healthcheck
и ограничение размера логов. Контейнер работает от пользователя node
с файловой системой только для чтения и отдельным временным /tmp.
Параметры сервисов описаны в
справочнике Compose.
Направьте весь трафик вашего HTTPS-домена на http://127.0.0.1:8080, включая
/api/, /healthz, /sw.js и /manifest.webmanifest. Если Nginx уже работает
на хосте и TLS-сертификат настроен, добавьте в его HTTPS-блок server:
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
Если изменили PWA_PORT, укажите тот же порт в proxy_pass.
Не добавляйте кеширование для /api/ и /sw.js. HTTPS нужен для установки
PWA, service worker, геолокации и системной отправки ссылки;
ограничения описаны в MDN.
Если ваш прокси работает в другом контейнере, подключите его и web к общей
Docker-сети и используйте upstream http://web:5173.
После обновления исходников повторите docker compose up -d --build --wait.
Для просмотра логов и остановки:
docker compose logs --tail=100 -f web
docker compose down
Серверная база данных и постоянный volume приложению не нужны: избранное, поездки и просмотренные расписания хранятся в браузере, а серверный кеш создаётся в памяти заново после перезапуска.
Стек и файлы
- React, TypeScript, Vite, React Router.
- Иконки
@vkontakte/icons, счётчик избранного отдельным бейджем в мобильной навигации. - Sass и цвета в
src/_tokens.scss; адаптивные стили вsrc/styles.scss. src/_motion.scss— длительности, отклик элементов и анимации появления; длинные списки ограничивают количество одновременно анимируемых строк.src/_features.scss— поездки, почасовая сетка, ближайшие остановки и экран рейса.- TanStack Query для загрузки и обновления, Dexie/IndexedDB для снимков и избранного.
vite-plugin-pwa/ Workbox для manifest, service worker и офлайн-оболочки.server/ridebus.mjs— адаптер RideBus, нормализация, таймауты, кеш.server/index.mjs— Node HTTP-сервер и фиксированный список методов чтения.Dockerfile,docker-compose.yml,.env.example— релизная сборка и запуск на сервере.src/App.tsx— поиск, выбор города/остановки и избранное.src/components/Departures.tsx— отправления, фильтр, сохранённые данные.src/components/FavoriteCard.tsx— ближайшие рейсы в избранном.src/components/About.tsx— страница автора проекта.src/components/QuickTrips.tsx— сохранение и карточки поездок.src/components/TripCarousel.tsx— адаптивная карусель с кнопками, свайпами и клавиатурой.src/components/ShareDeparture.tsx— отправка/копирование ссылки на конкретное отправление.src/components/StopList.tsx— общие строки и контейнер для обоих списков остановок.src/components/HourlyTimetable.tsx,RoutePage.tsx— почасовая сетка и конкретный рейс.src/components/ServiceAlerts.tsx,NearbyStops.tsx— изменения и поиск рядом.src/lib/storage.ts— версия 2 базы; существующие снимки/избранное сохраняются при обновлении.server/timetable.mjs— расчёт времени остановки по полному расписанию рейсов.server/features.mjs— нормализация сообщений и ближайших остановок.
Прокси использует GET /api/v1/cities, GET /api/v1/stops и
GET /api/v1/eta/stop/:id. Для полного расписания используются
GET /api/v1/stops/:id, GET /api/v1/routes/:id/stops и
GET /api/v1/routes/:id/schedule/today?date=YYYY-MM-DD.
Экран рейса дополнительно использует GET /api/v1/routes/:id;
сообщения — GET /api/v1/alerts/active, поиск рядом —
GET /api/v1/stops/nearby?lat=…&lon=…&radius=1000.
Город передаётся в X-City-Id, ключ — в X-API-Key.
Браузер получает нормализованные данные с того же origin.
Параметры города/остановки доступны в URL (/?city=1&stop=…), в том числе
при переходе из избранного. Полное расписание использует
/?city=1&stop=…&view=day&date=2026-10-07.
Сервер кеширует полное расписание и его исходные данные на 5 минут,
разделяя города и даты; ограничивает параллельные запросы к RideBus шестью.
Проверки
npm test
npm run typecheck
npm run build
PLAYWRIGHT_BROWSERS_PATH=.playwright npx playwright install chromium
npm run test:e2e
Playwright проверяет desktop и экран iPhone 13 в Chromium: поиск, фильтры,
сохранение избранного, ожидание в часах/минутах, выбор даты и направления,
страницу автора, поездки, миграцию хранилища, диалоги с клавиатурой,
почасовую сетку и экран рейса, разрешённую/отклонённую геолокацию,
отсутствие сохранённых координат, фильтрацию/истечение сообщений,
карусель от двух поездок на мобильном/трёх на десктопе, touch-свайпы,
переход к отправлениям между городами, уменьшение движения,
отправку/копирование/отмену и ручной резервный способ поделиться рейсом,
одинаковое оформление списков остановок,
затухание края карусели и отсутствие кнопок демо в релизном интерфейсе,
разделение городов, честную ошибку API и настоящий офлайн-перезапуск
через service worker, включая расписание на конкретную дату и рейс.
Это эмуляция размера экрана,
а не проверка на физическом iPhone/Safari. Тесты используют демонстрационные
данные и контролируемые ответы; координаты задаются в тестовом браузере.
Скриншоты сохраняются в test-results/.
npm run icons
Эта команда пересоздаёт PNG-иконки из исходного логотипа. Иконки и шрифты поставляются вместе с приложением и не требуют сети после первого открытия.