mirror of
https://github.com/LucasKalil-Programador/world-2026-hub.git
synced 2026-07-04 17:41:28 -03:00
feat(stats): degradation engine + scrollspy sub-nav (Stage A)
Scaffold the post-Cup stats screen (.agents/stats-screen-plan.md) on the feature/stats-final-screen branch. - loadData(): fault-tolerant optional data layers (players, player-events, awards, keeper-stats, curiosities, all-time-baselines) via loadOptional() — an absent/404 file defaults to empty SILENTLY (graceful degradation, keeps the console clean), warning only on a present-but-malformed file. The 6 core files still throw on failure. - stats.js SECTIONS registry: a section and its sub-nav chip render only when available(model) holds, else they are omitted from the DOM entirely (no placeholder / no coming-soon). Overview/Teams live; the 4 future sections stay dark until later stages. - Sticky scrollspy sub-nav: hash-safe anchor chips (preventDefault + scrollIntoView, never touch location.hash so the tab router does not bounce to Home); position-based scrollspy with an explicit page-bottom -> last section rule (robust on short pages). --header-h kept live via a ResizeObserver so the nav sticks correctly under the variable-height header. - flagImg() monogram fallback: a broken flag SVG becomes a 3-letter code span, never a broken-image icon. - i18n stats.nav* keys (EN/PT); stats.css for sub-nav / section / fallback. No DATA_VERSION bump (no deployed data changed). No index.html change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
79e746e4d4
commit
387fab3c8b
4 changed files with 253 additions and 4 deletions
|
|
@ -17,18 +17,49 @@ let data = null;
|
|||
|
||||
const DATA_VERSION = '2026-06-16-rev2';
|
||||
|
||||
// Optional data layers for the post-tournament stats screen (players, awards,
|
||||
// editorial — see .agents/stats-screen-plan.md §0.2). They don't exist yet, so
|
||||
// an absent/404 file is the NORMAL "this layer hasn't arrived" state: return the
|
||||
// empty default silently (graceful degradation — never surface the gap, and keep
|
||||
// the console clean). Warn only when a file is present but malformed (a real dev
|
||||
// error). Never throws — the stats screen lights these up as the JSON lands.
|
||||
async function loadOptional(name, fallback) {
|
||||
try {
|
||||
const res = await fetch(`data/${name}.json?v=${DATA_VERSION}`);
|
||||
if (!res.ok) return fallback; // not provided yet → empty, no noise
|
||||
return await res.json();
|
||||
} catch (err) {
|
||||
console.warn(`data/${name}.json present but unreadable — ignoring`, err);
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadData() {
|
||||
if (data) return data;
|
||||
const files = ['teams', 'groups', 'matches', 'results', 'stadiums', 'bracket-config'];
|
||||
const [teams, groups, matches, results, stadiums, bracketConfig] = await Promise.all(
|
||||
// Core files are mandatory: a failure here is fatal (throws → showError()).
|
||||
const corePromise = Promise.all(
|
||||
files.map(async (name) => {
|
||||
const res = await fetch(`data/${name}.json?v=${DATA_VERSION}`);
|
||||
if (!res.ok) throw new Error(`data/${name}.json — HTTP ${res.status}`);
|
||||
return res.json();
|
||||
}),
|
||||
);
|
||||
// Optional layers fetched concurrently; each defaults to empty, never fatal.
|
||||
const optionalPromise = Promise.all([
|
||||
loadOptional('players', []),
|
||||
loadOptional('player-events', []),
|
||||
loadOptional('awards', {}),
|
||||
loadOptional('keeper-stats', []),
|
||||
loadOptional('curiosities', []),
|
||||
loadOptional('all-time-baselines', {}),
|
||||
]);
|
||||
const [teams, groups, matches, results, stadiums, bracketConfig] = await corePromise;
|
||||
const [players, playerEvents, awards, keeperStats, curiosities, allTimeBaselines] =
|
||||
await optionalPromise;
|
||||
data = {
|
||||
teams, groups, matches, results, stadiums, bracketConfig,
|
||||
players, playerEvents, awards, keeperStats, curiosities, allTimeBaselines,
|
||||
teamById: new Map(teams.map((team) => [team.id, team])),
|
||||
stadiumByName: new Map(stadiums.map((s) => [s.name, s])),
|
||||
resultByMatchId: new Map(results.map((r) => [r.matchId, r])),
|
||||
|
|
@ -515,6 +546,18 @@ function initLangSwitch() {
|
|||
sync();
|
||||
}
|
||||
|
||||
// The header is sticky with a VARIABLE height (one row ≥1100px, two bands below).
|
||||
// Expose its live height as --header-h so the stats sub-nav can stick right
|
||||
// beneath it and sections can offset their scroll target at every breakpoint.
|
||||
function trackHeaderHeight() {
|
||||
const header = document.querySelector('.site-header');
|
||||
if (!header) return;
|
||||
const set = () => document.documentElement.style.setProperty('--header-h', `${header.offsetHeight}px`);
|
||||
set();
|
||||
if ('ResizeObserver' in window) new ResizeObserver(set).observe(header);
|
||||
else window.addEventListener('resize', set);
|
||||
}
|
||||
|
||||
function renderHome() {
|
||||
renderHero();
|
||||
renderDashboard();
|
||||
|
|
@ -530,6 +573,7 @@ function showError(error) {
|
|||
async function init() {
|
||||
initI18n();
|
||||
initTabs();
|
||||
trackHeaderHeight();
|
||||
initLangSwitch();
|
||||
initTimeToggle();
|
||||
initFavorites();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue