Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion data/2025-astropy-coordinator.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
},
"title": "Coordinator",
"details": [
"Member, Coordination Committee (executive committee)."
"Coordination Committee (executive committee), 2025–2028."
],
"institution": "Astropy",
"location": "Remote",
Expand Down
2 changes: 1 addition & 1 deletion data/2025-astropy-strategic-planner.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 2 additions & 3 deletions src/components/CvView.astro
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand Down
5 changes: 2 additions & 3 deletions src/lib/cvmodel.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand Down Expand Up @@ -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 ?? '',
Expand Down
8 changes: 8 additions & 0 deletions src/lib/data.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
39 changes: 32 additions & 7 deletions src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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);

---
<Base title="Nathaniel Starkman">
Expand Down Expand Up @@ -64,20 +67,42 @@ const background = [...byType('position'), ...byType('education')]
<div class="wrap">
<h2>Background</h2>
<div class="timeline">
{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 (
<div class="tl">
<span class="when">{dateLabel({ date: groupDate(row.items) })}</span>
<span class="what">
<strong>{main.title}</strong>, {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 }) => (
<span class="sub">{title && <>{title}: </>}<Inline spans={line} /></span>
))}
</span>
</div>
);
})()
) : (
<div class="tl">
<span class="when">{dateLabel(item)}</span>
<span class="when">{dateLabel(row.item)}</span>
<span class="what">
<strong>{item.title}</strong>{item.institution && <>, {item.institution}</>}
{item.details &&
<strong>{row.item.title}</strong>{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) => (
<span class="sub">
<Inline spans={line} />
</span>
))}
{item.thesis && <span class="sub">Thesis: {item.thesis}</span>}
{row.item.thesis && <span class="sub">Thesis: {row.item.thesis}</span>}
</span>
</div>
))}
Expand Down
2 changes: 1 addition & 1 deletion tests/cvmodel.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
});
Expand Down
Loading