- 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>
179 lines
4.9 KiB
JavaScript
179 lines
4.9 KiB
JavaScript
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 1–2 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;
|
||
}
|