- Implemented StopsMap component to display bus stops on a map using MapLibre GL. - Integrated walking directions feature in WalkingDirections component to calculate routes from user location to selected bus stop. - Added geo utility functions for coordinate validation and distance calculation. - Created route grouping and stop filtering logic in routeGroups module. - Developed custom hooks for managing stop routes with React Query. - Added tests for routing and stop functionalities to ensure reliability.
RideBus PWA
Возможности
- Четыре раздела: «Главная», «Остановки», «Избранное», «Настройки». На главной компактный выбор города и остановки; окно выбора содержит каталог. Общий список и поиск рядом переключаются, чтобы не занимать экран двумя списками. На телефоне выбор остановки закрывает окно и плавно переводит к отправлениям, в том числе для выбранной даты.
- Знакомство при первом посещении: пять подсказок с подсветкой настоящих функций главной страницы. Можно пропустить, пройти с клавиатуры или повторить из настроек. Завершение/пропуск сохраняются в этом браузере.
- Города из RideBus API, поиск остановок без повторяющихся названий и выбор направления чипами в карточке отправлений.
- «Остановки» — самостоятельная карта и справочник, без второй карточки отправлений. На карте отдельные точки посадки для разных направлений, сгруппированные при отдалении; список показывает каждое название один раз. Поиск синхронизирует список и точки. Карточка остановки содержит направления, действующие маршруты, добавление в избранное и построение пешего пути внутри приложения. «Открыть расписание» переводит на Главную с выбранной точкой посадки, сохраняет дату просмотра и прокручивает к отправлениям. В режиме списка на десктопе карточка занимает всю правую область. На телефоне карточка открывается диалогом по нажатию, в обоих режимах. Диалог закрывается кнопкой, нажатием вне окна или Escape и возвращает фокус; список не удлиняется из-за карточки. При входе в раздел мобильный диалог не открывается автоматически из-за ранее выбранной остановки. Кнопка геолокации в поиске показывает остановки в радиусе 1 км; расстояния рассчитываются по прямой, координаты пользователя остаются в памяти. Без сети доступен список и ранее просмотренные карточки остановок. MapLibre загружается только при открытии карты; библиотека и её worker не входят в предзагрузку офлайн-оболочки. Светлая/тёмная подложки OpenFreeMap следуют теме приложения, требуют подключения и не нуждаются в отдельном ключе. Атрибуция источников сохранена.
- Фильтр по маршруту синхронизирует список и карту. Одинаковые номера
объединяются, направления выбираются отдельно; при выборе одного направления
остановки идут в порядке движения. Названия не повторяются, точки посадки
и их идентификаторы сохраняются. Фильтр остаётся в URL (
line,direction) после перезагрузки; просмотренные направления доступны без сети. Поиск по названию и рядом работает вместе с фильтром. При переходе к расписанию выбранный маршрут сохраняется. Частично недоступные направления обозначаются, не заменяются полным списком города. Смена города сбрасывает фильтр. - «Как добраться пешком» запрашивает местоположение по нажатию и использует
RideBus OSRM
/routing/foot/route/v1/foot/…с геометрией GeoJSON. Путь, расстояние и приблизительное время показываются в отдельном диалоге. При отказе в геолокации или ошибке доступны повтор и внешние карты. В демонстрации построение пути отключено, чтобы примеры не выдавать за реальный маршрут. Начальная точка и геометрия пешего пути остаются в памяти, не сохраняются в IndexedDB/localStorage и серверном кеше приложения. - Ближайшие отправления и полное расписание остановки на выбранную дату, переключение направлений и фильтр по маршруту. В компактной карточке название и закладка находятся рядом, кнопка «+» сохраняет поездку, а на телефоне фильтры маршрутов прокручиваются горизонтально.
- Полное расписание в сетке по часам без скрытых рейсов; переход к ближайшему времени и открытие конкретного рейса нажатием на отправление.
- «Мои поездки»: город, остановка, направление и маршрут в одной карточке. Можно сохранить «Домой», «На работу» или своё название, переименовать и удалить. На мобильном несколько поездок листаются каруселью; на десктопе карусель включается от трёх поездок. Доступны свайпы, кнопки и управление с клавиатуры. Частично видимые карточки плавно затухают у края. На первом и последнем положении затухание остаётся только со стороны скрытых поездок. «Открыть поездку» выбирает нужный город/направление/маршрут и прокручивает страницу к карточке отправлений. При уменьшении движения переход происходит без анимации.
- Экран рейса с последовательностью остановок и временем на каждой из них, с учётом укороченных/обратных рейсов и перехода через полночь.
- Кнопка «Поделиться отправлением» у ближайших отправлений и на экране рейса. Ссылка сохраняет город, остановку, маршрут, дату и идентификатор конкретного рейса. Используется системное меню отправки, затем копирование ссылки, если меню недоступно. При недоступном буфере обмена можно скопировать ссылку вручную. Отмена системного меню не копирует ссылку и не показывает ошибку.
- Активные сообщения об отменах и изменениях рядом с затронутыми маршрутами. Полный текст сохраняется, сообщения нескольких маршрутов не дублируются, а закончившиеся сообщения исчезают даже без сети.
- Кнопка «Остановки рядом»: запрос местоположения только после нажатия, остановки в радиусе 1 км с расстояниями и группировкой одинаковых названий. Отказ в разрешении не мешает обычному поиску. Координаты и результаты поиска рядом не сохраняются в IndexedDB/localStorage и серверном кеше приложения. Обычный список и поиск рядом используют общие строки, состояния и скроллбары.
- Ожидание в часах и минутах: «через 2 ч 1 мин».
- Избранные остановки с тремя ближайшими отправлениями прямо в карточках, сохранением в браузере и переходом к расписанию.
- Короткие микроанимации карточек, кнопок, фильтров, закладки и счётчика избранного. Эффекты отключаются при системной настройке уменьшения движения.
- Просмотр ранее загруженных данных без интернета. Кеш разделён по городам, остановкам и режиму работы; время сохранения видно в интерфейсе. Отключение сети показывает один короткий общий статус. Свежие снимки не получают предупреждение об устаревании только из-за потери соединения. Предупреждение появляется по времени последнего обновления: от 30 минут для ближайших отправлений, 24 часов для расписания на дату/рейса и 15 минут для сообщений о движении. Это пороги напоминания об обновлении, а не гарантия неизменности расписания. Ошибки обновления онлайн и частичные результаты продолжают обозначаться отдельно.
- Установка PWA, локальные шрифты, иконки, офлайн-оболочка и предложение обновления.
- Раздел «Настройки» в основной навигации, нижней панели на телефоне и футере. Светлая, тёмная и системная темы применяются ко всем экранам. По умолчанию оформление следует теме устройства и реагирует на её изменения. Выбор сохраняется в этом браузере, синхронизируется между его вкладками и доступен без сети. Сохранённая тема применяется ещё до запуска React. При заблокированном хранилище переключение работает до закрытия вкладки; приложение сообщает, что сохранить выбор не удалось.
- Отдельный демонстрационный режим
/?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/StopsExplorer.tsx,StopsMap.tsx,src/_stops.scss— карта, каталог и карточка остановки;src/lib/geo.ts— расстояния и пешие ссылки.src/lib/useStopRoutes.ts,routeGroups.ts— номера/направления маршрутов и их остановки.src/components/WalkingDirections.tsx,server/walking.mjs— пеший путь и проверка геометрии OSRM.src/components/Settings.tsx,src/lib/theme.ts— выбор темы и её сохранение.src/components/FeatureDiscovery.tsx,src/_discovery.scss— подсказки первого входа.src/_navigation.scss,src/_departures.scss— разделы, компактный выбор и отправления.src/lib/freshness.ts— возраст данных без привязки к наличию сети.src/_tokens.scss— цвета светлой и тёмной тем;src/_settings.scss— настройки.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. Карточка остановки получает направления/маршруты через
/api/data?resource=stop&cityId=…&stopId=… → GET /api/v1/stops/:id;
координаты точек посадки и центра города нормализуются из исходных ответов.
Отсутствующие или неверные координаты не заменяются нулями: такие остановки
доступны в списке. Для фильтра используются resource=routes → GET /api/v1/routes
и resource=routeStops&routeId=… → GET /api/v1/routes/:id.
resource=walk&stopId=…&lat=…&lon=… берёт координаты назначения из остановки
и запрашивает /routing/foot/route/v1/foot/lon,lat;lon,lat без кеширования начала/пути.
Формат проверен по документации OSRM
и живому RideBus. Для полного расписания используются
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-свайпы,
переход к отправлениям между городами, уменьшение движения,
отправку/копирование/отмену и ручной резервный способ поделиться рейсом,
одинаковое оформление списков остановок,
затухание края карусели и отсутствие кнопок демо в релизном интерфейсе,
выбор/сохранение темы, системные изменения, синхронизацию вкладок,
первое отображение до запуска JavaScript-приложения и настройки без сети,
четыре раздела, компактность выбора на ширинах 320–1440 px,
выбор точки на карте, отдельные направления карточки остановки,
пешую ссылку, сохранение выбранной стороны в избранном,
полную ширину карточки в списке, мобильный диалог, номера/направления и порядок
остановок маршрута, сохранение фильтра без сети, частичный отказ направления,
пешую геометрию/метрики, отказ в разрешении и отсутствие сохранённого начала/пути,
список/просмотренную карточку остановки без сети и темы карты,
выбор остановки с прокруткой и уменьшением движения, переход на нужную дату,
один статус при отключении сети и предупреждение после реального старения данных,
первый вход с подсветкой, повторное обучение и подсказки на экране 320 × 568,
разделение городов, честную ошибку API и настоящий офлайн-перезапуск
через service worker, включая расписание на конкретную дату и рейс.
Это эмуляция размера экрана,
а не проверка на физическом iPhone/Safari. Тесты используют демонстрационные
данные и контролируемые ответы; координаты задаются в тестовом браузере.
Скриншоты сохраняются в test-results/.
npm run icons
Эта команда пересоздаёт PNG-иконки из исходного логотипа. Иконки и шрифты поставляются вместе с приложением и не требуют сети после первого открытия.