From d8490ce94f0ca8e88a2efa67f352558461748d85 Mon Sep 17 00:00:00 2001 From: Stan Runge Date: Sun, 25 Oct 2026 12:08:38 +0000 Subject: [PATCH] trackers: group cars, trains, planes and wildlife under one spotting tracker with /trackers/spotting landing page and category switcher --- src/lib/components/SpottingSwitcher.svelte | 74 +++++++++++++++ src/lib/projects.test.ts | 4 +- src/lib/projects.ts | 60 ++++++------ src/lib/seo.ts | 5 + src/lib/title.ts | 1 + src/lib/tracker-icons.test.ts | 4 +- src/lib/tracker-icons.ts | 14 +++ src/routes/sitemap.xml/+server.ts | 1 + src/routes/trackers/cars/+layout.svelte | 8 ++ src/routes/trackers/planes/+layout.svelte | 8 ++ src/routes/trackers/spotting/+page.server.ts | 32 +++++++ src/routes/trackers/spotting/+page.svelte | 99 ++++++++++++++++++++ src/routes/trackers/trains/+layout.svelte | 8 ++ src/routes/trackers/wildlife/+layout.svelte | 8 ++ 14 files changed, 295 insertions(+), 31 deletions(-) create mode 100644 src/lib/components/SpottingSwitcher.svelte create mode 100644 src/routes/trackers/cars/+layout.svelte create mode 100644 src/routes/trackers/planes/+layout.svelte create mode 100644 src/routes/trackers/spotting/+page.server.ts create mode 100644 src/routes/trackers/spotting/+page.svelte create mode 100644 src/routes/trackers/trains/+layout.svelte create mode 100644 src/routes/trackers/wildlife/+layout.svelte diff --git a/src/lib/components/SpottingSwitcher.svelte b/src/lib/components/SpottingSwitcher.svelte new file mode 100644 index 0000000..6551dea --- /dev/null +++ b/src/lib/components/SpottingSwitcher.svelte @@ -0,0 +1,74 @@ + + + + + diff --git a/src/lib/projects.test.ts b/src/lib/projects.test.ts index d9296a8..60c31ab 100644 --- a/src/lib/projects.test.ts +++ b/src/lib/projects.test.ts @@ -7,13 +7,13 @@ describe('trackers', () => { visibleTrackers(true) .map((t) => t.name) .slice(0, 4) - ).toEqual(['tasks', 'money', 'books', 'cars']); + ).toEqual(['tasks', 'money', 'books', 'spotting']); }); it('hides private trackers when logged out', () => { const names = visibleTrackers(false).map((t) => t.name); expect(names).not.toContain('tasks'); expect(names).not.toContain('health'); - expect(names.slice(0, 3)).toEqual(['money', 'books', 'cars']); + expect(names.slice(0, 3)).toEqual(['money', 'books', 'spotting']); }); it('uses short single-word names', () => { for (const t of visibleTrackers(true)) expect(t.name).toMatch(/^[a-z]+$/); diff --git a/src/lib/projects.ts b/src/lib/projects.ts index d161c2d..d88f4fe 100644 --- a/src/lib/projects.ts +++ b/src/lib/projects.ts @@ -59,9 +59,9 @@ const allTrackers = [ private: false }, { - name: 'cars', - subtitle: 'a car pokedex: every brand and model, with Dutch and US data', - href: '/trackers/cars', + name: 'spotting', + subtitle: "cars, trains, planes & wildlife I've spotted", + href: '/trackers/spotting', external: false, private: false }, @@ -114,7 +114,6 @@ const allTrackers = [ external: false, private: false }, - { name: 'skins', subtitle: 'Counter-Strike skin market prices across time', @@ -135,29 +134,6 @@ const allTrackers = [ href: '/lists', external: false, private: false - }, - { - name: 'planes', - subtitle: 'a plane pokedex: airliners, helicopters, bizjets, light aircraft and warbirds', - href: '/trackers/planes', - external: false, - private: false - }, - { - name: 'trains', - subtitle: - 'a train pokedex: steam, diesel and electric locos, high-speed trains, multiple units, metros and trams', - href: '/trackers/trains', - external: false, - private: false - }, - { - name: 'wildlife', - subtitle: - 'a wild animal pokedex: everything you can spot in the Netherlands, plus famous animals worldwide', - href: '/trackers/wildlife', - external: false, - private: false } ] as const; @@ -168,6 +144,36 @@ const strip = ({ name, subtitle, href, external }: Tracker): Tracker => ({ href, external }); +// The spotting trackers: pokedexes of things I can spot in the wild. Grouped under one +// "spotting" item in the header, home page and /trackers; each keeps its own URL. +export const spottingTrackers: readonly Tracker[] = [ + { + name: 'cars', + subtitle: 'a car pokedex: every brand and model, with Dutch and US data', + href: '/trackers/cars', + external: false + }, + { + name: 'trains', + subtitle: + 'a train pokedex: steam, diesel and electric locos, high-speed trains, multiple units, metros and trams', + href: '/trackers/trains', + external: false + }, + { + name: 'planes', + subtitle: 'a plane pokedex: airliners, helicopters, bizjets, light aircraft and warbirds', + href: '/trackers/planes', + external: false + }, + { + name: 'wildlife', + subtitle: + 'a wild animal pokedex: everything you can spot in the Netherlands, plus famous animals worldwide', + href: '/trackers/wildlife', + external: false + } +]; /** Public trackers (sitemap, logged-out navigation). */ export const trackers: readonly Tracker[] = allTrackers.filter((t) => !t.private).map(strip); /** Owner-only trackers. */ diff --git a/src/lib/seo.ts b/src/lib/seo.ts index 6b315d7..232d760 100644 --- a/src/lib/seo.ts +++ b/src/lib/seo.ts @@ -44,6 +44,11 @@ const publicPages: Record = { title: 'Books', description: 'Stan Runge’s library: books being read, finished and on the to-read pile.' }, + '/trackers/spotting': { + title: 'Spotting', + description: + 'Stan Runge’s spotting pokedexes: cars, trains, planes and wildlife, with specs, photos and how to recognise them.' + }, '/trackers/cars': { title: 'Car pokedex', description: diff --git a/src/lib/title.ts b/src/lib/title.ts index 38c39c0..04be764 100644 --- a/src/lib/title.ts +++ b/src/lib/title.ts @@ -19,6 +19,7 @@ export function getTitle(path: string, environment: SiteEnvironment = null): str '/projects': 'Projects', '/trackers': 'Trackers', '/trackers/books': 'Books', + '/trackers/spotting': 'Spotting', '/trackers/cars': 'Cars', '/trackers/planes': 'Planes', '/trackers/trains': 'Trains', diff --git a/src/lib/tracker-icons.test.ts b/src/lib/tracker-icons.test.ts index 929bfd8..10a5cff 100644 --- a/src/lib/tracker-icons.test.ts +++ b/src/lib/tracker-icons.test.ts @@ -1,10 +1,10 @@ import { describe, expect, it } from 'vitest'; import { trackerIcons } from './tracker-icons'; -import { visibleTrackers } from './projects'; +import { spottingTrackers, visibleTrackers } from './projects'; describe('tracker icons', () => { it('gives every tracker its own colour', () => { - const names = visibleTrackers(true).map((t) => t.name); + const names = [...visibleTrackers(true), ...spottingTrackers].map((t) => t.name); for (const name of names) expect(trackerIcons[name], name).toBeDefined(); const colours = names.map((n) => trackerIcons[n].to); expect(new Set(colours).size).toBe(colours.length); diff --git a/src/lib/tracker-icons.ts b/src/lib/tracker-icons.ts index 264720f..d4813ac 100644 --- a/src/lib/tracker-icons.ts +++ b/src/lib/tracker-icons.ts @@ -144,6 +144,20 @@ export const trackerIcons: Record = { 'M2 21c0-3 1.85-5.36 5.08-6C9.5 14.52 12 13 13 12' ] }, + spotting: { + from: '#fbbf24', + to: '#e11d48', + ink: WHITE, + // binoculars + paths: [ + 'M10 10h4', + 'M19 7V4a1 1 0 0 0-1-1h-2a1 1 0 0 0-1 1v3', + 'M20 21a2 2 0 0 0 2-2v-3.851c0-1.39-2-2.962-2-4.829V8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v11a2 2 0 0 0 2 2z', + 'M22 16H2', + 'M4 21a2 2 0 0 1-2-2v-3.851c0-1.39 2-2.962 2-4.829V8a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v11a2 2 0 0 1-2 2z', + 'M9 7V4a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1v3' + ] + }, lists: { from: '#94a3b8', to: '#475569', diff --git a/src/routes/sitemap.xml/+server.ts b/src/routes/sitemap.xml/+server.ts index 98e77c7..c3e6958 100644 --- a/src/routes/sitemap.xml/+server.ts +++ b/src/routes/sitemap.xml/+server.ts @@ -14,6 +14,7 @@ const staticPaths = [ '/blog', '/lists', '/trackers/books', + '/trackers/spotting', '/trackers/cars', '/trackers/cars/brands', '/trackers/planes', diff --git a/src/routes/trackers/cars/+layout.svelte b/src/routes/trackers/cars/+layout.svelte new file mode 100644 index 0000000..c7540c7 --- /dev/null +++ b/src/routes/trackers/cars/+layout.svelte @@ -0,0 +1,8 @@ + + + +{@render children()} diff --git a/src/routes/trackers/planes/+layout.svelte b/src/routes/trackers/planes/+layout.svelte new file mode 100644 index 0000000..c7540c7 --- /dev/null +++ b/src/routes/trackers/planes/+layout.svelte @@ -0,0 +1,8 @@ + + + +{@render children()} diff --git a/src/routes/trackers/spotting/+page.server.ts b/src/routes/trackers/spotting/+page.server.ts new file mode 100644 index 0000000..1a954b7 --- /dev/null +++ b/src/routes/trackers/spotting/+page.server.ts @@ -0,0 +1,32 @@ +import { carOverview } from '#lib/server/cars.js'; +import { planeOverview } from '#lib/server/planes.js'; +import { trainOverview } from '#lib/server/trains.js'; +import { wildlifeOverview } from '#lib/server/wildlife.js'; +import type { PageServerLoad } from './$types'; + +// Headline counts per spotting tracker. Each overview is cached for 10 minutes; a failing +// one just hides its count instead of breaking the landing page. +const safe = (p: Promise) => p.catch(() => null); + +export const load: PageServerLoad = async () => { + const [cars, trains, planes, wildlife] = await Promise.all([ + safe(carOverview()), + safe(trainOverview()), + safe(planeOverview()), + safe(wildlifeOverview()) + ]); + const counts: Record = { + cars: cars ? { value: cars.stats.models, label: 'models' } : null, + trains: trains ? { value: trains.stats.types, label: 'types' } : null, + planes: planes ? { value: planes.stats.types, label: 'types' } : null, + wildlife: wildlife ? { value: wildlife.stats.species, label: 'species' } : null + }; + return { + counts, + seo: { + title: 'Spotting', + description: + 'Stan Runge’s spotting pokedexes: cars, trains, planes and wildlife, with specs, photos and how to recognise them.' + } + }; +}; diff --git a/src/routes/trackers/spotting/+page.svelte b/src/routes/trackers/spotting/+page.svelte new file mode 100644 index 0000000..9b298f2 --- /dev/null +++ b/src/routes/trackers/spotting/+page.svelte @@ -0,0 +1,99 @@ + + +
+ +

+ spotting +

+

+ Cars, trains, planes and wildlife I've spotted. Pick a pokedex. +

+ +
+ + diff --git a/src/routes/trackers/trains/+layout.svelte b/src/routes/trackers/trains/+layout.svelte new file mode 100644 index 0000000..c7540c7 --- /dev/null +++ b/src/routes/trackers/trains/+layout.svelte @@ -0,0 +1,8 @@ + + + +{@render children()} diff --git a/src/routes/trackers/wildlife/+layout.svelte b/src/routes/trackers/wildlife/+layout.svelte new file mode 100644 index 0000000..c7540c7 --- /dev/null +++ b/src/routes/trackers/wildlife/+layout.svelte @@ -0,0 +1,8 @@ + + + +{@render children()}