A bus-focused single-page application for route lookup, nearby stops, and real-time transit information in Taiwan.
Visit bus.lynns.me to start using the app.
Search bus routes by service area and keyword.
- The Routes page defaults to the area resolved from the user's current location.
- If the user manually changes the area, that selection is preserved across revisits.
- When the search box is empty, the page shows recently viewed routes when available for quick access.
- Matching routes open a route detail page with subroute tabs, stop lists, and a synchronized map.
The Route page combines stop lists, map interaction, and real-time transit data.
- Official route shape data is used for more accurate route lines on the map when available.
- The stop list and map stay in sync: selecting a stop in one view updates the other.
- Stops can open Google Maps navigation from the stop list, and the route map includes a control to focus back on the user's current location.
- Each stop shows stop-level ETA based directly on
EstimatedTimeOfArrivalwhen upstream ETA data is available. - Real-time vehicle plates are shown as separate location cues in the stop list and do not define the stop ETA.
- Route map vehicle markers use live GPS coordinates from
RealTimeByFrequencywhen upstream realtime position data is available. - The app shows real-time status messaging such as temporary data issues or non-operating service periods.
The Nearby Stops page uses the user's current GPS location.
- If location permission is granted, the app resolves the current city and service area automatically.
- Stops within 0.5 kilometers are shown as both a list and map markers.
- Selecting a stop reveals stop details, including its distance, city, address, and serving route badges.
- Stop details can open Google Maps navigation directly.
- Opening a stop's route detail view shows the full route list grouped by direction.
If location permission is denied, the Nearby Stops feature becomes unavailable.
The Favorites page stores route-stop combinations for quick access.
- Each favorite keeps the route, subroute, direction, and a specific stop.
- Opening a favorite jumps back into the matching Route page and highlights the saved stop.
The app currently supports both zh-TW and en.
- Users can switch the interface language from the Settings page.
- The selected language is saved in local storage and restored on the next visit.
- Static UI copy comes from shared translation resources.
- API-backed route, subroute, stop, departure, and destination text follows the active locale, preferring English when available and falling back to
zh-TWwhen English data is missing.
- Framework: React SPA with React Router v7
- Language: TypeScript
- UI: Mantine
- State and Data: Redux Toolkit and RTK Query
- Maps: MapLibre GL JS with OpenFreeMap tiles
- API Proxy: Cloudflare Workers
- Worker Tooling: Wrangler
- Geospatial Utilities: Turf.js
- Testing: Vitest and React Testing Library
- Tooling: Vite, ESLint, pnpm
The app is organized around route-level pages, feature components, and shared domain modules.
app/
├── components/ # Shared and feature UI
├── modules/
│ ├── apis/ # RTK Query APIs
│ ├── consts/ # Shared constants and UI copy
│ ├── enums/ # Domain enums
│ ├── hooks/ # Reusable hooks
│ ├── i18n/ # Locale setup and translation resources
│ ├── interfaces/ # Domain and API models
│ ├── slices/ # Redux slices
│ ├── store/ # Redux store entry and preload helpers
│ ├── types/ # Shared type helpers
│ ├── utils/ # Shared helpers grouped by domain
│ │ ├── favorite/ # Favorite persistence normalization
│ │ ├── geo/ # Coordinate, area, city, and nearby query helpers
│ │ ├── i18n/ # Localized text and label helpers
│ │ ├── map/ # Map marker DOM helpers
│ │ ├── route/ # Route data transforms, realtime, and shape helpers
│ │ ├── routes/ # Route-search storage and ranking helpers
│ │ └── shared/ # Small cross-domain utilities
├── pages/ # Route pages, including Favorite, Routes, Nearby, Route, and Settings
├── test/ # Shared test setup and render helpers
├── root.tsx # App root
└── routes.ts # Route definitions
workers/
└── tdx-proxy/ # Cloudflare Worker proxy
The project relies on two external open data sources.
https://tdx.transportdata.tw/api/basic/v2/Bus
TDX, short for Transport Data eXchange, provides the route, stop, realtime, and shape data used by this app. Frontend requests go through a Cloudflare Worker proxy that handles TDX authentication.
The app uses TDX data for:
- route search and route detail lookups
- stop and station discovery for nearby views
- stop-level ETA, near-stop vehicle cues, and live map vehicle positions
- official route shape rendering on maps
Current endpoint usage:
| Endpoint | Used For |
|---|---|
/Route/City/:city |
route search and route detail |
/StopOfRoute/City/:city |
route stop lists and nearby route relationships |
/Stop/City/:city |
nearby stop discovery and map stop positions |
/EstimatedTimeOfArrival/City/:city |
stop-level ETA |
/RealTimeNearStop/City/:city |
stop-list vehicle cues and near-stop status |
/RealTimeByFrequency/City/:city |
route map vehicle GPS positions |
/Shape/City/:city |
route map path rendering |
Realtime data is best-effort and may be temporarily unavailable when the shared proxy-backed key hits upstream rate limits.
Boundary data comes from the counties dataset in dkaoster/taiwan-atlas:
https://cdn.jsdelivr.net/npm/taiwan-atlas/counties-10t.json
The project vendors that TopoJSON dataset as a local static asset and converts it into GeoJSON at runtime to determine the user's city and area for nearby-stop and route-search flows.
pnpm install- Copy
workers/tdx-proxy/.dev.vars.exampletoworkers/tdx-proxy/.dev.vars. - Fill in
TDX_CLIENT_IDandTDX_CLIENT_SECRET.
Start local development with:
pnpm run devThis starts both the frontend dev server and the local Cloudflare Worker proxy.
To test from another device on the same network, start the mobile-friendly dev mode, then open the site using your computer's LAN IP:
pnpm run dev:mobileThis exposes both the frontend and the local Worker proxy on your LAN. In development, the frontend uses the relative path /api/tdx, and the Vite dev server proxies that path to the local Worker on port 3000. Open http://<your-lan-ip>:5173 on your phone after replacing the IP with your own.
pnpm run lint
pnpm run typecheck
pnpm testThe frontend is deployed as a static app, while TDX authentication is handled by a separate Cloudflare Worker proxy.
- Store
TDX_CLIENT_ID,TDX_CLIENT_SECRET, andALLOWED_ORIGINSin Cloudflare Worker environment bindings. - Deploy the Worker with
pnpm run deploy:proxy. - Store
VITE_PROXY_API_BASE_URLas a GitHub Actions repository variable. - Optional: store
VITE_GA_IDas a GitHub Actions repository variable to enable GA4 pageview tracking. - Let the GitHub Pages build inject those values during
pnpm run build.
