import { el } from './lib/dom.js'; import { DexGrid } from './views/DexGrid.js'; import { PokemonDetail } from './views/PokemonDetail.js'; import { SearchView } from './views/SearchView.js'; import { SettingsView } from './views/SettingsView.js'; import { MoveDetail } from './views/MoveDetail.js'; import { ItemDetail } from './views/ItemDetail.js'; import { AbilityDetail } from './views/AbilityDetail.js'; 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 { BreedingView } from './views/BreedingView.js'; import { ShinyView } from './views/ShinyView.js'; import { WhosThatView } from './views/WhosThatView.js'; import { detailSkeleton, lookupSkeleton } from './components/skeletons.js'; import { prefersReducedMotion } from './store/settings.js'; const routes = [ { pattern: /^#?\/?$/, view: () => DexGrid() }, { pattern: /^#\/pokemon\/(\d+)$/, view: (m) => PokemonDetail(Number(m[1])), skeleton: detailSkeleton, }, { pattern: /^#\/move\/(\d+)$/, view: (m) => MoveDetail(Number(m[1])), skeleton: lookupSkeleton }, { pattern: /^#\/item\/(\d+)$/, view: (m) => ItemDetail(Number(m[1])), skeleton: lookupSkeleton }, { pattern: /^#\/ability\/(\d+)$/, view: (m) => AbilityDetail(Number(m[1])), skeleton: lookupSkeleton, }, { pattern: /^#\/team$/, view: () => TeamView() }, { pattern: /^#\/natures$/, view: () => NaturesView() }, { pattern: /^#\/types$/, view: () => TypeChartView() }, { pattern: /^#\/compare$/, view: () => CompareView() }, { pattern: /^#\/progress$/, view: () => ProgressView() }, { pattern: /^#\/breeding$/, view: () => BreedingView() }, { pattern: /^#\/shiny$/, view: () => ShinyView() }, { pattern: /^#\/search$/, view: () => SearchView() }, { pattern: /^#\/settings$/, view: () => SettingsView() }, { pattern: /^#\/whos-that$/, view: () => WhosThatView() }, // hidden — Konami / logo taps ]; function resolve(hash) { for (const route of routes) { const match = hash.match(route.pattern); if (match) return { route, match }; } return { route: routes[0], match: [] }; } const canAnimate = () => typeof document.startViewTransition === 'function' && !prefersReducedMotion(); let rerenderCurrent = null; /** Re-run the active route (e.g. after the selected game changed). */ export function rerender() { if (rerenderCurrent) rerenderCurrent(); } export function initRouter(host) { let current = null; function mount(node, animate) { if (current) current.dispatchEvent(new CustomEvent('view:teardown')); current = node; if (animate && canAnimate()) { const t = document.startViewTransition(() => host.replaceChildren(node)); // A fast follow-up navigation skips the in-flight transition; that's // expected — swallow the AbortError from every promise it exposes. t?.ready?.catch(() => {}); t?.finished?.catch(() => {}); t?.updateCallbackDone?.catch(() => {}); } else { host.replaceChildren(node); } } async function render() { const hash = location.hash || '#/'; const { route, match } = resolve(hash); // Instant feedback: a shaped skeleton for detail pages, a light // placeholder otherwise. Detail skeletons animate in so the tapped // card's sprite can morph into the hero (shared-element transition). const isPokemon = /^#\/pokemon\/\d+$/.test(hash); mount( route.skeleton ? route.skeleton(match) : el('div', { class: 'view view--loading' }, 'Loading…'), isPokemon, ); window.scrollTo(0, 0); try { const node = await route.view(match); if ((location.hash || '#/') !== hash) return; // superseded // Skeleton/placeholder -> content morphs via the View Transitions API. mount(node, true); } catch (err) { console.error(err); mount( el( 'div', { class: 'view view--error' }, el('h1', {}, 'Something went wrong'), el('p', {}, err.message || String(err)), el('a', { href: '#/', class: 'button' }, 'Back to the dex'), ), false, ); } } rerenderCurrent = render; window.addEventListener('hashchange', render); render(); }