Both-version flavour text; refresh detail page on game switch
- FlavorText: shows a version pill toggle (Sword/Shield, Scarlet/Violet…) when the two entries differ, sliding between them; collapses identical entries; falls back to the newest English entry, captioned - Switching games while viewing a Pokémon now rebuilds the detail page (router.rerender wired from a settings subscription in main.js) so flavour text, learnset, matchups, evolution, abilities, locations and era sprites all follow the newly selected game Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
59ff8d6b68
commit
0665d0f2ef
74
src/components/FlavorText.js
Normal file
74
src/components/FlavorText.js
Normal file
@ -0,0 +1,74 @@
|
||||
import { el } from '../lib/dom.js';
|
||||
import { prettify } from '../data/pokedex-resolver.js';
|
||||
|
||||
/**
|
||||
* Pokédex flavour text for the selected game. Most games ship two versions
|
||||
* (Scarlet/Violet, Sword/Shield…) with different entries — show a pill
|
||||
* toggle and slide between them. Falls back to the most recent English
|
||||
* entry when the game itself has none.
|
||||
*/
|
||||
export function FlavorText(species, vg) {
|
||||
const versions = vg ? vg.versions : [];
|
||||
const english = species.flavor_text_entries.filter((f) => f.language.name === 'en');
|
||||
|
||||
const byVersion = new Map();
|
||||
for (const f of english) {
|
||||
if (!byVersion.has(f.version.name)) {
|
||||
byVersion.set(f.version.name, f.flavor_text.replace(/\s+/g, ' '));
|
||||
}
|
||||
}
|
||||
|
||||
let picks = versions
|
||||
.filter((v) => byVersion.has(v))
|
||||
.map((v) => ({ version: v, text: byVersion.get(v) }));
|
||||
|
||||
// Collapse identical entries (both versions often share the same text).
|
||||
const seen = new Set();
|
||||
picks = picks.filter((p) => (seen.has(p.text) ? false : seen.add(p.text)));
|
||||
|
||||
if (!picks.length && english.length) {
|
||||
const last = english[english.length - 1];
|
||||
picks = [{ version: last.version.name, text: last.flavor_text.replace(/\s+/g, ' '), fallback: true }];
|
||||
}
|
||||
if (!picks.length) return null;
|
||||
|
||||
const quote = el('p', { class: 'ppanel__flavor' });
|
||||
const caption = el('span', { class: 'flavor__cap' });
|
||||
const toggle = el('div', { class: 'flavor__toggle' });
|
||||
let index = 0;
|
||||
|
||||
function paint() {
|
||||
const p = picks[index];
|
||||
quote.textContent = p.text;
|
||||
quote.style.animation = 'none';
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
quote.offsetWidth;
|
||||
quote.style.animation = '';
|
||||
caption.textContent = p.fallback
|
||||
? `— ${prettify(p.version)} (most recent)`
|
||||
: `— Pokémon ${prettify(p.version)}`;
|
||||
[...toggle.children].forEach((b, i) => b.classList.toggle('is-active', i === index));
|
||||
}
|
||||
|
||||
if (picks.length > 1) {
|
||||
picks.forEach((p, i) =>
|
||||
toggle.append(
|
||||
el(
|
||||
'button',
|
||||
{
|
||||
class: 'flavor__pill',
|
||||
type: 'button',
|
||||
onclick: () => {
|
||||
index = i;
|
||||
paint();
|
||||
},
|
||||
},
|
||||
prettify(p.version),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
paint();
|
||||
return el('div', { class: 'flavor' }, picks.length > 1 ? toggle : null, quote, caption);
|
||||
}
|
||||
16
src/main.js
16
src/main.js
@ -3,12 +3,24 @@ import './styles/layout.css';
|
||||
|
||||
import { registerSW } from 'virtual:pwa-register';
|
||||
import { el } from './lib/dom.js';
|
||||
import { initRouter } from './router.js';
|
||||
import { initRouter, rerender } from './router.js';
|
||||
import { Nav } from './components/Nav.js';
|
||||
import { settings, applyTheme } from './store/settings.js';
|
||||
|
||||
applyTheme();
|
||||
settings.subscribe(() => applyTheme());
|
||||
|
||||
// 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;
|
||||
settings.subscribe((s) => {
|
||||
applyTheme();
|
||||
if (s.versionGroup !== lastVersionGroup) {
|
||||
lastVersionGroup = s.versionGroup;
|
||||
if ((location.hash || '').startsWith('#/pokemon/')) rerender();
|
||||
}
|
||||
});
|
||||
window
|
||||
.matchMedia('(prefers-color-scheme: dark)')
|
||||
.addEventListener('change', () => applyTheme());
|
||||
|
||||
@ -19,6 +19,13 @@ function resolve(hash) {
|
||||
return { route: routes[0], match: [] };
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@ -56,6 +63,7 @@ export function initRouter(host) {
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
rerenderCurrent = render;
|
||||
window.addEventListener('hashchange', render);
|
||||
render();
|
||||
}
|
||||
|
||||
@ -862,9 +862,53 @@
|
||||
.ppanel__flavor {
|
||||
font-size: 1rem;
|
||||
line-height: 1.6;
|
||||
margin: 0 0 18px;
|
||||
margin: 0 0 6px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.flavor {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.flavor__toggle {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.flavor__pill {
|
||||
appearance: none;
|
||||
border: 1.5px solid var(--border);
|
||||
background: var(--surface);
|
||||
color: var(--text-dim);
|
||||
font: inherit;
|
||||
font-size: 0.76rem;
|
||||
font-weight: 600;
|
||||
padding: 5px 13px;
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
.flavor__pill.is-active {
|
||||
background: var(--type-main, var(--accent));
|
||||
border-color: var(--type-main, var(--accent));
|
||||
color: #fff;
|
||||
}
|
||||
.flavor .ppanel__flavor {
|
||||
animation: flavorSlide 0.28s var(--ease-spring);
|
||||
}
|
||||
@keyframes flavorSlide {
|
||||
from { opacity: 0; transform: translateX(14px); }
|
||||
to { opacity: 1; transform: none; }
|
||||
}
|
||||
.flavor__cap {
|
||||
display: block;
|
||||
font-size: 0.76rem;
|
||||
color: var(--text-dim);
|
||||
text-transform: capitalize;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.flavor .ppanel__flavor {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
.ppanel__sub {
|
||||
font-size: 0.8rem;
|
||||
text-transform: uppercase;
|
||||
|
||||
@ -12,6 +12,7 @@ import { EvolutionChain } from '../components/EvolutionChain.js';
|
||||
import { MovesList } from '../components/MovesList.js';
|
||||
import { TypeMatchups } from '../components/TypeMatchups.js';
|
||||
import { Locations } from '../components/Locations.js';
|
||||
import { FlavorText } from '../components/FlavorText.js';
|
||||
import { typeHex } from '../lib/type-color.js';
|
||||
|
||||
const idFromUrl = (u) => Number(u.replace(/\/$/, '').split('/').pop());
|
||||
@ -153,14 +154,6 @@ export async function PokemonDetail(nationalId) {
|
||||
syncTrack();
|
||||
|
||||
// ---- About panel -----------------------------------------------
|
||||
const wantedVersions = new Set(vg ? vg.versions : []);
|
||||
const englishFlavor = species.flavor_text_entries.filter(
|
||||
(f) => f.language.name === 'en',
|
||||
);
|
||||
const flavor =
|
||||
englishFlavor.find((f) => wantedVersions.has(f.version.name)) ||
|
||||
englishFlavor[englishFlavor.length - 1];
|
||||
|
||||
const abilities = pokemon.abilities
|
||||
.filter((a) => !a.is_hidden || vgGen >= 5)
|
||||
.map((a) => prettify(a.ability.name) + (a.is_hidden ? ' (hidden)' : ''));
|
||||
@ -168,9 +161,7 @@ export async function PokemonDetail(nationalId) {
|
||||
const aboutPanel = el(
|
||||
'div',
|
||||
{ class: 'ppanel' },
|
||||
flavor
|
||||
? el('p', { class: 'ppanel__flavor' }, flavor.flavor_text.replace(/\s+/g, ' '))
|
||||
: null,
|
||||
FlavorText(species, vg),
|
||||
el(
|
||||
'dl',
|
||||
{ class: 'pfacts' },
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user