Five UI and data features for return journeys and results page

- Replace native date inputs with always-open custom calendar; return
  journeys show two months side-by-side with Airbnb-style range selection
- Add min-connection filter (30/40/50/60 min) for the inbound leg of
  return journeys, separate from the outbound connection filter
- Fix total journey time: naive datetime subtraction across CET/BST was
  1 h too long outbound and 1 h too short inbound
- Filter inbound circle line suggestions when connection ≥ 40 min: only
  show services arriving ≥ 5 min before GWR departure at Paddington
- Add Std / SP labels to Eurostar fare lines so users can distinguish
  Standard from Standard Premier
- Row selection with a fixed summary bar showing NR + Eurostar + circle
  totals; selection is preserved in the URL
- Load walk-on fares sequentially, outbound section first
- Mobile: card-grid table layout, hide headcode/platform on small screens

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Edward Betts 2026-05-25 14:58:32 +01:00
parent a88b19fa4c
commit 1bc7631863
5 changed files with 695 additions and 122 deletions

13
app.py
View file

@ -90,6 +90,7 @@ def index():
VALID_MIN_CONNECTIONS = {45, 50, 60, 70, 80, 90, 100, 110, 120}
VALID_MAX_CONNECTIONS = {60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160, 170, 180}
VALID_INBOUND_MIN_CONNECTIONS = {20, 30, 40, 45, 50, 60, 70, 80, 90, 100, 110, 120}
VALID_INBOUND_RETURN_MIN_CONNECTIONS = {30, 40, 50, 60}
VALID_INBOUND_MAX_CONNECTIONS = {60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160, 170, 180}
VALID_JOURNEY_TYPES = {"outbound", "inbound", "return"}
VALID_NR_CLASSES = {'walkon', 'advance_std', 'advance_1st'}
@ -357,6 +358,7 @@ def _results(station_crs, slug, travel_date, journey_type, return_date):
if es_class not in VALID_ES_CLASSES:
es_class = DEFAULT_ES_CLASS
inbound_min_connection = INBOUND_MIN_CONNECTION_MINUTES
if journey_type == "return":
def _p(raw, default, valid):
return raw if raw in valid else default
@ -364,6 +366,11 @@ def _results(station_crs, slug, travel_date, journey_type, return_date):
nr_class_in = _p(request.args.get("nr_class_in"), DEFAULT_NR_CLASS, VALID_NR_CLASSES)
es_class_out = _p(request.args.get("es_class_out"), DEFAULT_ES_CLASS, VALID_ES_CLASSES)
es_class_in = _p(request.args.get("es_class_in"), DEFAULT_ES_CLASS, VALID_ES_CLASSES)
inbound_min_connection = _parse_connection(
request.args.get("min_connection_in"),
INBOUND_MIN_CONNECTION_MINUTES,
VALID_INBOUND_RETURN_MIN_CONNECTIONS,
)
else:
nr_class_out = nr_class_in = nr_class
es_class_out = es_class_in = es_class
@ -464,7 +471,7 @@ def _results(station_crs, slug, travel_date, journey_type, return_date):
section_min_connection = min_connection
section_max_connection = max_connection
if journey_type == "return" and direction == "inbound":
section_min_connection = INBOUND_MIN_CONNECTION_MINUTES
section_min_connection = inbound_min_connection
section_max_connection = INBOUND_MAX_CONNECTION_MINUTES
rtt_direction = "to_paddington" if direction == "outbound" else "from_paddington"
rtt_cache_key = _nr_exact_cache_key(rtt_direction, station_crs, section_date)
@ -672,6 +679,7 @@ def _results(station_crs, slug, travel_date, journey_type, return_date):
"return_date": return_date,
"min_connection": url_min,
"max_connection": url_max,
"min_connection_in": None if inbound_min_connection == INBOUND_MIN_CONNECTION_MINUTES else inbound_min_connection,
"nr_class_out": None if nr_class_out == DEFAULT_NR_CLASS else nr_class_out,
"nr_class_in": None if nr_class_in == DEFAULT_NR_CLASS else nr_class_in,
"es_class_out": None if es_class_out == DEFAULT_ES_CLASS else es_class_out,
@ -860,6 +868,9 @@ def _results(station_crs, slug, travel_date, journey_type, return_date):
advance_fares_stream_url=url_for("api_advance_fares_stream", station_crs=station_crs, travel_date=travel_date),
valid_min_connections=sorted(valid_min),
valid_max_connections=sorted(valid_max),
inbound_min_connection=inbound_min_connection,
default_inbound_min_connection=INBOUND_MIN_CONNECTION_MINUTES,
valid_inbound_return_min_connections=sorted(VALID_INBOUND_RETURN_MIN_CONNECTIONS),
)