Skip to main content

Nearby Transit Board

Display the next departures while demonstrating location-aware, searchable, and conditional configuration fields. Demo mode supplies stable departures; the live HTTP function is ready for a transit provider.

Transit board listing four aligned routes, destinations, and departure minutes

Complete app

app.star
load("encoding/json.star", "json")
load("http.star", "http")
load("render.star", "render")
load("schema.star", "schema")

DEMO_MODE = True
DEMO_DEPARTURES = [
{"route": "A", "destination": "Downtown", "minutes": 2},
{"route": "C", "destination": "Riverside", "minutes": 7},
{"route": "M", "destination": "Museum", "minutes": 12},
{"route": "B", "destination": "Airport", "minutes": 18},
]

def fetch_departures(station):
response = http.get(
"https://transit.example/departures?station=%s" % station,
ttl_seconds = 30,
)
if response.status_code != 200:
fail("Transit request failed: %d" % response.status_code)
return response.json()["departures"]

def departure_row(item):
return render.Row(
expanded = True,
main_align = "space_between",
children = [
render.Row(
children = [
render.Stack(
children = [
render.Box(width = 11, height = 8, color = "#2563eb"),
render.Padding(
pad = (4, 1, 0, 0),
child = render.Text(
item["route"],
font = "CG-pixel-3x5-mono",
color = "#ffffff",
),
),
],
),
render.Box(width = 2, height = 1),
render.Padding(
pad = (0, 1, 0, 0),
child = render.Text(
item["destination"],
font = "CG-pixel-3x5-mono",
color = "#dbeafe",
),
),
],
),
render.Padding(
pad = (0, 1, 0, 0),
child = render.Text(
"%dm" % item["minutes"],
font = "CG-pixel-3x5-mono",
color = "#fbbf24",
),
),
],
)

def main(config):
station = config.str("station_id", "central")
departures = DEMO_DEPARTURES if DEMO_MODE else fetch_departures(station)

return render.Root(
child = render.Padding(
pad = (0, 0, 1, 0),
child = render.Column(
children = [departure_row(item) for item in departures],
),
),
)

def nearby_stations(raw_location):
location = json.decode(raw_location)
locality = location.get("locality", "")
return [
schema.Option(
display = "%s Central" % locality,
value = "central",
),
]

def search_stations(pattern):
stations = [
schema.Option(display = "Central", value = "central"),
schema.Option(display = "Riverside", value = "riverside"),
]
query = pattern.lower()
return [station for station in stations if query in station.display.lower()]

def alert_fields(enabled):
if not enabled:
return []
return [
schema.Text(
id = "alert_text",
name = "Alert label",
desc = "Optional label displayed with service alerts.",
icon = "text",
default = "Service update",
),
]

def get_schema():
return schema.Schema(
version = "1",
fields = [
schema.Location(
id = "location",
name = "Location",
desc = "Find stations near this location.",
icon = "location-dot",
),
schema.LocationBased(
id = "nearby_station",
name = "Nearby station",
desc = "Choose a station near the selected location.",
icon = "location-crosshairs",
handler = nearby_stations,
),
schema.Typeahead(
id = "station_id",
name = "Station search",
desc = "Search all stations.",
icon = "text",
handler = search_stations,
),
schema.Toggle(
id = "show_alerts",
name = "Service alerts",
desc = "Configure service alert text.",
icon = "eye",
default = False,
),
schema.Generated(
id = "alert_options",
source = "show_alerts",
handler = alert_fields,
),
],
)

Download this app.star.

How it works

  1. departure_row() turns each departure object into a reusable nested row.
  2. The 30-second HTTP TTL balances fresh estimates with request volume.
  3. LocationBased receives location JSON and generates a nearby option.
  4. Typeahead filters the complete station list independently.
  5. Generated adds the alert text field only when its source toggle is enabled.

Connect live transit

Replace transit.example, adapt the response to the three demo keys, and set DEMO_MODE to False. Use your provider's stable station ID as the option value.