Skip to content

feat(search): search games on /search and in the navbar quick search - #58

Merged
kris1027 merged 22 commits into
mainfrom
feat/game-search-filters
Oct 26, 2026
Merged

kris1027 merged 22 commits into
mainfrom
feat/game-search-filters

Conversation

@kris1027

Copy link
Copy Markdown
Owner

What

This adds games to search. /search?type=game finds games by title and lets you narrow them by genre, platform, release year and minimum IGDB rating. The navbar quick search now works on every games page too. It all sits behind NEXT_PUBLIC_GAMES_LIBRARY, so production does not change.

Why

This builds scope feature G7 from spec 0029 (rationale). Until now the games tabs and pages had no search, a gap spec 0027 had accepted. IGDB applies every filter inside the search query and returns an exact count. So games need no page scan and no partial counts: every result meets every filter, and every count is IGDB's own (AGENTS.md section 10).

Changes

URL and modes (lib/search/)

  • SearchType is now LibraryType (tv | movie | game). game is accepted only while the flag is on, and the flag is read only when the raw type is game. That way an invalid flag value can never break shows or movies search.
  • Every TMDB read still takes MediaType, so the compiler makes you narrow the type before each call, and game can never reach TMDB.
  • New platform parameter, repeatable, up to 20 ids. On tv or movie it is invalid. searchHref writes platforms sorted after genres, so one search has one URL.
  • Games have three modes, decided by the URL alone: browse (IGDB Popular, the same cached list /games/popular uses), discover (filters only, most rated first, unrated games after them and still counted) and search (a title plus any filters, in IGDB relevance order).
  • New lib/search/game-read.ts (server only) for the three reads.
  • The count line uses IGDB's exact {n} games, and is left out when IGDB sends no count. Pages stop at 50, and a page above 50 is refused before any IGDB request.

IGDB module (lib/igdb/). Callers from G6 pass none of these options and work as before.

  • cond.allOf writes genres = [ids], so several genres mean all of. Platforms keep any of.
  • minRatingCount moves into GameFilters, so search accepts it too. Its condition is now written once, in sharedConditions.
  • searchGames accepts pageSize 20 or 24.
  • discoverGames accepts requireSortField (default true).
  • pnpm igdb:live gets two new checks: a two genre filter returns only games with both genres, and unrated games come after rated ones and are counted.

Filter bar (components/search/)

  • On type=game the bar shows the IGDB genres (MUST HAVE ALL OF), a new PlatformPicker (ON ANY OF, a filter box that gets focus when it opens, No platforms match), years from next year down to 1950, and Any IGDB rating with 5+ to 9+. A note, Only games with at least 10 IGDB ratings, shows when a rating is set.
  • Both pickers stop at 20 (Up to 20). Checkbox toggles wait 400 ms after the last click, so several quick clicks make one IGDB read. Hidden inputs keep the selection if you submit before the page hydrates.
  • If the genre or platform list fails to load, only that picker is disabled. The rest of the bar keeps working.
  • New games copy for the empty, invalid and Couldn't reach IGDB states, each with a retry or clear path.

Navigation and quick search

  • The navbar search now reads libraryType through WithLibraryTypedPage (WithTypedPage is removed). It shows Search games on /games*, on the library pages with type=game and on /search?type=game.
  • On /search the GAMES tab switches type in place. It keeps q, rating and any year the new type can take, and drops genre, platform and page. On /search?type=game the logo links to /games.
  • GET /api/search accepts type=game when the flag is on and returns the first 5 games from the cached IGDB search page 1. IgdbError answers 502, and IgdbValidationError answers 400, never 500.
  • Response shape change: tmdbRating is renamed rating for every type, and totalResults is now number | null.
  • Quick search rows show the game cover, year and an amber IGDB star.

Shared and showcase

  • Refactor: the 24 game page slice and the last page rule move into lib/catalog-list-read.ts, shared by the list pages and search.
  • GameListCard takes an optional meta line (the release year).
  • /showcase gets a Game search section: the platform picker with a filter typed, No platforms match, Platforms unavailable, and quick search rows with and without a cover.
  • Specs 0010, 0023, 0027 and 0028 each get a one line pointer to spec 0029.

How to test / verify

You can run these with pnpm dev:docker, NEXT_PUBLIC_GAMES_LIBRARY=on and the IGDB credentials in .env.local. The full checklist is in verify.md, and every item there is ticked.

  • pnpm typecheck && pnpm lint && pnpm test, pnpm igdb:live, pnpm build. /search, /games and the five /games/* list pages should be partially prerendered (◐).
  • Open /search?type=game&q=zelda. You should see 24 cards, {n} games and numbered pages, and page 2 shares no game with page 1.
  • Open /search?type=game&q=mario&genre=8&platform=130&platform=508&rating=7. Every card should be a Platform game on Switch or Switch 2 rated 7 or more.
  • Open /search?type=game. You should see Popular games on IGDB and the same first 24 games as /games/popular.
  • Try ?type=game&platform=0, ?type=game&year=1949, ?type=movie&platform=6 and ?type=game&q=%01. Each should show That filter isn't valid naming the parameter. ?type=game&page=51 should show That page doesn't exist.
  • Open Platforms and type switch, then zzz. Tick 20 platforms, and the rest should disable.
  • On /search?type=movie&q=dune&year=1940&genre=878, press GAMES. You should land on /search?type=game&q=dune.
  • Type hollow in the navbar on /games/top-rated. You should get game rows and See all to /search?type=game&q=hollow.
  • curl -i 'http://localhost:3000/api/search?type=game&q=zelda' should give 200 with public, s-maxage=300 and rating fields. q=%01%01 should give 400.
  • Turn the flag off and restart. /search?type=game should show the invalid type panel, /api/search?type=game should answer 400, and there should be no GAMES tab.

Risk & rollout

No migrations and nothing stored. The games paths are behind NEXT_PUBLIC_GAMES_LIBRARY, which production leaves unset. Shows and movies search are touched in two places. First, SearchType is wider, and the TMDB reads are kept narrowed by type. Second, the quick search response has changed: tmdbRating is now rating and totalResults can be null. The only consumer is the app's own quick search, updated here. Rollback is a plain revert.

Known tradeoffs the spec accepts:

  • Within the unrated tail of discover mode, IGDB's order is not documented, so a game there may repeat or be skipped across pages.
  • The 10 rating floor is much lower than TMDB's 100 vote floor.
  • Paging reaches at most 1,200 games.

Open follow up: each new filter combination is an uncached IGDB read against the shared 4 per second limiter, and the Vercel Firewall rule covers only /api/search. The spec leaves that decision for before G10.

Notes for reviewers

  • Genres mean all of and platforms mean any of. The picker legends carry that, but it is two rules in one bar, so it deserves a look.
  • The places that are easy to get wrong: the narrowing that keeps game away from TMDB (lib/search/params.ts, components/search/search-results.tsx), and the flag being read only for a raw type=game.
  • WithTypedPage is removed. The navbar search now shares WithLibraryTypedPage with the tabs. On /games/{id} and the typed paths, Search shows shows once before the type resolves (the brief flash spec 0027 already accepted).

🤖 Generated with Claude Code

kris1027 and others added 12 commits October 25, 2026 19:12
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Genres now write IGDB's [] list, minRatingCount is a shared filter so
search takes it too, search takes pageSize 24, and discover can leave
out the sort field condition so unrated games are counted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rating filters

type=game is accepted while the games library is on. Every filter is sent
to IGDB, so each game shown meets them and the count is IGDB's own; the
bar adds a platform picker with a filter box and labels the rating as
IGDB's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The navbar search, tabs and logo follow game on /search too: GAMES
switches /search in place, the quick search reads IGDB through
/api/search, and the quick result field tmdbRating becomes rating.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rows

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kris1027 kris1027 added docs Documentation additions and updates feature New user-facing functionality test Automated tests and test infrastructure ui Layouts, styling, and shared interface components labels Oct 26, 2026
@github-project-automation github-project-automation Bot moved this to Backlog in BeStats Oct 26, 2026
@kris1027 kris1027 self-assigned this Oct 26, 2026
@vercel

vercel Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
bestats Ready Ready Preview Oct 26, 2026 10:06am UTC

kris1027 and others added 8 commits October 26, 2026 10:29
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y builder

The control character pattern moves to lib/igdb/constants.ts, so the
/search parser (which the client filter bar imports) no longer pulls
query.ts into the browser bundle (lib/igdb/AGENTS.md, What lives where).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SEARCH_CATALOGS holds each type's minimum year, filter id cap, page cap
and quick search count endpoint, replacing scattered type === "game"
branches. Components read IGDB's limits through it instead of importing
lib/igdb/constants directly (lib/igdb/AGENTS.md, What lives where).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The results, the filter bar and /api/search each repeated the
type === "game" && getGamesLibrary() === "on" check; gameSearchEnabled
now holds it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- parseSearchParams and quickSearchRequestSchema take { gamesEnabled }
  rather than a bare boolean; without it the parse returns
  TmdbSearchParams, so the shows and movies branch needs no cast.
- parseGameSearchParams narrows a games parse at runtime, replacing the
  as GameSearchParams casts in the results and the filter bar.
- searchMode is an exhaustive switch, and its games overload leaves out
  filtered_search, so the "Games never filter locally" throw is gone
  (spec 0029, AC-1).
- settleList and readGameLists replace the two near identical list
  loaders in the results and the filter bar.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Spec 0029, AC-16 asks the loader test to prove the row's 42px sizes
resolves to a real IGDB_COVER_LADDER rung at 1x, 2x and 3x.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Spec 0029, AC-9: a typed query and a games list toggle made together
make one navigation whichever pause fires first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kris1027 and others added 2 commits October 26, 2026 11:04
…d row

AC-15 now says the static /games shells hold the real field, since no
fallback shows there; AC-16 says 2:3, the frame every card uses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kris1027
kris1027 merged commit 9de55ba into main Oct 26, 2026
4 checks passed
@github-project-automation github-project-automation Bot moved this from In review to Done in BeStats Oct 26, 2026
@kris1027
kris1027 deleted the feat/game-search-filters branch October 26, 2026 10:10

This branch was successfully deployed

1 active deployment
Preview — 60dafc78 Deployed Oct 26, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Documentation additions and updates feature New user-facing functionality test Automated tests and test infrastructure ui Layouts, styling, and shared interface components

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant