function splitName(name) {
const m = (name || "").trim().match(/^([^\s,"]+)([\s\S]*)$/);
return m ? {first: m[1], rest: m[2]} : {first: (name || "").trim(), rest: ""};
}
function firstName(name) {
return splitName(name).first;
}
// Wrap the first word of a display name so styles.scss can bold it
function boldFirstName(name) {
const {first, rest} = splitName(name);
return html`<span class="person-first-name">${first}</span>${rest}`;
}
function byName(a, b) {
const opts = {sensitivity: "base"};
return firstName(a.name).localeCompare(firstName(b.name), undefined, opts)
|| (a.name || "").localeCompare(b.name || "", undefined, opts);
}
people = FileAttachment("people.csv").csv({typed: false})
.then(rows => rows.sort(byName))
// The site can be served under a subpath (GitHub Pages project page:
// <org>.github.io/midsea-network/), so a "/images/..." path must resolve
// against the site root, not the domain root. This page is one folder deep.
siteRoot = new URL("../", location.href).href
function siteUrl(path) {
// Full URLs (https://...) pass through new URL() unchanged.
return new URL(path.replace(/^\/(?!\/)/, ""), siteRoot).href;
}
placeholderAvatar = siteUrl("images/people/placeholder-avatar.svg")
// people.csv stores the SHA-256 of the lower-cased email, never the email
// itself: the CSV is published with the site. Gravatar looks up by that hash.
function gravatarUrl(hash) {
// d=404 makes Gravatar return 404 when the email has no Gravatar, so the
// <img> onerror swaps in our placeholder. Gravatar cannot use our SVG as
// its d= default: it only accepts public .jpg/.gif/.png URLs.
return `https://www.gravatar.com/avatar/${hash}?s=200&d=404`;
}
function photoUrl(p) {
if (p.photo && p.photo.trim()) return siteUrl(p.photo.trim());
if (p.email_sha256 && p.email_sha256.trim()) return gravatarUrl(p.email_sha256.trim());
return placeholderAvatar;
}
photoUrls = people.map(photoUrl)
// The Cards / Compact switch only toggles a class on the grid; the compact
// layout (small round photo beside the name) lives in styles.scss.
{
const grid = html`<div class="people-grid">
${people.map((p, i) => html`
<div class="person-card">
<img src="${photoUrls[i]}" alt="${p.name}"
onerror="this.onerror=null;this.src='${placeholderAvatar}';">
<div class="person-card-body">
<h3>${p.profile_url && p.profile_url.trim()
? html`<a href="${p.profile_url}" target="_blank" rel="noopener noreferrer">${boldFirstName(p.name)}<i class="bi bi-box-arrow-up-right person-link-icon" aria-hidden="true"></i><span class="visually-hidden"> (opens in a new tab)</span></a>`
: boldFirstName(p.name)}</h3>
<p class="person-title">${p.title}</p>
<p class="person-institution">${p.institution}</p>
</div>
</div>
`)}
</div>`;
const toolbar = html`<div class="people-toolbar">
<div class="btn-group btn-group-sm" role="group" aria-label="People view">
<input type="radio" class="btn-check" name="people-view"
id="people-view-cards" value="cards" checked>
<label class="btn btn-outline-primary" for="people-view-cards">
<i class="bi bi-grid-3x3-gap" aria-hidden="true"></i> Cards
</label>
<input type="radio" class="btn-check" name="people-view"
id="people-view-compact" value="compact">
<label class="btn btn-outline-primary" for="people-view-compact">
<i class="bi bi-list-ul" aria-hidden="true"></i> Compact
</label>
</div>
</div>`;
toolbar.addEventListener("change", e => {
grid.classList.toggle("people-grid-compact", e.target.value === "compact");
});
return html`<div class="people-directory">${toolbar}${grid}</div>`;
}Researchers, students, public health professionals, and decision-makers from across the region working on infectious disease modelling.