chore(tenants): conteudo de publico/ atualizado

This commit is contained in:
deploy
2026-10-21 16:53:28 +00:00
parent 4d2389018b
commit 7b3c811f1b
20 changed files with 6220 additions and 30 deletions
+3
View File
@@ -163,6 +163,9 @@ spec:
- { key: "preparando.js", path: "preparando.js" }
- { key: "privacidade.html", path: "privacidade.html" }
- { key: "termos.html", path: "termos.html" }
- { key: "time.css", path: "time.css" }
- { key: "time.html", path: "time.html" }
- { key: "time.js", path: "time.js" }
- configMap:
name: frontend-publico-binario
items:
+619 -3
View File
@@ -409,7 +409,7 @@ data:
<form id="formulario" hidden novalidate>
<p class="explica">
Preencha os dados do seu time e dos jogadores (só o nome e o número da camisa).
Preencha os dados do seu time, dos jogadores (só o nome e o número da camisa) e da comissão técnica (nome e função).
A organização confere e aprova antes do time entrar na competição.
</p>
@@ -433,12 +433,23 @@ data:
<button type="button" id="maisJogador" class="secundario">+ Jogador</button>
</fieldset>
<fieldset>
<legend>Comissão técnica</legend>
<div id="comissao"></div>
<datalist id="funcoes-comissao">
<option value="Técnico"></option><option value="Auxiliar técnico"></option><option value="Preparador físico"></option>
<option value="Preparador de goleiros"></option><option value="Médico"></option><option value="Fisioterapeuta"></option>
<option value="Massagista"></option><option value="Roupeiro"></option><option value="Dirigente"></option>
</datalist>
<button type="button" id="maisComissao" class="secundario">+ Pessoa na comissão</button>
</fieldset>
<p id="erro" class="erro" role="alert" hidden></p>
<button type="submit" id="enviar">Enviar time</button>
</form>
</main>
<script src="/publico/desafio.js?v=1"></script>
<script src="/publico/convite.js?v=2"></script>
<script src="/publico/convite.js?v=3"></script>
</body>
</html>
@@ -465,6 +476,7 @@ data:
var TOKEN = /^[A-Za-z0-9_-]{20,64}$/;
var MAX_JOGADORES = 60;
var MAX_COMISSAO = 15;
var token = decodeURIComponent((location.pathname.split("/").filter(Boolean).pop() || ""));
var base = "/api/public/competicoes/convites/" + encodeURIComponent(token);
@@ -473,6 +485,7 @@ data:
var situacao = document.getElementById("situacao");
var formulario = document.getElementById("formulario");
var listaJogadores = document.getElementById("jogadores");
var listaComissao = document.getElementById("comissao");
var erro = document.getElementById("erro");
var enviar = document.getElementById("enviar");
var ROTULO_ENVIAR = enviar.textContent;
@@ -520,6 +533,39 @@ data:
return linha;
}
/** Comissao tecnica (21/10/2026): nome e funcao; a funcao tem sugestoes, mas aceita texto livre. */
function linhaDeComissao(funcao) {
var linha = document.createElement("div");
linha.className = "linha-jogador linha-comissao";
var nome = document.createElement("input");
nome.placeholder = "Nome";
nome.maxLength = 120;
nome.setAttribute("aria-label", "Nome na comissão técnica");
nome.setAttribute("data-campo", "nome");
var papel = document.createElement("input");
papel.placeholder = "Função";
papel.maxLength = 40;
papel.value = funcao || "";
papel.setAttribute("list", "funcoes-comissao");
papel.setAttribute("aria-label", "Função na comissão técnica");
papel.setAttribute("data-campo", "funcao");
var tirar = document.createElement("button");
tirar.type = "button";
tirar.className = "secundario";
tirar.textContent = "×";
tirar.setAttribute("aria-label", "Remover da comissão");
tirar.addEventListener("click", function () { linha.remove(); });
linha.appendChild(nome);
linha.appendChild(papel);
linha.appendChild(tirar);
listaComissao.appendChild(linha);
return linha;
}
function valor(id) {
var v = document.getElementById(id).value.trim();
return v === "" ? null : v;
@@ -534,14 +580,26 @@ data:
var c = linhas[i].querySelector('[data-campo="camisa"]').value;
jogadores.push({ nome: nome, numeroCamisa: c === "" ? null : parseInt(c, 10) });
}
var comissao = [];
var faltaFuncao = false;
var pessoas = listaComissao.querySelectorAll(".linha-comissao");
for (var k = 0; k < pessoas.length; k++) {
var n = pessoas[k].querySelector('[data-campo="nome"]').value.trim();
if (!n) continue;
var f = pessoas[k].querySelector('[data-campo="funcao"]').value.trim();
if (!f) faltaFuncao = true;
comissao.push({ nome: n, funcao: f });
}
return {
faltaFuncao: faltaFuncao,
nome: valor("nome"),
escola: valor("escola"),
cidade: valor("cidade"),
contatoNome: valor("contatoNome"),
contatoTelefone: valor("contatoTelefone"),
contatoEmail: valor("contatoEmail"),
jogadores: jogadores
jogadores: jogadores,
comissao: comissao
};
}
@@ -555,6 +613,9 @@ data:
var corpo = montarCorpo();
if (!corpo.nome) { mostrarErro("Informe o nome do time."); return; }
if (corpo.jogadores.length > MAX_JOGADORES) { mostrarErro("No máximo " + MAX_JOGADORES + " jogadores."); return; }
if (corpo.faltaFuncao) { mostrarErro("Informe a função de cada pessoa da comissão técnica."); return; }
if (corpo.comissao.length > MAX_COMISSAO) { mostrarErro("No máximo " + MAX_COMISSAO + " pessoas na comissão técnica."); return; }
delete corpo.faltaFuncao;
enviar.disabled = true;
enviar.textContent = "Verificando…";
enviar.setAttribute("aria-busy", "true");
@@ -615,6 +676,7 @@ data:
situacao.className = "";
formulario.hidden = false;
for (var i = 0; i < 5; i++) linhaDeJogador();
linhaDeComissao("Técnico");
}).catch(function () {
recusar("Não foi possível abrir o convite agora. Tente de novo em instantes.");
});
@@ -623,6 +685,9 @@ data:
document.getElementById("maisJogador").addEventListener("click", function () {
if (listaJogadores.querySelectorAll(".linha-jogador").length < MAX_JOGADORES) linhaDeJogador().querySelector("input").focus();
});
document.getElementById("maisComissao").addEventListener("click", function () {
if (listaComissao.querySelectorAll(".linha-comissao").length < MAX_COMISSAO) linhaDeComissao("").querySelector("input").focus();
});
formulario.addEventListener("submit", aoEnviar);
iniciar();
})();
@@ -8242,6 +8307,557 @@ data:
</body>
</html>
time.css: |
/* Link do time: página pública, sem fonte nem script de fora (a CSP só deixa 'self'). Mobile primeiro. */
main { max-width: 56rem; }
.explica, .legenda { color: var(--muted); font-size: .9rem; }
.abas { display: flex; gap: .25rem; overflow-x: auto; border-bottom: 2px solid var(--line); margin: 0 0 1rem; }
.abas button { background: transparent; color: var(--navy); border: 0; border-bottom: 3px solid transparent; border-radius: 0; padding: .6rem .9rem; white-space: nowrap; font: inherit; font-weight: 700; cursor: pointer; }
.abas button[aria-selected="true"] { border-bottom-color: var(--orange); }
.abas button:focus-visible, button:focus-visible, input:focus-visible { outline: 3px solid var(--orange); outline-offset: 1px; }
h2 { font-size: 1.15rem; margin: .5rem 0; }
.rolagem { overflow-x: auto; }
table { border-collapse: collapse; width: 100%; font-size: .9rem; background: #fff; }
th, td { padding: .4rem .45rem; text-align: center; border-bottom: 1px solid var(--line); white-space: nowrap; }
th:nth-child(2), td:nth-child(2) { text-align: left; white-space: normal; min-width: 8rem; }
tr.meu { background: #fff4e5; font-weight: 700; }
.forma { display: inline-flex; gap: 3px; vertical-align: middle; }
.ponto { width: .8rem; height: .8rem; border-radius: 50%; display: inline-block; }
.ponto.V { background: #067647; }
.ponto.E { background: #98a2b3; }
.ponto.D { background: #b42318; }
.cartao { display: inline-block; width: .7rem; height: .95rem; border-radius: 2px; vertical-align: middle; margin-right: 3px; }
.cartao.amarelo { background: #f5c518; }
.cartao.vermelho { background: #d92d20; }
.jogo, .sumula { background: #fff; border: 1px solid var(--line); border-radius: 8px; padding: .6rem .8rem; margin: 0 0 .6rem; }
.jogo .quando { color: var(--muted); font-size: .85rem; }
.jogo .confronto { display: grid; grid-template-columns: 1fr auto 1fr; gap: .5rem; align-items: center; font-weight: 700; }
.jogo .confronto span:first-child { text-align: right; }
.jogo .placar { background: var(--navy); color: #fff; border-radius: 6px; padding: .1rem .6rem; }
.etiqueta { display: inline-block; font-size: .75rem; font-weight: 700; padding: .05rem .5rem; border-radius: 99px; background: #fff1e3; border: 1px solid var(--orange); margin-left: .4rem; }
.sumula h3 { margin: 0 0 .3rem; font-size: 1rem; }
.sumula ul { margin: .2rem 0 .5rem; padding-left: 1.2rem; }
form { display: grid; gap: 1rem; }
fieldset { border: 1px solid var(--line); border-radius: 8px; background: #fff; padding: .75rem 1rem 1rem; display: grid; gap: .6rem; }
legend { font-weight: 700; padding: 0 .4rem; }
input { font: inherit; padding: .5rem .6rem; border: 1px solid var(--line); border-radius: 6px; width: 100%; }
.linha-ficha { display: grid; grid-template-columns: 1fr 5.5rem auto; gap: .4rem; align-items: center; }
.linha-ficha button { padding: .4rem .6rem; }
button { font: inherit; font-weight: 700; padding: .65rem 1.1rem; border: 0; border-radius: 8px; background: var(--orange); color: #fff; cursor: pointer; }
button.secundario { background: #fff; color: var(--navy); border: 1px solid var(--line); justify-self: start; }
button:disabled { opacity: .55; cursor: not-allowed; }
.erro { color: #b42318; margin: 0; font-weight: 600; }
.ok { background: #e7f6ec; border-left: 4px solid #067647; padding: .8rem 1rem; margin: 0; }
#situacao.invalido { background: #fff1e3; border-left: 4px solid var(--orange); padding: 1rem 1.2rem; }
.somente-leitura li { margin: .15rem 0; }
@media (max-width: 30rem) { .linha-ficha { grid-template-columns: 1fr 4.5rem auto; } }
time.html: |
<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow">
<meta name="referrer" content="no-referrer">
<title>Seu time na competição — Athletic Map</title>
<link rel="stylesheet" href="/publico/_estilo.css">
<link rel="stylesheet" href="/publico/time.css?v=1">
</head>
<body>
<header><div class="marca">Athletic <span>Map</span></div></header>
<main>
<h1 id="titulo">Seu time na competição</h1>
<p id="situacao" role="status" aria-live="polite">Abrindo o link…</p>
<div id="painel" hidden>
<div class="abas" role="tablist" aria-label="Seções">
<button type="button" role="tab" id="aba-tabela" aria-controls="sec-tabela" aria-selected="true" data-aba="tabela">Tabela</button>
<button type="button" role="tab" id="aba-jogos" aria-controls="sec-jogos" aria-selected="false" tabindex="-1" data-aba="jogos">Jogos</button>
<button type="button" role="tab" id="aba-sumulas" aria-controls="sec-sumulas" aria-selected="false" tabindex="-1" data-aba="sumulas">Súmulas</button>
<button type="button" role="tab" id="aba-ficha" aria-controls="sec-ficha" aria-selected="false" tabindex="-1" data-aba="ficha">Ficha do time</button>
</div>
<section id="sec-tabela" role="tabpanel" aria-labelledby="aba-tabela">
<h2>Tabela</h2>
<div class="rolagem" id="tabela"></div>
<p class="legenda">P pontos · J jogos · V vitórias · E empates · D derrotas · GP gols pró · GC gols contra · SG saldo de gols · % aproveitamento · CA cartões amarelos · CV cartões vermelhos</p>
</section>
<section id="sec-jogos" role="tabpanel" aria-labelledby="aba-jogos" hidden>
<h2>Jogos</h2>
<div id="jogos"></div>
</section>
<section id="sec-sumulas" role="tabpanel" aria-labelledby="aba-sumulas" hidden>
<h2>Súmulas</h2>
<div id="sumulas"></div>
</section>
<section id="sec-ficha" role="tabpanel" aria-labelledby="aba-ficha" hidden>
<h2>Ficha do time</h2>
<p id="ficha-aviso" class="explica"></p>
<form id="ficha" novalidate>
<fieldset>
<legend>Jogadores</legend>
<div id="jogadores"></div>
<button type="button" id="maisJogador" class="secundario">+ Jogador</button>
</fieldset>
<fieldset>
<legend>Comissão técnica</legend>
<div id="comissao"></div>
<datalist id="funcoes-comissao">
<option value="Técnico"></option><option value="Auxiliar técnico"></option><option value="Preparador físico"></option>
<option value="Preparador de goleiros"></option><option value="Médico"></option><option value="Fisioterapeuta"></option>
<option value="Massagista"></option><option value="Roupeiro"></option><option value="Dirigente"></option>
</datalist>
<button type="button" id="maisComissao" class="secundario">+ Pessoa na comissão</button>
</fieldset>
<p id="ficha-erro" class="erro" role="alert" hidden></p>
<p id="ficha-ok" class="ok" role="status" hidden></p>
<button type="submit" id="salvar">Salvar ficha</button>
</form>
</section>
</div>
</main>
<script src="/publico/time.js?v=1"></script>
</body>
</html>
time.js: |
/* ===========================================================================
* Link do time — a página que UMA pessoa do time abre pelo link com chave.
*
* Sem login: a chave no endereço é o único segredo. A página mostra a tabela,
* os jogos e as súmulas da competição e a ficha do time; se o time for de
* fora e ainda não houver jogo encerrado, a ficha pode ser corrigida aqui.
*
* Tudo o que vem do servidor entra na página com `textContent` — nunca como
* HTML. O script é externo porque a CSP é `script-src 'self'`.
* =========================================================================== */
(function () {
"use strict";
var TOKEN = /^[A-Za-z0-9_-]{20,64}$/;
var MAX_JOGADORES = 60;
var MAX_COMISSAO = 15;
var ABAS = ["tabela", "jogos", "sumulas", "ficha"];
var ROTULOS_FORMA = { V: "Vitória", E: "Empate", D: "Derrota" };
var ROTULOS_STATUS = {
AGENDADA: "Agendado", ADIADA: "Adiado", EM_ANDAMENTO: "Em andamento", ENCERRADA: "Encerrado", CANCELADA: "Cancelado"
};
var token = decodeURIComponent(location.pathname.split("/").filter(Boolean).pop() || "");
var base = "/api/public/competicoes/times/" + encodeURIComponent(token);
var titulo = document.getElementById("titulo");
var situacao = document.getElementById("situacao");
var painel = document.getElementById("painel");
var listaJogadores = document.getElementById("jogadores");
var listaComissao = document.getElementById("comissao");
var formFicha = document.getElementById("ficha");
var fichaErro = document.getElementById("ficha-erro");
var fichaOk = document.getElementById("ficha-ok");
var salvar = document.getElementById("salvar");
var visao = null;
function el(tag, classe, texto) {
var e = document.createElement(tag);
if (classe) e.className = classe;
if (texto !== undefined && texto !== null) e.textContent = String(texto);
return e;
}
function limpar(no) {
while (no.firstChild) no.removeChild(no.firstChild);
}
function recusar(texto) {
situacao.textContent = texto;
situacao.className = "invalido";
painel.hidden = true;
}
function lerResposta(r) {
return r.json().catch(function () { return {}; }).then(function (corpo) { return { status: r.status, corpo: corpo }; });
}
// ------------------------------------------------------------------ abas
function abrirAba(nome, focar) {
for (var i = 0; i < ABAS.length; i++) {
var ativa = ABAS[i] === nome;
var aba = document.getElementById("aba-" + ABAS[i]);
aba.setAttribute("aria-selected", ativa ? "true" : "false");
aba.tabIndex = ativa ? 0 : -1;
document.getElementById("sec-" + ABAS[i]).hidden = !ativa;
if (ativa && focar) aba.focus();
}
}
function aoTeclarNaAba(ev) {
var i = ABAS.indexOf(ev.currentTarget.getAttribute("data-aba"));
if (ev.key === "ArrowRight") { abrirAba(ABAS[(i + 1) % ABAS.length], true); ev.preventDefault(); }
else if (ev.key === "ArrowLeft") { abrirAba(ABAS[(i + ABAS.length - 1) % ABAS.length], true); ev.preventDefault(); }
}
// ------------------------------------------------------------------ tabela
function dataBr(iso) {
if (!iso) return "Data a definir";
var d = new Date(iso);
if (isNaN(d.getTime())) return "Data a definir";
try {
return d.toLocaleString("pt-BR", { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit" });
} catch (e) {
return d.toISOString();
}
}
function percentual(n) {
return (Math.round((n || 0) * 10) / 10).toString().replace(".", ",") + "%";
}
function montarTabela(linhas, meuNome) {
var alvo = document.getElementById("tabela");
limpar(alvo);
if (!linhas || !linhas.length) { alvo.appendChild(el("p", "explica", "A tabela aparece quando houver times e jogos.")); return; }
var t = el("table");
t.appendChild(el("caption", "explica", "Classificação"));
var cab = el("tr");
var cols = [["#", "Posição"], ["Time", "Time"], ["P", "Pontos"], ["J", "Jogos"], ["V", "Vitórias"], ["E", "Empates"],
["D", "Derrotas"], ["GP", "Gols pró"], ["GC", "Gols contra"], ["SG", "Saldo de gols"], ["%", "Aproveitamento"],
["Últimos 5", "Últimos 5 jogos"], ["CA", "Cartões amarelos"], ["CV", "Cartões vermelhos"]];
cols.forEach(function (c) {
var th = el("th", null, c[0]);
th.scope = "col";
th.title = c[1];
cab.appendChild(th);
});
var thead = el("thead");
thead.appendChild(cab);
t.appendChild(thead);
var corpo = el("tbody");
linhas.forEach(function (l) {
var tr = el("tr", l.time === meuNome ? "meu" : "");
[l.posicao, l.time, l.pontos, l.jogos, l.vitorias, l.empates, l.derrotas, l.golsPro, l.golsContra, l.saldoGols,
percentual(l.aproveitamento)].forEach(function (v) { tr.appendChild(el("td", null, v)); });
var forma = el("td");
var caixa = el("span", "forma");
(l.ultimos || []).forEach(function (r) {
var p = el("span", "ponto " + r);
p.title = ROTULOS_FORMA[r] || r;
p.setAttribute("role", "img");
p.setAttribute("aria-label", ROTULOS_FORMA[r] || r);
caixa.appendChild(p);
});
forma.appendChild(caixa);
tr.appendChild(forma);
var am = el("td");
am.appendChild(el("span", "cartao amarelo"));
am.appendChild(document.createTextNode(String(l.amarelos || 0)));
tr.appendChild(am);
var vm = el("td");
vm.appendChild(el("span", "cartao vermelho"));
vm.appendChild(document.createTextNode(String(l.vermelhos || 0)));
tr.appendChild(vm);
corpo.appendChild(tr);
});
t.appendChild(corpo);
alvo.appendChild(t);
}
// ------------------------------------------------------------------ jogos
function montarJogos(jogos) {
var alvo = document.getElementById("jogos");
limpar(alvo);
if (!jogos || !jogos.length) { alvo.appendChild(el("p", "explica", "Ainda não há jogos marcados.")); return; }
jogos.forEach(function (j) {
var card = el("div", "jogo");
var quando = el("div", "quando", (j.rodada ? j.rodada + " · " : "") + dataBr(j.data));
var etiqueta = el("span", "etiqueta", ROTULOS_STATUS[j.status] || j.status);
quando.appendChild(etiqueta);
if (j.wo) quando.appendChild(el("span", "etiqueta", "W.O."));
card.appendChild(quando);
var linha = el("div", "confronto");
linha.appendChild(el("span", null, j.mandante));
var temPlacar = j.placarMandante !== undefined && j.placarMandante !== null;
linha.appendChild(el("span", temPlacar ? "placar" : "", temPlacar ? j.placarMandante + " x " + j.placarVisitante : "x"));
linha.appendChild(el("span", null, j.visitante));
card.appendChild(linha);
if (j.penaltis) card.appendChild(el("div", "quando", "Pênaltis: " + j.penaltis.mandante + " x " + j.penaltis.visitante));
if (j.motivo) card.appendChild(el("div", "quando", "Motivo: " + j.motivo));
alvo.appendChild(card);
});
}
// ------------------------------------------------------------------ súmulas
function pessoa(p) {
if (!p) return "jogador não identificado";
if (p.nome && p.numeroCamisa !== undefined && p.numeroCamisa !== null) return p.nome + " (" + p.numeroCamisa + ")";
if (p.nome) return p.nome;
if (p.numeroCamisa !== undefined && p.numeroCamisa !== null) return "camisa " + p.numeroCamisa;
return "jogador não identificado";
}
function momento(e) {
var m = e.minuto !== undefined && e.minuto !== null ? e.minuto + "'" : "";
var t = e.tempo ? e.tempo + "º tempo" : "";
return [t, m].filter(Boolean).join(" ") || "—";
}
function secaoDaSumula(card, nome, itens, texto) {
if (!itens || !itens.length) return;
card.appendChild(el("strong", null, nome));
var ul = el("ul");
itens.forEach(function (i) { ul.appendChild(el("li", null, texto(i))); });
card.appendChild(ul);
}
function montarSumulas(sumulas, jogos) {
var alvo = document.getElementById("sumulas");
limpar(alvo);
if (!sumulas || !sumulas.length) { alvo.appendChild(el("p", "explica", "As súmulas dos jogos do seu time aparecem aqui.")); return; }
sumulas.forEach(function (s) {
var j = (jogos || []).filter(function (x) { return x.ref === s.jogoRef; })[0];
var card = el("div", "sumula");
var cab = j ? j.mandante + " x " + j.visitante : "Jogo " + s.jogoRef;
if (j && j.placarMandante !== undefined && j.placarMandante !== null) {
cab = j.mandante + " " + j.placarMandante + " x " + j.placarVisitante + " " + j.visitante;
}
card.appendChild(el("h3", null, cab));
if (j) card.appendChild(el("div", "quando", (j.rodada ? j.rodada + " · " : "") + dataBr(j.data)));
secaoDaSumula(card, "Gols", s.gols, function (g) { return momento(g) + " · " + g.time + " · " + pessoa(g.jogador); });
secaoDaSumula(card, "Cartões", s.cartoes, function (c) {
return momento(c) + " · " + (c.tipo === "VERMELHO" ? "vermelho" : "amarelo") + " · " + c.time + " · " + pessoa(c.jogador);
});
secaoDaSumula(card, "Substituições", s.substituicoes, function (t) {
return momento(t) + " · " + t.time + " · sai " + pessoa(t.sai) + ", entra " + pessoa(t.entra);
});
alvo.appendChild(card);
});
}
// ------------------------------------------------------------------ ficha
function linhaDeJogador(j) {
var linha = el("div", "linha-ficha linha-jogador");
if (j && j.id) linha.setAttribute("data-id", j.id);
var nome = el("input");
nome.placeholder = "Nome do jogador";
nome.maxLength = 120;
nome.value = (j && j.nome) || "";
nome.setAttribute("aria-label", "Nome do jogador");
nome.setAttribute("data-campo", "nome");
var camisa = el("input");
camisa.type = "number";
camisa.min = "0";
camisa.max = "999";
camisa.placeholder = "nº";
camisa.value = j && j.numeroCamisa !== undefined && j.numeroCamisa !== null ? j.numeroCamisa : "";
camisa.setAttribute("aria-label", "Número da camisa");
camisa.setAttribute("data-campo", "camisa");
var tirar = el("button", "secundario", "×");
tirar.type = "button";
tirar.setAttribute("aria-label", "Remover jogador");
tirar.addEventListener("click", function () { linha.remove(); });
linha.appendChild(nome);
linha.appendChild(camisa);
linha.appendChild(tirar);
listaJogadores.appendChild(linha);
return linha;
}
function linhaDeComissao(m) {
var linha = el("div", "linha-ficha linha-comissao");
if (m && m.id) linha.setAttribute("data-id", m.id);
var nome = el("input");
nome.placeholder = "Nome";
nome.maxLength = 120;
nome.value = (m && m.nome) || "";
nome.setAttribute("aria-label", "Nome na comissão técnica");
nome.setAttribute("data-campo", "nome");
var papel = el("input");
papel.placeholder = "Função";
papel.maxLength = 40;
papel.value = (m && m.funcao) || "";
papel.setAttribute("list", "funcoes-comissao");
papel.setAttribute("aria-label", "Função na comissão técnica");
papel.setAttribute("data-campo", "funcao");
var tirar = el("button", "secundario", "×");
tirar.type = "button";
tirar.setAttribute("aria-label", "Remover da comissão");
tirar.addEventListener("click", function () { linha.remove(); });
linha.appendChild(nome);
linha.appendChild(papel);
linha.appendChild(tirar);
listaComissao.appendChild(linha);
return linha;
}
/** Time da escola ou ficha fechada: só leitura (listas simples). */
function fichaSomenteLeitura(ficha) {
formFicha.hidden = true;
var alvo = document.getElementById("ficha-leitura");
if (!alvo) {
alvo = el("div", "somente-leitura");
alvo.id = "ficha-leitura";
formFicha.parentNode.appendChild(alvo);
}
limpar(alvo);
alvo.appendChild(el("h3", null, "Jogadores"));
var ul = el("ul");
(ficha.jogadores || []).forEach(function (j) {
ul.appendChild(el("li", null, pessoa({ nome: j.nome, numeroCamisa: j.numeroCamisa })));
});
if (!ul.childNodes.length) ul.appendChild(el("li", null, "Nenhum jogador na ficha."));
alvo.appendChild(ul);
if ((ficha.comissao || []).length) {
alvo.appendChild(el("h3", null, "Comissão técnica"));
var uc = el("ul");
ficha.comissao.forEach(function (m) { uc.appendChild(el("li", null, m.nome + " — " + m.funcao)); });
alvo.appendChild(uc);
}
}
function montarFicha(ficha) {
var aviso = document.getElementById("ficha-aviso");
limpar(listaJogadores);
limpar(listaComissao);
if (!ficha.editavel) {
aviso.textContent = "Esta ficha está fechada para alterações aqui. Se algo estiver errado, fale com a organização.";
fichaSomenteLeitura(ficha);
return;
}
aviso.textContent = "Confira os jogadores (nome e número da camisa) e a comissão técnica. Dá para corrigir até o primeiro jogo ser encerrado.";
formFicha.hidden = false;
(ficha.jogadores || []).forEach(linhaDeJogador);
if (!(ficha.jogadores || []).length) for (var i = 0; i < 3; i++) linhaDeJogador();
(ficha.comissao || []).forEach(linhaDeComissao);
}
function mostrarErroFicha(texto) {
fichaErro.textContent = texto || "";
fichaErro.hidden = !texto;
}
function mostrarOkFicha(texto) {
fichaOk.textContent = texto || "";
fichaOk.hidden = !texto;
}
function corpoDaFicha() {
var jogadores = [];
var linhas = listaJogadores.querySelectorAll(".linha-jogador");
for (var i = 0; i < linhas.length; i++) {
var nome = linhas[i].querySelector('[data-campo="nome"]').value.trim();
if (!nome) continue;
var c = linhas[i].querySelector('[data-campo="camisa"]').value;
var j = { nome: nome, numeroCamisa: c === "" ? null : parseInt(c, 10) };
if (linhas[i].getAttribute("data-id")) j.id = linhas[i].getAttribute("data-id");
jogadores.push(j);
}
var comissao = [];
var faltaFuncao = false;
var pessoas = listaComissao.querySelectorAll(".linha-comissao");
for (var k = 0; k < pessoas.length; k++) {
var n = pessoas[k].querySelector('[data-campo="nome"]').value.trim();
if (!n) continue;
var f = pessoas[k].querySelector('[data-campo="funcao"]').value.trim();
if (!f) faltaFuncao = true;
var m = { nome: n, funcao: f };
if (pessoas[k].getAttribute("data-id")) m.id = pessoas[k].getAttribute("data-id");
comissao.push(m);
}
return { jogadores: jogadores, comissao: comissao, faltaFuncao: faltaFuncao };
}
function aoSalvar(ev) {
ev.preventDefault();
mostrarErroFicha("");
mostrarOkFicha("");
var c = corpoDaFicha();
if (c.jogadores.length > MAX_JOGADORES) { mostrarErroFicha("No máximo " + MAX_JOGADORES + " jogadores."); return; }
if (c.faltaFuncao) { mostrarErroFicha("Informe a função de cada pessoa da comissão técnica."); return; }
if (c.comissao.length > MAX_COMISSAO) { mostrarErroFicha("No máximo " + MAX_COMISSAO + " pessoas na comissão técnica."); return; }
salvar.disabled = true;
salvar.setAttribute("aria-busy", "true");
fetch(base + "/ficha", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ jogadores: c.jogadores, comissao: c.comissao })
}).then(lerResposta).then(function (r) {
salvar.disabled = false;
salvar.removeAttribute("aria-busy");
if (r.status >= 200 && r.status < 300) {
mostrarOkFicha("Ficha salva.");
return carregar(true);
}
if (r.status === 404) { recusar("Este link não está mais disponível."); return null; }
mostrarErroFicha((r.corpo && r.corpo.detail) || "Não foi possível salvar agora. Tente de novo em instantes.");
return null;
}).catch(function () {
salvar.disabled = false;
salvar.removeAttribute("aria-busy");
mostrarErroFicha("Sem conexão. Confira a internet e tente de novo.");
});
}
// ------------------------------------------------------------------ carga
function mostrar(v, manterAba) {
visao = v;
var nomeTime = (v.time && v.time.nome) || "Seu time";
titulo.textContent = (v.competicao || "Competição") + " — " + nomeTime;
document.title = titulo.textContent + " — Athletic Map";
var partes = [];
if (v.time && v.time.escola) partes.push(v.time.escola);
if (v.time && v.time.cidade) partes.push(v.time.cidade);
if (v.temporada && v.temporada.nome) partes.push("Temporada " + v.temporada.nome);
if (v.time && v.time.status === "DESQUALIFICADO") partes.push("Time desqualificado");
situacao.textContent = partes.join(" · ");
situacao.className = "";
painel.hidden = false;
montarTabela(v.classificacao, nomeTime);
montarJogos(v.jogos);
montarSumulas(v.sumulas, v.jogos);
montarFicha(v.ficha || { jogadores: [], comissao: [], editavel: false });
if (!manterAba) abrirAba("tabela", false);
}
function carregar(manterAba) {
return fetch(base).then(lerResposta).then(function (r) {
if (r.status === 404) { recusar("Este link não está disponível ou foi desativado. Peça um novo à organização."); return; }
if (r.status === 429) { recusar("Muitas consultas seguidas. Aguarde alguns minutos e abra o link de novo."); return; }
if (r.status !== 200) { recusar("Não foi possível abrir agora. Tente de novo em instantes."); return; }
mostrar(r.corpo || {}, manterAba);
}).catch(function () {
recusar("Não foi possível abrir agora. Tente de novo em instantes.");
});
}
function iniciar() {
if (!TOKEN.test(token)) { recusar("Este endereço não é válido."); return; }
ABAS.forEach(function (nome) {
var aba = document.getElementById("aba-" + nome);
aba.addEventListener("click", function () { abrirAba(nome, false); });
aba.addEventListener("keydown", aoTeclarNaAba);
});
document.getElementById("maisJogador").addEventListener("click", function () {
if (listaJogadores.querySelectorAll(".linha-jogador").length < MAX_JOGADORES) linhaDeJogador().querySelector("input").focus();
});
document.getElementById("maisComissao").addEventListener("click", function () {
if (listaComissao.querySelectorAll(".linha-comissao").length < MAX_COMISSAO) linhaDeComissao(null).querySelector("input").focus();
});
formFicha.addEventListener("submit", aoSalvar);
carregar(false);
}
iniciar();
})();
---
apiVersion: v1
kind: ConfigMap
+3
View File
@@ -153,6 +153,9 @@ spec:
- { key: "preparando.js", path: "preparando.js" }
- { key: "privacidade.html", path: "privacidade.html" }
- { key: "termos.html", path: "termos.html" }
- { key: "time.css", path: "time.css" }
- { key: "time.html", path: "time.html" }
- { key: "time.js", path: "time.js" }
- configMap:
name: frontend-publico-binario
items:
+619 -3
View File
@@ -409,7 +409,7 @@ data:
<form id="formulario" hidden novalidate>
<p class="explica">
Preencha os dados do seu time e dos jogadores (só o nome e o número da camisa).
Preencha os dados do seu time, dos jogadores (só o nome e o número da camisa) e da comissão técnica (nome e função).
A organização confere e aprova antes do time entrar na competição.
</p>
@@ -433,12 +433,23 @@ data:
<button type="button" id="maisJogador" class="secundario">+ Jogador</button>
</fieldset>
<fieldset>
<legend>Comissão técnica</legend>
<div id="comissao"></div>
<datalist id="funcoes-comissao">
<option value="Técnico"></option><option value="Auxiliar técnico"></option><option value="Preparador físico"></option>
<option value="Preparador de goleiros"></option><option value="Médico"></option><option value="Fisioterapeuta"></option>
<option value="Massagista"></option><option value="Roupeiro"></option><option value="Dirigente"></option>
</datalist>
<button type="button" id="maisComissao" class="secundario">+ Pessoa na comissão</button>
</fieldset>
<p id="erro" class="erro" role="alert" hidden></p>
<button type="submit" id="enviar">Enviar time</button>
</form>
</main>
<script src="/publico/desafio.js?v=1"></script>
<script src="/publico/convite.js?v=2"></script>
<script src="/publico/convite.js?v=3"></script>
</body>
</html>
@@ -465,6 +476,7 @@ data:
var TOKEN = /^[A-Za-z0-9_-]{20,64}$/;
var MAX_JOGADORES = 60;
var MAX_COMISSAO = 15;
var token = decodeURIComponent((location.pathname.split("/").filter(Boolean).pop() || ""));
var base = "/api/public/competicoes/convites/" + encodeURIComponent(token);
@@ -473,6 +485,7 @@ data:
var situacao = document.getElementById("situacao");
var formulario = document.getElementById("formulario");
var listaJogadores = document.getElementById("jogadores");
var listaComissao = document.getElementById("comissao");
var erro = document.getElementById("erro");
var enviar = document.getElementById("enviar");
var ROTULO_ENVIAR = enviar.textContent;
@@ -520,6 +533,39 @@ data:
return linha;
}
/** Comissao tecnica (21/10/2026): nome e funcao; a funcao tem sugestoes, mas aceita texto livre. */
function linhaDeComissao(funcao) {
var linha = document.createElement("div");
linha.className = "linha-jogador linha-comissao";
var nome = document.createElement("input");
nome.placeholder = "Nome";
nome.maxLength = 120;
nome.setAttribute("aria-label", "Nome na comissão técnica");
nome.setAttribute("data-campo", "nome");
var papel = document.createElement("input");
papel.placeholder = "Função";
papel.maxLength = 40;
papel.value = funcao || "";
papel.setAttribute("list", "funcoes-comissao");
papel.setAttribute("aria-label", "Função na comissão técnica");
papel.setAttribute("data-campo", "funcao");
var tirar = document.createElement("button");
tirar.type = "button";
tirar.className = "secundario";
tirar.textContent = "×";
tirar.setAttribute("aria-label", "Remover da comissão");
tirar.addEventListener("click", function () { linha.remove(); });
linha.appendChild(nome);
linha.appendChild(papel);
linha.appendChild(tirar);
listaComissao.appendChild(linha);
return linha;
}
function valor(id) {
var v = document.getElementById(id).value.trim();
return v === "" ? null : v;
@@ -534,14 +580,26 @@ data:
var c = linhas[i].querySelector('[data-campo="camisa"]').value;
jogadores.push({ nome: nome, numeroCamisa: c === "" ? null : parseInt(c, 10) });
}
var comissao = [];
var faltaFuncao = false;
var pessoas = listaComissao.querySelectorAll(".linha-comissao");
for (var k = 0; k < pessoas.length; k++) {
var n = pessoas[k].querySelector('[data-campo="nome"]').value.trim();
if (!n) continue;
var f = pessoas[k].querySelector('[data-campo="funcao"]').value.trim();
if (!f) faltaFuncao = true;
comissao.push({ nome: n, funcao: f });
}
return {
faltaFuncao: faltaFuncao,
nome: valor("nome"),
escola: valor("escola"),
cidade: valor("cidade"),
contatoNome: valor("contatoNome"),
contatoTelefone: valor("contatoTelefone"),
contatoEmail: valor("contatoEmail"),
jogadores: jogadores
jogadores: jogadores,
comissao: comissao
};
}
@@ -555,6 +613,9 @@ data:
var corpo = montarCorpo();
if (!corpo.nome) { mostrarErro("Informe o nome do time."); return; }
if (corpo.jogadores.length > MAX_JOGADORES) { mostrarErro("No máximo " + MAX_JOGADORES + " jogadores."); return; }
if (corpo.faltaFuncao) { mostrarErro("Informe a função de cada pessoa da comissão técnica."); return; }
if (corpo.comissao.length > MAX_COMISSAO) { mostrarErro("No máximo " + MAX_COMISSAO + " pessoas na comissão técnica."); return; }
delete corpo.faltaFuncao;
enviar.disabled = true;
enviar.textContent = "Verificando…";
enviar.setAttribute("aria-busy", "true");
@@ -615,6 +676,7 @@ data:
situacao.className = "";
formulario.hidden = false;
for (var i = 0; i < 5; i++) linhaDeJogador();
linhaDeComissao("Técnico");
}).catch(function () {
recusar("Não foi possível abrir o convite agora. Tente de novo em instantes.");
});
@@ -623,6 +685,9 @@ data:
document.getElementById("maisJogador").addEventListener("click", function () {
if (listaJogadores.querySelectorAll(".linha-jogador").length < MAX_JOGADORES) linhaDeJogador().querySelector("input").focus();
});
document.getElementById("maisComissao").addEventListener("click", function () {
if (listaComissao.querySelectorAll(".linha-comissao").length < MAX_COMISSAO) linhaDeComissao("").querySelector("input").focus();
});
formulario.addEventListener("submit", aoEnviar);
iniciar();
})();
@@ -8242,6 +8307,557 @@ data:
</body>
</html>
time.css: |
/* Link do time: página pública, sem fonte nem script de fora (a CSP só deixa 'self'). Mobile primeiro. */
main { max-width: 56rem; }
.explica, .legenda { color: var(--muted); font-size: .9rem; }
.abas { display: flex; gap: .25rem; overflow-x: auto; border-bottom: 2px solid var(--line); margin: 0 0 1rem; }
.abas button { background: transparent; color: var(--navy); border: 0; border-bottom: 3px solid transparent; border-radius: 0; padding: .6rem .9rem; white-space: nowrap; font: inherit; font-weight: 700; cursor: pointer; }
.abas button[aria-selected="true"] { border-bottom-color: var(--orange); }
.abas button:focus-visible, button:focus-visible, input:focus-visible { outline: 3px solid var(--orange); outline-offset: 1px; }
h2 { font-size: 1.15rem; margin: .5rem 0; }
.rolagem { overflow-x: auto; }
table { border-collapse: collapse; width: 100%; font-size: .9rem; background: #fff; }
th, td { padding: .4rem .45rem; text-align: center; border-bottom: 1px solid var(--line); white-space: nowrap; }
th:nth-child(2), td:nth-child(2) { text-align: left; white-space: normal; min-width: 8rem; }
tr.meu { background: #fff4e5; font-weight: 700; }
.forma { display: inline-flex; gap: 3px; vertical-align: middle; }
.ponto { width: .8rem; height: .8rem; border-radius: 50%; display: inline-block; }
.ponto.V { background: #067647; }
.ponto.E { background: #98a2b3; }
.ponto.D { background: #b42318; }
.cartao { display: inline-block; width: .7rem; height: .95rem; border-radius: 2px; vertical-align: middle; margin-right: 3px; }
.cartao.amarelo { background: #f5c518; }
.cartao.vermelho { background: #d92d20; }
.jogo, .sumula { background: #fff; border: 1px solid var(--line); border-radius: 8px; padding: .6rem .8rem; margin: 0 0 .6rem; }
.jogo .quando { color: var(--muted); font-size: .85rem; }
.jogo .confronto { display: grid; grid-template-columns: 1fr auto 1fr; gap: .5rem; align-items: center; font-weight: 700; }
.jogo .confronto span:first-child { text-align: right; }
.jogo .placar { background: var(--navy); color: #fff; border-radius: 6px; padding: .1rem .6rem; }
.etiqueta { display: inline-block; font-size: .75rem; font-weight: 700; padding: .05rem .5rem; border-radius: 99px; background: #fff1e3; border: 1px solid var(--orange); margin-left: .4rem; }
.sumula h3 { margin: 0 0 .3rem; font-size: 1rem; }
.sumula ul { margin: .2rem 0 .5rem; padding-left: 1.2rem; }
form { display: grid; gap: 1rem; }
fieldset { border: 1px solid var(--line); border-radius: 8px; background: #fff; padding: .75rem 1rem 1rem; display: grid; gap: .6rem; }
legend { font-weight: 700; padding: 0 .4rem; }
input { font: inherit; padding: .5rem .6rem; border: 1px solid var(--line); border-radius: 6px; width: 100%; }
.linha-ficha { display: grid; grid-template-columns: 1fr 5.5rem auto; gap: .4rem; align-items: center; }
.linha-ficha button { padding: .4rem .6rem; }
button { font: inherit; font-weight: 700; padding: .65rem 1.1rem; border: 0; border-radius: 8px; background: var(--orange); color: #fff; cursor: pointer; }
button.secundario { background: #fff; color: var(--navy); border: 1px solid var(--line); justify-self: start; }
button:disabled { opacity: .55; cursor: not-allowed; }
.erro { color: #b42318; margin: 0; font-weight: 600; }
.ok { background: #e7f6ec; border-left: 4px solid #067647; padding: .8rem 1rem; margin: 0; }
#situacao.invalido { background: #fff1e3; border-left: 4px solid var(--orange); padding: 1rem 1.2rem; }
.somente-leitura li { margin: .15rem 0; }
@media (max-width: 30rem) { .linha-ficha { grid-template-columns: 1fr 4.5rem auto; } }
time.html: |
<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow">
<meta name="referrer" content="no-referrer">
<title>Seu time na competição — Athletic Map</title>
<link rel="stylesheet" href="/publico/_estilo.css">
<link rel="stylesheet" href="/publico/time.css?v=1">
</head>
<body>
<header><div class="marca">Athletic <span>Map</span></div></header>
<main>
<h1 id="titulo">Seu time na competição</h1>
<p id="situacao" role="status" aria-live="polite">Abrindo o link…</p>
<div id="painel" hidden>
<div class="abas" role="tablist" aria-label="Seções">
<button type="button" role="tab" id="aba-tabela" aria-controls="sec-tabela" aria-selected="true" data-aba="tabela">Tabela</button>
<button type="button" role="tab" id="aba-jogos" aria-controls="sec-jogos" aria-selected="false" tabindex="-1" data-aba="jogos">Jogos</button>
<button type="button" role="tab" id="aba-sumulas" aria-controls="sec-sumulas" aria-selected="false" tabindex="-1" data-aba="sumulas">Súmulas</button>
<button type="button" role="tab" id="aba-ficha" aria-controls="sec-ficha" aria-selected="false" tabindex="-1" data-aba="ficha">Ficha do time</button>
</div>
<section id="sec-tabela" role="tabpanel" aria-labelledby="aba-tabela">
<h2>Tabela</h2>
<div class="rolagem" id="tabela"></div>
<p class="legenda">P pontos · J jogos · V vitórias · E empates · D derrotas · GP gols pró · GC gols contra · SG saldo de gols · % aproveitamento · CA cartões amarelos · CV cartões vermelhos</p>
</section>
<section id="sec-jogos" role="tabpanel" aria-labelledby="aba-jogos" hidden>
<h2>Jogos</h2>
<div id="jogos"></div>
</section>
<section id="sec-sumulas" role="tabpanel" aria-labelledby="aba-sumulas" hidden>
<h2>Súmulas</h2>
<div id="sumulas"></div>
</section>
<section id="sec-ficha" role="tabpanel" aria-labelledby="aba-ficha" hidden>
<h2>Ficha do time</h2>
<p id="ficha-aviso" class="explica"></p>
<form id="ficha" novalidate>
<fieldset>
<legend>Jogadores</legend>
<div id="jogadores"></div>
<button type="button" id="maisJogador" class="secundario">+ Jogador</button>
</fieldset>
<fieldset>
<legend>Comissão técnica</legend>
<div id="comissao"></div>
<datalist id="funcoes-comissao">
<option value="Técnico"></option><option value="Auxiliar técnico"></option><option value="Preparador físico"></option>
<option value="Preparador de goleiros"></option><option value="Médico"></option><option value="Fisioterapeuta"></option>
<option value="Massagista"></option><option value="Roupeiro"></option><option value="Dirigente"></option>
</datalist>
<button type="button" id="maisComissao" class="secundario">+ Pessoa na comissão</button>
</fieldset>
<p id="ficha-erro" class="erro" role="alert" hidden></p>
<p id="ficha-ok" class="ok" role="status" hidden></p>
<button type="submit" id="salvar">Salvar ficha</button>
</form>
</section>
</div>
</main>
<script src="/publico/time.js?v=1"></script>
</body>
</html>
time.js: |
/* ===========================================================================
* Link do time — a página que UMA pessoa do time abre pelo link com chave.
*
* Sem login: a chave no endereço é o único segredo. A página mostra a tabela,
* os jogos e as súmulas da competição e a ficha do time; se o time for de
* fora e ainda não houver jogo encerrado, a ficha pode ser corrigida aqui.
*
* Tudo o que vem do servidor entra na página com `textContent` — nunca como
* HTML. O script é externo porque a CSP é `script-src 'self'`.
* =========================================================================== */
(function () {
"use strict";
var TOKEN = /^[A-Za-z0-9_-]{20,64}$/;
var MAX_JOGADORES = 60;
var MAX_COMISSAO = 15;
var ABAS = ["tabela", "jogos", "sumulas", "ficha"];
var ROTULOS_FORMA = { V: "Vitória", E: "Empate", D: "Derrota" };
var ROTULOS_STATUS = {
AGENDADA: "Agendado", ADIADA: "Adiado", EM_ANDAMENTO: "Em andamento", ENCERRADA: "Encerrado", CANCELADA: "Cancelado"
};
var token = decodeURIComponent(location.pathname.split("/").filter(Boolean).pop() || "");
var base = "/api/public/competicoes/times/" + encodeURIComponent(token);
var titulo = document.getElementById("titulo");
var situacao = document.getElementById("situacao");
var painel = document.getElementById("painel");
var listaJogadores = document.getElementById("jogadores");
var listaComissao = document.getElementById("comissao");
var formFicha = document.getElementById("ficha");
var fichaErro = document.getElementById("ficha-erro");
var fichaOk = document.getElementById("ficha-ok");
var salvar = document.getElementById("salvar");
var visao = null;
function el(tag, classe, texto) {
var e = document.createElement(tag);
if (classe) e.className = classe;
if (texto !== undefined && texto !== null) e.textContent = String(texto);
return e;
}
function limpar(no) {
while (no.firstChild) no.removeChild(no.firstChild);
}
function recusar(texto) {
situacao.textContent = texto;
situacao.className = "invalido";
painel.hidden = true;
}
function lerResposta(r) {
return r.json().catch(function () { return {}; }).then(function (corpo) { return { status: r.status, corpo: corpo }; });
}
// ------------------------------------------------------------------ abas
function abrirAba(nome, focar) {
for (var i = 0; i < ABAS.length; i++) {
var ativa = ABAS[i] === nome;
var aba = document.getElementById("aba-" + ABAS[i]);
aba.setAttribute("aria-selected", ativa ? "true" : "false");
aba.tabIndex = ativa ? 0 : -1;
document.getElementById("sec-" + ABAS[i]).hidden = !ativa;
if (ativa && focar) aba.focus();
}
}
function aoTeclarNaAba(ev) {
var i = ABAS.indexOf(ev.currentTarget.getAttribute("data-aba"));
if (ev.key === "ArrowRight") { abrirAba(ABAS[(i + 1) % ABAS.length], true); ev.preventDefault(); }
else if (ev.key === "ArrowLeft") { abrirAba(ABAS[(i + ABAS.length - 1) % ABAS.length], true); ev.preventDefault(); }
}
// ------------------------------------------------------------------ tabela
function dataBr(iso) {
if (!iso) return "Data a definir";
var d = new Date(iso);
if (isNaN(d.getTime())) return "Data a definir";
try {
return d.toLocaleString("pt-BR", { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit" });
} catch (e) {
return d.toISOString();
}
}
function percentual(n) {
return (Math.round((n || 0) * 10) / 10).toString().replace(".", ",") + "%";
}
function montarTabela(linhas, meuNome) {
var alvo = document.getElementById("tabela");
limpar(alvo);
if (!linhas || !linhas.length) { alvo.appendChild(el("p", "explica", "A tabela aparece quando houver times e jogos.")); return; }
var t = el("table");
t.appendChild(el("caption", "explica", "Classificação"));
var cab = el("tr");
var cols = [["#", "Posição"], ["Time", "Time"], ["P", "Pontos"], ["J", "Jogos"], ["V", "Vitórias"], ["E", "Empates"],
["D", "Derrotas"], ["GP", "Gols pró"], ["GC", "Gols contra"], ["SG", "Saldo de gols"], ["%", "Aproveitamento"],
["Últimos 5", "Últimos 5 jogos"], ["CA", "Cartões amarelos"], ["CV", "Cartões vermelhos"]];
cols.forEach(function (c) {
var th = el("th", null, c[0]);
th.scope = "col";
th.title = c[1];
cab.appendChild(th);
});
var thead = el("thead");
thead.appendChild(cab);
t.appendChild(thead);
var corpo = el("tbody");
linhas.forEach(function (l) {
var tr = el("tr", l.time === meuNome ? "meu" : "");
[l.posicao, l.time, l.pontos, l.jogos, l.vitorias, l.empates, l.derrotas, l.golsPro, l.golsContra, l.saldoGols,
percentual(l.aproveitamento)].forEach(function (v) { tr.appendChild(el("td", null, v)); });
var forma = el("td");
var caixa = el("span", "forma");
(l.ultimos || []).forEach(function (r) {
var p = el("span", "ponto " + r);
p.title = ROTULOS_FORMA[r] || r;
p.setAttribute("role", "img");
p.setAttribute("aria-label", ROTULOS_FORMA[r] || r);
caixa.appendChild(p);
});
forma.appendChild(caixa);
tr.appendChild(forma);
var am = el("td");
am.appendChild(el("span", "cartao amarelo"));
am.appendChild(document.createTextNode(String(l.amarelos || 0)));
tr.appendChild(am);
var vm = el("td");
vm.appendChild(el("span", "cartao vermelho"));
vm.appendChild(document.createTextNode(String(l.vermelhos || 0)));
tr.appendChild(vm);
corpo.appendChild(tr);
});
t.appendChild(corpo);
alvo.appendChild(t);
}
// ------------------------------------------------------------------ jogos
function montarJogos(jogos) {
var alvo = document.getElementById("jogos");
limpar(alvo);
if (!jogos || !jogos.length) { alvo.appendChild(el("p", "explica", "Ainda não há jogos marcados.")); return; }
jogos.forEach(function (j) {
var card = el("div", "jogo");
var quando = el("div", "quando", (j.rodada ? j.rodada + " · " : "") + dataBr(j.data));
var etiqueta = el("span", "etiqueta", ROTULOS_STATUS[j.status] || j.status);
quando.appendChild(etiqueta);
if (j.wo) quando.appendChild(el("span", "etiqueta", "W.O."));
card.appendChild(quando);
var linha = el("div", "confronto");
linha.appendChild(el("span", null, j.mandante));
var temPlacar = j.placarMandante !== undefined && j.placarMandante !== null;
linha.appendChild(el("span", temPlacar ? "placar" : "", temPlacar ? j.placarMandante + " x " + j.placarVisitante : "x"));
linha.appendChild(el("span", null, j.visitante));
card.appendChild(linha);
if (j.penaltis) card.appendChild(el("div", "quando", "Pênaltis: " + j.penaltis.mandante + " x " + j.penaltis.visitante));
if (j.motivo) card.appendChild(el("div", "quando", "Motivo: " + j.motivo));
alvo.appendChild(card);
});
}
// ------------------------------------------------------------------ súmulas
function pessoa(p) {
if (!p) return "jogador não identificado";
if (p.nome && p.numeroCamisa !== undefined && p.numeroCamisa !== null) return p.nome + " (" + p.numeroCamisa + ")";
if (p.nome) return p.nome;
if (p.numeroCamisa !== undefined && p.numeroCamisa !== null) return "camisa " + p.numeroCamisa;
return "jogador não identificado";
}
function momento(e) {
var m = e.minuto !== undefined && e.minuto !== null ? e.minuto + "'" : "";
var t = e.tempo ? e.tempo + "º tempo" : "";
return [t, m].filter(Boolean).join(" ") || "—";
}
function secaoDaSumula(card, nome, itens, texto) {
if (!itens || !itens.length) return;
card.appendChild(el("strong", null, nome));
var ul = el("ul");
itens.forEach(function (i) { ul.appendChild(el("li", null, texto(i))); });
card.appendChild(ul);
}
function montarSumulas(sumulas, jogos) {
var alvo = document.getElementById("sumulas");
limpar(alvo);
if (!sumulas || !sumulas.length) { alvo.appendChild(el("p", "explica", "As súmulas dos jogos do seu time aparecem aqui.")); return; }
sumulas.forEach(function (s) {
var j = (jogos || []).filter(function (x) { return x.ref === s.jogoRef; })[0];
var card = el("div", "sumula");
var cab = j ? j.mandante + " x " + j.visitante : "Jogo " + s.jogoRef;
if (j && j.placarMandante !== undefined && j.placarMandante !== null) {
cab = j.mandante + " " + j.placarMandante + " x " + j.placarVisitante + " " + j.visitante;
}
card.appendChild(el("h3", null, cab));
if (j) card.appendChild(el("div", "quando", (j.rodada ? j.rodada + " · " : "") + dataBr(j.data)));
secaoDaSumula(card, "Gols", s.gols, function (g) { return momento(g) + " · " + g.time + " · " + pessoa(g.jogador); });
secaoDaSumula(card, "Cartões", s.cartoes, function (c) {
return momento(c) + " · " + (c.tipo === "VERMELHO" ? "vermelho" : "amarelo") + " · " + c.time + " · " + pessoa(c.jogador);
});
secaoDaSumula(card, "Substituições", s.substituicoes, function (t) {
return momento(t) + " · " + t.time + " · sai " + pessoa(t.sai) + ", entra " + pessoa(t.entra);
});
alvo.appendChild(card);
});
}
// ------------------------------------------------------------------ ficha
function linhaDeJogador(j) {
var linha = el("div", "linha-ficha linha-jogador");
if (j && j.id) linha.setAttribute("data-id", j.id);
var nome = el("input");
nome.placeholder = "Nome do jogador";
nome.maxLength = 120;
nome.value = (j && j.nome) || "";
nome.setAttribute("aria-label", "Nome do jogador");
nome.setAttribute("data-campo", "nome");
var camisa = el("input");
camisa.type = "number";
camisa.min = "0";
camisa.max = "999";
camisa.placeholder = "nº";
camisa.value = j && j.numeroCamisa !== undefined && j.numeroCamisa !== null ? j.numeroCamisa : "";
camisa.setAttribute("aria-label", "Número da camisa");
camisa.setAttribute("data-campo", "camisa");
var tirar = el("button", "secundario", "×");
tirar.type = "button";
tirar.setAttribute("aria-label", "Remover jogador");
tirar.addEventListener("click", function () { linha.remove(); });
linha.appendChild(nome);
linha.appendChild(camisa);
linha.appendChild(tirar);
listaJogadores.appendChild(linha);
return linha;
}
function linhaDeComissao(m) {
var linha = el("div", "linha-ficha linha-comissao");
if (m && m.id) linha.setAttribute("data-id", m.id);
var nome = el("input");
nome.placeholder = "Nome";
nome.maxLength = 120;
nome.value = (m && m.nome) || "";
nome.setAttribute("aria-label", "Nome na comissão técnica");
nome.setAttribute("data-campo", "nome");
var papel = el("input");
papel.placeholder = "Função";
papel.maxLength = 40;
papel.value = (m && m.funcao) || "";
papel.setAttribute("list", "funcoes-comissao");
papel.setAttribute("aria-label", "Função na comissão técnica");
papel.setAttribute("data-campo", "funcao");
var tirar = el("button", "secundario", "×");
tirar.type = "button";
tirar.setAttribute("aria-label", "Remover da comissão");
tirar.addEventListener("click", function () { linha.remove(); });
linha.appendChild(nome);
linha.appendChild(papel);
linha.appendChild(tirar);
listaComissao.appendChild(linha);
return linha;
}
/** Time da escola ou ficha fechada: só leitura (listas simples). */
function fichaSomenteLeitura(ficha) {
formFicha.hidden = true;
var alvo = document.getElementById("ficha-leitura");
if (!alvo) {
alvo = el("div", "somente-leitura");
alvo.id = "ficha-leitura";
formFicha.parentNode.appendChild(alvo);
}
limpar(alvo);
alvo.appendChild(el("h3", null, "Jogadores"));
var ul = el("ul");
(ficha.jogadores || []).forEach(function (j) {
ul.appendChild(el("li", null, pessoa({ nome: j.nome, numeroCamisa: j.numeroCamisa })));
});
if (!ul.childNodes.length) ul.appendChild(el("li", null, "Nenhum jogador na ficha."));
alvo.appendChild(ul);
if ((ficha.comissao || []).length) {
alvo.appendChild(el("h3", null, "Comissão técnica"));
var uc = el("ul");
ficha.comissao.forEach(function (m) { uc.appendChild(el("li", null, m.nome + " — " + m.funcao)); });
alvo.appendChild(uc);
}
}
function montarFicha(ficha) {
var aviso = document.getElementById("ficha-aviso");
limpar(listaJogadores);
limpar(listaComissao);
if (!ficha.editavel) {
aviso.textContent = "Esta ficha está fechada para alterações aqui. Se algo estiver errado, fale com a organização.";
fichaSomenteLeitura(ficha);
return;
}
aviso.textContent = "Confira os jogadores (nome e número da camisa) e a comissão técnica. Dá para corrigir até o primeiro jogo ser encerrado.";
formFicha.hidden = false;
(ficha.jogadores || []).forEach(linhaDeJogador);
if (!(ficha.jogadores || []).length) for (var i = 0; i < 3; i++) linhaDeJogador();
(ficha.comissao || []).forEach(linhaDeComissao);
}
function mostrarErroFicha(texto) {
fichaErro.textContent = texto || "";
fichaErro.hidden = !texto;
}
function mostrarOkFicha(texto) {
fichaOk.textContent = texto || "";
fichaOk.hidden = !texto;
}
function corpoDaFicha() {
var jogadores = [];
var linhas = listaJogadores.querySelectorAll(".linha-jogador");
for (var i = 0; i < linhas.length; i++) {
var nome = linhas[i].querySelector('[data-campo="nome"]').value.trim();
if (!nome) continue;
var c = linhas[i].querySelector('[data-campo="camisa"]').value;
var j = { nome: nome, numeroCamisa: c === "" ? null : parseInt(c, 10) };
if (linhas[i].getAttribute("data-id")) j.id = linhas[i].getAttribute("data-id");
jogadores.push(j);
}
var comissao = [];
var faltaFuncao = false;
var pessoas = listaComissao.querySelectorAll(".linha-comissao");
for (var k = 0; k < pessoas.length; k++) {
var n = pessoas[k].querySelector('[data-campo="nome"]').value.trim();
if (!n) continue;
var f = pessoas[k].querySelector('[data-campo="funcao"]').value.trim();
if (!f) faltaFuncao = true;
var m = { nome: n, funcao: f };
if (pessoas[k].getAttribute("data-id")) m.id = pessoas[k].getAttribute("data-id");
comissao.push(m);
}
return { jogadores: jogadores, comissao: comissao, faltaFuncao: faltaFuncao };
}
function aoSalvar(ev) {
ev.preventDefault();
mostrarErroFicha("");
mostrarOkFicha("");
var c = corpoDaFicha();
if (c.jogadores.length > MAX_JOGADORES) { mostrarErroFicha("No máximo " + MAX_JOGADORES + " jogadores."); return; }
if (c.faltaFuncao) { mostrarErroFicha("Informe a função de cada pessoa da comissão técnica."); return; }
if (c.comissao.length > MAX_COMISSAO) { mostrarErroFicha("No máximo " + MAX_COMISSAO + " pessoas na comissão técnica."); return; }
salvar.disabled = true;
salvar.setAttribute("aria-busy", "true");
fetch(base + "/ficha", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ jogadores: c.jogadores, comissao: c.comissao })
}).then(lerResposta).then(function (r) {
salvar.disabled = false;
salvar.removeAttribute("aria-busy");
if (r.status >= 200 && r.status < 300) {
mostrarOkFicha("Ficha salva.");
return carregar(true);
}
if (r.status === 404) { recusar("Este link não está mais disponível."); return null; }
mostrarErroFicha((r.corpo && r.corpo.detail) || "Não foi possível salvar agora. Tente de novo em instantes.");
return null;
}).catch(function () {
salvar.disabled = false;
salvar.removeAttribute("aria-busy");
mostrarErroFicha("Sem conexão. Confira a internet e tente de novo.");
});
}
// ------------------------------------------------------------------ carga
function mostrar(v, manterAba) {
visao = v;
var nomeTime = (v.time && v.time.nome) || "Seu time";
titulo.textContent = (v.competicao || "Competição") + " — " + nomeTime;
document.title = titulo.textContent + " — Athletic Map";
var partes = [];
if (v.time && v.time.escola) partes.push(v.time.escola);
if (v.time && v.time.cidade) partes.push(v.time.cidade);
if (v.temporada && v.temporada.nome) partes.push("Temporada " + v.temporada.nome);
if (v.time && v.time.status === "DESQUALIFICADO") partes.push("Time desqualificado");
situacao.textContent = partes.join(" · ");
situacao.className = "";
painel.hidden = false;
montarTabela(v.classificacao, nomeTime);
montarJogos(v.jogos);
montarSumulas(v.sumulas, v.jogos);
montarFicha(v.ficha || { jogadores: [], comissao: [], editavel: false });
if (!manterAba) abrirAba("tabela", false);
}
function carregar(manterAba) {
return fetch(base).then(lerResposta).then(function (r) {
if (r.status === 404) { recusar("Este link não está disponível ou foi desativado. Peça um novo à organização."); return; }
if (r.status === 429) { recusar("Muitas consultas seguidas. Aguarde alguns minutos e abra o link de novo."); return; }
if (r.status !== 200) { recusar("Não foi possível abrir agora. Tente de novo em instantes."); return; }
mostrar(r.corpo || {}, manterAba);
}).catch(function () {
recusar("Não foi possível abrir agora. Tente de novo em instantes.");
});
}
function iniciar() {
if (!TOKEN.test(token)) { recusar("Este endereço não é válido."); return; }
ABAS.forEach(function (nome) {
var aba = document.getElementById("aba-" + nome);
aba.addEventListener("click", function () { abrirAba(nome, false); });
aba.addEventListener("keydown", aoTeclarNaAba);
});
document.getElementById("maisJogador").addEventListener("click", function () {
if (listaJogadores.querySelectorAll(".linha-jogador").length < MAX_JOGADORES) linhaDeJogador().querySelector("input").focus();
});
document.getElementById("maisComissao").addEventListener("click", function () {
if (listaComissao.querySelectorAll(".linha-comissao").length < MAX_COMISSAO) linhaDeComissao(null).querySelector("input").focus();
});
formFicha.addEventListener("submit", aoSalvar);
carregar(false);
}
iniciar();
})();
---
apiVersion: v1
kind: ConfigMap
+3
View File
@@ -153,6 +153,9 @@ spec:
- { key: "preparando.js", path: "preparando.js" }
- { key: "privacidade.html", path: "privacidade.html" }
- { key: "termos.html", path: "termos.html" }
- { key: "time.css", path: "time.css" }
- { key: "time.html", path: "time.html" }
- { key: "time.js", path: "time.js" }
- configMap:
name: frontend-publico-binario
items:
+619 -3
View File
@@ -409,7 +409,7 @@ data:
<form id="formulario" hidden novalidate>
<p class="explica">
Preencha os dados do seu time e dos jogadores (só o nome e o número da camisa).
Preencha os dados do seu time, dos jogadores (só o nome e o número da camisa) e da comissão técnica (nome e função).
A organização confere e aprova antes do time entrar na competição.
</p>
@@ -433,12 +433,23 @@ data:
<button type="button" id="maisJogador" class="secundario">+ Jogador</button>
</fieldset>
<fieldset>
<legend>Comissão técnica</legend>
<div id="comissao"></div>
<datalist id="funcoes-comissao">
<option value="Técnico"></option><option value="Auxiliar técnico"></option><option value="Preparador físico"></option>
<option value="Preparador de goleiros"></option><option value="Médico"></option><option value="Fisioterapeuta"></option>
<option value="Massagista"></option><option value="Roupeiro"></option><option value="Dirigente"></option>
</datalist>
<button type="button" id="maisComissao" class="secundario">+ Pessoa na comissão</button>
</fieldset>
<p id="erro" class="erro" role="alert" hidden></p>
<button type="submit" id="enviar">Enviar time</button>
</form>
</main>
<script src="/publico/desafio.js?v=1"></script>
<script src="/publico/convite.js?v=2"></script>
<script src="/publico/convite.js?v=3"></script>
</body>
</html>
@@ -465,6 +476,7 @@ data:
var TOKEN = /^[A-Za-z0-9_-]{20,64}$/;
var MAX_JOGADORES = 60;
var MAX_COMISSAO = 15;
var token = decodeURIComponent((location.pathname.split("/").filter(Boolean).pop() || ""));
var base = "/api/public/competicoes/convites/" + encodeURIComponent(token);
@@ -473,6 +485,7 @@ data:
var situacao = document.getElementById("situacao");
var formulario = document.getElementById("formulario");
var listaJogadores = document.getElementById("jogadores");
var listaComissao = document.getElementById("comissao");
var erro = document.getElementById("erro");
var enviar = document.getElementById("enviar");
var ROTULO_ENVIAR = enviar.textContent;
@@ -520,6 +533,39 @@ data:
return linha;
}
/** Comissao tecnica (21/10/2026): nome e funcao; a funcao tem sugestoes, mas aceita texto livre. */
function linhaDeComissao(funcao) {
var linha = document.createElement("div");
linha.className = "linha-jogador linha-comissao";
var nome = document.createElement("input");
nome.placeholder = "Nome";
nome.maxLength = 120;
nome.setAttribute("aria-label", "Nome na comissão técnica");
nome.setAttribute("data-campo", "nome");
var papel = document.createElement("input");
papel.placeholder = "Função";
papel.maxLength = 40;
papel.value = funcao || "";
papel.setAttribute("list", "funcoes-comissao");
papel.setAttribute("aria-label", "Função na comissão técnica");
papel.setAttribute("data-campo", "funcao");
var tirar = document.createElement("button");
tirar.type = "button";
tirar.className = "secundario";
tirar.textContent = "×";
tirar.setAttribute("aria-label", "Remover da comissão");
tirar.addEventListener("click", function () { linha.remove(); });
linha.appendChild(nome);
linha.appendChild(papel);
linha.appendChild(tirar);
listaComissao.appendChild(linha);
return linha;
}
function valor(id) {
var v = document.getElementById(id).value.trim();
return v === "" ? null : v;
@@ -534,14 +580,26 @@ data:
var c = linhas[i].querySelector('[data-campo="camisa"]').value;
jogadores.push({ nome: nome, numeroCamisa: c === "" ? null : parseInt(c, 10) });
}
var comissao = [];
var faltaFuncao = false;
var pessoas = listaComissao.querySelectorAll(".linha-comissao");
for (var k = 0; k < pessoas.length; k++) {
var n = pessoas[k].querySelector('[data-campo="nome"]').value.trim();
if (!n) continue;
var f = pessoas[k].querySelector('[data-campo="funcao"]').value.trim();
if (!f) faltaFuncao = true;
comissao.push({ nome: n, funcao: f });
}
return {
faltaFuncao: faltaFuncao,
nome: valor("nome"),
escola: valor("escola"),
cidade: valor("cidade"),
contatoNome: valor("contatoNome"),
contatoTelefone: valor("contatoTelefone"),
contatoEmail: valor("contatoEmail"),
jogadores: jogadores
jogadores: jogadores,
comissao: comissao
};
}
@@ -555,6 +613,9 @@ data:
var corpo = montarCorpo();
if (!corpo.nome) { mostrarErro("Informe o nome do time."); return; }
if (corpo.jogadores.length > MAX_JOGADORES) { mostrarErro("No máximo " + MAX_JOGADORES + " jogadores."); return; }
if (corpo.faltaFuncao) { mostrarErro("Informe a função de cada pessoa da comissão técnica."); return; }
if (corpo.comissao.length > MAX_COMISSAO) { mostrarErro("No máximo " + MAX_COMISSAO + " pessoas na comissão técnica."); return; }
delete corpo.faltaFuncao;
enviar.disabled = true;
enviar.textContent = "Verificando…";
enviar.setAttribute("aria-busy", "true");
@@ -615,6 +676,7 @@ data:
situacao.className = "";
formulario.hidden = false;
for (var i = 0; i < 5; i++) linhaDeJogador();
linhaDeComissao("Técnico");
}).catch(function () {
recusar("Não foi possível abrir o convite agora. Tente de novo em instantes.");
});
@@ -623,6 +685,9 @@ data:
document.getElementById("maisJogador").addEventListener("click", function () {
if (listaJogadores.querySelectorAll(".linha-jogador").length < MAX_JOGADORES) linhaDeJogador().querySelector("input").focus();
});
document.getElementById("maisComissao").addEventListener("click", function () {
if (listaComissao.querySelectorAll(".linha-comissao").length < MAX_COMISSAO) linhaDeComissao("").querySelector("input").focus();
});
formulario.addEventListener("submit", aoEnviar);
iniciar();
})();
@@ -8242,6 +8307,557 @@ data:
</body>
</html>
time.css: |
/* Link do time: página pública, sem fonte nem script de fora (a CSP só deixa 'self'). Mobile primeiro. */
main { max-width: 56rem; }
.explica, .legenda { color: var(--muted); font-size: .9rem; }
.abas { display: flex; gap: .25rem; overflow-x: auto; border-bottom: 2px solid var(--line); margin: 0 0 1rem; }
.abas button { background: transparent; color: var(--navy); border: 0; border-bottom: 3px solid transparent; border-radius: 0; padding: .6rem .9rem; white-space: nowrap; font: inherit; font-weight: 700; cursor: pointer; }
.abas button[aria-selected="true"] { border-bottom-color: var(--orange); }
.abas button:focus-visible, button:focus-visible, input:focus-visible { outline: 3px solid var(--orange); outline-offset: 1px; }
h2 { font-size: 1.15rem; margin: .5rem 0; }
.rolagem { overflow-x: auto; }
table { border-collapse: collapse; width: 100%; font-size: .9rem; background: #fff; }
th, td { padding: .4rem .45rem; text-align: center; border-bottom: 1px solid var(--line); white-space: nowrap; }
th:nth-child(2), td:nth-child(2) { text-align: left; white-space: normal; min-width: 8rem; }
tr.meu { background: #fff4e5; font-weight: 700; }
.forma { display: inline-flex; gap: 3px; vertical-align: middle; }
.ponto { width: .8rem; height: .8rem; border-radius: 50%; display: inline-block; }
.ponto.V { background: #067647; }
.ponto.E { background: #98a2b3; }
.ponto.D { background: #b42318; }
.cartao { display: inline-block; width: .7rem; height: .95rem; border-radius: 2px; vertical-align: middle; margin-right: 3px; }
.cartao.amarelo { background: #f5c518; }
.cartao.vermelho { background: #d92d20; }
.jogo, .sumula { background: #fff; border: 1px solid var(--line); border-radius: 8px; padding: .6rem .8rem; margin: 0 0 .6rem; }
.jogo .quando { color: var(--muted); font-size: .85rem; }
.jogo .confronto { display: grid; grid-template-columns: 1fr auto 1fr; gap: .5rem; align-items: center; font-weight: 700; }
.jogo .confronto span:first-child { text-align: right; }
.jogo .placar { background: var(--navy); color: #fff; border-radius: 6px; padding: .1rem .6rem; }
.etiqueta { display: inline-block; font-size: .75rem; font-weight: 700; padding: .05rem .5rem; border-radius: 99px; background: #fff1e3; border: 1px solid var(--orange); margin-left: .4rem; }
.sumula h3 { margin: 0 0 .3rem; font-size: 1rem; }
.sumula ul { margin: .2rem 0 .5rem; padding-left: 1.2rem; }
form { display: grid; gap: 1rem; }
fieldset { border: 1px solid var(--line); border-radius: 8px; background: #fff; padding: .75rem 1rem 1rem; display: grid; gap: .6rem; }
legend { font-weight: 700; padding: 0 .4rem; }
input { font: inherit; padding: .5rem .6rem; border: 1px solid var(--line); border-radius: 6px; width: 100%; }
.linha-ficha { display: grid; grid-template-columns: 1fr 5.5rem auto; gap: .4rem; align-items: center; }
.linha-ficha button { padding: .4rem .6rem; }
button { font: inherit; font-weight: 700; padding: .65rem 1.1rem; border: 0; border-radius: 8px; background: var(--orange); color: #fff; cursor: pointer; }
button.secundario { background: #fff; color: var(--navy); border: 1px solid var(--line); justify-self: start; }
button:disabled { opacity: .55; cursor: not-allowed; }
.erro { color: #b42318; margin: 0; font-weight: 600; }
.ok { background: #e7f6ec; border-left: 4px solid #067647; padding: .8rem 1rem; margin: 0; }
#situacao.invalido { background: #fff1e3; border-left: 4px solid var(--orange); padding: 1rem 1.2rem; }
.somente-leitura li { margin: .15rem 0; }
@media (max-width: 30rem) { .linha-ficha { grid-template-columns: 1fr 4.5rem auto; } }
time.html: |
<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow">
<meta name="referrer" content="no-referrer">
<title>Seu time na competição — Athletic Map</title>
<link rel="stylesheet" href="/publico/_estilo.css">
<link rel="stylesheet" href="/publico/time.css?v=1">
</head>
<body>
<header><div class="marca">Athletic <span>Map</span></div></header>
<main>
<h1 id="titulo">Seu time na competição</h1>
<p id="situacao" role="status" aria-live="polite">Abrindo o link…</p>
<div id="painel" hidden>
<div class="abas" role="tablist" aria-label="Seções">
<button type="button" role="tab" id="aba-tabela" aria-controls="sec-tabela" aria-selected="true" data-aba="tabela">Tabela</button>
<button type="button" role="tab" id="aba-jogos" aria-controls="sec-jogos" aria-selected="false" tabindex="-1" data-aba="jogos">Jogos</button>
<button type="button" role="tab" id="aba-sumulas" aria-controls="sec-sumulas" aria-selected="false" tabindex="-1" data-aba="sumulas">Súmulas</button>
<button type="button" role="tab" id="aba-ficha" aria-controls="sec-ficha" aria-selected="false" tabindex="-1" data-aba="ficha">Ficha do time</button>
</div>
<section id="sec-tabela" role="tabpanel" aria-labelledby="aba-tabela">
<h2>Tabela</h2>
<div class="rolagem" id="tabela"></div>
<p class="legenda">P pontos · J jogos · V vitórias · E empates · D derrotas · GP gols pró · GC gols contra · SG saldo de gols · % aproveitamento · CA cartões amarelos · CV cartões vermelhos</p>
</section>
<section id="sec-jogos" role="tabpanel" aria-labelledby="aba-jogos" hidden>
<h2>Jogos</h2>
<div id="jogos"></div>
</section>
<section id="sec-sumulas" role="tabpanel" aria-labelledby="aba-sumulas" hidden>
<h2>Súmulas</h2>
<div id="sumulas"></div>
</section>
<section id="sec-ficha" role="tabpanel" aria-labelledby="aba-ficha" hidden>
<h2>Ficha do time</h2>
<p id="ficha-aviso" class="explica"></p>
<form id="ficha" novalidate>
<fieldset>
<legend>Jogadores</legend>
<div id="jogadores"></div>
<button type="button" id="maisJogador" class="secundario">+ Jogador</button>
</fieldset>
<fieldset>
<legend>Comissão técnica</legend>
<div id="comissao"></div>
<datalist id="funcoes-comissao">
<option value="Técnico"></option><option value="Auxiliar técnico"></option><option value="Preparador físico"></option>
<option value="Preparador de goleiros"></option><option value="Médico"></option><option value="Fisioterapeuta"></option>
<option value="Massagista"></option><option value="Roupeiro"></option><option value="Dirigente"></option>
</datalist>
<button type="button" id="maisComissao" class="secundario">+ Pessoa na comissão</button>
</fieldset>
<p id="ficha-erro" class="erro" role="alert" hidden></p>
<p id="ficha-ok" class="ok" role="status" hidden></p>
<button type="submit" id="salvar">Salvar ficha</button>
</form>
</section>
</div>
</main>
<script src="/publico/time.js?v=1"></script>
</body>
</html>
time.js: |
/* ===========================================================================
* Link do time — a página que UMA pessoa do time abre pelo link com chave.
*
* Sem login: a chave no endereço é o único segredo. A página mostra a tabela,
* os jogos e as súmulas da competição e a ficha do time; se o time for de
* fora e ainda não houver jogo encerrado, a ficha pode ser corrigida aqui.
*
* Tudo o que vem do servidor entra na página com `textContent` — nunca como
* HTML. O script é externo porque a CSP é `script-src 'self'`.
* =========================================================================== */
(function () {
"use strict";
var TOKEN = /^[A-Za-z0-9_-]{20,64}$/;
var MAX_JOGADORES = 60;
var MAX_COMISSAO = 15;
var ABAS = ["tabela", "jogos", "sumulas", "ficha"];
var ROTULOS_FORMA = { V: "Vitória", E: "Empate", D: "Derrota" };
var ROTULOS_STATUS = {
AGENDADA: "Agendado", ADIADA: "Adiado", EM_ANDAMENTO: "Em andamento", ENCERRADA: "Encerrado", CANCELADA: "Cancelado"
};
var token = decodeURIComponent(location.pathname.split("/").filter(Boolean).pop() || "");
var base = "/api/public/competicoes/times/" + encodeURIComponent(token);
var titulo = document.getElementById("titulo");
var situacao = document.getElementById("situacao");
var painel = document.getElementById("painel");
var listaJogadores = document.getElementById("jogadores");
var listaComissao = document.getElementById("comissao");
var formFicha = document.getElementById("ficha");
var fichaErro = document.getElementById("ficha-erro");
var fichaOk = document.getElementById("ficha-ok");
var salvar = document.getElementById("salvar");
var visao = null;
function el(tag, classe, texto) {
var e = document.createElement(tag);
if (classe) e.className = classe;
if (texto !== undefined && texto !== null) e.textContent = String(texto);
return e;
}
function limpar(no) {
while (no.firstChild) no.removeChild(no.firstChild);
}
function recusar(texto) {
situacao.textContent = texto;
situacao.className = "invalido";
painel.hidden = true;
}
function lerResposta(r) {
return r.json().catch(function () { return {}; }).then(function (corpo) { return { status: r.status, corpo: corpo }; });
}
// ------------------------------------------------------------------ abas
function abrirAba(nome, focar) {
for (var i = 0; i < ABAS.length; i++) {
var ativa = ABAS[i] === nome;
var aba = document.getElementById("aba-" + ABAS[i]);
aba.setAttribute("aria-selected", ativa ? "true" : "false");
aba.tabIndex = ativa ? 0 : -1;
document.getElementById("sec-" + ABAS[i]).hidden = !ativa;
if (ativa && focar) aba.focus();
}
}
function aoTeclarNaAba(ev) {
var i = ABAS.indexOf(ev.currentTarget.getAttribute("data-aba"));
if (ev.key === "ArrowRight") { abrirAba(ABAS[(i + 1) % ABAS.length], true); ev.preventDefault(); }
else if (ev.key === "ArrowLeft") { abrirAba(ABAS[(i + ABAS.length - 1) % ABAS.length], true); ev.preventDefault(); }
}
// ------------------------------------------------------------------ tabela
function dataBr(iso) {
if (!iso) return "Data a definir";
var d = new Date(iso);
if (isNaN(d.getTime())) return "Data a definir";
try {
return d.toLocaleString("pt-BR", { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit" });
} catch (e) {
return d.toISOString();
}
}
function percentual(n) {
return (Math.round((n || 0) * 10) / 10).toString().replace(".", ",") + "%";
}
function montarTabela(linhas, meuNome) {
var alvo = document.getElementById("tabela");
limpar(alvo);
if (!linhas || !linhas.length) { alvo.appendChild(el("p", "explica", "A tabela aparece quando houver times e jogos.")); return; }
var t = el("table");
t.appendChild(el("caption", "explica", "Classificação"));
var cab = el("tr");
var cols = [["#", "Posição"], ["Time", "Time"], ["P", "Pontos"], ["J", "Jogos"], ["V", "Vitórias"], ["E", "Empates"],
["D", "Derrotas"], ["GP", "Gols pró"], ["GC", "Gols contra"], ["SG", "Saldo de gols"], ["%", "Aproveitamento"],
["Últimos 5", "Últimos 5 jogos"], ["CA", "Cartões amarelos"], ["CV", "Cartões vermelhos"]];
cols.forEach(function (c) {
var th = el("th", null, c[0]);
th.scope = "col";
th.title = c[1];
cab.appendChild(th);
});
var thead = el("thead");
thead.appendChild(cab);
t.appendChild(thead);
var corpo = el("tbody");
linhas.forEach(function (l) {
var tr = el("tr", l.time === meuNome ? "meu" : "");
[l.posicao, l.time, l.pontos, l.jogos, l.vitorias, l.empates, l.derrotas, l.golsPro, l.golsContra, l.saldoGols,
percentual(l.aproveitamento)].forEach(function (v) { tr.appendChild(el("td", null, v)); });
var forma = el("td");
var caixa = el("span", "forma");
(l.ultimos || []).forEach(function (r) {
var p = el("span", "ponto " + r);
p.title = ROTULOS_FORMA[r] || r;
p.setAttribute("role", "img");
p.setAttribute("aria-label", ROTULOS_FORMA[r] || r);
caixa.appendChild(p);
});
forma.appendChild(caixa);
tr.appendChild(forma);
var am = el("td");
am.appendChild(el("span", "cartao amarelo"));
am.appendChild(document.createTextNode(String(l.amarelos || 0)));
tr.appendChild(am);
var vm = el("td");
vm.appendChild(el("span", "cartao vermelho"));
vm.appendChild(document.createTextNode(String(l.vermelhos || 0)));
tr.appendChild(vm);
corpo.appendChild(tr);
});
t.appendChild(corpo);
alvo.appendChild(t);
}
// ------------------------------------------------------------------ jogos
function montarJogos(jogos) {
var alvo = document.getElementById("jogos");
limpar(alvo);
if (!jogos || !jogos.length) { alvo.appendChild(el("p", "explica", "Ainda não há jogos marcados.")); return; }
jogos.forEach(function (j) {
var card = el("div", "jogo");
var quando = el("div", "quando", (j.rodada ? j.rodada + " · " : "") + dataBr(j.data));
var etiqueta = el("span", "etiqueta", ROTULOS_STATUS[j.status] || j.status);
quando.appendChild(etiqueta);
if (j.wo) quando.appendChild(el("span", "etiqueta", "W.O."));
card.appendChild(quando);
var linha = el("div", "confronto");
linha.appendChild(el("span", null, j.mandante));
var temPlacar = j.placarMandante !== undefined && j.placarMandante !== null;
linha.appendChild(el("span", temPlacar ? "placar" : "", temPlacar ? j.placarMandante + " x " + j.placarVisitante : "x"));
linha.appendChild(el("span", null, j.visitante));
card.appendChild(linha);
if (j.penaltis) card.appendChild(el("div", "quando", "Pênaltis: " + j.penaltis.mandante + " x " + j.penaltis.visitante));
if (j.motivo) card.appendChild(el("div", "quando", "Motivo: " + j.motivo));
alvo.appendChild(card);
});
}
// ------------------------------------------------------------------ súmulas
function pessoa(p) {
if (!p) return "jogador não identificado";
if (p.nome && p.numeroCamisa !== undefined && p.numeroCamisa !== null) return p.nome + " (" + p.numeroCamisa + ")";
if (p.nome) return p.nome;
if (p.numeroCamisa !== undefined && p.numeroCamisa !== null) return "camisa " + p.numeroCamisa;
return "jogador não identificado";
}
function momento(e) {
var m = e.minuto !== undefined && e.minuto !== null ? e.minuto + "'" : "";
var t = e.tempo ? e.tempo + "º tempo" : "";
return [t, m].filter(Boolean).join(" ") || "—";
}
function secaoDaSumula(card, nome, itens, texto) {
if (!itens || !itens.length) return;
card.appendChild(el("strong", null, nome));
var ul = el("ul");
itens.forEach(function (i) { ul.appendChild(el("li", null, texto(i))); });
card.appendChild(ul);
}
function montarSumulas(sumulas, jogos) {
var alvo = document.getElementById("sumulas");
limpar(alvo);
if (!sumulas || !sumulas.length) { alvo.appendChild(el("p", "explica", "As súmulas dos jogos do seu time aparecem aqui.")); return; }
sumulas.forEach(function (s) {
var j = (jogos || []).filter(function (x) { return x.ref === s.jogoRef; })[0];
var card = el("div", "sumula");
var cab = j ? j.mandante + " x " + j.visitante : "Jogo " + s.jogoRef;
if (j && j.placarMandante !== undefined && j.placarMandante !== null) {
cab = j.mandante + " " + j.placarMandante + " x " + j.placarVisitante + " " + j.visitante;
}
card.appendChild(el("h3", null, cab));
if (j) card.appendChild(el("div", "quando", (j.rodada ? j.rodada + " · " : "") + dataBr(j.data)));
secaoDaSumula(card, "Gols", s.gols, function (g) { return momento(g) + " · " + g.time + " · " + pessoa(g.jogador); });
secaoDaSumula(card, "Cartões", s.cartoes, function (c) {
return momento(c) + " · " + (c.tipo === "VERMELHO" ? "vermelho" : "amarelo") + " · " + c.time + " · " + pessoa(c.jogador);
});
secaoDaSumula(card, "Substituições", s.substituicoes, function (t) {
return momento(t) + " · " + t.time + " · sai " + pessoa(t.sai) + ", entra " + pessoa(t.entra);
});
alvo.appendChild(card);
});
}
// ------------------------------------------------------------------ ficha
function linhaDeJogador(j) {
var linha = el("div", "linha-ficha linha-jogador");
if (j && j.id) linha.setAttribute("data-id", j.id);
var nome = el("input");
nome.placeholder = "Nome do jogador";
nome.maxLength = 120;
nome.value = (j && j.nome) || "";
nome.setAttribute("aria-label", "Nome do jogador");
nome.setAttribute("data-campo", "nome");
var camisa = el("input");
camisa.type = "number";
camisa.min = "0";
camisa.max = "999";
camisa.placeholder = "nº";
camisa.value = j && j.numeroCamisa !== undefined && j.numeroCamisa !== null ? j.numeroCamisa : "";
camisa.setAttribute("aria-label", "Número da camisa");
camisa.setAttribute("data-campo", "camisa");
var tirar = el("button", "secundario", "×");
tirar.type = "button";
tirar.setAttribute("aria-label", "Remover jogador");
tirar.addEventListener("click", function () { linha.remove(); });
linha.appendChild(nome);
linha.appendChild(camisa);
linha.appendChild(tirar);
listaJogadores.appendChild(linha);
return linha;
}
function linhaDeComissao(m) {
var linha = el("div", "linha-ficha linha-comissao");
if (m && m.id) linha.setAttribute("data-id", m.id);
var nome = el("input");
nome.placeholder = "Nome";
nome.maxLength = 120;
nome.value = (m && m.nome) || "";
nome.setAttribute("aria-label", "Nome na comissão técnica");
nome.setAttribute("data-campo", "nome");
var papel = el("input");
papel.placeholder = "Função";
papel.maxLength = 40;
papel.value = (m && m.funcao) || "";
papel.setAttribute("list", "funcoes-comissao");
papel.setAttribute("aria-label", "Função na comissão técnica");
papel.setAttribute("data-campo", "funcao");
var tirar = el("button", "secundario", "×");
tirar.type = "button";
tirar.setAttribute("aria-label", "Remover da comissão");
tirar.addEventListener("click", function () { linha.remove(); });
linha.appendChild(nome);
linha.appendChild(papel);
linha.appendChild(tirar);
listaComissao.appendChild(linha);
return linha;
}
/** Time da escola ou ficha fechada: só leitura (listas simples). */
function fichaSomenteLeitura(ficha) {
formFicha.hidden = true;
var alvo = document.getElementById("ficha-leitura");
if (!alvo) {
alvo = el("div", "somente-leitura");
alvo.id = "ficha-leitura";
formFicha.parentNode.appendChild(alvo);
}
limpar(alvo);
alvo.appendChild(el("h3", null, "Jogadores"));
var ul = el("ul");
(ficha.jogadores || []).forEach(function (j) {
ul.appendChild(el("li", null, pessoa({ nome: j.nome, numeroCamisa: j.numeroCamisa })));
});
if (!ul.childNodes.length) ul.appendChild(el("li", null, "Nenhum jogador na ficha."));
alvo.appendChild(ul);
if ((ficha.comissao || []).length) {
alvo.appendChild(el("h3", null, "Comissão técnica"));
var uc = el("ul");
ficha.comissao.forEach(function (m) { uc.appendChild(el("li", null, m.nome + " — " + m.funcao)); });
alvo.appendChild(uc);
}
}
function montarFicha(ficha) {
var aviso = document.getElementById("ficha-aviso");
limpar(listaJogadores);
limpar(listaComissao);
if (!ficha.editavel) {
aviso.textContent = "Esta ficha está fechada para alterações aqui. Se algo estiver errado, fale com a organização.";
fichaSomenteLeitura(ficha);
return;
}
aviso.textContent = "Confira os jogadores (nome e número da camisa) e a comissão técnica. Dá para corrigir até o primeiro jogo ser encerrado.";
formFicha.hidden = false;
(ficha.jogadores || []).forEach(linhaDeJogador);
if (!(ficha.jogadores || []).length) for (var i = 0; i < 3; i++) linhaDeJogador();
(ficha.comissao || []).forEach(linhaDeComissao);
}
function mostrarErroFicha(texto) {
fichaErro.textContent = texto || "";
fichaErro.hidden = !texto;
}
function mostrarOkFicha(texto) {
fichaOk.textContent = texto || "";
fichaOk.hidden = !texto;
}
function corpoDaFicha() {
var jogadores = [];
var linhas = listaJogadores.querySelectorAll(".linha-jogador");
for (var i = 0; i < linhas.length; i++) {
var nome = linhas[i].querySelector('[data-campo="nome"]').value.trim();
if (!nome) continue;
var c = linhas[i].querySelector('[data-campo="camisa"]').value;
var j = { nome: nome, numeroCamisa: c === "" ? null : parseInt(c, 10) };
if (linhas[i].getAttribute("data-id")) j.id = linhas[i].getAttribute("data-id");
jogadores.push(j);
}
var comissao = [];
var faltaFuncao = false;
var pessoas = listaComissao.querySelectorAll(".linha-comissao");
for (var k = 0; k < pessoas.length; k++) {
var n = pessoas[k].querySelector('[data-campo="nome"]').value.trim();
if (!n) continue;
var f = pessoas[k].querySelector('[data-campo="funcao"]').value.trim();
if (!f) faltaFuncao = true;
var m = { nome: n, funcao: f };
if (pessoas[k].getAttribute("data-id")) m.id = pessoas[k].getAttribute("data-id");
comissao.push(m);
}
return { jogadores: jogadores, comissao: comissao, faltaFuncao: faltaFuncao };
}
function aoSalvar(ev) {
ev.preventDefault();
mostrarErroFicha("");
mostrarOkFicha("");
var c = corpoDaFicha();
if (c.jogadores.length > MAX_JOGADORES) { mostrarErroFicha("No máximo " + MAX_JOGADORES + " jogadores."); return; }
if (c.faltaFuncao) { mostrarErroFicha("Informe a função de cada pessoa da comissão técnica."); return; }
if (c.comissao.length > MAX_COMISSAO) { mostrarErroFicha("No máximo " + MAX_COMISSAO + " pessoas na comissão técnica."); return; }
salvar.disabled = true;
salvar.setAttribute("aria-busy", "true");
fetch(base + "/ficha", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ jogadores: c.jogadores, comissao: c.comissao })
}).then(lerResposta).then(function (r) {
salvar.disabled = false;
salvar.removeAttribute("aria-busy");
if (r.status >= 200 && r.status < 300) {
mostrarOkFicha("Ficha salva.");
return carregar(true);
}
if (r.status === 404) { recusar("Este link não está mais disponível."); return null; }
mostrarErroFicha((r.corpo && r.corpo.detail) || "Não foi possível salvar agora. Tente de novo em instantes.");
return null;
}).catch(function () {
salvar.disabled = false;
salvar.removeAttribute("aria-busy");
mostrarErroFicha("Sem conexão. Confira a internet e tente de novo.");
});
}
// ------------------------------------------------------------------ carga
function mostrar(v, manterAba) {
visao = v;
var nomeTime = (v.time && v.time.nome) || "Seu time";
titulo.textContent = (v.competicao || "Competição") + " — " + nomeTime;
document.title = titulo.textContent + " — Athletic Map";
var partes = [];
if (v.time && v.time.escola) partes.push(v.time.escola);
if (v.time && v.time.cidade) partes.push(v.time.cidade);
if (v.temporada && v.temporada.nome) partes.push("Temporada " + v.temporada.nome);
if (v.time && v.time.status === "DESQUALIFICADO") partes.push("Time desqualificado");
situacao.textContent = partes.join(" · ");
situacao.className = "";
painel.hidden = false;
montarTabela(v.classificacao, nomeTime);
montarJogos(v.jogos);
montarSumulas(v.sumulas, v.jogos);
montarFicha(v.ficha || { jogadores: [], comissao: [], editavel: false });
if (!manterAba) abrirAba("tabela", false);
}
function carregar(manterAba) {
return fetch(base).then(lerResposta).then(function (r) {
if (r.status === 404) { recusar("Este link não está disponível ou foi desativado. Peça um novo à organização."); return; }
if (r.status === 429) { recusar("Muitas consultas seguidas. Aguarde alguns minutos e abra o link de novo."); return; }
if (r.status !== 200) { recusar("Não foi possível abrir agora. Tente de novo em instantes."); return; }
mostrar(r.corpo || {}, manterAba);
}).catch(function () {
recusar("Não foi possível abrir agora. Tente de novo em instantes.");
});
}
function iniciar() {
if (!TOKEN.test(token)) { recusar("Este endereço não é válido."); return; }
ABAS.forEach(function (nome) {
var aba = document.getElementById("aba-" + nome);
aba.addEventListener("click", function () { abrirAba(nome, false); });
aba.addEventListener("keydown", aoTeclarNaAba);
});
document.getElementById("maisJogador").addEventListener("click", function () {
if (listaJogadores.querySelectorAll(".linha-jogador").length < MAX_JOGADORES) linhaDeJogador().querySelector("input").focus();
});
document.getElementById("maisComissao").addEventListener("click", function () {
if (listaComissao.querySelectorAll(".linha-comissao").length < MAX_COMISSAO) linhaDeComissao(null).querySelector("input").focus();
});
formFicha.addEventListener("submit", aoSalvar);
carregar(false);
}
iniciar();
})();
---
apiVersion: v1
kind: ConfigMap
@@ -162,6 +162,9 @@ spec:
- { key: "preparando.js", path: "preparando.js" }
- { key: "privacidade.html", path: "privacidade.html" }
- { key: "termos.html", path: "termos.html" }
- { key: "time.css", path: "time.css" }
- { key: "time.html", path: "time.html" }
- { key: "time.js", path: "time.js" }
- configMap:
name: frontend-publico-binario
items:
@@ -409,7 +409,7 @@ data:
<form id="formulario" hidden novalidate>
<p class="explica">
Preencha os dados do seu time e dos jogadores (só o nome e o número da camisa).
Preencha os dados do seu time, dos jogadores (só o nome e o número da camisa) e da comissão técnica (nome e função).
A organização confere e aprova antes do time entrar na competição.
</p>
@@ -433,12 +433,23 @@ data:
<button type="button" id="maisJogador" class="secundario">+ Jogador</button>
</fieldset>
<fieldset>
<legend>Comissão técnica</legend>
<div id="comissao"></div>
<datalist id="funcoes-comissao">
<option value="Técnico"></option><option value="Auxiliar técnico"></option><option value="Preparador físico"></option>
<option value="Preparador de goleiros"></option><option value="Médico"></option><option value="Fisioterapeuta"></option>
<option value="Massagista"></option><option value="Roupeiro"></option><option value="Dirigente"></option>
</datalist>
<button type="button" id="maisComissao" class="secundario">+ Pessoa na comissão</button>
</fieldset>
<p id="erro" class="erro" role="alert" hidden></p>
<button type="submit" id="enviar">Enviar time</button>
</form>
</main>
<script src="/publico/desafio.js?v=1"></script>
<script src="/publico/convite.js?v=2"></script>
<script src="/publico/convite.js?v=3"></script>
</body>
</html>
@@ -465,6 +476,7 @@ data:
var TOKEN = /^[A-Za-z0-9_-]{20,64}$/;
var MAX_JOGADORES = 60;
var MAX_COMISSAO = 15;
var token = decodeURIComponent((location.pathname.split("/").filter(Boolean).pop() || ""));
var base = "/api/public/competicoes/convites/" + encodeURIComponent(token);
@@ -473,6 +485,7 @@ data:
var situacao = document.getElementById("situacao");
var formulario = document.getElementById("formulario");
var listaJogadores = document.getElementById("jogadores");
var listaComissao = document.getElementById("comissao");
var erro = document.getElementById("erro");
var enviar = document.getElementById("enviar");
var ROTULO_ENVIAR = enviar.textContent;
@@ -520,6 +533,39 @@ data:
return linha;
}
/** Comissao tecnica (21/10/2026): nome e funcao; a funcao tem sugestoes, mas aceita texto livre. */
function linhaDeComissao(funcao) {
var linha = document.createElement("div");
linha.className = "linha-jogador linha-comissao";
var nome = document.createElement("input");
nome.placeholder = "Nome";
nome.maxLength = 120;
nome.setAttribute("aria-label", "Nome na comissão técnica");
nome.setAttribute("data-campo", "nome");
var papel = document.createElement("input");
papel.placeholder = "Função";
papel.maxLength = 40;
papel.value = funcao || "";
papel.setAttribute("list", "funcoes-comissao");
papel.setAttribute("aria-label", "Função na comissão técnica");
papel.setAttribute("data-campo", "funcao");
var tirar = document.createElement("button");
tirar.type = "button";
tirar.className = "secundario";
tirar.textContent = "×";
tirar.setAttribute("aria-label", "Remover da comissão");
tirar.addEventListener("click", function () { linha.remove(); });
linha.appendChild(nome);
linha.appendChild(papel);
linha.appendChild(tirar);
listaComissao.appendChild(linha);
return linha;
}
function valor(id) {
var v = document.getElementById(id).value.trim();
return v === "" ? null : v;
@@ -534,14 +580,26 @@ data:
var c = linhas[i].querySelector('[data-campo="camisa"]').value;
jogadores.push({ nome: nome, numeroCamisa: c === "" ? null : parseInt(c, 10) });
}
var comissao = [];
var faltaFuncao = false;
var pessoas = listaComissao.querySelectorAll(".linha-comissao");
for (var k = 0; k < pessoas.length; k++) {
var n = pessoas[k].querySelector('[data-campo="nome"]').value.trim();
if (!n) continue;
var f = pessoas[k].querySelector('[data-campo="funcao"]').value.trim();
if (!f) faltaFuncao = true;
comissao.push({ nome: n, funcao: f });
}
return {
faltaFuncao: faltaFuncao,
nome: valor("nome"),
escola: valor("escola"),
cidade: valor("cidade"),
contatoNome: valor("contatoNome"),
contatoTelefone: valor("contatoTelefone"),
contatoEmail: valor("contatoEmail"),
jogadores: jogadores
jogadores: jogadores,
comissao: comissao
};
}
@@ -555,6 +613,9 @@ data:
var corpo = montarCorpo();
if (!corpo.nome) { mostrarErro("Informe o nome do time."); return; }
if (corpo.jogadores.length > MAX_JOGADORES) { mostrarErro("No máximo " + MAX_JOGADORES + " jogadores."); return; }
if (corpo.faltaFuncao) { mostrarErro("Informe a função de cada pessoa da comissão técnica."); return; }
if (corpo.comissao.length > MAX_COMISSAO) { mostrarErro("No máximo " + MAX_COMISSAO + " pessoas na comissão técnica."); return; }
delete corpo.faltaFuncao;
enviar.disabled = true;
enviar.textContent = "Verificando…";
enviar.setAttribute("aria-busy", "true");
@@ -615,6 +676,7 @@ data:
situacao.className = "";
formulario.hidden = false;
for (var i = 0; i < 5; i++) linhaDeJogador();
linhaDeComissao("Técnico");
}).catch(function () {
recusar("Não foi possível abrir o convite agora. Tente de novo em instantes.");
});
@@ -623,6 +685,9 @@ data:
document.getElementById("maisJogador").addEventListener("click", function () {
if (listaJogadores.querySelectorAll(".linha-jogador").length < MAX_JOGADORES) linhaDeJogador().querySelector("input").focus();
});
document.getElementById("maisComissao").addEventListener("click", function () {
if (listaComissao.querySelectorAll(".linha-comissao").length < MAX_COMISSAO) linhaDeComissao("").querySelector("input").focus();
});
formulario.addEventListener("submit", aoEnviar);
iniciar();
})();
@@ -8242,6 +8307,557 @@ data:
</body>
</html>
time.css: |
/* Link do time: página pública, sem fonte nem script de fora (a CSP só deixa 'self'). Mobile primeiro. */
main { max-width: 56rem; }
.explica, .legenda { color: var(--muted); font-size: .9rem; }
.abas { display: flex; gap: .25rem; overflow-x: auto; border-bottom: 2px solid var(--line); margin: 0 0 1rem; }
.abas button { background: transparent; color: var(--navy); border: 0; border-bottom: 3px solid transparent; border-radius: 0; padding: .6rem .9rem; white-space: nowrap; font: inherit; font-weight: 700; cursor: pointer; }
.abas button[aria-selected="true"] { border-bottom-color: var(--orange); }
.abas button:focus-visible, button:focus-visible, input:focus-visible { outline: 3px solid var(--orange); outline-offset: 1px; }
h2 { font-size: 1.15rem; margin: .5rem 0; }
.rolagem { overflow-x: auto; }
table { border-collapse: collapse; width: 100%; font-size: .9rem; background: #fff; }
th, td { padding: .4rem .45rem; text-align: center; border-bottom: 1px solid var(--line); white-space: nowrap; }
th:nth-child(2), td:nth-child(2) { text-align: left; white-space: normal; min-width: 8rem; }
tr.meu { background: #fff4e5; font-weight: 700; }
.forma { display: inline-flex; gap: 3px; vertical-align: middle; }
.ponto { width: .8rem; height: .8rem; border-radius: 50%; display: inline-block; }
.ponto.V { background: #067647; }
.ponto.E { background: #98a2b3; }
.ponto.D { background: #b42318; }
.cartao { display: inline-block; width: .7rem; height: .95rem; border-radius: 2px; vertical-align: middle; margin-right: 3px; }
.cartao.amarelo { background: #f5c518; }
.cartao.vermelho { background: #d92d20; }
.jogo, .sumula { background: #fff; border: 1px solid var(--line); border-radius: 8px; padding: .6rem .8rem; margin: 0 0 .6rem; }
.jogo .quando { color: var(--muted); font-size: .85rem; }
.jogo .confronto { display: grid; grid-template-columns: 1fr auto 1fr; gap: .5rem; align-items: center; font-weight: 700; }
.jogo .confronto span:first-child { text-align: right; }
.jogo .placar { background: var(--navy); color: #fff; border-radius: 6px; padding: .1rem .6rem; }
.etiqueta { display: inline-block; font-size: .75rem; font-weight: 700; padding: .05rem .5rem; border-radius: 99px; background: #fff1e3; border: 1px solid var(--orange); margin-left: .4rem; }
.sumula h3 { margin: 0 0 .3rem; font-size: 1rem; }
.sumula ul { margin: .2rem 0 .5rem; padding-left: 1.2rem; }
form { display: grid; gap: 1rem; }
fieldset { border: 1px solid var(--line); border-radius: 8px; background: #fff; padding: .75rem 1rem 1rem; display: grid; gap: .6rem; }
legend { font-weight: 700; padding: 0 .4rem; }
input { font: inherit; padding: .5rem .6rem; border: 1px solid var(--line); border-radius: 6px; width: 100%; }
.linha-ficha { display: grid; grid-template-columns: 1fr 5.5rem auto; gap: .4rem; align-items: center; }
.linha-ficha button { padding: .4rem .6rem; }
button { font: inherit; font-weight: 700; padding: .65rem 1.1rem; border: 0; border-radius: 8px; background: var(--orange); color: #fff; cursor: pointer; }
button.secundario { background: #fff; color: var(--navy); border: 1px solid var(--line); justify-self: start; }
button:disabled { opacity: .55; cursor: not-allowed; }
.erro { color: #b42318; margin: 0; font-weight: 600; }
.ok { background: #e7f6ec; border-left: 4px solid #067647; padding: .8rem 1rem; margin: 0; }
#situacao.invalido { background: #fff1e3; border-left: 4px solid var(--orange); padding: 1rem 1.2rem; }
.somente-leitura li { margin: .15rem 0; }
@media (max-width: 30rem) { .linha-ficha { grid-template-columns: 1fr 4.5rem auto; } }
time.html: |
<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow">
<meta name="referrer" content="no-referrer">
<title>Seu time na competição — Athletic Map</title>
<link rel="stylesheet" href="/publico/_estilo.css">
<link rel="stylesheet" href="/publico/time.css?v=1">
</head>
<body>
<header><div class="marca">Athletic <span>Map</span></div></header>
<main>
<h1 id="titulo">Seu time na competição</h1>
<p id="situacao" role="status" aria-live="polite">Abrindo o link…</p>
<div id="painel" hidden>
<div class="abas" role="tablist" aria-label="Seções">
<button type="button" role="tab" id="aba-tabela" aria-controls="sec-tabela" aria-selected="true" data-aba="tabela">Tabela</button>
<button type="button" role="tab" id="aba-jogos" aria-controls="sec-jogos" aria-selected="false" tabindex="-1" data-aba="jogos">Jogos</button>
<button type="button" role="tab" id="aba-sumulas" aria-controls="sec-sumulas" aria-selected="false" tabindex="-1" data-aba="sumulas">Súmulas</button>
<button type="button" role="tab" id="aba-ficha" aria-controls="sec-ficha" aria-selected="false" tabindex="-1" data-aba="ficha">Ficha do time</button>
</div>
<section id="sec-tabela" role="tabpanel" aria-labelledby="aba-tabela">
<h2>Tabela</h2>
<div class="rolagem" id="tabela"></div>
<p class="legenda">P pontos · J jogos · V vitórias · E empates · D derrotas · GP gols pró · GC gols contra · SG saldo de gols · % aproveitamento · CA cartões amarelos · CV cartões vermelhos</p>
</section>
<section id="sec-jogos" role="tabpanel" aria-labelledby="aba-jogos" hidden>
<h2>Jogos</h2>
<div id="jogos"></div>
</section>
<section id="sec-sumulas" role="tabpanel" aria-labelledby="aba-sumulas" hidden>
<h2>Súmulas</h2>
<div id="sumulas"></div>
</section>
<section id="sec-ficha" role="tabpanel" aria-labelledby="aba-ficha" hidden>
<h2>Ficha do time</h2>
<p id="ficha-aviso" class="explica"></p>
<form id="ficha" novalidate>
<fieldset>
<legend>Jogadores</legend>
<div id="jogadores"></div>
<button type="button" id="maisJogador" class="secundario">+ Jogador</button>
</fieldset>
<fieldset>
<legend>Comissão técnica</legend>
<div id="comissao"></div>
<datalist id="funcoes-comissao">
<option value="Técnico"></option><option value="Auxiliar técnico"></option><option value="Preparador físico"></option>
<option value="Preparador de goleiros"></option><option value="Médico"></option><option value="Fisioterapeuta"></option>
<option value="Massagista"></option><option value="Roupeiro"></option><option value="Dirigente"></option>
</datalist>
<button type="button" id="maisComissao" class="secundario">+ Pessoa na comissão</button>
</fieldset>
<p id="ficha-erro" class="erro" role="alert" hidden></p>
<p id="ficha-ok" class="ok" role="status" hidden></p>
<button type="submit" id="salvar">Salvar ficha</button>
</form>
</section>
</div>
</main>
<script src="/publico/time.js?v=1"></script>
</body>
</html>
time.js: |
/* ===========================================================================
* Link do time — a página que UMA pessoa do time abre pelo link com chave.
*
* Sem login: a chave no endereço é o único segredo. A página mostra a tabela,
* os jogos e as súmulas da competição e a ficha do time; se o time for de
* fora e ainda não houver jogo encerrado, a ficha pode ser corrigida aqui.
*
* Tudo o que vem do servidor entra na página com `textContent` — nunca como
* HTML. O script é externo porque a CSP é `script-src 'self'`.
* =========================================================================== */
(function () {
"use strict";
var TOKEN = /^[A-Za-z0-9_-]{20,64}$/;
var MAX_JOGADORES = 60;
var MAX_COMISSAO = 15;
var ABAS = ["tabela", "jogos", "sumulas", "ficha"];
var ROTULOS_FORMA = { V: "Vitória", E: "Empate", D: "Derrota" };
var ROTULOS_STATUS = {
AGENDADA: "Agendado", ADIADA: "Adiado", EM_ANDAMENTO: "Em andamento", ENCERRADA: "Encerrado", CANCELADA: "Cancelado"
};
var token = decodeURIComponent(location.pathname.split("/").filter(Boolean).pop() || "");
var base = "/api/public/competicoes/times/" + encodeURIComponent(token);
var titulo = document.getElementById("titulo");
var situacao = document.getElementById("situacao");
var painel = document.getElementById("painel");
var listaJogadores = document.getElementById("jogadores");
var listaComissao = document.getElementById("comissao");
var formFicha = document.getElementById("ficha");
var fichaErro = document.getElementById("ficha-erro");
var fichaOk = document.getElementById("ficha-ok");
var salvar = document.getElementById("salvar");
var visao = null;
function el(tag, classe, texto) {
var e = document.createElement(tag);
if (classe) e.className = classe;
if (texto !== undefined && texto !== null) e.textContent = String(texto);
return e;
}
function limpar(no) {
while (no.firstChild) no.removeChild(no.firstChild);
}
function recusar(texto) {
situacao.textContent = texto;
situacao.className = "invalido";
painel.hidden = true;
}
function lerResposta(r) {
return r.json().catch(function () { return {}; }).then(function (corpo) { return { status: r.status, corpo: corpo }; });
}
// ------------------------------------------------------------------ abas
function abrirAba(nome, focar) {
for (var i = 0; i < ABAS.length; i++) {
var ativa = ABAS[i] === nome;
var aba = document.getElementById("aba-" + ABAS[i]);
aba.setAttribute("aria-selected", ativa ? "true" : "false");
aba.tabIndex = ativa ? 0 : -1;
document.getElementById("sec-" + ABAS[i]).hidden = !ativa;
if (ativa && focar) aba.focus();
}
}
function aoTeclarNaAba(ev) {
var i = ABAS.indexOf(ev.currentTarget.getAttribute("data-aba"));
if (ev.key === "ArrowRight") { abrirAba(ABAS[(i + 1) % ABAS.length], true); ev.preventDefault(); }
else if (ev.key === "ArrowLeft") { abrirAba(ABAS[(i + ABAS.length - 1) % ABAS.length], true); ev.preventDefault(); }
}
// ------------------------------------------------------------------ tabela
function dataBr(iso) {
if (!iso) return "Data a definir";
var d = new Date(iso);
if (isNaN(d.getTime())) return "Data a definir";
try {
return d.toLocaleString("pt-BR", { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit" });
} catch (e) {
return d.toISOString();
}
}
function percentual(n) {
return (Math.round((n || 0) * 10) / 10).toString().replace(".", ",") + "%";
}
function montarTabela(linhas, meuNome) {
var alvo = document.getElementById("tabela");
limpar(alvo);
if (!linhas || !linhas.length) { alvo.appendChild(el("p", "explica", "A tabela aparece quando houver times e jogos.")); return; }
var t = el("table");
t.appendChild(el("caption", "explica", "Classificação"));
var cab = el("tr");
var cols = [["#", "Posição"], ["Time", "Time"], ["P", "Pontos"], ["J", "Jogos"], ["V", "Vitórias"], ["E", "Empates"],
["D", "Derrotas"], ["GP", "Gols pró"], ["GC", "Gols contra"], ["SG", "Saldo de gols"], ["%", "Aproveitamento"],
["Últimos 5", "Últimos 5 jogos"], ["CA", "Cartões amarelos"], ["CV", "Cartões vermelhos"]];
cols.forEach(function (c) {
var th = el("th", null, c[0]);
th.scope = "col";
th.title = c[1];
cab.appendChild(th);
});
var thead = el("thead");
thead.appendChild(cab);
t.appendChild(thead);
var corpo = el("tbody");
linhas.forEach(function (l) {
var tr = el("tr", l.time === meuNome ? "meu" : "");
[l.posicao, l.time, l.pontos, l.jogos, l.vitorias, l.empates, l.derrotas, l.golsPro, l.golsContra, l.saldoGols,
percentual(l.aproveitamento)].forEach(function (v) { tr.appendChild(el("td", null, v)); });
var forma = el("td");
var caixa = el("span", "forma");
(l.ultimos || []).forEach(function (r) {
var p = el("span", "ponto " + r);
p.title = ROTULOS_FORMA[r] || r;
p.setAttribute("role", "img");
p.setAttribute("aria-label", ROTULOS_FORMA[r] || r);
caixa.appendChild(p);
});
forma.appendChild(caixa);
tr.appendChild(forma);
var am = el("td");
am.appendChild(el("span", "cartao amarelo"));
am.appendChild(document.createTextNode(String(l.amarelos || 0)));
tr.appendChild(am);
var vm = el("td");
vm.appendChild(el("span", "cartao vermelho"));
vm.appendChild(document.createTextNode(String(l.vermelhos || 0)));
tr.appendChild(vm);
corpo.appendChild(tr);
});
t.appendChild(corpo);
alvo.appendChild(t);
}
// ------------------------------------------------------------------ jogos
function montarJogos(jogos) {
var alvo = document.getElementById("jogos");
limpar(alvo);
if (!jogos || !jogos.length) { alvo.appendChild(el("p", "explica", "Ainda não há jogos marcados.")); return; }
jogos.forEach(function (j) {
var card = el("div", "jogo");
var quando = el("div", "quando", (j.rodada ? j.rodada + " · " : "") + dataBr(j.data));
var etiqueta = el("span", "etiqueta", ROTULOS_STATUS[j.status] || j.status);
quando.appendChild(etiqueta);
if (j.wo) quando.appendChild(el("span", "etiqueta", "W.O."));
card.appendChild(quando);
var linha = el("div", "confronto");
linha.appendChild(el("span", null, j.mandante));
var temPlacar = j.placarMandante !== undefined && j.placarMandante !== null;
linha.appendChild(el("span", temPlacar ? "placar" : "", temPlacar ? j.placarMandante + " x " + j.placarVisitante : "x"));
linha.appendChild(el("span", null, j.visitante));
card.appendChild(linha);
if (j.penaltis) card.appendChild(el("div", "quando", "Pênaltis: " + j.penaltis.mandante + " x " + j.penaltis.visitante));
if (j.motivo) card.appendChild(el("div", "quando", "Motivo: " + j.motivo));
alvo.appendChild(card);
});
}
// ------------------------------------------------------------------ súmulas
function pessoa(p) {
if (!p) return "jogador não identificado";
if (p.nome && p.numeroCamisa !== undefined && p.numeroCamisa !== null) return p.nome + " (" + p.numeroCamisa + ")";
if (p.nome) return p.nome;
if (p.numeroCamisa !== undefined && p.numeroCamisa !== null) return "camisa " + p.numeroCamisa;
return "jogador não identificado";
}
function momento(e) {
var m = e.minuto !== undefined && e.minuto !== null ? e.minuto + "'" : "";
var t = e.tempo ? e.tempo + "º tempo" : "";
return [t, m].filter(Boolean).join(" ") || "—";
}
function secaoDaSumula(card, nome, itens, texto) {
if (!itens || !itens.length) return;
card.appendChild(el("strong", null, nome));
var ul = el("ul");
itens.forEach(function (i) { ul.appendChild(el("li", null, texto(i))); });
card.appendChild(ul);
}
function montarSumulas(sumulas, jogos) {
var alvo = document.getElementById("sumulas");
limpar(alvo);
if (!sumulas || !sumulas.length) { alvo.appendChild(el("p", "explica", "As súmulas dos jogos do seu time aparecem aqui.")); return; }
sumulas.forEach(function (s) {
var j = (jogos || []).filter(function (x) { return x.ref === s.jogoRef; })[0];
var card = el("div", "sumula");
var cab = j ? j.mandante + " x " + j.visitante : "Jogo " + s.jogoRef;
if (j && j.placarMandante !== undefined && j.placarMandante !== null) {
cab = j.mandante + " " + j.placarMandante + " x " + j.placarVisitante + " " + j.visitante;
}
card.appendChild(el("h3", null, cab));
if (j) card.appendChild(el("div", "quando", (j.rodada ? j.rodada + " · " : "") + dataBr(j.data)));
secaoDaSumula(card, "Gols", s.gols, function (g) { return momento(g) + " · " + g.time + " · " + pessoa(g.jogador); });
secaoDaSumula(card, "Cartões", s.cartoes, function (c) {
return momento(c) + " · " + (c.tipo === "VERMELHO" ? "vermelho" : "amarelo") + " · " + c.time + " · " + pessoa(c.jogador);
});
secaoDaSumula(card, "Substituições", s.substituicoes, function (t) {
return momento(t) + " · " + t.time + " · sai " + pessoa(t.sai) + ", entra " + pessoa(t.entra);
});
alvo.appendChild(card);
});
}
// ------------------------------------------------------------------ ficha
function linhaDeJogador(j) {
var linha = el("div", "linha-ficha linha-jogador");
if (j && j.id) linha.setAttribute("data-id", j.id);
var nome = el("input");
nome.placeholder = "Nome do jogador";
nome.maxLength = 120;
nome.value = (j && j.nome) || "";
nome.setAttribute("aria-label", "Nome do jogador");
nome.setAttribute("data-campo", "nome");
var camisa = el("input");
camisa.type = "number";
camisa.min = "0";
camisa.max = "999";
camisa.placeholder = "nº";
camisa.value = j && j.numeroCamisa !== undefined && j.numeroCamisa !== null ? j.numeroCamisa : "";
camisa.setAttribute("aria-label", "Número da camisa");
camisa.setAttribute("data-campo", "camisa");
var tirar = el("button", "secundario", "×");
tirar.type = "button";
tirar.setAttribute("aria-label", "Remover jogador");
tirar.addEventListener("click", function () { linha.remove(); });
linha.appendChild(nome);
linha.appendChild(camisa);
linha.appendChild(tirar);
listaJogadores.appendChild(linha);
return linha;
}
function linhaDeComissao(m) {
var linha = el("div", "linha-ficha linha-comissao");
if (m && m.id) linha.setAttribute("data-id", m.id);
var nome = el("input");
nome.placeholder = "Nome";
nome.maxLength = 120;
nome.value = (m && m.nome) || "";
nome.setAttribute("aria-label", "Nome na comissão técnica");
nome.setAttribute("data-campo", "nome");
var papel = el("input");
papel.placeholder = "Função";
papel.maxLength = 40;
papel.value = (m && m.funcao) || "";
papel.setAttribute("list", "funcoes-comissao");
papel.setAttribute("aria-label", "Função na comissão técnica");
papel.setAttribute("data-campo", "funcao");
var tirar = el("button", "secundario", "×");
tirar.type = "button";
tirar.setAttribute("aria-label", "Remover da comissão");
tirar.addEventListener("click", function () { linha.remove(); });
linha.appendChild(nome);
linha.appendChild(papel);
linha.appendChild(tirar);
listaComissao.appendChild(linha);
return linha;
}
/** Time da escola ou ficha fechada: só leitura (listas simples). */
function fichaSomenteLeitura(ficha) {
formFicha.hidden = true;
var alvo = document.getElementById("ficha-leitura");
if (!alvo) {
alvo = el("div", "somente-leitura");
alvo.id = "ficha-leitura";
formFicha.parentNode.appendChild(alvo);
}
limpar(alvo);
alvo.appendChild(el("h3", null, "Jogadores"));
var ul = el("ul");
(ficha.jogadores || []).forEach(function (j) {
ul.appendChild(el("li", null, pessoa({ nome: j.nome, numeroCamisa: j.numeroCamisa })));
});
if (!ul.childNodes.length) ul.appendChild(el("li", null, "Nenhum jogador na ficha."));
alvo.appendChild(ul);
if ((ficha.comissao || []).length) {
alvo.appendChild(el("h3", null, "Comissão técnica"));
var uc = el("ul");
ficha.comissao.forEach(function (m) { uc.appendChild(el("li", null, m.nome + " — " + m.funcao)); });
alvo.appendChild(uc);
}
}
function montarFicha(ficha) {
var aviso = document.getElementById("ficha-aviso");
limpar(listaJogadores);
limpar(listaComissao);
if (!ficha.editavel) {
aviso.textContent = "Esta ficha está fechada para alterações aqui. Se algo estiver errado, fale com a organização.";
fichaSomenteLeitura(ficha);
return;
}
aviso.textContent = "Confira os jogadores (nome e número da camisa) e a comissão técnica. Dá para corrigir até o primeiro jogo ser encerrado.";
formFicha.hidden = false;
(ficha.jogadores || []).forEach(linhaDeJogador);
if (!(ficha.jogadores || []).length) for (var i = 0; i < 3; i++) linhaDeJogador();
(ficha.comissao || []).forEach(linhaDeComissao);
}
function mostrarErroFicha(texto) {
fichaErro.textContent = texto || "";
fichaErro.hidden = !texto;
}
function mostrarOkFicha(texto) {
fichaOk.textContent = texto || "";
fichaOk.hidden = !texto;
}
function corpoDaFicha() {
var jogadores = [];
var linhas = listaJogadores.querySelectorAll(".linha-jogador");
for (var i = 0; i < linhas.length; i++) {
var nome = linhas[i].querySelector('[data-campo="nome"]').value.trim();
if (!nome) continue;
var c = linhas[i].querySelector('[data-campo="camisa"]').value;
var j = { nome: nome, numeroCamisa: c === "" ? null : parseInt(c, 10) };
if (linhas[i].getAttribute("data-id")) j.id = linhas[i].getAttribute("data-id");
jogadores.push(j);
}
var comissao = [];
var faltaFuncao = false;
var pessoas = listaComissao.querySelectorAll(".linha-comissao");
for (var k = 0; k < pessoas.length; k++) {
var n = pessoas[k].querySelector('[data-campo="nome"]').value.trim();
if (!n) continue;
var f = pessoas[k].querySelector('[data-campo="funcao"]').value.trim();
if (!f) faltaFuncao = true;
var m = { nome: n, funcao: f };
if (pessoas[k].getAttribute("data-id")) m.id = pessoas[k].getAttribute("data-id");
comissao.push(m);
}
return { jogadores: jogadores, comissao: comissao, faltaFuncao: faltaFuncao };
}
function aoSalvar(ev) {
ev.preventDefault();
mostrarErroFicha("");
mostrarOkFicha("");
var c = corpoDaFicha();
if (c.jogadores.length > MAX_JOGADORES) { mostrarErroFicha("No máximo " + MAX_JOGADORES + " jogadores."); return; }
if (c.faltaFuncao) { mostrarErroFicha("Informe a função de cada pessoa da comissão técnica."); return; }
if (c.comissao.length > MAX_COMISSAO) { mostrarErroFicha("No máximo " + MAX_COMISSAO + " pessoas na comissão técnica."); return; }
salvar.disabled = true;
salvar.setAttribute("aria-busy", "true");
fetch(base + "/ficha", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ jogadores: c.jogadores, comissao: c.comissao })
}).then(lerResposta).then(function (r) {
salvar.disabled = false;
salvar.removeAttribute("aria-busy");
if (r.status >= 200 && r.status < 300) {
mostrarOkFicha("Ficha salva.");
return carregar(true);
}
if (r.status === 404) { recusar("Este link não está mais disponível."); return null; }
mostrarErroFicha((r.corpo && r.corpo.detail) || "Não foi possível salvar agora. Tente de novo em instantes.");
return null;
}).catch(function () {
salvar.disabled = false;
salvar.removeAttribute("aria-busy");
mostrarErroFicha("Sem conexão. Confira a internet e tente de novo.");
});
}
// ------------------------------------------------------------------ carga
function mostrar(v, manterAba) {
visao = v;
var nomeTime = (v.time && v.time.nome) || "Seu time";
titulo.textContent = (v.competicao || "Competição") + " — " + nomeTime;
document.title = titulo.textContent + " — Athletic Map";
var partes = [];
if (v.time && v.time.escola) partes.push(v.time.escola);
if (v.time && v.time.cidade) partes.push(v.time.cidade);
if (v.temporada && v.temporada.nome) partes.push("Temporada " + v.temporada.nome);
if (v.time && v.time.status === "DESQUALIFICADO") partes.push("Time desqualificado");
situacao.textContent = partes.join(" · ");
situacao.className = "";
painel.hidden = false;
montarTabela(v.classificacao, nomeTime);
montarJogos(v.jogos);
montarSumulas(v.sumulas, v.jogos);
montarFicha(v.ficha || { jogadores: [], comissao: [], editavel: false });
if (!manterAba) abrirAba("tabela", false);
}
function carregar(manterAba) {
return fetch(base).then(lerResposta).then(function (r) {
if (r.status === 404) { recusar("Este link não está disponível ou foi desativado. Peça um novo à organização."); return; }
if (r.status === 429) { recusar("Muitas consultas seguidas. Aguarde alguns minutos e abra o link de novo."); return; }
if (r.status !== 200) { recusar("Não foi possível abrir agora. Tente de novo em instantes."); return; }
mostrar(r.corpo || {}, manterAba);
}).catch(function () {
recusar("Não foi possível abrir agora. Tente de novo em instantes.");
});
}
function iniciar() {
if (!TOKEN.test(token)) { recusar("Este endereço não é válido."); return; }
ABAS.forEach(function (nome) {
var aba = document.getElementById("aba-" + nome);
aba.addEventListener("click", function () { abrirAba(nome, false); });
aba.addEventListener("keydown", aoTeclarNaAba);
});
document.getElementById("maisJogador").addEventListener("click", function () {
if (listaJogadores.querySelectorAll(".linha-jogador").length < MAX_JOGADORES) linhaDeJogador().querySelector("input").focus();
});
document.getElementById("maisComissao").addEventListener("click", function () {
if (listaComissao.querySelectorAll(".linha-comissao").length < MAX_COMISSAO) linhaDeComissao(null).querySelector("input").focus();
});
formFicha.addEventListener("submit", aoSalvar);
carregar(false);
}
iniciar();
})();
---
apiVersion: v1
kind: ConfigMap
@@ -162,6 +162,9 @@ spec:
- { key: "preparando.js", path: "preparando.js" }
- { key: "privacidade.html", path: "privacidade.html" }
- { key: "termos.html", path: "termos.html" }
- { key: "time.css", path: "time.css" }
- { key: "time.html", path: "time.html" }
- { key: "time.js", path: "time.js" }
- configMap:
name: frontend-publico-binario
items:
@@ -409,7 +409,7 @@ data:
<form id="formulario" hidden novalidate>
<p class="explica">
Preencha os dados do seu time e dos jogadores (só o nome e o número da camisa).
Preencha os dados do seu time, dos jogadores (só o nome e o número da camisa) e da comissão técnica (nome e função).
A organização confere e aprova antes do time entrar na competição.
</p>
@@ -433,12 +433,23 @@ data:
<button type="button" id="maisJogador" class="secundario">+ Jogador</button>
</fieldset>
<fieldset>
<legend>Comissão técnica</legend>
<div id="comissao"></div>
<datalist id="funcoes-comissao">
<option value="Técnico"></option><option value="Auxiliar técnico"></option><option value="Preparador físico"></option>
<option value="Preparador de goleiros"></option><option value="Médico"></option><option value="Fisioterapeuta"></option>
<option value="Massagista"></option><option value="Roupeiro"></option><option value="Dirigente"></option>
</datalist>
<button type="button" id="maisComissao" class="secundario">+ Pessoa na comissão</button>
</fieldset>
<p id="erro" class="erro" role="alert" hidden></p>
<button type="submit" id="enviar">Enviar time</button>
</form>
</main>
<script src="/publico/desafio.js?v=1"></script>
<script src="/publico/convite.js?v=2"></script>
<script src="/publico/convite.js?v=3"></script>
</body>
</html>
@@ -465,6 +476,7 @@ data:
var TOKEN = /^[A-Za-z0-9_-]{20,64}$/;
var MAX_JOGADORES = 60;
var MAX_COMISSAO = 15;
var token = decodeURIComponent((location.pathname.split("/").filter(Boolean).pop() || ""));
var base = "/api/public/competicoes/convites/" + encodeURIComponent(token);
@@ -473,6 +485,7 @@ data:
var situacao = document.getElementById("situacao");
var formulario = document.getElementById("formulario");
var listaJogadores = document.getElementById("jogadores");
var listaComissao = document.getElementById("comissao");
var erro = document.getElementById("erro");
var enviar = document.getElementById("enviar");
var ROTULO_ENVIAR = enviar.textContent;
@@ -520,6 +533,39 @@ data:
return linha;
}
/** Comissao tecnica (21/10/2026): nome e funcao; a funcao tem sugestoes, mas aceita texto livre. */
function linhaDeComissao(funcao) {
var linha = document.createElement("div");
linha.className = "linha-jogador linha-comissao";
var nome = document.createElement("input");
nome.placeholder = "Nome";
nome.maxLength = 120;
nome.setAttribute("aria-label", "Nome na comissão técnica");
nome.setAttribute("data-campo", "nome");
var papel = document.createElement("input");
papel.placeholder = "Função";
papel.maxLength = 40;
papel.value = funcao || "";
papel.setAttribute("list", "funcoes-comissao");
papel.setAttribute("aria-label", "Função na comissão técnica");
papel.setAttribute("data-campo", "funcao");
var tirar = document.createElement("button");
tirar.type = "button";
tirar.className = "secundario";
tirar.textContent = "×";
tirar.setAttribute("aria-label", "Remover da comissão");
tirar.addEventListener("click", function () { linha.remove(); });
linha.appendChild(nome);
linha.appendChild(papel);
linha.appendChild(tirar);
listaComissao.appendChild(linha);
return linha;
}
function valor(id) {
var v = document.getElementById(id).value.trim();
return v === "" ? null : v;
@@ -534,14 +580,26 @@ data:
var c = linhas[i].querySelector('[data-campo="camisa"]').value;
jogadores.push({ nome: nome, numeroCamisa: c === "" ? null : parseInt(c, 10) });
}
var comissao = [];
var faltaFuncao = false;
var pessoas = listaComissao.querySelectorAll(".linha-comissao");
for (var k = 0; k < pessoas.length; k++) {
var n = pessoas[k].querySelector('[data-campo="nome"]').value.trim();
if (!n) continue;
var f = pessoas[k].querySelector('[data-campo="funcao"]').value.trim();
if (!f) faltaFuncao = true;
comissao.push({ nome: n, funcao: f });
}
return {
faltaFuncao: faltaFuncao,
nome: valor("nome"),
escola: valor("escola"),
cidade: valor("cidade"),
contatoNome: valor("contatoNome"),
contatoTelefone: valor("contatoTelefone"),
contatoEmail: valor("contatoEmail"),
jogadores: jogadores
jogadores: jogadores,
comissao: comissao
};
}
@@ -555,6 +613,9 @@ data:
var corpo = montarCorpo();
if (!corpo.nome) { mostrarErro("Informe o nome do time."); return; }
if (corpo.jogadores.length > MAX_JOGADORES) { mostrarErro("No máximo " + MAX_JOGADORES + " jogadores."); return; }
if (corpo.faltaFuncao) { mostrarErro("Informe a função de cada pessoa da comissão técnica."); return; }
if (corpo.comissao.length > MAX_COMISSAO) { mostrarErro("No máximo " + MAX_COMISSAO + " pessoas na comissão técnica."); return; }
delete corpo.faltaFuncao;
enviar.disabled = true;
enviar.textContent = "Verificando…";
enviar.setAttribute("aria-busy", "true");
@@ -615,6 +676,7 @@ data:
situacao.className = "";
formulario.hidden = false;
for (var i = 0; i < 5; i++) linhaDeJogador();
linhaDeComissao("Técnico");
}).catch(function () {
recusar("Não foi possível abrir o convite agora. Tente de novo em instantes.");
});
@@ -623,6 +685,9 @@ data:
document.getElementById("maisJogador").addEventListener("click", function () {
if (listaJogadores.querySelectorAll(".linha-jogador").length < MAX_JOGADORES) linhaDeJogador().querySelector("input").focus();
});
document.getElementById("maisComissao").addEventListener("click", function () {
if (listaComissao.querySelectorAll(".linha-comissao").length < MAX_COMISSAO) linhaDeComissao("").querySelector("input").focus();
});
formulario.addEventListener("submit", aoEnviar);
iniciar();
})();
@@ -8242,6 +8307,557 @@ data:
</body>
</html>
time.css: |
/* Link do time: página pública, sem fonte nem script de fora (a CSP só deixa 'self'). Mobile primeiro. */
main { max-width: 56rem; }
.explica, .legenda { color: var(--muted); font-size: .9rem; }
.abas { display: flex; gap: .25rem; overflow-x: auto; border-bottom: 2px solid var(--line); margin: 0 0 1rem; }
.abas button { background: transparent; color: var(--navy); border: 0; border-bottom: 3px solid transparent; border-radius: 0; padding: .6rem .9rem; white-space: nowrap; font: inherit; font-weight: 700; cursor: pointer; }
.abas button[aria-selected="true"] { border-bottom-color: var(--orange); }
.abas button:focus-visible, button:focus-visible, input:focus-visible { outline: 3px solid var(--orange); outline-offset: 1px; }
h2 { font-size: 1.15rem; margin: .5rem 0; }
.rolagem { overflow-x: auto; }
table { border-collapse: collapse; width: 100%; font-size: .9rem; background: #fff; }
th, td { padding: .4rem .45rem; text-align: center; border-bottom: 1px solid var(--line); white-space: nowrap; }
th:nth-child(2), td:nth-child(2) { text-align: left; white-space: normal; min-width: 8rem; }
tr.meu { background: #fff4e5; font-weight: 700; }
.forma { display: inline-flex; gap: 3px; vertical-align: middle; }
.ponto { width: .8rem; height: .8rem; border-radius: 50%; display: inline-block; }
.ponto.V { background: #067647; }
.ponto.E { background: #98a2b3; }
.ponto.D { background: #b42318; }
.cartao { display: inline-block; width: .7rem; height: .95rem; border-radius: 2px; vertical-align: middle; margin-right: 3px; }
.cartao.amarelo { background: #f5c518; }
.cartao.vermelho { background: #d92d20; }
.jogo, .sumula { background: #fff; border: 1px solid var(--line); border-radius: 8px; padding: .6rem .8rem; margin: 0 0 .6rem; }
.jogo .quando { color: var(--muted); font-size: .85rem; }
.jogo .confronto { display: grid; grid-template-columns: 1fr auto 1fr; gap: .5rem; align-items: center; font-weight: 700; }
.jogo .confronto span:first-child { text-align: right; }
.jogo .placar { background: var(--navy); color: #fff; border-radius: 6px; padding: .1rem .6rem; }
.etiqueta { display: inline-block; font-size: .75rem; font-weight: 700; padding: .05rem .5rem; border-radius: 99px; background: #fff1e3; border: 1px solid var(--orange); margin-left: .4rem; }
.sumula h3 { margin: 0 0 .3rem; font-size: 1rem; }
.sumula ul { margin: .2rem 0 .5rem; padding-left: 1.2rem; }
form { display: grid; gap: 1rem; }
fieldset { border: 1px solid var(--line); border-radius: 8px; background: #fff; padding: .75rem 1rem 1rem; display: grid; gap: .6rem; }
legend { font-weight: 700; padding: 0 .4rem; }
input { font: inherit; padding: .5rem .6rem; border: 1px solid var(--line); border-radius: 6px; width: 100%; }
.linha-ficha { display: grid; grid-template-columns: 1fr 5.5rem auto; gap: .4rem; align-items: center; }
.linha-ficha button { padding: .4rem .6rem; }
button { font: inherit; font-weight: 700; padding: .65rem 1.1rem; border: 0; border-radius: 8px; background: var(--orange); color: #fff; cursor: pointer; }
button.secundario { background: #fff; color: var(--navy); border: 1px solid var(--line); justify-self: start; }
button:disabled { opacity: .55; cursor: not-allowed; }
.erro { color: #b42318; margin: 0; font-weight: 600; }
.ok { background: #e7f6ec; border-left: 4px solid #067647; padding: .8rem 1rem; margin: 0; }
#situacao.invalido { background: #fff1e3; border-left: 4px solid var(--orange); padding: 1rem 1.2rem; }
.somente-leitura li { margin: .15rem 0; }
@media (max-width: 30rem) { .linha-ficha { grid-template-columns: 1fr 4.5rem auto; } }
time.html: |
<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow">
<meta name="referrer" content="no-referrer">
<title>Seu time na competição — Athletic Map</title>
<link rel="stylesheet" href="/publico/_estilo.css">
<link rel="stylesheet" href="/publico/time.css?v=1">
</head>
<body>
<header><div class="marca">Athletic <span>Map</span></div></header>
<main>
<h1 id="titulo">Seu time na competição</h1>
<p id="situacao" role="status" aria-live="polite">Abrindo o link…</p>
<div id="painel" hidden>
<div class="abas" role="tablist" aria-label="Seções">
<button type="button" role="tab" id="aba-tabela" aria-controls="sec-tabela" aria-selected="true" data-aba="tabela">Tabela</button>
<button type="button" role="tab" id="aba-jogos" aria-controls="sec-jogos" aria-selected="false" tabindex="-1" data-aba="jogos">Jogos</button>
<button type="button" role="tab" id="aba-sumulas" aria-controls="sec-sumulas" aria-selected="false" tabindex="-1" data-aba="sumulas">Súmulas</button>
<button type="button" role="tab" id="aba-ficha" aria-controls="sec-ficha" aria-selected="false" tabindex="-1" data-aba="ficha">Ficha do time</button>
</div>
<section id="sec-tabela" role="tabpanel" aria-labelledby="aba-tabela">
<h2>Tabela</h2>
<div class="rolagem" id="tabela"></div>
<p class="legenda">P pontos · J jogos · V vitórias · E empates · D derrotas · GP gols pró · GC gols contra · SG saldo de gols · % aproveitamento · CA cartões amarelos · CV cartões vermelhos</p>
</section>
<section id="sec-jogos" role="tabpanel" aria-labelledby="aba-jogos" hidden>
<h2>Jogos</h2>
<div id="jogos"></div>
</section>
<section id="sec-sumulas" role="tabpanel" aria-labelledby="aba-sumulas" hidden>
<h2>Súmulas</h2>
<div id="sumulas"></div>
</section>
<section id="sec-ficha" role="tabpanel" aria-labelledby="aba-ficha" hidden>
<h2>Ficha do time</h2>
<p id="ficha-aviso" class="explica"></p>
<form id="ficha" novalidate>
<fieldset>
<legend>Jogadores</legend>
<div id="jogadores"></div>
<button type="button" id="maisJogador" class="secundario">+ Jogador</button>
</fieldset>
<fieldset>
<legend>Comissão técnica</legend>
<div id="comissao"></div>
<datalist id="funcoes-comissao">
<option value="Técnico"></option><option value="Auxiliar técnico"></option><option value="Preparador físico"></option>
<option value="Preparador de goleiros"></option><option value="Médico"></option><option value="Fisioterapeuta"></option>
<option value="Massagista"></option><option value="Roupeiro"></option><option value="Dirigente"></option>
</datalist>
<button type="button" id="maisComissao" class="secundario">+ Pessoa na comissão</button>
</fieldset>
<p id="ficha-erro" class="erro" role="alert" hidden></p>
<p id="ficha-ok" class="ok" role="status" hidden></p>
<button type="submit" id="salvar">Salvar ficha</button>
</form>
</section>
</div>
</main>
<script src="/publico/time.js?v=1"></script>
</body>
</html>
time.js: |
/* ===========================================================================
* Link do time — a página que UMA pessoa do time abre pelo link com chave.
*
* Sem login: a chave no endereço é o único segredo. A página mostra a tabela,
* os jogos e as súmulas da competição e a ficha do time; se o time for de
* fora e ainda não houver jogo encerrado, a ficha pode ser corrigida aqui.
*
* Tudo o que vem do servidor entra na página com `textContent` — nunca como
* HTML. O script é externo porque a CSP é `script-src 'self'`.
* =========================================================================== */
(function () {
"use strict";
var TOKEN = /^[A-Za-z0-9_-]{20,64}$/;
var MAX_JOGADORES = 60;
var MAX_COMISSAO = 15;
var ABAS = ["tabela", "jogos", "sumulas", "ficha"];
var ROTULOS_FORMA = { V: "Vitória", E: "Empate", D: "Derrota" };
var ROTULOS_STATUS = {
AGENDADA: "Agendado", ADIADA: "Adiado", EM_ANDAMENTO: "Em andamento", ENCERRADA: "Encerrado", CANCELADA: "Cancelado"
};
var token = decodeURIComponent(location.pathname.split("/").filter(Boolean).pop() || "");
var base = "/api/public/competicoes/times/" + encodeURIComponent(token);
var titulo = document.getElementById("titulo");
var situacao = document.getElementById("situacao");
var painel = document.getElementById("painel");
var listaJogadores = document.getElementById("jogadores");
var listaComissao = document.getElementById("comissao");
var formFicha = document.getElementById("ficha");
var fichaErro = document.getElementById("ficha-erro");
var fichaOk = document.getElementById("ficha-ok");
var salvar = document.getElementById("salvar");
var visao = null;
function el(tag, classe, texto) {
var e = document.createElement(tag);
if (classe) e.className = classe;
if (texto !== undefined && texto !== null) e.textContent = String(texto);
return e;
}
function limpar(no) {
while (no.firstChild) no.removeChild(no.firstChild);
}
function recusar(texto) {
situacao.textContent = texto;
situacao.className = "invalido";
painel.hidden = true;
}
function lerResposta(r) {
return r.json().catch(function () { return {}; }).then(function (corpo) { return { status: r.status, corpo: corpo }; });
}
// ------------------------------------------------------------------ abas
function abrirAba(nome, focar) {
for (var i = 0; i < ABAS.length; i++) {
var ativa = ABAS[i] === nome;
var aba = document.getElementById("aba-" + ABAS[i]);
aba.setAttribute("aria-selected", ativa ? "true" : "false");
aba.tabIndex = ativa ? 0 : -1;
document.getElementById("sec-" + ABAS[i]).hidden = !ativa;
if (ativa && focar) aba.focus();
}
}
function aoTeclarNaAba(ev) {
var i = ABAS.indexOf(ev.currentTarget.getAttribute("data-aba"));
if (ev.key === "ArrowRight") { abrirAba(ABAS[(i + 1) % ABAS.length], true); ev.preventDefault(); }
else if (ev.key === "ArrowLeft") { abrirAba(ABAS[(i + ABAS.length - 1) % ABAS.length], true); ev.preventDefault(); }
}
// ------------------------------------------------------------------ tabela
function dataBr(iso) {
if (!iso) return "Data a definir";
var d = new Date(iso);
if (isNaN(d.getTime())) return "Data a definir";
try {
return d.toLocaleString("pt-BR", { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit" });
} catch (e) {
return d.toISOString();
}
}
function percentual(n) {
return (Math.round((n || 0) * 10) / 10).toString().replace(".", ",") + "%";
}
function montarTabela(linhas, meuNome) {
var alvo = document.getElementById("tabela");
limpar(alvo);
if (!linhas || !linhas.length) { alvo.appendChild(el("p", "explica", "A tabela aparece quando houver times e jogos.")); return; }
var t = el("table");
t.appendChild(el("caption", "explica", "Classificação"));
var cab = el("tr");
var cols = [["#", "Posição"], ["Time", "Time"], ["P", "Pontos"], ["J", "Jogos"], ["V", "Vitórias"], ["E", "Empates"],
["D", "Derrotas"], ["GP", "Gols pró"], ["GC", "Gols contra"], ["SG", "Saldo de gols"], ["%", "Aproveitamento"],
["Últimos 5", "Últimos 5 jogos"], ["CA", "Cartões amarelos"], ["CV", "Cartões vermelhos"]];
cols.forEach(function (c) {
var th = el("th", null, c[0]);
th.scope = "col";
th.title = c[1];
cab.appendChild(th);
});
var thead = el("thead");
thead.appendChild(cab);
t.appendChild(thead);
var corpo = el("tbody");
linhas.forEach(function (l) {
var tr = el("tr", l.time === meuNome ? "meu" : "");
[l.posicao, l.time, l.pontos, l.jogos, l.vitorias, l.empates, l.derrotas, l.golsPro, l.golsContra, l.saldoGols,
percentual(l.aproveitamento)].forEach(function (v) { tr.appendChild(el("td", null, v)); });
var forma = el("td");
var caixa = el("span", "forma");
(l.ultimos || []).forEach(function (r) {
var p = el("span", "ponto " + r);
p.title = ROTULOS_FORMA[r] || r;
p.setAttribute("role", "img");
p.setAttribute("aria-label", ROTULOS_FORMA[r] || r);
caixa.appendChild(p);
});
forma.appendChild(caixa);
tr.appendChild(forma);
var am = el("td");
am.appendChild(el("span", "cartao amarelo"));
am.appendChild(document.createTextNode(String(l.amarelos || 0)));
tr.appendChild(am);
var vm = el("td");
vm.appendChild(el("span", "cartao vermelho"));
vm.appendChild(document.createTextNode(String(l.vermelhos || 0)));
tr.appendChild(vm);
corpo.appendChild(tr);
});
t.appendChild(corpo);
alvo.appendChild(t);
}
// ------------------------------------------------------------------ jogos
function montarJogos(jogos) {
var alvo = document.getElementById("jogos");
limpar(alvo);
if (!jogos || !jogos.length) { alvo.appendChild(el("p", "explica", "Ainda não há jogos marcados.")); return; }
jogos.forEach(function (j) {
var card = el("div", "jogo");
var quando = el("div", "quando", (j.rodada ? j.rodada + " · " : "") + dataBr(j.data));
var etiqueta = el("span", "etiqueta", ROTULOS_STATUS[j.status] || j.status);
quando.appendChild(etiqueta);
if (j.wo) quando.appendChild(el("span", "etiqueta", "W.O."));
card.appendChild(quando);
var linha = el("div", "confronto");
linha.appendChild(el("span", null, j.mandante));
var temPlacar = j.placarMandante !== undefined && j.placarMandante !== null;
linha.appendChild(el("span", temPlacar ? "placar" : "", temPlacar ? j.placarMandante + " x " + j.placarVisitante : "x"));
linha.appendChild(el("span", null, j.visitante));
card.appendChild(linha);
if (j.penaltis) card.appendChild(el("div", "quando", "Pênaltis: " + j.penaltis.mandante + " x " + j.penaltis.visitante));
if (j.motivo) card.appendChild(el("div", "quando", "Motivo: " + j.motivo));
alvo.appendChild(card);
});
}
// ------------------------------------------------------------------ súmulas
function pessoa(p) {
if (!p) return "jogador não identificado";
if (p.nome && p.numeroCamisa !== undefined && p.numeroCamisa !== null) return p.nome + " (" + p.numeroCamisa + ")";
if (p.nome) return p.nome;
if (p.numeroCamisa !== undefined && p.numeroCamisa !== null) return "camisa " + p.numeroCamisa;
return "jogador não identificado";
}
function momento(e) {
var m = e.minuto !== undefined && e.minuto !== null ? e.minuto + "'" : "";
var t = e.tempo ? e.tempo + "º tempo" : "";
return [t, m].filter(Boolean).join(" ") || "—";
}
function secaoDaSumula(card, nome, itens, texto) {
if (!itens || !itens.length) return;
card.appendChild(el("strong", null, nome));
var ul = el("ul");
itens.forEach(function (i) { ul.appendChild(el("li", null, texto(i))); });
card.appendChild(ul);
}
function montarSumulas(sumulas, jogos) {
var alvo = document.getElementById("sumulas");
limpar(alvo);
if (!sumulas || !sumulas.length) { alvo.appendChild(el("p", "explica", "As súmulas dos jogos do seu time aparecem aqui.")); return; }
sumulas.forEach(function (s) {
var j = (jogos || []).filter(function (x) { return x.ref === s.jogoRef; })[0];
var card = el("div", "sumula");
var cab = j ? j.mandante + " x " + j.visitante : "Jogo " + s.jogoRef;
if (j && j.placarMandante !== undefined && j.placarMandante !== null) {
cab = j.mandante + " " + j.placarMandante + " x " + j.placarVisitante + " " + j.visitante;
}
card.appendChild(el("h3", null, cab));
if (j) card.appendChild(el("div", "quando", (j.rodada ? j.rodada + " · " : "") + dataBr(j.data)));
secaoDaSumula(card, "Gols", s.gols, function (g) { return momento(g) + " · " + g.time + " · " + pessoa(g.jogador); });
secaoDaSumula(card, "Cartões", s.cartoes, function (c) {
return momento(c) + " · " + (c.tipo === "VERMELHO" ? "vermelho" : "amarelo") + " · " + c.time + " · " + pessoa(c.jogador);
});
secaoDaSumula(card, "Substituições", s.substituicoes, function (t) {
return momento(t) + " · " + t.time + " · sai " + pessoa(t.sai) + ", entra " + pessoa(t.entra);
});
alvo.appendChild(card);
});
}
// ------------------------------------------------------------------ ficha
function linhaDeJogador(j) {
var linha = el("div", "linha-ficha linha-jogador");
if (j && j.id) linha.setAttribute("data-id", j.id);
var nome = el("input");
nome.placeholder = "Nome do jogador";
nome.maxLength = 120;
nome.value = (j && j.nome) || "";
nome.setAttribute("aria-label", "Nome do jogador");
nome.setAttribute("data-campo", "nome");
var camisa = el("input");
camisa.type = "number";
camisa.min = "0";
camisa.max = "999";
camisa.placeholder = "nº";
camisa.value = j && j.numeroCamisa !== undefined && j.numeroCamisa !== null ? j.numeroCamisa : "";
camisa.setAttribute("aria-label", "Número da camisa");
camisa.setAttribute("data-campo", "camisa");
var tirar = el("button", "secundario", "×");
tirar.type = "button";
tirar.setAttribute("aria-label", "Remover jogador");
tirar.addEventListener("click", function () { linha.remove(); });
linha.appendChild(nome);
linha.appendChild(camisa);
linha.appendChild(tirar);
listaJogadores.appendChild(linha);
return linha;
}
function linhaDeComissao(m) {
var linha = el("div", "linha-ficha linha-comissao");
if (m && m.id) linha.setAttribute("data-id", m.id);
var nome = el("input");
nome.placeholder = "Nome";
nome.maxLength = 120;
nome.value = (m && m.nome) || "";
nome.setAttribute("aria-label", "Nome na comissão técnica");
nome.setAttribute("data-campo", "nome");
var papel = el("input");
papel.placeholder = "Função";
papel.maxLength = 40;
papel.value = (m && m.funcao) || "";
papel.setAttribute("list", "funcoes-comissao");
papel.setAttribute("aria-label", "Função na comissão técnica");
papel.setAttribute("data-campo", "funcao");
var tirar = el("button", "secundario", "×");
tirar.type = "button";
tirar.setAttribute("aria-label", "Remover da comissão");
tirar.addEventListener("click", function () { linha.remove(); });
linha.appendChild(nome);
linha.appendChild(papel);
linha.appendChild(tirar);
listaComissao.appendChild(linha);
return linha;
}
/** Time da escola ou ficha fechada: só leitura (listas simples). */
function fichaSomenteLeitura(ficha) {
formFicha.hidden = true;
var alvo = document.getElementById("ficha-leitura");
if (!alvo) {
alvo = el("div", "somente-leitura");
alvo.id = "ficha-leitura";
formFicha.parentNode.appendChild(alvo);
}
limpar(alvo);
alvo.appendChild(el("h3", null, "Jogadores"));
var ul = el("ul");
(ficha.jogadores || []).forEach(function (j) {
ul.appendChild(el("li", null, pessoa({ nome: j.nome, numeroCamisa: j.numeroCamisa })));
});
if (!ul.childNodes.length) ul.appendChild(el("li", null, "Nenhum jogador na ficha."));
alvo.appendChild(ul);
if ((ficha.comissao || []).length) {
alvo.appendChild(el("h3", null, "Comissão técnica"));
var uc = el("ul");
ficha.comissao.forEach(function (m) { uc.appendChild(el("li", null, m.nome + " — " + m.funcao)); });
alvo.appendChild(uc);
}
}
function montarFicha(ficha) {
var aviso = document.getElementById("ficha-aviso");
limpar(listaJogadores);
limpar(listaComissao);
if (!ficha.editavel) {
aviso.textContent = "Esta ficha está fechada para alterações aqui. Se algo estiver errado, fale com a organização.";
fichaSomenteLeitura(ficha);
return;
}
aviso.textContent = "Confira os jogadores (nome e número da camisa) e a comissão técnica. Dá para corrigir até o primeiro jogo ser encerrado.";
formFicha.hidden = false;
(ficha.jogadores || []).forEach(linhaDeJogador);
if (!(ficha.jogadores || []).length) for (var i = 0; i < 3; i++) linhaDeJogador();
(ficha.comissao || []).forEach(linhaDeComissao);
}
function mostrarErroFicha(texto) {
fichaErro.textContent = texto || "";
fichaErro.hidden = !texto;
}
function mostrarOkFicha(texto) {
fichaOk.textContent = texto || "";
fichaOk.hidden = !texto;
}
function corpoDaFicha() {
var jogadores = [];
var linhas = listaJogadores.querySelectorAll(".linha-jogador");
for (var i = 0; i < linhas.length; i++) {
var nome = linhas[i].querySelector('[data-campo="nome"]').value.trim();
if (!nome) continue;
var c = linhas[i].querySelector('[data-campo="camisa"]').value;
var j = { nome: nome, numeroCamisa: c === "" ? null : parseInt(c, 10) };
if (linhas[i].getAttribute("data-id")) j.id = linhas[i].getAttribute("data-id");
jogadores.push(j);
}
var comissao = [];
var faltaFuncao = false;
var pessoas = listaComissao.querySelectorAll(".linha-comissao");
for (var k = 0; k < pessoas.length; k++) {
var n = pessoas[k].querySelector('[data-campo="nome"]').value.trim();
if (!n) continue;
var f = pessoas[k].querySelector('[data-campo="funcao"]').value.trim();
if (!f) faltaFuncao = true;
var m = { nome: n, funcao: f };
if (pessoas[k].getAttribute("data-id")) m.id = pessoas[k].getAttribute("data-id");
comissao.push(m);
}
return { jogadores: jogadores, comissao: comissao, faltaFuncao: faltaFuncao };
}
function aoSalvar(ev) {
ev.preventDefault();
mostrarErroFicha("");
mostrarOkFicha("");
var c = corpoDaFicha();
if (c.jogadores.length > MAX_JOGADORES) { mostrarErroFicha("No máximo " + MAX_JOGADORES + " jogadores."); return; }
if (c.faltaFuncao) { mostrarErroFicha("Informe a função de cada pessoa da comissão técnica."); return; }
if (c.comissao.length > MAX_COMISSAO) { mostrarErroFicha("No máximo " + MAX_COMISSAO + " pessoas na comissão técnica."); return; }
salvar.disabled = true;
salvar.setAttribute("aria-busy", "true");
fetch(base + "/ficha", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ jogadores: c.jogadores, comissao: c.comissao })
}).then(lerResposta).then(function (r) {
salvar.disabled = false;
salvar.removeAttribute("aria-busy");
if (r.status >= 200 && r.status < 300) {
mostrarOkFicha("Ficha salva.");
return carregar(true);
}
if (r.status === 404) { recusar("Este link não está mais disponível."); return null; }
mostrarErroFicha((r.corpo && r.corpo.detail) || "Não foi possível salvar agora. Tente de novo em instantes.");
return null;
}).catch(function () {
salvar.disabled = false;
salvar.removeAttribute("aria-busy");
mostrarErroFicha("Sem conexão. Confira a internet e tente de novo.");
});
}
// ------------------------------------------------------------------ carga
function mostrar(v, manterAba) {
visao = v;
var nomeTime = (v.time && v.time.nome) || "Seu time";
titulo.textContent = (v.competicao || "Competição") + " — " + nomeTime;
document.title = titulo.textContent + " — Athletic Map";
var partes = [];
if (v.time && v.time.escola) partes.push(v.time.escola);
if (v.time && v.time.cidade) partes.push(v.time.cidade);
if (v.temporada && v.temporada.nome) partes.push("Temporada " + v.temporada.nome);
if (v.time && v.time.status === "DESQUALIFICADO") partes.push("Time desqualificado");
situacao.textContent = partes.join(" · ");
situacao.className = "";
painel.hidden = false;
montarTabela(v.classificacao, nomeTime);
montarJogos(v.jogos);
montarSumulas(v.sumulas, v.jogos);
montarFicha(v.ficha || { jogadores: [], comissao: [], editavel: false });
if (!manterAba) abrirAba("tabela", false);
}
function carregar(manterAba) {
return fetch(base).then(lerResposta).then(function (r) {
if (r.status === 404) { recusar("Este link não está disponível ou foi desativado. Peça um novo à organização."); return; }
if (r.status === 429) { recusar("Muitas consultas seguidas. Aguarde alguns minutos e abra o link de novo."); return; }
if (r.status !== 200) { recusar("Não foi possível abrir agora. Tente de novo em instantes."); return; }
mostrar(r.corpo || {}, manterAba);
}).catch(function () {
recusar("Não foi possível abrir agora. Tente de novo em instantes.");
});
}
function iniciar() {
if (!TOKEN.test(token)) { recusar("Este endereço não é válido."); return; }
ABAS.forEach(function (nome) {
var aba = document.getElementById("aba-" + nome);
aba.addEventListener("click", function () { abrirAba(nome, false); });
aba.addEventListener("keydown", aoTeclarNaAba);
});
document.getElementById("maisJogador").addEventListener("click", function () {
if (listaJogadores.querySelectorAll(".linha-jogador").length < MAX_JOGADORES) linhaDeJogador().querySelector("input").focus();
});
document.getElementById("maisComissao").addEventListener("click", function () {
if (listaComissao.querySelectorAll(".linha-comissao").length < MAX_COMISSAO) linhaDeComissao(null).querySelector("input").focus();
});
formFicha.addEventListener("submit", aoSalvar);
carregar(false);
}
iniciar();
})();
---
apiVersion: v1
kind: ConfigMap
+3
View File
@@ -153,6 +153,9 @@ spec:
- { key: "preparando.js", path: "preparando.js" }
- { key: "privacidade.html", path: "privacidade.html" }
- { key: "termos.html", path: "termos.html" }
- { key: "time.css", path: "time.css" }
- { key: "time.html", path: "time.html" }
- { key: "time.js", path: "time.js" }
- configMap:
name: frontend-publico-binario
items:
+619 -3
View File
@@ -409,7 +409,7 @@ data:
<form id="formulario" hidden novalidate>
<p class="explica">
Preencha os dados do seu time e dos jogadores (só o nome e o número da camisa).
Preencha os dados do seu time, dos jogadores (só o nome e o número da camisa) e da comissão técnica (nome e função).
A organização confere e aprova antes do time entrar na competição.
</p>
@@ -433,12 +433,23 @@ data:
<button type="button" id="maisJogador" class="secundario">+ Jogador</button>
</fieldset>
<fieldset>
<legend>Comissão técnica</legend>
<div id="comissao"></div>
<datalist id="funcoes-comissao">
<option value="Técnico"></option><option value="Auxiliar técnico"></option><option value="Preparador físico"></option>
<option value="Preparador de goleiros"></option><option value="Médico"></option><option value="Fisioterapeuta"></option>
<option value="Massagista"></option><option value="Roupeiro"></option><option value="Dirigente"></option>
</datalist>
<button type="button" id="maisComissao" class="secundario">+ Pessoa na comissão</button>
</fieldset>
<p id="erro" class="erro" role="alert" hidden></p>
<button type="submit" id="enviar">Enviar time</button>
</form>
</main>
<script src="/publico/desafio.js?v=1"></script>
<script src="/publico/convite.js?v=2"></script>
<script src="/publico/convite.js?v=3"></script>
</body>
</html>
@@ -465,6 +476,7 @@ data:
var TOKEN = /^[A-Za-z0-9_-]{20,64}$/;
var MAX_JOGADORES = 60;
var MAX_COMISSAO = 15;
var token = decodeURIComponent((location.pathname.split("/").filter(Boolean).pop() || ""));
var base = "/api/public/competicoes/convites/" + encodeURIComponent(token);
@@ -473,6 +485,7 @@ data:
var situacao = document.getElementById("situacao");
var formulario = document.getElementById("formulario");
var listaJogadores = document.getElementById("jogadores");
var listaComissao = document.getElementById("comissao");
var erro = document.getElementById("erro");
var enviar = document.getElementById("enviar");
var ROTULO_ENVIAR = enviar.textContent;
@@ -520,6 +533,39 @@ data:
return linha;
}
/** Comissao tecnica (21/10/2026): nome e funcao; a funcao tem sugestoes, mas aceita texto livre. */
function linhaDeComissao(funcao) {
var linha = document.createElement("div");
linha.className = "linha-jogador linha-comissao";
var nome = document.createElement("input");
nome.placeholder = "Nome";
nome.maxLength = 120;
nome.setAttribute("aria-label", "Nome na comissão técnica");
nome.setAttribute("data-campo", "nome");
var papel = document.createElement("input");
papel.placeholder = "Função";
papel.maxLength = 40;
papel.value = funcao || "";
papel.setAttribute("list", "funcoes-comissao");
papel.setAttribute("aria-label", "Função na comissão técnica");
papel.setAttribute("data-campo", "funcao");
var tirar = document.createElement("button");
tirar.type = "button";
tirar.className = "secundario";
tirar.textContent = "×";
tirar.setAttribute("aria-label", "Remover da comissão");
tirar.addEventListener("click", function () { linha.remove(); });
linha.appendChild(nome);
linha.appendChild(papel);
linha.appendChild(tirar);
listaComissao.appendChild(linha);
return linha;
}
function valor(id) {
var v = document.getElementById(id).value.trim();
return v === "" ? null : v;
@@ -534,14 +580,26 @@ data:
var c = linhas[i].querySelector('[data-campo="camisa"]').value;
jogadores.push({ nome: nome, numeroCamisa: c === "" ? null : parseInt(c, 10) });
}
var comissao = [];
var faltaFuncao = false;
var pessoas = listaComissao.querySelectorAll(".linha-comissao");
for (var k = 0; k < pessoas.length; k++) {
var n = pessoas[k].querySelector('[data-campo="nome"]').value.trim();
if (!n) continue;
var f = pessoas[k].querySelector('[data-campo="funcao"]').value.trim();
if (!f) faltaFuncao = true;
comissao.push({ nome: n, funcao: f });
}
return {
faltaFuncao: faltaFuncao,
nome: valor("nome"),
escola: valor("escola"),
cidade: valor("cidade"),
contatoNome: valor("contatoNome"),
contatoTelefone: valor("contatoTelefone"),
contatoEmail: valor("contatoEmail"),
jogadores: jogadores
jogadores: jogadores,
comissao: comissao
};
}
@@ -555,6 +613,9 @@ data:
var corpo = montarCorpo();
if (!corpo.nome) { mostrarErro("Informe o nome do time."); return; }
if (corpo.jogadores.length > MAX_JOGADORES) { mostrarErro("No máximo " + MAX_JOGADORES + " jogadores."); return; }
if (corpo.faltaFuncao) { mostrarErro("Informe a função de cada pessoa da comissão técnica."); return; }
if (corpo.comissao.length > MAX_COMISSAO) { mostrarErro("No máximo " + MAX_COMISSAO + " pessoas na comissão técnica."); return; }
delete corpo.faltaFuncao;
enviar.disabled = true;
enviar.textContent = "Verificando…";
enviar.setAttribute("aria-busy", "true");
@@ -615,6 +676,7 @@ data:
situacao.className = "";
formulario.hidden = false;
for (var i = 0; i < 5; i++) linhaDeJogador();
linhaDeComissao("Técnico");
}).catch(function () {
recusar("Não foi possível abrir o convite agora. Tente de novo em instantes.");
});
@@ -623,6 +685,9 @@ data:
document.getElementById("maisJogador").addEventListener("click", function () {
if (listaJogadores.querySelectorAll(".linha-jogador").length < MAX_JOGADORES) linhaDeJogador().querySelector("input").focus();
});
document.getElementById("maisComissao").addEventListener("click", function () {
if (listaComissao.querySelectorAll(".linha-comissao").length < MAX_COMISSAO) linhaDeComissao("").querySelector("input").focus();
});
formulario.addEventListener("submit", aoEnviar);
iniciar();
})();
@@ -8242,6 +8307,557 @@ data:
</body>
</html>
time.css: |
/* Link do time: página pública, sem fonte nem script de fora (a CSP só deixa 'self'). Mobile primeiro. */
main { max-width: 56rem; }
.explica, .legenda { color: var(--muted); font-size: .9rem; }
.abas { display: flex; gap: .25rem; overflow-x: auto; border-bottom: 2px solid var(--line); margin: 0 0 1rem; }
.abas button { background: transparent; color: var(--navy); border: 0; border-bottom: 3px solid transparent; border-radius: 0; padding: .6rem .9rem; white-space: nowrap; font: inherit; font-weight: 700; cursor: pointer; }
.abas button[aria-selected="true"] { border-bottom-color: var(--orange); }
.abas button:focus-visible, button:focus-visible, input:focus-visible { outline: 3px solid var(--orange); outline-offset: 1px; }
h2 { font-size: 1.15rem; margin: .5rem 0; }
.rolagem { overflow-x: auto; }
table { border-collapse: collapse; width: 100%; font-size: .9rem; background: #fff; }
th, td { padding: .4rem .45rem; text-align: center; border-bottom: 1px solid var(--line); white-space: nowrap; }
th:nth-child(2), td:nth-child(2) { text-align: left; white-space: normal; min-width: 8rem; }
tr.meu { background: #fff4e5; font-weight: 700; }
.forma { display: inline-flex; gap: 3px; vertical-align: middle; }
.ponto { width: .8rem; height: .8rem; border-radius: 50%; display: inline-block; }
.ponto.V { background: #067647; }
.ponto.E { background: #98a2b3; }
.ponto.D { background: #b42318; }
.cartao { display: inline-block; width: .7rem; height: .95rem; border-radius: 2px; vertical-align: middle; margin-right: 3px; }
.cartao.amarelo { background: #f5c518; }
.cartao.vermelho { background: #d92d20; }
.jogo, .sumula { background: #fff; border: 1px solid var(--line); border-radius: 8px; padding: .6rem .8rem; margin: 0 0 .6rem; }
.jogo .quando { color: var(--muted); font-size: .85rem; }
.jogo .confronto { display: grid; grid-template-columns: 1fr auto 1fr; gap: .5rem; align-items: center; font-weight: 700; }
.jogo .confronto span:first-child { text-align: right; }
.jogo .placar { background: var(--navy); color: #fff; border-radius: 6px; padding: .1rem .6rem; }
.etiqueta { display: inline-block; font-size: .75rem; font-weight: 700; padding: .05rem .5rem; border-radius: 99px; background: #fff1e3; border: 1px solid var(--orange); margin-left: .4rem; }
.sumula h3 { margin: 0 0 .3rem; font-size: 1rem; }
.sumula ul { margin: .2rem 0 .5rem; padding-left: 1.2rem; }
form { display: grid; gap: 1rem; }
fieldset { border: 1px solid var(--line); border-radius: 8px; background: #fff; padding: .75rem 1rem 1rem; display: grid; gap: .6rem; }
legend { font-weight: 700; padding: 0 .4rem; }
input { font: inherit; padding: .5rem .6rem; border: 1px solid var(--line); border-radius: 6px; width: 100%; }
.linha-ficha { display: grid; grid-template-columns: 1fr 5.5rem auto; gap: .4rem; align-items: center; }
.linha-ficha button { padding: .4rem .6rem; }
button { font: inherit; font-weight: 700; padding: .65rem 1.1rem; border: 0; border-radius: 8px; background: var(--orange); color: #fff; cursor: pointer; }
button.secundario { background: #fff; color: var(--navy); border: 1px solid var(--line); justify-self: start; }
button:disabled { opacity: .55; cursor: not-allowed; }
.erro { color: #b42318; margin: 0; font-weight: 600; }
.ok { background: #e7f6ec; border-left: 4px solid #067647; padding: .8rem 1rem; margin: 0; }
#situacao.invalido { background: #fff1e3; border-left: 4px solid var(--orange); padding: 1rem 1.2rem; }
.somente-leitura li { margin: .15rem 0; }
@media (max-width: 30rem) { .linha-ficha { grid-template-columns: 1fr 4.5rem auto; } }
time.html: |
<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow">
<meta name="referrer" content="no-referrer">
<title>Seu time na competição — Athletic Map</title>
<link rel="stylesheet" href="/publico/_estilo.css">
<link rel="stylesheet" href="/publico/time.css?v=1">
</head>
<body>
<header><div class="marca">Athletic <span>Map</span></div></header>
<main>
<h1 id="titulo">Seu time na competição</h1>
<p id="situacao" role="status" aria-live="polite">Abrindo o link…</p>
<div id="painel" hidden>
<div class="abas" role="tablist" aria-label="Seções">
<button type="button" role="tab" id="aba-tabela" aria-controls="sec-tabela" aria-selected="true" data-aba="tabela">Tabela</button>
<button type="button" role="tab" id="aba-jogos" aria-controls="sec-jogos" aria-selected="false" tabindex="-1" data-aba="jogos">Jogos</button>
<button type="button" role="tab" id="aba-sumulas" aria-controls="sec-sumulas" aria-selected="false" tabindex="-1" data-aba="sumulas">Súmulas</button>
<button type="button" role="tab" id="aba-ficha" aria-controls="sec-ficha" aria-selected="false" tabindex="-1" data-aba="ficha">Ficha do time</button>
</div>
<section id="sec-tabela" role="tabpanel" aria-labelledby="aba-tabela">
<h2>Tabela</h2>
<div class="rolagem" id="tabela"></div>
<p class="legenda">P pontos · J jogos · V vitórias · E empates · D derrotas · GP gols pró · GC gols contra · SG saldo de gols · % aproveitamento · CA cartões amarelos · CV cartões vermelhos</p>
</section>
<section id="sec-jogos" role="tabpanel" aria-labelledby="aba-jogos" hidden>
<h2>Jogos</h2>
<div id="jogos"></div>
</section>
<section id="sec-sumulas" role="tabpanel" aria-labelledby="aba-sumulas" hidden>
<h2>Súmulas</h2>
<div id="sumulas"></div>
</section>
<section id="sec-ficha" role="tabpanel" aria-labelledby="aba-ficha" hidden>
<h2>Ficha do time</h2>
<p id="ficha-aviso" class="explica"></p>
<form id="ficha" novalidate>
<fieldset>
<legend>Jogadores</legend>
<div id="jogadores"></div>
<button type="button" id="maisJogador" class="secundario">+ Jogador</button>
</fieldset>
<fieldset>
<legend>Comissão técnica</legend>
<div id="comissao"></div>
<datalist id="funcoes-comissao">
<option value="Técnico"></option><option value="Auxiliar técnico"></option><option value="Preparador físico"></option>
<option value="Preparador de goleiros"></option><option value="Médico"></option><option value="Fisioterapeuta"></option>
<option value="Massagista"></option><option value="Roupeiro"></option><option value="Dirigente"></option>
</datalist>
<button type="button" id="maisComissao" class="secundario">+ Pessoa na comissão</button>
</fieldset>
<p id="ficha-erro" class="erro" role="alert" hidden></p>
<p id="ficha-ok" class="ok" role="status" hidden></p>
<button type="submit" id="salvar">Salvar ficha</button>
</form>
</section>
</div>
</main>
<script src="/publico/time.js?v=1"></script>
</body>
</html>
time.js: |
/* ===========================================================================
* Link do time — a página que UMA pessoa do time abre pelo link com chave.
*
* Sem login: a chave no endereço é o único segredo. A página mostra a tabela,
* os jogos e as súmulas da competição e a ficha do time; se o time for de
* fora e ainda não houver jogo encerrado, a ficha pode ser corrigida aqui.
*
* Tudo o que vem do servidor entra na página com `textContent` — nunca como
* HTML. O script é externo porque a CSP é `script-src 'self'`.
* =========================================================================== */
(function () {
"use strict";
var TOKEN = /^[A-Za-z0-9_-]{20,64}$/;
var MAX_JOGADORES = 60;
var MAX_COMISSAO = 15;
var ABAS = ["tabela", "jogos", "sumulas", "ficha"];
var ROTULOS_FORMA = { V: "Vitória", E: "Empate", D: "Derrota" };
var ROTULOS_STATUS = {
AGENDADA: "Agendado", ADIADA: "Adiado", EM_ANDAMENTO: "Em andamento", ENCERRADA: "Encerrado", CANCELADA: "Cancelado"
};
var token = decodeURIComponent(location.pathname.split("/").filter(Boolean).pop() || "");
var base = "/api/public/competicoes/times/" + encodeURIComponent(token);
var titulo = document.getElementById("titulo");
var situacao = document.getElementById("situacao");
var painel = document.getElementById("painel");
var listaJogadores = document.getElementById("jogadores");
var listaComissao = document.getElementById("comissao");
var formFicha = document.getElementById("ficha");
var fichaErro = document.getElementById("ficha-erro");
var fichaOk = document.getElementById("ficha-ok");
var salvar = document.getElementById("salvar");
var visao = null;
function el(tag, classe, texto) {
var e = document.createElement(tag);
if (classe) e.className = classe;
if (texto !== undefined && texto !== null) e.textContent = String(texto);
return e;
}
function limpar(no) {
while (no.firstChild) no.removeChild(no.firstChild);
}
function recusar(texto) {
situacao.textContent = texto;
situacao.className = "invalido";
painel.hidden = true;
}
function lerResposta(r) {
return r.json().catch(function () { return {}; }).then(function (corpo) { return { status: r.status, corpo: corpo }; });
}
// ------------------------------------------------------------------ abas
function abrirAba(nome, focar) {
for (var i = 0; i < ABAS.length; i++) {
var ativa = ABAS[i] === nome;
var aba = document.getElementById("aba-" + ABAS[i]);
aba.setAttribute("aria-selected", ativa ? "true" : "false");
aba.tabIndex = ativa ? 0 : -1;
document.getElementById("sec-" + ABAS[i]).hidden = !ativa;
if (ativa && focar) aba.focus();
}
}
function aoTeclarNaAba(ev) {
var i = ABAS.indexOf(ev.currentTarget.getAttribute("data-aba"));
if (ev.key === "ArrowRight") { abrirAba(ABAS[(i + 1) % ABAS.length], true); ev.preventDefault(); }
else if (ev.key === "ArrowLeft") { abrirAba(ABAS[(i + ABAS.length - 1) % ABAS.length], true); ev.preventDefault(); }
}
// ------------------------------------------------------------------ tabela
function dataBr(iso) {
if (!iso) return "Data a definir";
var d = new Date(iso);
if (isNaN(d.getTime())) return "Data a definir";
try {
return d.toLocaleString("pt-BR", { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit" });
} catch (e) {
return d.toISOString();
}
}
function percentual(n) {
return (Math.round((n || 0) * 10) / 10).toString().replace(".", ",") + "%";
}
function montarTabela(linhas, meuNome) {
var alvo = document.getElementById("tabela");
limpar(alvo);
if (!linhas || !linhas.length) { alvo.appendChild(el("p", "explica", "A tabela aparece quando houver times e jogos.")); return; }
var t = el("table");
t.appendChild(el("caption", "explica", "Classificação"));
var cab = el("tr");
var cols = [["#", "Posição"], ["Time", "Time"], ["P", "Pontos"], ["J", "Jogos"], ["V", "Vitórias"], ["E", "Empates"],
["D", "Derrotas"], ["GP", "Gols pró"], ["GC", "Gols contra"], ["SG", "Saldo de gols"], ["%", "Aproveitamento"],
["Últimos 5", "Últimos 5 jogos"], ["CA", "Cartões amarelos"], ["CV", "Cartões vermelhos"]];
cols.forEach(function (c) {
var th = el("th", null, c[0]);
th.scope = "col";
th.title = c[1];
cab.appendChild(th);
});
var thead = el("thead");
thead.appendChild(cab);
t.appendChild(thead);
var corpo = el("tbody");
linhas.forEach(function (l) {
var tr = el("tr", l.time === meuNome ? "meu" : "");
[l.posicao, l.time, l.pontos, l.jogos, l.vitorias, l.empates, l.derrotas, l.golsPro, l.golsContra, l.saldoGols,
percentual(l.aproveitamento)].forEach(function (v) { tr.appendChild(el("td", null, v)); });
var forma = el("td");
var caixa = el("span", "forma");
(l.ultimos || []).forEach(function (r) {
var p = el("span", "ponto " + r);
p.title = ROTULOS_FORMA[r] || r;
p.setAttribute("role", "img");
p.setAttribute("aria-label", ROTULOS_FORMA[r] || r);
caixa.appendChild(p);
});
forma.appendChild(caixa);
tr.appendChild(forma);
var am = el("td");
am.appendChild(el("span", "cartao amarelo"));
am.appendChild(document.createTextNode(String(l.amarelos || 0)));
tr.appendChild(am);
var vm = el("td");
vm.appendChild(el("span", "cartao vermelho"));
vm.appendChild(document.createTextNode(String(l.vermelhos || 0)));
tr.appendChild(vm);
corpo.appendChild(tr);
});
t.appendChild(corpo);
alvo.appendChild(t);
}
// ------------------------------------------------------------------ jogos
function montarJogos(jogos) {
var alvo = document.getElementById("jogos");
limpar(alvo);
if (!jogos || !jogos.length) { alvo.appendChild(el("p", "explica", "Ainda não há jogos marcados.")); return; }
jogos.forEach(function (j) {
var card = el("div", "jogo");
var quando = el("div", "quando", (j.rodada ? j.rodada + " · " : "") + dataBr(j.data));
var etiqueta = el("span", "etiqueta", ROTULOS_STATUS[j.status] || j.status);
quando.appendChild(etiqueta);
if (j.wo) quando.appendChild(el("span", "etiqueta", "W.O."));
card.appendChild(quando);
var linha = el("div", "confronto");
linha.appendChild(el("span", null, j.mandante));
var temPlacar = j.placarMandante !== undefined && j.placarMandante !== null;
linha.appendChild(el("span", temPlacar ? "placar" : "", temPlacar ? j.placarMandante + " x " + j.placarVisitante : "x"));
linha.appendChild(el("span", null, j.visitante));
card.appendChild(linha);
if (j.penaltis) card.appendChild(el("div", "quando", "Pênaltis: " + j.penaltis.mandante + " x " + j.penaltis.visitante));
if (j.motivo) card.appendChild(el("div", "quando", "Motivo: " + j.motivo));
alvo.appendChild(card);
});
}
// ------------------------------------------------------------------ súmulas
function pessoa(p) {
if (!p) return "jogador não identificado";
if (p.nome && p.numeroCamisa !== undefined && p.numeroCamisa !== null) return p.nome + " (" + p.numeroCamisa + ")";
if (p.nome) return p.nome;
if (p.numeroCamisa !== undefined && p.numeroCamisa !== null) return "camisa " + p.numeroCamisa;
return "jogador não identificado";
}
function momento(e) {
var m = e.minuto !== undefined && e.minuto !== null ? e.minuto + "'" : "";
var t = e.tempo ? e.tempo + "º tempo" : "";
return [t, m].filter(Boolean).join(" ") || "—";
}
function secaoDaSumula(card, nome, itens, texto) {
if (!itens || !itens.length) return;
card.appendChild(el("strong", null, nome));
var ul = el("ul");
itens.forEach(function (i) { ul.appendChild(el("li", null, texto(i))); });
card.appendChild(ul);
}
function montarSumulas(sumulas, jogos) {
var alvo = document.getElementById("sumulas");
limpar(alvo);
if (!sumulas || !sumulas.length) { alvo.appendChild(el("p", "explica", "As súmulas dos jogos do seu time aparecem aqui.")); return; }
sumulas.forEach(function (s) {
var j = (jogos || []).filter(function (x) { return x.ref === s.jogoRef; })[0];
var card = el("div", "sumula");
var cab = j ? j.mandante + " x " + j.visitante : "Jogo " + s.jogoRef;
if (j && j.placarMandante !== undefined && j.placarMandante !== null) {
cab = j.mandante + " " + j.placarMandante + " x " + j.placarVisitante + " " + j.visitante;
}
card.appendChild(el("h3", null, cab));
if (j) card.appendChild(el("div", "quando", (j.rodada ? j.rodada + " · " : "") + dataBr(j.data)));
secaoDaSumula(card, "Gols", s.gols, function (g) { return momento(g) + " · " + g.time + " · " + pessoa(g.jogador); });
secaoDaSumula(card, "Cartões", s.cartoes, function (c) {
return momento(c) + " · " + (c.tipo === "VERMELHO" ? "vermelho" : "amarelo") + " · " + c.time + " · " + pessoa(c.jogador);
});
secaoDaSumula(card, "Substituições", s.substituicoes, function (t) {
return momento(t) + " · " + t.time + " · sai " + pessoa(t.sai) + ", entra " + pessoa(t.entra);
});
alvo.appendChild(card);
});
}
// ------------------------------------------------------------------ ficha
function linhaDeJogador(j) {
var linha = el("div", "linha-ficha linha-jogador");
if (j && j.id) linha.setAttribute("data-id", j.id);
var nome = el("input");
nome.placeholder = "Nome do jogador";
nome.maxLength = 120;
nome.value = (j && j.nome) || "";
nome.setAttribute("aria-label", "Nome do jogador");
nome.setAttribute("data-campo", "nome");
var camisa = el("input");
camisa.type = "number";
camisa.min = "0";
camisa.max = "999";
camisa.placeholder = "nº";
camisa.value = j && j.numeroCamisa !== undefined && j.numeroCamisa !== null ? j.numeroCamisa : "";
camisa.setAttribute("aria-label", "Número da camisa");
camisa.setAttribute("data-campo", "camisa");
var tirar = el("button", "secundario", "×");
tirar.type = "button";
tirar.setAttribute("aria-label", "Remover jogador");
tirar.addEventListener("click", function () { linha.remove(); });
linha.appendChild(nome);
linha.appendChild(camisa);
linha.appendChild(tirar);
listaJogadores.appendChild(linha);
return linha;
}
function linhaDeComissao(m) {
var linha = el("div", "linha-ficha linha-comissao");
if (m && m.id) linha.setAttribute("data-id", m.id);
var nome = el("input");
nome.placeholder = "Nome";
nome.maxLength = 120;
nome.value = (m && m.nome) || "";
nome.setAttribute("aria-label", "Nome na comissão técnica");
nome.setAttribute("data-campo", "nome");
var papel = el("input");
papel.placeholder = "Função";
papel.maxLength = 40;
papel.value = (m && m.funcao) || "";
papel.setAttribute("list", "funcoes-comissao");
papel.setAttribute("aria-label", "Função na comissão técnica");
papel.setAttribute("data-campo", "funcao");
var tirar = el("button", "secundario", "×");
tirar.type = "button";
tirar.setAttribute("aria-label", "Remover da comissão");
tirar.addEventListener("click", function () { linha.remove(); });
linha.appendChild(nome);
linha.appendChild(papel);
linha.appendChild(tirar);
listaComissao.appendChild(linha);
return linha;
}
/** Time da escola ou ficha fechada: só leitura (listas simples). */
function fichaSomenteLeitura(ficha) {
formFicha.hidden = true;
var alvo = document.getElementById("ficha-leitura");
if (!alvo) {
alvo = el("div", "somente-leitura");
alvo.id = "ficha-leitura";
formFicha.parentNode.appendChild(alvo);
}
limpar(alvo);
alvo.appendChild(el("h3", null, "Jogadores"));
var ul = el("ul");
(ficha.jogadores || []).forEach(function (j) {
ul.appendChild(el("li", null, pessoa({ nome: j.nome, numeroCamisa: j.numeroCamisa })));
});
if (!ul.childNodes.length) ul.appendChild(el("li", null, "Nenhum jogador na ficha."));
alvo.appendChild(ul);
if ((ficha.comissao || []).length) {
alvo.appendChild(el("h3", null, "Comissão técnica"));
var uc = el("ul");
ficha.comissao.forEach(function (m) { uc.appendChild(el("li", null, m.nome + " — " + m.funcao)); });
alvo.appendChild(uc);
}
}
function montarFicha(ficha) {
var aviso = document.getElementById("ficha-aviso");
limpar(listaJogadores);
limpar(listaComissao);
if (!ficha.editavel) {
aviso.textContent = "Esta ficha está fechada para alterações aqui. Se algo estiver errado, fale com a organização.";
fichaSomenteLeitura(ficha);
return;
}
aviso.textContent = "Confira os jogadores (nome e número da camisa) e a comissão técnica. Dá para corrigir até o primeiro jogo ser encerrado.";
formFicha.hidden = false;
(ficha.jogadores || []).forEach(linhaDeJogador);
if (!(ficha.jogadores || []).length) for (var i = 0; i < 3; i++) linhaDeJogador();
(ficha.comissao || []).forEach(linhaDeComissao);
}
function mostrarErroFicha(texto) {
fichaErro.textContent = texto || "";
fichaErro.hidden = !texto;
}
function mostrarOkFicha(texto) {
fichaOk.textContent = texto || "";
fichaOk.hidden = !texto;
}
function corpoDaFicha() {
var jogadores = [];
var linhas = listaJogadores.querySelectorAll(".linha-jogador");
for (var i = 0; i < linhas.length; i++) {
var nome = linhas[i].querySelector('[data-campo="nome"]').value.trim();
if (!nome) continue;
var c = linhas[i].querySelector('[data-campo="camisa"]').value;
var j = { nome: nome, numeroCamisa: c === "" ? null : parseInt(c, 10) };
if (linhas[i].getAttribute("data-id")) j.id = linhas[i].getAttribute("data-id");
jogadores.push(j);
}
var comissao = [];
var faltaFuncao = false;
var pessoas = listaComissao.querySelectorAll(".linha-comissao");
for (var k = 0; k < pessoas.length; k++) {
var n = pessoas[k].querySelector('[data-campo="nome"]').value.trim();
if (!n) continue;
var f = pessoas[k].querySelector('[data-campo="funcao"]').value.trim();
if (!f) faltaFuncao = true;
var m = { nome: n, funcao: f };
if (pessoas[k].getAttribute("data-id")) m.id = pessoas[k].getAttribute("data-id");
comissao.push(m);
}
return { jogadores: jogadores, comissao: comissao, faltaFuncao: faltaFuncao };
}
function aoSalvar(ev) {
ev.preventDefault();
mostrarErroFicha("");
mostrarOkFicha("");
var c = corpoDaFicha();
if (c.jogadores.length > MAX_JOGADORES) { mostrarErroFicha("No máximo " + MAX_JOGADORES + " jogadores."); return; }
if (c.faltaFuncao) { mostrarErroFicha("Informe a função de cada pessoa da comissão técnica."); return; }
if (c.comissao.length > MAX_COMISSAO) { mostrarErroFicha("No máximo " + MAX_COMISSAO + " pessoas na comissão técnica."); return; }
salvar.disabled = true;
salvar.setAttribute("aria-busy", "true");
fetch(base + "/ficha", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ jogadores: c.jogadores, comissao: c.comissao })
}).then(lerResposta).then(function (r) {
salvar.disabled = false;
salvar.removeAttribute("aria-busy");
if (r.status >= 200 && r.status < 300) {
mostrarOkFicha("Ficha salva.");
return carregar(true);
}
if (r.status === 404) { recusar("Este link não está mais disponível."); return null; }
mostrarErroFicha((r.corpo && r.corpo.detail) || "Não foi possível salvar agora. Tente de novo em instantes.");
return null;
}).catch(function () {
salvar.disabled = false;
salvar.removeAttribute("aria-busy");
mostrarErroFicha("Sem conexão. Confira a internet e tente de novo.");
});
}
// ------------------------------------------------------------------ carga
function mostrar(v, manterAba) {
visao = v;
var nomeTime = (v.time && v.time.nome) || "Seu time";
titulo.textContent = (v.competicao || "Competição") + " — " + nomeTime;
document.title = titulo.textContent + " — Athletic Map";
var partes = [];
if (v.time && v.time.escola) partes.push(v.time.escola);
if (v.time && v.time.cidade) partes.push(v.time.cidade);
if (v.temporada && v.temporada.nome) partes.push("Temporada " + v.temporada.nome);
if (v.time && v.time.status === "DESQUALIFICADO") partes.push("Time desqualificado");
situacao.textContent = partes.join(" · ");
situacao.className = "";
painel.hidden = false;
montarTabela(v.classificacao, nomeTime);
montarJogos(v.jogos);
montarSumulas(v.sumulas, v.jogos);
montarFicha(v.ficha || { jogadores: [], comissao: [], editavel: false });
if (!manterAba) abrirAba("tabela", false);
}
function carregar(manterAba) {
return fetch(base).then(lerResposta).then(function (r) {
if (r.status === 404) { recusar("Este link não está disponível ou foi desativado. Peça um novo à organização."); return; }
if (r.status === 429) { recusar("Muitas consultas seguidas. Aguarde alguns minutos e abra o link de novo."); return; }
if (r.status !== 200) { recusar("Não foi possível abrir agora. Tente de novo em instantes."); return; }
mostrar(r.corpo || {}, manterAba);
}).catch(function () {
recusar("Não foi possível abrir agora. Tente de novo em instantes.");
});
}
function iniciar() {
if (!TOKEN.test(token)) { recusar("Este endereço não é válido."); return; }
ABAS.forEach(function (nome) {
var aba = document.getElementById("aba-" + nome);
aba.addEventListener("click", function () { abrirAba(nome, false); });
aba.addEventListener("keydown", aoTeclarNaAba);
});
document.getElementById("maisJogador").addEventListener("click", function () {
if (listaJogadores.querySelectorAll(".linha-jogador").length < MAX_JOGADORES) linhaDeJogador().querySelector("input").focus();
});
document.getElementById("maisComissao").addEventListener("click", function () {
if (listaComissao.querySelectorAll(".linha-comissao").length < MAX_COMISSAO) linhaDeComissao(null).querySelector("input").focus();
});
formFicha.addEventListener("submit", aoSalvar);
carregar(false);
}
iniciar();
})();
---
apiVersion: v1
kind: ConfigMap
+3
View File
@@ -153,6 +153,9 @@ spec:
- { key: "preparando.js", path: "preparando.js" }
- { key: "privacidade.html", path: "privacidade.html" }
- { key: "termos.html", path: "termos.html" }
- { key: "time.css", path: "time.css" }
- { key: "time.html", path: "time.html" }
- { key: "time.js", path: "time.js" }
- configMap:
name: frontend-publico-binario
items:
+619 -3
View File
@@ -409,7 +409,7 @@ data:
<form id="formulario" hidden novalidate>
<p class="explica">
Preencha os dados do seu time e dos jogadores (só o nome e o número da camisa).
Preencha os dados do seu time, dos jogadores (só o nome e o número da camisa) e da comissão técnica (nome e função).
A organização confere e aprova antes do time entrar na competição.
</p>
@@ -433,12 +433,23 @@ data:
<button type="button" id="maisJogador" class="secundario">+ Jogador</button>
</fieldset>
<fieldset>
<legend>Comissão técnica</legend>
<div id="comissao"></div>
<datalist id="funcoes-comissao">
<option value="Técnico"></option><option value="Auxiliar técnico"></option><option value="Preparador físico"></option>
<option value="Preparador de goleiros"></option><option value="Médico"></option><option value="Fisioterapeuta"></option>
<option value="Massagista"></option><option value="Roupeiro"></option><option value="Dirigente"></option>
</datalist>
<button type="button" id="maisComissao" class="secundario">+ Pessoa na comissão</button>
</fieldset>
<p id="erro" class="erro" role="alert" hidden></p>
<button type="submit" id="enviar">Enviar time</button>
</form>
</main>
<script src="/publico/desafio.js?v=1"></script>
<script src="/publico/convite.js?v=2"></script>
<script src="/publico/convite.js?v=3"></script>
</body>
</html>
@@ -465,6 +476,7 @@ data:
var TOKEN = /^[A-Za-z0-9_-]{20,64}$/;
var MAX_JOGADORES = 60;
var MAX_COMISSAO = 15;
var token = decodeURIComponent((location.pathname.split("/").filter(Boolean).pop() || ""));
var base = "/api/public/competicoes/convites/" + encodeURIComponent(token);
@@ -473,6 +485,7 @@ data:
var situacao = document.getElementById("situacao");
var formulario = document.getElementById("formulario");
var listaJogadores = document.getElementById("jogadores");
var listaComissao = document.getElementById("comissao");
var erro = document.getElementById("erro");
var enviar = document.getElementById("enviar");
var ROTULO_ENVIAR = enviar.textContent;
@@ -520,6 +533,39 @@ data:
return linha;
}
/** Comissao tecnica (21/10/2026): nome e funcao; a funcao tem sugestoes, mas aceita texto livre. */
function linhaDeComissao(funcao) {
var linha = document.createElement("div");
linha.className = "linha-jogador linha-comissao";
var nome = document.createElement("input");
nome.placeholder = "Nome";
nome.maxLength = 120;
nome.setAttribute("aria-label", "Nome na comissão técnica");
nome.setAttribute("data-campo", "nome");
var papel = document.createElement("input");
papel.placeholder = "Função";
papel.maxLength = 40;
papel.value = funcao || "";
papel.setAttribute("list", "funcoes-comissao");
papel.setAttribute("aria-label", "Função na comissão técnica");
papel.setAttribute("data-campo", "funcao");
var tirar = document.createElement("button");
tirar.type = "button";
tirar.className = "secundario";
tirar.textContent = "×";
tirar.setAttribute("aria-label", "Remover da comissão");
tirar.addEventListener("click", function () { linha.remove(); });
linha.appendChild(nome);
linha.appendChild(papel);
linha.appendChild(tirar);
listaComissao.appendChild(linha);
return linha;
}
function valor(id) {
var v = document.getElementById(id).value.trim();
return v === "" ? null : v;
@@ -534,14 +580,26 @@ data:
var c = linhas[i].querySelector('[data-campo="camisa"]').value;
jogadores.push({ nome: nome, numeroCamisa: c === "" ? null : parseInt(c, 10) });
}
var comissao = [];
var faltaFuncao = false;
var pessoas = listaComissao.querySelectorAll(".linha-comissao");
for (var k = 0; k < pessoas.length; k++) {
var n = pessoas[k].querySelector('[data-campo="nome"]').value.trim();
if (!n) continue;
var f = pessoas[k].querySelector('[data-campo="funcao"]').value.trim();
if (!f) faltaFuncao = true;
comissao.push({ nome: n, funcao: f });
}
return {
faltaFuncao: faltaFuncao,
nome: valor("nome"),
escola: valor("escola"),
cidade: valor("cidade"),
contatoNome: valor("contatoNome"),
contatoTelefone: valor("contatoTelefone"),
contatoEmail: valor("contatoEmail"),
jogadores: jogadores
jogadores: jogadores,
comissao: comissao
};
}
@@ -555,6 +613,9 @@ data:
var corpo = montarCorpo();
if (!corpo.nome) { mostrarErro("Informe o nome do time."); return; }
if (corpo.jogadores.length > MAX_JOGADORES) { mostrarErro("No máximo " + MAX_JOGADORES + " jogadores."); return; }
if (corpo.faltaFuncao) { mostrarErro("Informe a função de cada pessoa da comissão técnica."); return; }
if (corpo.comissao.length > MAX_COMISSAO) { mostrarErro("No máximo " + MAX_COMISSAO + " pessoas na comissão técnica."); return; }
delete corpo.faltaFuncao;
enviar.disabled = true;
enviar.textContent = "Verificando…";
enviar.setAttribute("aria-busy", "true");
@@ -615,6 +676,7 @@ data:
situacao.className = "";
formulario.hidden = false;
for (var i = 0; i < 5; i++) linhaDeJogador();
linhaDeComissao("Técnico");
}).catch(function () {
recusar("Não foi possível abrir o convite agora. Tente de novo em instantes.");
});
@@ -623,6 +685,9 @@ data:
document.getElementById("maisJogador").addEventListener("click", function () {
if (listaJogadores.querySelectorAll(".linha-jogador").length < MAX_JOGADORES) linhaDeJogador().querySelector("input").focus();
});
document.getElementById("maisComissao").addEventListener("click", function () {
if (listaComissao.querySelectorAll(".linha-comissao").length < MAX_COMISSAO) linhaDeComissao("").querySelector("input").focus();
});
formulario.addEventListener("submit", aoEnviar);
iniciar();
})();
@@ -8242,6 +8307,557 @@ data:
</body>
</html>
time.css: |
/* Link do time: página pública, sem fonte nem script de fora (a CSP só deixa 'self'). Mobile primeiro. */
main { max-width: 56rem; }
.explica, .legenda { color: var(--muted); font-size: .9rem; }
.abas { display: flex; gap: .25rem; overflow-x: auto; border-bottom: 2px solid var(--line); margin: 0 0 1rem; }
.abas button { background: transparent; color: var(--navy); border: 0; border-bottom: 3px solid transparent; border-radius: 0; padding: .6rem .9rem; white-space: nowrap; font: inherit; font-weight: 700; cursor: pointer; }
.abas button[aria-selected="true"] { border-bottom-color: var(--orange); }
.abas button:focus-visible, button:focus-visible, input:focus-visible { outline: 3px solid var(--orange); outline-offset: 1px; }
h2 { font-size: 1.15rem; margin: .5rem 0; }
.rolagem { overflow-x: auto; }
table { border-collapse: collapse; width: 100%; font-size: .9rem; background: #fff; }
th, td { padding: .4rem .45rem; text-align: center; border-bottom: 1px solid var(--line); white-space: nowrap; }
th:nth-child(2), td:nth-child(2) { text-align: left; white-space: normal; min-width: 8rem; }
tr.meu { background: #fff4e5; font-weight: 700; }
.forma { display: inline-flex; gap: 3px; vertical-align: middle; }
.ponto { width: .8rem; height: .8rem; border-radius: 50%; display: inline-block; }
.ponto.V { background: #067647; }
.ponto.E { background: #98a2b3; }
.ponto.D { background: #b42318; }
.cartao { display: inline-block; width: .7rem; height: .95rem; border-radius: 2px; vertical-align: middle; margin-right: 3px; }
.cartao.amarelo { background: #f5c518; }
.cartao.vermelho { background: #d92d20; }
.jogo, .sumula { background: #fff; border: 1px solid var(--line); border-radius: 8px; padding: .6rem .8rem; margin: 0 0 .6rem; }
.jogo .quando { color: var(--muted); font-size: .85rem; }
.jogo .confronto { display: grid; grid-template-columns: 1fr auto 1fr; gap: .5rem; align-items: center; font-weight: 700; }
.jogo .confronto span:first-child { text-align: right; }
.jogo .placar { background: var(--navy); color: #fff; border-radius: 6px; padding: .1rem .6rem; }
.etiqueta { display: inline-block; font-size: .75rem; font-weight: 700; padding: .05rem .5rem; border-radius: 99px; background: #fff1e3; border: 1px solid var(--orange); margin-left: .4rem; }
.sumula h3 { margin: 0 0 .3rem; font-size: 1rem; }
.sumula ul { margin: .2rem 0 .5rem; padding-left: 1.2rem; }
form { display: grid; gap: 1rem; }
fieldset { border: 1px solid var(--line); border-radius: 8px; background: #fff; padding: .75rem 1rem 1rem; display: grid; gap: .6rem; }
legend { font-weight: 700; padding: 0 .4rem; }
input { font: inherit; padding: .5rem .6rem; border: 1px solid var(--line); border-radius: 6px; width: 100%; }
.linha-ficha { display: grid; grid-template-columns: 1fr 5.5rem auto; gap: .4rem; align-items: center; }
.linha-ficha button { padding: .4rem .6rem; }
button { font: inherit; font-weight: 700; padding: .65rem 1.1rem; border: 0; border-radius: 8px; background: var(--orange); color: #fff; cursor: pointer; }
button.secundario { background: #fff; color: var(--navy); border: 1px solid var(--line); justify-self: start; }
button:disabled { opacity: .55; cursor: not-allowed; }
.erro { color: #b42318; margin: 0; font-weight: 600; }
.ok { background: #e7f6ec; border-left: 4px solid #067647; padding: .8rem 1rem; margin: 0; }
#situacao.invalido { background: #fff1e3; border-left: 4px solid var(--orange); padding: 1rem 1.2rem; }
.somente-leitura li { margin: .15rem 0; }
@media (max-width: 30rem) { .linha-ficha { grid-template-columns: 1fr 4.5rem auto; } }
time.html: |
<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow">
<meta name="referrer" content="no-referrer">
<title>Seu time na competição — Athletic Map</title>
<link rel="stylesheet" href="/publico/_estilo.css">
<link rel="stylesheet" href="/publico/time.css?v=1">
</head>
<body>
<header><div class="marca">Athletic <span>Map</span></div></header>
<main>
<h1 id="titulo">Seu time na competição</h1>
<p id="situacao" role="status" aria-live="polite">Abrindo o link…</p>
<div id="painel" hidden>
<div class="abas" role="tablist" aria-label="Seções">
<button type="button" role="tab" id="aba-tabela" aria-controls="sec-tabela" aria-selected="true" data-aba="tabela">Tabela</button>
<button type="button" role="tab" id="aba-jogos" aria-controls="sec-jogos" aria-selected="false" tabindex="-1" data-aba="jogos">Jogos</button>
<button type="button" role="tab" id="aba-sumulas" aria-controls="sec-sumulas" aria-selected="false" tabindex="-1" data-aba="sumulas">Súmulas</button>
<button type="button" role="tab" id="aba-ficha" aria-controls="sec-ficha" aria-selected="false" tabindex="-1" data-aba="ficha">Ficha do time</button>
</div>
<section id="sec-tabela" role="tabpanel" aria-labelledby="aba-tabela">
<h2>Tabela</h2>
<div class="rolagem" id="tabela"></div>
<p class="legenda">P pontos · J jogos · V vitórias · E empates · D derrotas · GP gols pró · GC gols contra · SG saldo de gols · % aproveitamento · CA cartões amarelos · CV cartões vermelhos</p>
</section>
<section id="sec-jogos" role="tabpanel" aria-labelledby="aba-jogos" hidden>
<h2>Jogos</h2>
<div id="jogos"></div>
</section>
<section id="sec-sumulas" role="tabpanel" aria-labelledby="aba-sumulas" hidden>
<h2>Súmulas</h2>
<div id="sumulas"></div>
</section>
<section id="sec-ficha" role="tabpanel" aria-labelledby="aba-ficha" hidden>
<h2>Ficha do time</h2>
<p id="ficha-aviso" class="explica"></p>
<form id="ficha" novalidate>
<fieldset>
<legend>Jogadores</legend>
<div id="jogadores"></div>
<button type="button" id="maisJogador" class="secundario">+ Jogador</button>
</fieldset>
<fieldset>
<legend>Comissão técnica</legend>
<div id="comissao"></div>
<datalist id="funcoes-comissao">
<option value="Técnico"></option><option value="Auxiliar técnico"></option><option value="Preparador físico"></option>
<option value="Preparador de goleiros"></option><option value="Médico"></option><option value="Fisioterapeuta"></option>
<option value="Massagista"></option><option value="Roupeiro"></option><option value="Dirigente"></option>
</datalist>
<button type="button" id="maisComissao" class="secundario">+ Pessoa na comissão</button>
</fieldset>
<p id="ficha-erro" class="erro" role="alert" hidden></p>
<p id="ficha-ok" class="ok" role="status" hidden></p>
<button type="submit" id="salvar">Salvar ficha</button>
</form>
</section>
</div>
</main>
<script src="/publico/time.js?v=1"></script>
</body>
</html>
time.js: |
/* ===========================================================================
* Link do time — a página que UMA pessoa do time abre pelo link com chave.
*
* Sem login: a chave no endereço é o único segredo. A página mostra a tabela,
* os jogos e as súmulas da competição e a ficha do time; se o time for de
* fora e ainda não houver jogo encerrado, a ficha pode ser corrigida aqui.
*
* Tudo o que vem do servidor entra na página com `textContent` — nunca como
* HTML. O script é externo porque a CSP é `script-src 'self'`.
* =========================================================================== */
(function () {
"use strict";
var TOKEN = /^[A-Za-z0-9_-]{20,64}$/;
var MAX_JOGADORES = 60;
var MAX_COMISSAO = 15;
var ABAS = ["tabela", "jogos", "sumulas", "ficha"];
var ROTULOS_FORMA = { V: "Vitória", E: "Empate", D: "Derrota" };
var ROTULOS_STATUS = {
AGENDADA: "Agendado", ADIADA: "Adiado", EM_ANDAMENTO: "Em andamento", ENCERRADA: "Encerrado", CANCELADA: "Cancelado"
};
var token = decodeURIComponent(location.pathname.split("/").filter(Boolean).pop() || "");
var base = "/api/public/competicoes/times/" + encodeURIComponent(token);
var titulo = document.getElementById("titulo");
var situacao = document.getElementById("situacao");
var painel = document.getElementById("painel");
var listaJogadores = document.getElementById("jogadores");
var listaComissao = document.getElementById("comissao");
var formFicha = document.getElementById("ficha");
var fichaErro = document.getElementById("ficha-erro");
var fichaOk = document.getElementById("ficha-ok");
var salvar = document.getElementById("salvar");
var visao = null;
function el(tag, classe, texto) {
var e = document.createElement(tag);
if (classe) e.className = classe;
if (texto !== undefined && texto !== null) e.textContent = String(texto);
return e;
}
function limpar(no) {
while (no.firstChild) no.removeChild(no.firstChild);
}
function recusar(texto) {
situacao.textContent = texto;
situacao.className = "invalido";
painel.hidden = true;
}
function lerResposta(r) {
return r.json().catch(function () { return {}; }).then(function (corpo) { return { status: r.status, corpo: corpo }; });
}
// ------------------------------------------------------------------ abas
function abrirAba(nome, focar) {
for (var i = 0; i < ABAS.length; i++) {
var ativa = ABAS[i] === nome;
var aba = document.getElementById("aba-" + ABAS[i]);
aba.setAttribute("aria-selected", ativa ? "true" : "false");
aba.tabIndex = ativa ? 0 : -1;
document.getElementById("sec-" + ABAS[i]).hidden = !ativa;
if (ativa && focar) aba.focus();
}
}
function aoTeclarNaAba(ev) {
var i = ABAS.indexOf(ev.currentTarget.getAttribute("data-aba"));
if (ev.key === "ArrowRight") { abrirAba(ABAS[(i + 1) % ABAS.length], true); ev.preventDefault(); }
else if (ev.key === "ArrowLeft") { abrirAba(ABAS[(i + ABAS.length - 1) % ABAS.length], true); ev.preventDefault(); }
}
// ------------------------------------------------------------------ tabela
function dataBr(iso) {
if (!iso) return "Data a definir";
var d = new Date(iso);
if (isNaN(d.getTime())) return "Data a definir";
try {
return d.toLocaleString("pt-BR", { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit" });
} catch (e) {
return d.toISOString();
}
}
function percentual(n) {
return (Math.round((n || 0) * 10) / 10).toString().replace(".", ",") + "%";
}
function montarTabela(linhas, meuNome) {
var alvo = document.getElementById("tabela");
limpar(alvo);
if (!linhas || !linhas.length) { alvo.appendChild(el("p", "explica", "A tabela aparece quando houver times e jogos.")); return; }
var t = el("table");
t.appendChild(el("caption", "explica", "Classificação"));
var cab = el("tr");
var cols = [["#", "Posição"], ["Time", "Time"], ["P", "Pontos"], ["J", "Jogos"], ["V", "Vitórias"], ["E", "Empates"],
["D", "Derrotas"], ["GP", "Gols pró"], ["GC", "Gols contra"], ["SG", "Saldo de gols"], ["%", "Aproveitamento"],
["Últimos 5", "Últimos 5 jogos"], ["CA", "Cartões amarelos"], ["CV", "Cartões vermelhos"]];
cols.forEach(function (c) {
var th = el("th", null, c[0]);
th.scope = "col";
th.title = c[1];
cab.appendChild(th);
});
var thead = el("thead");
thead.appendChild(cab);
t.appendChild(thead);
var corpo = el("tbody");
linhas.forEach(function (l) {
var tr = el("tr", l.time === meuNome ? "meu" : "");
[l.posicao, l.time, l.pontos, l.jogos, l.vitorias, l.empates, l.derrotas, l.golsPro, l.golsContra, l.saldoGols,
percentual(l.aproveitamento)].forEach(function (v) { tr.appendChild(el("td", null, v)); });
var forma = el("td");
var caixa = el("span", "forma");
(l.ultimos || []).forEach(function (r) {
var p = el("span", "ponto " + r);
p.title = ROTULOS_FORMA[r] || r;
p.setAttribute("role", "img");
p.setAttribute("aria-label", ROTULOS_FORMA[r] || r);
caixa.appendChild(p);
});
forma.appendChild(caixa);
tr.appendChild(forma);
var am = el("td");
am.appendChild(el("span", "cartao amarelo"));
am.appendChild(document.createTextNode(String(l.amarelos || 0)));
tr.appendChild(am);
var vm = el("td");
vm.appendChild(el("span", "cartao vermelho"));
vm.appendChild(document.createTextNode(String(l.vermelhos || 0)));
tr.appendChild(vm);
corpo.appendChild(tr);
});
t.appendChild(corpo);
alvo.appendChild(t);
}
// ------------------------------------------------------------------ jogos
function montarJogos(jogos) {
var alvo = document.getElementById("jogos");
limpar(alvo);
if (!jogos || !jogos.length) { alvo.appendChild(el("p", "explica", "Ainda não há jogos marcados.")); return; }
jogos.forEach(function (j) {
var card = el("div", "jogo");
var quando = el("div", "quando", (j.rodada ? j.rodada + " · " : "") + dataBr(j.data));
var etiqueta = el("span", "etiqueta", ROTULOS_STATUS[j.status] || j.status);
quando.appendChild(etiqueta);
if (j.wo) quando.appendChild(el("span", "etiqueta", "W.O."));
card.appendChild(quando);
var linha = el("div", "confronto");
linha.appendChild(el("span", null, j.mandante));
var temPlacar = j.placarMandante !== undefined && j.placarMandante !== null;
linha.appendChild(el("span", temPlacar ? "placar" : "", temPlacar ? j.placarMandante + " x " + j.placarVisitante : "x"));
linha.appendChild(el("span", null, j.visitante));
card.appendChild(linha);
if (j.penaltis) card.appendChild(el("div", "quando", "Pênaltis: " + j.penaltis.mandante + " x " + j.penaltis.visitante));
if (j.motivo) card.appendChild(el("div", "quando", "Motivo: " + j.motivo));
alvo.appendChild(card);
});
}
// ------------------------------------------------------------------ súmulas
function pessoa(p) {
if (!p) return "jogador não identificado";
if (p.nome && p.numeroCamisa !== undefined && p.numeroCamisa !== null) return p.nome + " (" + p.numeroCamisa + ")";
if (p.nome) return p.nome;
if (p.numeroCamisa !== undefined && p.numeroCamisa !== null) return "camisa " + p.numeroCamisa;
return "jogador não identificado";
}
function momento(e) {
var m = e.minuto !== undefined && e.minuto !== null ? e.minuto + "'" : "";
var t = e.tempo ? e.tempo + "º tempo" : "";
return [t, m].filter(Boolean).join(" ") || "—";
}
function secaoDaSumula(card, nome, itens, texto) {
if (!itens || !itens.length) return;
card.appendChild(el("strong", null, nome));
var ul = el("ul");
itens.forEach(function (i) { ul.appendChild(el("li", null, texto(i))); });
card.appendChild(ul);
}
function montarSumulas(sumulas, jogos) {
var alvo = document.getElementById("sumulas");
limpar(alvo);
if (!sumulas || !sumulas.length) { alvo.appendChild(el("p", "explica", "As súmulas dos jogos do seu time aparecem aqui.")); return; }
sumulas.forEach(function (s) {
var j = (jogos || []).filter(function (x) { return x.ref === s.jogoRef; })[0];
var card = el("div", "sumula");
var cab = j ? j.mandante + " x " + j.visitante : "Jogo " + s.jogoRef;
if (j && j.placarMandante !== undefined && j.placarMandante !== null) {
cab = j.mandante + " " + j.placarMandante + " x " + j.placarVisitante + " " + j.visitante;
}
card.appendChild(el("h3", null, cab));
if (j) card.appendChild(el("div", "quando", (j.rodada ? j.rodada + " · " : "") + dataBr(j.data)));
secaoDaSumula(card, "Gols", s.gols, function (g) { return momento(g) + " · " + g.time + " · " + pessoa(g.jogador); });
secaoDaSumula(card, "Cartões", s.cartoes, function (c) {
return momento(c) + " · " + (c.tipo === "VERMELHO" ? "vermelho" : "amarelo") + " · " + c.time + " · " + pessoa(c.jogador);
});
secaoDaSumula(card, "Substituições", s.substituicoes, function (t) {
return momento(t) + " · " + t.time + " · sai " + pessoa(t.sai) + ", entra " + pessoa(t.entra);
});
alvo.appendChild(card);
});
}
// ------------------------------------------------------------------ ficha
function linhaDeJogador(j) {
var linha = el("div", "linha-ficha linha-jogador");
if (j && j.id) linha.setAttribute("data-id", j.id);
var nome = el("input");
nome.placeholder = "Nome do jogador";
nome.maxLength = 120;
nome.value = (j && j.nome) || "";
nome.setAttribute("aria-label", "Nome do jogador");
nome.setAttribute("data-campo", "nome");
var camisa = el("input");
camisa.type = "number";
camisa.min = "0";
camisa.max = "999";
camisa.placeholder = "nº";
camisa.value = j && j.numeroCamisa !== undefined && j.numeroCamisa !== null ? j.numeroCamisa : "";
camisa.setAttribute("aria-label", "Número da camisa");
camisa.setAttribute("data-campo", "camisa");
var tirar = el("button", "secundario", "×");
tirar.type = "button";
tirar.setAttribute("aria-label", "Remover jogador");
tirar.addEventListener("click", function () { linha.remove(); });
linha.appendChild(nome);
linha.appendChild(camisa);
linha.appendChild(tirar);
listaJogadores.appendChild(linha);
return linha;
}
function linhaDeComissao(m) {
var linha = el("div", "linha-ficha linha-comissao");
if (m && m.id) linha.setAttribute("data-id", m.id);
var nome = el("input");
nome.placeholder = "Nome";
nome.maxLength = 120;
nome.value = (m && m.nome) || "";
nome.setAttribute("aria-label", "Nome na comissão técnica");
nome.setAttribute("data-campo", "nome");
var papel = el("input");
papel.placeholder = "Função";
papel.maxLength = 40;
papel.value = (m && m.funcao) || "";
papel.setAttribute("list", "funcoes-comissao");
papel.setAttribute("aria-label", "Função na comissão técnica");
papel.setAttribute("data-campo", "funcao");
var tirar = el("button", "secundario", "×");
tirar.type = "button";
tirar.setAttribute("aria-label", "Remover da comissão");
tirar.addEventListener("click", function () { linha.remove(); });
linha.appendChild(nome);
linha.appendChild(papel);
linha.appendChild(tirar);
listaComissao.appendChild(linha);
return linha;
}
/** Time da escola ou ficha fechada: só leitura (listas simples). */
function fichaSomenteLeitura(ficha) {
formFicha.hidden = true;
var alvo = document.getElementById("ficha-leitura");
if (!alvo) {
alvo = el("div", "somente-leitura");
alvo.id = "ficha-leitura";
formFicha.parentNode.appendChild(alvo);
}
limpar(alvo);
alvo.appendChild(el("h3", null, "Jogadores"));
var ul = el("ul");
(ficha.jogadores || []).forEach(function (j) {
ul.appendChild(el("li", null, pessoa({ nome: j.nome, numeroCamisa: j.numeroCamisa })));
});
if (!ul.childNodes.length) ul.appendChild(el("li", null, "Nenhum jogador na ficha."));
alvo.appendChild(ul);
if ((ficha.comissao || []).length) {
alvo.appendChild(el("h3", null, "Comissão técnica"));
var uc = el("ul");
ficha.comissao.forEach(function (m) { uc.appendChild(el("li", null, m.nome + " — " + m.funcao)); });
alvo.appendChild(uc);
}
}
function montarFicha(ficha) {
var aviso = document.getElementById("ficha-aviso");
limpar(listaJogadores);
limpar(listaComissao);
if (!ficha.editavel) {
aviso.textContent = "Esta ficha está fechada para alterações aqui. Se algo estiver errado, fale com a organização.";
fichaSomenteLeitura(ficha);
return;
}
aviso.textContent = "Confira os jogadores (nome e número da camisa) e a comissão técnica. Dá para corrigir até o primeiro jogo ser encerrado.";
formFicha.hidden = false;
(ficha.jogadores || []).forEach(linhaDeJogador);
if (!(ficha.jogadores || []).length) for (var i = 0; i < 3; i++) linhaDeJogador();
(ficha.comissao || []).forEach(linhaDeComissao);
}
function mostrarErroFicha(texto) {
fichaErro.textContent = texto || "";
fichaErro.hidden = !texto;
}
function mostrarOkFicha(texto) {
fichaOk.textContent = texto || "";
fichaOk.hidden = !texto;
}
function corpoDaFicha() {
var jogadores = [];
var linhas = listaJogadores.querySelectorAll(".linha-jogador");
for (var i = 0; i < linhas.length; i++) {
var nome = linhas[i].querySelector('[data-campo="nome"]').value.trim();
if (!nome) continue;
var c = linhas[i].querySelector('[data-campo="camisa"]').value;
var j = { nome: nome, numeroCamisa: c === "" ? null : parseInt(c, 10) };
if (linhas[i].getAttribute("data-id")) j.id = linhas[i].getAttribute("data-id");
jogadores.push(j);
}
var comissao = [];
var faltaFuncao = false;
var pessoas = listaComissao.querySelectorAll(".linha-comissao");
for (var k = 0; k < pessoas.length; k++) {
var n = pessoas[k].querySelector('[data-campo="nome"]').value.trim();
if (!n) continue;
var f = pessoas[k].querySelector('[data-campo="funcao"]').value.trim();
if (!f) faltaFuncao = true;
var m = { nome: n, funcao: f };
if (pessoas[k].getAttribute("data-id")) m.id = pessoas[k].getAttribute("data-id");
comissao.push(m);
}
return { jogadores: jogadores, comissao: comissao, faltaFuncao: faltaFuncao };
}
function aoSalvar(ev) {
ev.preventDefault();
mostrarErroFicha("");
mostrarOkFicha("");
var c = corpoDaFicha();
if (c.jogadores.length > MAX_JOGADORES) { mostrarErroFicha("No máximo " + MAX_JOGADORES + " jogadores."); return; }
if (c.faltaFuncao) { mostrarErroFicha("Informe a função de cada pessoa da comissão técnica."); return; }
if (c.comissao.length > MAX_COMISSAO) { mostrarErroFicha("No máximo " + MAX_COMISSAO + " pessoas na comissão técnica."); return; }
salvar.disabled = true;
salvar.setAttribute("aria-busy", "true");
fetch(base + "/ficha", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ jogadores: c.jogadores, comissao: c.comissao })
}).then(lerResposta).then(function (r) {
salvar.disabled = false;
salvar.removeAttribute("aria-busy");
if (r.status >= 200 && r.status < 300) {
mostrarOkFicha("Ficha salva.");
return carregar(true);
}
if (r.status === 404) { recusar("Este link não está mais disponível."); return null; }
mostrarErroFicha((r.corpo && r.corpo.detail) || "Não foi possível salvar agora. Tente de novo em instantes.");
return null;
}).catch(function () {
salvar.disabled = false;
salvar.removeAttribute("aria-busy");
mostrarErroFicha("Sem conexão. Confira a internet e tente de novo.");
});
}
// ------------------------------------------------------------------ carga
function mostrar(v, manterAba) {
visao = v;
var nomeTime = (v.time && v.time.nome) || "Seu time";
titulo.textContent = (v.competicao || "Competição") + " — " + nomeTime;
document.title = titulo.textContent + " — Athletic Map";
var partes = [];
if (v.time && v.time.escola) partes.push(v.time.escola);
if (v.time && v.time.cidade) partes.push(v.time.cidade);
if (v.temporada && v.temporada.nome) partes.push("Temporada " + v.temporada.nome);
if (v.time && v.time.status === "DESQUALIFICADO") partes.push("Time desqualificado");
situacao.textContent = partes.join(" · ");
situacao.className = "";
painel.hidden = false;
montarTabela(v.classificacao, nomeTime);
montarJogos(v.jogos);
montarSumulas(v.sumulas, v.jogos);
montarFicha(v.ficha || { jogadores: [], comissao: [], editavel: false });
if (!manterAba) abrirAba("tabela", false);
}
function carregar(manterAba) {
return fetch(base).then(lerResposta).then(function (r) {
if (r.status === 404) { recusar("Este link não está disponível ou foi desativado. Peça um novo à organização."); return; }
if (r.status === 429) { recusar("Muitas consultas seguidas. Aguarde alguns minutos e abra o link de novo."); return; }
if (r.status !== 200) { recusar("Não foi possível abrir agora. Tente de novo em instantes."); return; }
mostrar(r.corpo || {}, manterAba);
}).catch(function () {
recusar("Não foi possível abrir agora. Tente de novo em instantes.");
});
}
function iniciar() {
if (!TOKEN.test(token)) { recusar("Este endereço não é válido."); return; }
ABAS.forEach(function (nome) {
var aba = document.getElementById("aba-" + nome);
aba.addEventListener("click", function () { abrirAba(nome, false); });
aba.addEventListener("keydown", aoTeclarNaAba);
});
document.getElementById("maisJogador").addEventListener("click", function () {
if (listaJogadores.querySelectorAll(".linha-jogador").length < MAX_JOGADORES) linhaDeJogador().querySelector("input").focus();
});
document.getElementById("maisComissao").addEventListener("click", function () {
if (listaComissao.querySelectorAll(".linha-comissao").length < MAX_COMISSAO) linhaDeComissao(null).querySelector("input").focus();
});
formFicha.addEventListener("submit", aoSalvar);
carregar(false);
}
iniciar();
})();
---
apiVersion: v1
kind: ConfigMap
+3
View File
@@ -150,6 +150,9 @@ spec:
- { key: "preparando.js", path: "preparando.js" }
- { key: "privacidade.html", path: "privacidade.html" }
- { key: "termos.html", path: "termos.html" }
- { key: "time.css", path: "time.css" }
- { key: "time.html", path: "time.html" }
- { key: "time.js", path: "time.js" }
- configMap:
name: frontend-publico-binario
items:
+619 -3
View File
@@ -409,7 +409,7 @@ data:
<form id="formulario" hidden novalidate>
<p class="explica">
Preencha os dados do seu time e dos jogadores (só o nome e o número da camisa).
Preencha os dados do seu time, dos jogadores (só o nome e o número da camisa) e da comissão técnica (nome e função).
A organização confere e aprova antes do time entrar na competição.
</p>
@@ -433,12 +433,23 @@ data:
<button type="button" id="maisJogador" class="secundario">+ Jogador</button>
</fieldset>
<fieldset>
<legend>Comissão técnica</legend>
<div id="comissao"></div>
<datalist id="funcoes-comissao">
<option value="Técnico"></option><option value="Auxiliar técnico"></option><option value="Preparador físico"></option>
<option value="Preparador de goleiros"></option><option value="Médico"></option><option value="Fisioterapeuta"></option>
<option value="Massagista"></option><option value="Roupeiro"></option><option value="Dirigente"></option>
</datalist>
<button type="button" id="maisComissao" class="secundario">+ Pessoa na comissão</button>
</fieldset>
<p id="erro" class="erro" role="alert" hidden></p>
<button type="submit" id="enviar">Enviar time</button>
</form>
</main>
<script src="/publico/desafio.js?v=1"></script>
<script src="/publico/convite.js?v=2"></script>
<script src="/publico/convite.js?v=3"></script>
</body>
</html>
@@ -465,6 +476,7 @@ data:
var TOKEN = /^[A-Za-z0-9_-]{20,64}$/;
var MAX_JOGADORES = 60;
var MAX_COMISSAO = 15;
var token = decodeURIComponent((location.pathname.split("/").filter(Boolean).pop() || ""));
var base = "/api/public/competicoes/convites/" + encodeURIComponent(token);
@@ -473,6 +485,7 @@ data:
var situacao = document.getElementById("situacao");
var formulario = document.getElementById("formulario");
var listaJogadores = document.getElementById("jogadores");
var listaComissao = document.getElementById("comissao");
var erro = document.getElementById("erro");
var enviar = document.getElementById("enviar");
var ROTULO_ENVIAR = enviar.textContent;
@@ -520,6 +533,39 @@ data:
return linha;
}
/** Comissao tecnica (21/10/2026): nome e funcao; a funcao tem sugestoes, mas aceita texto livre. */
function linhaDeComissao(funcao) {
var linha = document.createElement("div");
linha.className = "linha-jogador linha-comissao";
var nome = document.createElement("input");
nome.placeholder = "Nome";
nome.maxLength = 120;
nome.setAttribute("aria-label", "Nome na comissão técnica");
nome.setAttribute("data-campo", "nome");
var papel = document.createElement("input");
papel.placeholder = "Função";
papel.maxLength = 40;
papel.value = funcao || "";
papel.setAttribute("list", "funcoes-comissao");
papel.setAttribute("aria-label", "Função na comissão técnica");
papel.setAttribute("data-campo", "funcao");
var tirar = document.createElement("button");
tirar.type = "button";
tirar.className = "secundario";
tirar.textContent = "×";
tirar.setAttribute("aria-label", "Remover da comissão");
tirar.addEventListener("click", function () { linha.remove(); });
linha.appendChild(nome);
linha.appendChild(papel);
linha.appendChild(tirar);
listaComissao.appendChild(linha);
return linha;
}
function valor(id) {
var v = document.getElementById(id).value.trim();
return v === "" ? null : v;
@@ -534,14 +580,26 @@ data:
var c = linhas[i].querySelector('[data-campo="camisa"]').value;
jogadores.push({ nome: nome, numeroCamisa: c === "" ? null : parseInt(c, 10) });
}
var comissao = [];
var faltaFuncao = false;
var pessoas = listaComissao.querySelectorAll(".linha-comissao");
for (var k = 0; k < pessoas.length; k++) {
var n = pessoas[k].querySelector('[data-campo="nome"]').value.trim();
if (!n) continue;
var f = pessoas[k].querySelector('[data-campo="funcao"]').value.trim();
if (!f) faltaFuncao = true;
comissao.push({ nome: n, funcao: f });
}
return {
faltaFuncao: faltaFuncao,
nome: valor("nome"),
escola: valor("escola"),
cidade: valor("cidade"),
contatoNome: valor("contatoNome"),
contatoTelefone: valor("contatoTelefone"),
contatoEmail: valor("contatoEmail"),
jogadores: jogadores
jogadores: jogadores,
comissao: comissao
};
}
@@ -555,6 +613,9 @@ data:
var corpo = montarCorpo();
if (!corpo.nome) { mostrarErro("Informe o nome do time."); return; }
if (corpo.jogadores.length > MAX_JOGADORES) { mostrarErro("No máximo " + MAX_JOGADORES + " jogadores."); return; }
if (corpo.faltaFuncao) { mostrarErro("Informe a função de cada pessoa da comissão técnica."); return; }
if (corpo.comissao.length > MAX_COMISSAO) { mostrarErro("No máximo " + MAX_COMISSAO + " pessoas na comissão técnica."); return; }
delete corpo.faltaFuncao;
enviar.disabled = true;
enviar.textContent = "Verificando…";
enviar.setAttribute("aria-busy", "true");
@@ -615,6 +676,7 @@ data:
situacao.className = "";
formulario.hidden = false;
for (var i = 0; i < 5; i++) linhaDeJogador();
linhaDeComissao("Técnico");
}).catch(function () {
recusar("Não foi possível abrir o convite agora. Tente de novo em instantes.");
});
@@ -623,6 +685,9 @@ data:
document.getElementById("maisJogador").addEventListener("click", function () {
if (listaJogadores.querySelectorAll(".linha-jogador").length < MAX_JOGADORES) linhaDeJogador().querySelector("input").focus();
});
document.getElementById("maisComissao").addEventListener("click", function () {
if (listaComissao.querySelectorAll(".linha-comissao").length < MAX_COMISSAO) linhaDeComissao("").querySelector("input").focus();
});
formulario.addEventListener("submit", aoEnviar);
iniciar();
})();
@@ -8242,6 +8307,557 @@ data:
</body>
</html>
time.css: |
/* Link do time: página pública, sem fonte nem script de fora (a CSP só deixa 'self'). Mobile primeiro. */
main { max-width: 56rem; }
.explica, .legenda { color: var(--muted); font-size: .9rem; }
.abas { display: flex; gap: .25rem; overflow-x: auto; border-bottom: 2px solid var(--line); margin: 0 0 1rem; }
.abas button { background: transparent; color: var(--navy); border: 0; border-bottom: 3px solid transparent; border-radius: 0; padding: .6rem .9rem; white-space: nowrap; font: inherit; font-weight: 700; cursor: pointer; }
.abas button[aria-selected="true"] { border-bottom-color: var(--orange); }
.abas button:focus-visible, button:focus-visible, input:focus-visible { outline: 3px solid var(--orange); outline-offset: 1px; }
h2 { font-size: 1.15rem; margin: .5rem 0; }
.rolagem { overflow-x: auto; }
table { border-collapse: collapse; width: 100%; font-size: .9rem; background: #fff; }
th, td { padding: .4rem .45rem; text-align: center; border-bottom: 1px solid var(--line); white-space: nowrap; }
th:nth-child(2), td:nth-child(2) { text-align: left; white-space: normal; min-width: 8rem; }
tr.meu { background: #fff4e5; font-weight: 700; }
.forma { display: inline-flex; gap: 3px; vertical-align: middle; }
.ponto { width: .8rem; height: .8rem; border-radius: 50%; display: inline-block; }
.ponto.V { background: #067647; }
.ponto.E { background: #98a2b3; }
.ponto.D { background: #b42318; }
.cartao { display: inline-block; width: .7rem; height: .95rem; border-radius: 2px; vertical-align: middle; margin-right: 3px; }
.cartao.amarelo { background: #f5c518; }
.cartao.vermelho { background: #d92d20; }
.jogo, .sumula { background: #fff; border: 1px solid var(--line); border-radius: 8px; padding: .6rem .8rem; margin: 0 0 .6rem; }
.jogo .quando { color: var(--muted); font-size: .85rem; }
.jogo .confronto { display: grid; grid-template-columns: 1fr auto 1fr; gap: .5rem; align-items: center; font-weight: 700; }
.jogo .confronto span:first-child { text-align: right; }
.jogo .placar { background: var(--navy); color: #fff; border-radius: 6px; padding: .1rem .6rem; }
.etiqueta { display: inline-block; font-size: .75rem; font-weight: 700; padding: .05rem .5rem; border-radius: 99px; background: #fff1e3; border: 1px solid var(--orange); margin-left: .4rem; }
.sumula h3 { margin: 0 0 .3rem; font-size: 1rem; }
.sumula ul { margin: .2rem 0 .5rem; padding-left: 1.2rem; }
form { display: grid; gap: 1rem; }
fieldset { border: 1px solid var(--line); border-radius: 8px; background: #fff; padding: .75rem 1rem 1rem; display: grid; gap: .6rem; }
legend { font-weight: 700; padding: 0 .4rem; }
input { font: inherit; padding: .5rem .6rem; border: 1px solid var(--line); border-radius: 6px; width: 100%; }
.linha-ficha { display: grid; grid-template-columns: 1fr 5.5rem auto; gap: .4rem; align-items: center; }
.linha-ficha button { padding: .4rem .6rem; }
button { font: inherit; font-weight: 700; padding: .65rem 1.1rem; border: 0; border-radius: 8px; background: var(--orange); color: #fff; cursor: pointer; }
button.secundario { background: #fff; color: var(--navy); border: 1px solid var(--line); justify-self: start; }
button:disabled { opacity: .55; cursor: not-allowed; }
.erro { color: #b42318; margin: 0; font-weight: 600; }
.ok { background: #e7f6ec; border-left: 4px solid #067647; padding: .8rem 1rem; margin: 0; }
#situacao.invalido { background: #fff1e3; border-left: 4px solid var(--orange); padding: 1rem 1.2rem; }
.somente-leitura li { margin: .15rem 0; }
@media (max-width: 30rem) { .linha-ficha { grid-template-columns: 1fr 4.5rem auto; } }
time.html: |
<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow">
<meta name="referrer" content="no-referrer">
<title>Seu time na competição — Athletic Map</title>
<link rel="stylesheet" href="/publico/_estilo.css">
<link rel="stylesheet" href="/publico/time.css?v=1">
</head>
<body>
<header><div class="marca">Athletic <span>Map</span></div></header>
<main>
<h1 id="titulo">Seu time na competição</h1>
<p id="situacao" role="status" aria-live="polite">Abrindo o link…</p>
<div id="painel" hidden>
<div class="abas" role="tablist" aria-label="Seções">
<button type="button" role="tab" id="aba-tabela" aria-controls="sec-tabela" aria-selected="true" data-aba="tabela">Tabela</button>
<button type="button" role="tab" id="aba-jogos" aria-controls="sec-jogos" aria-selected="false" tabindex="-1" data-aba="jogos">Jogos</button>
<button type="button" role="tab" id="aba-sumulas" aria-controls="sec-sumulas" aria-selected="false" tabindex="-1" data-aba="sumulas">Súmulas</button>
<button type="button" role="tab" id="aba-ficha" aria-controls="sec-ficha" aria-selected="false" tabindex="-1" data-aba="ficha">Ficha do time</button>
</div>
<section id="sec-tabela" role="tabpanel" aria-labelledby="aba-tabela">
<h2>Tabela</h2>
<div class="rolagem" id="tabela"></div>
<p class="legenda">P pontos · J jogos · V vitórias · E empates · D derrotas · GP gols pró · GC gols contra · SG saldo de gols · % aproveitamento · CA cartões amarelos · CV cartões vermelhos</p>
</section>
<section id="sec-jogos" role="tabpanel" aria-labelledby="aba-jogos" hidden>
<h2>Jogos</h2>
<div id="jogos"></div>
</section>
<section id="sec-sumulas" role="tabpanel" aria-labelledby="aba-sumulas" hidden>
<h2>Súmulas</h2>
<div id="sumulas"></div>
</section>
<section id="sec-ficha" role="tabpanel" aria-labelledby="aba-ficha" hidden>
<h2>Ficha do time</h2>
<p id="ficha-aviso" class="explica"></p>
<form id="ficha" novalidate>
<fieldset>
<legend>Jogadores</legend>
<div id="jogadores"></div>
<button type="button" id="maisJogador" class="secundario">+ Jogador</button>
</fieldset>
<fieldset>
<legend>Comissão técnica</legend>
<div id="comissao"></div>
<datalist id="funcoes-comissao">
<option value="Técnico"></option><option value="Auxiliar técnico"></option><option value="Preparador físico"></option>
<option value="Preparador de goleiros"></option><option value="Médico"></option><option value="Fisioterapeuta"></option>
<option value="Massagista"></option><option value="Roupeiro"></option><option value="Dirigente"></option>
</datalist>
<button type="button" id="maisComissao" class="secundario">+ Pessoa na comissão</button>
</fieldset>
<p id="ficha-erro" class="erro" role="alert" hidden></p>
<p id="ficha-ok" class="ok" role="status" hidden></p>
<button type="submit" id="salvar">Salvar ficha</button>
</form>
</section>
</div>
</main>
<script src="/publico/time.js?v=1"></script>
</body>
</html>
time.js: |
/* ===========================================================================
* Link do time — a página que UMA pessoa do time abre pelo link com chave.
*
* Sem login: a chave no endereço é o único segredo. A página mostra a tabela,
* os jogos e as súmulas da competição e a ficha do time; se o time for de
* fora e ainda não houver jogo encerrado, a ficha pode ser corrigida aqui.
*
* Tudo o que vem do servidor entra na página com `textContent` — nunca como
* HTML. O script é externo porque a CSP é `script-src 'self'`.
* =========================================================================== */
(function () {
"use strict";
var TOKEN = /^[A-Za-z0-9_-]{20,64}$/;
var MAX_JOGADORES = 60;
var MAX_COMISSAO = 15;
var ABAS = ["tabela", "jogos", "sumulas", "ficha"];
var ROTULOS_FORMA = { V: "Vitória", E: "Empate", D: "Derrota" };
var ROTULOS_STATUS = {
AGENDADA: "Agendado", ADIADA: "Adiado", EM_ANDAMENTO: "Em andamento", ENCERRADA: "Encerrado", CANCELADA: "Cancelado"
};
var token = decodeURIComponent(location.pathname.split("/").filter(Boolean).pop() || "");
var base = "/api/public/competicoes/times/" + encodeURIComponent(token);
var titulo = document.getElementById("titulo");
var situacao = document.getElementById("situacao");
var painel = document.getElementById("painel");
var listaJogadores = document.getElementById("jogadores");
var listaComissao = document.getElementById("comissao");
var formFicha = document.getElementById("ficha");
var fichaErro = document.getElementById("ficha-erro");
var fichaOk = document.getElementById("ficha-ok");
var salvar = document.getElementById("salvar");
var visao = null;
function el(tag, classe, texto) {
var e = document.createElement(tag);
if (classe) e.className = classe;
if (texto !== undefined && texto !== null) e.textContent = String(texto);
return e;
}
function limpar(no) {
while (no.firstChild) no.removeChild(no.firstChild);
}
function recusar(texto) {
situacao.textContent = texto;
situacao.className = "invalido";
painel.hidden = true;
}
function lerResposta(r) {
return r.json().catch(function () { return {}; }).then(function (corpo) { return { status: r.status, corpo: corpo }; });
}
// ------------------------------------------------------------------ abas
function abrirAba(nome, focar) {
for (var i = 0; i < ABAS.length; i++) {
var ativa = ABAS[i] === nome;
var aba = document.getElementById("aba-" + ABAS[i]);
aba.setAttribute("aria-selected", ativa ? "true" : "false");
aba.tabIndex = ativa ? 0 : -1;
document.getElementById("sec-" + ABAS[i]).hidden = !ativa;
if (ativa && focar) aba.focus();
}
}
function aoTeclarNaAba(ev) {
var i = ABAS.indexOf(ev.currentTarget.getAttribute("data-aba"));
if (ev.key === "ArrowRight") { abrirAba(ABAS[(i + 1) % ABAS.length], true); ev.preventDefault(); }
else if (ev.key === "ArrowLeft") { abrirAba(ABAS[(i + ABAS.length - 1) % ABAS.length], true); ev.preventDefault(); }
}
// ------------------------------------------------------------------ tabela
function dataBr(iso) {
if (!iso) return "Data a definir";
var d = new Date(iso);
if (isNaN(d.getTime())) return "Data a definir";
try {
return d.toLocaleString("pt-BR", { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit" });
} catch (e) {
return d.toISOString();
}
}
function percentual(n) {
return (Math.round((n || 0) * 10) / 10).toString().replace(".", ",") + "%";
}
function montarTabela(linhas, meuNome) {
var alvo = document.getElementById("tabela");
limpar(alvo);
if (!linhas || !linhas.length) { alvo.appendChild(el("p", "explica", "A tabela aparece quando houver times e jogos.")); return; }
var t = el("table");
t.appendChild(el("caption", "explica", "Classificação"));
var cab = el("tr");
var cols = [["#", "Posição"], ["Time", "Time"], ["P", "Pontos"], ["J", "Jogos"], ["V", "Vitórias"], ["E", "Empates"],
["D", "Derrotas"], ["GP", "Gols pró"], ["GC", "Gols contra"], ["SG", "Saldo de gols"], ["%", "Aproveitamento"],
["Últimos 5", "Últimos 5 jogos"], ["CA", "Cartões amarelos"], ["CV", "Cartões vermelhos"]];
cols.forEach(function (c) {
var th = el("th", null, c[0]);
th.scope = "col";
th.title = c[1];
cab.appendChild(th);
});
var thead = el("thead");
thead.appendChild(cab);
t.appendChild(thead);
var corpo = el("tbody");
linhas.forEach(function (l) {
var tr = el("tr", l.time === meuNome ? "meu" : "");
[l.posicao, l.time, l.pontos, l.jogos, l.vitorias, l.empates, l.derrotas, l.golsPro, l.golsContra, l.saldoGols,
percentual(l.aproveitamento)].forEach(function (v) { tr.appendChild(el("td", null, v)); });
var forma = el("td");
var caixa = el("span", "forma");
(l.ultimos || []).forEach(function (r) {
var p = el("span", "ponto " + r);
p.title = ROTULOS_FORMA[r] || r;
p.setAttribute("role", "img");
p.setAttribute("aria-label", ROTULOS_FORMA[r] || r);
caixa.appendChild(p);
});
forma.appendChild(caixa);
tr.appendChild(forma);
var am = el("td");
am.appendChild(el("span", "cartao amarelo"));
am.appendChild(document.createTextNode(String(l.amarelos || 0)));
tr.appendChild(am);
var vm = el("td");
vm.appendChild(el("span", "cartao vermelho"));
vm.appendChild(document.createTextNode(String(l.vermelhos || 0)));
tr.appendChild(vm);
corpo.appendChild(tr);
});
t.appendChild(corpo);
alvo.appendChild(t);
}
// ------------------------------------------------------------------ jogos
function montarJogos(jogos) {
var alvo = document.getElementById("jogos");
limpar(alvo);
if (!jogos || !jogos.length) { alvo.appendChild(el("p", "explica", "Ainda não há jogos marcados.")); return; }
jogos.forEach(function (j) {
var card = el("div", "jogo");
var quando = el("div", "quando", (j.rodada ? j.rodada + " · " : "") + dataBr(j.data));
var etiqueta = el("span", "etiqueta", ROTULOS_STATUS[j.status] || j.status);
quando.appendChild(etiqueta);
if (j.wo) quando.appendChild(el("span", "etiqueta", "W.O."));
card.appendChild(quando);
var linha = el("div", "confronto");
linha.appendChild(el("span", null, j.mandante));
var temPlacar = j.placarMandante !== undefined && j.placarMandante !== null;
linha.appendChild(el("span", temPlacar ? "placar" : "", temPlacar ? j.placarMandante + " x " + j.placarVisitante : "x"));
linha.appendChild(el("span", null, j.visitante));
card.appendChild(linha);
if (j.penaltis) card.appendChild(el("div", "quando", "Pênaltis: " + j.penaltis.mandante + " x " + j.penaltis.visitante));
if (j.motivo) card.appendChild(el("div", "quando", "Motivo: " + j.motivo));
alvo.appendChild(card);
});
}
// ------------------------------------------------------------------ súmulas
function pessoa(p) {
if (!p) return "jogador não identificado";
if (p.nome && p.numeroCamisa !== undefined && p.numeroCamisa !== null) return p.nome + " (" + p.numeroCamisa + ")";
if (p.nome) return p.nome;
if (p.numeroCamisa !== undefined && p.numeroCamisa !== null) return "camisa " + p.numeroCamisa;
return "jogador não identificado";
}
function momento(e) {
var m = e.minuto !== undefined && e.minuto !== null ? e.minuto + "'" : "";
var t = e.tempo ? e.tempo + "º tempo" : "";
return [t, m].filter(Boolean).join(" ") || "—";
}
function secaoDaSumula(card, nome, itens, texto) {
if (!itens || !itens.length) return;
card.appendChild(el("strong", null, nome));
var ul = el("ul");
itens.forEach(function (i) { ul.appendChild(el("li", null, texto(i))); });
card.appendChild(ul);
}
function montarSumulas(sumulas, jogos) {
var alvo = document.getElementById("sumulas");
limpar(alvo);
if (!sumulas || !sumulas.length) { alvo.appendChild(el("p", "explica", "As súmulas dos jogos do seu time aparecem aqui.")); return; }
sumulas.forEach(function (s) {
var j = (jogos || []).filter(function (x) { return x.ref === s.jogoRef; })[0];
var card = el("div", "sumula");
var cab = j ? j.mandante + " x " + j.visitante : "Jogo " + s.jogoRef;
if (j && j.placarMandante !== undefined && j.placarMandante !== null) {
cab = j.mandante + " " + j.placarMandante + " x " + j.placarVisitante + " " + j.visitante;
}
card.appendChild(el("h3", null, cab));
if (j) card.appendChild(el("div", "quando", (j.rodada ? j.rodada + " · " : "") + dataBr(j.data)));
secaoDaSumula(card, "Gols", s.gols, function (g) { return momento(g) + " · " + g.time + " · " + pessoa(g.jogador); });
secaoDaSumula(card, "Cartões", s.cartoes, function (c) {
return momento(c) + " · " + (c.tipo === "VERMELHO" ? "vermelho" : "amarelo") + " · " + c.time + " · " + pessoa(c.jogador);
});
secaoDaSumula(card, "Substituições", s.substituicoes, function (t) {
return momento(t) + " · " + t.time + " · sai " + pessoa(t.sai) + ", entra " + pessoa(t.entra);
});
alvo.appendChild(card);
});
}
// ------------------------------------------------------------------ ficha
function linhaDeJogador(j) {
var linha = el("div", "linha-ficha linha-jogador");
if (j && j.id) linha.setAttribute("data-id", j.id);
var nome = el("input");
nome.placeholder = "Nome do jogador";
nome.maxLength = 120;
nome.value = (j && j.nome) || "";
nome.setAttribute("aria-label", "Nome do jogador");
nome.setAttribute("data-campo", "nome");
var camisa = el("input");
camisa.type = "number";
camisa.min = "0";
camisa.max = "999";
camisa.placeholder = "nº";
camisa.value = j && j.numeroCamisa !== undefined && j.numeroCamisa !== null ? j.numeroCamisa : "";
camisa.setAttribute("aria-label", "Número da camisa");
camisa.setAttribute("data-campo", "camisa");
var tirar = el("button", "secundario", "×");
tirar.type = "button";
tirar.setAttribute("aria-label", "Remover jogador");
tirar.addEventListener("click", function () { linha.remove(); });
linha.appendChild(nome);
linha.appendChild(camisa);
linha.appendChild(tirar);
listaJogadores.appendChild(linha);
return linha;
}
function linhaDeComissao(m) {
var linha = el("div", "linha-ficha linha-comissao");
if (m && m.id) linha.setAttribute("data-id", m.id);
var nome = el("input");
nome.placeholder = "Nome";
nome.maxLength = 120;
nome.value = (m && m.nome) || "";
nome.setAttribute("aria-label", "Nome na comissão técnica");
nome.setAttribute("data-campo", "nome");
var papel = el("input");
papel.placeholder = "Função";
papel.maxLength = 40;
papel.value = (m && m.funcao) || "";
papel.setAttribute("list", "funcoes-comissao");
papel.setAttribute("aria-label", "Função na comissão técnica");
papel.setAttribute("data-campo", "funcao");
var tirar = el("button", "secundario", "×");
tirar.type = "button";
tirar.setAttribute("aria-label", "Remover da comissão");
tirar.addEventListener("click", function () { linha.remove(); });
linha.appendChild(nome);
linha.appendChild(papel);
linha.appendChild(tirar);
listaComissao.appendChild(linha);
return linha;
}
/** Time da escola ou ficha fechada: só leitura (listas simples). */
function fichaSomenteLeitura(ficha) {
formFicha.hidden = true;
var alvo = document.getElementById("ficha-leitura");
if (!alvo) {
alvo = el("div", "somente-leitura");
alvo.id = "ficha-leitura";
formFicha.parentNode.appendChild(alvo);
}
limpar(alvo);
alvo.appendChild(el("h3", null, "Jogadores"));
var ul = el("ul");
(ficha.jogadores || []).forEach(function (j) {
ul.appendChild(el("li", null, pessoa({ nome: j.nome, numeroCamisa: j.numeroCamisa })));
});
if (!ul.childNodes.length) ul.appendChild(el("li", null, "Nenhum jogador na ficha."));
alvo.appendChild(ul);
if ((ficha.comissao || []).length) {
alvo.appendChild(el("h3", null, "Comissão técnica"));
var uc = el("ul");
ficha.comissao.forEach(function (m) { uc.appendChild(el("li", null, m.nome + " — " + m.funcao)); });
alvo.appendChild(uc);
}
}
function montarFicha(ficha) {
var aviso = document.getElementById("ficha-aviso");
limpar(listaJogadores);
limpar(listaComissao);
if (!ficha.editavel) {
aviso.textContent = "Esta ficha está fechada para alterações aqui. Se algo estiver errado, fale com a organização.";
fichaSomenteLeitura(ficha);
return;
}
aviso.textContent = "Confira os jogadores (nome e número da camisa) e a comissão técnica. Dá para corrigir até o primeiro jogo ser encerrado.";
formFicha.hidden = false;
(ficha.jogadores || []).forEach(linhaDeJogador);
if (!(ficha.jogadores || []).length) for (var i = 0; i < 3; i++) linhaDeJogador();
(ficha.comissao || []).forEach(linhaDeComissao);
}
function mostrarErroFicha(texto) {
fichaErro.textContent = texto || "";
fichaErro.hidden = !texto;
}
function mostrarOkFicha(texto) {
fichaOk.textContent = texto || "";
fichaOk.hidden = !texto;
}
function corpoDaFicha() {
var jogadores = [];
var linhas = listaJogadores.querySelectorAll(".linha-jogador");
for (var i = 0; i < linhas.length; i++) {
var nome = linhas[i].querySelector('[data-campo="nome"]').value.trim();
if (!nome) continue;
var c = linhas[i].querySelector('[data-campo="camisa"]').value;
var j = { nome: nome, numeroCamisa: c === "" ? null : parseInt(c, 10) };
if (linhas[i].getAttribute("data-id")) j.id = linhas[i].getAttribute("data-id");
jogadores.push(j);
}
var comissao = [];
var faltaFuncao = false;
var pessoas = listaComissao.querySelectorAll(".linha-comissao");
for (var k = 0; k < pessoas.length; k++) {
var n = pessoas[k].querySelector('[data-campo="nome"]').value.trim();
if (!n) continue;
var f = pessoas[k].querySelector('[data-campo="funcao"]').value.trim();
if (!f) faltaFuncao = true;
var m = { nome: n, funcao: f };
if (pessoas[k].getAttribute("data-id")) m.id = pessoas[k].getAttribute("data-id");
comissao.push(m);
}
return { jogadores: jogadores, comissao: comissao, faltaFuncao: faltaFuncao };
}
function aoSalvar(ev) {
ev.preventDefault();
mostrarErroFicha("");
mostrarOkFicha("");
var c = corpoDaFicha();
if (c.jogadores.length > MAX_JOGADORES) { mostrarErroFicha("No máximo " + MAX_JOGADORES + " jogadores."); return; }
if (c.faltaFuncao) { mostrarErroFicha("Informe a função de cada pessoa da comissão técnica."); return; }
if (c.comissao.length > MAX_COMISSAO) { mostrarErroFicha("No máximo " + MAX_COMISSAO + " pessoas na comissão técnica."); return; }
salvar.disabled = true;
salvar.setAttribute("aria-busy", "true");
fetch(base + "/ficha", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ jogadores: c.jogadores, comissao: c.comissao })
}).then(lerResposta).then(function (r) {
salvar.disabled = false;
salvar.removeAttribute("aria-busy");
if (r.status >= 200 && r.status < 300) {
mostrarOkFicha("Ficha salva.");
return carregar(true);
}
if (r.status === 404) { recusar("Este link não está mais disponível."); return null; }
mostrarErroFicha((r.corpo && r.corpo.detail) || "Não foi possível salvar agora. Tente de novo em instantes.");
return null;
}).catch(function () {
salvar.disabled = false;
salvar.removeAttribute("aria-busy");
mostrarErroFicha("Sem conexão. Confira a internet e tente de novo.");
});
}
// ------------------------------------------------------------------ carga
function mostrar(v, manterAba) {
visao = v;
var nomeTime = (v.time && v.time.nome) || "Seu time";
titulo.textContent = (v.competicao || "Competição") + " — " + nomeTime;
document.title = titulo.textContent + " — Athletic Map";
var partes = [];
if (v.time && v.time.escola) partes.push(v.time.escola);
if (v.time && v.time.cidade) partes.push(v.time.cidade);
if (v.temporada && v.temporada.nome) partes.push("Temporada " + v.temporada.nome);
if (v.time && v.time.status === "DESQUALIFICADO") partes.push("Time desqualificado");
situacao.textContent = partes.join(" · ");
situacao.className = "";
painel.hidden = false;
montarTabela(v.classificacao, nomeTime);
montarJogos(v.jogos);
montarSumulas(v.sumulas, v.jogos);
montarFicha(v.ficha || { jogadores: [], comissao: [], editavel: false });
if (!manterAba) abrirAba("tabela", false);
}
function carregar(manterAba) {
return fetch(base).then(lerResposta).then(function (r) {
if (r.status === 404) { recusar("Este link não está disponível ou foi desativado. Peça um novo à organização."); return; }
if (r.status === 429) { recusar("Muitas consultas seguidas. Aguarde alguns minutos e abra o link de novo."); return; }
if (r.status !== 200) { recusar("Não foi possível abrir agora. Tente de novo em instantes."); return; }
mostrar(r.corpo || {}, manterAba);
}).catch(function () {
recusar("Não foi possível abrir agora. Tente de novo em instantes.");
});
}
function iniciar() {
if (!TOKEN.test(token)) { recusar("Este endereço não é válido."); return; }
ABAS.forEach(function (nome) {
var aba = document.getElementById("aba-" + nome);
aba.addEventListener("click", function () { abrirAba(nome, false); });
aba.addEventListener("keydown", aoTeclarNaAba);
});
document.getElementById("maisJogador").addEventListener("click", function () {
if (listaJogadores.querySelectorAll(".linha-jogador").length < MAX_JOGADORES) linhaDeJogador().querySelector("input").focus();
});
document.getElementById("maisComissao").addEventListener("click", function () {
if (listaComissao.querySelectorAll(".linha-comissao").length < MAX_COMISSAO) linhaDeComissao(null).querySelector("input").focus();
});
formFicha.addEventListener("submit", aoSalvar);
carregar(false);
}
iniciar();
})();
---
apiVersion: v1
kind: ConfigMap
+3
View File
@@ -162,6 +162,9 @@ spec:
- { key: "preparando.js", path: "preparando.js" }
- { key: "privacidade.html", path: "privacidade.html" }
- { key: "termos.html", path: "termos.html" }
- { key: "time.css", path: "time.css" }
- { key: "time.html", path: "time.html" }
- { key: "time.js", path: "time.js" }
- configMap:
name: frontend-publico-binario
items:
+619 -3
View File
@@ -409,7 +409,7 @@ data:
<form id="formulario" hidden novalidate>
<p class="explica">
Preencha os dados do seu time e dos jogadores (só o nome e o número da camisa).
Preencha os dados do seu time, dos jogadores (só o nome e o número da camisa) e da comissão técnica (nome e função).
A organização confere e aprova antes do time entrar na competição.
</p>
@@ -433,12 +433,23 @@ data:
<button type="button" id="maisJogador" class="secundario">+ Jogador</button>
</fieldset>
<fieldset>
<legend>Comissão técnica</legend>
<div id="comissao"></div>
<datalist id="funcoes-comissao">
<option value="Técnico"></option><option value="Auxiliar técnico"></option><option value="Preparador físico"></option>
<option value="Preparador de goleiros"></option><option value="Médico"></option><option value="Fisioterapeuta"></option>
<option value="Massagista"></option><option value="Roupeiro"></option><option value="Dirigente"></option>
</datalist>
<button type="button" id="maisComissao" class="secundario">+ Pessoa na comissão</button>
</fieldset>
<p id="erro" class="erro" role="alert" hidden></p>
<button type="submit" id="enviar">Enviar time</button>
</form>
</main>
<script src="/publico/desafio.js?v=1"></script>
<script src="/publico/convite.js?v=2"></script>
<script src="/publico/convite.js?v=3"></script>
</body>
</html>
@@ -465,6 +476,7 @@ data:
var TOKEN = /^[A-Za-z0-9_-]{20,64}$/;
var MAX_JOGADORES = 60;
var MAX_COMISSAO = 15;
var token = decodeURIComponent((location.pathname.split("/").filter(Boolean).pop() || ""));
var base = "/api/public/competicoes/convites/" + encodeURIComponent(token);
@@ -473,6 +485,7 @@ data:
var situacao = document.getElementById("situacao");
var formulario = document.getElementById("formulario");
var listaJogadores = document.getElementById("jogadores");
var listaComissao = document.getElementById("comissao");
var erro = document.getElementById("erro");
var enviar = document.getElementById("enviar");
var ROTULO_ENVIAR = enviar.textContent;
@@ -520,6 +533,39 @@ data:
return linha;
}
/** Comissao tecnica (21/10/2026): nome e funcao; a funcao tem sugestoes, mas aceita texto livre. */
function linhaDeComissao(funcao) {
var linha = document.createElement("div");
linha.className = "linha-jogador linha-comissao";
var nome = document.createElement("input");
nome.placeholder = "Nome";
nome.maxLength = 120;
nome.setAttribute("aria-label", "Nome na comissão técnica");
nome.setAttribute("data-campo", "nome");
var papel = document.createElement("input");
papel.placeholder = "Função";
papel.maxLength = 40;
papel.value = funcao || "";
papel.setAttribute("list", "funcoes-comissao");
papel.setAttribute("aria-label", "Função na comissão técnica");
papel.setAttribute("data-campo", "funcao");
var tirar = document.createElement("button");
tirar.type = "button";
tirar.className = "secundario";
tirar.textContent = "×";
tirar.setAttribute("aria-label", "Remover da comissão");
tirar.addEventListener("click", function () { linha.remove(); });
linha.appendChild(nome);
linha.appendChild(papel);
linha.appendChild(tirar);
listaComissao.appendChild(linha);
return linha;
}
function valor(id) {
var v = document.getElementById(id).value.trim();
return v === "" ? null : v;
@@ -534,14 +580,26 @@ data:
var c = linhas[i].querySelector('[data-campo="camisa"]').value;
jogadores.push({ nome: nome, numeroCamisa: c === "" ? null : parseInt(c, 10) });
}
var comissao = [];
var faltaFuncao = false;
var pessoas = listaComissao.querySelectorAll(".linha-comissao");
for (var k = 0; k < pessoas.length; k++) {
var n = pessoas[k].querySelector('[data-campo="nome"]').value.trim();
if (!n) continue;
var f = pessoas[k].querySelector('[data-campo="funcao"]').value.trim();
if (!f) faltaFuncao = true;
comissao.push({ nome: n, funcao: f });
}
return {
faltaFuncao: faltaFuncao,
nome: valor("nome"),
escola: valor("escola"),
cidade: valor("cidade"),
contatoNome: valor("contatoNome"),
contatoTelefone: valor("contatoTelefone"),
contatoEmail: valor("contatoEmail"),
jogadores: jogadores
jogadores: jogadores,
comissao: comissao
};
}
@@ -555,6 +613,9 @@ data:
var corpo = montarCorpo();
if (!corpo.nome) { mostrarErro("Informe o nome do time."); return; }
if (corpo.jogadores.length > MAX_JOGADORES) { mostrarErro("No máximo " + MAX_JOGADORES + " jogadores."); return; }
if (corpo.faltaFuncao) { mostrarErro("Informe a função de cada pessoa da comissão técnica."); return; }
if (corpo.comissao.length > MAX_COMISSAO) { mostrarErro("No máximo " + MAX_COMISSAO + " pessoas na comissão técnica."); return; }
delete corpo.faltaFuncao;
enviar.disabled = true;
enviar.textContent = "Verificando…";
enviar.setAttribute("aria-busy", "true");
@@ -615,6 +676,7 @@ data:
situacao.className = "";
formulario.hidden = false;
for (var i = 0; i < 5; i++) linhaDeJogador();
linhaDeComissao("Técnico");
}).catch(function () {
recusar("Não foi possível abrir o convite agora. Tente de novo em instantes.");
});
@@ -623,6 +685,9 @@ data:
document.getElementById("maisJogador").addEventListener("click", function () {
if (listaJogadores.querySelectorAll(".linha-jogador").length < MAX_JOGADORES) linhaDeJogador().querySelector("input").focus();
});
document.getElementById("maisComissao").addEventListener("click", function () {
if (listaComissao.querySelectorAll(".linha-comissao").length < MAX_COMISSAO) linhaDeComissao("").querySelector("input").focus();
});
formulario.addEventListener("submit", aoEnviar);
iniciar();
})();
@@ -8242,6 +8307,557 @@ data:
</body>
</html>
time.css: |
/* Link do time: página pública, sem fonte nem script de fora (a CSP só deixa 'self'). Mobile primeiro. */
main { max-width: 56rem; }
.explica, .legenda { color: var(--muted); font-size: .9rem; }
.abas { display: flex; gap: .25rem; overflow-x: auto; border-bottom: 2px solid var(--line); margin: 0 0 1rem; }
.abas button { background: transparent; color: var(--navy); border: 0; border-bottom: 3px solid transparent; border-radius: 0; padding: .6rem .9rem; white-space: nowrap; font: inherit; font-weight: 700; cursor: pointer; }
.abas button[aria-selected="true"] { border-bottom-color: var(--orange); }
.abas button:focus-visible, button:focus-visible, input:focus-visible { outline: 3px solid var(--orange); outline-offset: 1px; }
h2 { font-size: 1.15rem; margin: .5rem 0; }
.rolagem { overflow-x: auto; }
table { border-collapse: collapse; width: 100%; font-size: .9rem; background: #fff; }
th, td { padding: .4rem .45rem; text-align: center; border-bottom: 1px solid var(--line); white-space: nowrap; }
th:nth-child(2), td:nth-child(2) { text-align: left; white-space: normal; min-width: 8rem; }
tr.meu { background: #fff4e5; font-weight: 700; }
.forma { display: inline-flex; gap: 3px; vertical-align: middle; }
.ponto { width: .8rem; height: .8rem; border-radius: 50%; display: inline-block; }
.ponto.V { background: #067647; }
.ponto.E { background: #98a2b3; }
.ponto.D { background: #b42318; }
.cartao { display: inline-block; width: .7rem; height: .95rem; border-radius: 2px; vertical-align: middle; margin-right: 3px; }
.cartao.amarelo { background: #f5c518; }
.cartao.vermelho { background: #d92d20; }
.jogo, .sumula { background: #fff; border: 1px solid var(--line); border-radius: 8px; padding: .6rem .8rem; margin: 0 0 .6rem; }
.jogo .quando { color: var(--muted); font-size: .85rem; }
.jogo .confronto { display: grid; grid-template-columns: 1fr auto 1fr; gap: .5rem; align-items: center; font-weight: 700; }
.jogo .confronto span:first-child { text-align: right; }
.jogo .placar { background: var(--navy); color: #fff; border-radius: 6px; padding: .1rem .6rem; }
.etiqueta { display: inline-block; font-size: .75rem; font-weight: 700; padding: .05rem .5rem; border-radius: 99px; background: #fff1e3; border: 1px solid var(--orange); margin-left: .4rem; }
.sumula h3 { margin: 0 0 .3rem; font-size: 1rem; }
.sumula ul { margin: .2rem 0 .5rem; padding-left: 1.2rem; }
form { display: grid; gap: 1rem; }
fieldset { border: 1px solid var(--line); border-radius: 8px; background: #fff; padding: .75rem 1rem 1rem; display: grid; gap: .6rem; }
legend { font-weight: 700; padding: 0 .4rem; }
input { font: inherit; padding: .5rem .6rem; border: 1px solid var(--line); border-radius: 6px; width: 100%; }
.linha-ficha { display: grid; grid-template-columns: 1fr 5.5rem auto; gap: .4rem; align-items: center; }
.linha-ficha button { padding: .4rem .6rem; }
button { font: inherit; font-weight: 700; padding: .65rem 1.1rem; border: 0; border-radius: 8px; background: var(--orange); color: #fff; cursor: pointer; }
button.secundario { background: #fff; color: var(--navy); border: 1px solid var(--line); justify-self: start; }
button:disabled { opacity: .55; cursor: not-allowed; }
.erro { color: #b42318; margin: 0; font-weight: 600; }
.ok { background: #e7f6ec; border-left: 4px solid #067647; padding: .8rem 1rem; margin: 0; }
#situacao.invalido { background: #fff1e3; border-left: 4px solid var(--orange); padding: 1rem 1.2rem; }
.somente-leitura li { margin: .15rem 0; }
@media (max-width: 30rem) { .linha-ficha { grid-template-columns: 1fr 4.5rem auto; } }
time.html: |
<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow">
<meta name="referrer" content="no-referrer">
<title>Seu time na competição — Athletic Map</title>
<link rel="stylesheet" href="/publico/_estilo.css">
<link rel="stylesheet" href="/publico/time.css?v=1">
</head>
<body>
<header><div class="marca">Athletic <span>Map</span></div></header>
<main>
<h1 id="titulo">Seu time na competição</h1>
<p id="situacao" role="status" aria-live="polite">Abrindo o link…</p>
<div id="painel" hidden>
<div class="abas" role="tablist" aria-label="Seções">
<button type="button" role="tab" id="aba-tabela" aria-controls="sec-tabela" aria-selected="true" data-aba="tabela">Tabela</button>
<button type="button" role="tab" id="aba-jogos" aria-controls="sec-jogos" aria-selected="false" tabindex="-1" data-aba="jogos">Jogos</button>
<button type="button" role="tab" id="aba-sumulas" aria-controls="sec-sumulas" aria-selected="false" tabindex="-1" data-aba="sumulas">Súmulas</button>
<button type="button" role="tab" id="aba-ficha" aria-controls="sec-ficha" aria-selected="false" tabindex="-1" data-aba="ficha">Ficha do time</button>
</div>
<section id="sec-tabela" role="tabpanel" aria-labelledby="aba-tabela">
<h2>Tabela</h2>
<div class="rolagem" id="tabela"></div>
<p class="legenda">P pontos · J jogos · V vitórias · E empates · D derrotas · GP gols pró · GC gols contra · SG saldo de gols · % aproveitamento · CA cartões amarelos · CV cartões vermelhos</p>
</section>
<section id="sec-jogos" role="tabpanel" aria-labelledby="aba-jogos" hidden>
<h2>Jogos</h2>
<div id="jogos"></div>
</section>
<section id="sec-sumulas" role="tabpanel" aria-labelledby="aba-sumulas" hidden>
<h2>Súmulas</h2>
<div id="sumulas"></div>
</section>
<section id="sec-ficha" role="tabpanel" aria-labelledby="aba-ficha" hidden>
<h2>Ficha do time</h2>
<p id="ficha-aviso" class="explica"></p>
<form id="ficha" novalidate>
<fieldset>
<legend>Jogadores</legend>
<div id="jogadores"></div>
<button type="button" id="maisJogador" class="secundario">+ Jogador</button>
</fieldset>
<fieldset>
<legend>Comissão técnica</legend>
<div id="comissao"></div>
<datalist id="funcoes-comissao">
<option value="Técnico"></option><option value="Auxiliar técnico"></option><option value="Preparador físico"></option>
<option value="Preparador de goleiros"></option><option value="Médico"></option><option value="Fisioterapeuta"></option>
<option value="Massagista"></option><option value="Roupeiro"></option><option value="Dirigente"></option>
</datalist>
<button type="button" id="maisComissao" class="secundario">+ Pessoa na comissão</button>
</fieldset>
<p id="ficha-erro" class="erro" role="alert" hidden></p>
<p id="ficha-ok" class="ok" role="status" hidden></p>
<button type="submit" id="salvar">Salvar ficha</button>
</form>
</section>
</div>
</main>
<script src="/publico/time.js?v=1"></script>
</body>
</html>
time.js: |
/* ===========================================================================
* Link do time — a página que UMA pessoa do time abre pelo link com chave.
*
* Sem login: a chave no endereço é o único segredo. A página mostra a tabela,
* os jogos e as súmulas da competição e a ficha do time; se o time for de
* fora e ainda não houver jogo encerrado, a ficha pode ser corrigida aqui.
*
* Tudo o que vem do servidor entra na página com `textContent` — nunca como
* HTML. O script é externo porque a CSP é `script-src 'self'`.
* =========================================================================== */
(function () {
"use strict";
var TOKEN = /^[A-Za-z0-9_-]{20,64}$/;
var MAX_JOGADORES = 60;
var MAX_COMISSAO = 15;
var ABAS = ["tabela", "jogos", "sumulas", "ficha"];
var ROTULOS_FORMA = { V: "Vitória", E: "Empate", D: "Derrota" };
var ROTULOS_STATUS = {
AGENDADA: "Agendado", ADIADA: "Adiado", EM_ANDAMENTO: "Em andamento", ENCERRADA: "Encerrado", CANCELADA: "Cancelado"
};
var token = decodeURIComponent(location.pathname.split("/").filter(Boolean).pop() || "");
var base = "/api/public/competicoes/times/" + encodeURIComponent(token);
var titulo = document.getElementById("titulo");
var situacao = document.getElementById("situacao");
var painel = document.getElementById("painel");
var listaJogadores = document.getElementById("jogadores");
var listaComissao = document.getElementById("comissao");
var formFicha = document.getElementById("ficha");
var fichaErro = document.getElementById("ficha-erro");
var fichaOk = document.getElementById("ficha-ok");
var salvar = document.getElementById("salvar");
var visao = null;
function el(tag, classe, texto) {
var e = document.createElement(tag);
if (classe) e.className = classe;
if (texto !== undefined && texto !== null) e.textContent = String(texto);
return e;
}
function limpar(no) {
while (no.firstChild) no.removeChild(no.firstChild);
}
function recusar(texto) {
situacao.textContent = texto;
situacao.className = "invalido";
painel.hidden = true;
}
function lerResposta(r) {
return r.json().catch(function () { return {}; }).then(function (corpo) { return { status: r.status, corpo: corpo }; });
}
// ------------------------------------------------------------------ abas
function abrirAba(nome, focar) {
for (var i = 0; i < ABAS.length; i++) {
var ativa = ABAS[i] === nome;
var aba = document.getElementById("aba-" + ABAS[i]);
aba.setAttribute("aria-selected", ativa ? "true" : "false");
aba.tabIndex = ativa ? 0 : -1;
document.getElementById("sec-" + ABAS[i]).hidden = !ativa;
if (ativa && focar) aba.focus();
}
}
function aoTeclarNaAba(ev) {
var i = ABAS.indexOf(ev.currentTarget.getAttribute("data-aba"));
if (ev.key === "ArrowRight") { abrirAba(ABAS[(i + 1) % ABAS.length], true); ev.preventDefault(); }
else if (ev.key === "ArrowLeft") { abrirAba(ABAS[(i + ABAS.length - 1) % ABAS.length], true); ev.preventDefault(); }
}
// ------------------------------------------------------------------ tabela
function dataBr(iso) {
if (!iso) return "Data a definir";
var d = new Date(iso);
if (isNaN(d.getTime())) return "Data a definir";
try {
return d.toLocaleString("pt-BR", { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit" });
} catch (e) {
return d.toISOString();
}
}
function percentual(n) {
return (Math.round((n || 0) * 10) / 10).toString().replace(".", ",") + "%";
}
function montarTabela(linhas, meuNome) {
var alvo = document.getElementById("tabela");
limpar(alvo);
if (!linhas || !linhas.length) { alvo.appendChild(el("p", "explica", "A tabela aparece quando houver times e jogos.")); return; }
var t = el("table");
t.appendChild(el("caption", "explica", "Classificação"));
var cab = el("tr");
var cols = [["#", "Posição"], ["Time", "Time"], ["P", "Pontos"], ["J", "Jogos"], ["V", "Vitórias"], ["E", "Empates"],
["D", "Derrotas"], ["GP", "Gols pró"], ["GC", "Gols contra"], ["SG", "Saldo de gols"], ["%", "Aproveitamento"],
["Últimos 5", "Últimos 5 jogos"], ["CA", "Cartões amarelos"], ["CV", "Cartões vermelhos"]];
cols.forEach(function (c) {
var th = el("th", null, c[0]);
th.scope = "col";
th.title = c[1];
cab.appendChild(th);
});
var thead = el("thead");
thead.appendChild(cab);
t.appendChild(thead);
var corpo = el("tbody");
linhas.forEach(function (l) {
var tr = el("tr", l.time === meuNome ? "meu" : "");
[l.posicao, l.time, l.pontos, l.jogos, l.vitorias, l.empates, l.derrotas, l.golsPro, l.golsContra, l.saldoGols,
percentual(l.aproveitamento)].forEach(function (v) { tr.appendChild(el("td", null, v)); });
var forma = el("td");
var caixa = el("span", "forma");
(l.ultimos || []).forEach(function (r) {
var p = el("span", "ponto " + r);
p.title = ROTULOS_FORMA[r] || r;
p.setAttribute("role", "img");
p.setAttribute("aria-label", ROTULOS_FORMA[r] || r);
caixa.appendChild(p);
});
forma.appendChild(caixa);
tr.appendChild(forma);
var am = el("td");
am.appendChild(el("span", "cartao amarelo"));
am.appendChild(document.createTextNode(String(l.amarelos || 0)));
tr.appendChild(am);
var vm = el("td");
vm.appendChild(el("span", "cartao vermelho"));
vm.appendChild(document.createTextNode(String(l.vermelhos || 0)));
tr.appendChild(vm);
corpo.appendChild(tr);
});
t.appendChild(corpo);
alvo.appendChild(t);
}
// ------------------------------------------------------------------ jogos
function montarJogos(jogos) {
var alvo = document.getElementById("jogos");
limpar(alvo);
if (!jogos || !jogos.length) { alvo.appendChild(el("p", "explica", "Ainda não há jogos marcados.")); return; }
jogos.forEach(function (j) {
var card = el("div", "jogo");
var quando = el("div", "quando", (j.rodada ? j.rodada + " · " : "") + dataBr(j.data));
var etiqueta = el("span", "etiqueta", ROTULOS_STATUS[j.status] || j.status);
quando.appendChild(etiqueta);
if (j.wo) quando.appendChild(el("span", "etiqueta", "W.O."));
card.appendChild(quando);
var linha = el("div", "confronto");
linha.appendChild(el("span", null, j.mandante));
var temPlacar = j.placarMandante !== undefined && j.placarMandante !== null;
linha.appendChild(el("span", temPlacar ? "placar" : "", temPlacar ? j.placarMandante + " x " + j.placarVisitante : "x"));
linha.appendChild(el("span", null, j.visitante));
card.appendChild(linha);
if (j.penaltis) card.appendChild(el("div", "quando", "Pênaltis: " + j.penaltis.mandante + " x " + j.penaltis.visitante));
if (j.motivo) card.appendChild(el("div", "quando", "Motivo: " + j.motivo));
alvo.appendChild(card);
});
}
// ------------------------------------------------------------------ súmulas
function pessoa(p) {
if (!p) return "jogador não identificado";
if (p.nome && p.numeroCamisa !== undefined && p.numeroCamisa !== null) return p.nome + " (" + p.numeroCamisa + ")";
if (p.nome) return p.nome;
if (p.numeroCamisa !== undefined && p.numeroCamisa !== null) return "camisa " + p.numeroCamisa;
return "jogador não identificado";
}
function momento(e) {
var m = e.minuto !== undefined && e.minuto !== null ? e.minuto + "'" : "";
var t = e.tempo ? e.tempo + "º tempo" : "";
return [t, m].filter(Boolean).join(" ") || "—";
}
function secaoDaSumula(card, nome, itens, texto) {
if (!itens || !itens.length) return;
card.appendChild(el("strong", null, nome));
var ul = el("ul");
itens.forEach(function (i) { ul.appendChild(el("li", null, texto(i))); });
card.appendChild(ul);
}
function montarSumulas(sumulas, jogos) {
var alvo = document.getElementById("sumulas");
limpar(alvo);
if (!sumulas || !sumulas.length) { alvo.appendChild(el("p", "explica", "As súmulas dos jogos do seu time aparecem aqui.")); return; }
sumulas.forEach(function (s) {
var j = (jogos || []).filter(function (x) { return x.ref === s.jogoRef; })[0];
var card = el("div", "sumula");
var cab = j ? j.mandante + " x " + j.visitante : "Jogo " + s.jogoRef;
if (j && j.placarMandante !== undefined && j.placarMandante !== null) {
cab = j.mandante + " " + j.placarMandante + " x " + j.placarVisitante + " " + j.visitante;
}
card.appendChild(el("h3", null, cab));
if (j) card.appendChild(el("div", "quando", (j.rodada ? j.rodada + " · " : "") + dataBr(j.data)));
secaoDaSumula(card, "Gols", s.gols, function (g) { return momento(g) + " · " + g.time + " · " + pessoa(g.jogador); });
secaoDaSumula(card, "Cartões", s.cartoes, function (c) {
return momento(c) + " · " + (c.tipo === "VERMELHO" ? "vermelho" : "amarelo") + " · " + c.time + " · " + pessoa(c.jogador);
});
secaoDaSumula(card, "Substituições", s.substituicoes, function (t) {
return momento(t) + " · " + t.time + " · sai " + pessoa(t.sai) + ", entra " + pessoa(t.entra);
});
alvo.appendChild(card);
});
}
// ------------------------------------------------------------------ ficha
function linhaDeJogador(j) {
var linha = el("div", "linha-ficha linha-jogador");
if (j && j.id) linha.setAttribute("data-id", j.id);
var nome = el("input");
nome.placeholder = "Nome do jogador";
nome.maxLength = 120;
nome.value = (j && j.nome) || "";
nome.setAttribute("aria-label", "Nome do jogador");
nome.setAttribute("data-campo", "nome");
var camisa = el("input");
camisa.type = "number";
camisa.min = "0";
camisa.max = "999";
camisa.placeholder = "nº";
camisa.value = j && j.numeroCamisa !== undefined && j.numeroCamisa !== null ? j.numeroCamisa : "";
camisa.setAttribute("aria-label", "Número da camisa");
camisa.setAttribute("data-campo", "camisa");
var tirar = el("button", "secundario", "×");
tirar.type = "button";
tirar.setAttribute("aria-label", "Remover jogador");
tirar.addEventListener("click", function () { linha.remove(); });
linha.appendChild(nome);
linha.appendChild(camisa);
linha.appendChild(tirar);
listaJogadores.appendChild(linha);
return linha;
}
function linhaDeComissao(m) {
var linha = el("div", "linha-ficha linha-comissao");
if (m && m.id) linha.setAttribute("data-id", m.id);
var nome = el("input");
nome.placeholder = "Nome";
nome.maxLength = 120;
nome.value = (m && m.nome) || "";
nome.setAttribute("aria-label", "Nome na comissão técnica");
nome.setAttribute("data-campo", "nome");
var papel = el("input");
papel.placeholder = "Função";
papel.maxLength = 40;
papel.value = (m && m.funcao) || "";
papel.setAttribute("list", "funcoes-comissao");
papel.setAttribute("aria-label", "Função na comissão técnica");
papel.setAttribute("data-campo", "funcao");
var tirar = el("button", "secundario", "×");
tirar.type = "button";
tirar.setAttribute("aria-label", "Remover da comissão");
tirar.addEventListener("click", function () { linha.remove(); });
linha.appendChild(nome);
linha.appendChild(papel);
linha.appendChild(tirar);
listaComissao.appendChild(linha);
return linha;
}
/** Time da escola ou ficha fechada: só leitura (listas simples). */
function fichaSomenteLeitura(ficha) {
formFicha.hidden = true;
var alvo = document.getElementById("ficha-leitura");
if (!alvo) {
alvo = el("div", "somente-leitura");
alvo.id = "ficha-leitura";
formFicha.parentNode.appendChild(alvo);
}
limpar(alvo);
alvo.appendChild(el("h3", null, "Jogadores"));
var ul = el("ul");
(ficha.jogadores || []).forEach(function (j) {
ul.appendChild(el("li", null, pessoa({ nome: j.nome, numeroCamisa: j.numeroCamisa })));
});
if (!ul.childNodes.length) ul.appendChild(el("li", null, "Nenhum jogador na ficha."));
alvo.appendChild(ul);
if ((ficha.comissao || []).length) {
alvo.appendChild(el("h3", null, "Comissão técnica"));
var uc = el("ul");
ficha.comissao.forEach(function (m) { uc.appendChild(el("li", null, m.nome + " — " + m.funcao)); });
alvo.appendChild(uc);
}
}
function montarFicha(ficha) {
var aviso = document.getElementById("ficha-aviso");
limpar(listaJogadores);
limpar(listaComissao);
if (!ficha.editavel) {
aviso.textContent = "Esta ficha está fechada para alterações aqui. Se algo estiver errado, fale com a organização.";
fichaSomenteLeitura(ficha);
return;
}
aviso.textContent = "Confira os jogadores (nome e número da camisa) e a comissão técnica. Dá para corrigir até o primeiro jogo ser encerrado.";
formFicha.hidden = false;
(ficha.jogadores || []).forEach(linhaDeJogador);
if (!(ficha.jogadores || []).length) for (var i = 0; i < 3; i++) linhaDeJogador();
(ficha.comissao || []).forEach(linhaDeComissao);
}
function mostrarErroFicha(texto) {
fichaErro.textContent = texto || "";
fichaErro.hidden = !texto;
}
function mostrarOkFicha(texto) {
fichaOk.textContent = texto || "";
fichaOk.hidden = !texto;
}
function corpoDaFicha() {
var jogadores = [];
var linhas = listaJogadores.querySelectorAll(".linha-jogador");
for (var i = 0; i < linhas.length; i++) {
var nome = linhas[i].querySelector('[data-campo="nome"]').value.trim();
if (!nome) continue;
var c = linhas[i].querySelector('[data-campo="camisa"]').value;
var j = { nome: nome, numeroCamisa: c === "" ? null : parseInt(c, 10) };
if (linhas[i].getAttribute("data-id")) j.id = linhas[i].getAttribute("data-id");
jogadores.push(j);
}
var comissao = [];
var faltaFuncao = false;
var pessoas = listaComissao.querySelectorAll(".linha-comissao");
for (var k = 0; k < pessoas.length; k++) {
var n = pessoas[k].querySelector('[data-campo="nome"]').value.trim();
if (!n) continue;
var f = pessoas[k].querySelector('[data-campo="funcao"]').value.trim();
if (!f) faltaFuncao = true;
var m = { nome: n, funcao: f };
if (pessoas[k].getAttribute("data-id")) m.id = pessoas[k].getAttribute("data-id");
comissao.push(m);
}
return { jogadores: jogadores, comissao: comissao, faltaFuncao: faltaFuncao };
}
function aoSalvar(ev) {
ev.preventDefault();
mostrarErroFicha("");
mostrarOkFicha("");
var c = corpoDaFicha();
if (c.jogadores.length > MAX_JOGADORES) { mostrarErroFicha("No máximo " + MAX_JOGADORES + " jogadores."); return; }
if (c.faltaFuncao) { mostrarErroFicha("Informe a função de cada pessoa da comissão técnica."); return; }
if (c.comissao.length > MAX_COMISSAO) { mostrarErroFicha("No máximo " + MAX_COMISSAO + " pessoas na comissão técnica."); return; }
salvar.disabled = true;
salvar.setAttribute("aria-busy", "true");
fetch(base + "/ficha", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ jogadores: c.jogadores, comissao: c.comissao })
}).then(lerResposta).then(function (r) {
salvar.disabled = false;
salvar.removeAttribute("aria-busy");
if (r.status >= 200 && r.status < 300) {
mostrarOkFicha("Ficha salva.");
return carregar(true);
}
if (r.status === 404) { recusar("Este link não está mais disponível."); return null; }
mostrarErroFicha((r.corpo && r.corpo.detail) || "Não foi possível salvar agora. Tente de novo em instantes.");
return null;
}).catch(function () {
salvar.disabled = false;
salvar.removeAttribute("aria-busy");
mostrarErroFicha("Sem conexão. Confira a internet e tente de novo.");
});
}
// ------------------------------------------------------------------ carga
function mostrar(v, manterAba) {
visao = v;
var nomeTime = (v.time && v.time.nome) || "Seu time";
titulo.textContent = (v.competicao || "Competição") + " — " + nomeTime;
document.title = titulo.textContent + " — Athletic Map";
var partes = [];
if (v.time && v.time.escola) partes.push(v.time.escola);
if (v.time && v.time.cidade) partes.push(v.time.cidade);
if (v.temporada && v.temporada.nome) partes.push("Temporada " + v.temporada.nome);
if (v.time && v.time.status === "DESQUALIFICADO") partes.push("Time desqualificado");
situacao.textContent = partes.join(" · ");
situacao.className = "";
painel.hidden = false;
montarTabela(v.classificacao, nomeTime);
montarJogos(v.jogos);
montarSumulas(v.sumulas, v.jogos);
montarFicha(v.ficha || { jogadores: [], comissao: [], editavel: false });
if (!manterAba) abrirAba("tabela", false);
}
function carregar(manterAba) {
return fetch(base).then(lerResposta).then(function (r) {
if (r.status === 404) { recusar("Este link não está disponível ou foi desativado. Peça um novo à organização."); return; }
if (r.status === 429) { recusar("Muitas consultas seguidas. Aguarde alguns minutos e abra o link de novo."); return; }
if (r.status !== 200) { recusar("Não foi possível abrir agora. Tente de novo em instantes."); return; }
mostrar(r.corpo || {}, manterAba);
}).catch(function () {
recusar("Não foi possível abrir agora. Tente de novo em instantes.");
});
}
function iniciar() {
if (!TOKEN.test(token)) { recusar("Este endereço não é válido."); return; }
ABAS.forEach(function (nome) {
var aba = document.getElementById("aba-" + nome);
aba.addEventListener("click", function () { abrirAba(nome, false); });
aba.addEventListener("keydown", aoTeclarNaAba);
});
document.getElementById("maisJogador").addEventListener("click", function () {
if (listaJogadores.querySelectorAll(".linha-jogador").length < MAX_JOGADORES) linhaDeJogador().querySelector("input").focus();
});
document.getElementById("maisComissao").addEventListener("click", function () {
if (listaComissao.querySelectorAll(".linha-comissao").length < MAX_COMISSAO) linhaDeComissao(null).querySelector("input").focus();
});
formFicha.addEventListener("submit", aoSalvar);
carregar(false);
}
iniciar();
})();
---
apiVersion: v1
kind: ConfigMap
+3
View File
@@ -162,6 +162,9 @@ spec:
- { key: "preparando.js", path: "preparando.js" }
- { key: "privacidade.html", path: "privacidade.html" }
- { key: "termos.html", path: "termos.html" }
- { key: "time.css", path: "time.css" }
- { key: "time.html", path: "time.html" }
- { key: "time.js", path: "time.js" }
- configMap:
name: frontend-publico-binario
items:
+619 -3
View File
@@ -409,7 +409,7 @@ data:
<form id="formulario" hidden novalidate>
<p class="explica">
Preencha os dados do seu time e dos jogadores (só o nome e o número da camisa).
Preencha os dados do seu time, dos jogadores (só o nome e o número da camisa) e da comissão técnica (nome e função).
A organização confere e aprova antes do time entrar na competição.
</p>
@@ -433,12 +433,23 @@ data:
<button type="button" id="maisJogador" class="secundario">+ Jogador</button>
</fieldset>
<fieldset>
<legend>Comissão técnica</legend>
<div id="comissao"></div>
<datalist id="funcoes-comissao">
<option value="Técnico"></option><option value="Auxiliar técnico"></option><option value="Preparador físico"></option>
<option value="Preparador de goleiros"></option><option value="Médico"></option><option value="Fisioterapeuta"></option>
<option value="Massagista"></option><option value="Roupeiro"></option><option value="Dirigente"></option>
</datalist>
<button type="button" id="maisComissao" class="secundario">+ Pessoa na comissão</button>
</fieldset>
<p id="erro" class="erro" role="alert" hidden></p>
<button type="submit" id="enviar">Enviar time</button>
</form>
</main>
<script src="/publico/desafio.js?v=1"></script>
<script src="/publico/convite.js?v=2"></script>
<script src="/publico/convite.js?v=3"></script>
</body>
</html>
@@ -465,6 +476,7 @@ data:
var TOKEN = /^[A-Za-z0-9_-]{20,64}$/;
var MAX_JOGADORES = 60;
var MAX_COMISSAO = 15;
var token = decodeURIComponent((location.pathname.split("/").filter(Boolean).pop() || ""));
var base = "/api/public/competicoes/convites/" + encodeURIComponent(token);
@@ -473,6 +485,7 @@ data:
var situacao = document.getElementById("situacao");
var formulario = document.getElementById("formulario");
var listaJogadores = document.getElementById("jogadores");
var listaComissao = document.getElementById("comissao");
var erro = document.getElementById("erro");
var enviar = document.getElementById("enviar");
var ROTULO_ENVIAR = enviar.textContent;
@@ -520,6 +533,39 @@ data:
return linha;
}
/** Comissao tecnica (21/10/2026): nome e funcao; a funcao tem sugestoes, mas aceita texto livre. */
function linhaDeComissao(funcao) {
var linha = document.createElement("div");
linha.className = "linha-jogador linha-comissao";
var nome = document.createElement("input");
nome.placeholder = "Nome";
nome.maxLength = 120;
nome.setAttribute("aria-label", "Nome na comissão técnica");
nome.setAttribute("data-campo", "nome");
var papel = document.createElement("input");
papel.placeholder = "Função";
papel.maxLength = 40;
papel.value = funcao || "";
papel.setAttribute("list", "funcoes-comissao");
papel.setAttribute("aria-label", "Função na comissão técnica");
papel.setAttribute("data-campo", "funcao");
var tirar = document.createElement("button");
tirar.type = "button";
tirar.className = "secundario";
tirar.textContent = "×";
tirar.setAttribute("aria-label", "Remover da comissão");
tirar.addEventListener("click", function () { linha.remove(); });
linha.appendChild(nome);
linha.appendChild(papel);
linha.appendChild(tirar);
listaComissao.appendChild(linha);
return linha;
}
function valor(id) {
var v = document.getElementById(id).value.trim();
return v === "" ? null : v;
@@ -534,14 +580,26 @@ data:
var c = linhas[i].querySelector('[data-campo="camisa"]').value;
jogadores.push({ nome: nome, numeroCamisa: c === "" ? null : parseInt(c, 10) });
}
var comissao = [];
var faltaFuncao = false;
var pessoas = listaComissao.querySelectorAll(".linha-comissao");
for (var k = 0; k < pessoas.length; k++) {
var n = pessoas[k].querySelector('[data-campo="nome"]').value.trim();
if (!n) continue;
var f = pessoas[k].querySelector('[data-campo="funcao"]').value.trim();
if (!f) faltaFuncao = true;
comissao.push({ nome: n, funcao: f });
}
return {
faltaFuncao: faltaFuncao,
nome: valor("nome"),
escola: valor("escola"),
cidade: valor("cidade"),
contatoNome: valor("contatoNome"),
contatoTelefone: valor("contatoTelefone"),
contatoEmail: valor("contatoEmail"),
jogadores: jogadores
jogadores: jogadores,
comissao: comissao
};
}
@@ -555,6 +613,9 @@ data:
var corpo = montarCorpo();
if (!corpo.nome) { mostrarErro("Informe o nome do time."); return; }
if (corpo.jogadores.length > MAX_JOGADORES) { mostrarErro("No máximo " + MAX_JOGADORES + " jogadores."); return; }
if (corpo.faltaFuncao) { mostrarErro("Informe a função de cada pessoa da comissão técnica."); return; }
if (corpo.comissao.length > MAX_COMISSAO) { mostrarErro("No máximo " + MAX_COMISSAO + " pessoas na comissão técnica."); return; }
delete corpo.faltaFuncao;
enviar.disabled = true;
enviar.textContent = "Verificando…";
enviar.setAttribute("aria-busy", "true");
@@ -615,6 +676,7 @@ data:
situacao.className = "";
formulario.hidden = false;
for (var i = 0; i < 5; i++) linhaDeJogador();
linhaDeComissao("Técnico");
}).catch(function () {
recusar("Não foi possível abrir o convite agora. Tente de novo em instantes.");
});
@@ -623,6 +685,9 @@ data:
document.getElementById("maisJogador").addEventListener("click", function () {
if (listaJogadores.querySelectorAll(".linha-jogador").length < MAX_JOGADORES) linhaDeJogador().querySelector("input").focus();
});
document.getElementById("maisComissao").addEventListener("click", function () {
if (listaComissao.querySelectorAll(".linha-comissao").length < MAX_COMISSAO) linhaDeComissao("").querySelector("input").focus();
});
formulario.addEventListener("submit", aoEnviar);
iniciar();
})();
@@ -8242,6 +8307,557 @@ data:
</body>
</html>
time.css: |
/* Link do time: página pública, sem fonte nem script de fora (a CSP só deixa 'self'). Mobile primeiro. */
main { max-width: 56rem; }
.explica, .legenda { color: var(--muted); font-size: .9rem; }
.abas { display: flex; gap: .25rem; overflow-x: auto; border-bottom: 2px solid var(--line); margin: 0 0 1rem; }
.abas button { background: transparent; color: var(--navy); border: 0; border-bottom: 3px solid transparent; border-radius: 0; padding: .6rem .9rem; white-space: nowrap; font: inherit; font-weight: 700; cursor: pointer; }
.abas button[aria-selected="true"] { border-bottom-color: var(--orange); }
.abas button:focus-visible, button:focus-visible, input:focus-visible { outline: 3px solid var(--orange); outline-offset: 1px; }
h2 { font-size: 1.15rem; margin: .5rem 0; }
.rolagem { overflow-x: auto; }
table { border-collapse: collapse; width: 100%; font-size: .9rem; background: #fff; }
th, td { padding: .4rem .45rem; text-align: center; border-bottom: 1px solid var(--line); white-space: nowrap; }
th:nth-child(2), td:nth-child(2) { text-align: left; white-space: normal; min-width: 8rem; }
tr.meu { background: #fff4e5; font-weight: 700; }
.forma { display: inline-flex; gap: 3px; vertical-align: middle; }
.ponto { width: .8rem; height: .8rem; border-radius: 50%; display: inline-block; }
.ponto.V { background: #067647; }
.ponto.E { background: #98a2b3; }
.ponto.D { background: #b42318; }
.cartao { display: inline-block; width: .7rem; height: .95rem; border-radius: 2px; vertical-align: middle; margin-right: 3px; }
.cartao.amarelo { background: #f5c518; }
.cartao.vermelho { background: #d92d20; }
.jogo, .sumula { background: #fff; border: 1px solid var(--line); border-radius: 8px; padding: .6rem .8rem; margin: 0 0 .6rem; }
.jogo .quando { color: var(--muted); font-size: .85rem; }
.jogo .confronto { display: grid; grid-template-columns: 1fr auto 1fr; gap: .5rem; align-items: center; font-weight: 700; }
.jogo .confronto span:first-child { text-align: right; }
.jogo .placar { background: var(--navy); color: #fff; border-radius: 6px; padding: .1rem .6rem; }
.etiqueta { display: inline-block; font-size: .75rem; font-weight: 700; padding: .05rem .5rem; border-radius: 99px; background: #fff1e3; border: 1px solid var(--orange); margin-left: .4rem; }
.sumula h3 { margin: 0 0 .3rem; font-size: 1rem; }
.sumula ul { margin: .2rem 0 .5rem; padding-left: 1.2rem; }
form { display: grid; gap: 1rem; }
fieldset { border: 1px solid var(--line); border-radius: 8px; background: #fff; padding: .75rem 1rem 1rem; display: grid; gap: .6rem; }
legend { font-weight: 700; padding: 0 .4rem; }
input { font: inherit; padding: .5rem .6rem; border: 1px solid var(--line); border-radius: 6px; width: 100%; }
.linha-ficha { display: grid; grid-template-columns: 1fr 5.5rem auto; gap: .4rem; align-items: center; }
.linha-ficha button { padding: .4rem .6rem; }
button { font: inherit; font-weight: 700; padding: .65rem 1.1rem; border: 0; border-radius: 8px; background: var(--orange); color: #fff; cursor: pointer; }
button.secundario { background: #fff; color: var(--navy); border: 1px solid var(--line); justify-self: start; }
button:disabled { opacity: .55; cursor: not-allowed; }
.erro { color: #b42318; margin: 0; font-weight: 600; }
.ok { background: #e7f6ec; border-left: 4px solid #067647; padding: .8rem 1rem; margin: 0; }
#situacao.invalido { background: #fff1e3; border-left: 4px solid var(--orange); padding: 1rem 1.2rem; }
.somente-leitura li { margin: .15rem 0; }
@media (max-width: 30rem) { .linha-ficha { grid-template-columns: 1fr 4.5rem auto; } }
time.html: |
<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow">
<meta name="referrer" content="no-referrer">
<title>Seu time na competição — Athletic Map</title>
<link rel="stylesheet" href="/publico/_estilo.css">
<link rel="stylesheet" href="/publico/time.css?v=1">
</head>
<body>
<header><div class="marca">Athletic <span>Map</span></div></header>
<main>
<h1 id="titulo">Seu time na competição</h1>
<p id="situacao" role="status" aria-live="polite">Abrindo o link…</p>
<div id="painel" hidden>
<div class="abas" role="tablist" aria-label="Seções">
<button type="button" role="tab" id="aba-tabela" aria-controls="sec-tabela" aria-selected="true" data-aba="tabela">Tabela</button>
<button type="button" role="tab" id="aba-jogos" aria-controls="sec-jogos" aria-selected="false" tabindex="-1" data-aba="jogos">Jogos</button>
<button type="button" role="tab" id="aba-sumulas" aria-controls="sec-sumulas" aria-selected="false" tabindex="-1" data-aba="sumulas">Súmulas</button>
<button type="button" role="tab" id="aba-ficha" aria-controls="sec-ficha" aria-selected="false" tabindex="-1" data-aba="ficha">Ficha do time</button>
</div>
<section id="sec-tabela" role="tabpanel" aria-labelledby="aba-tabela">
<h2>Tabela</h2>
<div class="rolagem" id="tabela"></div>
<p class="legenda">P pontos · J jogos · V vitórias · E empates · D derrotas · GP gols pró · GC gols contra · SG saldo de gols · % aproveitamento · CA cartões amarelos · CV cartões vermelhos</p>
</section>
<section id="sec-jogos" role="tabpanel" aria-labelledby="aba-jogos" hidden>
<h2>Jogos</h2>
<div id="jogos"></div>
</section>
<section id="sec-sumulas" role="tabpanel" aria-labelledby="aba-sumulas" hidden>
<h2>Súmulas</h2>
<div id="sumulas"></div>
</section>
<section id="sec-ficha" role="tabpanel" aria-labelledby="aba-ficha" hidden>
<h2>Ficha do time</h2>
<p id="ficha-aviso" class="explica"></p>
<form id="ficha" novalidate>
<fieldset>
<legend>Jogadores</legend>
<div id="jogadores"></div>
<button type="button" id="maisJogador" class="secundario">+ Jogador</button>
</fieldset>
<fieldset>
<legend>Comissão técnica</legend>
<div id="comissao"></div>
<datalist id="funcoes-comissao">
<option value="Técnico"></option><option value="Auxiliar técnico"></option><option value="Preparador físico"></option>
<option value="Preparador de goleiros"></option><option value="Médico"></option><option value="Fisioterapeuta"></option>
<option value="Massagista"></option><option value="Roupeiro"></option><option value="Dirigente"></option>
</datalist>
<button type="button" id="maisComissao" class="secundario">+ Pessoa na comissão</button>
</fieldset>
<p id="ficha-erro" class="erro" role="alert" hidden></p>
<p id="ficha-ok" class="ok" role="status" hidden></p>
<button type="submit" id="salvar">Salvar ficha</button>
</form>
</section>
</div>
</main>
<script src="/publico/time.js?v=1"></script>
</body>
</html>
time.js: |
/* ===========================================================================
* Link do time — a página que UMA pessoa do time abre pelo link com chave.
*
* Sem login: a chave no endereço é o único segredo. A página mostra a tabela,
* os jogos e as súmulas da competição e a ficha do time; se o time for de
* fora e ainda não houver jogo encerrado, a ficha pode ser corrigida aqui.
*
* Tudo o que vem do servidor entra na página com `textContent` — nunca como
* HTML. O script é externo porque a CSP é `script-src 'self'`.
* =========================================================================== */
(function () {
"use strict";
var TOKEN = /^[A-Za-z0-9_-]{20,64}$/;
var MAX_JOGADORES = 60;
var MAX_COMISSAO = 15;
var ABAS = ["tabela", "jogos", "sumulas", "ficha"];
var ROTULOS_FORMA = { V: "Vitória", E: "Empate", D: "Derrota" };
var ROTULOS_STATUS = {
AGENDADA: "Agendado", ADIADA: "Adiado", EM_ANDAMENTO: "Em andamento", ENCERRADA: "Encerrado", CANCELADA: "Cancelado"
};
var token = decodeURIComponent(location.pathname.split("/").filter(Boolean).pop() || "");
var base = "/api/public/competicoes/times/" + encodeURIComponent(token);
var titulo = document.getElementById("titulo");
var situacao = document.getElementById("situacao");
var painel = document.getElementById("painel");
var listaJogadores = document.getElementById("jogadores");
var listaComissao = document.getElementById("comissao");
var formFicha = document.getElementById("ficha");
var fichaErro = document.getElementById("ficha-erro");
var fichaOk = document.getElementById("ficha-ok");
var salvar = document.getElementById("salvar");
var visao = null;
function el(tag, classe, texto) {
var e = document.createElement(tag);
if (classe) e.className = classe;
if (texto !== undefined && texto !== null) e.textContent = String(texto);
return e;
}
function limpar(no) {
while (no.firstChild) no.removeChild(no.firstChild);
}
function recusar(texto) {
situacao.textContent = texto;
situacao.className = "invalido";
painel.hidden = true;
}
function lerResposta(r) {
return r.json().catch(function () { return {}; }).then(function (corpo) { return { status: r.status, corpo: corpo }; });
}
// ------------------------------------------------------------------ abas
function abrirAba(nome, focar) {
for (var i = 0; i < ABAS.length; i++) {
var ativa = ABAS[i] === nome;
var aba = document.getElementById("aba-" + ABAS[i]);
aba.setAttribute("aria-selected", ativa ? "true" : "false");
aba.tabIndex = ativa ? 0 : -1;
document.getElementById("sec-" + ABAS[i]).hidden = !ativa;
if (ativa && focar) aba.focus();
}
}
function aoTeclarNaAba(ev) {
var i = ABAS.indexOf(ev.currentTarget.getAttribute("data-aba"));
if (ev.key === "ArrowRight") { abrirAba(ABAS[(i + 1) % ABAS.length], true); ev.preventDefault(); }
else if (ev.key === "ArrowLeft") { abrirAba(ABAS[(i + ABAS.length - 1) % ABAS.length], true); ev.preventDefault(); }
}
// ------------------------------------------------------------------ tabela
function dataBr(iso) {
if (!iso) return "Data a definir";
var d = new Date(iso);
if (isNaN(d.getTime())) return "Data a definir";
try {
return d.toLocaleString("pt-BR", { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit" });
} catch (e) {
return d.toISOString();
}
}
function percentual(n) {
return (Math.round((n || 0) * 10) / 10).toString().replace(".", ",") + "%";
}
function montarTabela(linhas, meuNome) {
var alvo = document.getElementById("tabela");
limpar(alvo);
if (!linhas || !linhas.length) { alvo.appendChild(el("p", "explica", "A tabela aparece quando houver times e jogos.")); return; }
var t = el("table");
t.appendChild(el("caption", "explica", "Classificação"));
var cab = el("tr");
var cols = [["#", "Posição"], ["Time", "Time"], ["P", "Pontos"], ["J", "Jogos"], ["V", "Vitórias"], ["E", "Empates"],
["D", "Derrotas"], ["GP", "Gols pró"], ["GC", "Gols contra"], ["SG", "Saldo de gols"], ["%", "Aproveitamento"],
["Últimos 5", "Últimos 5 jogos"], ["CA", "Cartões amarelos"], ["CV", "Cartões vermelhos"]];
cols.forEach(function (c) {
var th = el("th", null, c[0]);
th.scope = "col";
th.title = c[1];
cab.appendChild(th);
});
var thead = el("thead");
thead.appendChild(cab);
t.appendChild(thead);
var corpo = el("tbody");
linhas.forEach(function (l) {
var tr = el("tr", l.time === meuNome ? "meu" : "");
[l.posicao, l.time, l.pontos, l.jogos, l.vitorias, l.empates, l.derrotas, l.golsPro, l.golsContra, l.saldoGols,
percentual(l.aproveitamento)].forEach(function (v) { tr.appendChild(el("td", null, v)); });
var forma = el("td");
var caixa = el("span", "forma");
(l.ultimos || []).forEach(function (r) {
var p = el("span", "ponto " + r);
p.title = ROTULOS_FORMA[r] || r;
p.setAttribute("role", "img");
p.setAttribute("aria-label", ROTULOS_FORMA[r] || r);
caixa.appendChild(p);
});
forma.appendChild(caixa);
tr.appendChild(forma);
var am = el("td");
am.appendChild(el("span", "cartao amarelo"));
am.appendChild(document.createTextNode(String(l.amarelos || 0)));
tr.appendChild(am);
var vm = el("td");
vm.appendChild(el("span", "cartao vermelho"));
vm.appendChild(document.createTextNode(String(l.vermelhos || 0)));
tr.appendChild(vm);
corpo.appendChild(tr);
});
t.appendChild(corpo);
alvo.appendChild(t);
}
// ------------------------------------------------------------------ jogos
function montarJogos(jogos) {
var alvo = document.getElementById("jogos");
limpar(alvo);
if (!jogos || !jogos.length) { alvo.appendChild(el("p", "explica", "Ainda não há jogos marcados.")); return; }
jogos.forEach(function (j) {
var card = el("div", "jogo");
var quando = el("div", "quando", (j.rodada ? j.rodada + " · " : "") + dataBr(j.data));
var etiqueta = el("span", "etiqueta", ROTULOS_STATUS[j.status] || j.status);
quando.appendChild(etiqueta);
if (j.wo) quando.appendChild(el("span", "etiqueta", "W.O."));
card.appendChild(quando);
var linha = el("div", "confronto");
linha.appendChild(el("span", null, j.mandante));
var temPlacar = j.placarMandante !== undefined && j.placarMandante !== null;
linha.appendChild(el("span", temPlacar ? "placar" : "", temPlacar ? j.placarMandante + " x " + j.placarVisitante : "x"));
linha.appendChild(el("span", null, j.visitante));
card.appendChild(linha);
if (j.penaltis) card.appendChild(el("div", "quando", "Pênaltis: " + j.penaltis.mandante + " x " + j.penaltis.visitante));
if (j.motivo) card.appendChild(el("div", "quando", "Motivo: " + j.motivo));
alvo.appendChild(card);
});
}
// ------------------------------------------------------------------ súmulas
function pessoa(p) {
if (!p) return "jogador não identificado";
if (p.nome && p.numeroCamisa !== undefined && p.numeroCamisa !== null) return p.nome + " (" + p.numeroCamisa + ")";
if (p.nome) return p.nome;
if (p.numeroCamisa !== undefined && p.numeroCamisa !== null) return "camisa " + p.numeroCamisa;
return "jogador não identificado";
}
function momento(e) {
var m = e.minuto !== undefined && e.minuto !== null ? e.minuto + "'" : "";
var t = e.tempo ? e.tempo + "º tempo" : "";
return [t, m].filter(Boolean).join(" ") || "—";
}
function secaoDaSumula(card, nome, itens, texto) {
if (!itens || !itens.length) return;
card.appendChild(el("strong", null, nome));
var ul = el("ul");
itens.forEach(function (i) { ul.appendChild(el("li", null, texto(i))); });
card.appendChild(ul);
}
function montarSumulas(sumulas, jogos) {
var alvo = document.getElementById("sumulas");
limpar(alvo);
if (!sumulas || !sumulas.length) { alvo.appendChild(el("p", "explica", "As súmulas dos jogos do seu time aparecem aqui.")); return; }
sumulas.forEach(function (s) {
var j = (jogos || []).filter(function (x) { return x.ref === s.jogoRef; })[0];
var card = el("div", "sumula");
var cab = j ? j.mandante + " x " + j.visitante : "Jogo " + s.jogoRef;
if (j && j.placarMandante !== undefined && j.placarMandante !== null) {
cab = j.mandante + " " + j.placarMandante + " x " + j.placarVisitante + " " + j.visitante;
}
card.appendChild(el("h3", null, cab));
if (j) card.appendChild(el("div", "quando", (j.rodada ? j.rodada + " · " : "") + dataBr(j.data)));
secaoDaSumula(card, "Gols", s.gols, function (g) { return momento(g) + " · " + g.time + " · " + pessoa(g.jogador); });
secaoDaSumula(card, "Cartões", s.cartoes, function (c) {
return momento(c) + " · " + (c.tipo === "VERMELHO" ? "vermelho" : "amarelo") + " · " + c.time + " · " + pessoa(c.jogador);
});
secaoDaSumula(card, "Substituições", s.substituicoes, function (t) {
return momento(t) + " · " + t.time + " · sai " + pessoa(t.sai) + ", entra " + pessoa(t.entra);
});
alvo.appendChild(card);
});
}
// ------------------------------------------------------------------ ficha
function linhaDeJogador(j) {
var linha = el("div", "linha-ficha linha-jogador");
if (j && j.id) linha.setAttribute("data-id", j.id);
var nome = el("input");
nome.placeholder = "Nome do jogador";
nome.maxLength = 120;
nome.value = (j && j.nome) || "";
nome.setAttribute("aria-label", "Nome do jogador");
nome.setAttribute("data-campo", "nome");
var camisa = el("input");
camisa.type = "number";
camisa.min = "0";
camisa.max = "999";
camisa.placeholder = "nº";
camisa.value = j && j.numeroCamisa !== undefined && j.numeroCamisa !== null ? j.numeroCamisa : "";
camisa.setAttribute("aria-label", "Número da camisa");
camisa.setAttribute("data-campo", "camisa");
var tirar = el("button", "secundario", "×");
tirar.type = "button";
tirar.setAttribute("aria-label", "Remover jogador");
tirar.addEventListener("click", function () { linha.remove(); });
linha.appendChild(nome);
linha.appendChild(camisa);
linha.appendChild(tirar);
listaJogadores.appendChild(linha);
return linha;
}
function linhaDeComissao(m) {
var linha = el("div", "linha-ficha linha-comissao");
if (m && m.id) linha.setAttribute("data-id", m.id);
var nome = el("input");
nome.placeholder = "Nome";
nome.maxLength = 120;
nome.value = (m && m.nome) || "";
nome.setAttribute("aria-label", "Nome na comissão técnica");
nome.setAttribute("data-campo", "nome");
var papel = el("input");
papel.placeholder = "Função";
papel.maxLength = 40;
papel.value = (m && m.funcao) || "";
papel.setAttribute("list", "funcoes-comissao");
papel.setAttribute("aria-label", "Função na comissão técnica");
papel.setAttribute("data-campo", "funcao");
var tirar = el("button", "secundario", "×");
tirar.type = "button";
tirar.setAttribute("aria-label", "Remover da comissão");
tirar.addEventListener("click", function () { linha.remove(); });
linha.appendChild(nome);
linha.appendChild(papel);
linha.appendChild(tirar);
listaComissao.appendChild(linha);
return linha;
}
/** Time da escola ou ficha fechada: só leitura (listas simples). */
function fichaSomenteLeitura(ficha) {
formFicha.hidden = true;
var alvo = document.getElementById("ficha-leitura");
if (!alvo) {
alvo = el("div", "somente-leitura");
alvo.id = "ficha-leitura";
formFicha.parentNode.appendChild(alvo);
}
limpar(alvo);
alvo.appendChild(el("h3", null, "Jogadores"));
var ul = el("ul");
(ficha.jogadores || []).forEach(function (j) {
ul.appendChild(el("li", null, pessoa({ nome: j.nome, numeroCamisa: j.numeroCamisa })));
});
if (!ul.childNodes.length) ul.appendChild(el("li", null, "Nenhum jogador na ficha."));
alvo.appendChild(ul);
if ((ficha.comissao || []).length) {
alvo.appendChild(el("h3", null, "Comissão técnica"));
var uc = el("ul");
ficha.comissao.forEach(function (m) { uc.appendChild(el("li", null, m.nome + " — " + m.funcao)); });
alvo.appendChild(uc);
}
}
function montarFicha(ficha) {
var aviso = document.getElementById("ficha-aviso");
limpar(listaJogadores);
limpar(listaComissao);
if (!ficha.editavel) {
aviso.textContent = "Esta ficha está fechada para alterações aqui. Se algo estiver errado, fale com a organização.";
fichaSomenteLeitura(ficha);
return;
}
aviso.textContent = "Confira os jogadores (nome e número da camisa) e a comissão técnica. Dá para corrigir até o primeiro jogo ser encerrado.";
formFicha.hidden = false;
(ficha.jogadores || []).forEach(linhaDeJogador);
if (!(ficha.jogadores || []).length) for (var i = 0; i < 3; i++) linhaDeJogador();
(ficha.comissao || []).forEach(linhaDeComissao);
}
function mostrarErroFicha(texto) {
fichaErro.textContent = texto || "";
fichaErro.hidden = !texto;
}
function mostrarOkFicha(texto) {
fichaOk.textContent = texto || "";
fichaOk.hidden = !texto;
}
function corpoDaFicha() {
var jogadores = [];
var linhas = listaJogadores.querySelectorAll(".linha-jogador");
for (var i = 0; i < linhas.length; i++) {
var nome = linhas[i].querySelector('[data-campo="nome"]').value.trim();
if (!nome) continue;
var c = linhas[i].querySelector('[data-campo="camisa"]').value;
var j = { nome: nome, numeroCamisa: c === "" ? null : parseInt(c, 10) };
if (linhas[i].getAttribute("data-id")) j.id = linhas[i].getAttribute("data-id");
jogadores.push(j);
}
var comissao = [];
var faltaFuncao = false;
var pessoas = listaComissao.querySelectorAll(".linha-comissao");
for (var k = 0; k < pessoas.length; k++) {
var n = pessoas[k].querySelector('[data-campo="nome"]').value.trim();
if (!n) continue;
var f = pessoas[k].querySelector('[data-campo="funcao"]').value.trim();
if (!f) faltaFuncao = true;
var m = { nome: n, funcao: f };
if (pessoas[k].getAttribute("data-id")) m.id = pessoas[k].getAttribute("data-id");
comissao.push(m);
}
return { jogadores: jogadores, comissao: comissao, faltaFuncao: faltaFuncao };
}
function aoSalvar(ev) {
ev.preventDefault();
mostrarErroFicha("");
mostrarOkFicha("");
var c = corpoDaFicha();
if (c.jogadores.length > MAX_JOGADORES) { mostrarErroFicha("No máximo " + MAX_JOGADORES + " jogadores."); return; }
if (c.faltaFuncao) { mostrarErroFicha("Informe a função de cada pessoa da comissão técnica."); return; }
if (c.comissao.length > MAX_COMISSAO) { mostrarErroFicha("No máximo " + MAX_COMISSAO + " pessoas na comissão técnica."); return; }
salvar.disabled = true;
salvar.setAttribute("aria-busy", "true");
fetch(base + "/ficha", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ jogadores: c.jogadores, comissao: c.comissao })
}).then(lerResposta).then(function (r) {
salvar.disabled = false;
salvar.removeAttribute("aria-busy");
if (r.status >= 200 && r.status < 300) {
mostrarOkFicha("Ficha salva.");
return carregar(true);
}
if (r.status === 404) { recusar("Este link não está mais disponível."); return null; }
mostrarErroFicha((r.corpo && r.corpo.detail) || "Não foi possível salvar agora. Tente de novo em instantes.");
return null;
}).catch(function () {
salvar.disabled = false;
salvar.removeAttribute("aria-busy");
mostrarErroFicha("Sem conexão. Confira a internet e tente de novo.");
});
}
// ------------------------------------------------------------------ carga
function mostrar(v, manterAba) {
visao = v;
var nomeTime = (v.time && v.time.nome) || "Seu time";
titulo.textContent = (v.competicao || "Competição") + " — " + nomeTime;
document.title = titulo.textContent + " — Athletic Map";
var partes = [];
if (v.time && v.time.escola) partes.push(v.time.escola);
if (v.time && v.time.cidade) partes.push(v.time.cidade);
if (v.temporada && v.temporada.nome) partes.push("Temporada " + v.temporada.nome);
if (v.time && v.time.status === "DESQUALIFICADO") partes.push("Time desqualificado");
situacao.textContent = partes.join(" · ");
situacao.className = "";
painel.hidden = false;
montarTabela(v.classificacao, nomeTime);
montarJogos(v.jogos);
montarSumulas(v.sumulas, v.jogos);
montarFicha(v.ficha || { jogadores: [], comissao: [], editavel: false });
if (!manterAba) abrirAba("tabela", false);
}
function carregar(manterAba) {
return fetch(base).then(lerResposta).then(function (r) {
if (r.status === 404) { recusar("Este link não está disponível ou foi desativado. Peça um novo à organização."); return; }
if (r.status === 429) { recusar("Muitas consultas seguidas. Aguarde alguns minutos e abra o link de novo."); return; }
if (r.status !== 200) { recusar("Não foi possível abrir agora. Tente de novo em instantes."); return; }
mostrar(r.corpo || {}, manterAba);
}).catch(function () {
recusar("Não foi possível abrir agora. Tente de novo em instantes.");
});
}
function iniciar() {
if (!TOKEN.test(token)) { recusar("Este endereço não é válido."); return; }
ABAS.forEach(function (nome) {
var aba = document.getElementById("aba-" + nome);
aba.addEventListener("click", function () { abrirAba(nome, false); });
aba.addEventListener("keydown", aoTeclarNaAba);
});
document.getElementById("maisJogador").addEventListener("click", function () {
if (listaJogadores.querySelectorAll(".linha-jogador").length < MAX_JOGADORES) linhaDeJogador().querySelector("input").focus();
});
document.getElementById("maisComissao").addEventListener("click", function () {
if (listaComissao.querySelectorAll(".linha-comissao").length < MAX_COMISSAO) linhaDeComissao(null).querySelector("input").focus();
});
formFicha.addEventListener("submit", aoSalvar);
carregar(false);
}
iniciar();
})();
---
apiVersion: v1
kind: ConfigMap