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:
parent
a88b19fa4c
commit
1bc7631863
5 changed files with 695 additions and 122 deletions
|
|
@ -1,8 +1,75 @@
|
|||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<style>
|
||||
/* ── Calendar ───────────────────────────────────────────────────── */
|
||||
.cal-wrap { margin-top: .5rem; }
|
||||
|
||||
.cal-nav {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
margin-bottom: .9rem;
|
||||
}
|
||||
.cal-nav-btn {
|
||||
flex-shrink: 0; width: 2rem; height: 2rem;
|
||||
border: 1px solid #e2e8f0; border-radius: 50%;
|
||||
background: #fff; cursor: pointer;
|
||||
font-size: 1.3rem; line-height: 1; color: #4a5568;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
transition: background .12s;
|
||||
}
|
||||
.cal-nav-btn:hover:not(:disabled) { background: #f0f4f8; }
|
||||
.cal-nav-btn:disabled { opacity: .3; cursor: default; }
|
||||
|
||||
#cal-titles {
|
||||
display: flex; flex: 1; gap: 0;
|
||||
margin: 0 .5rem;
|
||||
font-weight: 600; font-size: .9rem; color: #1a202c;
|
||||
}
|
||||
#cal-titles span { flex: 1; text-align: center; }
|
||||
|
||||
#cal-months { display: flex; gap: 2rem; }
|
||||
.cal-month { flex: 1; min-width: 0; }
|
||||
|
||||
.cal-grid {
|
||||
display: grid; grid-template-columns: repeat(7, 1fr);
|
||||
}
|
||||
.cal-dow {
|
||||
text-align: center; font-size: .72rem; font-weight: 600;
|
||||
color: #718096; padding: .4rem 0 .25rem;
|
||||
}
|
||||
.cal-cell { padding: 1px 0; } /* background set inline for range */
|
||||
|
||||
.cal-btn {
|
||||
width: 100%; aspect-ratio: 1;
|
||||
max-width: 2.4rem; max-height: 2.4rem;
|
||||
border: none; background: none; cursor: pointer;
|
||||
font-size: .875rem; border-radius: 50%;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
margin: 0 auto;
|
||||
transition: background .1s;
|
||||
font-family: inherit; color: #1a202c;
|
||||
}
|
||||
.cal-btn:hover:not(:disabled):not(.cal-selected) { background: #e2e8f0; }
|
||||
.cal-btn.cal-past { color: #d1d5db !important; cursor: default; }
|
||||
.cal-btn.cal-today { color: #00539f; font-weight: 700; }
|
||||
.cal-btn.cal-selected {
|
||||
background: #00539f !important; color: #fff !important; font-weight: 600;
|
||||
}
|
||||
/* subtly darken the in-range hover */
|
||||
.cal-cell.cal-in-range .cal-btn:hover:not(:disabled) { background: #bfdbfe; }
|
||||
|
||||
.cal-hint { margin-top: .75rem; font-size: .88rem; color: #4a5568; min-height: 1.4rem; }
|
||||
.cal-cta { color: #00539f; font-weight: 600; }
|
||||
|
||||
@media (max-width: 620px) {
|
||||
#cal-months { flex-direction: column; gap: 1.25rem; }
|
||||
#cal-titles { gap: 0; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="card">
|
||||
<h2>Plan your journey</h2>
|
||||
<form method="get" action="{{ url_for('search') }}" id="search-form">
|
||||
|
||||
<div class="form-group-lg">
|
||||
<label for="station_crs" class="field-label">Departure point</label>
|
||||
<select id="station_crs" name="station_crs" class="form-control">
|
||||
|
|
@ -16,25 +83,16 @@
|
|||
<span class="field-label">Journey type</span>
|
||||
<div class="destination-grid" role="radiogroup" aria-label="Journey type">
|
||||
<div class="destination-option">
|
||||
<input type='radio' id="journey-outbound" name="journey_type" value="outbound" checked>
|
||||
<label for="journey-outbound">
|
||||
<strong>Out</strong>
|
||||
<span>UK to Europe</span>
|
||||
</label>
|
||||
<input type="radio" id="journey-outbound" name="journey_type" value="outbound" checked>
|
||||
<label for="journey-outbound"><strong>Out</strong><span>UK to Europe</span></label>
|
||||
</div>
|
||||
<div class="destination-option">
|
||||
<input type='radio' id="journey-inbound" name="journey_type" value="inbound">
|
||||
<label for="journey-inbound">
|
||||
<strong>Back</strong>
|
||||
<span>Europe to UK</span>
|
||||
</label>
|
||||
<input type="radio" id="journey-inbound" name="journey_type" value="inbound">
|
||||
<label for="journey-inbound"><strong>Back</strong><span>Europe to UK</span></label>
|
||||
</div>
|
||||
<div class="destination-option">
|
||||
<input type='radio' id="journey-return" name="journey_type" value="return">
|
||||
<label for="journey-return">
|
||||
<strong>Return</strong>
|
||||
<span>Out and back</span>
|
||||
</label>
|
||||
<input type="radio" id="journey-return" name="journey_type" value="return">
|
||||
<label for="journey-return"><strong>Return</strong><span>Out and back</span></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -43,40 +101,31 @@
|
|||
<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', '') %}
|
||||
{% set city = name.replace(' Gare du Nord','').replace(' Centraal','').replace(' Midi','').replace(' Europe','') %}
|
||||
<div class="destination-option">
|
||||
<input
|
||||
type="radio"
|
||||
id="destination-{{ slug }}"
|
||||
name="destination"
|
||||
value="{{ slug }}"
|
||||
{% if loop.first %}checked{% endif %}
|
||||
required>
|
||||
<label for="destination-{{ slug }}">
|
||||
<strong>{{ city }}</strong>
|
||||
<span>{{ name }}</span>
|
||||
</label>
|
||||
<input type="radio" id="dest-{{ slug }}" name="destination" value="{{ slug }}"
|
||||
{% if loop.first %}checked{% endif %} required>
|
||||
<label for="dest-{{ slug }}"><strong>{{ city }}</strong><span>{{ name }}</span></label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Calendar date picker ──────────────────────────────────────── -->
|
||||
<div class="form-group-lg">
|
||||
<label for="travel_date" class="field-label">
|
||||
Outbound / single date
|
||||
</label>
|
||||
<input type="date" id="travel_date" name="travel_date" required
|
||||
min="{{ today }}" value="{{ today }}"
|
||||
class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="form-group-lg" id="return-date-group">
|
||||
<label for="return_date" class="field-label">
|
||||
Return date
|
||||
</label>
|
||||
<input type="date" id="return_date" name="return_date"
|
||||
min="{{ today }}" value="{{ default_return_date }}"
|
||||
class="form-control" disabled>
|
||||
<span class="field-label">Travel dates</span>
|
||||
<div class="cal-wrap">
|
||||
<div class="cal-nav">
|
||||
<button type="button" id="cal-prev" class="cal-nav-btn" aria-label="Previous month">‹</button>
|
||||
<div id="cal-titles"></div>
|
||||
<button type="button" id="cal-next" class="cal-nav-btn" aria-label="Next month">›</button>
|
||||
</div>
|
||||
<div id="cal-months"></div>
|
||||
<div id="cal-hint" class="cal-hint"></div>
|
||||
</div>
|
||||
<!-- Hidden inputs submitted with the form -->
|
||||
<input type="hidden" id="travel_date" name="travel_date">
|
||||
<input type="hidden" id="return_date" name="">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
|
|
@ -101,60 +150,330 @@
|
|||
</select>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-primary">
|
||||
Search journeys
|
||||
</button>
|
||||
<button type="submit" class="btn-primary">Search journeys</button>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
var form = document.getElementById('search-form');
|
||||
var travelDate = document.getElementById('travel_date');
|
||||
var returnDate = document.getElementById('return_date');
|
||||
var returnRadio = document.getElementById('journey-return');
|
||||
var journeyRadios = document.querySelectorAll('input[name="journey_type"]');
|
||||
var returnDateName = returnDate.name;
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
function currentJourneyType() {
|
||||
var checked = document.querySelector('input[name="journey_type"]:checked');
|
||||
return checked ? checked.value : 'outbound';
|
||||
const TODAY = new Date('{{ today }}T00:00:00');
|
||||
|
||||
/* ── state ───────────────────────────────────────────────────────── */
|
||||
let viewYear = TODAY.getFullYear();
|
||||
let viewMonth = TODAY.getMonth(); // 0-based
|
||||
let outDate = new Date(TODAY); // default: today selected
|
||||
let retDate = null;
|
||||
let hoverDate = null;
|
||||
let isReturn = false;
|
||||
let retPhase = false; // true while waiting for return-date click
|
||||
|
||||
/* ── helpers ─────────────────────────────────────────────────────── */
|
||||
|
||||
function toYMD(d) {
|
||||
if (!d) return '';
|
||||
return d.getFullYear() + '-' +
|
||||
String(d.getMonth() + 1).padStart(2, '0') + '-' +
|
||||
String(d.getDate()).padStart(2, '0');
|
||||
}
|
||||
|
||||
function dispDate(d) {
|
||||
return d ? d.toLocaleDateString('en-GB', { weekday: 'short', day: 'numeric', month: 'short' }) : '';
|
||||
}
|
||||
|
||||
function sameDay(a, b) {
|
||||
return a && b && a.toDateString() === b.toDateString();
|
||||
}
|
||||
|
||||
function advMonth(y, m, delta) {
|
||||
m += delta;
|
||||
while (m < 0) { m += 12; y--; }
|
||||
while (m > 11) { m -= 12; y++; }
|
||||
return [y, m];
|
||||
}
|
||||
|
||||
/* ── sync hidden inputs ──────────────────────────────────────────── */
|
||||
|
||||
function syncInputs() {
|
||||
document.getElementById('travel_date').value = toYMD(outDate);
|
||||
var ri = document.getElementById('return_date');
|
||||
if (isReturn && retDate) {
|
||||
ri.value = toYMD(retDate);
|
||||
ri.name = 'return_date';
|
||||
} else {
|
||||
ri.value = '';
|
||||
ri.name = '';
|
||||
}
|
||||
}
|
||||
|
||||
function syncReturnDate() {
|
||||
var isReturn = currentJourneyType() === 'return';
|
||||
returnDate.name = isReturn ? returnDateName : '';
|
||||
returnDate.disabled = !isReturn;
|
||||
}
|
||||
/* ── day click ───────────────────────────────────────────────────── */
|
||||
|
||||
function updateReturnMin() {
|
||||
if (travelDate.value) {
|
||||
returnDate.min = travelDate.value;
|
||||
if (returnDate.value < travelDate.value) {
|
||||
returnDate.value = travelDate.value;
|
||||
}
|
||||
function onDayClick(d) {
|
||||
if (!isReturn) {
|
||||
outDate = d;
|
||||
} else if (!outDate || !retPhase) {
|
||||
/* start fresh: set outbound, await return */
|
||||
outDate = d;
|
||||
retDate = null;
|
||||
retPhase = true;
|
||||
} else {
|
||||
/* selecting return date */
|
||||
if (sameDay(d, outDate)) {
|
||||
/* tapped same day → reset */
|
||||
outDate = null;
|
||||
retDate = null;
|
||||
retPhase = false;
|
||||
} else if (d < outDate) {
|
||||
/* earlier than outbound → new outbound, keep retPhase */
|
||||
outDate = d;
|
||||
} else {
|
||||
retDate = d;
|
||||
retPhase = false;
|
||||
}
|
||||
}
|
||||
hoverDate = null;
|
||||
syncInputs();
|
||||
render();
|
||||
}
|
||||
|
||||
// Clicking anywhere in the return date group (including label/disabled input)
|
||||
// activates return journey type and enables the field
|
||||
document.getElementById('return-date-group').addEventListener('click', function() {
|
||||
if (returnDate.disabled) {
|
||||
returnRadio.checked = true;
|
||||
syncReturnDate();
|
||||
returnDate.focus();
|
||||
/* ── render ──────────────────────────────────────────────────────── */
|
||||
|
||||
function render() {
|
||||
var fmt = { month: 'long', year: 'numeric' };
|
||||
var t0 = new Date(viewYear, viewMonth, 1).toLocaleDateString('en-GB', fmt);
|
||||
var titleHtml = '<span>' + t0 + '</span>';
|
||||
if (isReturn) {
|
||||
var next = advMonth(viewYear, viewMonth, 1);
|
||||
var t1 = new Date(next[0], next[1], 1).toLocaleDateString('en-GB', fmt);
|
||||
titleHtml += '<span>' + t1 + '</span>';
|
||||
}
|
||||
document.getElementById('cal-titles').innerHTML = titleHtml;
|
||||
|
||||
document.getElementById('cal-prev').disabled =
|
||||
(viewYear === TODAY.getFullYear() && viewMonth === TODAY.getMonth());
|
||||
|
||||
renderMonths();
|
||||
renderHint();
|
||||
}
|
||||
|
||||
function renderMonths() {
|
||||
var cont = document.getElementById('cal-months');
|
||||
cont.innerHTML = '';
|
||||
var n = isReturn ? 2 : 1;
|
||||
for (var i = 0; i < n; i++) {
|
||||
var ym = advMonth(viewYear, viewMonth, i);
|
||||
cont.appendChild(buildMonth(ym[0], ym[1]));
|
||||
}
|
||||
}
|
||||
|
||||
function buildMonth(year, month) {
|
||||
/* effective range (includes hover preview) */
|
||||
var rangeA = null, rangeB = null;
|
||||
if (outDate && retDate) {
|
||||
rangeA = outDate < retDate ? outDate : retDate;
|
||||
rangeB = outDate < retDate ? retDate : outDate;
|
||||
} else if (outDate && retPhase && hoverDate && hoverDate > outDate) {
|
||||
rangeA = outDate;
|
||||
rangeB = hoverDate;
|
||||
}
|
||||
|
||||
var wrap = document.createElement('div');
|
||||
wrap.className = 'cal-month';
|
||||
|
||||
var grid = document.createElement('div');
|
||||
grid.className = 'cal-grid';
|
||||
|
||||
/* day-of-week headers, Mon first */
|
||||
['Mo','Tu','We','Th','Fr','Sa','Su'].forEach(function (lbl) {
|
||||
var el = document.createElement('div');
|
||||
el.className = 'cal-dow';
|
||||
el.textContent = lbl;
|
||||
grid.appendChild(el);
|
||||
});
|
||||
|
||||
/* offset: Mon=0 … Sun=6 */
|
||||
var firstDay = new Date(year, month, 1);
|
||||
var offset = firstDay.getDay() - 1;
|
||||
if (offset < 0) offset = 6;
|
||||
|
||||
var lastDate = new Date(year, month + 1, 0).getDate();
|
||||
|
||||
/* leading empty cells */
|
||||
for (var p = 0; p < offset; p++) {
|
||||
grid.appendChild(Object.assign(document.createElement('div'), { className: 'cal-cell' }));
|
||||
}
|
||||
|
||||
/* day cells */
|
||||
for (var d = 1; d <= lastDate; d++) {
|
||||
var date = new Date(year, month, d);
|
||||
var past = date < TODAY;
|
||||
var isTod = sameDay(date, TODAY);
|
||||
var isOut = sameDay(date, outDate);
|
||||
var isRet = sameDay(date, retDate);
|
||||
var col = (d - 1 + offset) % 7; /* 0=Mon, 6=Sun */
|
||||
var inRange = rangeA && rangeB && date > rangeA && date < rangeB;
|
||||
var isRA = rangeA && sameDay(date, rangeA);
|
||||
var isRB = rangeB && sameDay(date, rangeB);
|
||||
|
||||
var cell = document.createElement('div');
|
||||
cell.className = 'cal-cell' + (inRange ? ' cal-in-range' : '');
|
||||
|
||||
/* range background: gradient at endpoints, solid between */
|
||||
if (isRA && rangeB) {
|
||||
cell.style.background = col < 6
|
||||
? 'linear-gradient(to right,transparent 50%,#dbeafe 50%)'
|
||||
: 'transparent'; /* Sunday: no gradient needed */
|
||||
} else if (isRB && rangeA) {
|
||||
cell.style.background = col > 0
|
||||
? 'linear-gradient(to left,transparent 50%,#dbeafe 50%)'
|
||||
: 'transparent'; /* Monday: no gradient needed */
|
||||
} else if (inRange) {
|
||||
cell.style.background = '#dbeafe';
|
||||
}
|
||||
|
||||
var btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
var cls = 'cal-btn';
|
||||
if (past) cls += ' cal-past';
|
||||
else if (isTod && !isOut && !isRet) cls += ' cal-today';
|
||||
if (isOut || isRet) cls += ' cal-selected';
|
||||
btn.className = cls;
|
||||
btn.textContent = d;
|
||||
btn.disabled = past;
|
||||
btn.setAttribute('aria-label',
|
||||
date.toLocaleDateString('en-GB', { day: 'numeric', month: 'long', year: 'numeric' }));
|
||||
|
||||
cell.setAttribute('data-date', toYMD(date));
|
||||
cell.setAttribute('data-col', col);
|
||||
|
||||
if (!past) {
|
||||
(function (dt) {
|
||||
btn.addEventListener('click', function () { onDayClick(dt); });
|
||||
btn.addEventListener('mouseenter', function () {
|
||||
if (isReturn && retPhase && (!hoverDate || !sameDay(hoverDate, dt))) {
|
||||
hoverDate = dt;
|
||||
applyHoverStyles(outDate, dt);
|
||||
}
|
||||
});
|
||||
})(new Date(date));
|
||||
}
|
||||
|
||||
cell.appendChild(btn);
|
||||
grid.appendChild(cell);
|
||||
}
|
||||
|
||||
grid.addEventListener('mouseleave', function () {
|
||||
if (hoverDate) {
|
||||
hoverDate = null;
|
||||
applyHoverStyles(
|
||||
outDate && retDate ? (outDate < retDate ? outDate : retDate) : null,
|
||||
outDate && retDate ? (outDate < retDate ? retDate : outDate) : null
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
travelDate.addEventListener('change', updateReturnMin);
|
||||
wrap.appendChild(grid);
|
||||
return wrap;
|
||||
}
|
||||
|
||||
journeyRadios.forEach(function(radio) {
|
||||
radio.addEventListener('change', syncReturnDate);
|
||||
/* ── hover range: update cell styles in-place (no DOM rebuild) ───── */
|
||||
|
||||
function applyHoverStyles(rangeA, rangeB) {
|
||||
document.querySelectorAll('.cal-cell[data-date]').forEach(function (cell) {
|
||||
var d = new Date(cell.getAttribute('data-date') + 'T00:00:00');
|
||||
var col = parseInt(cell.getAttribute('data-col'), 10);
|
||||
var inRange = rangeA && rangeB && d > rangeA && d < rangeB;
|
||||
var isRA = rangeA && rangeB && sameDay(d, rangeA);
|
||||
var isRB = rangeA && rangeB && sameDay(d, rangeB);
|
||||
cell.classList.toggle('cal-in-range', !!inRange);
|
||||
if (isRA) {
|
||||
cell.style.background = col < 6
|
||||
? 'linear-gradient(to right,transparent 50%,#dbeafe 50%)' : '';
|
||||
} else if (isRB) {
|
||||
cell.style.background = col > 0
|
||||
? 'linear-gradient(to left,transparent 50%,#dbeafe 50%)' : '';
|
||||
} else if (inRange) {
|
||||
cell.style.background = '#dbeafe';
|
||||
} else {
|
||||
cell.style.background = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
form.addEventListener('submit', syncReturnDate);
|
||||
function renderHint() {
|
||||
var el = document.getElementById('cal-hint');
|
||||
if (!el) return;
|
||||
if (!isReturn) {
|
||||
el.innerHTML = outDate
|
||||
? 'Date: <strong>' + dispDate(outDate) + '</strong>'
|
||||
: '<span class="cal-cta">Select a travel date</span>';
|
||||
} else if (!outDate) {
|
||||
el.innerHTML = '<span class="cal-cta">Select departure date</span>';
|
||||
} else if (retPhase) {
|
||||
el.innerHTML = 'Depart: <strong>' + dispDate(outDate) +
|
||||
'</strong> · <span class="cal-cta">Now select return date</span>';
|
||||
} else {
|
||||
el.innerHTML = 'Depart: <strong>' + dispDate(outDate) +
|
||||
'</strong> · Return: <strong>' + dispDate(retDate) + '</strong>';
|
||||
}
|
||||
}
|
||||
|
||||
updateReturnMin();
|
||||
syncReturnDate();
|
||||
})();
|
||||
/* ── navigation ──────────────────────────────────────────────────── */
|
||||
|
||||
document.getElementById('cal-prev').addEventListener('click', function () {
|
||||
var ym = advMonth(viewYear, viewMonth, -1);
|
||||
viewYear = ym[0]; viewMonth = ym[1];
|
||||
render();
|
||||
});
|
||||
document.getElementById('cal-next').addEventListener('click', function () {
|
||||
var ym = advMonth(viewYear, viewMonth, 1);
|
||||
viewYear = ym[0]; viewMonth = ym[1];
|
||||
render();
|
||||
});
|
||||
|
||||
/* ── journey type ────────────────────────────────────────────────── */
|
||||
|
||||
document.querySelectorAll('input[name="journey_type"]').forEach(function (r) {
|
||||
r.addEventListener('change', function () {
|
||||
var wasReturn = isReturn;
|
||||
isReturn = this.value === 'return';
|
||||
if (isReturn && !wasReturn) {
|
||||
retDate = null;
|
||||
retPhase = outDate !== null;
|
||||
} else if (!isReturn) {
|
||||
retDate = null;
|
||||
retPhase = false;
|
||||
}
|
||||
syncInputs();
|
||||
render();
|
||||
});
|
||||
});
|
||||
|
||||
/* ── form submit ─────────────────────────────────────────────────── */
|
||||
|
||||
document.getElementById('search-form').addEventListener('submit', function (e) {
|
||||
if (!outDate) {
|
||||
e.preventDefault();
|
||||
alert('Please select a travel date.');
|
||||
return;
|
||||
}
|
||||
if (isReturn && !retDate) {
|
||||
e.preventDefault();
|
||||
alert('Please select a return date.');
|
||||
return;
|
||||
}
|
||||
syncInputs();
|
||||
});
|
||||
|
||||
/* ── init ────────────────────────────────────────────────────────── */
|
||||
|
||||
var initType = document.querySelector('input[name="journey_type"]:checked');
|
||||
isReturn = initType && initType.value === 'return';
|
||||
retPhase = isReturn && outDate !== null;
|
||||
|
||||
syncInputs();
|
||||
render();
|
||||
}());
|
||||
</script>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue