dex/src/views/DexGrid.js
chris 404ca7dbbd Trim typings and matchups to the selected generation
- Detail page resolves a Pokemon's typing from PokeAPI past_types for the
  selected game's generation (Gen 1 Magnemite is pure Electric, pre-Gen 6
  Clefairy is Normal); feeds hero pills, theming and matchups
- Type chart excludes Steel/Dark (pre-Gen 2) and Fairy (pre-Gen 6) as
  attacking types entirely
- Gen 1-2 game-era sprites (opaque white bg) shown as a small framed tile
  on cards and the detail hero

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 12:27:02 -04:00

179 lines
4.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { el, clear, onTeardown } from '../lib/dom.js';
import { loadSnapshot } from '../data/snapshot.js';
import { resolvePokedex, dexRows, prettify } from '../data/pokedex-resolver.js';
import { settings } from '../store/settings.js';
import { selection, stats } from '../store/selection.js';
import { Card } from '../components/Card.js';
import { ProgressRing } from '../components/ProgressRing.js';
const FILTERS = [
{ key: 'all', label: 'All', test: () => true },
{ key: 'caught', label: 'Caught', test: (e) => e.caught },
{ key: 'missing', label: 'Missing', test: (e) => !e.caught },
{ key: 'favorite', label: 'Favorites', test: (e) => e.favorite },
];
const SEARCH_ICON =
'<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>';
export async function DexGrid() {
const view = el('section', { class: 'view dexgrid' });
const snap = await loadSnapshot();
const grid = el('div', { class: 'grid' });
let query = '';
let filterKey = 'all';
let rows = [];
let ids = [];
const ring = ProgressRing();
const title = el('h1', {});
const gamePill = el(
'a',
{ class: 'game-pill', href: '#/games' },
el('span', {}),
el('span', { class: 'game-pill__chev', 'aria-hidden': 'true' }, '⌄'),
);
const search = el('input', {
class: 'field-search__input',
type: 'search',
placeholder: 'Filter this Pokédex…',
oninput: (e) => {
query = e.target.value.trim().toLowerCase();
paintGrid();
},
});
const filterBar = el('div', { class: 'feed-chips' });
const chipEls = FILTERS.map((f) => {
const count = el('span', { class: 'feed-chip__count' });
const btn = el(
'button',
{
class: 'feed-chip',
type: 'button',
onclick: () => {
filterKey = f.key;
chipEls.forEach((c, i) => c.btn.classList.toggle('is-active', FILTERS[i].key === f.key));
paintGrid();
},
},
el('span', {}, f.label),
count,
);
filterBar.append(btn);
return { btn, count };
});
chipEls[0].btn.classList.add('is-active');
view.append(
el(
'header',
{ class: 'feed-head' },
el(
'div',
{ class: 'feed-head__top' },
el('div', { class: 'feed-head__id' }, title, gamePill),
ring.node,
),
el(
'div',
{ class: 'field-search' },
el('span', { class: 'field-search__icon', html: SEARCH_ICON, 'aria-hidden': 'true' }),
search,
),
filterBar,
),
grid,
);
function counts() {
const p = selection.get().pokemon;
let caught = 0;
let favorite = 0;
for (const id of ids) {
const e = p[id];
if (!e) continue;
if (e.caught) caught++;
if (e.favorite) favorite++;
}
return { all: ids.length, caught, missing: ids.length - caught, favorite };
}
function refreshMeta() {
ring.update(stats(ids));
const c = counts();
chipEls.forEach(({ count }, i) => {
count.textContent = c[FILTERS[i].key];
});
}
function paintGrid() {
const st = settings.get();
const pokemonState = selection.get().pokemon;
// Gen 12 game-era sprites have opaque white backgrounds — frame them.
const boxed =
st.spriteStyle === 'game' &&
(snap.versionGroupByKey.get(st.versionGroup)?.generation ?? 9) <= 2;
const filterTest = FILTERS.find((f) => f.key === filterKey).test;
const frag = document.createDocumentFragment();
let shown = 0;
for (const { species, number } of rows) {
if (
query &&
!species.name.includes(query) &&
String(number) !== query &&
String(species.id) !== query
) {
continue;
}
const e = pokemonState[species.id] || { seen: false, caught: false, favorite: false };
if (!filterTest(e)) continue;
const card = Card(species, number, {
spriteStyle: st.spriteStyle,
versionGroup: st.versionGroup,
boxed,
});
if (shown < 30) card.style.setProperty('--i', String(shown));
frag.append(card);
shown++;
}
clear(grid);
grid.append(
shown === 0
? el('p', { class: 'grid__empty' }, 'Nothing matches those filters.')
: frag,
);
}
function rebuild() {
const st = settings.get();
const dex = resolvePokedex(snap, st);
rows = dexRows(snap, dex);
ids = rows.map((r) => r.species.id);
title.textContent = prettify(dex.name || dex.key);
gamePill.firstChild.textContent = prettify(st.versionGroup);
refreshMeta();
paintGrid();
}
rebuild();
const offSettings = settings.subscribe(rebuild);
const offSelection = selection.subscribe(() => {
refreshMeta();
if (filterKey !== 'all') paintGrid();
});
onTeardown(view, () => {
offSettings();
offSelection();
});
return view;
}