diff --git a/data/2025-astropy-coordinator.json b/data/2025-astropy-coordinator.json index 249111a0..68d59703 100644 --- a/data/2025-astropy-coordinator.json +++ b/data/2025-astropy-coordinator.json @@ -13,7 +13,7 @@ }, "title": "Coordinator", "details": [ - "Member, Coordination Committee (executive committee)." + "Coordination Committee (executive committee), 2025–2028." ], "institution": "Astropy", "location": "Remote", diff --git a/data/2025-astropy-strategic-planner.json b/data/2025-astropy-strategic-planner.json index 31688517..e9d676fe 100644 --- a/data/2025-astropy-strategic-planner.json +++ b/data/2025-astropy-strategic-planner.json @@ -13,7 +13,7 @@ }, "title": "Strategic Planner", "details": [ - "Member, Strategic Planning and Organizing Committee." + "Strategic Planning and Organizing Committee, 2025–2026." ], "institution": "Astropy", "location": "Remote", diff --git a/src/components/CvView.astro b/src/components/CvView.astro index 1115e722..2ed8e502 100644 --- a/src/components/CvView.astro +++ b/src/components/CvView.astro @@ -5,7 +5,7 @@ import { PRESET } from '../lib/cards.js'; import '../styles/cv/page.css'; import '../styles/cv/minimap.css'; import CvHeader from './CvHeader.astro'; -import { softwarePapers, positionStops, institutionGroups, groupedByDefault, groupDate, links, REL_ICON, relKey, dateLabel } from '../lib/data.js'; +import { softwarePapers, positionStops, institutionGroups, groupedByDefault, groupDate, groupTitle, links, REL_ICON, relKey, dateLabel } from '../lib/data.js'; import { detailLines } from '../lib/inline.js'; import CollabMini from './CollabMini.astro'; import ConfMini from './ConfMini.astro'; @@ -35,8 +35,7 @@ const { cv, pick = false, picked = null, level = 'full', completeLines = cv.incl // Roles at one institution as the one row the CV reads: the roles joined, the // dates spanning them. const merged = (list) => { - const names = list.map((i) => i.title); - return { ...list[0], title: names.length > 1 ? `${names.slice(0, -1).join(', ')} & ${names.at(-1)}` : names[0], + return { ...list[0], title: groupTitle(list), date: groupDate(list), details: undefined }; }; const isPicked = (id) => !picked || picked.has(id); diff --git a/src/lib/cvmodel.js b/src/lib/cvmodel.js index bd4a061a..68255e5d 100644 --- a/src/lib/cvmodel.js +++ b/src/lib/cvmodel.js @@ -7,7 +7,7 @@ import person from '/config/person.json'; import { resolve } from './presets.js'; -import { authors, venueLine, dateLabel, links, money, softwarePapers, REL_ICON, relKey, institutionGroups, groupedByDefault, groupDate } from './data.js'; +import { authors, venueLine, dateLabel, links, money, softwarePapers, REL_ICON, relKey, institutionGroups, groupedByDefault, groupDate, groupTitle } from './data.js'; import { spans, detailLines } from './inline.js'; /** @@ -150,12 +150,11 @@ function itemsOf(s, cv, keepLine, group) { const rows = g.items.map(row); const [first] = g.items; const home = links(first).find((l) => l.rel === 'homepage'); - const names = rows.map((r) => r.title); return [{ ...rows[0], id: `group:${first.institution}`, when: dateLabel({ date: groupDate(g.items) }, { month: true }), - title: names.length > 1 ? `${names.slice(0, -1).join(', ')} & ${names.at(-1)}` : names[0], + title: groupTitle(g.items), subject: subject(first) ? [{ t: first.institution, b: false }] : [], lines: rows.flatMap((r) => r.lines), trailing: first.location ?? '', diff --git a/src/lib/data.js b/src/lib/data.js index 28f1b910..662a8065 100644 --- a/src/lib/data.js +++ b/src/lib/data.js @@ -437,6 +437,14 @@ export function institutionGroups(list, grouped = groupedByDefault) { return out; } +/** A group's roles joined as one title — "Coordinator (2025 – 2028) & Core + * Developer" — a role's own dates given where they differ from the group's. */ +export function groupTitle(items) { + const span = dateLabel({ date: groupDate(items) }); + const names = items.map((i) => (dateLabel(i) === span ? i.title : `${i.title} (${dateLabel(i)})`)); + return names.length > 1 ? `${names.slice(0, -1).join(', ')} & ${names.at(-1)}` : names[0]; +} + /** The span a group of dated items covers, as one item's `date`: earliest start, * and open-ended if any member is, else the latest end. */ export function groupDate(items) { diff --git a/src/pages/index.astro b/src/pages/index.astro index cecedae3..2ed7c635 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -3,7 +3,7 @@ import { PRESET } from '../lib/cards.js'; import Base from '../layouts/Base.astro'; import Card from '../components/Card.astro'; import SoftwareSection from '../components/SoftwareSection.astro'; -import { byType, featured, dateLabel } from '../lib/data.js'; +import { byType, featured, dateLabel, institutionGroups, groupDate } from '../lib/data.js'; import { lines as longLines } from '../lib/inline.js'; import Inline from '../components/Inline.astro'; import portrait from '../../cv/assets/portrait.jpg'; @@ -16,6 +16,9 @@ const pubs = featured('publication').slice(0, 5); // so undergraduate summer positions stay on the CV where they belong. const background = [...byType('position'), ...byType('education')] .filter((i) => (i.cvs ?? []).includes('2page')); +// Roles at one institution that opt in with `groupRoles` read as one place, +// as on the CV — so the Astropy committees sit under Astropy. +const rows = institutionGroups(background); --- @@ -64,20 +67,42 @@ const background = [...byType('position'), ...byType('education')]

Background

- {background.map((item) => ( + {rows.map((row) => row.items ? ( + /* a group is its ongoing role, the others' details beneath it */ + (() => { + const main = row.items.find((i) => i.date.present) ?? row.items[0]; + return ( +
+ {dateLabel({ date: groupDate(row.items) })} + + {main.title}, {main.institution} + {(() => { + // The ongoing role's first line, the other roles', then the rest + const own = longLines(main.details ?? [], '/cv/'); + const others = row.items.filter((i) => i !== main).flatMap((i) => + longLines(i.details ?? [], '/cv/').map((line, n) => ({ line, title: n === 0 ? i.title : null }))); + return [...own.slice(0, 1).map((line) => ({ line })), ...others, ...own.slice(1).map((line) => ({ line }))]; + })().map(({ line, title }) => ( + {title && <>{title}: } + ))} + +
+ ); + })() + ) : (
- {dateLabel(item)} + {dateLabel(row.item)} - {item.title}{item.institution && <>, {item.institution}} - {item.details && + {row.item.title}{row.item.institution && <>, {row.item.institution}} + {row.item.details && /* spans, like the CV — otherwise an array joins with no separator and [text](url) prints as its own markup */ - longLines(item.details, '/cv/').map((line) => ( + longLines(row.item.details, '/cv/').map((line) => ( ))} - {item.thesis && Thesis: {item.thesis}} + {row.item.thesis && Thesis: {row.item.thesis}}
))} diff --git a/tests/cvmodel.test.js b/tests/cvmodel.test.js index 580021e8..8b4f0523 100644 --- a/tests/cvmodel.test.js +++ b/tests/cvmodel.test.js @@ -50,7 +50,7 @@ describe('detail levels', () => { .filter((i) => i.subject.some((sp) => sp.t === 'Astropy')); const [merged] = rows(); expect(rows()).toHaveLength(1); - expect(merged.title).toBe('Coordinator, Strategic Planner & Core Developer'); + expect(merged.title).toBe('Coordinator (2025 – 2028), Strategic Planner (2025 – 2026) & Core Developer'); expect(rows({ group: { Astropy: false } }).map((r) => r.title)) .toEqual(['Coordinator', 'Strategic Planner', 'Core Developer']); });