agenda/templates/conference_detail.html

152 lines
13 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "base.html" %}
{% from "macros.html" import trip_link with context %}
{% block title %}{{ conf.name }} - Edward Betts{% endblock %}
{% block style %}
<style>
.conference-page h1 { overflow-wrap: anywhere; }
.conference-details dd { margin-bottom: 1rem; }
.flight-table td { vertical-align: middle; padding: .8rem .5rem; }
.flight-date { display: block; color: var(--bs-secondary-color, #6c757d); font-size: .85rem; white-space: nowrap; }
.flight-time { display: block; font-size: 1.1rem; font-weight: 600; }
.flight-price a { font-weight: 600; white-space: nowrap; }
@media (max-width: 575.98px) {
.conference-page { padding-left: 1rem; padding-right: 1rem; }
.conference-page h1 { font-size: 1.8rem; }
.conference-details { display: grid; grid-template-columns: minmax(0, 2fr) minmax(0, 3fr); gap: .5rem 1rem; margin: 0; }
.conference-details dt, .conference-details dd { width: auto; padding: 0; margin: 0; font-size: .9rem; }
#flight-search { flex-direction: column; align-items: stretch !important; gap: .75rem !important; }
#flight-search input { max-width: none !important; }
.flight-table, .flight-table tbody { display: block; width: 100%; }
.flight-table thead { display: none; }
.flight-table tr { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); grid-template-areas: "route price" "departure arrival" "stops duration"; gap: .75rem 1rem; border: 1px solid #dee2e6; border-radius: .65rem; padding: 1rem; margin-bottom: .75rem; }
.flight-table td { display: block; min-width: 0; padding: 0; border: 0; box-shadow: none; background: transparent; overflow-wrap: anywhere; }
.flight-table td::before { content: attr(data-label); display: block; font-size: .75rem; color: var(--bs-secondary-color, #6c757d); margin-bottom: .2rem; }
.flight-departure { grid-area: departure; }
.flight-arrival { grid-area: arrival; }
.flight-route { grid-area: route; font-size: .9rem; }
.flight-stops { grid-area: stops; font-size: .85rem; }
.flight-duration { grid-area: duration; font-size: .85rem; }
.flight-price { grid-area: price; text-align: right; font-size: 1.15rem; }
.flight-price a { white-space: normal; }
.flight-route::before, .flight-price::before, .flight-stops::before, .flight-duration::before { display: none !important; }
.flight-date { font-size: .8rem; white-space: normal; }
}
</style>
{% endblock %}
{% block content %}
<div class="container py-4 conference-page">
<a href="{{ url_for('conference_list') }}">← Conferences</a>
<h1 class="mt-3">{{ conf.name }}</h1>
<p class="lead">{{ conf.display_date }}{% if not conf.has_exact_dates %} <span class="badge text-bg-secondary">{{ conf.date_status }}</span>{% endif %}</p>
{% if conf.url %}<p><a href="{{ conf.url }}" class="btn btn-outline-primary">Conference website ↗</a></p>{% endif %}
<div class="row g-4">
<div class="col-lg-6">
<h2 class="h4">Location and venue</h2>
<p>{{ conf.location }}{% if conf.country and get_country(conf.country) %} · {{ get_country(conf.country).flag }} {{ get_country(conf.country).name }}{% endif %}</p>
{% if conf.venue %}<p class="fw-semibold mb-1">{{ conf.venue }}</p>{% endif %}
{% if conf.address %}<p>{{ conf.address }}</p>{% endif %}
{% if conf.latitude is defined and conf.longitude is defined and conf.latitude is not none and conf.longitude is not none %}
<p><a href="https://www.openstreetmap.org/?mlat={{ conf.latitude }}&amp;mlon={{ conf.longitude }}#map=16/{{ conf.latitude }}/{{ conf.longitude }}">View venue on map</a> <span class="text-muted">({{ conf.latitude }}, {{ conf.longitude }})</span></p>
{% elif conf.venue or conf.address %}
<p><a href="https://www.google.com/maps/search/?api=1&amp;query={{ ((conf.venue or '') ~ ' ' ~ (conf.address or '') ~ ' ' ~ conf.location)|urlencode }}">Find venue on map</a></p>
{% endif %}
{% if conf.series_detail %}<p>Series: <a href="{{ url_for('conference_series_page', series_id=conf.series) }}">{{ conf.series_detail.name }}</a></p>{% endif %}
{% if conf.linked_trip %}<p>{{ trip_link(conf.linked_trip) }}</p>{% endif %}
</div>
<div class="col-lg-6">
<h2 class="h4">Conference details</h2>
<dl class="row conference-details">
{% for label, value in fields %}
<dt class="col-sm-4">{{ label }}</dt>
<dd class="col-sm-8 text-break">{% if value is sameas true %}Yes{% elif value is sameas false %}No{% elif value is string and (value.startswith('https://') or value.startswith('http://')) %}<a href="{{ value }}">{{ value }}</a>{% else %}{{ value|conference_date }}{% endif %}</dd>
{% endfor %}
</dl>
</div>
</div>
<section class="card mt-4"><div class="card-body">
<h2 class="h4">Flights</h2>
{% if cooldown %}<p class="alert alert-warning">{{ cooldown }}</p>{% endif %}
{% if eligible %}
{% if conf.date_status == 'tentative' %}<p class="text-muted">Using tentative conference dates: {{ conf.start_date|conference_date }} – {{ conf.end_date|conference_date }}.</p>{% endif %}
<p>{% if short_haul %}Direct flights from Bristol first. Start the day before and return the day after; if no flights are found, check progressively further dates, up to four days each side. Each direction stops at the nearest date with flights. London is checked if either direction has no Bristol flights.{% else %}Flights from London, departing two days before and returning the day after the conference. If none are found, try departing one day earlier. Direct or one-stop flights first.{% endif %}</p>
<form method="post" class="d-flex flex-wrap align-items-end gap-3" id="flight-search">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div><label for="airport" class="form-label">Destination city or airport</label>
<input id="airport" name="airport" value="{{ airport }}" class="form-control" list="airport-suggestions" placeholder="Copenhagen, Denmark or CPH" maxlength="200" autocomplete="off" required style="max-width: 24rem" aria-describedby="airport-help"><datalist id="airport-suggestions"></datalist></div>
<button class="btn btn-primary" type="submit"{% if cooldown %} disabled{% endif %}>{{ 'Update flights' if cached else 'Look up flights' }}</button>
</form>
{% if resolved_airport %}<p class="small mt-2 mb-1">Destination: <strong>{{ resolved_name }}{% if resolved_name != resolved_airport %} ({{ resolved_airport }}){% endif %}</strong></p>{% endif %}
{% if default_google_flights and (not cached or not cached.searches) %}
<p class="mt-3"><a class="btn btn-outline-secondary" href="{{ default_google_flights.url }}" target="_blank" rel="noopener">Open in Google Flights ↗</a><span class="small text-muted d-block mt-1">Round trip: {{ default_google_flights.departure|conference_date }} – {{ default_google_flights.return|conference_date }}</span></p>
{% endif %}
{% if resolution_error %}<p class="text-danger small mt-2">{{ resolution_error }}</p>{% endif %}
<p id="airport-help" class="text-muted small mt-2">{% if suggested %}Suggested {{ suggested }}: {{ airport_reason }}. Enter a city, airport name or IATA code to choose another airport.{% else %}{{ airport_reason }}{% endif %}</p>
<p class="small text-muted">Direct flights first, with British Airways preferred within each stop count. Other airlines are included. Two-stop flights from London are checked only if no direct or one-stop options are found.</p>
<p class="small text-muted">Prices in GBP for one adult, economy. Each direction is priced separately. Results are cached for six hours; updates within that time reuse the cache.</p>
{% else %}
<p class="text-muted">Flight lookup is available for future conferences overseas or in Belfast with confirmed or tentative dates and an in-person venue.</p>
{% endif %}
{% if cached %}
<p>Last updated: <time class="cache-updated" datetime="{{ cached.updated_at }}">{{ cached.updated_at|conference_date }} UTC</time></p>
{% if cached.error %}<div class="alert alert-warning"><p class="mb-0">{{ cached.error }}{% if cached.searches %} Showing previously cached results.{% endif %}</p>{% if cached.error_details %}<p class="small text-break mt-2 mb-0">{{ cached.error_details }}</p>{% endif %}</div>{% endif %}
{% for search in cached.searches %}
<h3 class="h5 mt-4">{{ 'Bristol (BRS), direct only' if search.origin == 'BRS' else 'London (LON)' }} → {{ resolved_name or cached.destination }}{% if resolved_name and resolved_name != cached.destination %} ({{ cached.destination }}){% endif %}</h3>
<p><a class="btn btn-outline-secondary" href="{{ search.google_flights.url }}" target="_blank" rel="noopener">Open in Google Flights ↗</a><span class="small text-muted d-block mt-1">Round trip: {{ search.google_flights.departure|conference_date }} – {{ search.google_flights.return|conference_date }}</span></p>
{% for label, rows, days in [('Outbound', search.outbound, search.outbound_dates), ('Return', search.inbound, search.inbound_dates)] %}
<h4 class="h6">{{ label }} · one-way fares</h4>
{% if rows %}<p class="small text-muted mb-2">Departure and arrival times are local to each airport.</p>{% endif %}
{% if rows %}<div class="table-responsive"><table class="table table-sm flight-table">
<thead><tr><th>Departure</th><th>Arrival</th><th>Flight</th><th>Stops</th><th>Duration</th><th>Price</th></tr></thead><tbody>
{% for flight in rows %}<tr>
<td class="flight-departure" data-label="Departure"><time datetime="{{ flight.departure }}"><span class="flight-date">{{ flight.departure|conference_date('date') }}</span><span class="flight-time">{{ flight.departure|conference_date('time') }}</span></time></td>
<td class="flight-arrival" data-label="Arrival"><time datetime="{{ flight.arrival }}"><span class="flight-date">{{ flight.arrival|conference_date('date') }}</span><span class="flight-time">{{ flight.arrival|conference_date('time') }}</span></time></td>
<td class="flight-route" data-label="Flight">{% for leg in flight.legs %}{{ leg.airline }} {{ leg.number }} ({{ leg.origin }}–{{ leg.destination }}){% if not loop.last %}<br>{% endif %}{% endfor %}</td>
<td class="flight-stops" data-label="Stops">{{ 'Direct' if flight.stops == 0 else flight.stops|string ~ (' stop' if flight.stops == 1 else ' stops') }}</td>
<td class="flight-duration" data-label="Duration">{% if flight.duration is defined %}{{ flight.duration // 60 }}h {{ flight.duration % 60 }}m{% else %}—{% endif %}</td>
<td class="flight-price" data-label="Price"><a href="{{ flight.url }}">{% if flight.price is not none %}{{ '£' if flight.currency == 'GBP' else flight.currency ~ ' ' }}{{ '%.2f'|format(flight.price) }}{% else %}Check price{% endif %} ↗</a></td>
</tr>{% endfor %}
</tbody></table></div>{% else %}<p class="text-muted">{{ 'No direct flights found' if search.origin == 'BRS' else 'No suitable flights found' }}{% if days %} for departures on {{ days|map('conference_date')|join(', ') }}{% else %}: no eligible departure dates{% endif %}.{% if search.origin == 'BRS' and days %} Direct flights may not operate every day.{% endif %}</p>{% endif %}
{% endfor %}
{% endfor %}
{% endif %}
</div></section>
</div>
<script>
const airportInput = document.getElementById('airport');
const suggestions = document.getElementById('airport-suggestions');
let suggestionTimer;
let suggestionRequest;
if (airportInput) airportInput.addEventListener('input', function () {
clearTimeout(suggestionTimer);
if (suggestionRequest) suggestionRequest.abort();
suggestions.replaceChildren();
const query = airportInput.value.trim();
if (query.length < 2) return;
suggestionTimer = setTimeout(async function () {
suggestionRequest = new AbortController();
try {
const response = await fetch({{ url_for('conference.airport_search')|tojson }} + '?q=' + encodeURIComponent(query), {signal: suggestionRequest.signal});
const matches = await response.json();
if (airportInput.value.trim() !== query || !Array.isArray(matches)) return;
suggestions.replaceChildren(...matches.map(function (match) {
const option = document.createElement('option');
option.value = match.code;
option.label = match.name;
return option;
}));
} catch (error) {
// Typed names still resolve on submission if autocomplete is unavailable.
}
}, 250);
});
const form = document.getElementById('flight-search');
if (form) form.addEventListener('submit', function () {
const button = form.querySelector('button');
button.disabled = true;
button.textContent = 'Looking up flights…';
});
document.querySelectorAll('.cache-updated[datetime]').forEach(function (element) {
element.textContent = new Date(element.dateTime).toLocaleString('en-GB', {day: 'numeric', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit', timeZoneName: 'short'});
});
</script>
{% endblock %}