agenda/templates/conference_list.html

269 lines
7.6 KiB
HTML

{% extends "base.html" %}
{% from "macros.html" import trip_link with context %}
{% block title %}Conferences - Edward Betts{% endblock %}
{% block style %}
<style>
/* Timeline */
.conf-timeline {
position: relative;
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 4px;
overflow: hidden;
user-select: none;
}
.conf-tl-month {
position: absolute;
top: 0;
height: 100%;
border-left: 1px solid #dee2e6;
pointer-events: none;
}
.conf-tl-month-label {
position: absolute;
top: 3px;
left: 3px;
font-size: 0.68em;
color: #6c757d;
white-space: nowrap;
}
.conf-tl-today {
position: absolute;
top: 22px;
height: calc(100% - 22px);
border-left: 2px solid #dc3545;
pointer-events: none;
z-index: 10;
}
.conf-tl-bar {
position: absolute;
height: 26px;
border-radius: 3px;
overflow: hidden;
z-index: 5;
}
.conf-tl-bar a, .conf-tl-bar span {
display: block;
padding: 4px 6px;
font-size: 0.72em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 18px;
color: white;
text-decoration: none;
}
/* Bidirectional hover highlight */
.conf-tl-bar.conf-hl {
filter: brightness(1.25);
box-shadow: inset 0 0 0 2px rgba(255,255,255,0.8);
z-index: 15;
}
tr.conf-hl > td {
background-color: rgba(255, 193, 7, 0.25) !important;
}
/* Conference table */
.conf-section-row td {
background: #343a40 !important;
color: #fff;
font-weight: 700;
font-size: 0.85em;
padding-top: 0.5rem;
padding-bottom: 0.4rem;
border-bottom: none;
}
.conf-month-row td {
background: #e9ecef !important;
font-weight: 600;
font-size: 0.8em;
text-transform: uppercase;
letter-spacing: 0.06em;
color: #495057;
padding-top: 0.5rem;
padding-bottom: 0.3rem;
border-bottom: none;
}
.conf-going {
--bs-table-bg: rgba(25, 135, 84, 0.07);
}
</style>
{% endblock %}
{% set tl_colors = ["#0d6efd","#198754","#dc3545","#fd7e14","#6f42c1","#20c997","#0dcaf0","#d63384"] %}
{% macro render_timeline(timeline) %}
{% if timeline %}
{% set row_h = 32 %}
{% set header_h = 22 %}
{% set total_h = timeline.lane_count * row_h + header_h %}
<div class="mb-4">
<h3>Next 90 days</h3>
<div class="conf-timeline" style="height: {{ total_h }}px;">
{% for m in timeline.months %}
<div class="conf-tl-month" style="left: {{ m.left_pct }}%;">
<span class="conf-tl-month-label">{{ m.label }}</span>
</div>
{% endfor %}
<div class="conf-tl-today" style="left: 0;"></div>
{% for conf in timeline.confs %}
{% set color = tl_colors[conf.lane % tl_colors | length] %}
{% set top_px = conf.lane * row_h + header_h %}
<div class="conf-tl-bar"
style="left: {{ conf.left_pct }}%; width: {{ conf.width_pct }}%; top: {{ top_px }}px; background: {{ color }};"
title="{{ conf.label }}"
data-conf-key="{{ conf.key }}">
{% if conf.url %}<a href="{{ conf.url }}">{{ conf.name }}</a>
{% else %}<span>{{ conf.name }}</span>{% endif %}
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% endmacro %}
{% macro conf_rows(heading, item_list, badge) %}
{% if item_list %}
{% set count = item_list | length %}
<tr class="conf-section-row">
<td colspan="7">{{ heading }} <span class="fw-normal opacity-75">{{ count }} conference{{ "" if count == 1 else "s" }}</span></td>
</tr>
{% set ns = namespace(prev_month="") %}
{% for item in item_list %}
{% set month_label = item.sort_date.strftime("%B %Y") %}
{% if month_label != ns.prev_month %}
{% set ns.prev_month = month_label %}
<tr class="conf-month-row">
<td colspan="7">{{ month_label }}</td>
</tr>
{% endif %}
<tr{% if item.going %} class="conf-going"{% endif %} data-conf-key="{{ item.sort_date.isoformat() }}|{{ item.name }}">
<td class="text-nowrap text-muted small">
{{ item.display_date }}
{% if item.date_status == "tentative" %}
<span class="badge text-bg-warning ms-1">tentative</span>
{% elif item.date_status == "approximate" %}
<span class="badge text-bg-secondary ms-1">approximate</span>
{% endif %}
</td>
<td>
{% if item.url %}<a href="{{ item.url }}">{{ item.name }}</a>
{% else %}{{ item.name }}{% endif %}
{% if item.going and not (item.accommodation_booked or item.travel_booked) %}
<span class="badge text-bg-primary ms-1">{{ badge }}</span>
{% endif %}
{% if item.accommodation_booked %}
<span class="badge text-bg-success ms-1">accommodation</span>
{% endif %}
{% if item.transport_booked %}
<span class="badge text-bg-success ms-1">transport</span>
{% endif %}
{% if item.linked_trip %}
{% set trip = item.linked_trip %}
<a href="{{ url_for('trip_page', start=trip.start.isoformat()) }}"
class="text-muted ms-1 text-decoration-none"
title="Trip: {{ trip.title }}">
🧳{% if trip.title != item.name %} {{ trip.title }}{% endif %}
</a>
{% endif %}
</td>
<td class="text-muted small">{{ item.topic }}</td>
<td class="text-nowrap text-muted small">
{% if item.series and item.series_detail %}
<a href="{{ url_for('conference_series_page', series_id=item.series) }}">{{ item.series_detail.name }}</a>
{% elif item.series %}
{{ item.series }}
{% endif %}
</td>
<td class="text-nowrap">
{% set country = get_country(item.country) if item.country else None %}
{% if country %}{{ country.flag }} {{ item.location }}
{% elif item.online %}💻 Online
{% else %}{{ item.location }}{% endif %}
</td>
<td class="text-nowrap text-muted small">
{% if item.cfp_end %}{{ item.cfp_end.strftime("%-d %b %Y") }}{% endif %}
</td>
<td>
{% if item.price and item.currency %}
<span class="badge bg-info text-nowrap">{{ "{:,d}".format(item.price | int) }} {{ item.currency }}</span>
{% if item.currency != "GBP" and item.currency in fx_rate %}
<span class="badge bg-info text-nowrap">{{ "{:,.0f}".format(item.price / fx_rate[item.currency]) }} GBP</span>
{% endif %}
{% elif item.free %}
<span class="badge bg-success text-nowrap">free</span>
{% endif %}
</td>
</tr>
{% endfor %}
{% endif %}
{% endmacro %}
{% block content %}
<div class="container-fluid mt-2">
<h1>Conferences</h1>
{{ render_timeline(timeline) }}
<table class="table table-sm table-hover align-middle">
<colgroup>
<col style="width: 9rem">
<col>
<col style="width: 18rem">
<col style="width: 14rem">
<col style="width: 14rem">
<col style="width: 7rem">
<col style="width: 10rem">
</colgroup>
<thead class="table-light">
<tr>
<th>Dates</th>
<th>Conference</th>
<th>Topic</th>
<th>Series</th>
<th>Location</th>
<th>CFP ends</th>
<th>Price</th>
</tr>
</thead>
<tbody>
{{ conf_rows("Current", current, "attending") }}
{{ conf_rows("Future", future, "going") }}
{{ conf_rows("Past", past|reverse|list, "went") }}
</tbody>
</table>
</div>
<script>
(function () {
// Build a map from conf-key → all matching elements (bars + rows)
const map = new Map();
document.querySelectorAll('[data-conf-key]').forEach(el => {
const k = el.dataset.confKey;
if (!map.has(k)) map.set(k, []);
map.get(k).push(el);
});
function setHighlight(key, on) {
(map.get(key) || []).forEach(el => el.classList.toggle('conf-hl', on));
}
map.forEach((els, key) => {
els.forEach(el => {
el.addEventListener('mouseenter', () => setHighlight(key, true));
el.addEventListener('mouseleave', () => setHighlight(key, false));
});
});
})();
</script>
{% endblock %}