The loading page now opens an EventSource to a new ?render=stream endpoint. The server immediately sends a shell event (full page chrome: nav, filters, JS — no external fetches needed), then a section event per direction as each one's NR + Eurostar data arrives, and finally a done event with the summary and timetable-refresh URL. The client slots each section card into a placeholder and calls initialiseResultsPage() only after done, so fares and advance-fare streaming start at the right moment. Adds results_shell.html (shell template with empty JS data globals and mergeSectionData/finaliseResults hooks), results_section.html (extracted section card partial used by both the full and stream render paths), and helper functions _section_trip_fares() and _build_summary_html() to avoid duplicating fare-dict assembly between the two paths. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
118 lines
4.3 KiB
HTML
118 lines
4.3 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}{% if journey_type == 'inbound' %}{{ destination }} to {{ departure_station_name }} via Eurostar{% elif journey_type == 'return' %}{{ departure_station_name }} to {{ destination }} return via Eurostar{% else %}{{ departure_station_name }} to {{ destination }} via Eurostar{% endif %}{% endblock %}
|
|
{% block og_title %}{{ self.title()|trim }}{% endblock %}
|
|
{% block og_description %}Train options from {{ departure_station_name }} to {{ destination }} via Paddington, St Pancras, and Eurostar.{% endblock %}
|
|
{% block twitter_title %}{{ self.title()|trim }}{% endblock %}
|
|
{% block twitter_description %}Train options from {{ departure_station_name }} to {{ destination }} via Paddington, St Pancras, and Eurostar.{% endblock %}
|
|
{% block content %}
|
|
|
|
<p class="back-link">
|
|
<a href="{{ index_url }}">← New search</a>
|
|
</p>
|
|
|
|
<div class="card" style="margin-bottom:1.5rem">
|
|
<h2>
|
|
{% if journey_type == 'inbound' %}
|
|
{{ destination }} → {{ departure_station_name }}
|
|
{% elif journey_type == 'return' %}
|
|
{{ departure_station_name }} ↔ {{ destination }}
|
|
{% else %}
|
|
{{ departure_station_name }} → {{ destination }}
|
|
{% endif %}
|
|
</h2>
|
|
<p class="card-meta">
|
|
{{ travel_date_display }}{% if return_date_display %} to {{ return_date_display }}{% endif %}
|
|
</p>
|
|
<div class="loading-panel" role="status" aria-live="polite">
|
|
<span class="spinner" aria-hidden="true"></span>
|
|
<div>
|
|
<strong>Loading train times and fares</strong>
|
|
<p class="text-muted text-sm">Fetching National Rail, Eurostar, and fare data. Results will appear here as soon as they are ready.</p>
|
|
</div>
|
|
</div>
|
|
<noscript>
|
|
<p><a href="{{ full_results_url }}">Load results</a></p>
|
|
</noscript>
|
|
</div>
|
|
|
|
<script>
|
|
(function() {
|
|
var attempts = 0;
|
|
|
|
function runScripts(root) {
|
|
root.querySelectorAll('script').forEach(function(oldScript) {
|
|
var script = document.createElement('script');
|
|
for (var i = 0; i < oldScript.attributes.length; i++) {
|
|
var attr = oldScript.attributes[i];
|
|
script.setAttribute(attr.name, attr.value);
|
|
}
|
|
script.text = oldScript.text;
|
|
oldScript.parentNode.replaceChild(script, oldScript);
|
|
});
|
|
}
|
|
|
|
function showError() {
|
|
var panel = document.querySelector('.loading-panel');
|
|
if (panel) {
|
|
panel.innerHTML = '<div><strong>Could not load results</strong><p class="text-muted text-sm"><a href="{{ full_results_url }}">Try loading the full results page</a>.</p></div>';
|
|
}
|
|
}
|
|
|
|
function loadResults() {
|
|
if (!window.EventSource) {
|
|
window.location.href = {{ full_results_url|tojson }};
|
|
return;
|
|
}
|
|
|
|
attempts += 1;
|
|
var source = new EventSource({{ stream_url|tojson }});
|
|
|
|
source.onmessage = function(event) {
|
|
var msg;
|
|
try { msg = JSON.parse(event.data); } catch(e) { return; }
|
|
|
|
if (msg.type === 'shell') {
|
|
var doc = new DOMParser().parseFromString(msg.html, 'text/html');
|
|
document.title = doc.title;
|
|
var nextMain = doc.querySelector('main');
|
|
var currentMain = document.querySelector('main');
|
|
if (!nextMain || !currentMain) { source.close(); return; }
|
|
currentMain.innerHTML = nextMain.innerHTML;
|
|
runScripts(currentMain);
|
|
history.replaceState(null, '', window.location.href);
|
|
|
|
} else if (msg.type === 'section') {
|
|
var placeholder = document.getElementById('section-placeholder-' + msg.id);
|
|
if (placeholder) {
|
|
var tmp = document.createElement('div');
|
|
tmp.innerHTML = msg.html;
|
|
var card = tmp.firstElementChild;
|
|
if (card) placeholder.parentNode.replaceChild(card, placeholder);
|
|
}
|
|
if (typeof mergeSectionData === 'function') mergeSectionData(msg);
|
|
|
|
} else if (msg.type === 'done') {
|
|
if (typeof finaliseResults === 'function') finaliseResults(msg);
|
|
source.close();
|
|
|
|
} else if (msg.type === 'error') {
|
|
source.close();
|
|
showError();
|
|
}
|
|
};
|
|
|
|
source.onerror = function() {
|
|
source.close();
|
|
if (attempts < 3) {
|
|
window.setTimeout(loadResults, attempts * 2000);
|
|
} else {
|
|
showError();
|
|
}
|
|
};
|
|
}
|
|
|
|
loadResults();
|
|
})();
|
|
</script>
|
|
|
|
{% endblock %}
|