Per-game caught breakdown (#/progress)

New view (linked from Settings > Your data): the unified caught record
counted against every game's dex at once — National total on top, then
each version group grouped by generation with a seen/caught bar and
X / Y count. Tapping a game switches the active version group and jumps
to the dex.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
This commit is contained in:
chris 2026-09-09 18:52:01 -04:00
parent 5f6414528e
commit 53f4f4381d
5 changed files with 188 additions and 1 deletions

View File

@ -9,7 +9,7 @@ framework.
| 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`. |
| **Unified Pokémon selection** | One tracking record per Pokémon, keyed by **National Dex id**. Mark Pikachu once and every game's view reflects it — each dex computes its own "Seen / Caught" totals by intersecting its species list with that single map. The active Pokémon is also a single deep-linkable route (`#/pokemon/25`). |
| **Unified Pokémon selection** | One tracking record per Pokémon, keyed by **National Dex id**. Mark Pikachu once and every game's view reflects it — each dex computes its own "Seen / Caught" totals by intersecting its species list with that single map. `#/progress` breaks that down for every game at once. The active Pokémon is also a single deep-linkable route (`#/pokemon/25`). |
## Data & storage split

View File

@ -10,6 +10,7 @@ import { TeamView } from './views/TeamView.js';
import { NaturesView } from './views/NaturesView.js';
import { TypeChartView } from './views/TypeChartView.js';
import { CompareView } from './views/CompareView.js';
import { ProgressView } from './views/ProgressView.js';
import { detailSkeleton, lookupSkeleton } from './components/skeletons.js';
import { prefersReducedMotion } from './store/settings.js';
@ -23,6 +24,7 @@ const routes = [
{ pattern: /^#\/natures$/, view: () => NaturesView() },
{ pattern: /^#\/types$/, view: () => TypeChartView() },
{ pattern: /^#\/compare$/, view: () => CompareView() },
{ pattern: /^#\/progress$/, view: () => ProgressView() },
{ pattern: /^#\/search$/, view: () => SearchView() },
{ pattern: /^#\/settings$/, view: () => SettingsView() },
];

View File

@ -3014,3 +3014,88 @@
.compareview__body {
margin-top: 16px;
}
/* ---- Progress by game -------------------------------------- */
.prog {
display: flex;
flex-direction: column;
gap: 6px;
margin-top: 12px;
}
.prog__gen {
font-size: 0.72rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--text-dim);
margin: 12px 0 2px;
}
.prog__row {
display: grid;
grid-template-columns: 8.5rem 1fr 3.6rem;
align-items: center;
gap: 10px;
width: 100%;
padding: 8px 10px;
border: none;
border-radius: 10px;
background: var(--surface-2);
color: inherit;
font: inherit;
text-align: left;
cursor: pointer;
}
.prog__row--nat {
background: color-mix(in srgb, var(--accent) 14%, var(--surface-2));
cursor: default;
font-weight: 700;
}
.prog__name {
font-size: 0.82rem;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.prog__track {
position: relative;
height: 8px;
border-radius: 999px;
background: var(--surface);
overflow: hidden;
}
.prog__seen,
.prog__caught {
position: absolute;
inset: 0 auto 0 0;
border-radius: 999px;
}
.prog__seen {
background: color-mix(in srgb, var(--accent) 35%, transparent);
}
.prog__caught {
background: var(--good);
}
.prog__count {
font-size: 0.74rem;
font-weight: 700;
font-variant-numeric: tabular-nums;
color: var(--text-dim);
text-align: right;
}
.prog__legend {
display: flex;
align-items: center;
gap: 4px;
font-size: 0.75rem;
color: var(--text-dim);
}
.prog__key {
display: inline-block;
width: 1.1rem;
height: 0.6rem;
border-radius: 999px;
vertical-align: middle;
}
.prog__key--caught { background: var(--good); }
.prog__key--seen { background: color-mix(in srgb, var(--accent) 35%, transparent); }

94
src/views/ProgressView.js Normal file
View File

@ -0,0 +1,94 @@
import { el, clear, onTeardown } from '../lib/dom.js';
import { loadSnapshot } from '../data/snapshot.js';
import { settings } from '../store/settings.js';
import { selection, stats } from '../store/selection.js';
import { dexesForVersionGroup, versionGroupsByGeneration } from '../data/pokedex-resolver.js';
function bar(seen, caught, total) {
const pc = total ? (caught / total) * 100 : 0;
const ps = total ? (seen / total) * 100 : 0;
const track = el('div', { class: 'prog__track' });
const seenEl = el('div', { class: 'prog__seen' });
const caughtEl = el('div', { class: 'prog__caught' });
seenEl.style.width = `${ps}%`;
caughtEl.style.width = `${pc}%`;
track.append(seenEl, caughtEl);
return track;
}
export async function ProgressView() {
const view = el('section', { class: 'view lookup progressview' });
const snap = await loadSnapshot();
const speciesIdsFor = (vgKey) => {
const ids = new Set();
for (const dex of dexesForVersionGroup(snap, vgKey)) {
for (const [sid] of dex.entries) ids.add(sid);
}
return [...ids];
};
const list = el('div', { class: 'prog' });
function render() {
clear(list);
// National total
const nat = stats(snap.species.map((s) => s.id));
list.append(
el(
'div',
{ class: 'prog__row prog__row--nat' },
el('span', { class: 'prog__name' }, 'National Dex'),
bar(nat.seen, nat.caught, nat.total),
el('span', { class: 'prog__count' }, `${nat.caught} / ${nat.total}`),
),
);
for (const { generation, versionGroups } of versionGroupsByGeneration(snap)) {
list.append(el('h3', { class: 'prog__gen' }, `Gen ${generation.id} · ${generation.name}`));
for (const vg of versionGroups) {
const s = stats(speciesIdsFor(vg.key));
list.append(
el(
'button',
{
type: 'button',
class: 'prog__row',
onclick: () => {
settings.set({ versionGroup: vg.key, pokedex: null });
location.hash = '#/';
},
},
el('span', { class: 'prog__name' }, vg.name),
bar(s.seen, s.caught, s.total),
el('span', { class: 'prog__count' }, `${s.caught} / ${s.total}`),
),
);
}
}
}
render();
clear(view).append(
el('nav', { class: 'lookup__nav' }, el('a', { class: 'link', href: '#/settings' }, ' Settings')),
el(
'header',
{ class: 'view__header' },
el('h1', {}, 'Progress by game'),
el('p', {}, 'Your one unified caught record, counted against each games dex. Tap a game to switch to it.'),
),
el(
'p',
{ class: 'prog__legend' },
el('span', { class: 'prog__key prog__key--caught' }),
' caught ',
el('span', { class: 'prog__key prog__key--seen' }),
' seen',
),
list,
);
const off = selection.subscribe(render);
onTeardown(view, () => off());
return view;
}

View File

@ -182,6 +182,12 @@ export async function SettingsView() {
),
el('h2', {}, 'Your data'),
el(
'p',
{ class: 'settings__note' },
el('a', { class: 'link', href: '#/progress' }, 'Progress by game'),
' — caught totals against every games dex.',
),
storageNote,
el('div', { class: 'settings__actions' },
el('button', {