Catching a Pokémon in Blue was counting it as caught in Gold, Emerald and
every other game, because the whole app ran on one caught record keyed by
National Dex id. Progress by game just intersected that set with each
game's species list, so the per-game bars were meaningless.
selection store is now:
pokemon: { [id]: { favorite, note } } -- global (describe the mon)
games: { [vg]: { [id]: { seen, caught } } } -- per game
- entry(id) / toggle(id, field) / stats(ids) default to the selected game;
favorite/note stay global. 'all' (All-games mode) reads as the union of
every game -- "caught anywhere" -- and is a real bucket you write to when
no specific game is selected.
- v2 stores migrate on load (normalizeSelection): favorite/note lift out,
seen/caught drop into the 'all' bucket so nothing is falsely attributed
to a game. Same path runs on JSON backup import.
- ProgressView: each game row counts its own bucket; National row =
statsNational (union). Intro text slimmed.
- Save import writes into one game bucket -- the selected game if it's a
candidate for that save, else the parser's first guess -- and the dialog
names which game it's importing to.
- DexGrid feed (ring, filter counts, caught-sort), PokemonDetail track
buttons, search dots, shiny "found it" all follow the selected game.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
158 lines
9.1 KiB
Markdown
158 lines
9.1 KiB
Markdown
# Pokédex PWA
|
|
|
|
A responsive, offline-capable Pokédex reference built on
|
|
[PokéAPI](https://pokeapi.co/). Vanilla JS + Vite + Workbox — no UI
|
|
framework.
|
|
|
|
## Two headline features
|
|
|
|
| Feature | How it works |
|
|
| --- | --- |
|
|
| **Game series selection** | Pick a *game* (PokéAPI version group, e.g. Scarlet & Violet). If it has more than one regional dex (Paldea / Kitakami / Blueberry) a sub-dex switcher appears. The choice drives which species show, their regional numbering, and which version's flavor text the detail page uses. Stored in `localStorage`. |
|
|
| **Per-game Pokémon tracking** | Seen / caught are stored **per game** (version-group key) — catching Pikachu in Blue says nothing about your Gold save. `favorite` and `note` are global (they describe the Pokémon, keyed by National Dex id). The `all` bucket ("All games" mode) reads as the union — "caught anywhere" — and is where a pre-per-game save or an older backup migrates to. `#/progress` shows each game's own tally (National = the union), leading with the games you've played and tucking the rest behind a toggle. The active Pokémon is also a single deep-linkable route (`#/pokemon/25`). |
|
|
|
|
## Data & storage split
|
|
|
|
- **Preferences + tracking state** → `localStorage` (`pdx.settings`, `pdx.selection`). Tiny, synchronous, restored on reload.
|
|
- **Build-time snapshot** (`src/data/snapshot.json`, ~150 KB) → generated by `npm run snapshot` from PokéAPI: every species (id, name, generation, types), all regional Pokédex lists, and all version groups. Precached by the service worker, so the list / search / game switching work on the first offline launch.
|
|
- **Detail data** (stats, abilities, flavor text, evolution) → fetched lazily from `pokeapi.co` per Pokémon, then cached by the service worker (stale-while-revalidate, 30-day TTL).
|
|
- **Sprites** → cache-first with a capped LRU.
|
|
|
|
Export / import of all local state (settings, tracking, team, form
|
|
tracking, shiny hunts) as JSON lives in **Settings**, which also imports
|
|
the Pokédex straight out of a **game save file** (`.sav` / `.srm` /
|
|
`.dsv`): Gen 1 (R/B/Y), Gen 2 (G/S/C), Gen 3 (R/S/E, FR/LG), Gen 4
|
|
(D/P/Pt, HG/SS) and Gen 5 (B/W, B2/W2). It reads the game's own
|
|
seen/owned bitfields — checksum-picking Gold/Silver vs Crystal, Gen 3's
|
|
newer slot and rotated sections, Gen 4/5's active NDS slot (DeSmuME
|
|
footer stripped) and OR-ing their per-form "seen" copies — then a
|
|
dialog offers to merge or replace.
|
|
|
|
## Scripts
|
|
|
|
```bash
|
|
npm install
|
|
npm run snapshot # fetch data from PokéAPI -> src/data/snapshot.json (run once; refreshes if >30 days old)
|
|
npm run dev # vite dev server
|
|
npm run build # runs snapshot (prebuild) then vite build
|
|
npm run preview # serve the production build (needed to exercise the service worker)
|
|
```
|
|
|
|
Deploying under a sub-path (e.g. GitHub Pages project site): `BASE_PATH=/repo-name/ npm run build`.
|
|
|
|
## Docker
|
|
|
|
Multi-stage build (Node → nginx) that serves the static `dist/`:
|
|
|
|
```bash
|
|
docker compose up --build # → http://localhost:8080
|
|
# or without compose:
|
|
docker build -t pokedex-pwa .
|
|
docker run --rm -p 8080:80 pokedex-pwa
|
|
```
|
|
|
|
The build stage runs `npm run build`, whose `prebuild` step fetches the
|
|
PokéAPI snapshot — so `docker build` needs network access to `pokeapi.co`
|
|
unless a fresh `src/data/snapshot.json` is already present (it gets copied in
|
|
and reused). Serving under a sub-path: `docker build --build-arg BASE_PATH=/dex/ .`.
|
|
`nginx.conf` sets the SPA fallback and long-cache headers for `/assets/*`
|
|
while keeping `index.html` / `sw.js` uncached.
|
|
|
|
## Project layout
|
|
|
|
```
|
|
scripts/build-snapshot.mjs PokéAPI -> snapshot.json
|
|
src/
|
|
main.js boot: theme, nav, router, SW registration
|
|
router.js hash router (#/, #/pokemon/:id, #/games, #/search, #/settings)
|
|
sw.js Workbox service worker (injectManifest)
|
|
store/ createStore + settings + selection (localStorage)
|
|
data/ snapshot loader, pokedex resolver, lazy API client
|
|
components/ Card, Sprite, TypeChip, StatBar, ProgressHeader, Nav
|
|
views/ DexGrid, PokemonDetail, GamePicker, SearchView, SettingsView
|
|
styles/ tokens.css (palette, type colors, light/dark), layout.css
|
|
```
|
|
|
|
## Status
|
|
|
|
Working app with a type-themed UI:
|
|
|
|
- **Dex grid** — type-tinted cards (official artwork, spotlight, ghost number),
|
|
per-dex progress, name/number filter, caught/favorite filters, plus a
|
|
filter drawer: type, generation, ability, egg group, minimum BST, and
|
|
"fully evolved only" (derived client-side from each species' evolves-from
|
|
link — no extra fetches). A "Notes" chip filters to Pokémon with a note;
|
|
cards with one get a small 📝 mark. When a specific game is selected,
|
|
cards show a version-exclusive badge ("Ruby only") — derived from the
|
|
snapshot's baked wild-encounter data, so gift/trade-only exclusives are
|
|
missed.
|
|
- **Notes** — a free-text note per Pokémon on its detail page (trade plans,
|
|
where you caught it, anything), autosaving as you type and flushing
|
|
immediately on blur or navigation so a quick tab-away never drops a
|
|
keystroke. Rides along in the existing JSON export/import.
|
|
- **Forms** — Megas, Gigantamax, regional forms and alternate formes in the
|
|
snapshot; a pill switcher on the detail page rebuilds types / stats /
|
|
matchups / abilities / learnset / artwork for the chosen form. Purely
|
|
cosmetic variants (Unown's 27 letters, Vivillon's 19 patterns, Furfrou
|
|
trims, seasonal Deerling/Sawsbuck, Flabébé line colors, Alcremie's 62
|
|
decorations, Pikachu caps — 278 across 50 species) get a separate compact
|
|
"Appearance" dropdown that only swaps the sprite. Cards show a "+N forms"
|
|
badge.
|
|
- **Detail** — type-gradient hero + tabbed sheet (About / Stats / Evolution /
|
|
Moves / Locations); colored animated stat bars; defensive type matchups;
|
|
evolution chain re-parented to the selected game's generation; learnset with
|
|
per-move power/type/accuracy/PP/effect (era-accurate via `past_values`,
|
|
incl. pre-Gen-4 physical/special-by-type), the TM/HM group ordered by
|
|
TM number for the selected game; wild encounter locations.
|
|
- **Game-aware** — abilities gated to Gen 3+ (hidden to Gen 5+); type chart
|
|
applies Gen 1 / pre-Gen 6 rules.
|
|
- **Games** — overlay picker with stylised version-color cover tiles,
|
|
sub-dex switch, and an "All games" (National, no gen limits) option.
|
|
- **Search** — tabbed lookup: Pokémon, Moves, Items and Abilities, all
|
|
browsable offline from the snapshot. Moves filter by type / damage class
|
|
/ "TMs only" / "HMs only" for the selected game and sort by power /
|
|
accuracy / recency — or, with a machine filter on, by TM/HM number
|
|
(numbers are baked into the snapshot). Items filter by category. Each
|
|
has its own detail page; the move, ability and item pages list the
|
|
Pokémon connected to them (learners / users / wild holders) as a
|
|
filterable grid, narrowed to the selected game (by generation, or
|
|
exactly by version for held items). Query, scroll, tab and filters
|
|
persist.
|
|
- **Team** — a lineup of up to 6. Coverage leads with a "weak spots"
|
|
summary (types that hit 2+ members, or that someone's weak to and nobody
|
|
resists), then a full matchup grid with a diverging resist◀│▶weak bar per
|
|
type; plus offensive STAB gaps and at-a-glance stats. A Compare table
|
|
stacks the six side by side (the same table is also a standalone
|
|
`#/compare` view with its own picker — up to 4 Pokémon, persisted).
|
|
A third Calc mode is a damage calculator —
|
|
pick (or quick-pick from your team) an attacker + move + defender, and
|
|
get a damage range, % of HP, hits-to-KO, STAB/effectiveness badges, with
|
|
level/nature/EV controls; it's a base-stats estimate (31 IVs assumed,
|
|
no items/abilities/weather/terrain). All three follow the selected
|
|
game — its generation's type chart, era-accurate typings (pre-Gen-6
|
|
Clefairy is Normal, etc.), and era move data. Toggleable in the nav
|
|
from Settings. Links out to a Natures table and an interactive Type
|
|
chart (tap a type for its offensive + defensive breakdown; both
|
|
gen-aware).
|
|
- **Settings** — theme (System / Light / Dark / Black / Sepia) + accent
|
|
color, text size, an in-app reduce-motion override (on top of the OS
|
|
preference, which is always respected too), sprite style, haptic
|
|
feedback on catch (Vibration API), which screen to land on at launch,
|
|
JSON export/import.
|
|
- **PWA** — Workbox SW: precache shell + snapshot, SWR for API JSON,
|
|
cache-first LRU for sprites, in-app update toast. Installable: PNG +
|
|
maskable icons, `beforeinstallprompt` captured for an in-app "Install"
|
|
offer (toast + Settings button), manifest shortcuts to Team / Search /
|
|
Type chart.
|
|
|
|
Not yet done: an interactive region map (PokéAPI has no map imagery or
|
|
coordinates), and a formal Lighthouse pass.
|
|
|
|
## Notes
|
|
|
|
- `npm audit` reports the known esbuild dev-server advisory via Vite 5. It
|
|
affects the local dev server only, not the production build. Vite 8 (which
|
|
fixes it) requires a newer Node than this environment has.
|
|
- Data and images © Nintendo / Game Freak / The Pokémon Company, served via
|
|
PokéAPI. This project is a non-commercial reference tool.
|