A bus-focused single-page application for route lookup, nearby stops, and real-time transit information in Taiwan.
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 while they continue browsing.
- Search keywords are also preserved when returning to the Routes page during the same session.
- 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.
- Each stop shows the nearest estimated arrival when upstream ETA data is available.
- Real-time buses are displayed on both the route map and the stop timeline when live vehicle 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 city, address, and serving route badges.
- 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.
- Framework: React SPA with React Router v7
- Language: TypeScript
- UI: Mantine
- State and Data: Redux Toolkit and RTK Query
- Maps: MapLibre GL JS with CARTO raster 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/
│ ├── common/ # Shared UI components
│ ├── favorite/ # Favorite feature components
│ ├── nearby/ # Nearby feature components
│ ├── routes/ # Route feature components
│ └── providers/ # App providers
├── modules/
│ ├── apis/ # RTK Query APIs
│ ├── consts/ # Constant maps and UI copy
│ ├── enums/ # Domain enums
│ ├── hooks/ # Reusable app hooks
│ ├── interfaces/ # Domain and API object models
│ ├── slices/ # Redux slices
│ ├── types/ # Shared type helpers
│ ├── utils/ # Shared helpers
│ └── store.ts # Redux store
├── pages/ # Route page modules
├── root.tsx # App root
└── routes.ts # Route definitions
workers/
└── tdx-proxy/ # Cloudflare Worker proxy
The project relies on two external open data sources.
Base URL:
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. Requests go through a Cloudflare Worker proxy that handles TDX authentication for the frontend.
Current endpoints used by the app:
| Method | Endpoint | Used For | Response Summary |
|---|---|---|---|
GET |
/Route/City/:city |
Route search, route detail, and area-level route fan-out requests | Route-level data including subroutes, operators, departure stop names, and destination stop names |
GET |
/StopOfRoute/City/:city |
Route detail and building stop-to-route relationships for nearby stops | Stops under each route plus subroute and direction-specific stop sequences |
GET |
/Stop/City/:city |
Nearby page stop discovery and route map stop positions | Stop positions and metadata used to group nearby stops into stations |
GET |
/EstimatedTimeOfArrival/City/:city |
Route detail realtime ETA | Stop-level estimated arrival times and operating status |
GET |
/RealTimeNearStop/City/:city |
Route detail realtime vehicles | Live vehicle positions and nearest stop information |
GET |
/Shape/City/:city |
Route detail map path rendering | Official route geometry for map line rendering |
The Route page polls EstimatedTimeOfArrival and RealTimeNearStop every 30 seconds while visible, so one active Route page generates about 4 requests per minute under steady-state conditions.
Actual request load can be higher during hard refreshes, reconnects, route changes, multiple open tabs, or upstream instability. Real-time features should therefore be treated as a best-effort integration with the upstream provider rather than a strict availability guarantee.
The Cloudflare Worker proxy also emits structured request logs for operational visibility.
Boundary data comes from the counties dataset in dkaoster/taiwan-atlas:
https://cdn.jsdelivr.net/npm/taiwan-atlas/counties-10t.json
The app converts that TopoJSON dataset into GeoJSON 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_ID,TDX_CLIENT_SECRET, andTDX_ALLOWED_ORIGINS. - The frontend is already pointed at the local Worker in
.env.development:
VITE_PROXY_API_BASE_URL=http://127.0.0.1:3000/api/tdxStart local development with:
pnpm run devThis starts both the frontend dev server and the local Cloudflare Worker proxy.
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, andTDX_ALLOWED_ORIGINSin Cloudflare Worker environment bindings. - Deploy the Worker with
pnpm run deploy:proxy. - Store
VITE_PROXY_API_BASE_URLas a GitHub Actions repository variable. - Let the GitHub Pages build inject that value during
pnpm run build.
