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 |
|
| 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`. |
|
| **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
|
## Data & storage split
|
||||||
|
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import { TeamView } from './views/TeamView.js';
|
|||||||
import { NaturesView } from './views/NaturesView.js';
|
import { NaturesView } from './views/NaturesView.js';
|
||||||
import { TypeChartView } from './views/TypeChartView.js';
|
import { TypeChartView } from './views/TypeChartView.js';
|
||||||
import { CompareView } from './views/CompareView.js';
|
import { CompareView } from './views/CompareView.js';
|
||||||
|
import { ProgressView } from './views/ProgressView.js';
|
||||||
import { detailSkeleton, lookupSkeleton } from './components/skeletons.js';
|
import { detailSkeleton, lookupSkeleton } from './components/skeletons.js';
|
||||||
import { prefersReducedMotion } from './store/settings.js';
|
import { prefersReducedMotion } from './store/settings.js';
|
||||||
|
|
||||||
@ -23,6 +24,7 @@ const routes = [
|
|||||||
{ pattern: /^#\/natures$/, view: () => NaturesView() },
|
{ pattern: /^#\/natures$/, view: () => NaturesView() },
|
||||||
{ pattern: /^#\/types$/, view: () => TypeChartView() },
|
{ pattern: /^#\/types$/, view: () => TypeChartView() },
|
||||||
{ pattern: /^#\/compare$/, view: () => CompareView() },
|
{ pattern: /^#\/compare$/, view: () => CompareView() },
|
||||||
|
{ pattern: /^#\/progress$/, view: () => ProgressView() },
|
||||||
{ pattern: /^#\/search$/, view: () => SearchView() },
|
{ pattern: /^#\/search$/, view: () => SearchView() },
|
||||||
{ pattern: /^#\/settings$/, view: () => SettingsView() },
|
{ pattern: /^#\/settings$/, view: () => SettingsView() },
|
||||||
];
|
];
|
||||||
|
|||||||
@ -3014,3 +3014,88 @@
|
|||||||
.compareview__body {
|
.compareview__body {
|
||||||
margin-top: 16px;
|
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('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,
|
storageNote,
|
||||||
el('div', { class: 'settings__actions' },
|
el('div', { class: 'settings__actions' },
|
||||||
el('button', {
|
el('button', {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user