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
96 changes: 96 additions & 0 deletions __tests__/travel-transforms.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { describe, it, expect } from 'vitest'
import { buildContinentBars, countriesUpTo, nextPlayState, yearBounds, type Country } from '../lib/content/travel'

const country = (overrides: Partial<Country> = {}): Country => ({
id: 'x',
name: 'X',
flag: '🏳️',
firstVisited: 2000,
order: 0,
continent: 'Europe',
coordinates: [0, 0],
...overrides,
})

const FIXTURE: Country[] = [
country({ id: 'a', name: 'A', continent: 'Europe', firstVisited: 1986, order: 1 }),
country({ id: 'b', name: 'B', continent: 'Europe', firstVisited: 1990, order: 2 }),
country({ id: 'c', name: 'C', continent: 'Asia', firstVisited: 1995, order: 3 }),
country({ id: 'd', name: 'D', continent: 'Asia', firstVisited: 2010, order: 4 }),
country({ id: 'e', name: 'E', continent: 'Americas', firstVisited: 2020, order: 5 }),
]

describe('countriesUpTo', () => {
it('returns all countries when year is omitted', () => {
expect(countriesUpTo(FIXTURE)).toEqual(FIXTURE)
})

it('filters to countries first visited on or before year', () => {
expect(countriesUpTo(FIXTURE, 1995).map((c) => c.id)).toEqual(['a', 'b', 'c'])
})

it('returns [] for a year before the first trip', () => {
expect(countriesUpTo(FIXTURE, 1980)).toEqual([])
})
})

describe('buildContinentBars', () => {
it('sorts bars by count descending', () => {
const bars = buildContinentBars(FIXTURE)
expect(bars.map((b) => b.name)).toEqual(['Europe', 'Asia', 'Americas'])
})

it('scales pct relative to the largest continent', () => {
const bars = buildContinentBars(FIXTURE)
const europe = bars.find((b) => b.name === 'Europe')!
const americas = bars.find((b) => b.name === 'Americas')!
expect(europe.pct).toBe(100)
expect(americas.pct).toBe(50)
})

it('excludes countries first visited after year', () => {
const bars = buildContinentBars(FIXTURE, 1995)
expect(bars).toEqual([
{ name: 'Europe', count: 2, pct: 100 },
{ name: 'Asia', count: 1, pct: 50 },
])
})

it('returns [] for a year before the first trip', () => {
expect(buildContinentBars(FIXTURE, 1980)).toEqual([])
})
})

describe('yearBounds', () => {
it('returns the min and max firstVisited years', () => {
expect(yearBounds(FIXTURE)).toEqual([1986, 2020])
})
})

describe('nextPlayState', () => {
const min = 1986
const max = 2020

it('stops without touching the year', () => {
expect(nextPlayState({ year: 2000, playing: true }, min, max)).toEqual({
year: 2000,
playing: false,
})
})

it('starts from the current year when not at the end', () => {
expect(nextPlayState({ year: 2000, playing: false }, min, max)).toEqual({
year: 2000,
playing: true,
})
})

it('restarts at min — not min - 1 — when starting from max', () => {
// min - 1 sits outside a <input type=range min={min}> and gets clamped
// by the browser, desyncing the rendered label from the slider thumb.
expect(nextPlayState({ year: max, playing: false }, min, max)).toEqual({
year: min,
playing: true,
})
})
})
69 changes: 34 additions & 35 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,14 @@ input,
--border-strong: var(--bone-400);
--rule: color-mix(in srgb, var(--bone-100) 18%, transparent);

/* ---- Travel globe tokens -------------------------------- */
--map-sphere: var(--oxblood-1000);
--map-land: #2a2a2a;
--map-land-hover: #3a3a3a;
--map-visited: var(--accent-press);
--map-visited-hover: var(--accent);
--map-stroke: #1a1a1a;

/* ---- Font families (next/font variables from layout) ---- */
--font-display: var(--font-archivo-black), "Helvetica Neue", Arial, sans-serif;
--font-sans: var(--font-space-grotesk), "Helvetica Neue", Arial, sans-serif;
Expand Down Expand Up @@ -257,6 +265,11 @@ input,
--border-bru: var(--ink-000);
--border-strong: var(--ink-000);
--rule: var(--ink-000);

--map-land: #333;
--map-visited: #fff;
--map-visited-hover: #ddd;
--map-stroke: #000;
}

.brutalist-root *,
Expand Down Expand Up @@ -573,37 +586,15 @@ input,
color: var(--fg);
text-align: right;
}
.travel-highlights {
border: 1px solid var(--rule);
padding: var(--s-5);
display: flex;
flex-direction: column;
gap: var(--s-3);
}
.highlight-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
}
.highlight-list li {
display: flex;
align-items: baseline;
gap: 12px;
padding: 10px 0;
border-top: 1px solid var(--rule);
font-family: var(--font-sans);
font-size: 14px;
color: var(--fg);
}
.highlight-list li:first-child { border-top: 0; }
.hl-num {
font-family: var(--font-mono);
font-size: 11px;
color: var(--accent);
letter-spacing: 0.08em;
}
.scrub { border: 1px solid var(--rule); padding: var(--s-5); display: flex; flex-direction: column; gap: var(--s-4); }
.scrub-year { font-family: var(--font-display); font-size: 64px; line-height: .9; letter-spacing: -.04em; color: var(--accent); }
.scrub input[type=range] { width: 100%; accent-color: var(--accent); }
.scrub-row { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.scrub-stat { font-family: var(--font-mono); font-size: 11px; letter-spacing: .12em; text-transform: uppercase; color: var(--fg-muted); }
.scrub-stat b { color: var(--fg); font-weight: 500; }
.scrub-new { display: flex; flex-wrap: wrap; gap: 6px; }
.scrub-chip { font-family: var(--font-mono); font-size: 10px; letter-spacing: .08em; padding: 4px 8px; border: 1px solid var(--accent); color: var(--accent); }
.scrub-chip.is-muted { border-color: var(--rule); color: var(--fg-low); }

/* ---- Travel: real globe map embed ------------------------ */
.travel-map-wrap { margin-top: var(--s-7); }
Expand All @@ -620,11 +611,19 @@ input,
padding: var(--s-4);
background: var(--bg-elevated);
}
.travel-map-frame :where(.relative) {
background: transparent !important;
border: 0 !important;
border-radius: 0 !important;
.map-frame { position: relative; width: 100%; overflow: hidden; }
.map-tip {
position: fixed;
z-index: 60;
background: var(--bg-elevated);
border: 1px solid var(--accent);
padding: 6px 10px;
pointer-events: none;
font-family: var(--font-mono);
font-size: 12px;
color: var(--fg);
}
.map-tip small { display: block; color: var(--fg-muted); font-size: 11px; }
.travel-map-toggle {
font-family: var(--font-mono);
font-size: 11px;
Expand Down
13 changes: 0 additions & 13 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,13 @@ import {
BrutalistLanding,
type LandingTravelData,
} from '@/components/landing/BrutalistLanding'
import type { ContinentBar } from '@/components/landing/Travel'

function buildContinentBars(byContinent: Record<string, number>): ContinentBar[] {
const entries = Object.entries(byContinent).sort(([, a], [, b]) => b - a)
const max = entries.length > 0 ? entries[0][1] : 1

return entries.map(([name, count]) => ({
name,
count,
pct: Math.round((count / max) * 100),
}))
}

export default function Home() {
const { countries, stats } = getTravelData()
const flightRoutes = getFlightRoutes()
const flightStats = getFlightStats()

const travel: LandingTravelData = {
continents: buildContinentBars(stats.countriesByContinent),
totalCountries: stats.totalCountries,
totalContinents: stats.continents,
countries,
Expand Down
4 changes: 1 addition & 3 deletions components/landing/BrutalistLanding.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,14 @@ import { Availability } from './Availability'
import { Hero } from './Hero'
import { Projects } from './Projects'
import { Writing } from './Writing'
import { Travel, type ContinentBar } from './Travel'
import { Travel } from './Travel'
import { Contact } from './Contact'
import { Resume } from './Resume'
import { Footer } from './Footer'
import { hasPosts } from '@/lib/content/writing'
import type { Country, FlightRoute } from '@/lib/content/travel'

export interface LandingTravelData {
continents: ContinentBar[]
totalCountries: number
totalContinents: number
countries: Country[]
Expand Down Expand Up @@ -83,7 +82,6 @@ export function BrutalistLanding({ travel }: { travel: LandingTravelData }) {
{showWriting && <Writing />}
<Resume />
<Travel
continents={travel.continents}
totalCountries={travel.totalCountries}
totalContinents={travel.totalContinents}
countries={travel.countries}
Expand Down
Loading
Loading