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
88 changes: 84 additions & 4 deletions src/components/Experience.astro
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ function split({ summary, link }: Mission): (string | NonNullable<Mission['link'
if (!link || at < 0) return [summary];
return [summary.slice(0, at), link, summary.slice(at + link.text.length)];
}

// The oldest missions are folded under a button, so the timeline reads from
// the current work; they stay in the page for search engines and without
// JavaScript.
const earlier = experience.flatMap(({ earlier, missions }) =>
missions.filter((mission) => earlier || mission.earlier),
);
const years = earlier.flatMap(({ period }) => period.match(/\d{4}/g) ?? []).sort();
const span = years.length ? `${years[0]}–${years.at(-1)}` : '';
---

<section id="experience" class="experience" aria-labelledby="experience-title">
Expand All @@ -16,9 +25,9 @@ function split({ summary, link }: Mission): (string | NonNullable<Mission['link'
<h2 id="experience-title">Where I've made an impact.</h2>
<p class="lede">By employer, with the clients I worked for as a consultant.</p>
</div>
<ol class="timeline">
{experience.map(({ name, period, summary, current, missions }) => (
<li class:list={['employer', 'reveal', { current }]}>
<ol class="timeline" id="experience-list">
{experience.map(({ name, period, summary, current, earlier, missions }) => (
<li class:list={['employer', 'reveal', { current, earlier }]}>
<div class="head">
<h3>
{name}
Expand All @@ -29,7 +38,7 @@ function split({ summary, link }: Mission): (string | NonNullable<Mission['link'
</div>
<ol class="missions">
{missions.map((mission) => (
<li class="mission">
<li class:list={['mission', { earlier: mission.earlier }]}>
<h4>{mission.client ?? mission.role}</h4>
{mission.client && <p class="role">{mission.role}</p>}
<p class="meta">
Expand Down Expand Up @@ -57,14 +66,81 @@ function split({ summary, link }: Mission): (string | NonNullable<Mission['link'
</li>
))}
</ol>
{earlier.length > 0 && (
<button class="more" type="button" aria-expanded="false" aria-controls="experience-list">
<span class="label">Show earlier experience</span>
<span class="count">
{earlier.length} roles · {span}
</span>
</button>
)}
</div>
</section>

<script>
const timeline = document.getElementById('experience-list');
const more = document.querySelector<HTMLButtonElement>('#experience .more');
more?.addEventListener('click', () => {
const open = timeline!.classList.toggle('open');
more.setAttribute('aria-expanded', String(open));
more.querySelector('.label')!.textContent = open
? 'Hide earlier experience'
: 'Show earlier experience';
if (open) {
window.goatcounter?.count({ path: 'show earlier experience', event: true });
} else {
// The roles above it have folded away: bring the button back into view.
more.scrollIntoView({ block: 'center' });
}
});
</script>

<style>
.experience {
padding-top: 0;
}

/* Folded until the button opens them; without JavaScript, all shown. */
:global(.js) .timeline:not(.open) .earlier {
display: none;
}

.more {
display: none;
flex-wrap: wrap;
align-items: baseline;
justify-content: center;
gap: 4px 12px;
margin: 8px 0 0 36px;
padding: 12px 22px;
border: 1.5px solid var(--border);
border-radius: 12px;
background: var(--surface);
font: inherit;
color: var(--text);
cursor: pointer;
transition: border-color 0.25s;
}

:global(.js) .more {
display: inline-flex;
}

.more:hover {
border-color: var(--primary);
}

.more .label {
font-family: var(--font-title);
font-size: 15px;
font-weight: 600;
}

.more .count {
font-size: 13px;
color: var(--soft);
}

.timeline {
margin-top: 56px;
border-left: 1.5px solid var(--border);
Expand Down Expand Up @@ -210,5 +286,9 @@ function split({ summary, link }: Mission): (string | NonNullable<Mission['link'
gap: 18px;
padding-left: 26px;
}

.more {
margin-left: 26px;
}
}
</style>
7 changes: 7 additions & 0 deletions src/data/profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ export interface Mission {
/** A phrase of the summary that downloads a document, such as a thesis. */
link?: { text: string; url: string };
stack: string[];
/** Folded under "Show earlier experience" until asked for. */
earlier?: boolean;
}

/** An employer, and the missions done while employed there, newest first. */
Expand All @@ -146,6 +148,8 @@ export interface Employer {
/** What the employment was, when it is more than its missions. */
summary?: string;
current?: boolean;
/** Folded, with all its missions, under "Show earlier experience". */
earlier?: boolean;
missions: Mission[];
}

Expand Down Expand Up @@ -233,6 +237,7 @@ export const experience: Employer[] = [
{
role: 'Software Engineer',
client: 'SoccerLAB',
earlier: true,
period: 'Aug – Sep 2022',
place: 'Hasselt, Belgium',
summary: 'A Svelte proof of concept against React and Angular, with a design system.',
Expand All @@ -241,6 +246,7 @@ export const experience: Employer[] = [
{
role: 'Data Scientist',
client: 'Datavillage',
earlier: true,
period: 'Jan – Apr 2022',
place: 'Liège, Belgium',
summary:
Expand All @@ -251,6 +257,7 @@ export const experience: Employer[] = [
},
{
name: 'IDLab – imec',
earlier: true,
period: 'Feb – Jun 2021',
missions: [
{
Expand Down
8 changes: 7 additions & 1 deletion src/styles/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,9 @@

html {
scroll-behavior: smooth;
scroll-padding-top: 80px;
/* The navigation's height, its 1px border included (see Nav.astro), so a
link lands with its section starting right under the bar. */
scroll-padding-top: 73px;
/* What a browser shows past the page's edges: behind Safari's toolbar on
iPhone, and when a scroll bounces. The theme's background at the top, the
footer's green once the footer is on screen (set in Base.astro), so the
Expand Down Expand Up @@ -276,6 +278,10 @@ h2 {
}

@media (max-width: 920px) {
html {
scroll-padding-top: 61px;
}

section {
padding: 80px 0;
}
Expand Down
22 changes: 22 additions & 0 deletions tests/site.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,26 @@ test("paints past the page's edges in the theme's colour, then the footer's", as
await expect(edge).toBeHidden();
});

test('folds the earliest roles under a button, and unfolds them', async ({ page }) => {
await page.goto('/');
const earlier = page.locator('#experience .earlier');
// Found by place, as its name changes from "Show" to "Hide".
const more = page.locator('#experience .more');
await expect(more).toHaveAttribute('aria-expanded', 'false');
await expect(more).toContainText('3 roles · 2021–2022');
for (const role of await earlier.all()) await expect(role).toBeHidden();
await expect(page.getByRole('heading', { name: 'Partenamut' })).toBeVisible();

await more.click();
await expect(more).toHaveAttribute('aria-expanded', 'true');
await expect(more).toContainText('Hide earlier experience');
await expect(page.getByRole('heading', { name: 'SoccerLAB' })).toBeVisible();
await expect(page.getByRole('heading', { name: 'IDLab – imec' })).toBeVisible();

await more.click();
for (const role of await earlier.all()) await expect(role).toBeHidden();
});

test('copies the email address in one click, and says so', async ({ page }) => {
// A stand-in clipboard, the same in every browser, that records what it is given.
await page.addInitScript(() => {
Expand Down Expand Up @@ -312,6 +332,7 @@ test('counts the visit, and the CV, the thesis, an email and a link out', async

await page.evaluate(() => document.addEventListener('click', (event) => event.preventDefault()));
await page.locator('#top a[download]').click();
await page.getByRole('button', { name: /Show earlier experience/ }).click();
await page.locator(`#experience a[href$="master-thesis.pdf"]`).click();
await page.locator('#contact a[href^="mailto:"]').click();
await page.locator('#contact a[href*="linkedin.com"]').click();
Expand All @@ -320,6 +341,7 @@ test('counts the visit, and the CV, the thesis, an email and a link out', async
expect(await page.evaluate(() => (window as unknown as { counted: string[] }).counted)).toEqual([
'/',
'download terencio-agozzino-cv.pdf',
'show earlier experience',
'download terencio-agozzino-master-thesis.pdf',
'email',
'out www.linkedin.com/in/terencioagozzino',
Expand Down
Loading