dex/src/main.js
chris 31cc076c0c Name the project Pocketdex; full README, CONTRIBUTING, LICENSE
Rename the app from the working-title "Pokédex" to Pocketdex across the
nav brand, install toast, PWA manifest, index.html title/meta, and
package.json. Generic in-universe uses of "Pokédex" (sub-dex label, "all
Pokédex entries", save-import messages) are left as-is.

Docs for going public:
- README.md rewritten: quick start, full feature list, how-it-works
  (data/storage split, per-game tracking, routing), deployment, legal
  disclaimer, contributing pointer.
- CONTRIBUTING.md: ground rules (no copyrighted assets, framework-free,
  offline-first), setup, project map, code style, common tasks, manual
  test checklist, PR conventions.
- LICENSE: MIT (+ note that it covers source only, not Pokémon data).
- .editorconfig, .github/PULL_REQUEST_TEMPLATE.md.
- package.json: add "engines": node >=20.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
2026-09-10 11:03:23 -04:00

113 lines
3.6 KiB
JavaScript

import './styles/tokens.css';
import './styles/layout.css';
import { registerSW } from 'virtual:pwa-register';
import { el } from './lib/dom.js';
import { initRouter, rerender } from './router.js';
import { Nav } from './components/Nav.js';
import { settings, applyTheme } from './store/settings.js';
import { ui } from './store/ui.js';
import { markPlayed } from './store/playedGames.js';
import { onInstallChange, promptInstall } from './lib/install.js';
applyTheme();
// Own the scroll position ourselves (views restore it deliberately).
if ('scrollRestoration' in history) history.scrollRestoration = 'manual';
// Navigation scroll memory is per-session — start a cold launch at the top.
ui.set({ feedScroll: 0, searchScroll: 0 });
// Land on the configured default screen for a fresh launch (no hash yet at
// all — a bookmark, the home-screen icon, or typing the bare URL). Explicit
// navigation, like tapping "Dex" in the nav, is never redirected — that
// only ever touches location.hash after this point.
if (!location.hash) {
const DEFAULT_ROUTES = { dex: '#/', team: '#/team', search: '#/search' };
location.hash = DEFAULT_ROUTES[settings.get().defaultRoute] || '#/';
}
// React to preference changes. The dex feed updates itself in place, but the
// Pokémon detail page is built once per visit — rebuild it when the selected
// game changes so its data (flavour text, learnset, matchups, evolution,
// abilities, locations, era sprites) matches the new game.
let lastVersionGroup = settings.get().versionGroup;
// Selecting a game counts as having played it (see store/playedGames.js).
markPlayed(lastVersionGroup);
settings.subscribe((s) => {
applyTheme();
if (s.versionGroup !== lastVersionGroup) {
lastVersionGroup = s.versionGroup;
markPlayed(s.versionGroup);
if ((location.hash || '').startsWith('#/pokemon/')) rerender();
}
});
window
.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', () => applyTheme());
const app = document.getElementById('app');
app.replaceChildren();
const viewHost = el('main', { id: 'view', class: 'view-host' });
app.append(Nav(), viewHost);
initRouter(viewHost);
// --- Service worker: prompt to refresh rather than silently swapping ------
const updateSW = registerSW({
onNeedRefresh() {
showToast('A new version is available.', 'Reload', () => updateSW(true));
},
onOfflineReady() {
showToast('Ready to use offline.', 'Dismiss');
},
});
// --- Install prompt: offer it once, unobtrusively ---------------------
let installOffered = false;
onInstallChange((can) => {
if (!can || installOffered) return;
installOffered = true;
showToast('Install Pocketdex for offline, full-screen use?', 'Install', () => {
promptInstall();
});
});
// --- Offline indicator -------------------------------------------------
const netPill = el('div', { class: 'netpill', role: 'status' }, 'Offline');
function syncNet() {
if (navigator.onLine) {
netPill.remove();
} else if (!netPill.isConnected) {
document.body.append(netPill);
}
}
window.addEventListener('offline', syncNet);
window.addEventListener('online', () => {
syncNet();
showToast('Back online.', 'Dismiss');
});
syncNet();
function showToast(message, actionLabel, onAction) {
const toast = el(
'div',
{ class: 'toast', role: 'status' },
el('span', {}, message),
el(
'button',
{
class: 'toast__action',
type: 'button',
onclick: () => {
toast.remove();
onAction?.();
},
},
actionLabel,
),
);
document.body.append(toast);
if (!onAction) setTimeout(() => toast.remove(), 5000);
}