";
+ return;
+ }
+ detail.innerHTML = buildConfDetail(selected);
+
+ var searchEl = document.getElementById("conf-pres-search");
+ if (searchEl) {
+ searchEl.value = confPresSearch;
+ searchEl.addEventListener("input", function () {
+ confPresSearch = searchEl.value;
+ renderConfPresentations(selected);
+ });
+ }
+ var topicEl = document.getElementById("conf-topic-filter");
+ if (topicEl) {
+ topicEl.value = confTopicFilter;
+ topicEl.addEventListener("change", function () {
+ confTopicFilter = topicEl.value;
+ renderConfPresentations(selected);
+ });
+ }
+ document.querySelectorAll("#conf-detail .conf-fit-chip").forEach(function (chip) {
+ chip.classList.toggle("active", chip.getAttribute("data-fit") === confActiveFit);
+ chip.addEventListener("click", function () {
+ confActiveFit = chip.getAttribute("data-fit");
+ document.querySelectorAll("#conf-detail .conf-fit-chip").forEach(function (c) { c.classList.remove("active"); });
+ chip.classList.add("active");
+ renderConfPresentations(selected);
+ });
+ });
+ var clearBtn = document.getElementById("conf-clear-filters");
+ if (clearBtn) {
+ clearBtn.addEventListener("click", function (e) {
+ e.preventDefault();
+ confPresSearch = "";
+ confTopicFilter = "all";
+ confActiveFit = "all";
+ confPresSelectedIdx = null;
+ renderConferences();
+ });
+ }
+ renderConfPresentations(selected);
+ }
+
+ var confItinSelectedKey = null;
+ var confItinExpandedId = null; // which conference's row is expanded (null = all collapsed)
+
+ var ITIN_INTRO = "Peter is in San Diego for the full run of SPIE Optics + Photonics 2026, splitting his time across four events.";
+ var ITIN_CLOSING = "Below is a selected short list of presenters considered to be valuable contacts from Peter's Choice Conference List.";
+ var ITIN_FIXED_COMMITMENTS = [
+ {
+ date: "August 25", time: "11:10 AM - 11:40 AM", location: "Conv. Ctr. Room 15B",
+ what_label: "Presenting his own paper - Session 4 (14223),",
+ what_detail: "“Anharmonic phonon mixing and structural breakdown in laser-excited nanocrystals”"
+ },
+ {
+ date: "August 25", time: "1:40 PM - 3:10 PM", location: "Conv. Ctr. Room 15B",
+ what_label: "Session Chair - Session 5 (14223),",
+ what_detail: "Nonlinear Excitation Regimes in Materials III"
+ }
+ ];
+
+ // Old-school, literal grouping: one card per (day, conference) pair, not one per
+ // conference. A conference active on two different days becomes two separate cards, each
+ // showing only that day's own stops — no multi-day content hiding behind one row.
+ function sanitizeIdPart(s) { return (s || "").replace(/[^a-zA-Z0-9]/g, ""); }
+
+ function getItinCards() {
+ var cards = [];
+ var byKey = {};
+ var itin = CONF_ROOT.itinerary;
+ (itin && itin.days || []).forEach(function (day) {
+ day.stops.forEach(function (stop) {
+ var confKey = stop.conference_number || stop.conference_name;
+ var cardKey = sanitizeIdPart(day.date) + "-" + sanitizeIdPart(confKey);
+ if (!byKey[cardKey]) {
+ byKey[cardKey] = {
+ id: cardKey, date: day.date, conference_number: stop.conference_number,
+ conference_name: stop.conference_name, room: stop.room, stops: []
+ };
+ cards.push(byKey[cardKey]);
+ }
+ byKey[cardKey].stops.push(stop);
+ });
+ });
+ return cards;
+ }
+
+ // "10:00 AM - 10:20 AM" + "3:00 PM - 3:20 PM" (in stop order, already chronological) -> "10:00 AM - 3:20 PM"
+ function timeSpanFor(stops) {
+ if (!stops.length) { return ""; }
+ var firstStart = stops[0].time.split(" - ")[0];
+ var lastParts = stops[stops.length - 1].time.split(" - ");
+ var lastEnd = lastParts[lastParts.length - 1];
+ return firstStart + " - " + lastEnd;
+ }
+
+ // "August 23, 2026" -> "August 23" (matches the "Month Day" shape buildCalendarIcon expects)
+ function dateAnchorFromDayString(dayDate) {
+ return (dayDate || "").split(",")[0];
+ }
+
+ // Flatten one card's own stops (already scoped to a single day + conference) into an
+ // ordered list: each shortlist person becomes its own clickable entry (resolves to a full
+ // presenter record), each fixed-only stop (no shortlist person — e.g. "Peter chairs this
+ // session") becomes a non-clickable divider so the where-am-I-next flow isn't lost.
+ // Person entries only — Peter's own fixed commitments live in the narrative/summary area
+ // above the list now (see getCardFixedNotes), not mixed in as non-clickable divider rows.
+ function getCardEntries(card) {
+ var entries = [];
+ card.stops.forEach(function (stop) {
+ var meta = card.date + " · " + stop.time + " · " + stop.room;
+ (stop.shortlist || []).forEach(function (s) {
+ entries.push({ kind: "person", key: "person-" + entries.length, meta: meta, name: s.name, stop: stop });
+ });
+ });
+ return entries;
+ }
+
+ function getCardFixedNotes(card) {
+ return card.stops.filter(function (s) { return s.fixed_note; }).map(function (s) { return s.fixed_note; });
+ }
+
+ function resolveItinPresenter(name, stop) {
+ for (var i = 0; i < CONF_DATA.length; i++) {
+ var c = CONF_DATA[i];
+ var isMatch = stop.conference_number ? c.conference_number === stop.conference_number : c.name === stop.conference_name;
+ if (!isMatch) { continue; }
+ var found = (c.presenters || []).filter(function (x) { return x.name === name; })[0];
+ if (found) { return found; }
+ }
+ return null;
+ }
+
+ // A compact calendar-icon showing the actual start day (month header + big day number),
+ // rather than a generic icon that looks the same regardless of date. "orange" variant is
+ // for general event info; the default (blue) is reserved for Peter's own commitments, so
+ // the two read as visually distinct categories at a glance.
+ function buildCalendarIcon(dateAnchor, variant) {
+ var parts = (dateAnchor || "").split(" ");
+ var month = (parts[0] || "").slice(0, 3).toUpperCase();
+ var day = parts[1] || "";
+ return (
+ "
" +
+ "
" + confEsc(month) + "
" +
+ "
" + confEsc(day) + "
" +
+ "
"
+ );
+ }
+
+ // A 64px icon matching buildCalendarIcon's footprint, used to flag Peter's own fixed
+ // commitments inline with the same visual weight as the day cards' calendar icons.
+ function buildClockIcon() {
+ return (
+ "
" +
+ "" +
+ "
"
+ );
+ }
+
+ // Shared 3-column layout: calendar icon | Time/Date/Location | what's happening (label
+ // plus an optional second line, e.g. a paper title). Used by both the fixed-commitment
+ // cards and the four event rows so they read as one consistent visual language.
+ function buildItinCols(icon, time, date, location, whatLabel, whatDetail) {
+ return (
+ icon +
+ "
" +
+ (time ? "
" + confEsc(time) + "
" : "") +
+ "
" + confEsc(date) + "
" +
+ "
" + confEsc(location) + "
" +
+ "
" +
+ "
" +
+ "
" + confEsc(whatLabel) + "
" +
+ (whatDetail ? "
" + confEsc(whatDetail) + "
" : "") +
+ "
"
+ );
+ }
+
+ function findConfForCard(card) {
+ return CONF_DATA.filter(function (x) {
+ return card.conference_number ? x.conference_number === card.conference_number : x.name === card.conference_name;
+ })[0];
+ }
+
+ // Default landing view for the Conferences tab: just the SPIE org narrative + Peter's
+ // affiliation, before he clicks into either his itinerary or a specific conference.
+ function renderConfIntro() {
+ var detail = document.getElementById("conf-detail");
+ var pe = CONF_ROOT.parent_event || {};
+ detail.innerHTML =
+ "
" +
+ "
" + confEsc(pe.name) + "
" +
+ "
" + confEsc(pe.dates) + "
" +
+ (pe.about ? "
" + confEsc(pe.about) + "
" : "") +
+ (pe.our_affiliation ? "
" + confEsc(pe.our_affiliation) + "
" : "") +
+ "
";
+ }
+
+ var confItinValueFilter = null; // "money" | "idea" | "connector" | "collaborator" | null
+ var confItinValueSelectedKey = null;
+ var VALUE_TAG_ORDER = ["money", "idea", "connector", "collaborator"];
+
+ // Everyone on Peter's shortlist across the whole week, deduped by name, with the day/stop
+ // context they were curated under (used both for the value-tag counts and the combined view).
+ function getAllShortlistPeople() {
+ var out = [];
+ var seen = {};
+ var itin = CONF_ROOT.itinerary;
+ (itin && itin.days || []).forEach(function (day) {
+ day.stops.forEach(function (stop) {
+ (stop.shortlist || []).forEach(function (s) {
+ if (seen[s.name]) { return; }
+ seen[s.name] = true;
+ out.push({ name: s.name, presenter: resolveItinPresenter(s.name, stop), day: day, stop: stop });
+ });
+ });
+ });
+ return out;
+ }
+
+ function getItinPeopleWithTag(tag) {
+ return getAllShortlistPeople().filter(function (e) {
+ return e.presenter && (e.presenter.value_tags || []).indexOf(tag) !== -1;
+ });
+ }
+
+ // Big clickable icon row: click one to swap the day-card list below for a combined view of
+ // everyone on the shortlist carrying that tag, regardless of which day/conference they're on.
+ function buildValueTagFilterRow() {
+ var all = getAllShortlistPeople();
+ return "
"
+ );
+ }
+
+ function personHaystack(p) {
+ var roleBits = (p.roles || []).map(function (r) {
+ return [r.session, r.title, r.abstract].join(" ");
+ }).join(" ");
+ return [p.name, p.affiliation, p.bio, roleBits].join(" ").toLowerCase();
+ }
+
+ function renderConfPresentations(c) {
+ var body = document.getElementById("conf-pres-body");
+ var countTag = document.getElementById("conf-pres-count-tag");
+ if (!body) { return; }
+ var prevListEl = document.getElementById("conf-pres-list");
+ var prevScrollTop = prevListEl ? prevListEl.scrollTop : 0;
+ var all = c.presenters || [];
+ var q = confPresSearch.trim().toLowerCase();
+ var filtered = [];
+ all.forEach(function (p, idx) {
+ var hay = personHaystack(p);
+ var matchesSearch = !q || hay.indexOf(q) !== -1;
+ var matchesTopic = confTopicFilter === "all" || hay.indexOf(confTopicFilter.toLowerCase()) !== -1;
+ var matchesFit = confActiveFit === "all" || p.fit_rating === confActiveFit;
+ if (matchesSearch && matchesTopic && matchesFit) { filtered.push({ p: p, idx: idx }); }
+ });
+
+ if (!all.length) {
+ countTag.textContent = "";
+ body.innerHTML = "
Awaiting the presenter list for this conference. Fill in conferences/speakers_template.json (or the .yaml version) and send it back — it drops straight into this directory.
";
+ item.addEventListener("click", function () {
+ confPresSelectedIdx = f.idx;
+ renderConfPresentations(c);
+ });
+ listEl.appendChild(item);
+ });
+ listEl.scrollTop = prevScrollTop;
+
+ var p = all[confPresSelectedIdx];
+ document.getElementById("conf-pres-detail").innerHTML = buildPresenterDetailHtml(p);
+ wireToggleButtons("#conf-pres-detail");
+ }
+
+ // Shared by the normal presenter directory AND the itinerary view — same detail card
+ // rendering either way, since the itinerary is just a curated lens on the same people.
+ function buildPresenterDetailHtml(p) {
+ var website = confPlainLink(p.website, "Website");
+ var linkedin = confPlainLink(p.linkedin, "LinkedIn");
+
+ var rolesHtml = (p.roles || []).map(function (r, i) {
+ var when = [r.date, r.time].filter(Boolean).join(" · ");
+ var badgeClass = "conf-role-badge-" + r.type;
+ var titleLine = r.type === "presenter" ? "
“" + confEsc(r.title || "(untitled)") + "”
" : "";
+ var abstractToggle = "";
+ if (r.abstract) {
+ var key = "abstract" + i;
+ abstractToggle =
+ "" +
+ "