Refine homepage journey form layout

This commit is contained in:
Edward Betts 2026-05-26 12:55:23 +01:00
parent 13c4341f3a
commit ed8a5626a4
4 changed files with 74 additions and 38 deletions

31
app.py
View file

@ -32,6 +32,8 @@ from trip_planner import (
find_unreachable_inbound_eurostars,
find_unreachable_morning_eurostars,
)
import cache
import circle_line
RTT_PADDINGTON_URL = (
"https://www.realtimetrains.co.uk/search/detailed/"
@ -51,9 +53,6 @@ _local = os.path.join(os.path.dirname(__file__), "config", "local.py")
if os.path.exists(_local):
app.config.from_pyfile(_local)
import cache
import circle_line
cache.CACHE_DIR = app.config["CACHE_DIR"] # type: ignore[attr-defined]
circle_line._TXC_XML = app.config["CIRCLE_LINE_XML"] # type: ignore[attr-defined]
@ -72,13 +71,25 @@ def _load_stations() -> list[tuple[str, str]]:
STATIONS = _load_stations()
STATION_BY_CRS = {crs: name for name, crs in STATIONS}
DESTINATION_OPTIONS = [
{"slug": "paris", "city": "Paris", "destination": "Paris Gare du Nord"},
{"slug": "brussels", "city": "Brussels", "destination": "Brussels Midi"},
{"slug": "lille", "city": "Lille", "destination": "Lille Europe"},
{
"slug": "amsterdam",
"city": "Amsterdam",
"destination": "Amsterdam Centraal",
},
{
"slug": "rotterdam",
"city": "Rotterdam",
"destination": "Rotterdam Centraal",
},
{"slug": "cologne", "city": "Cologne Hbf", "destination": "Cologne Hbf"},
]
DESTINATIONS = {
"paris": "Paris Gare du Nord",
"brussels": "Brussels Midi",
"lille": "Lille Europe",
"amsterdam": "Amsterdam Centraal",
"rotterdam": "Rotterdam Centraal",
"cologne": "Cologne Hbf",
option["slug"]: option["destination"] for option in DESTINATION_OPTIONS
}
@ -88,7 +99,7 @@ def index() -> ResponseReturnValue:
default_min, default_max = _get_defaults()
return render_template(
"index.html",
destinations=DESTINATIONS,
destination_options=DESTINATION_OPTIONS,
today=today,
stations=STATIONS,
default_min_connection=default_min,

View file

@ -95,6 +95,10 @@
gap: 0.75rem;
}
.destination-grid--eurostar {
grid-template-columns: repeat(6, minmax(0, 1fr));
}
.destination-option {
position: relative;
}
@ -110,7 +114,7 @@
.destination-option label {
display: block;
min-height: 100%;
padding: 0.95rem 1rem;
padding: 0.8rem 0.85rem;
border: 1px solid #cbd5e0;
border-radius: 10px;
background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%);
@ -121,7 +125,7 @@
.destination-option label strong {
display: block;
color: #0f172a;
font-size: 1rem;
font-size: 0.98rem;
margin-bottom: 0.2rem;
}
@ -184,6 +188,10 @@
@media (max-width: 640px) {
.card { padding: 1.25rem; }
.destination-grid--eurostar {
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}
/* Convert results table to a 2-column card layout per row */
.results-table, .results-table tbody { display: block; }
.results-table thead { display: none; }
@ -233,6 +241,21 @@
/* Form groups */
.form-group { margin-bottom: 1.2rem; }
.form-group-lg { margin-bottom: 1.5rem; }
.form-row {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1rem;
margin-bottom: 1.5rem;
}
.form-row .form-group,
.form-row .form-group-lg {
margin-bottom: 0;
}
@media (max-width: 640px) {
.form-row {
grid-template-columns: 1fr;
}
}
#return-date-group:has(input:disabled) { cursor: pointer; }
/* Buttons */

View file

@ -99,13 +99,12 @@
<div class="form-group">
<span class="field-label">Eurostar destination</span>
<div class="destination-grid" role="radiogroup" aria-label="Eurostar destination">
{% for slug, name in destinations.items() %}
{% set city = name.replace(' Gare du Nord','').replace(' Centraal','').replace(' Midi','').replace(' Europe','') %}
<div class="destination-grid destination-grid--eurostar" role="radiogroup" aria-label="Eurostar destination">
{% for destination in destination_options %}
<div class="destination-option">
<input type="radio" id="dest-{{ slug }}" name="destination" value="{{ slug }}"
<input type="radio" id="dest-{{ destination.slug }}" name="destination" value="{{ destination.slug }}"
{% if loop.first %}checked{% endif %} required>
<label for="dest-{{ slug }}"><strong>{{ city }}</strong><span>{{ name }}</span></label>
<label for="dest-{{ destination.slug }}"><strong>{{ destination.city }}</strong><span>{{ destination.destination }}</span></label>
</div>
{% endfor %}
</div>
@ -128,26 +127,28 @@
<input type="hidden" id="return_date" name="">
</div>
<div class="form-group">
<label for="min_connection" class="field-label">
Minimum connection time (Paddington &rarr; St&nbsp;Pancras)
</label>
<select id="min_connection" name="min_connection" class="form-control">
{% for mins in valid_min_connections %}
<option value="{{ mins }}" {% if mins == default_min_connection %}selected{% endif %}>{{ mins }} min</option>
{% endfor %}
</select>
</div>
<div class="form-row">
<div class="form-group">
<label for="min_connection" class="field-label">
Minimum connection time (Paddington &rarr; St&nbsp;Pancras)
</label>
<select id="min_connection" name="min_connection" class="form-control">
{% for mins in valid_min_connections %}
<option value="{{ mins }}" {% if mins == default_min_connection %}selected{% endif %}>{{ mins }} min</option>
{% endfor %}
</select>
</div>
<div class="form-group-lg">
<label for="max_connection" class="field-label">
Maximum connection time (Paddington &rarr; St&nbsp;Pancras)
</label>
<select id="max_connection" name="max_connection" class="form-control">
{% for mins in valid_max_connections %}
<option value="{{ mins }}" {% if mins == default_max_connection %}selected{% endif %}>{{ mins }} min</option>
{% endfor %}
</select>
<div class="form-group">
<label for="max_connection" class="field-label">
Maximum connection time (Paddington &rarr; St&nbsp;Pancras)
</label>
<select id="max_connection" name="max_connection" class="form-control">
{% for mins in valid_max_connections %}
<option value="{{ mins }}" {% if mins == default_max_connection %}selected{% endif %}>{{ mins }} min</option>
{% endfor %}
</select>
</div>
</div>
<button type="submit" class="btn-primary">Search journeys</button>

View file

@ -64,8 +64,9 @@ def test_index_shows_station_dropdown_and_destination_radios() -> None:
assert "Departure point" in html
assert "Bristol Temple Meads" in html
assert 'name="station_crs"' in html
assert html.count('type="radio"') == len(app_module.DESTINATIONS)
assert "destination-rotterdam" in html
assert html.count('name="destination"') == len(app_module.DESTINATIONS)
assert 'id="dest-rotterdam"' in html
assert "<strong>Cologne Hbf</strong><span>Cologne Hbf</span>" in html
def test_search_redirects_to_results_with_selected_params() -> None: