games: active vs non-active games (games.library.active, owner toggles on the dashboard and each game ⋯ menu; nothing deleted). Header flyout and mobile menu list every game split into active / non-active; /trackers and home tiles show active chips and open both sections on hover; /start shows active with a non-active toggle. Games dashboard moved from /projects/game-tracker to /trackers/games (dashboard of active games only; old URL and /credits 301); deep trackers stay put
All checks were successful
Deploy production / deploy (push) Successful in 1m13s

This commit is contained in:
2026-10-25 20:23:15 +00:00
parent 60370d4572
commit 3ad13e5828
31 changed files with 31471 additions and 565 deletions

View File

@@ -43,6 +43,8 @@ type Tracker = {
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;
};
@@ -82,6 +84,9 @@ export const spottingTrackers: readonly Tracker[] = [
// 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).
@@ -185,6 +190,66 @@ export const gameTrackers: readonly Tracker[] = [
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
@@ -273,7 +338,7 @@ const allTrackers: readonly (Tracker & { private: boolean })[] = [
{
name: 'games',
subtitle: 'every pokemon in every game, and call of duty camo mastery',
href: '/projects/game-tracker',
href: '/trackers/games',
external: false,
children: gameTrackers,
private: false
@@ -323,6 +388,14 @@ export const trackers: readonly Tracker[] = allTrackers.filter((t) => !t.private
export const privateTrackers: readonly Tracker[] = allTrackers.filter((t) => t.private).map(strip);
const ownerTrackers: readonly Tracker[] = allTrackers.map(strip);
export function visibleTrackers(isOwner: boolean): readonly Tracker[] {
return isOwner ? ownerTrackers : trackers;
/** 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));
}