Мобильный гид по достопримечательностям городов: каталог мест с фильтрами и поиском, интерактивная карта с кастомными маркерами, построение маршрутов через Google Directions, избранное и офлайн-режим.
Больше скриншотов
| Города и места — выбор города, режим «Рядом», экран города | Карта |
![]() |
![]() |
| Авторизация | Тёмная тема |
![]() |
![]() |
Каталог
- Сетка достопримечательностей с изображениями и скелетон-загрузкой
- Поиск по названию и фильтрация по категориям
- Три режима просмотра: «Все», «Избранные», «Рядом»
- Режим «Рядом» считает расстояние от текущей геопозиции по формуле гаверсинуса, оставляет места в радиусе 10 км и сортирует по удалённости
- Экраны детальной информации о достопримечательности и о городе: карусель изображений, описание, история, координаты
Карта
- Google Maps с кастомными SVG-маркерами, свой значок для каждой категории (памятник, статуя, мемориал, улица, архитектура, музей, храм)
- Bottom sheet с карточкой места при тапе на маркер
- Построение маршрута до выбранного места — авто и пешком, с расстоянием и временем в пути, камера автоматически подгоняется под полигон маршрута
- Переключение типа карты (схема / спутник / гибрид), слой пробок, точки интереса
- Компас со сбросом на север, кнопка «моё местоположение», зум кнопками с ускорением по долгому нажатию
- Отдельная тёмная стилизация карты, синхронная с темой системы
Аккаунт и данные
- Регистрация, вход, восстановление пароля через Firebase Auth
- Избранное, привязанное к аккаунту, с синхронизацией в Firestore
- Редактирование профиля, выход, очистка локального кэша
- Выбор города при первом запуске, смена города в любой момент
Офлайн-режим
- Все данные города (город, достопримечательности, категории) кэшируются в AsyncStorage
- При старте приложение сначала поднимает кэш и рисует интерфейс, затем обновляет данные из Firestore
NetInfoотслеживает соединение: без сети приложение работает на кэше и сообщает об этом- Сессия пользователя сохраняется между запусками через
getReactNativePersistence
src/
├── components/
│ ├── main.js # Инициализация: кэш → Firestore, авторизация, таб-навигация
│ ├── Home/ # Стек: список, город, достопримечательность, профиль
│ │ └── Profile/ # Профиль и экран авторизации
│ ├── Map/ # Карта, маркеры, контролы, bottom sheets
│ │ └── hooks/ # useLocation, useMapCamera, darkMapStyle
│ └── Layouts/ # Переиспользуемые элементы: карусель, чипы, скелетоны, кнопки
├── features/
│ ├── store/ # 7 слайсов Redux Toolkit
│ └── AsyncStorage.js # Обёртка над локальным хранилищем
├── firebase/ # Инициализация SDK и сервисный слой запросов
├── utils/ # Расчёт расстояний и хелперы выборки
└── assets/icons/ # SVG-иконки категорийИмпорты используют алиасы из jsconfig.json (@components/*, @features/*, @utils/*, @firebase/*), которые babel-preset-expo резолвит без дополнительных плагинов.
Стек по слоям
| Слой | Технологии |
|---|---|
| Платформа | React Native 0.74, Expo SDK 51, expo-dev-client |
| Состояние | Redux Toolkit, react-redux |
| UI | React Native Paper (MD3), кастомная палитра, react-native-reanimated-carousel, @gorhom/bottom-sheet |
| Навигация | React Navigation (Stack) + Paper BottomNavigation |
| Карты | react-native-maps, react-native-maps-directions, expo-location |
| Бэкенд | Firebase Auth, Cloud Firestore |
| Хранилище | AsyncStorage, @react-native-community/netinfo |
| Графика | react-native-svg, expo-linear-gradient |
Как устроен запуск приложения
src/components/main.js — единственная точка, где приложение решает, что показывать:
- Читает
current-city-idиз AsyncStorage. Если города нет — открывает экран выбора города. - Если город есть,
loadCityDataсначала поднимает кэш (city-{id},attractions-{id},categories) и сразу диспатчит его в стор — интерфейс отрисовывается без ожидания сети. - Затем, если есть соединение, тремя параллельными запросами тянет актуальные данные из Firestore и перезаписывает кэш.
- Сплэш-скрин скрывается только после того, как решение принято, — без мигания пустым экраном.
authStateListenerнезависимо подписывается на состояние Firebase Auth и подтягивает документ пользователя изusers/{uid}.
Переходы между вкладками через стор
Вкладки «Главная» и «Карта» — независимые деревья навигации, поэтому переход «показать это место на карте» нельзя выразить обычным navigation.navigate. Вместо этого используются слайсы map и attraction с флагом shouldAnimate:
- Экран достопримечательности диспатчит
setMapCoordinates({ latitude, longitude, attraction })→ вкладка «Карта» видит флаг, анимирует камеру, открывает bottom sheet места и сбрасывает флаг. - Карточка на карте диспатчит
setAttraction(...)→ вкладка «Главная» перехватывает флаг и открывает экран достопримечательности.
Флаг сбрасывается сразу после обработки, чтобы повторные монтирования вкладки не запускали анимацию заново.
Модель данных Firestore
cities/{cityId}
name, description, history, founded, area, population
images: string[]
location: { latitude, longitude }
attractions/{attractionId}
name, description, history
city_id: string # связь с городом
category: string # id категории, он же ключ иконки маркера
images: string[]
location: { latitude, longitude }
categories/{categoryId}
name # id: statue | monument | memorial | street | architecture | museum | temple
users/{uid}
firstname, lastname, email
created_at: Timestamp
favorites: [{ attraction_id: string, added_at: Timestamp }]Достопримечательности запрашиваются по where('city_id', '==', cityId) — приложение никогда не тянет каталог целиком.
Проект использует нативные модули (react-native-maps с конфигом Google Maps), поэтому Expo Go не подходит — приложение не запустится ни по QR, ни любым другим способом через него. Нужен development build: он ставится на устройство как обычное приложение и дальше работает с Fast Refresh, как обычная разработка.
git clone https://github.com/choompuy/reactnative-attractionsguide.git
cd reactnative-attractionsguide
npm installСоздайте файл .env в корне:
# Firebase (Project settings → General → Your apps → Web app)
EXPO_PUBLIC_API_KEY=
EXPO_PUBLIC_AUTH_DOMAIN=
EXPO_PUBLIC_PROJECT_ID=
EXPO_PUBLIC_STORAGE_BUCKET=
EXPO_PUBLIC_MESSAGING_SENDER_ID=
EXPO_PUBLIC_APP_ID=
# Google Cloud → APIs & Services → Credentials
EXPO_PUBLIC_GOOGLE_MAPS_API_KEY=Для ключа Google нужно включить Maps SDK for Android и Directions API.
Локально на своём телефоне (нужен Android SDK — ставится вместе с Android Studio):
- На телефоне: Настройки → О телефоне → 7 раз тапнуть по номеру сборки → Для разработчиков → включить «Отладка по USB».
- Подключить телефон кабелем, подтвердить запрос на экране, проверить
adb devices— телефон должен быть в списке со статусомdevice. - Собрать и установить:
npx expo run:androidПервая сборка займёт несколько минут — Gradle тянет зависимости и собирает нативный код. Дальше, пока телефон подключён и Metro запущен, изменения в JS подхватываются мгновенно; пересборка нужна только при изменении нативных зависимостей или app.json.
Эмулятор Android Studio подходит так же, отличие практическое: реальные GPS-координаты для проверки режима «Рядом» и построения маршрутов удобнее брать с телефона, а образ эмулятора для карт должен быть с Google Play Services (обычный образ без них карту не отрисует).
Без локальной среды — сборка в облаке через EAS:
eas build -p android --profile previewПодготовка Firebase
- Создайте проект в Firebase Console.
- Включите Authentication → Sign-in method → Email/Password.
- Создайте базу Cloud Firestore и коллекции
cities,attractions,categoriesпо схеме выше. - Опубликуйте правила безопасности: справочные коллекции доступны на чтение всем, документ пользователя — только владельцу.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /cities/{document=**} { allow read: if true; allow write: if false; }
match /attractions/{document=**} { allow read: if true; allow write: if false; }
match /categories/{document=**} { allow read: if true; allow write: if false; }
match /users/{userId} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}
}
}- Отдельный экран списка избранного с сортировкой по дате добавления
- Кэширование изображений на устройстве для полноценного офлайн-просмотра
- Инкрементальная синхронизация данных города вместо полной перезагрузки коллекций
- Промежуточные точки в маршрутах и режимы «велосипед» / «общественный транспорт»
- Перевод кодовой базы на TypeScript




