games: per-game pages, games.library table, games group in header/tiles/start
All checks were successful
Deploy production / deploy (push) Successful in 1m7s

This commit is contained in:
2026-10-25 15:20:45 +00:00
parent 037a525f91
commit d79b73ad83
19 changed files with 27712 additions and 38 deletions

View File

@@ -0,0 +1,51 @@
import { sql } from 'drizzle-orm';
import { check, integer, text } from 'drizzle-orm/pg-core';
import { gamesSchema } from './schemas';
import { id, timestamps } from './helpers';
// Every game I play (plus speedcubing): one row per game, shown at /trackers/games/<slug>.
// The header, home, /trackers and /start chips come from `gameTrackers` in #lib/projects
// (static, so navigation needs no query); keep the slugs in sync with these rows.
// Deep trackers (osu! journal, Call of Duty mastery, Pokémon catches) stay where they are
// and are linked through `tracker_href`.
export const GAME_STATUSES = ['main', 'active', 'occasional', 'unknown'] as const;
export const GAME_KINDS = ['video', 'puzzle'] as const;
export const gameLibrary = gamesSchema.table(
'library',
{
id: id(),
slug: text('slug').notNull().unique(),
name: text('name').notNull(),
shortName: text('short_name').notNull(),
kind: text('kind', { enum: GAME_KINDS }).notNull().default('video'),
status: text('status', { enum: GAME_STATUSES }).notNull().default('unknown'),
/** Where I play it (PC, Nintendo, physical…); null when not known yet. */
platform: text('platform'),
/** Internal path of a deeper tracker for this game, if there is one. */
trackerHref: text('tracker_href'),
/** Rank as a position (#40 000 is 40000); only values I gave. */
currentRank: integer('current_rank'),
peakRank: integer('peak_rank'),
/** What the rank numbers mean, e.g. "osu! standard global rank". */
rankLabel: text('rank_label'),
/** Names this game goes by elsewhere (hardware.sensitivities.game, old titles). */
aliases: text('aliases')
.array()
.notNull()
.default(sql`'{}'::text[]`),
notes: text('notes'),
sortOrder: integer('sort_order').notNull().default(100),
...timestamps
},
(t) => [
check('library_status_check', sql`${t.status} IN ('main','active','occasional','unknown')`),
check('library_kind_check', sql`${t.kind} IN ('video','puzzle')`),
check(
'library_rank_check',
sql`(${t.currentRank} IS NULL OR ${t.currentRank} > 0) AND (${t.peakRank} IS NULL OR ${t.peakRank} > 0)`
)
]
);
export type GameLibraryRow = typeof gameLibrary.$inferSelect;

View File

@@ -5,6 +5,7 @@ export * from './blog';
export * from './progress';
export * from './catch-em-all';
export * from './game-tracker';
export * from './game-library';
export * from './tracker-journal';
export * from './animaldex';
export * from './lists'; // meta registry; the lists themselves are runtime tables