Departs is a mobile-first web app that shows nearby public transit stops and real-time departure times. It uses the browser's geolocation to find stops within 250m, displays them on a map, and shows upcoming departures when a stop is selected.
| Layer | Technology |
|---|---|
| Framework | Next.js 16 (App Router, Turbopack) |
| Language | TypeScript (strict) |
| Styling | Tailwind CSS 4 + CSS custom properties |
| Map | Mapbox GL JS via react-map-gl |
| Transit data | HERE Public Transit API v8 |
| Walking directions | Mapbox Directions API |
| Testing | Vitest, React Testing Library, MSW, Playwright |
| Deployment | Vercel |
| iOS App | SwiftUI (iOS 17+), MapKit, CoreLocation, URLSession |
app/
├── api/
│ ├── departures/route.ts # Proxy → HERE /v8/boards
│ ├── directions/route.ts # Proxy → Mapbox Directions
│ └── stops/nearby/route.ts # Proxy → HERE /v8/stations
├── components/
│ ├── BottomSheet.tsx # Fixed bottom sheet (flush, no drag handle)
│ ├── DepartureDetail.tsx # Screen 2: departure list + header
│ ├── DepartureRow.tsx # Individual departure (12h time + clock icon)
│ ├── ErrorState.tsx # Error/permission/offline states
│ ├── LoadingState.tsx # Centered spinner
│ ├── Map.tsx # Mapbox GL map with custom markers
│ ├── StopList.tsx # Screen 1: nearby stops list
│ ├── StopRow.tsx # Stop row (transport type + line info)
│ └── TransportIcon.tsx # 40px circle with transport letter + per-stop color palette
├── hooks/
│ ├── useDepartures.ts # Fetches departures + walking route
│ ├── useGeolocation.ts # Browser geolocation with retry
│ └── useNearbyStops.ts # Fetches nearby stops
├── lib/
│ ├── api.ts # Client fetch helpers
│ └── transitProvider.ts # HERE API response transformers
├── __tests__/ # Fixtures, mocks, setup
├── globals.css # Design tokens + layout
├── layout.tsx # Root layout (no custom fonts)
├── page.tsx # Main page (state machine)
└── types.ts # Shared TypeScript interfaces
docs/
├── PRD.md # Product requirements
├── specs.md # Technical specifications
├── design-system.md # UI design tokens & components
└── user-journey.md # User flow documentation
e2e/ # Playwright e2e tests
ios/Departs/
├── Departs.xcodeproj/ # Xcode project (iOS 17+ target)
├── Departs/
│ ├── Models/ # Codable structs matching web types.ts
│ ├── Services/ # APIService (actor), LocationService (@Observable)
│ ├── ViewModels/ # NearbyStopsViewModel, DepartureDetailViewModel
│ ├── Views/ # SwiftUI views (ContentView, screens, rows)
│ │ └── Map/ # UIViewRepresentable MKMapView wrappers
│ ├── Utilities/ # StopPalette, TimeFormatter, Color+Hex
│ └── Preview Content/ # Sample data for Xcode previews
└── DepartsTests/ # XCTest unit tests
- 5:4 aspect ratio map shows user location (blue dot) and stop pins with transport-type letters
- Each stop gets a unique muted pastel color (from a 10-color palette) on both the map marker and list icon
- "NEARBY STOPS" uppercase section header above the list
- Stop rows show stop name as primary text, line → direction as secondary
- Skeleton loading (5 placeholder rows) while stops are being fetched
- Haptic feedback and highlight on tap; pull-to-refresh
- 5:4 aspect ratio map zooms to fit user + stop pin with walking route (dashed line)
- Map uses
MapContainerViewwrapper to defer zoom until frame is valid (handles NavigationStack transition timing) - Content card with rounded top corners: line + direction as bold heading, walking time (clamped to 1 min minimum), "DEPARTURES" section header
- Relative times ("Now", "in 3 min") for departures ≤30 min away, absolute time as secondary
- Up to 4 departure times using device locale (12h or 24h per user setting)
- Full light/dark mode via CSS custom properties (
--color-bg-primary,--color-text-primary, etc.) - Dark mode: auto-detected via
prefers-color-schememedia query - Map styles switch between
light-v11anddark-v11 - Max width 430px, centered on desktop
- iOS system font stack
Browser Geolocation → useGeolocation hook
↓
/api/stops/nearby → HERE /v8/stations → transformStations → useNearbyStops
↓ (user taps stop)
/api/departures → HERE /v8/boards → transformDepartures → useDepartures
/api/directions → Mapbox Directions → flat {geometry, duration, distance} → useDepartures
Native SwiftUI companion app that reuses the Vercel backend (no API keys on device). Zero third-party dependencies — MapKit for maps, CoreLocation for location, URLSession for networking.
NavigationStackwith.navigationDestination(item:)for native push/pop transitions and swipe-back gesture- Same 2-screen flow as web app: nearby stops → departure detail
- Inline "Departs" navigation title on both screens
- Content cards with
UnevenRoundedRectanglerounded top corners and subtle shadow separating map from list - Pull-to-refresh on stops list to re-request location and reload stops
- Edge-to-edge 5:4 aspect ratio maps on both screens
- MapKit
.mutedStandardwith automatic dark mode UIViewRepresentableMKMapView for custom annotations (30pt colored pins, pulsing user dot, dashed route)- Detail map uses
MapContainerView(UIView wrapper) to defersetRegioninlayoutSubviews— solves zero-frame timing during NavigationStack transitions - Detail map loads route and departures in parallel — map shows user + stop immediately
- Per-stop pastel colors (same 10-color palette as web)
APIServiceactor with 3 async methods callingdeparts.vercel.app/api/*- Time formatting uses device locale (respects 12h/24h setting)
- Relative departure times ("Now", "in 3 min") via
TimeFormatter.relativeTime(_:) Departure.iduses UUID (not time string) to avoid duplicate identity warnings- App icon included (dark charcoal with app name and transit line motif)
- Bundle ID:
app.departs.Departs, iPhone-only target
- Core functionality working: geolocation, nearby stops, departure fetching, walking directions
- Design system fully implemented with light/dark mode
- Custom map markers rendering (blue dot, stop pins with letters, walking route)
- iOS design polish complete: NavigationStack navigation, content cards, relative times, skeleton loading, haptic feedback
- 129 web unit tests passing, 8 iOS unit tests passing, production build succeeds
- Deployed to Vercel at https://departs.vercel.app
- iOS app ready for TestFlight (archive builds, app icon, signed)
- HERE API free tier: 5,000 transactions/month
- Mapbox free tier: 50,000 map loads/month, 100,000 direction requests/month
- Departures endpoint may return 500 for some stop ID formats — IDs are now URL-encoded
useDeparturesresets state (departures, walkingRoute, error) when navigating back (stopId → null)- E2e tests exist in
e2e/but require browser setup via Playwright