PageSourceSearch

https://restud-na-tours.netlify.app/assets/js/tour-renderer.js

js restud-na-tours.netlify.app collected 2026-10-03 11:20:39 UTC 17,988 bytes, 486 lines download raw bytes

1(function () {
2  "use strict";
3
4  const data = window.RESTUD_TOUR_DATA;
5  if (!data || !data.upcomingTour) {
6    return;
7  }
8
9  const socialIcons = {
10    "x-twitter": "fa-brands fa-x-twitter",
11    twitter: "fa-brands fa-x-twitter",
12    linkedin: "fa-brands fa-linkedin-in",
13    github: "fa-brands fa-github",
14    website: "fa fa-globe",
15    homepage: "fa fa-globe"
16  };
17
18  function byId(id) {
19    return document.getElementById(id);
20  }
21
22  function escapeHtml(value) {
23    return String(value || "")
24      .replace(/&/g, "&")
25      .replace(/</g, "&lt;")
26      .replace(/>/g, "&gt;")
27      .replace(/"/g, "&quot;")
28      .replace(/'/g, "&#39;");
29  }
30
31  function escapeAttr(value) {
32    return escapeHtml(value).replace(/`/g, "&#96;");
33  }
34
35  function textToHtml(value) {
36    return escapeHtml(value).replace(/\n/g, "<br>");
37  }
38
39  function safeId(value) {
40    return String(value || "")
41      .toLowerCase()
42      .replace(/[^a-z0-9]+/g, "-")
43      .replace(/^-+|-+$/g, "");
44  }
45
46  function isVisible(item) {
47    return item && item.status !== "hidden";
48  }
49
50  function hasUrl(value) {
51    return Boolean(normalizeLink(value).url);
52  }
53
54  function externalAttrs(url) {
55    return /^https?:\/\//.test(normalizeLink(url).url || "") ? ' target="_blank" rel="noopener"' : "";
56  }
57
58  function normalizeLink(value) {
59    const raw = String(value || "").trim();
60    if (!raw || raw === "#") {
61      return { url: "", label: "" };
62    }
63
64    const markdownMatch = raw.match(/^\[([^\]]+)\]\(([^)]+)\)$/);
65    if (markdownMatch) {
66      return {
67        label: markdownMatch[1],
68        url: markdownMatch[2].trim()
69      };
70    }
71
72    return { url: raw, label: "" };
73  }
74
75  function linkHtml(url, label, className) {
76    const link = normalizeLink(url);
77    if (!link.url) {
78      return "";
79    }
80    const classAttr = className ? ` class="${escapeAttr(className)}"` : "";
81    return `<a${classAttr} href="${escapeAttr(link.url)}"${externalAttrs(link.url)}>${escapeHtml(label || link.label || link.url)}</a>`;
82  }
83
84  function renderTextBlocks(value) {
85    const blocks = Array.isArray(value) ? value : [value].filter(Boolean);
86    return blocks.map((block) => `<p>${textToHtml(block)}</p>`).join("");
87  }
88
89  function renderHostLinks(host) {
90    const links = visibleItems(host.links).filter((link) => hasUrl(link.url));
91
92    if (!links.length && hasUrl(host.url)) {
93      links.push({ url: host.url, label: host.linkLabel || host.name });
94    }
95
96    if (!links.length) {
97      return "";
98    }
99
100    return `<div class="host-links">${links.map((link) => linkHtml(link.url, link.label || link.linkLabel || link.name || host.name, "btn btn-ghost me-2 mb-2")).join("")}</div>`;
101  }
102
103  function formatLocations(locations) {
104    if (!Array.isArray(locations) || !locations.length) {
105      return "Locations to be announced";
106    }
107    return locations.filter(Boolean).join(", ");
108  }
109
110  function visibleItems(items) {
111    return Array.isArray(items) ? items.filter(isVisible) : [];
112  }
113
114  function speakerModalId(speaker) {
115    return `modal-speaker-${safeId(speaker.id || speaker.name)}`;
116  }
117
118  function renderSocialLinks(profileLinks) {
119    const links = visibleItems(profileLinks).filter((link) => hasUrl(link.url));
120    if (!links.length) {
121      return "";
122    }
123
124    const items = links.map((link) => {
125      const normalized = normalizeLink(link.url);
126      const iconClass = socialIcons[link.type] || "fa fa-link";
127      const label = link.label || normalized.label || link.type || "Profile";
128      return `<li class="list-inline-item"><a href="${escapeAttr(normalized.url)}"${externalAttrs(normalized.url)} aria-label="${escapeAttr(label)}"><i class="${escapeAttr(iconClass)} fa-fw"></i></a></li>`;
129    }).join("");
130
131    return `<ul class="social-list list-inline mb-0">${items}</ul>`;
132  }
133
134  function renderPeopleLinks(people) {
135    const visiblePeople = visibleItems(people);
136    if (!visiblePeople.length) {
137      return "";
138    }
139
140    return visiblePeople.map((person) => {
141      if (typeof person === "string") {
142        return escapeHtml(person);
143      }
144      const normalized = normalizeLink(person.url);
145      return normalized.url
146        ? `<a href="${escapeAttr(normalized.url)}" class="d-block d-md-inline-block"${externalAttrs(normalized.url)}>${escapeHtml(person.name || normalized.label || normalized.url)}</a>`
147        : escapeHtml(person.name);
148    }).join(" and ");
149  }
150
151  function renderHero(tour) {
152    const hero = byId("tour-hero-content");
153    const heroBlock = byId("hero-block");
154    if (!hero) {
155      return;
156    }
157
158    if (tour.heroImage && heroBlock) {
159      heroBlock.style.backgroundImage = `linear-gradient(rgba(0, 0, 0, 0.5), rgba(0,0, 0, 0.1)), url("${tour.heroImage}")`;
160    }
161
162    document.title = `${tour.title} | REStud North-American Tours`;
163    const description = document.querySelector('meta[name="description"]');
164    if (description) {
165      description.setAttribute("content", `${tour.title}: ${tour.dates.display}; ${formatLocations(tour.locations)}`);
166    }
167
168    const dateLine = tour.dates && tour.dates.display ? tour.dates.display : "Dates to be announced";
169    const locationLine = formatLocations(tour.locations);
170    const application = tour.application || {};
171    const applicationButton = hasUrl(application.url)
172      ? `<div class="mt-4">${linkHtml(application.url, application.label || "Apply", "btn btn-primary btn-lg")}</div>`
173      : "";
174
175    hero.innerHTML = `
176      <h1 class="hero-heading mb-2">${escapeHtml(tour.title)}</h1>
177      <div class="hero-meta mb-3"><i class="far fa-calendar-alt me-2"></i>${escapeHtml(dateLine)}</div>
178      <div class="hero-meta mb-3"><i class="fas fa-map-marker-alt mx-2"></i>${escapeHtml(locationLine)}</div>
179      ${applicationButton}
180    `;
181  }
182
183  function renderHosts(tour) {
184    const heading = byId("hosts-heading");
185    const container = byId("tour-hosts");
186    if (!container) {
187      return;
188    }
189
190    if (heading) {
191      heading.textContent = `Hosts (${tour.editionYear})`;
192    }
193
194    const hosts = visibleItems(tour.hostInstitutions);
195    if (!hosts.length) {
196      container.innerHTML = '<p class="section-intro text-center single-col-max mx-auto mb-0">Host institutions to be announced.</p>';
197      return;
198    }
199
200    container.innerHTML = hosts.map((host, index) => {
201      const figureClass = `figure-${(index % 4) + 1}`;
202      const localHosts = renderPeopleLinks(host.localHosts);
203      const hostLinks = renderHostLinks(host);
204      const imageHtml = host.image ? `
205        <div class="col-5 col-lg-3 col-md-4 offset-1 h-100 d-flex align-items-center justify-content-center">
206          <div class="figure ${figureClass}"><img class="img-fluid d-none d-md-block" src="${escapeAttr(host.image)}" alt="${escapeAttr(host.alt || host.name)}"></div>
207        </div>
208      ` : "";
209
210      return `
211        <div class="row">
212          <div class="${host.image ? "col-12 col-md-7 col-lg-8" : "col-12"} h-100 d-flex flex-column justify-content-between">
213            <div class="desc h-100 align-items-center">
214              <h4 class="text-white mb-4 mt-3 mt-lg-5">${escapeHtml(host.name)}</h4>
215              ${renderTextBlocks(host.description)}
216              ${hostLinks}
217              ${localHosts ? `<p class="mt-3 mb-0"><strong>Local hosts:</strong> ${localHosts}</p>` : ""}
218            </div>
219            <br>
220            <br>
221          </div>
222          ${imageHtml}
223        </div>
224      `;
225    }).join("");
226  }
227
228  function renderSpeakers(tour) {
229    const heading = byId("speakers-heading");
230    const container = byId("tour-speakers");
231    const modals = byId("speaker-modals");
232    if (!container) {
233      return;
234    }
235
236    if (heading) {
237      heading.textContent = `Tourists (${tour.editionYear})`;
238    }
239
240    const speakers = visibleItems(tour.speakers);
241    if (!speakers.length) {
242      container.innerHTML = '<div class="col-12"><p class="section-intro text-center single-col-max mx-auto mb-0">Tourists to be announced.</p></div>';
243      if (modals) {
244        modals.innerHTML = "";
245      }
246      return;
247    }
248
249    container.innerHTML = speakers.map((speaker) => {
250      const modalId = speakerModalId(speaker);
251      const image = speaker.image
252        ? `<img src="${escapeAttr(speaker.image)}" class="card-img-top rounded-0" alt="${escapeAttr(speaker.name)}">`
253        : "";
254      const imageLink = image
255        ? `<a href="#${escapeAttr(modalId)}" data-bs-toggle="modal" data-bs-target="#${escapeAttr(modalId)}">${image}</a>`
256        : "";
257      const social = renderSocialLinks(speaker.profileLinks);
258      const website = linkHtml(speaker.website, "Personal Website →");
259      const jmp = speaker.jmp && hasUrl(speaker.jmp.url)
260        ? linkHtml(speaker.jmp.url, "JMP →")
261        : "";
262
263      return `
264        <div class="col-xs-12 col-sm-6 col-lg-4 mb-4">
265          <div class="card rounded-0">
266            ${imageLink}
267            <div class="card-body">
268              <h5 class="card-title mb-2">${escapeHtml(speaker.name)}</h5>
269              ${speaker.affiliation ? `<div class="card-text mb-3"><div class="meta">${escapeHtml(speaker.affiliation)}</div></div>` : ""}
270              ${website}
271              ${website && jmp ? "<br>" : ""}
272              ${jmp}
273            </div>
274            ${social ? `<div class="card-footer text-muted">${social}</div>` : ""}
275          </div>
276        </div>
277      `;
278    }).join("");
279
280    if (modals) {
281      modals.innerHTML = speakers.map(renderSpeakerModal).join("");
282    }
283  }
284
285  function renderSpeakerModal(speaker) {
286    const modalId = speakerModalId(speaker);
287    const social = renderSocialLinks(speaker.profileLinks);
288    const jmpTitle = speaker.jmp && speaker.jmp.title ? speaker.jmp.title : "";
289    const jmpLine = jmpTitle
290      ? `<p><strong>JMP:</strong> ${hasUrl(speaker.jmp.url) ? linkHtml(speaker.jmp.url, jmpTitle) : escapeHtml(jmpTitle)}</p>`
291      : "";
292    const websiteLine = hasUrl(speaker.website)
293      ? `<p>${linkHtml(speaker.website, "Personal Website")}</p>`
294      : "";
295
296    return `
297      <div class="modal modal-speaker" id="${escapeAttr(modalId)}" tabindex="-1" role="dialog" aria-labelledby="${escapeAttr(modalId)}-label" aria-hidden="true">
298        <div class="modal-dialog">
299          <div class="modal-content">
300            <div class="modal-header">
301              <button type="button" class="btn-close" data-bs-dismiss="modal" aria-hidden="true">&times;</button>
302              <h4 id="${escapeAttr(modalId)}-label" class="modal-title sr-only">${escapeHtml(speaker.name)}</h4>
303            </div>
304            <div class="modal-body p-0">
305              <div class="theme-bg-light p-5">
306                <div class="row">
307                  ${speaker.image ? `<div class="col-12 col-md-auto text-center"><img class="profile-image mb-3 mb-md-0 me-md-4 rounded-circle mx-auto" src="${escapeAttr(speaker.image)}" alt="${escapeAttr(speaker.name)}"></div>` : ""}
308                  <div class="col text-center text-md-start mx-auto">
309                    <h2 class="name mb-2">${escapeHtml(speaker.name)}</h2>
310                    ${speaker.affiliation ? `<div class="meta mb-2">${escapeHtml(speaker.affiliation)}</div>` : ""}
311                    ${social}
312                  </div>
313                </div>
314              </div>
315              <div class="desc p-4 p-lg-5">
316                ${speaker.bio ? `<p>${textToHtml(speaker.bio)}</p>` : ""}
317                ${jmpLine}
318                ${websiteLine}
319              </div>
320            </div>
321          </div>
322        </div>
323      </div>
324    `;
325  }
326
327  function renderSchedule(tour) {
328    const heading = byId("schedule-heading");
329    const container = byId("tour-schedule");
330    if (!container) {
331      return;
332    }
333
334    if (heading) {
335      heading.textContent = `Tour Agenda (${tour.editionYear})`;
336    }
337
338    const stops = visibleItems(tour.schedule);
339    const speakersById = {};
340    visibleItems(tour.speakers).forEach((speaker) => {
341      speakersById[speaker.id] = speaker;
342    });
343
344    if (!stops.length) {
345      container.innerHTML = '<p class="section-intro text-center single-col-max mx-auto mb-0">Agenda to be announced.</p>';
346      return;
347    }
348
349    const nav = stops.map((stop, index) => {
350      const tabId = `tab-${safeId(stop.id || stop.host || index + 1)}`;
351      return `
352        <li class="nav-item ${index < stops.length - 1 ? "me-2" : ""}">
353          <a class="nav-link ${index === 0 ? "active" : ""}" id="${escapeAttr(tabId)}" data-bs-toggle="tab" href="#${escapeAttr(tabId)}-content" role="tab" aria-controls="${escapeAttr(tabId)}-content" aria-selected="${index === 0 ? "true" : "false"}">
354            <span class="heading">${escapeHtml(stop.host)}</span>
355            ${stop.date ? `<span class="meta">(${escapeHtml(stop.date)})</span>` : ""}
356          </a>
357        </li>
358      `;
359    }).join("");
360
361    const panes = stops.map((stop, index) => renderSchedulePane(stop, index, speakersById)).join("");
362
363    container.innerHTML = `
364      <ul class="schedule-nav nav nav-pills nav-fill" id="tour-schedule-tabs" role="tablist">${nav}</ul>
365      <div class="schedule-tab-content tab-content">${panes}</div>
366    `;
367  }
368
369  function renderSchedulePane(stop, index, speakersById) {
370    const tabId = `tab-${safeId(stop.id || stop.host || index + 1)}`;
371    const items = visibleItems(stop.items);
372    const noTimeline = stop.noTimeline || !items.some((item) => item.type === "talk");
373    const localHosts = renderPeopleLinks(stop.localHosts);
374
375    const agenda = items.length
376      ? items.map((item) => renderScheduleItem(item, speakersById)).join("")
377      : '<h4 class="text-center py-5 text-muted">Agenda to be announced.</h4>';
378
379    return `
380      <div class="tab-pane ${index === 0 ? "active" : ""} ${noTimeline ? "no-timeline" : ""}" id="${escapeAttr(tabId)}-content" role="tabpanel" aria-labelledby="${escapeAttr(tabId)}">
381        ${agenda}
382        ${localHosts ? `<br><div><h6>Local Organizers: ${localHosts}</h6></div><br>` : ""}
383      </div>
384    `;
385  }
386
387  function renderScheduleItem(item, speakersById) {
388    if (item.type === "day") {
389      return renderScheduleDay(item);
390    }
391
392    const speaker = item.speakerId ? speakersById[item.speakerId] : null;
393    const title = item.title || (speaker && speaker.jmp && speaker.jmp.title) || "Talk";
394    const titleUrl = item.url || item.link || item.paperUrl || (speaker && speaker.jmp && speaker.jmp.url) || "";
395    const titleHtml = hasUrl(titleUrl)
396      ? linkHtml(titleUrl, title, "theme-link")
397      : escapeHtml(title);
398    const itemClass = item.type === "talk" ? "item-talk" : "item-other";
399    const profile = speaker ? renderScheduleProfile(speaker) : "";
400    const descriptions = Array.isArray(item.description) ? item.description : [item.description].filter(Boolean);
401    const discussant = item.discussant ? `<div class="desc">Discussant: ${textToHtml(item.discussant)}</div>` : "";
402
403    return `
404      <div class="item ${itemClass}">
405        <div class="meta">
406          ${item.time ? `<h4 class="time mb-3">${escapeHtml(item.time)}</h4>` : ""}
407          ${profile}
408        </div>
409        <div class="content">
410          <h3 class="title mb-3">${titleHtml}</h3>
411          ${item.location ? `<div class="location mb-3"><i class="fas fa-map-marker-alt me-2"></i>${escapeHtml(item.location)}</div>` : ""}
412          ${descriptions.map((description) => `<div class="desc">${textToHtml(description)}</div>`).join("")}
413          ${discussant}
414        </div>
415      </div>
416    `;
417  }
418
419  function renderScheduleDay(item) {
420    const title = item.title || item.date || "Schedule Day";
421    const descriptions = Array.isArray(item.description) ? item.description : [item.description].filter(Boolean);
422
423    return `
424      <div class="item item-day">
425        <div class="day-content">
426          <h3 class="day-heading">${escapeHtml(title)}</h3>
427          ${descriptions.map((description) => `<div class="day-desc">${textToHtml(description)}</div>`).join("")}
428        </div>
429      </div>
430    `;
431  }
432
433  function renderScheduleProfile(speaker) {
434    const modalId = speakerModalId(speaker);
435    const image = speaker.image
436      ? `<a href="#${escapeAttr(modalId)}" data-bs-toggle="modal" data-bs-target="#${escapeAttr(modalId)}"><img class="profile-image rounded-circle mb-2" src="${escapeAttr(speaker.image)}" alt="${escapeAttr(speaker.name)}"></a>`
437      : "";
438    return `
439      <div class="profile">
440        ${image}
441        <div class="name"><a class="theme-link" href="#${escapeAttr(modalId)}" data-bs-toggle="modal" data-bs-target="#${escapeAttr(modalId)}">${escapeHtml(speaker.name)}</a></div>
442      </div>
443    `;
444  }
445
446  function renderPastTours() {
447    const container = byId("past-tours-carousel");
448    if (!container) {
449      return;
450    }
451
452    const tours = visibleItems(data.pastTours);
453    if (!tours.length) {
454      container.innerHTML = "";
455      return;
456    }
457
458    container.innerHTML = tours.map((tour, index) => {
459      const position = tour.imagePosition || "center center";
460      const locations = formatLocations(tour.locations);
461      return `
462        <div class="carousel-item ${index === 0 ? "active" : ""}">
463          <div class="d-flex align-items-center justify-content-center text-center" style="height: 70vh; background: linear-gradient(rgba(0, 0, 0, 0.1), rgba(0,0, 0, 0.6)), url('${escapeAttr(tour.image)}') no-repeat ${escapeAttr(position)}; background-size: cover;">
464            <a href="${escapeAttr(tour.url)}" class="text-white text-decoration-none"${externalAttrs(tour.url)}>
465              <h1 class="text-center text-white">${escapeHtml(tour.title)}</h1>
466              <div class="hero-meta mb-2">${escapeHtml(tour.dates || "")}</div>
467              <div>${escapeHtml(locations)}</div>
468            </a>
469          </div>
470        </div>
471      `;
472    }).join("");
473  }
474
475  function renderSite() {
476    const tour = data.upcomingTour;
477    renderHero(tour);
478    renderHosts(tour);
479    renderSpeakers(tour);
480    renderSchedule(tour);
481    renderPastTours();
482  }
483
484  window.RESTUD_TOUR_RENDERER = { render: renderSite };
485  renderSite();
486})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.