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:
parent
5f6414528e
commit
53f4f4381d
@ -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
|
||||
|
||||
|
||||
@ -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() },
|
||||
];
|
||||
|
||||
@ -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
94
src/views/ProgressView.js
Normal 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 game’s 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;
|
||||
}
|
||||
@ -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 game’s dex.',
|
||||
),
|
||||
storageNote,
|
||||
el('div', { class: 'settings__actions' },
|
||||
el('button', {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user