All checks were successful
Deploy production / deploy (push) Successful in 1m10s
- CBS_SERIES: CPI, household tariffs (2007-now), energy balance, renewables, heat pumps, solar, cars, population (monthly, since 1942, components), house prices, housing stock, municipal levies for Gouda and its neighbours - shared StatChart/Kpi/chart helpers with per-chart source attribution - drop Eurostat nrg_pc_20x_h (404; CBS covers that history)
424 lines
14 KiB
TypeScript
424 lines
14 KiB
TypeScript
// Real projects (tools and experiments). Personal data trackers live in `trackers`.
|
||
export const projects = [
|
||
{
|
||
name: 'vash esports',
|
||
subtitle: 'the best way to play osu!',
|
||
href: 'https://esports.vash.software',
|
||
external: true
|
||
},
|
||
{
|
||
name: 'osu! map finder',
|
||
subtitle: 'find osu! maps for Spotify songs/albums/playlists',
|
||
href: '/projects/spotify-beatmap-finder',
|
||
external: false
|
||
},
|
||
{
|
||
name: 'derby lens (warning: AI slop)',
|
||
subtitle: 'estimate odds for horse betting',
|
||
href: '/projects/derby',
|
||
external: false
|
||
},
|
||
{
|
||
name: 'world opinions (warning: AI slop)',
|
||
subtitle: 'ideologies, values and biases across countries',
|
||
href: '/projects/world-opinions',
|
||
external: false
|
||
},
|
||
{
|
||
name: 'language learner (warning: AI slop)',
|
||
subtitle: 'Dutch, Korean and English, from zero to fluent',
|
||
href: '/projects/language-learner',
|
||
external: false
|
||
}
|
||
] as const;
|
||
|
||
// The public /stats section: open data on the economy, energy and Gouda (src/routes/stats).
|
||
export const statsPages = [
|
||
{
|
||
name: 'economy',
|
||
subtitle: 'rates, inflation, bond spreads, GDP and debt',
|
||
href: '/stats/economy',
|
||
external: false
|
||
},
|
||
{
|
||
name: 'energy',
|
||
subtitle: 'tariffs, oil and gas, renewables, heat pumps and EVs',
|
||
href: '/stats/energy',
|
||
external: false
|
||
},
|
||
{
|
||
name: 'gouda',
|
||
subtitle: 'population, housing and neighbourhoods in Gouda',
|
||
href: '/stats/gouda',
|
||
external: false
|
||
}
|
||
] as const;
|
||
|
||
type Tracker = {
|
||
name: string;
|
||
subtitle: string;
|
||
href: string;
|
||
external: boolean;
|
||
/** Sub-trackers linked directly from this item (header flyout, tile chips). */
|
||
children?: readonly Tracker[];
|
||
/** Icon key in #lib/tracker-icons when it differs from `name` (games). */
|
||
icon?: string;
|
||
/** Shown on group tiles before the "more" chip (main and actively played games). */
|
||
featured?: boolean;
|
||
/** A game I don't play now (games.library.active = false): listed under "non-active". */
|
||
inactive?: boolean;
|
||
/** Owner-only (needs login): every listing shows a small lock after the name. */
|
||
locked?: boolean;
|
||
};
|
||
|
||
// 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
|
||
}
|
||
];
|
||
|
||
// Games I play (plus speedcubing), grouped under "games" like spotting: every game has its
|
||
// own page at /trackers/games/<slug>, one click away from the header flyout and the chips.
|
||
// `featured` games (my main game and the ones I actively play) show first on tiles; the
|
||
// rest sit behind a "more" chip. Data per game lives in games.library (same slugs).
|
||
// Which games are active (the ones I play now) is my pick in games.library.active, toggled on
|
||
// /trackers/games; sortedGames() puts them first, makes them the tile chips and flags the
|
||
// rest `inactive`, so the header, /start and the tiles show "active" and "non-active" parts.
|
||
// Call of Duty is one entry per title and Pokémon one per generation (remakes such as
|
||
// HeartGold & SoulSilver and spin-offs separate); full names, no abbreviations. The old
|
||
// /trackers/games/bo2 and /trackers/games/pokemon URLs redirect (see [slug]/+page.server.ts).
|
||
const game = (slug: string, name: string, subtitle: string, featured = false): Tracker => ({
|
||
name,
|
||
subtitle,
|
||
href: `/trackers/games/${slug}`,
|
||
external: false,
|
||
icon: `game-${slug}`,
|
||
...(featured ? { featured } : {})
|
||
});
|
||
export const gameTrackers: readonly Tracker[] = [
|
||
game('osu', 'osu!', 'rhythm game, my main focus right now', true),
|
||
game('cs2', 'cs2', 'Counter-Strike 2 (CS:GO)', true),
|
||
game('pubg', 'pubg', 'PUBG: Battlegrounds', true),
|
||
game('r6', 'r6 siege', 'Tom Clancy’s Rainbow Six Siege', true),
|
||
game('tarkov', 'tarkov', 'Escape from Tarkov', true),
|
||
game('black-ops-2', 'black ops 2', 'Call of Duty: Black Ops II (2012)', true),
|
||
game('valorant', 'valorant', 'Riot’s tactical shooter', false),
|
||
game('lol', 'league', 'League of Legends', false),
|
||
game('overwatch', 'overwatch 2', 'Blizzard’s hero shooter', false),
|
||
game('minecraft', 'minecraft', 'blocks, mining and building', false),
|
||
game('factorio', 'factorio', 'factory building and automation', false),
|
||
game('ksp', 'ksp', 'Kerbal Space Program', false),
|
||
game('smash', 'smash bros', 'Super Smash Bros.', false),
|
||
game('cubing', 'speedcubing', 'Rubik’s cube solving, not a video game', false),
|
||
game('sot', 'sea of thieves', 'pirate sandbox', false),
|
||
// Call of Duty, one entry per title (the Black Ops 2 one is featured above).
|
||
game('black-ops', 'black ops', 'Call of Duty: Black Ops (2010)'),
|
||
game('ghosts', 'ghosts', 'Call of Duty: Ghosts (2013)'),
|
||
game('advanced-warfare', 'advanced warfare', 'Call of Duty: Advanced Warfare (2014)'),
|
||
game('black-ops-3', 'black ops 3', 'Call of Duty: Black Ops III (2015)'),
|
||
game('infinite-warfare', 'infinite warfare', 'Call of Duty: Infinite Warfare (2016)'),
|
||
game('wwii', 'wwii', 'Call of Duty: WWII (2017)'),
|
||
// Pokémon, one entry per generation; remakes and spin-offs get their own.
|
||
game('pokemon-red-blue-yellow', 'pokémon red, blue & yellow', 'generation 1, Game Boy'),
|
||
game(
|
||
'pokemon-gold-silver-crystal',
|
||
'pokémon gold, silver & crystal',
|
||
'generation 2, Game Boy Color'
|
||
),
|
||
game(
|
||
'pokemon-ruby-sapphire-emerald',
|
||
'pokémon ruby, sapphire & emerald',
|
||
'generation 3, Game Boy Advance'
|
||
),
|
||
game('pokemon-colosseum', 'pokémon colosseum', 'generation 3 spin-off, GameCube'),
|
||
game('pokemon-xd', 'pokémon xd: gale of darkness', 'generation 3 spin-off, GameCube'),
|
||
game(
|
||
'pokemon-firered-leafgreen',
|
||
'pokémon firered & leafgreen',
|
||
'generation 1 remake, Game Boy Advance'
|
||
),
|
||
game(
|
||
'pokemon-diamond-pearl-platinum',
|
||
'pokémon diamond, pearl & platinum',
|
||
'generation 4, Nintendo DS'
|
||
),
|
||
game(
|
||
'pokemon-heartgold-soulsilver',
|
||
'pokémon heartgold & soulsilver',
|
||
'generation 2 remake, Nintendo DS'
|
||
),
|
||
game(
|
||
'pokemon-black-white',
|
||
'pokémon black & white',
|
||
'generation 5 with Black 2 & White 2, Nintendo DS'
|
||
),
|
||
game('pokemon-x-y', 'pokémon x & y', 'generation 6, Nintendo 3DS'),
|
||
game(
|
||
'pokemon-omega-ruby-alpha-sapphire',
|
||
'pokémon omega ruby & alpha sapphire',
|
||
'generation 3 remake, Nintendo 3DS'
|
||
),
|
||
game(
|
||
'pokemon-sun-moon',
|
||
'pokémon sun & moon',
|
||
'generation 7 with Ultra Sun & Ultra Moon, Nintendo 3DS'
|
||
),
|
||
game(
|
||
'pokemon-lets-go',
|
||
'pokémon let’s go, pikachu & eevee',
|
||
'generation 1 remake, Nintendo Switch'
|
||
),
|
||
game(
|
||
'pokemon-sword-shield',
|
||
'pokémon sword & shield',
|
||
'generation 8 with The Isle of Armor & The Crown Tundra, Nintendo Switch'
|
||
),
|
||
game(
|
||
'pokemon-brilliant-diamond-shining-pearl',
|
||
'pokémon brilliant diamond & shining pearl',
|
||
'generation 4 remake, Nintendo Switch'
|
||
),
|
||
game('pokemon-legends-arceus', 'pokémon legends: arceus', 'generation 8, Nintendo Switch'),
|
||
game(
|
||
'pokemon-scarlet-violet',
|
||
'pokémon scarlet & violet',
|
||
'generation 9 with The Teal Mask & The Indigo Disk, Nintendo Switch'
|
||
),
|
||
game('pokemon-legends-z-a', 'pokémon legends: z-a', 'generation 9, Nintendo Switch')
|
||
];
|
||
|
||
/** games.library slug of a game tracker entry (`/trackers/games/<slug>`). */
|
||
export const gameSlug = (t: { href: string }) => t.href.replace('/trackers/games/', '');
|
||
|
||
/** Every game, active ones first (games.library.active, my pick), non-active ones flagged
|
||
* `inactive`; `inactiveGames` comes from the root layout data. */
|
||
export function sortedGames(inactiveGames: readonly string[] = []): readonly Tracker[] {
|
||
if (!inactiveGames.length) return gameTrackers;
|
||
const off = new Set(inactiveGames);
|
||
const flagged = gameTrackers.map((g) =>
|
||
off.has(gameSlug(g)) ? { ...g, inactive: true, featured: false } : { ...g, featured: true }
|
||
);
|
||
return [...flagged.filter((g) => !g.inactive), ...flagged.filter((g) => g.inactive)];
|
||
}
|
||
/** Only the games I play now. */
|
||
export const activeGames = (inactiveGames: readonly string[] = []) =>
|
||
sortedGames(inactiveGames).filter((g) => !g.inactive);
|
||
|
||
// Which games.library entry each Pokémon journal game (ref.pokemon_games.slug) belongs to,
|
||
// so hiding a generation also hides its games on the hub. Unlisted games always show.
|
||
const POKEMON_ENTRY_GAMES: Record<string, readonly string[]> = {
|
||
'pokemon-red-blue-yellow': ['red', 'blue', 'yellow', 'red-japan', 'blue-japan', 'green-japan'],
|
||
'pokemon-gold-silver-crystal': ['gold', 'silver', 'crystal'],
|
||
'pokemon-ruby-sapphire-emerald': ['ruby', 'sapphire', 'emerald'],
|
||
'pokemon-colosseum': ['colosseum'],
|
||
'pokemon-xd': ['xd'],
|
||
'pokemon-firered-leafgreen': ['firered', 'leafgreen'],
|
||
'pokemon-diamond-pearl-platinum': ['diamond', 'pearl', 'platinum'],
|
||
'pokemon-heartgold-soulsilver': ['heartgold', 'soulsilver'],
|
||
'pokemon-black-white': ['black', 'white', 'black-2', 'white-2'],
|
||
'pokemon-x-y': ['x', 'y'],
|
||
'pokemon-omega-ruby-alpha-sapphire': ['omega-ruby', 'alpha-sapphire'],
|
||
'pokemon-sun-moon': ['sun', 'moon', 'ultra-sun', 'ultra-moon'],
|
||
'pokemon-lets-go': ['lets-go-pikachu', 'lets-go-eevee'],
|
||
'pokemon-sword-shield': [
|
||
'sword',
|
||
'shield',
|
||
'the-isle-of-armor-sword',
|
||
'the-isle-of-armor-shield',
|
||
'the-crown-tundra-sword',
|
||
'the-crown-tundra-shield'
|
||
],
|
||
'pokemon-brilliant-diamond-shining-pearl': ['brilliant-diamond', 'shining-pearl'],
|
||
'pokemon-legends-arceus': ['legends-arceus'],
|
||
'pokemon-scarlet-violet': [
|
||
'scarlet',
|
||
'violet',
|
||
'the-teal-mask-scarlet',
|
||
'the-teal-mask-violet',
|
||
'the-indigo-disk-scarlet',
|
||
'the-indigo-disk-violet'
|
||
],
|
||
'pokemon-legends-z-a': ['legends-za', 'mega-dimension']
|
||
};
|
||
const POKEMON_GAME_ENTRY = new Map(
|
||
Object.entries(POKEMON_ENTRY_GAMES).flatMap(([entry, games]) => games.map((g) => [g, entry]))
|
||
);
|
||
/** games.library entry for a Pokémon journal game slug, if it has one. */
|
||
export const pokemonEntryFor = (slug: string | null | undefined) =>
|
||
slug ? POKEMON_GAME_ENTRY.get(slug) : undefined;
|
||
|
||
// Trackers, in display order. Names are short single nouns. Most keep their existing
|
||
// /projects/... URLs (no moves, so no redirects needed). `private` trackers are shown in the
|
||
// header, home page and /trackers only to the logged-in owner (layout data `isOwner`), never
|
||
// in the sitemap; their routes 404 for everyone else. Private trackers, and listed ones that
|
||
// still need a login (`locked`, e.g. money), get a lock icon next to their name everywhere.
|
||
const allTrackers: readonly (Tracker & { private: boolean })[] = [
|
||
{
|
||
name: 'tasks',
|
||
subtitle: 'to-dos, goals and my TickTick history',
|
||
href: '/trackers/tasks',
|
||
external: false,
|
||
private: true
|
||
},
|
||
{
|
||
name: 'money',
|
||
subtitle: 'a manual ledger for accounts, bills and utilities',
|
||
href: '/trackers/money',
|
||
external: false,
|
||
locked: true,
|
||
private: false
|
||
},
|
||
{
|
||
name: 'reading',
|
||
subtitle: 'books, scientific papers and articles I am reading, have read and want to read',
|
||
href: '/trackers/books',
|
||
external: false,
|
||
private: false
|
||
},
|
||
{
|
||
name: 'spotting',
|
||
subtitle: "cars, trains, planes & wildlife I've spotted",
|
||
href: '/trackers/spotting',
|
||
external: false,
|
||
children: spottingTrackers,
|
||
private: false
|
||
},
|
||
{
|
||
name: 'music',
|
||
subtitle: 'everything I listen to: top artists, tracks, albums and streaks',
|
||
href: '/trackers/music',
|
||
external: false,
|
||
private: false
|
||
},
|
||
{
|
||
name: 'pc',
|
||
subtitle: 'my PC setup: every part, my devices, the next build and mouse sensitivities',
|
||
href: '/trackers/pc',
|
||
external: false,
|
||
private: false
|
||
},
|
||
{
|
||
name: 'health',
|
||
subtitle: 'body, workouts, sleep, meds and vaccinations',
|
||
href: '/trackers/health',
|
||
external: false,
|
||
private: true
|
||
},
|
||
{
|
||
name: 'groceries',
|
||
subtitle: 'supermarket products, favorites and prices per store',
|
||
href: '/trackers/groceries',
|
||
external: false,
|
||
private: true
|
||
},
|
||
{
|
||
name: 'houses',
|
||
subtitle: 'rentals around Gouda, applications and alerts',
|
||
href: '/trackers/houses',
|
||
external: false,
|
||
private: true
|
||
},
|
||
{
|
||
name: 'holders',
|
||
subtitle: 'who has my address, email, phone and bank details, and a moving checklist',
|
||
href: '/trackers/data-holders',
|
||
external: false,
|
||
private: true
|
||
},
|
||
{
|
||
name: 'energy',
|
||
subtitle: 'Dutch electricity mix, utilization and gas storage',
|
||
href: '/projects/energy',
|
||
external: false,
|
||
private: false
|
||
},
|
||
{
|
||
name: 'games',
|
||
subtitle: 'every pokemon in every game, and call of duty camo mastery',
|
||
href: '/trackers/games',
|
||
external: false,
|
||
children: gameTrackers,
|
||
private: false
|
||
},
|
||
{
|
||
name: 'skins',
|
||
subtitle: 'Counter-Strike skin market prices across time',
|
||
href: '/projects/cs-prices',
|
||
external: false,
|
||
private: false
|
||
},
|
||
{
|
||
name: 'pets',
|
||
subtitle: 'animaldex: add neighborhood pets to the world map',
|
||
href: '/projects/animaldex',
|
||
external: false,
|
||
private: false
|
||
},
|
||
{
|
||
name: 'lists',
|
||
subtitle: 'books, games, places and other things worth keeping',
|
||
href: '/lists',
|
||
external: false,
|
||
private: false
|
||
}
|
||
];
|
||
|
||
const strip = ({
|
||
name,
|
||
subtitle,
|
||
href,
|
||
external,
|
||
children,
|
||
locked,
|
||
private: hidden
|
||
}: Tracker & { private: boolean }): Tracker => ({
|
||
name,
|
||
subtitle,
|
||
href,
|
||
external,
|
||
...(children ? { children } : {}),
|
||
...(locked || hidden ? { locked: true } : {})
|
||
});
|
||
/** Public trackers (sitemap, logged-out navigation). */
|
||
export const trackers: readonly Tracker[] = allTrackers.filter((t) => !t.private).map(strip);
|
||
/** Owner-only trackers. */
|
||
export const privateTrackers: readonly Tracker[] = allTrackers.filter((t) => t.private).map(strip);
|
||
const ownerTrackers: readonly Tracker[] = allTrackers.map(strip);
|
||
|
||
/** Trackers for the header, home, /trackers, /start and error pages; with `inactiveGames` (root
|
||
* layout data) the games group lists active games first and flags the rest `inactive`. */
|
||
export function visibleTrackers(
|
||
isOwner: boolean,
|
||
inactiveGames: readonly string[] = []
|
||
): readonly Tracker[] {
|
||
const list = isOwner ? ownerTrackers : trackers;
|
||
if (!inactiveGames.length) return list;
|
||
const games = sortedGames(inactiveGames);
|
||
return list.map((t) => (t.children === gameTrackers ? { ...t, children: games } : t));
|
||
}
|