Screens
Map Screen (app/(tabs)/index.tsx)
The main screen. Shows a full-screen Google Map with a two-tier marker system, a floating search bar with EM logo, zoom in/out controls, a "my location" button, and a bottom sheet with both a place list and inline place detail.
Two-tier marker system:
- Primary markers: Custom SVG pin icons (
MarkerIconcomponent) with place name labels below. Overlap-deduplicated at ~50px spacing. - Secondary markers: Small teal dots (14px circles with white border) for lower-priority places. Overlap-deduplicated at ~10px spacing, up to 200 dots.
- Selected markers: When a marker is selected (primary or secondary), it turns red (
#B91C1C) with zIndex 999. Selected secondary dots are promoted from small dots to full red pin markers (MarkerIcon). - Both tiers use a staggered opacity fade-in animation (
react-native-reanimatedwithDelay+withTiming). Primary markers fade in first (30ms stagger), secondary dots follow (15ms stagger).
Key interactions:
- Pan/zoom the map → triggers
onRegionChangeComplete→ updatesuseMapStore.region→usePlacesre-fetches - Tap a marker (primary or secondary) → selects place, shows inline place detail in the bottom sheet at 45%
- Tap a place card in the list → same as tapping a marker (opens inline drawer)
- Tap "← Back to results" → clears selection, returns to place list
- Tap the search bar → opens the search modal (
/search-modal) - Tap "my location" → requests GPS permission, animates map to current location
- Tap +/− zoom buttons → zooms in/out with animation
- When bottom sheet is fully expanded (85%), a floating "Back to map" pill appears center-bottom
Data flow: useMapStore.region → usePlaces(0, 50) → tRPC place.searchPlacesByBounds → parsePlace() → tieredMarkers() → render primary pins + secondary dots + list
Overlap removal: Uses priority-based two-tier deduplication (see Map Implementation).
Inline Place Detail (Bottom Sheet)
When a marker or list card is tapped, the bottom sheet switches from the place list to an inline place detail view. Shows place name, address, badges, star rating, accessibility attributes (step-free, WC, seating, parking) with thumbs-up/down icons, and a "Write a Review" CTA. A "← Back to results" link at the top clears the selection and returns to the list.
Data flow: The selectPlace() callback sets selectedPlace state and selectedPlaceId in the store, then snaps the bottom sheet to 45%. No navigation or additional API call needed.
Place Detail Screen (app/place/[id].tsx)
Fullscreen detail view for a single place (used for deep links). Shows the same information as the inline detail. In normal usage, the inline bottom sheet detail is preferred.
Data flow: Receives all place data via route params (no additional API call needed). Reads useAuthStore.isAuthenticated to gate review submission.
Profile (app/(tabs)/profile.tsx)
Shows user info (avatar, name, email, role badge) if authenticated, or a sign-in prompt if not. Includes a logout button that clears SecureStore and resets useAuthStore.
Search Modal (app/search-modal.tsx)
Two-tab modal: Search (Google Places text search via place.searchGooglePlaces) and Filters (category chips from category.getAll + ACS attribute toggles). Selecting a search result pans the map; applying filters updates useMapStore.categories and useMapStore.acsAttrs.
Login (app/login.tsx)
Google OAuth sign-in screen with the EnAccess Maps logo. Uses expo-auth-session to get an OAuth token, exchanges it with the backend, stores the session in SecureStore, and updates useAuthStore.
Search List (app/(tabs)/search.tsx)
Hidden tab (no tab bar entry via href: null). Shows a flat list of places in the current map bounds, reusing usePlaces and PlaceCard. Used for list-based browsing.