paddington-eurostar/templates/index.html

480 lines
18 KiB
HTML

{% 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">
{% for name, crs in stations %}
<option value="{{ crs }}" {% if crs == 'BRI' %}selected{% endif %}>{{ name }} ({{ crs }})</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<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>
</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>
</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>
</div>
</div>
</div>
<div class="form-group">
<span class="field-label">Eurostar destination</span>
<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-{{ destination.slug }}" name="destination" value="{{ destination.slug }}"
{% if loop.first %}checked{% endif %} required>
<label for="dest-{{ destination.slug }}"><strong>{{ destination.city }}</strong><span>{{ destination.destination }}</span></label>
</div>
{% endfor %}
</div>
</div>
<!-- ── Calendar date picker ──────────────────────────────────────── -->
<div class="form-group-lg">
<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">&#8249;</button>
<div id="cal-titles"></div>
<button type="button" id="cal-next" class="cal-nav-btn" aria-label="Next month">&#8250;</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-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">
<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>
</form>
<script>
(function () {
'use strict';
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 = '';
}
}
/* ── day click ───────────────────────────────────────────────────── */
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();
}
/* ── 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
);
}
});
wrap.appendChild(grid);
return wrap;
}
/* ── 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 = '';
}
});
}
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> &nbsp;&middot;&nbsp; <span class="cal-cta">Now select return date</span>';
} else {
el.innerHTML = 'Depart: <strong>' + dispDate(outDate) +
'</strong> &nbsp;&middot;&nbsp; Return: <strong>' + dispDate(retDate) + '</strong>';
}
}
/* ── 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 %}