Move inline styles to CSS classes; update README
Extract repeated inline styles from templates into named CSS classes in base.html: layout helpers, buttons, form groups, alert boxes, results table rules, row highlight classes, typography utilities, and empty-state styles. Remove the per-page <style> block from results.html. Update README to reflect current destinations, GraphQL data source, Circle Line timetable, configurable connection range, and GWR fare table. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e6f310f517
commit
71be0dd8cf
4 changed files with 214 additions and 97 deletions
46
README.md
46
README.md
|
|
@ -2,9 +2,9 @@
|
|||
|
||||
Source: https://git.4angle.com/edward/bristol-eurostar
|
||||
|
||||
Plan a trip from Bristol Temple Meads to Europe on Eurostar.
|
||||
Plan a trip from Bristol Temple Meads to Europe via Eurostar.
|
||||
|
||||
Combines GWR trains (Bristol → Paddington) with Eurostar services (St Pancras → destination) and shows all valid same-day connections, filtering by journey time and minimum/maximum transfer window at Paddington/St Pancras.
|
||||
Combines GWR trains (Bristol → Paddington) with Eurostar services (St Pancras → destination) and shows all valid same-day connections, including Circle Line times for the Paddington → St Pancras transfer. Displays GWR walk-on fares, Eurostar Standard prices, seat availability, and total journey cost.
|
||||
|
||||
## Destinations
|
||||
|
||||
|
|
@ -12,23 +12,42 @@ Combines GWR trains (Bristol → Paddington) with Eurostar services (St Pancras
|
|||
- Brussels Midi
|
||||
- Lille Europe
|
||||
- Amsterdam Centraal
|
||||
- Rotterdam Centraal
|
||||
- Cologne Hbf
|
||||
|
||||
## How it works
|
||||
|
||||
Train times are fetched from two sources simultaneously:
|
||||
Train times and prices are fetched from two sources:
|
||||
|
||||
- **GWR** — scraped from [Realtime Trains](https://www.realtimetrains.co.uk/) using httpx
|
||||
- **Eurostar** — scraped from the Eurostar timetable pages via the embedded `__NEXT_DATA__` JSON (no browser required)
|
||||
- **Eurostar** — fetched from the Eurostar GraphQL API (single call returns timetable, Standard fares, and seat availability)
|
||||
|
||||
The Paddington → St Pancras transfer uses a real Circle Line timetable parsed from a TfL TransXChange XML file, accounting for walk time to the platform at Paddington and walk time from the platform to the St Pancras check-in.
|
||||
|
||||
Results are cached to disk by date and destination.
|
||||
|
||||
## Connection constraints
|
||||
|
||||
Configurable via the search form. Defaults:
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Minimum Paddington → St Pancras | 75 min |
|
||||
| Maximum Paddington → St Pancras | 2h 20m |
|
||||
| Maximum Bristol → Paddington | 1h 50m |
|
||||
| Minimum Paddington → St Pancras | 50 min |
|
||||
| Maximum Paddington → St Pancras | 110 min |
|
||||
|
||||
Valid range: 45–120 min (min), 60–180 min (max).
|
||||
|
||||
## GWR fares
|
||||
|
||||
Walk-on single fares for Bristol Temple Meads → Paddington, selected automatically by departure time:
|
||||
|
||||
| Ticket | Price | Restriction (weekdays) |
|
||||
|---|---|---|
|
||||
| Super Off-Peak | £45.00 | Not valid 05:05–09:57 |
|
||||
| Off-Peak | £63.60 | Not valid before 08:26 |
|
||||
| Anytime | £138.70 | No restriction |
|
||||
|
||||
Weekends always use Super Off-Peak.
|
||||
|
||||
## Setup
|
||||
|
||||
|
|
@ -36,6 +55,19 @@ Results are cached to disk by date and destination.
|
|||
pip install -e ".[dev]"
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
Copy or create `config/local.py` (gitignored) to override defaults:
|
||||
|
||||
```python
|
||||
CACHE_DIR = '/var/cache/bristol-eurostar'
|
||||
CIRCLE_LINE_XML = '/path/to/output_txc_01CIR_.xml'
|
||||
```
|
||||
|
||||
Defaults (in `config/default.py`) use `~/lib/data/tfl/`.
|
||||
|
||||
The Circle Line XML is a TfL TransXChange timetable file. The Paddington (H&C Line) stop is `9400ZZLUPAH1`; the King's Cross St Pancras stop is `9400ZZLUKSX3`.
|
||||
|
||||
## Running
|
||||
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -185,9 +185,103 @@
|
|||
.card {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
.col-transfer { display: none; }
|
||||
}
|
||||
|
||||
a { color: #00539f; }
|
||||
|
||||
/* Card helpers */
|
||||
.card > h2:first-child { margin-top: 0; }
|
||||
.card-scroll { overflow-x: auto; }
|
||||
|
||||
/* Form groups */
|
||||
.form-group { margin-bottom: 1.2rem; }
|
||||
.form-group-lg { margin-bottom: 1.5rem; }
|
||||
|
||||
/* Buttons */
|
||||
.btn-primary {
|
||||
background: #00539f;
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 0.75rem 2rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-nav {
|
||||
padding: 0.3rem 0.75rem;
|
||||
border: 1px solid #cbd5e0;
|
||||
border-radius: 4px;
|
||||
text-decoration: none;
|
||||
color: #00539f;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Inline select */
|
||||
.select-inline {
|
||||
padding: 0.3rem 0.6rem;
|
||||
font-size: 0.9rem;
|
||||
border: 1px solid #cbd5e0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Alert boxes */
|
||||
.alert {
|
||||
margin-top: 1rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.alert-error {
|
||||
background: #fff5f5;
|
||||
border: 1px solid #fc8181;
|
||||
color: #c53030;
|
||||
}
|
||||
.alert-warning {
|
||||
background: #fffbeb;
|
||||
border: 1px solid #f6e05e;
|
||||
color: #744210;
|
||||
}
|
||||
|
||||
/* Results page layout */
|
||||
.back-link { margin-bottom: 1rem; }
|
||||
.date-nav { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.5rem; }
|
||||
.switcher-section { margin: 0.9rem 0 1rem; }
|
||||
.section-label { font-size: 0.9rem; font-weight: 600; margin-bottom: 0.45rem; }
|
||||
.filter-row { margin-top: 0.75rem; display: flex; gap: 1.5rem; align-items: center; }
|
||||
.filter-label { font-size: 0.9rem; font-weight: 600; margin-right: 0.5rem; }
|
||||
.card-meta { color: #4a5568; margin: 0; }
|
||||
.footnote { margin-top: 1rem; font-size: 0.82rem; color: #718096; }
|
||||
|
||||
/* Results table */
|
||||
.results-table { width: 100%; border-collapse: collapse; font-size: 0.95rem; }
|
||||
.results-table th,
|
||||
.results-table td { padding: 0.6rem 0.8rem; }
|
||||
.results-table thead tr { border-bottom: 2px solid #e2e8f0; text-align: left; }
|
||||
.results-table tbody tr { border-bottom: 1px solid #e2e8f0; }
|
||||
.row-fast { background: #f0fff4; }
|
||||
.row-slow { background: #fff5f5; }
|
||||
.row-alt { background: #f7fafc; }
|
||||
.row-unreachable { background: #f7fafc; color: #a0aec0; }
|
||||
|
||||
/* Empty state */
|
||||
.empty-state { color: #4a5568; text-align: center; padding: 3rem 2rem; }
|
||||
.empty-state p { margin: 0; }
|
||||
.empty-state p:first-child { font-size: 1.1rem; margin-bottom: 0.5rem; }
|
||||
.empty-state p:last-child { font-size: 0.9rem; }
|
||||
|
||||
/* Utilities */
|
||||
.text-muted { color: #718096; }
|
||||
.text-dimmed { color: #a0aec0; }
|
||||
.text-green { color: #276749; }
|
||||
.text-red { color: #c53030; }
|
||||
.text-blue { color: #00539f; }
|
||||
.text-sm { font-size: 0.85rem; }
|
||||
.text-xs { font-size: 0.75rem; }
|
||||
.nowrap { white-space: nowrap; }
|
||||
.font-bold { font-weight: 600; }
|
||||
.font-normal { font-weight: 400; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<h2 style="margin-top:0">Plan your journey</h2>
|
||||
<h2>Plan your journey</h2>
|
||||
<form method="get" action="{{ url_for('search') }}">
|
||||
<div style="margin-bottom:1.2rem">
|
||||
<div class="form-group">
|
||||
<span class="field-label">Departure point</span>
|
||||
<div class="fixed-station" aria-label="Departure point">
|
||||
<strong>Bristol Temple Meads</strong>
|
||||
|
|
@ -11,7 +11,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:1.2rem">
|
||||
<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() %}
|
||||
|
|
@ -33,7 +33,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:1.5rem">
|
||||
<div class="form-group-lg">
|
||||
<label for="travel_date" class="field-label">
|
||||
Travel date
|
||||
</label>
|
||||
|
|
@ -42,7 +42,7 @@
|
|||
class="form-control">
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:1.2rem">
|
||||
<div class="form-group">
|
||||
<label for="min_connection" class="field-label">
|
||||
Minimum connection time (Paddington → St Pancras)
|
||||
</label>
|
||||
|
|
@ -53,7 +53,7 @@
|
|||
</select>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:1.5rem">
|
||||
<div class="form-group-lg">
|
||||
<label for="max_connection" class="field-label">
|
||||
Maximum connection time (Paddington → St Pancras)
|
||||
</label>
|
||||
|
|
@ -64,9 +64,7 @@
|
|||
</select>
|
||||
</div>
|
||||
|
||||
<button type="submit"
|
||||
style="background:#00539f;color:#fff;border:none;padding:0.75rem 2rem;
|
||||
font-size:1rem;font-weight:600;border-radius:4px;cursor:pointer">
|
||||
<button type="submit" class="btn-primary">
|
||||
Search journeys
|
||||
</button>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -5,31 +5,24 @@
|
|||
{% block twitter_title %}Bristol to {{ destination }} via Eurostar{% endblock %}
|
||||
{% block twitter_description %}Train options from Bristol Temple Meads to {{ destination }} on {{ travel_date_display }} via Paddington, St Pancras, and Eurostar.{% endblock %}
|
||||
{% block content %}
|
||||
<style>
|
||||
@media (max-width: 640px) {
|
||||
.col-transfer { display: none; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<p style="margin-bottom:1rem">
|
||||
<p class="back-link">
|
||||
<a href="{{ url_for('index') }}">← New search</a>
|
||||
</p>
|
||||
|
||||
<div class="card" style="margin-bottom:1.5rem">
|
||||
<h2 style="margin-top:0">
|
||||
<h2>
|
||||
Bristol Temple Meads → {{ destination }}
|
||||
</h2>
|
||||
<div style="display:flex;align-items:center;gap:0.75rem;margin-bottom:0.5rem">
|
||||
<div class="date-nav">
|
||||
<a href="{{ url_for('results', slug=slug, travel_date=prev_date, min_connection=min_connection, max_connection=max_connection) }}"
|
||||
style="padding:0.3rem 0.75rem;border:1px solid #cbd5e0;border-radius:4px;
|
||||
text-decoration:none;color:#00539f;font-size:0.9rem">← Prev</a>
|
||||
class="btn-nav">← Prev</a>
|
||||
<strong>{{ travel_date_display }}</strong>
|
||||
<a href="{{ url_for('results', slug=slug, travel_date=next_date, min_connection=min_connection, max_connection=max_connection) }}"
|
||||
style="padding:0.3rem 0.75rem;border:1px solid #cbd5e0;border-radius:4px;
|
||||
text-decoration:none;color:#00539f;font-size:0.9rem">Next →</a>
|
||||
class="btn-nav">Next →</a>
|
||||
</div>
|
||||
<div style="margin:0.9rem 0 1rem">
|
||||
<div style="font-size:0.9rem;font-weight:600;margin-bottom:0.45rem">Switch destination for {{ travel_date_display }}</div>
|
||||
<div class="switcher-section">
|
||||
<div class="section-label">Switch destination for {{ travel_date_display }}</div>
|
||||
<div class="chip-row">
|
||||
{% for destination_slug, destination_name in destinations.items() %}
|
||||
{% if destination_slug == slug %}
|
||||
|
|
@ -43,26 +36,26 @@
|
|||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top:0.75rem;display:flex;gap:1.5rem;align-items:center">
|
||||
<div class="filter-row">
|
||||
<div>
|
||||
<label for="min_conn_select" style="font-size:0.9rem;font-weight:600;margin-right:0.5rem">
|
||||
<label for="min_conn_select" class="filter-label">
|
||||
Min connection:
|
||||
</label>
|
||||
<select id="min_conn_select"
|
||||
onchange="applyConnectionFilter()"
|
||||
style="padding:0.3rem 0.6rem;font-size:0.9rem;border:1px solid #cbd5e0;border-radius:4px">
|
||||
class="select-inline">
|
||||
{% for mins in valid_min_connections %}
|
||||
<option value="{{ mins }}" {% if mins == min_connection %}selected{% endif %}>{{ mins }} min</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="max_conn_select" style="font-size:0.9rem;font-weight:600;margin-right:0.5rem">
|
||||
<label for="max_conn_select" class="filter-label">
|
||||
Max connection:
|
||||
</label>
|
||||
<select id="max_conn_select"
|
||||
onchange="applyConnectionFilter()"
|
||||
style="padding:0.3rem 0.6rem;font-size:0.9rem;border:1px solid #cbd5e0;border-radius:4px">
|
||||
class="select-inline">
|
||||
{% for mins in valid_max_connections %}
|
||||
<option value="{{ mins }}" {% if mins == max_connection %}selected{% endif %}>{{ mins }} min</option>
|
||||
{% endfor %}
|
||||
|
|
@ -76,45 +69,45 @@
|
|||
window.location = '{{ url_for('results', slug=slug, travel_date=travel_date) }}?min_connection=' + min + '&max_connection=' + max;
|
||||
}
|
||||
</script>
|
||||
<p style="color:#4a5568;margin:0">
|
||||
<p class="card-meta">
|
||||
{{ gwr_count }} GWR service{{ 's' if gwr_count != 1 }}
|
||||
·
|
||||
{{ eurostar_count }} Eurostar service{{ 's' if eurostar_count != 1 }}
|
||||
{% if unreachable_morning_services %}
|
||||
·
|
||||
<span style="color:#718096">
|
||||
<span class="text-muted">
|
||||
{{ unreachable_morning_services | length }} Eurostar service{{ 's' if unreachable_morning_services | length != 1 }} unavailable from Bristol
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if from_cache %}
|
||||
· <span style="color:#718096;font-size:0.85rem">(cached)</span>
|
||||
· <span class="text-muted text-sm">(cached)</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
{% if error %}
|
||||
<div style="margin-top:1rem;padding:0.75rem 1rem;background:#fff5f5;border:1px solid #fc8181;border-radius:4px;color:#c53030">
|
||||
<div class="alert alert-error">
|
||||
<strong>Warning:</strong> {{ error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if no_prices_note %}
|
||||
<div style="margin-top:1rem;padding:0.75rem 1rem;background:#fffbeb;border:1px solid #f6e05e;border-radius:4px;color:#744210">
|
||||
<div class="alert alert-warning">
|
||||
{{ no_prices_note }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if trips or unreachable_morning_services %}
|
||||
<div class="card" style="overflow-x:auto">
|
||||
<table style="width:100%;border-collapse:collapse;font-size:0.95rem">
|
||||
<div class="card card-scroll">
|
||||
<table class="results-table">
|
||||
<thead>
|
||||
<tr style="border-bottom:2px solid #e2e8f0;text-align:left">
|
||||
<th style="padding:0.6rem 0.8rem;white-space:nowrap">Bristol</th>
|
||||
<th style="padding:0.6rem 0.8rem;white-space:nowrap">Paddington</th>
|
||||
<th style="padding:0.6rem 0.8rem;white-space:nowrap">GWR Fare</th>
|
||||
<th class="col-transfer" style="padding:0.6rem 0.8rem;white-space:nowrap">Transfer</th>
|
||||
<th style="padding:0.6rem 0.8rem;white-space:nowrap">Depart STP</th>
|
||||
<th style="padding:0.6rem 0.8rem">{{ destination }}</th>
|
||||
<th style="padding:0.6rem 0.8rem;white-space:nowrap">ES Std</th>
|
||||
<th style="padding:0.6rem 0.8rem;white-space:nowrap">Total</th>
|
||||
<tr>
|
||||
<th class="nowrap">Bristol</th>
|
||||
<th class="nowrap">Paddington</th>
|
||||
<th class="nowrap">GWR Fare</th>
|
||||
<th class="col-transfer nowrap">Transfer</th>
|
||||
<th class="nowrap">Depart STP</th>
|
||||
<th>{{ destination }}</th>
|
||||
<th class="nowrap">ES Std</th>
|
||||
<th class="nowrap">Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
@ -129,91 +122,91 @@
|
|||
{% endif %}
|
||||
{% for row in result_rows %}
|
||||
{% if row.row_type == 'trip' and row.total_minutes <= best_mins + 5 and trips | length > 1 %}
|
||||
{% set row_bg = 'background:#f0fff4' %}
|
||||
{% set row_class = 'row-fast' %}
|
||||
{% elif row.row_type == 'trip' and row.total_minutes >= worst_mins - 5 and trips | length > 1 %}
|
||||
{% set row_bg = 'background:#fff5f5' %}
|
||||
{% set row_class = 'row-slow' %}
|
||||
{% elif row.row_type == 'unreachable' %}
|
||||
{% set row_bg = 'background:#f7fafc;color:#a0aec0' %}
|
||||
{% set row_class = 'row-unreachable' %}
|
||||
{% elif loop.index is odd %}
|
||||
{% set row_bg = 'background:#f7fafc' %}
|
||||
{% set row_class = 'row-alt' %}
|
||||
{% else %}
|
||||
{% set row_bg = '' %}
|
||||
{% set row_class = '' %}
|
||||
{% endif %}
|
||||
<tr style="border-bottom:1px solid #e2e8f0;{{ row_bg }}">
|
||||
<tr class="{{ row_class }}">
|
||||
{% if row.row_type == 'trip' %}
|
||||
<td style="padding:0.6rem 0.8rem;font-weight:600">
|
||||
<td class="font-bold">
|
||||
{{ row.depart_bristol }}
|
||||
{% if row.headcode %}<br><span style="font-size:0.75rem;font-weight:400;color:#718096">{{ row.headcode }}</span>{% endif %}
|
||||
{% if row.headcode %}<br><span class="text-xs font-normal text-muted">{{ row.headcode }}</span>{% endif %}
|
||||
</td>
|
||||
<td style="padding:0.6rem 0.8rem">
|
||||
<td>
|
||||
{{ row.arrive_paddington }}
|
||||
<span style="font-size:0.8rem;color:#718096;white-space:nowrap">({{ row.gwr_duration }})</span>
|
||||
<span class="text-sm text-muted nowrap">({{ row.gwr_duration }})</span>
|
||||
</td>
|
||||
<td style="padding:0.6rem 0.8rem;white-space:nowrap">
|
||||
<td class="nowrap">
|
||||
£{{ "%.2f"|format(row.ticket_price) }}
|
||||
<br><span style="font-size:0.75rem;color:#718096">{{ row.ticket_name }}</span>
|
||||
<br><span class="text-xs text-muted">{{ row.ticket_name }}</span>
|
||||
</td>
|
||||
<td class="col-transfer" style="padding:0.6rem 0.8rem;color:#4a5568;white-space:nowrap">
|
||||
<td class="col-transfer nowrap" style="color:#4a5568">
|
||||
{{ row.connection_duration }}{% if row.connection_minutes < 80 %} <span title="Tight connection">⚠️</span>{% endif %}
|
||||
<br><span style="font-size:0.75rem;color:#718096">Circle {{ row.circle_line_depart }} → STP {{ row.circle_arrive_checkin }}</span>
|
||||
<br><span class="text-xs text-muted">Circle {{ row.circle_line_depart }} → STP {{ row.circle_arrive_checkin }}</span>
|
||||
</td>
|
||||
<td style="padding:0.6rem 0.8rem;font-weight:600">
|
||||
<td class="font-bold">
|
||||
{{ row.depart_st_pancras }}
|
||||
{% if row.train_number %}<br><span style="font-size:0.75rem;font-weight:400;color:#718096">{% for part in row.train_number.split(' + ') %}<span style="white-space:nowrap">{{ part }}</span>{% if not loop.last %} + {% endif %}{% endfor %}</span>{% endif %}
|
||||
{% if row.train_number %}<br><span class="text-xs font-normal text-muted">{% for part in row.train_number.split(' + ') %}<span class="nowrap">{{ part }}</span>{% if not loop.last %} + {% endif %}{% endfor %}</span>{% endif %}
|
||||
</td>
|
||||
<td style="padding:0.6rem 0.8rem">
|
||||
<td>
|
||||
{{ row.arrive_destination }}
|
||||
<span style="font-weight:400;color:#718096;font-size:0.85em">(CET)</span>
|
||||
{% if row.eurostar_duration %}<br><span style="font-size:0.8rem;color:#718096;white-space:nowrap">({{ row.eurostar_duration }})</span>{% endif %}
|
||||
<span class="font-normal text-muted" style="font-size:0.85em">(CET)</span>
|
||||
{% if row.eurostar_duration %}<br><span class="text-sm text-muted nowrap">({{ row.eurostar_duration }})</span>{% endif %}
|
||||
</td>
|
||||
<td style="padding:0.6rem 0.8rem;white-space:nowrap">
|
||||
<td class="nowrap">
|
||||
{% if row.eurostar_price is not none %}
|
||||
£{{ "%.2f"|format(row.eurostar_price) }}
|
||||
{% if row.eurostar_seats is not none %}
|
||||
<br><span style="font-size:0.75rem;color:#718096">{{ row.eurostar_seats }} at this price</span>
|
||||
<br><span class="text-xs text-muted">{{ row.eurostar_seats }} at this price</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span style="color:#718096">–</span>
|
||||
<span class="text-muted">–</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td style="padding:0.6rem 0.8rem;font-weight:600;white-space:nowrap">
|
||||
<td class="font-bold nowrap">
|
||||
{% if row.total_minutes <= best_mins + 5 and trips | length > 1 %}
|
||||
<span style="color:#276749" title="Fastest journey">{{ row.total_duration }} ⚡</span>
|
||||
<span class="text-green" title="Fastest journey">{{ row.total_duration }} ⚡</span>
|
||||
{% elif row.total_minutes >= worst_mins - 5 and trips | length > 1 %}
|
||||
<span style="color:#c53030" title="Slowest journey">{{ row.total_duration }} 🐢</span>
|
||||
<span class="text-red" title="Slowest journey">{{ row.total_duration }} 🐢</span>
|
||||
{% else %}
|
||||
<span style="color:#00539f">{{ row.total_duration }}</span>
|
||||
<span class="text-blue">{{ row.total_duration }}</span>
|
||||
{% endif %}
|
||||
{% if row.total_price is not none %}
|
||||
<br><span style="font-size:0.8rem;font-weight:700;color:#276749">£{{ "%.2f"|format(row.total_price) }}{% if min_price is defined and max_price is defined %}{% if row.total_price <= min_price + 10 %} <span title="Cheapest journey">🪙</span>{% elif row.total_price >= max_price - 10 %} <span title="Most expensive journey">💸</span>{% endif %}{% endif %}</span>
|
||||
<br><span class="text-sm text-green" style="font-weight:700">£{{ "%.2f"|format(row.total_price) }}{% if min_price is defined and max_price is defined %}{% if row.total_price <= min_price + 10 %} <span title="Cheapest journey">🪙</span>{% elif row.total_price >= max_price - 10 %} <span title="Most expensive journey">💸</span>{% endif %}{% endif %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% else %}
|
||||
<td style="padding:0.6rem 0.8rem;font-weight:600">—</td>
|
||||
<td style="padding:0.6rem 0.8rem">—</td>
|
||||
<td class="col-transfer" style="padding:0.6rem 0.8rem">—</td>
|
||||
<td style="padding:0.6rem 0.8rem">n/a</td>
|
||||
<td style="padding:0.6rem 0.8rem;font-weight:600">
|
||||
<td class="font-bold">—</td>
|
||||
<td>—</td>
|
||||
<td class="col-transfer">—</td>
|
||||
<td>n/a</td>
|
||||
<td class="font-bold">
|
||||
{{ row.depart_st_pancras }}
|
||||
{% if row.train_number %}<br><span style="font-size:0.75rem;font-weight:400;color:#a0aec0">{% for part in row.train_number.split(' + ') %}<span style="white-space:nowrap">{{ part }}</span>{% if not loop.last %} + {% endif %}{% endfor %}</span>{% endif %}
|
||||
{% if row.train_number %}<br><span class="text-xs font-normal text-dimmed">{% for part in row.train_number.split(' + ') %}<span class="nowrap">{{ part }}</span>{% if not loop.last %} + {% endif %}{% endfor %}</span>{% endif %}
|
||||
</td>
|
||||
<td style="padding:0.6rem 0.8rem">
|
||||
<td>
|
||||
{{ row.arrive_destination }}
|
||||
<span style="font-weight:400;color:#a0aec0;font-size:0.85em">(CET)</span>
|
||||
{% if row.eurostar_duration %}<br><span style="font-size:0.8rem;color:#a0aec0;white-space:nowrap">({{ row.eurostar_duration }})</span>{% endif %}
|
||||
<span class="font-normal text-dimmed" style="font-size:0.85em">(CET)</span>
|
||||
{% if row.eurostar_duration %}<br><span class="text-sm text-dimmed nowrap">({{ row.eurostar_duration }})</span>{% endif %}
|
||||
</td>
|
||||
<td style="padding:0.6rem 0.8rem;white-space:nowrap">
|
||||
<td class="nowrap">
|
||||
{% if row.eurostar_price is not none %}
|
||||
<span style="color:#a0aec0">£{{ "%.2f"|format(row.eurostar_price) }}</span>
|
||||
<span class="text-dimmed">£{{ "%.2f"|format(row.eurostar_price) }}</span>
|
||||
{% if row.eurostar_seats is not none %}
|
||||
<br><span style="font-size:0.75rem;color:#a0aec0">{{ row.eurostar_seats }} at this price</span>
|
||||
<br><span class="text-xs text-dimmed">{{ row.eurostar_seats }} at this price</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span style="color:#a0aec0">–</span>
|
||||
<span class="text-dimmed">–</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td style="padding:0.6rem 0.8rem;font-weight:600">
|
||||
<span title="No same-day Bristol connection" style="color:#a0aec0;white-space:nowrap">Too early</span>
|
||||
<td class="font-bold">
|
||||
<span title="No same-day Bristol connection" class="text-dimmed nowrap">Too early</span>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
|
|
@ -222,7 +215,7 @@
|
|||
</table>
|
||||
</div>
|
||||
|
||||
<p style="margin-top:1rem;font-size:0.82rem;color:#718096">
|
||||
<p class="footnote">
|
||||
Paddington → St Pancras connection: {{ min_connection }}–{{ max_connection }} min.
|
||||
GWR walk-on single prices for Bristol Temple Meads → Paddington.
|
||||
Eurostar Standard prices are for 1 adult in GBP; always check
|
||||
|
|
@ -234,9 +227,9 @@
|
|||
</p>
|
||||
|
||||
{% else %}
|
||||
<div class="card" style="color:#4a5568;text-align:center;padding:3rem 2rem">
|
||||
<p style="font-size:1.1rem;margin:0 0 0.5rem">No valid journeys found.</p>
|
||||
<p style="font-size:0.9rem;margin:0">
|
||||
<div class="card empty-state">
|
||||
<p>No valid journeys found.</p>
|
||||
<p>
|
||||
{% if gwr_count == 0 and eurostar_count == 0 %}
|
||||
Could not retrieve train data. Check your network connection or try again.
|
||||
{% elif gwr_count == 0 %}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue