19 KiB
Plano de Implementação — Tela de Estatísticas Finais (World Cup 2026 Hub)
Documento de planejamento (NÃO é implementação). Gerado em 2026-06-14 após workflow multi-agente (5 perspectivas) + aprovação da lista de dados pelo usuário. Escopo aprovado: as 4 camadas (✅ existentes · 🟡🧩 acréscimos baratos · 🔴 dados de jogadores · 📝 editorial).
PRINCÍPIO REITOR (requisito explícito do usuário): degradação graciosa. Quando um dado não puder ser obtido, o tratamento deve ser elegante — não pode quebrar a UI nem deixar visível para o usuário final que algo está faltando. Sem
—, sem cards vazios, sem "dados em breve". A regra é: um dado/seção só aparece quando está completo o bastante para ser apresentado como autoritativo; caso contrário é removido do DOM (não escondido com placeholder, removido). Ver §0 e §6.
0 · Camada de degradação graciosa (a espinha dorsal de todo o resto)
Esta é a peça arquitetural mais importante do plano. Tudo abaixo se apoia nela.
0.1 Contrato de disponibilidade
Cada estatística, card, linha de tabela e sub-seção declara um predicado isAvailable(model):
- Sub-seção inteira sem dados → a seção não é renderizada e seu chip no sub-nav é removido (navegação nunca aponta para o vazio).
- Card/recorde individual sem dados → o card não é inserido; o grid reflui naturalmente.
- Célula opcional numa linha (ex.: xG de um time sem feed) → a coluna some para todos OU a célula cai para um valor neutro só se isso não denunciar ausência. Preferência: a coluna inteira só existe se todos os times tiverem o dado.
- Agregado sobre
statsesparso (posse/chutes/cartões) → só renderiza com cobertura completa (todos os jogos da amostra relevante preenchidos). Sem cobertura → escondido, sem disclaimer. (Decisão: o usuário NÃO quer "baseado em N jogos" visível. Ou faz backfill total, ou não mostra.)
0.2 Carga de dados tolerante a falha
loadData() em app.js passa a buscar os novos JSON; cada fetch novo é opcional:
arquivo ausente / 404 / JSON inválido → console.warn (dev) + default vazio ([]/{}),
nunca exceção que derrube o app. Os 6 arquivos atuais continuam obrigatórios.
0.3 Mídia com fallback
Fotos de jogadores / bandeiras quebradas → onerror cai para iniciais (monograma) ou silhueta
genérica. Nunca ícone de imagem quebrada.
0.4 Camadas se auto-desligam
- Sem
players.json/eventos → toda a seção Jogadores + Prêmios + comparador de jogadores + recordes de tempo de gol somem (chips inclusive). A tela continua completa só com dados de time. - Sem
curiosities.json→ Recordes mostra só os auto-deriváveis; nada de "em breve". - Sem
attendance→ recordes de público somem; o resto do Overview fica intacto.
Resultado: a mesma base de código entrega uma tela coerente e "cheia" com só os dados existentes hoje, e vai "acendendo" seções conforme os dados das camadas 2/3/4 forem entrando — sem nenhuma data de corte nem buraco visível.
1 · Arquitetura da tela
1.1 Estrutura de componentes (segue o padrão por-view existente)
index.html
├─ nav.tabs + <button data-tab="stats" ...> (6º tab, após Stadiums)
└─ <section id="panel-stats" class="panel" role="tabpanel" hidden>
<h2 class="section-title" data-i18n="nav.stats">…</h2>
<div id="stats-root"><p class="placeholder glass" data-i18n="app.comingSoon"></p></div>
assets/js/stats.js (NOVO módulo — espelha schedule.js/groups.js/bracket.js)
- initStats() chamado por app.js (mesmo padrão de import circular intencional)
- buildStatsModel(data) deriva TODO o modelo computável 1x, memoizado
- renderStats() / renderSection(id) render preguiçoso por seção (IntersectionObserver)
- escuta langchange (re-render só de labels) / favchange / timemodechange
assets/css/stats.css (NOVO — como bracket.css; componentes da tela de stats)
1.2 Rotas
- Hash route
#stats+ persistênciawc2026_prefs.lastTab→ de graça (roteamento por hash já existe). - Sub-navegação interna não é rota nem segundo
tablist: é uma<nav>de âncoras (#stats-teams…) com scrollspy via IntersectionObserver (evita conflito de setas com otablistdo topo — ver §3). - Opcional (nice-to-have): deep-link
#stats=playersque faz scroll até a seção ao carregar.
1.3 Estado global
- Nenhum estado persistente novo é obrigatório. O modelo de stats é derivado de
getData()e memoizado em memória no módulo (let statsModel); recalcula só se os dados mudarem (não mudam em runtime).langchangere-renderiza labels, não recomputa valores (nomes não se traduzem). - Reusa estado existente:
wc2026_favorites(destaque dourado em linhas/cards do time favorito),wc2026_prefs.timeMode(arquivo de resultados + modal). - Opcional:
wc2026_prefs.statsSortp/ lembrar a última ordenação de tabela (baixa prioridade).
1.4 Integração com o existente (reuso, não reinvenção)
openMatchModal(matchId)→ cards de recorde, linhas do arquivo e qualquer linha ligada a um jogo.resolveBracketTeams,computeStandings,getBracketTree,calculateChallengeScore→ reusados para ranking, caminho do campeão, fase alcançada.t(key), eventoslangchange/favchange/timemodechange,.glass,.slide-up,.container.- Cache-busting é automático:
loadData()já anexa?t=Date.now()a cada fetch dedata/*.json, então novos arquivos de dados não exigem nenhum passo de versão (DATA_VERSIONfoi removido em 2026-06-18).
2 · Fontes de dados
2.1 Já existe (Camada 1 — ~70% da tela, zero coleta)
teams.json, groups.json, matches.json, results.json (placar/status/penalties + stats
opcional), stadiums.json (capacidade, timezone), bracket-config.json. Computados: standings,
bracket, challenge.
2.2 Precisa coletar/calcular
| Camada | Arquivo / campo novo | Schema | Destrava | Esforço |
|---|---|---|---|---|
| 🟡 2 | results.json → attendance |
inteiro por jogo | público total/médio/ocupação, maior público | dado público FIFA |
| 🟡 2 | results.json → cards: {home:{y,r}, away:{y,r}} |
split amarelo/vermelho | disciplina completa, fair-play | reformatar campo atual |
| 🟡 2 | results.json → decidedIn |
"regulation"|"ET"|"penalties" |
separar prorrogação de pênaltis | trivial |
| 🟡 2 | results.json → backfill stats nos 104 |
posse/chutes existentes | posse/chutes/conversão como agregado confiável | médio (104 jogos) |
| 🟡 2 | results.json → shotsOnTarget, passes, passAccuracy (opc.) |
nível time | chutes no alvo, passes | opcional |
| 🧩 2 | teams.json → ranking/seed, wcDebut, confederation |
por time | zebras, Cinderela, estreantes, desempenho por confederação | trivial 1x |
| 🧩 2 | stadiums.json → lat,lng |
por estádio | distância total percorrida | trivial 1x |
| 🔴 3 | players.json |
roster mínimo {id,name,team,position,birthDate,shirt?} — só envolvidos |
base de todos os individuais | médio |
| 🔴 3 | player-events.json |
log append-only {type:goal|card|ownGoal, player, team, matchId, minute, goalType?, assist?, card?} |
artilharia, hat-tricks, tempo de gol, disciplina | alto, ~400+ linhas |
| 🔴 3 | awards.json |
{goldenBall, goldenBoot, goldenGlove, bestYoung, squadOfTournament[], goalOfTournament} |
prêmios FIFA + Seleção do Torneio | trivial (1x pós-final) |
| 🔴 3 | (opc.) keeper-stats.json |
defesas/clean-sheets por goleiro | Luva de Ouro detalhada | opcional |
| 📝 4 | curiosities.json |
array de cards {id,type,priority,titleEN/PT,bodyEN/PT,matchId?,teamId?,mediaRef?,statRef?} |
gol mais bonito, VAR, histórias | redação bilíngue |
| 📝 4 | all-time-baselines.json (ou embutido) |
recordes históricos fixos p/ comparação | painel "esta Copa vs história" | trivial 1x |
Decisão de modelagem (jogadores): usar roster + log de eventos (não agregados pré-somados). O log deriva tudo (artilharia, assistências, hat-tricks, gol mais rápido/jovem/tardio) com o mesmo padrão do projeto (results.json é por jogo; o código computa o resto). Minutos jogados e defesas de goleiro — caros por evento — ficam como agregado opcional (
keeper-stats.json) ou são omitidos. Entrada incremental por rodada (encaixa no fluxo diário/update-worldcup), não num lote único.
2.3 Fora de escopo v1
- xG: não existe feed; exige provedor externo. Slot de UI previsto, escondido até haver dado.
- Distância percorrida por jogador, dribles, sprints: custo de manutenção alto demais para site estático.
3 · UI/UX — Proposta de navegação (wireframe textual)
Colocação: novo 6º tab Stats / Estatísticas, último (é o epílogo do torneio). Reusa o
tablist WAI-ARIA existente (roving tabindex, Setas/Home/End) — zero paradigma novo.
Sub-navegação: barra de chips sticky (scrollspy) abaixo do hero — <nav> de âncoras, não um
segundo tablist (evita conflito de Setas com o tab do topo; é também o gatilho do render preguiçoso).
Chips: Overview · Times · Jogadores · Recordes · Comparador · Arquivo (chips de seções vazias somem).
┌─ #panel-stats ───────────────────────────────────────────────────────────┐
│ ╔═ HERO "o veredito" (glass, slide-up) ════════════════════════════════╗ │
│ ║ 🏆 CAMPEÃO [bandeira] vice · 3º · Chuteira de Ouro · Seleção ║ │
│ ║ [ 172 ] [ 2.68 ] [ 31 ] [ 3.1M ] ← tiles count-up ║ │
│ ║ Gols Gols/jogo Pênaltis Público ║ │
│ ╚═══════════════════════════════════════════════════════════════════════╝ │
│ ┌─ SUB-NAV sticky (scrollspy) ● Overview Times Jogadores … Arquivo ───┐ │
│ ═══ #stats-overview ═══ cards (Partidas/Média/Cartões/Clean sheets) + │
│ CHART "gols por fase" (barras SVG, reveal) + "gols por rodada" │
│ ═══ #stats-teams ═══ filtros[confederação▾ fase▾ ⌕] + │
│ LEADERBOARD ordenável (#, time, P W E D, GF GA GD, Pts, [xG]) + │
│ cards: maior goleada, caminho do campeão, forma V/E/D │
│ ═══ #stats-players ═══ PÓDIO top-3 (Chuteira de Ouro, count-up) + │
│ chips[Artilheiros·Assist·Cartões·Defesas] → troca corpo da tabela +│
│ bloco PRÊMIOS + SELEÇÃO DO TORNEIO (gráfico de formação) │
│ ═══ #stats-records ═══ grid de record-cards (auto) + faixa "ESTREIAS DO │
│ FORMATO 48" (destaque) + cards editoriais (curiosities.json) │
│ ═══ #stats-comparator ═══ [A▾] vs [B▾] toggle Times/Jogadores + │
│ barras divergentes espelhadas (anima na escolha) │
│ ═══ #stats-archive ═══ 104 resultados, accordion por fase, filtros/sort, │
│ linha → openMatchModal() (preguiçoso, por último) │
└───────────────────────────────────────────────────────────────────────────┘
Acima da dobra (desktop): hero veredito + 4 tiles + sub-nav. O resto é recompensa de scroll.
Componentes reutilizáveis: stat-card (tile count-up), leaderboard-table (ordenável, a11y,
linha favorita dourada), podium (top-3), record-card (clica → modal), chart-panel (SVG/CSS +
<details> tabela alternativa), comparator, filter-bar, section-nav (scrollspy),
results-archive, chip-tabs (toggle intra-seção via aria-pressed, não tablist).
Interações/animações (todas atrás de prefers-reduced-motion):
count-up dos números (IntersectionObserver, ~900ms ease-out), barras crescendo da base,
linha com stroke-dashoffset, FLIP no re-sort, duelo de barras no comparador, reveal por seção.
Responsividade: ≤767 hero empilha + tiles 2×2, sub-nav scroll horizontal com snap, tabelas com
rank+nome congelados e scroll-x numa região focável; 768–1439 grids 2-col; 1440+ .container
(min(1200px,100%-2rem)), grid 4-col, hero numa linha.
Acessibilidade: <table> real com <caption>/scope/aria-sort (header ordenável = <button>);
sub-nav é <nav> (não tablist); charts com alternativa textual (<details> tabela + aria-label);
count-up: valor final é o DOM real, tween só visual (aria-live="off"); foco visível via
:focus-visible existente.
4 · Stack e dependências
Princípio: zero dependência nova de runtime. Mantém o mandato do projeto (vanilla ES Modules, sem framework/bundler/CDN) e o orçamento de JS < 300KB (hoje ~74KB).
- Gráficos: SEM biblioteca. Barras/linhas/donut feitos à mão em SVG inline + CSS (casa com a
estética e com o orçamento). Se um gráfico realmente exigir mais,
import()dinâmico de micro-lib (<poucos KB) só quando a seção entra na viewport — postura padrão: peso zero de chart-lib. - Animação: reusa
animations.css+ IntersectionObserver; helpercountUp()em vanilla (requestAnimationFrame). Tudo desligado emprefers-reduced-motion. - i18n: novo namespace
stats.*emi18n.js(dicts EN e PT). Nomes de dados não se traduzem. - CSS: novo
assets/css/stats.css(linkado no<head>comobracket.css), usando os tokens existentes (--accent-gold,--glass-bg,--radius, etc.). - Tooling opcional (não-shippado): script Node sem deps para gerar/validar agregados de jogadores a partir do log de eventos — facilita a entrada manual. Fora do bundle do site.
5 · Roadmap de implementação (incremental, com portões de aprovação)
Convenção do projeto: 1 etapa por vez, resumo + aprovação antes da próxima. Esforço: S ≈ meia sessão · M ≈ 1 sessão · L ≈ ~2 sessões. Etapas A–F entregam uma tela completa só com dados existentes; G–I acendem as camadas 2/3/4; J é polimento.
| # | Etapa | Entrega | Camada | Esforço |
|---|---|---|---|---|
| A | Scaffolding + motor de degradação | 6º tab, #panel-stats, stats.js, stats.css, namespace stats.*, render preguiçoso, scrollspy, loadData() tolerante a falha, contrato isAvailable (§0) |
infra | M |
| B | Overview + Hero | veredito (campeão/vice/3º/4º), 4 tiles count-up, gráfico gols-por-fase e gols-por-rodada | ✅1 | M |
| C | Estatísticas de times | leaderboard-table ordenável+a11y, ranking final 1–48 (cadeia de desempate), cards (maior goleada, caminho do campeão, forma, splits, clean sheets, sequências) |
✅1 | L |
| D | Recordes + Estreias do formato 48 | record-cards auto-deriváveis + faixa de destaque "104 jogos / maior caminho à final / Round of 32 / melhor 3º / 1º campeão da nova era" | ✅1 | M |
| E | Arquivo de resultados | 104 jogos navegáveis, filtros/sort, linha → modal (reusa padrões de schedule.js) |
✅1 | M |
| F | Comparador de times | seletor A vs B + barras divergentes animadas | ✅1 | M |
| G | Camada 2 — dados baratos | estende results.json (attendance, cards y/r, decidedIn), teams.json (ranking/wcDebut/confederation), stadiums.json (coords); backfill stats; liga recordes de público, disciplina, zebras, distância |
🟡🧩2 | M + entrada de dados |
| H | Camada 3 — jogadores | players.json+player-events.json+awards.json; pódio artilharia, chips (assist/cartões/defesas), bloco de prêmios, Seleção do Torneio (formação), comparador de jogadores, recordes de tempo de gol |
🔴3 | L (maior) + entrada contínua |
| I | Camada 4 — editorial | curiosities.json + all-time-baselines.json; render de cards editoriais + painel "esta Copa vs história" |
📝4 | M + redação |
| J | Polimento | auditoria responsiva/a11y, performance (lazy, sem blur em cards repetidos), Lighthouse, README + i18n review | todas | M |
Caminho mínimo para uma tela publicável e bonita: A→B→C→D→E→F (tudo com dados de hoje). G/H/I são aditivos e podem entrar em qualquer ordem conforme os dados aparecem (graças a §0).
6 · Pontos de atenção (edge cases, dados incompletos, fallbacks)
- Torneio ainda não acabou (hoje 2026-06-14). A tela é pós-Copa. Campeão/ranking só existem após
a final → o hero veredito não renderiza enquanto
FINALnão estiverfinished. Opção: manter o tab oculto até a final terminar, ou deixá-lo "acender" progressivamente. Decidir antes da Etapa B. statsesparso (hoje ~9/104). Agregados de posse/chutes/cartões só aparecem com cobertura completa; senão ficam escondidos (sem disclaimer — requisito do usuário). Backfill é a Etapa G.cardsé só amarelo hoje. Índice de disciplina/fair-play e "time mais faltoso" ficam incompletos até o split y/r (Etapa G). Antes disso: rotular como "amarelos" ou esconder.- Convenção de V/E/D no mata-mata. Empate decidido nos pênaltis conta como empate (para gols) mas vitória/derrota (para avanço). Definir e documentar na camada de dados antes de somar retrospectos.
- Desempate do ranking 1–48. Cadeia determinística explícita: fase alcançada → pontos → SG → GP → (fallback id). Documentar; sem isso o ranking não é reproduzível.
- Burden e confiabilidade dos dados de jogador. ~400+ linhas manuais; risco de typo/evento perdido. Usar uma fonte autoritativa (site oficial FIFA), entrada incremental por rodada. UI esconde seções/linhas sem dado (§0) — nunca mostra lacuna.
- Editorial bilíngue. Todo card de
curiosities.jsonprecisa EN e PT; se faltar um idioma, fallback para o outro (não renderiza em branco). - Performance. Render preguiçoso por seção; sem
backdrop-filterem cards repetidos (custo de paint — regra já vigente no projeto); charts em SVG/CSS sem lib; memoizar o modelo. - Cache-busting & deploy. Automático via
?t=Date.now()em cada fetch (semDATA_VERSIONa bumpar — removido em 2026-06-18); novos arquivos emdata/são deployados (bom);.agents/(este plano) é excluído (bom). Paths relativos para fotos de jogador (gotcha #7 — subpath Hostinger/Pages). - Não quebrar o existente. Manter o padrão de import circular com
app.js; novo tab não pode alterar o contrato dotablist/roteamento atual. - Mídia ausente. Fotos de jogador/bandeiras →
onerrorpara monograma/silhueta; nunca imagem quebrada. - xG sem feed. Coluna/seção xG só existe se todos os times tiverem o dado; senão, removida (não mostrar coluna pela metade).
Apêndice — proveniência
Consolidado de 5 sub-agentes (Times, Individuais, Partidas, Curiosidades, UX). Lista completa de métricas com tags de disponibilidade está no histórico da sessão de 2026-06-14 (Fase 1). Escopo aprovado: 4 camadas + degradação graciosa como requisito de primeira ordem.