Skip to content

Frontend Guide

Hails edited this page Jul 15, 2026 · 7 revisions

Frontend Guide

The frontend is plain TypeScript bundled with esbuild into one self contained file per page, layered on top of Go html/template pages. There is no framework and no client side router: the server renders the page shell with translated strings, and the page's bundle builds the interactive parts into a mount div. This page is the contributor map: which file powers what, what the shared modules do, and the conventions to follow when adding a page.

Build pipeline

package.json defines the whole build (esbuild ^0.24, TypeScript ^5.6):

esbuild ts/raids.ts ts/dps.ts ts/pvp.ts ts/events.ts ts/shinies.ts ts/iv.ts ts/raidfinder.ts ts/notifications.ts ts/social.ts
  --bundle --outdir=static/js --target=es2020 --minify

npm run build produces the bundles, npm run watch rebuilds on change. Each entrypoint becomes static/js/<name>.js. See Building-and-Development for the full workflow.

Entrypoint map

Entrypoint Bundle Template Page
ts/raids.ts raids.js templates/raids.html /raids (Raids-and-Counters)
ts/dps.ts dps.js templates/dps.html /dps (DPS-Calculator)
ts/pvp.ts pvp.js templates/pvp.html /pvp (PvP-IV-Ranker)
ts/events.ts events.js templates/events.html /events (Events)
ts/shinies.ts shinies.js templates/shinies.html /shinies (Shiny-Tracking)
ts/iv.ts iv.js templates/iv.html /iv (IV Calculator page)
ts/raidfinder.ts raidfinder.js templates/trainers.html /trainers, Raid Finder tab (Raid-Finder)
ts/notifications.ts notifications.js templates/base.html 🔔 badge poller, toast display, and ding sound, loaded on every page for logged-in users
ts/social.ts social.js templates/trainer.html Social action buttons (Add/Remove Friend, Block/Unblock) on trainer profile pages

The Trainer Directory tab of /trainers and pages like settings, admin, translate, friends, and notifications use inline <script> blocks in their templates instead of bundles.

Shared modules (ts/shared/)

  • gamedata.ts: the data backbone. loadGameData() fetches the merged game data once per page load and caches it in a module variable; it tries the session endpoint /api/app/data first and falls back to the public /api/data on a 401. Also home to pokeSprite(id) (PokeAPI GitHub raw sprite URLs), pokeName() (localizes Pokemon names through the pokemonNames map using the SITE_LANG global, English fallback), cpForLevel() and cpmFromCP() (CP math and its inverse), lookup helpers, and typeEffectiveness().
  • damage.ts: the PvE math: per hit damage, fast move DPS and EPS, full cycle DPS, and the TDO estimate. Formulas are written out on DPS-Calculator.
  • counters.ts: the counter search: calcCounters() (exhaustive best moveset search over every Pokemon, top 20 by DPS) and calcSinglePokemon() (one Pokemon versus one boss with a PokemonConfig of CPM, IVs, and form). Handles Shadow, Purified, Primal, and Mega forms, stripping the form prefix to fall back to the base form's move pool, and renders the shared counter table.
  • picker.ts: createPicker(), the searchable sprite picker used across DPS and other pages. Takes a list of PickerEntry items (key, search name, localized label, sprite, type badges, optional tier tag, sort group), renders a dropdown with match ranking and a selected preview block, and reports selection through callbacks. The returned Picker handle exposes selectByName(name) for programmatic restore (used by the DPS page to reload a persisted target from localStorage).
  • tabs.ts: buildTabs(), the lazy tab bar: each panel's render() runs only on first activation, so heavy panels (like a PvP league) cost nothing until opened.
  • pokedex.ts: on demand PokeAPI fetches: fetchSpeciesData() (flavor text, genus, legendary and mythical flags, alternate varieties), fetchCryUrl(), and fetchFormSprites() (normal and shiny pair). Every function caches in a Map and resolves to safe empty values on failure, so PokeAPI being down degrades the page rather than breaking it.
  • typecolors.ts: the TYPE_COLORS palette and typeBadge() factory for the colored type pills.
  • costumes.ts: costume resolution for the shiny collection page and the Pokemon box. Rather than carrying its own table, it imports internal/costumes/catalog.json and internal/costumes/labels.json directly, the same two files the server go:embeds, so both sides resolve from one source and cannot drift (there is no generated Go mirror any more). catalog.json is machine-generated from the mined asset tree (which costume codes and shiny art exist, and which species can wear each), labels.json is the curated human labels. Templates inject the merged label set (curated file plus admin-named overlay) as a COSTUME_LABELS global so the picker knows about costumes named since the last deploy. Costumed sprites are served from the app's own proxy at /api/costume-sprite/{file}, never hotlinked.
  • regionalForms.ts: the per-species regional and battle-forme table (Alolan, Galarian, Hisuian, Paldean, and other formes) with the PokeAPI variant sprite ids each one uses; drives the region picker on the shiny features and is mirrored server-side by internal/handlers/regional.go.
  • evolutions.ts: the evolution-target data for the shiny tracker's evolve action; it gained a REGIONAL_EVOLUTION_NEXT map so region-aware evolution lines resolve to the correct regional target.
  • types.ts: the shared interfaces (GameData, PokemonStat, RaidBoss, move types, and so on).

regionalForms.ts (including its VIVILLON_PATTERNS and UNOWN_LETTERS tables) is a hand-maintained table that the server mirrors in internal/handlers/regional.go; when you change one, update the other in the same change or the browser and server will disagree on what is valid (a unit test guards against silent drift). Costumes no longer need this dance: costumes.ts and the server both read the shared internal/costumes JSON, so there is a single source of truth and nothing to regenerate.

Template conventions

templates/base.html defines the base layout: head, nav sidebar, language switcher, footer, and the particle background. Pages define three blocks: title, content, and scripts. A typical page template is small: a header, a mount div like <div id="raids-app"> with a loading spinner, and a scripts block that loads the bundle.

Things every page gets from the layout and PageData (defined in internal/handlers/handlers.go):

  • PageData fields: User (nil when logged out), Data (page specific payload), CSRFToken, StoreEnabled, Maintenance (per feature kill switches that templates check before rendering nav links and sections), Lang, Langs, and AssetVersion.
  • CSRF: <meta name="csrf-token" content="{{.CSRFToken}}"> in the head; scripts read it and send it as the X-CSRF-Token header on mutating fetches.
  • SITE_LANG: injected as a global by base.html for client side name localization and date formatting.
  • Inline globals: bundles cannot call {{T}}, so templates define objects of pre-translated strings in their scripts block (DP in dps, EV in events, RF/RF2/RAID_CTX in trainers, TL_STRINGS in translate). Auth context is passed the same way when a page needs to know the login state at runtime (DPS_CTX in dps, RAID_CTX in trainers). See Localization.
  • Asset versioning: every static asset URL carries ?v={{.AssetVersion}}, a hash of the contents of static/css and static/js computed at startup, so browser caches bust themselves on deploy.

CSS conventions

All styling is one file, static/css/main.css, themed by variables in :root: --bg (#07071a), --surface and --surface-2 (translucent whites), --border, --text (#ebebf5), --text-2 (#8888b8), --text-3 (#55558a), accent colors --red (#e53935), --blue (#5b9cf6), --green (#00d68f), --gold (#ffb340), radii --r (14px), --r-sm, --r-pill, the glass effect --blur (blur(18px) saturate(160%)), the --font stack (Inter first), and --trans for transitions.

The signature look is the dark glassmorphism card, composed from those variables:

.some-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r);
  backdrop-filter: var(--blur);
  -webkit-backdrop-filter: var(--blur);
}

Grids are responsive via grid-template-columns: repeat(auto-fill, minmax(...)) patterns, and several views swap layout entirely at a 768px breakpoint (for example the DPS Compare table becomes stacked cards). A global text shadow outline keeps text readable over the rotating background images; form controls opt out of it.

Adding a new page: checklist

  1. Template: create templates/mypage.html with title, content (a mount div plus loading state), and scripts blocks.
  2. TypeScript entrypoint: create ts/mypage.ts; use loadGameData() and the shared modules rather than refetching.
  3. Build line: add ts/mypage.ts to both the build and watch scripts in package.json, then rebuild.
  4. Go route and handler: add a handler that calls h.render(w, r, "mypage", data) and register the route in internal/server/server.go (wrap with RequireAuth or a maintenance check as appropriate).
  5. Navbar link: add the link in templates/base.html, gated on the relevant maintenance flag.
  6. Locale keys: add nav.mypage, mypage.title, and the rest to internal/i18n/locales/en.json first; see Localization.

Notes and gotchas

  • static/js/ is gitignored. Bundles are build artifacts; always run npm run build after pulling or before deploying. A stale bundle is the usual cause of "my change does nothing".
  • Sprites fail gracefully. Sprite <img> elements attach error handlers that hide the image, because PokeAPI's repository does not have a sprite for every form ID. Follow the same pattern for new images.
  • localStorage is used sparingly. The DPS Calculator saves the selected target Pokemon to dps_calc_target and dps_cmp_target for logged-in users. The notifications system uses notif_seen_lobbies (seen toast IDs, capped at 200) and notif_dismissed (page-dismissed lobby IDs, capped at 500), and notif_sound_volume (0-100 integer) for the ding volume. All other client state either lives server side or is a plain module variable that resets on reload.
  • Locale files are BOM sensitive; if you script edits to them on Windows, read the warning in Localization.

Clone this wiki locally