Name the project Pocketdex; full README, CONTRIBUTING, LICENSE
Rename the app from the working-title "Pokédex" to Pocketdex across the nav brand, install toast, PWA manifest, index.html title/meta, and package.json. Generic in-universe uses of "Pokédex" (sub-dex label, "all Pokédex entries", save-import messages) are left as-is. Docs for going public: - README.md rewritten: quick start, full feature list, how-it-works (data/storage split, per-game tracking, routing), deployment, legal disclaimer, contributing pointer. - CONTRIBUTING.md: ground rules (no copyrighted assets, framework-free, offline-first), setup, project map, code style, common tasks, manual test checklist, PR conventions. - LICENSE: MIT (+ note that it covers source only, not Pokémon data). - .editorconfig, .github/PULL_REQUEST_TEMPLATE.md. - package.json: add "engines": node >=20. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
This commit is contained in:
parent
c1f6007b25
commit
31cc076c0c
12
.editorconfig
Normal file
12
.editorconfig
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
25
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
25
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<!-- Thanks for contributing to Pocketdex! Keep PRs focused — one change per PR. -->
|
||||||
|
|
||||||
|
## What & why
|
||||||
|
|
||||||
|
<!-- What does this change, and what problem does it solve? Link any issue. -->
|
||||||
|
|
||||||
|
## How I tested
|
||||||
|
|
||||||
|
<!-- Manual steps you ran. See CONTRIBUTING.md → "Testing your change". -->
|
||||||
|
|
||||||
|
- [ ] `npm run build` succeeds
|
||||||
|
- [ ] Checked light and dark theme
|
||||||
|
- [ ] Checked a narrow (mobile) viewport
|
||||||
|
- [ ] Switched games and confirmed game-aware data still updates (if relevant)
|
||||||
|
|
||||||
|
## Screenshots / clip
|
||||||
|
|
||||||
|
<!-- Required for anything visual. Before/after if it's a tweak. -->
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
- [ ] No copyrighted assets committed (sprites, artwork, ROMs, save files)
|
||||||
|
- [ ] No new runtime dependencies (or discussed in an issue first)
|
||||||
|
- [ ] `dist/` and `src/data/snapshot.json` are not in the diff
|
||||||
|
- [ ] Matches the surrounding code style
|
||||||
123
CONTRIBUTING.md
Normal file
123
CONTRIBUTING.md
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
# Contributing to Pocketdex
|
||||||
|
|
||||||
|
Thanks for taking the time. This is a small, framework-free codebase and
|
||||||
|
easy to get into.
|
||||||
|
|
||||||
|
## Ground rules
|
||||||
|
|
||||||
|
- **No copyrighted assets.** Don't commit Pokémon sprites, artwork, audio,
|
||||||
|
ROMs or save files. Everything visual is fetched at runtime from PokéAPI.
|
||||||
|
PRs that add such files will be asked to remove them.
|
||||||
|
- **Non-commercial.** No ads, analytics, telemetry, paid tiers, affiliate
|
||||||
|
links or "sign in" flows.
|
||||||
|
- **Stay framework-free.** No React/Vue/Svelte/etc., and no new runtime
|
||||||
|
dependencies without discussing it in an issue first. Build-time dev
|
||||||
|
dependencies are a lower bar but still worth raising.
|
||||||
|
- **Offline-first stays true.** The list, search, sort, filters and game
|
||||||
|
switching must keep working with no network. New always-needed data goes
|
||||||
|
in the snapshot, not a runtime fetch.
|
||||||
|
- **Be kind.** Assume good faith in issues and reviews.
|
||||||
|
|
||||||
|
## Getting set up
|
||||||
|
|
||||||
|
Requires **Node 20+**.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run snapshot # build src/data/snapshot.json from PokéAPI (git-ignored)
|
||||||
|
npm run dev # http://localhost:5173
|
||||||
|
```
|
||||||
|
|
||||||
|
To exercise the service worker, offline behaviour or the install prompt,
|
||||||
|
you need a real build:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build && npm run preview
|
||||||
|
```
|
||||||
|
|
||||||
|
## Where things live
|
||||||
|
|
||||||
|
See the **Project layout** and **How it works** sections in
|
||||||
|
[README.md](README.md). In short:
|
||||||
|
|
||||||
|
- `src/views/<Name>.js` — one file per route, an `async` function returning
|
||||||
|
a DOM node. Register it in `src/router.js`.
|
||||||
|
- `src/components/` — reusable pieces (`Card`, `Sprite`, `TypeChip`, …).
|
||||||
|
- `src/store/` — `createStore()`-backed reactive state, persisted to
|
||||||
|
`localStorage` under `pdx.*`.
|
||||||
|
- `src/lib/` — framework-y helpers with no app knowledge (`dom`, `anim`,
|
||||||
|
`damage-calc`, `savedex`, …).
|
||||||
|
- `src/data/` — the snapshot loader, the Pokédex resolver, the lazy PokéAPI
|
||||||
|
client, the type chart, natures.
|
||||||
|
- `scripts/build-snapshot.mjs` — the only thing that talks to PokéAPI at
|
||||||
|
build time.
|
||||||
|
|
||||||
|
## Code style
|
||||||
|
|
||||||
|
There's no linter or formatter config yet — **match the surrounding
|
||||||
|
code**:
|
||||||
|
|
||||||
|
- 2-space indent, semicolons, single quotes, trailing commas in
|
||||||
|
multi-line literals (an `.editorconfig` covers whitespace).
|
||||||
|
- Build DOM with the `el()` helper from `src/lib/dom.js`, not template
|
||||||
|
strings or `innerHTML` (except the deliberate `html:` prop for trusted
|
||||||
|
inline SVG).
|
||||||
|
- Stores: `set(fn)` **replaces** state — spread `...s` yourself. Read with
|
||||||
|
`get()`, react with `subscribe()`, and unsubscribe in the view's
|
||||||
|
teardown (`onTeardown(view, off)`).
|
||||||
|
- Keep comments about *why*, not *what*. Match the existing density.
|
||||||
|
- Respect `prefersReducedMotion()` for any new animation.
|
||||||
|
- Keep everything game-aware: if you touch typings, type effectiveness,
|
||||||
|
learnsets or evolution, honour the selected game's generation
|
||||||
|
(`typesForGen`, `multiplier(atk, def, gen)`, `past_values`, …).
|
||||||
|
|
||||||
|
## Common tasks
|
||||||
|
|
||||||
|
**Add a setting** — add the key + default to `src/store/settings.js`
|
||||||
|
(and `applyTheme()` if it affects the document), then a field in
|
||||||
|
`src/views/SettingsView.js`. Add it to the export payload if it should
|
||||||
|
survive a backup.
|
||||||
|
|
||||||
|
**Add a snapshot field** — add it in `scripts/build-snapshot.mjs`, run
|
||||||
|
`npm run snapshot -- --force`, and consume it via `loadSnapshot()`. Keep
|
||||||
|
the snapshot small; it's precached on every install.
|
||||||
|
|
||||||
|
**Add a route** — new file in `src/views/`, one entry in the `routes`
|
||||||
|
array in `src/router.js`. Add a skeleton if the view does async work
|
||||||
|
before it can render.
|
||||||
|
|
||||||
|
## Testing your change
|
||||||
|
|
||||||
|
There's no automated suite. Before opening a PR, manually check:
|
||||||
|
|
||||||
|
- The **dex grid** — filters, sort, the progress ring, catching from a
|
||||||
|
card.
|
||||||
|
- **Switching games** (the Games sheet) — regional numbers, era typings,
|
||||||
|
and per-game seen/caught all update.
|
||||||
|
- A **detail page** — every tab, form switching, the track buttons.
|
||||||
|
- **`npm run build`** succeeds, and `npm run preview` still works offline
|
||||||
|
(DevTools → Network → Offline, then reload).
|
||||||
|
- Light **and** dark theme, and a narrow (mobile) viewport.
|
||||||
|
|
||||||
|
Describe what you tested in the PR.
|
||||||
|
|
||||||
|
## Commits & pull requests
|
||||||
|
|
||||||
|
- Branch off `main`. One focused change per PR.
|
||||||
|
- Imperative commit subjects ("Add …", "Fix …"), with a body explaining
|
||||||
|
the *why* when it isn't obvious.
|
||||||
|
- Don't commit `dist/` or `src/data/snapshot.json` (both git-ignored).
|
||||||
|
- Fill in the PR template. Screenshots or a short clip for anything
|
||||||
|
visual.
|
||||||
|
- It's fine to open a draft PR early to discuss direction.
|
||||||
|
|
||||||
|
## Reporting bugs
|
||||||
|
|
||||||
|
Open an issue with: what you did, what you expected, what happened, the
|
||||||
|
browser/OS, and the selected game if it's relevant. A screenshot or the
|
||||||
|
console output helps a lot.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
By contributing you agree that your contributions are licensed under the
|
||||||
|
[MIT License](LICENSE).
|
||||||
28
LICENSE
Normal file
28
LICENSE
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Pocketdex contributors
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
This license applies to the Pocketdex source code only. Pokémon and all
|
||||||
|
related names, imagery and data are the property of Nintendo, Game Freak and
|
||||||
|
The Pokémon Company, and are served at runtime via PokéAPI
|
||||||
|
(https://pokeapi.co/). No such assets are included in this repository.
|
||||||
385
README.md
385
README.md
@ -1,157 +1,280 @@
|
|||||||
# Pokédex PWA
|
# Pocketdex
|
||||||
|
|
||||||
A responsive, offline-capable Pokédex reference built on
|
An offline-first **Pokédex, team builder and save-file tracker**, built on
|
||||||
[PokéAPI](https://pokeapi.co/). Vanilla JS + Vite + Workbox — no UI
|
[PokéAPI](https://pokeapi.co/). Vanilla JS + [Vite](https://vitejs.dev/) +
|
||||||
framework.
|
[Workbox](https://developer.chrome.com/docs/workbox) — no UI framework, no
|
||||||
|
runtime dependencies in the app bundle.
|
||||||
|
|
||||||
## Two headline features
|
- **Pick a game** and everything reshapes to it — regional numbering,
|
||||||
|
era-accurate typings, that generation's type chart and move data, the
|
||||||
|
right flavour text.
|
||||||
|
- **Track seen/caught per game.** Catching Pikachu in Blue says nothing
|
||||||
|
about your Gold save. `#/progress` shows each game's own tally.
|
||||||
|
- **Import a real save file** (`.sav` / `.srm` / `.dsv`), Gen 1–5, and read
|
||||||
|
the Pokédex straight out of it.
|
||||||
|
- **Works offline** after the first load, and installs as a PWA.
|
||||||
|
|
||||||
| Feature | How it works |
|
> **Not affiliated with Nintendo, Game Freak or The Pokémon Company.** This
|
||||||
| --- | --- |
|
> is a non-commercial fan reference tool. See [Legal](#legal).
|
||||||
| **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.
|
## Quick start
|
||||||
- **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
|
Requires **Node 20+**.
|
||||||
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
|
```bash
|
||||||
|
git clone https://github.com/OWNER/pocketdex.git
|
||||||
|
cd pocketdex
|
||||||
npm install
|
npm install
|
||||||
npm run snapshot # fetch data from PokéAPI -> src/data/snapshot.json (run once; refreshes if >30 days old)
|
npm run snapshot # fetch the data snapshot from PokéAPI (one time)
|
||||||
npm run dev # vite dev server
|
npm run dev # http://localhost:5173
|
||||||
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`.
|
| Script | What it does |
|
||||||
|
| --- | --- |
|
||||||
|
| `npm run snapshot` | Build `src/data/snapshot.json` from PokéAPI. Skips if the file is under 30 days old; `npm run snapshot -- --force` to rebuild anyway. |
|
||||||
|
| `npm run dev` | Vite dev server with HMR. |
|
||||||
|
| `npm run build` | Runs `snapshot` (as `prebuild`), then builds to `dist/`. |
|
||||||
|
| `npm run preview` | Serve the production build. **Use this to test the service worker / offline / install** — none of that runs under `npm run dev`. |
|
||||||
|
|
||||||
## Docker
|
The snapshot is **git-ignored** — it's a build artifact. `npm install`
|
||||||
|
doesn't create it; run `npm run snapshot` (or `npm run build`) first, or
|
||||||
|
the app has no data to show.
|
||||||
|
|
||||||
Multi-stage build (Node → nginx) that serves the static `dist/`:
|
---
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### Dex grid
|
||||||
|
Type-tinted cards — official artwork over a type-coloured spotlight, ghost
|
||||||
|
number, per-dex progress ring. Filter by name/number; chips for
|
||||||
|
caught / missing / favourites / legendary / has-a-note; a filter drawer for
|
||||||
|
type, generation, ability, egg group, minimum BST and "fully evolved only"
|
||||||
|
(all derived client-side from the snapshot — no extra fetches). Sort by dex
|
||||||
|
number, name, any base stat, BST, height, weight, base EXP, catch rate or
|
||||||
|
"recently caught". When a specific game is selected, cards show a
|
||||||
|
version-exclusive badge ("Ruby only"), derived from baked wild-encounter
|
||||||
|
data (so gift/trade-only exclusives are missed).
|
||||||
|
|
||||||
|
### Detail page
|
||||||
|
Type-gradient hero with a shared-element morph from the tapped card, then a
|
||||||
|
tabbed sheet:
|
||||||
|
|
||||||
|
- **About** — flavour text for the selected game (with an "all entries"
|
||||||
|
disclosure), physical data, abilities (each linking to its own page),
|
||||||
|
held items, egg groups, gender rate, catch rate, growth rate.
|
||||||
|
- **Stats** — colour-coded animated bars with an animated total.
|
||||||
|
- **Evolution** — the chain re-parented to the selected game's generation.
|
||||||
|
- **Moves** — the full learnset with per-move power / type / damage class /
|
||||||
|
accuracy / PP / effect, **era-accurate** via PokéAPI `past_values`
|
||||||
|
(including pre-Gen-4 physical/special-by-type). The TM/HM group is ordered
|
||||||
|
by machine number for the selected game.
|
||||||
|
- **Locations** — wild encounter locations for the selected game.
|
||||||
|
|
||||||
|
Forms (Mega, Gigantamax, regional, alternate formes) get a pill switcher
|
||||||
|
that rebuilds types / stats / matchups / abilities / learnset / artwork.
|
||||||
|
Purely cosmetic variants — Unown's letters, Vivillon's patterns, Alcremie's
|
||||||
|
decorations, Pikachu caps, seasonal Deerling, and so on — get a separate
|
||||||
|
compact **Appearance** dropdown, plus a per-species "form dex" checklist
|
||||||
|
tracked independently of the main caught flag.
|
||||||
|
|
||||||
|
### Team builder (`#/team`)
|
||||||
|
A lineup of up to 6 (specific forms included). Three modes:
|
||||||
|
|
||||||
|
- **Coverage** — leads with a *weak spots* summary (types that hit 2+
|
||||||
|
members, or that someone's weak to and nobody resists), then a full
|
||||||
|
defensive matchup grid with a diverging resist ◀│▶ weak bar per type,
|
||||||
|
offensive STAB gaps, and at-a-glance stats.
|
||||||
|
- **Compare** — the six stacked side by side. (The same table is also a
|
||||||
|
standalone `#/compare` view with its own picker, up to 4 Pokémon.)
|
||||||
|
- **Calc** — a damage calculator. Attacker + move + defender → 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. Links out to a **Natures** table, an interactive **Type chart**
|
||||||
|
(`#/types` — tap a type for its offensive + defensive breakdown, gen-aware)
|
||||||
|
and a **Breeding** helper (`#/breeding` — egg-group browser, compatibility
|
||||||
|
check, and egg moves resolved to the base breeding stage).
|
||||||
|
|
||||||
|
### Search (`#/search`)
|
||||||
|
Tabbed lookup — Pokémon, Moves, Items, 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
|
||||||
|
by TM/HM number when a machine filter is on. Items filter by category. The
|
||||||
|
move, ability and item detail pages each list the Pokémon connected to them
|
||||||
|
(learners / users / wild holders) as a filterable grid, narrowed to the
|
||||||
|
selected game. Query, scroll, tab and filters persist.
|
||||||
|
|
||||||
|
### Progress by game (`#/progress`)
|
||||||
|
Every game's seen/caught tally against its own dex, from its own per-game
|
||||||
|
bucket. Leads with the games you've played (selected at least once, or
|
||||||
|
imported a save for); the rest sit behind a toggle. The National Dex row is
|
||||||
|
the union across all games.
|
||||||
|
|
||||||
|
### Shiny hunt tracker (`#/shiny`)
|
||||||
|
Per-hunt counters with method-aware odds (full odds, Masuda, SOS, chain
|
||||||
|
fishing, DexNav, radar, dynamax adventures, outbreaks, mass outbreaks) and
|
||||||
|
Shiny Charm, cumulative-probability readout, notes, and a "found it" that
|
||||||
|
marks the Pokémon caught.
|
||||||
|
|
||||||
|
### Save-file import
|
||||||
|
**Settings → Import from a game save.** Reads the game's own seen/owned
|
||||||
|
bitfields — not an approximation — from:
|
||||||
|
|
||||||
|
| Gen | Games | Format |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 1 | Red / Blue / Yellow | 32 KB SRAM (`.sav` / `.srm`) |
|
||||||
|
| 2 | Gold / Silver / Crystal | 32 KB SRAM — checksum picks G/S vs Crystal |
|
||||||
|
| 3 | Ruby / Sapphire / Emerald, FireRed / LeafGreen | 128 KB — newer slot, rotated sections |
|
||||||
|
| 4 | Diamond / Pearl / Platinum, HeartGold / SoulSilver | 512 KB NDS — active slot, DeSmuME `.dsv` footer stripped |
|
||||||
|
| 5 | Black / White, Black 2 / White 2 | 512 KB NDS — per-form "seen" copies OR'd |
|
||||||
|
|
||||||
|
Gen 1–3 are checksum-verified; Gen 4–5 offsets come from PKHeX and are
|
||||||
|
validated structurally (nothing set past the last species; every caught
|
||||||
|
species is also seen). A dialog then offers **merge** or **replace** into
|
||||||
|
one game's bucket — the selected game if the save could be it, otherwise
|
||||||
|
the parser's best guess.
|
||||||
|
|
||||||
|
Full local state (settings, per-game tracking, team, form tracking, shiny
|
||||||
|
hunts, played games) also exports / imports as a single JSON file.
|
||||||
|
|
||||||
|
### Settings
|
||||||
|
Theme (System / Light / Dark / Black OLED / Sepia) + accent colour; text
|
||||||
|
size; an in-app reduce-motion override (the OS preference is always
|
||||||
|
respected too); sprite style (modern pixel / game-era pixel / official
|
||||||
|
artwork / HOME); haptic feedback on catch (Vibration API); which screen to
|
||||||
|
land on at launch; JSON export / import; cache and tracking resets.
|
||||||
|
|
||||||
|
### PWA
|
||||||
|
Workbox service worker: precache the app shell + snapshot; stale-while-
|
||||||
|
revalidate for PokéAPI JSON (30-day TTL); cache-first LRU for sprites;
|
||||||
|
in-app update toast. Installable with maskable icons, `beforeinstallprompt`
|
||||||
|
captured for an in-app offer, and manifest shortcuts to Team / Search /
|
||||||
|
Type chart.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
### Data & storage
|
||||||
|
|
||||||
|
| Layer | Where | Notes |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Preferences + tracking | `localStorage` (`pdx.*` keys) | Tiny, synchronous, restored on reload. |
|
||||||
|
| Build-time snapshot | `src/data/snapshot.json` (~150 KB) | Every species (id, name, generation, current + past typings, base stats, height/weight, species flags), every regional Pokédex, every version group, plus move / item / ability name indexes and baked TM numbers and version-exclusivity. Precached by the SW. |
|
||||||
|
| Detail data | `pokeapi.co`, fetched lazily per page | Stats, abilities, flavour text, evolution, encounters. Cached by the SW (stale-while-revalidate). |
|
||||||
|
| Sprites | PokéAPI sprites repo, cache-first | Capped LRU. |
|
||||||
|
|
||||||
|
The list, search, sort, filters and game switching run **entirely off the
|
||||||
|
snapshot** — no network until you open a detail page.
|
||||||
|
|
||||||
|
### Per-game tracking
|
||||||
|
|
||||||
|
`src/store/selection.js`:
|
||||||
|
|
||||||
|
```
|
||||||
|
pokemon: { [id]: { favorite, note } } // global — describe the Pokémon
|
||||||
|
games: { [vgKey]: { [id]: { seen, caught } } } // per game
|
||||||
|
```
|
||||||
|
|
||||||
|
`entry(id)` / `toggle(id, field)` / `stats(ids)` default to the selected
|
||||||
|
game; `favorite` and `note` are always global. The `all` key ("All games"
|
||||||
|
mode) reads as the **union** of every game — and is where an older
|
||||||
|
single-record store or JSON backup migrates to (`normalizeSelection`), so
|
||||||
|
nothing is falsely attributed to a game you never played.
|
||||||
|
|
||||||
|
### Routing & rendering
|
||||||
|
|
||||||
|
Hash router (`src/router.js`) over a small route table. Each view is an
|
||||||
|
`async` function that returns a DOM node; the router swaps it into `#view`
|
||||||
|
using the View Transitions API when available (and when the user hasn't
|
||||||
|
asked for reduced motion). Detail routes get a shaped skeleton first so the
|
||||||
|
tapped card's sprite can morph into the hero.
|
||||||
|
|
||||||
|
No framework. DOM is built with a ~30-line `el()` helper
|
||||||
|
(`src/lib/dom.js`). State is a handful of `localStorage`-backed reactive
|
||||||
|
stores from `createStore()` — `get()`, `set(patch | fn)`,
|
||||||
|
`subscribe(fn) → unsub`, `replace(next)`.
|
||||||
|
|
||||||
|
### Project layout
|
||||||
|
|
||||||
|
```
|
||||||
|
scripts/build-snapshot.mjs PokéAPI → snapshot.json
|
||||||
|
src/
|
||||||
|
main.js boot: theme, nav, router, SW registration, install prompt
|
||||||
|
router.js hash router + View Transitions
|
||||||
|
sw.js Workbox service worker (injectManifest)
|
||||||
|
store/ createStore + settings / selection / team / ui / …
|
||||||
|
data/ snapshot loader, pokedex resolver, lazy API client,
|
||||||
|
type chart, natures, game colours
|
||||||
|
lib/ dom, anim, damage-calc, savedex, swipe, dialog, haptics, …
|
||||||
|
components/ Card, Sprite, TypeChip, StatBar, CompareTable, Nav, …
|
||||||
|
views/ one file per route (DexGrid, PokemonDetail, TeamView, …)
|
||||||
|
styles/ tokens.css (palette, type colours, themes), layout.css
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
Static output — host `dist/` anywhere.
|
||||||
|
|
||||||
|
**Sub-path** (e.g. a GitHub Pages project site at `/pocketdex/`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
BASE_PATH=/pocketdex/ npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
**Docker** (multi-stage Node → nginx):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose up --build # → http://localhost:8080
|
docker compose up --build # → http://localhost:8080
|
||||||
# or without compose:
|
# or
|
||||||
docker build -t pokedex-pwa .
|
docker build -t pocketdex .
|
||||||
docker run --rm -p 8080:80 pokedex-pwa
|
docker run --rm -p 8080:80 pocketdex
|
||||||
```
|
```
|
||||||
|
|
||||||
The build stage runs `npm run build`, whose `prebuild` step fetches the
|
The build stage's `prebuild` fetches the snapshot, so `docker build` needs
|
||||||
PokéAPI snapshot — so `docker build` needs network access to `pokeapi.co`
|
network access to `pokeapi.co` unless a fresh `src/data/snapshot.json` is
|
||||||
unless a fresh `src/data/snapshot.json` is already present (it gets copied in
|
already present (it's copied in and reused). Sub-path:
|
||||||
and reused). Serving under a sub-path: `docker build --build-arg BASE_PATH=/dex/ .`.
|
`docker build --build-arg BASE_PATH=/pocketdex/ .`. `nginx.conf` sets the
|
||||||
`nginx.conf` sets the SPA fallback and long-cache headers for `/assets/*`
|
SPA fallback and long-cache headers for `/assets/*` while keeping
|
||||||
while keeping `index.html` / `sw.js` uncached.
|
`index.html` / `sw.js` uncached.
|
||||||
|
|
||||||
## Project layout
|
---
|
||||||
|
|
||||||
```
|
## Contributing
|
||||||
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
|
Issues and pull requests are welcome — see
|
||||||
|
**[CONTRIBUTING.md](CONTRIBUTING.md)** for setup, project conventions, and
|
||||||
|
what's in and out of scope.
|
||||||
|
|
||||||
Working app with a type-themed UI:
|
Known gaps: no interactive region map (PokéAPI has no map imagery or
|
||||||
|
coordinates), no automated test suite yet, and no formal Lighthouse pass.
|
||||||
|
|
||||||
- **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
|
## Legal
|
||||||
coordinates), and a formal Lighthouse pass.
|
|
||||||
|
|
||||||
## Notes
|
Pocketdex is an unofficial, non-commercial fan project. It is **not
|
||||||
|
affiliated with, endorsed by, or sponsored by** Nintendo, Game Freak, The
|
||||||
|
Pokémon Company, or PokéAPI.
|
||||||
|
|
||||||
- `npm audit` reports the known esbuild dev-server advisory via Vite 5. It
|
- Pokémon and all related names, sprites and artwork are **© Nintendo /
|
||||||
affects the local dev server only, not the production build. Vite 8 (which
|
Game Freak / The Pokémon Company**. They are served at runtime from
|
||||||
fixes it) requires a newer Node than this environment has.
|
PokéAPI and its sprite repository; **none are committed to this
|
||||||
- Data and images © Nintendo / Game Freak / The Pokémon Company, served via
|
repository**, and none should be added in a pull request.
|
||||||
PokéAPI. This project is a non-commercial reference tool.
|
- Game data is provided by [PokéAPI](https://pokeapi.co/) under its terms.
|
||||||
|
- The build-time snapshot (`snapshot.json`) is generated locally from
|
||||||
|
PokéAPI and is git-ignored.
|
||||||
|
|
||||||
|
If you represent a rights holder and have a concern, please open an issue.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[MIT](LICENSE) — applies to the Pocketdex source code only, not to any
|
||||||
|
Pokémon data or assets it displays.
|
||||||
|
|||||||
@ -4,7 +4,7 @@ services:
|
|||||||
context: .
|
context: .
|
||||||
args:
|
args:
|
||||||
BASE_PATH: /
|
BASE_PATH: /
|
||||||
image: pokedex-pwa
|
image: pocketdex
|
||||||
ports:
|
ports:
|
||||||
- "8080:80"
|
- "8080:80"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|||||||
@ -9,18 +9,18 @@
|
|||||||
<meta name="theme-color" content="#b3161a" />
|
<meta name="theme-color" content="#b3161a" />
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="A responsive, offline-capable Pokédex reference powered by PokéAPI."
|
content="Pocketdex — an offline-first Pokédex, team builder and save-file tracker powered by PokéAPI."
|
||||||
/>
|
/>
|
||||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
<meta name="apple-mobile-web-app-title" content="Pokédex" />
|
<meta name="apple-mobile-web-app-title" content="Pocketdex" />
|
||||||
<title>Pokédex</title>
|
<title>Pocketdex</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app" class="app">
|
<div id="app" class="app">
|
||||||
<noscript>This Pokédex needs JavaScript enabled.</noscript>
|
<noscript>Pocketdex needs JavaScript enabled.</noscript>
|
||||||
</div>
|
</div>
|
||||||
<script type="module" src="/src/main.js"></script>
|
<script type="module" src="/src/main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "pokedex-pwa",
|
"name": "pocketdex",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "A responsive, offline-capable Pokédex reference PWA powered by PokéAPI.",
|
"description": "An offline-first Pokédex, team builder and save-file tracker powered by PokéAPI. Vanilla JS + Vite + Workbox.",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"snapshot": "node scripts/build-snapshot.mjs",
|
"snapshot": "node scripts/build-snapshot.mjs",
|
||||||
"prebuild": "node scripts/build-snapshot.mjs",
|
"prebuild": "node scripts/build-snapshot.mjs",
|
||||||
@ -11,6 +11,9 @@
|
|||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^5.4.21",
|
"vite": "^5.4.21",
|
||||||
|
|||||||
@ -31,7 +31,7 @@ export function Nav() {
|
|||||||
? el('button', { class: 'nav__link', type: 'button', onclick: () => item.action() }, ...inner)
|
? el('button', { class: 'nav__link', type: 'button', onclick: () => item.action() }, ...inner)
|
||||||
: el('a', { class: 'nav__link', href: item.href }, ...inner);
|
: el('a', { class: 'nav__link', href: item.href }, ...inner);
|
||||||
});
|
});
|
||||||
nav.replaceChildren(el('span', { class: 'nav__brand' }, 'Pokédex'), ...links);
|
nav.replaceChildren(el('span', { class: 'nav__brand' }, 'Pocketdex'), ...links);
|
||||||
nav._items = items;
|
nav._items = items;
|
||||||
sync();
|
sync();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -68,7 +68,7 @@ let installOffered = false;
|
|||||||
onInstallChange((can) => {
|
onInstallChange((can) => {
|
||||||
if (!can || installOffered) return;
|
if (!can || installOffered) return;
|
||||||
installOffered = true;
|
installOffered = true;
|
||||||
showToast('Install this Pokédex for offline, full-screen use?', 'Install', () => {
|
showToast('Install Pocketdex for offline, full-screen use?', 'Install', () => {
|
||||||
promptInstall();
|
promptInstall();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -27,10 +27,10 @@ export default defineConfig({
|
|||||||
'apple-touch-icon.png',
|
'apple-touch-icon.png',
|
||||||
],
|
],
|
||||||
manifest: {
|
manifest: {
|
||||||
name: 'Pokédex',
|
name: 'Pocketdex',
|
||||||
short_name: 'Pokédex',
|
short_name: 'Pocketdex',
|
||||||
description:
|
description:
|
||||||
'A responsive, offline-capable Pokédex reference powered by PokéAPI.',
|
'An offline-first Pokédex, team builder and save-file tracker powered by PokéAPI.',
|
||||||
start_url: './',
|
start_url: './',
|
||||||
scope: './',
|
scope: './',
|
||||||
display: 'standalone',
|
display: 'standalone',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user