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 { registerSW } from 'virtual:pwa-register';
|
||||||
import { el } from './lib/dom.js';
|
import { el } from './lib/dom.js';
|
||||||
import { initRouter } from './router.js';
|
import { initRouter, rerender } from './router.js';
|
||||||
import { Nav } from './components/Nav.js';
|
import { Nav } from './components/Nav.js';
|
||||||
import { settings, applyTheme } from './store/settings.js';
|
import { settings, applyTheme } from './store/settings.js';
|
||||||
|
|
||||||
applyTheme();
|
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
|
window
|
||||||
.matchMedia('(prefers-color-scheme: dark)')
|
.matchMedia('(prefers-color-scheme: dark)')
|
||||||
.addEventListener('change', () => applyTheme());
|
.addEventListener('change', () => applyTheme());
|
||||||
|
|||||||
@ -19,6 +19,13 @@ function resolve(hash) {
|
|||||||
return { route: routes[0], match: [] };
|
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) {
|
export function initRouter(host) {
|
||||||
let current = null;
|
let current = null;
|
||||||
|
|
||||||
@ -56,6 +63,7 @@ export function initRouter(host) {
|
|||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rerenderCurrent = render;
|
||||||
window.addEventListener('hashchange', render);
|
window.addEventListener('hashchange', render);
|
||||||
render();
|
render();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -862,9 +862,53 @@
|
|||||||
.ppanel__flavor {
|
.ppanel__flavor {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
margin: 0 0 18px;
|
margin: 0 0 6px;
|
||||||
overflow-wrap: anywhere;
|
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 {
|
.ppanel__sub {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import { EvolutionChain } from '../components/EvolutionChain.js';
|
|||||||
import { MovesList } from '../components/MovesList.js';
|
import { MovesList } from '../components/MovesList.js';
|
||||||
import { TypeMatchups } from '../components/TypeMatchups.js';
|
import { TypeMatchups } from '../components/TypeMatchups.js';
|
||||||
import { Locations } from '../components/Locations.js';
|
import { Locations } from '../components/Locations.js';
|
||||||
|
import { FlavorText } from '../components/FlavorText.js';
|
||||||
import { typeHex } from '../lib/type-color.js';
|
import { typeHex } from '../lib/type-color.js';
|
||||||
|
|
||||||
const idFromUrl = (u) => Number(u.replace(/\/$/, '').split('/').pop());
|
const idFromUrl = (u) => Number(u.replace(/\/$/, '').split('/').pop());
|
||||||
@ -153,14 +154,6 @@ export async function PokemonDetail(nationalId) {
|
|||||||
syncTrack();
|
syncTrack();
|
||||||
|
|
||||||
// ---- About panel -----------------------------------------------
|
// ---- 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
|
const abilities = pokemon.abilities
|
||||||
.filter((a) => !a.is_hidden || vgGen >= 5)
|
.filter((a) => !a.is_hidden || vgGen >= 5)
|
||||||
.map((a) => prettify(a.ability.name) + (a.is_hidden ? ' (hidden)' : ''));
|
.map((a) => prettify(a.ability.name) + (a.is_hidden ? ' (hidden)' : ''));
|
||||||
@ -168,9 +161,7 @@ export async function PokemonDetail(nationalId) {
|
|||||||
const aboutPanel = el(
|
const aboutPanel = el(
|
||||||
'div',
|
'div',
|
||||||
{ class: 'ppanel' },
|
{ class: 'ppanel' },
|
||||||
flavor
|
FlavorText(species, vg),
|
||||||
? el('p', { class: 'ppanel__flavor' }, flavor.flavor_text.replace(/\s+/g, ' '))
|
|
||||||
: null,
|
|
||||||
el(
|
el(
|
||||||
'dl',
|
'dl',
|
||||||
{ class: 'pfacts' },
|
{ class: 'pfacts' },
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user