All Lab Projects
PokéLocator logo

PokéLocator

Pokémon explorer with infinite scroll, filters, comparisons and dark mode

2026Next.js 16TypeScriptTailwind CSS v4TanStack Query v5Framer Motion v12Zustand v5next-themesLucide ReactVitestTesting LibraryPlaywrightESLint 9PrettierHuskyGitHub Actions

What I Built

  • Infinite scroll grid with TanStack Query useInfiniteQuery; type filter transparently swaps to a regular useQuery when PokéAPI returns a non-paginated full-type list, without changes to the UI layer
  • Client-side search: all ~1 300 Pokémon names fetched once (staleTime: Infinity) and filtered with a 300ms debounce — eliminates per-keystroke API round-trips
  • Static generation with generateStaticParams for the first 151 detail pages; server-side fetch cache set to revalidate: 86400 for the rest
  • Tailwind CSS v4 with the new @theme CSS token API: design tokens defined in :root / .dark and mapped to utility classes via @theme inline — no tailwind.config.js needed
  • Side-by-side Pokémon comparison (Zustand v5 + Persist middleware) reading from the existing TanStack Query cache — no duplicate network requests
  • Promise.allSettled on batch detail fetches: individual PokéAPI 404s for non-standard forms are swallowed without aborting the list render
  • Dark mode with next-themes: system preference honoured on first server render, user override persisted in localStorage, zero FOUC
  • Full quality pipeline: Vitest unit and integration tests, Playwright E2E, ESLint 9, Prettier, Husky pre-commit hooks, GitHub Actions CI

How It Works

  1. 1The list page runs useInfiniteQuery against PokéAPI, fetching 24 items at a time and batch-resolving each page's details in parallel with Promise.allSettled.
  2. 2Activating a type filter switches the data source to /type/{name} and replaces infinite scroll with a regular grid — the URL search param drives both the query key and the UI state.
  3. 3The search dropdown fetches all ~1 300 names once on mount and filters client-side with a 300ms debounce; results are displayed as a keyboard-navigable suggestions list.
  4. 4The comparison view reads both Pokémon from the existing Query cache — if a detail page was already visited, no new request is made.
  5. 5On the detail page, the evolution chain is resolved recursively from the species endpoint; stat bars animate in on mount with Framer Motion.