Fixed table of contents issues
This commit is contained in:
parent
450ac1b7e8
commit
0bf0ba54a8
11 changed files with 495 additions and 123 deletions
|
|
@ -1,10 +1,12 @@
|
|||
## Sobre o Anti-Doping/Clean Sport
|
||||
# Anti-Doping
|
||||
|
||||
> Conteúdo traduzido do site da IPF. Pode consultar a página da IPF [aqui.](https://www.powerlifting.sport/anti-doping/about-anti-doping-/-clean-sport). Informação foi adicionada a esta página.
|
||||
|
||||
## Sobre o Anti-Doping/Clean Sport
|
||||
|
||||
### Introdução
|
||||
|
||||
**A IPF ESTÁ COMPROMETIDA COM O SEU PROGRAMA E COM O CUMPRIMENTO DAS SUAS OBRIGAÇÕES DE CONFORMIDADE COM O CÓDIGO MUNDIAL DE ANTI-DOPING**
|
||||
**A IPF ESTÁ COMPROMETIDA COM O SEU PROGRAMA E COM O CUMPRIMENTO DAS SUAS OBRIGAÇÕES DE CONFORMIDADE COM O CÓDIGO MUNDIAL DE ANTI-DOPING**
|
||||
|
||||
O *doping* pode ser prejudicial para a saúde de um atleta, prejudica a integridade do desporto e é moral e eticamente errado. Todos os atletas da IPF e qualquer atleta que pretenda participar em competições sob a jurisdição da IPF devem cumprir as Regras Anti-Doping da IPF.
|
||||
O texto abaixo oferece respostas a questões importantes relacionadas com o antidoping.
|
||||
|
|
@ -48,4 +50,164 @@ Ao educar os nossos atletas, treinadores, pessoal de apoio, equipas médicas e a
|
|||
|
||||
É também o seu papel denunciar o doping se o presenciar: pode ajudar a proteger o atleta que está a tomar uma substância proibida, bem como o seu desporto, ao defender os valores do desporto limpo.
|
||||
|
||||
### O que é o doping?
|
||||
### O que é o *doping*?
|
||||
|
||||
A dopagem (*doping*) não é apenas um teste positivo que mostra a presença de uma substância proibida numa amostra de urina de um atleta. A dopagem é definida como a ocorrência de uma ou mais das 11 Violações das Regras Antidopagem (VRA) descritas no [Código Mundial Antidopagem](https://www.wada-ama.org/en/resources/the-code/world-anti-doping-code) e nas [Regras Antidopagem da IPF IPF Anti-Doping Rules - International Powerlifting Federation IPF](https://www.powerlifting.sport/anti-doping/ipf-anti-doping-rules)
|
||||
|
||||
Estas são:
|
||||
|
||||
1. Presença de uma substância proibida, seus metabolitos ou marcadores na amostra de um atleta
|
||||
2. Uso ou tentativa de uso de uma substância ou método proibido por um atleta
|
||||
3. Recusa, evasão ou não submissão à recolha de amostras por um atleta
|
||||
4. Falha no fornecimento de informações de paradeiro e/ou testes falhados por um atleta
|
||||
5. Adulteração ou tentativa de adulteração do processo de controlo de dopagem por um atleta ou outra pessoa
|
||||
6. Posse de uma substância ou método proibido por um atleta ou pessoal de apoio ao atleta
|
||||
7. Tráfico ou tentativa de tráfico de uma substância ou método proibido por um atleta ou outra pessoa
|
||||
8. Administração ou tentativa de administração de uma substância ou método proibido a um atleta
|
||||
9. Cumplicidade ou tentativa de cumplicidade numa VRA por um atleta ou outra pessoa
|
||||
10. Associação Proibida por um atleta ou outra pessoa com pessoal de apoio ao atleta sancionado
|
||||
11. Atos para desencorajar ou retaliar contra denúncias às autoridades
|
||||
|
||||
### Porque é que a dopagem no desporto é proibida?
|
||||
|
||||
O uso de [substâncias de dopagem ou métodos de dopagem](https://www.wada-ama.org/en/prohibited-list#search-anchor) para melhorar o desempenho é fundamentalmente errado e é prejudicial ao espírito geral do desporto. O uso indevido de substâncias pode ser prejudicial para a saúde de um atleta e para outros atletas que competem no desporto. Prejudica severamente a integridade, imagem e valor do desporto, quer a motivação para usar substâncias seja ou não melhorar o desempenho. Para alcançar integridade e justiça no desporto, um compromisso com o desporto limpo é fundamental.
|
||||
|
||||
### O que significa 'Responsabilidade Estrita'?
|
||||
|
||||
* O princípio da responsabilidade estrita aplica-se a todos os atletas que competem em qualquer desporto com um programa antidopagem. Significa que os atletas são responsáveis por qualquer substância proibida, ou seus metabolitos ou marcadores, que sejam encontrados presentes na sua amostra de urina e/ou sangue recolhida durante o controlo de dopagem, independentemente de o atleta ter usado intencional ou não intencionalmente uma substância ou método proibido. Portanto, é importante lembrar que é responsabilidade última de cada atleta saber o que entra no seu corpo.
|
||||
* A regra que estabelece esse princípio, ao abrigo do [Artigo 2.1 e Artigo 2.2](https://www.wada-ama.org/en/resources/the-code/world-anti-doping-code) do Código, declara que não é necessário que a intenção, culpa, negligência ou uso consciente por parte do atleta seja demonstrado pela Organização Antidopagem para estabelecer uma violação das regras antidopagem.
|
||||
|
||||
### Porque é que a dopagem é perigosa?
|
||||
|
||||
A dopagem pode resultar em graves consequências para a saúde, mas também acarreta consequências desportivas, sociais, financeiras e legais. Para um atleta, a dopagem pode significar o fim da sua carreira desportiva, reputação e perspetivas tanto dentro como fora do desporto.
|
||||
|
||||
#### Consequências Desportivas
|
||||
|
||||
As sanções por uma Violação das Regras Antidopagem (VRA) podem incluir:
|
||||
* Suspensão Provisória. O atleta ou outra pessoa é temporariamente proibido de participar em qualquer competição ou atividade enquanto aguarda a conclusão do processo de gestão de resultados ou até que a decisão final seja proferida.
|
||||
* Inelegibilidade. O atleta ou outra pessoa não é autorizado a competir ou participar em qualquer outra atividade, como treino, coaching, ou mesmo acesso a financiamento devido a uma VRA. Este período de inelegibilidade pode ser de até 4 anos ou mesmo vitalício, dependendo das circunstâncias da VRA.
|
||||
* Desqualificação de resultados. Os resultados do atleta durante um determinado período, competição ou evento são invalidados, o que implica a perda de quaisquer medalhas, pontos e prémios.
|
||||
* Divulgação Pública. A Organização Antidopagem (OAD) informa o público em geral da VRA.
|
||||
* Multas.
|
||||
|
||||
#### Consequências para a Saúde
|
||||
|
||||
As consequências para a saúde de um atleta podem incluir:
|
||||
* Saúde física. Medicamentos e intervenções médicas foram desenvolvidos para tratar uma condição ou doença particular. Não um atleta saudável. Dependendo da substância, da dosagem e da frequência de consumo, os produtos de dopagem podem ter efeitos secundários particularmente negativos na saúde.
|
||||
* Saúde psicológica. Algumas substâncias de dopagem podem ter um impacto na saúde mental do atleta. Ansiedade, perturbações obsessivo-compulsivas ou psicose são consequências diretas da dopagem.
|
||||
|
||||
#### Consequências Sociais
|
||||
|
||||
Algumas das consequências sociais da dopagem incluem:
|
||||
* Danos à reputação e imagem, que podem ser permanentes com a atenção dos media, e futuros desempenhos limpos podem ser recebidos com ceticismo.
|
||||
* Danos às perspetivas de carreira futura.
|
||||
* Isolamento dos pares e do desporto.
|
||||
* Relações danificadas com amigos e família.
|
||||
* Efeitos no bem-estar emocional e psicológico.
|
||||
* Perda de estatuto, fama, respeito e credibilidade.
|
||||
|
||||
#### Consequências Financeiras
|
||||
|
||||
As consequências financeiras da dopagem podem incluir:
|
||||
* Multas que uma Organização Antidopagem (OAD) pode ter incluído nas suas regras antidopagem, incluindo custos associados a uma Violação das Regras Antidopagem (VRA).
|
||||
* Perda de rendimento/apoio financeiro, tal como financiamento governamental, outras formas de apoio financeiro e por não participar nas competições.
|
||||
* Perda de apoio financeiro devido à retirada de patrocinador.
|
||||
* Requisito de reembolsar patrocinador, se incluído no contrato.
|
||||
* Reembolso de prémios monetários.
|
||||
* Impacto da reputação danificada nas perspetivas de carreira futura.
|
||||
|
||||
#### Consequências Legais
|
||||
|
||||
Para além das consequências desportivas, de saúde, sociais e financeiras listadas acima, a dopagem pode acarretar outras consequências legais, tais como:
|
||||
* Alguns países foram além do Código Mundial Antidopagem e tornaram o uso de uma substância proibida um crime (por exemplo, Áustria, Itália, França).
|
||||
* Em alguns países, VRA relacionadas com tráfico, posse ou administração de uma substância proibida ou algumas substâncias na Lista de Substâncias Proibidas são consideradas um crime.
|
||||
|
||||
### O que os atletas e o pessoal de apoio ao atleta precisam de saber sobre antidopagem?
|
||||
|
||||
Os atletas, o seu pessoal de apoio e outros que estão sujeitos às regras antidopagem têm todos direitos e responsabilidades ao abrigo do Código Mundial Antidopagem (Código). A Parte Três do Código descreve todos os papéis e responsabilidades de cada parte interessada no sistema antidopagem.
|
||||
|
||||
#### Direitos dos Atletas
|
||||
|
||||
"Cada atleta tem o direito ao desporto limpo!"
|
||||
|
||||
Garantir que os atletas estão conscientes dos seus direitos e que estes direitos são respeitados é vital para o sucesso do desporto limpo. O Comité de Atletas da AMA (agora Conselho de Atletas) redigiu a Lei dos Direitos Antidopagem dos Atletas (Lei). Esta Lei é composta por duas partes. A primeira parte estabelece direitos que se encontram no Código e nas Normas Internacionais. A segunda parte estabelece direitos de atletas recomendados que não se encontram no Código ou nas Normas Internacionais, mas são direitos que os atletas recomendam que as Organizações Antidopagem (OAD) adotem como melhores práticas.
|
||||
|
||||
Os direitos dos atletas descritos no Código incluem:
|
||||
|
||||
Oportunidades iguais na sua busca pelo desporto, livre da participação de outros atletas que fazem dopagem
|
||||
Programas de testes equitativos e justos
|
||||
Um processo de candidatura para Autorização de Uso Terapêutico (AUT)
|
||||
Ser ouvido, ter uma audiência justa dentro de um prazo razoável por um painel de audiência justo, imparcial e operacionalmente independente, com uma decisão fundamentada oportuna que inclua especificamente uma explicação das razões da decisão
|
||||
Direito de recorrer da decisão da audiência
|
||||
Qualquer OAD que tenha jurisdição sobre eles será responsável pelas suas ações e um atleta terá a capacidade de denunciar qualquer questão de conformidade
|
||||
Capacidade de denunciar Violações das Regras Antidopagem (VRA) através de um mecanismo anónimo e não ser sujeito a ameaças ou intimidação
|
||||
Receber educação antidopagem
|
||||
Tratamento justo das suas informações pessoais pelas OAD de acordo com a Norma Internacional para a Proteção da Privacidade e Informação Pessoal (NIPPIP) e qualquer lei local aplicável
|
||||
Procurar indemnização de outro atleta cujas ações tenham prejudicado esse atleta através da prática de uma VRA
|
||||
Durante o processo de recolha de amostras, direito a:
|
||||
Ver a identificação do Oficial de Controlo de Dopagem (OCD)
|
||||
Solicitar informações adicionais sobre o processo de recolha de amostras, sobre a autoridade sob a qual será realizado e sobre o tipo de recolha de amostras
|
||||
Hidratar-se
|
||||
Ser acompanhado por um representante e, se disponível, um intérprete
|
||||
Solicitar um atraso na apresentação na estação de controlo de dopagem por razões válidas (Norma Internacional para Testes e Investigações Art. 5.4.4)
|
||||
Solicitar modificações para atletas com deficiências (se aplicável)
|
||||
Ser informado dos seus direitos e responsabilidades
|
||||
Receber uma cópia dos registos do processo
|
||||
Ter proteções adicionais para "pessoas protegidas" devido à sua idade ou falta de capacidade legal
|
||||
Solicitar e assistir à análise da amostra B (no caso de um Resultado Analítico Adverso)
|
||||
|
||||
|
||||
|
||||
Responsabilidades dos Atletas
|
||||
|
||||
Os direitos dos atletas ao desporto limpo vêm com responsabilidades correspondentes, e os atletas podem ser testados dentro e fora de competição, a qualquer momento, em qualquer lugar e sem aviso prévio.
|
||||
|
||||
As suas responsabilidades no desporto limpo incluem (mas não se limitam a):
|
||||
|
||||
Cumprir as Regras Antidopagem da IPF IPF Anti-Doping Rules - International Powerlifting Federation IPF (em conformidade com o Código Mundial Antidopagem)
|
||||
Estar disponível para recolha de amostras (urina, sangue ou mancha de sangue seco (MSS)), seja dentro ou fora de competição
|
||||
Permanecer sob observação direta do Oficial de Controlo de Dopagem (OCD) ou acompanhante em todos os momentos desde a notificação até à conclusão do processo de recolha de amostras
|
||||
Fornecer identificação mediante solicitação durante o processo de recolha de amostras
|
||||
Garantir que nenhuma substância proibida entra no seu corpo e que nenhum método proibido é usado neles
|
||||
Garantir que qualquer tratamento não é proibido de acordo com a 2026 Prohibited List | World Anti Doping Agency em vigor e verificar isto com os médicos prescritores, ou diretamente com a IPF se necessário
|
||||
Candidatar-se à IPF ou à sua ONAD se nenhum tratamento alternativo permitido for possível e uma Autorização de Uso Terapêutico (AUT) for necessária (ver TUEs - International Powerlifting Federation IPF)
|
||||
Apresentar-se imediatamente para recolha de amostras após ser notificado de ter sido selecionado para controlo de dopagem
|
||||
Garantir a exatidão das informações inseridas no Formulário de Controlo de Dopagem (FCD)
|
||||
Cooperar com OAD que investigam VRA
|
||||
Não trabalhar com treinadores, preparadores, médicos ou outro pessoal de apoio ao atleta que estejam inelegíveis por conta de uma VRA ou que tenham sido condenados criminalmente ou disciplinados profissionalmente em relação à dopagem (ver Lista de Associação Proibida da AMA)
|
||||
|
||||
|
||||
|
||||
Direitos do Pessoal de Apoio ao Atleta
|
||||
|
||||
O pessoal de apoio ao atleta e outras pessoas também têm direitos e responsabilidades ao abrigo do Código. Estes incluem:
|
||||
|
||||
Direito a uma audiência justa, perante um painel de audiência independente
|
||||
Direito de recorrer da decisão da audiência
|
||||
Direitos relativos à proteção de dados, de acordo com a NIPPIP e qualquer lei local aplicável
|
||||
|
||||
|
||||
|
||||
Responsabilidades do Pessoal de Apoio ao Atleta
|
||||
|
||||
As responsabilidades do pessoal de apoio ao atleta ao abrigo do Código incluem:
|
||||
|
||||
Usar a sua influência nos valores e comportamentos dos atletas para promover comportamentos de desporto limpo
|
||||
Conhecer e cumprir todas as políticas e regras antidopagem aplicáveis, incluindo as Regras Antidopagem da IPF (em conformidade com o Código)
|
||||
Cooperar com o programa de controlo de dopagem dos atletas
|
||||
Cooperar com OAD que investigam Violações das Regras Antidopagem (VRA)
|
||||
Informar a FI relevante e/ou ONAD se cometeram uma VRA nos últimos 10 anos
|
||||
Abster-se de possuir uma substância proibida (ou um método proibido)*, administrar tal substância ou método a um atleta, traficar, encobrir uma violação das regras antidopagem (VRA) ou outras formas de cumplicidade e associar-se com uma pessoa condenada por dopagem (associação proibida). Estas são VRA aplicáveis ao pessoal de apoio ao atleta ao abrigo do Artigo 2 do Código Mundial Antidopagem e Artigo 2 das Regras Antidopagem da IPF.
|
||||
|
||||
|
||||
|
||||
* A menos que o pessoal de apoio ao atleta possa estabelecer que a posse é consistente com uma AUT concedida a um atleta ou outra justificação aceitável. Justificação aceitável incluiria, por exemplo, um médico de equipa que transporta substâncias proibidas para lidar com situações agudas e de emergência.
|
||||
|
||||
Recomendação da IPF ao Pessoal de Apoio ao Atleta
|
||||
|
||||
Aqui estão algumas formas como o pessoal de apoio ao atleta pode apoiar os seus atletas na sua educação sobre desporto limpo:
|
||||
|
||||
Partilhar a Lei dos Direitos Antidopagem dos Atletas com o seu atleta
|
||||
Registar-se e fazer um curso adequado para si na plataforma ADEL da AMA
|
||||
Contactar ipfantidoping@cces.ca para quaisquer questões que possa ter
|
||||
|
||||
|
|
|
|||
|
|
@ -6,12 +6,12 @@ import { TableOfContents } from "@/components/markdown/TableOfContents";
|
|||
export default function AboutPage() {
|
||||
const [headings, setHeadings] = useState([] as { id: string; text: string; level: number }[]);
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-[1fr_300px] gap-8">
|
||||
<div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-[1fr_280px] gap-6 lg:gap-8">
|
||||
<div className="min-w-0">
|
||||
<h1 className="text-2xl font-bold">About</h1>
|
||||
<MarkdownRenderer contentPath={"en/sobre/quem-somos.md"} onHeadings={setHeadings} />
|
||||
</div>
|
||||
<aside>
|
||||
<aside className="min-w-0">
|
||||
<TableOfContents headings={headings} />
|
||||
</aside>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -115,26 +115,46 @@ p { font-size: 1rem; line-height: 1.6; color: var(--foreground); }
|
|||
/* Markdown content */
|
||||
.markdown-content {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
hyphens: auto;
|
||||
}
|
||||
.markdown-content h1,
|
||||
.markdown-content h2,
|
||||
.markdown-content h3 {
|
||||
.markdown-content h3,
|
||||
.markdown-content h4,
|
||||
.markdown-content h5,
|
||||
.markdown-content h6 {
|
||||
margin-top: 1.25rem;
|
||||
margin-bottom: 0.75rem;
|
||||
scroll-margin-top: 80px; /* Account for header + subnav on mobile */
|
||||
scroll-margin-top: 100px; /* Account for header + subnav on mobile */
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.markdown-content h1,
|
||||
.markdown-content h2,
|
||||
.markdown-content h3 {
|
||||
scroll-margin-top: 120px; /* More space on desktop */
|
||||
.markdown-content h3,
|
||||
.markdown-content h4,
|
||||
.markdown-content h5,
|
||||
.markdown-content h6 {
|
||||
scroll-margin-top: 140px; /* More space on desktop */
|
||||
}
|
||||
}
|
||||
.markdown-content p, .markdown-content ul, .markdown-content ol { margin-bottom: 0.75rem; }
|
||||
.markdown-content p, .markdown-content ul, .markdown-content ol {
|
||||
margin-bottom: 0.75rem;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.markdown-content ul { padding-left: 1.25rem; list-style: disc; }
|
||||
.markdown-content ol { padding-left: 1.25rem; list-style: decimal; }
|
||||
.markdown-content a { text-decoration: underline; }
|
||||
.markdown-content a {
|
||||
text-decoration: underline;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* Markdown blockquote */
|
||||
.markdown-blockquote {
|
||||
|
|
@ -157,6 +177,7 @@ p { font-size: 1rem; line-height: 1.6; color: var(--foreground); }
|
|||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-size: 0.9em;
|
||||
color: var(--color-red);
|
||||
word-break: break-word;
|
||||
}
|
||||
.markdown-pre {
|
||||
background: #f5f5f5;
|
||||
|
|
@ -165,29 +186,36 @@ p { font-size: 1rem; line-height: 1.6; color: var(--foreground); }
|
|||
padding: 1rem;
|
||||
overflow-x: auto;
|
||||
margin: 1rem 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
.markdown-pre code {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
color: var(--foreground);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* Markdown table */
|
||||
.markdown-table-wrapper {
|
||||
overflow-x: auto;
|
||||
margin: 1.25rem 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
.markdown-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
border: 1px solid var(--color-border);
|
||||
table-layout: auto;
|
||||
}
|
||||
.markdown-table th,
|
||||
.markdown-table td {
|
||||
padding: 0.75rem 1rem;
|
||||
text-align: left;
|
||||
border: 1px solid var(--color-border);
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
.markdown-table th {
|
||||
background: rgba(0, 106, 61, 0.08);
|
||||
|
|
@ -522,6 +550,9 @@ p { font-size: 1rem; line-height: 1.6; color: var(--foreground); }
|
|||
|
||||
/* Subnav bar */
|
||||
.subnav-bar {
|
||||
position: sticky;
|
||||
top: 66px; /* Below main header */
|
||||
z-index: 40;
|
||||
border-top: 1px solid var(--color-border);
|
||||
background: #fff;
|
||||
}
|
||||
|
|
@ -540,6 +571,116 @@ p { font-size: 1rem; line-height: 1.6; color: var(--foreground); }
|
|||
background: rgba(0,0,0,0.06);
|
||||
}
|
||||
|
||||
/* Table of Contents */
|
||||
.toc-container {
|
||||
position: sticky;
|
||||
top: 110px; /* Below header + subnav */
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
.toc-toggle {
|
||||
width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
padding: 0.75rem 1rem;
|
||||
background: #f0f7ff;
|
||||
color: #1a73e8;
|
||||
border: 1px solid #c2d9f7;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
transition: background 150ms ease, border-color 150ms ease;
|
||||
}
|
||||
|
||||
.toc-toggle:hover {
|
||||
background: #e0efff;
|
||||
border-color: #a0c4f0;
|
||||
}
|
||||
|
||||
.toc-toggle-icon {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.toc-nav {
|
||||
display: none;
|
||||
padding: 1rem;
|
||||
background: #fff;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.toc-nav--open {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.toc-toggle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.toc-nav {
|
||||
display: block;
|
||||
max-height: calc(100vh - 140px);
|
||||
overflow-y: auto;
|
||||
padding: 1rem;
|
||||
background: #fafafa;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.toc-title {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--color-muted);
|
||||
margin-bottom: 0.75rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.toc-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.toc-item {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.toc-link {
|
||||
display: block;
|
||||
padding: 0.375rem 0.625rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.4;
|
||||
color: var(--foreground);
|
||||
text-decoration: none;
|
||||
transition: background 150ms ease, color 150ms ease;
|
||||
border-left: 2px solid transparent;
|
||||
margin-left: -2px;
|
||||
}
|
||||
|
||||
.toc-link:hover {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.toc-link--active {
|
||||
background: rgba(26, 115, 232, 0.08);
|
||||
color: #1a73e8;
|
||||
font-weight: 600;
|
||||
border-left-color: #1a73e8;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes fade-in-up {
|
||||
from { opacity: 0; transform: translateY(8px); }
|
||||
|
|
|
|||
|
|
@ -6,12 +6,12 @@ import { TableOfContents } from "@/components/markdown/TableOfContents";
|
|||
export default function AcessibilidadePage() {
|
||||
const [headings, setHeadings] = useState([] as { id: string; text: string; level: number }[]);
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-[1fr_300px] gap-8">
|
||||
<div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-[1fr_280px] gap-6 lg:gap-8">
|
||||
<div className="min-w-0">
|
||||
<h1 className="text-2xl font-bold">Declaração de Acessibilidade</h1>
|
||||
<MarkdownRenderer contentPath={"pt/acessibilidade.md"} onHeadings={setHeadings} />
|
||||
</div>
|
||||
<aside>
|
||||
<aside className="min-w-0">
|
||||
<TableOfContents headings={headings} />
|
||||
</aside>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,12 +6,12 @@ import { TableOfContents } from "@/components/markdown/TableOfContents";
|
|||
export default function AvisosLegaisPage() {
|
||||
const [headings, setHeadings] = useState([] as { id: string; text: string; level: number }[]);
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-[1fr_300px] gap-8">
|
||||
<div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-[1fr_280px] gap-6 lg:gap-8">
|
||||
<div className="min-w-0">
|
||||
<h1 className="text-2xl font-bold">Avisos Legais</h1>
|
||||
<MarkdownRenderer contentPath={"pt/avisos-legais.md"} onHeadings={setHeadings} />
|
||||
</div>
|
||||
<aside>
|
||||
<aside className="min-w-0">
|
||||
<TableOfContents headings={headings} />
|
||||
</aside>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,11 +6,11 @@ import { TableOfContents } from "@/components/markdown/TableOfContents";
|
|||
export default function AntiDopingPage() {
|
||||
const [headings, setHeadings] = useState([] as { id: string; text: string; level: number }[]);
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-[1fr_300px] gap-8">
|
||||
<div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-[1fr_280px] gap-6 lg:gap-8">
|
||||
<div className="min-w-0">
|
||||
<MarkdownRenderer contentPath={"pt/regras/anti-doping.md"} onHeadings={setHeadings} />
|
||||
</div>
|
||||
<aside>
|
||||
<aside className="min-w-0">
|
||||
<TableOfContents headings={headings} />
|
||||
</aside>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,11 +6,11 @@ import { TableOfContents } from "@/components/markdown/TableOfContents";
|
|||
export default function IpfRuleBookPage() {
|
||||
const [headings, setHeadings] = useState([] as { id: string; text: string; level: number }[]);
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-[1fr_300px] gap-8">
|
||||
<div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-[1fr_280px] gap-6 lg:gap-8">
|
||||
<div className="min-w-0">
|
||||
<MarkdownRenderer contentPath={"pt/regras/ipf-rule-book.md"} onHeadings={setHeadings} />
|
||||
</div>
|
||||
<aside>
|
||||
<aside className="min-w-0">
|
||||
<TableOfContents headings={headings} />
|
||||
</aside>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -11,15 +11,15 @@ export default function QuemSomosPage() {
|
|||
}[]>([]);
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-[1fr_300px] gap-8">
|
||||
<div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-[1fr_280px] gap-6 lg:gap-8">
|
||||
<div className="min-w-0">
|
||||
<h1 className="text-2xl font-bold">Quem Somos</h1>
|
||||
<MarkdownRenderer
|
||||
contentPath={"pt/sobre/quem-somos.md"}
|
||||
onHeadings={setHeadings}
|
||||
/>
|
||||
</div>
|
||||
<aside aria-label="Tabela de conteúdos" className="md:block">
|
||||
<aside className="min-w-0">
|
||||
<TableOfContents headings={headings} />
|
||||
</aside>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ export function SubnavBar() {
|
|||
label: locale === "en" ? "Rules" : "Regras",
|
||||
path: `${base}/regras`,
|
||||
items: [
|
||||
{ label: "IPF Rule Book", href: `${base}/regras/ipf-rule-book` },
|
||||
{ label: "Regras/Info. IPF", href: `${base}/regras/ipf-rule-book` },
|
||||
{ label: locale === "en" ? "Anti-Doping" : "Anti-Doping", href: `${base}/regras/anti-doping` },
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
"use client";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import type { Components } from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import rehypeSlug from "rehype-slug";
|
||||
import rehypeAutolinkHeadings from "rehype-autolink-headings";
|
||||
import { parseMarkdownWithFrontmatter } from "@/lib/markdown";
|
||||
|
||||
type MarkdownRendererProps = {
|
||||
contentPath: string;
|
||||
|
|
@ -14,16 +13,49 @@ type MarkdownRendererProps = {
|
|||
|
||||
export function MarkdownRenderer({ contentPath, onHeadings }: MarkdownRendererProps) {
|
||||
const [source, setSource] = useState<string>("");
|
||||
const articleRef = useRef<HTMLElement>(null);
|
||||
const hasExtractedHeadings = useRef(false);
|
||||
|
||||
// Fetch content
|
||||
useEffect(() => {
|
||||
hasExtractedHeadings.current = false;
|
||||
(async () => {
|
||||
const res = await fetch(`/api/content?path=${encodeURIComponent(contentPath)}`);
|
||||
const text = await res.text();
|
||||
const { content, headings } = parseMarkdownWithFrontmatter(text);
|
||||
// Just get the content without frontmatter
|
||||
const frontmatterMatch = text.match(/^---\n[\s\S]*?\n---\n/);
|
||||
const content = frontmatterMatch ? text.slice(frontmatterMatch[0].length) : text;
|
||||
setSource(content);
|
||||
onHeadings?.(headings);
|
||||
})();
|
||||
}, [contentPath, onHeadings]);
|
||||
}, [contentPath]);
|
||||
|
||||
// Extract headings from rendered DOM after content is rendered
|
||||
useEffect(() => {
|
||||
if (!source || !articleRef.current || !onHeadings || hasExtractedHeadings.current) return;
|
||||
|
||||
// Use a small delay to ensure rehype-slug has processed the headings
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (!articleRef.current) return;
|
||||
|
||||
const headingElements = articleRef.current.querySelectorAll("h1, h2, h3, h4, h5, h6");
|
||||
const headings: { id: string; text: string; level: number }[] = [];
|
||||
|
||||
headingElements.forEach((el) => {
|
||||
const id = el.getAttribute("id");
|
||||
const text = el.textContent?.trim() || "";
|
||||
const level = parseInt(el.tagName[1], 10);
|
||||
|
||||
if (id && text) {
|
||||
headings.push({ id, text, level });
|
||||
}
|
||||
});
|
||||
|
||||
hasExtractedHeadings.current = true;
|
||||
onHeadings(headings);
|
||||
}, 50);
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [source, onHeadings]);
|
||||
|
||||
const components: Components = {
|
||||
blockquote: ({ children, ...props }) => (
|
||||
|
|
@ -57,7 +89,7 @@ export function MarkdownRenderer({ contentPath, onHeadings }: MarkdownRendererPr
|
|||
};
|
||||
|
||||
return (
|
||||
<article className="markdown-content" aria-label="Conteúdo">
|
||||
<article ref={articleRef} className="markdown-content" aria-label="Conteúdo">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
rehypePlugins={[rehypeSlug, [rehypeAutolinkHeadings, { behavior: "wrap" }]]}
|
||||
|
|
|
|||
|
|
@ -1,123 +1,160 @@
|
|||
"use client";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
type Heading = { id: string; text: string; level: number };
|
||||
|
||||
export function TableOfContents({ headings }: { headings: Heading[] }) {
|
||||
const [activeId, setActiveId] = useState<string | null>(null);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const observerRef = useRef<IntersectionObserver | null>(null);
|
||||
const isManualScrollRef = useRef(false);
|
||||
const manualScrollTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// Get offset for fixed headers
|
||||
const getOffset = useCallback(() => {
|
||||
const header = document.querySelector(".app-header");
|
||||
const subnav = document.querySelector(".subnav-bar");
|
||||
let offset = 32;
|
||||
if (header) offset += header.getBoundingClientRect().height;
|
||||
if (subnav) offset += subnav.getBoundingClientRect().height;
|
||||
return offset;
|
||||
}, []);
|
||||
|
||||
// Scroll spy - track which heading is currently visible
|
||||
useEffect(() => {
|
||||
// Disconnect old observer if it exists
|
||||
if (observerRef.current) {
|
||||
observerRef.current.disconnect();
|
||||
}
|
||||
if (headings.length === 0) return;
|
||||
|
||||
// Only set up observer if we have headings
|
||||
if (headings.length === 0) {
|
||||
setActiveId(null);
|
||||
return;
|
||||
}
|
||||
const updateActiveHeading = () => {
|
||||
if (isManualScrollRef.current) return;
|
||||
|
||||
const visibleHeadings = new Map<string, number>();
|
||||
|
||||
const callback: IntersectionObserverCallback = (entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
visibleHeadings.set(entry.target.id, entry.boundingClientRect.top);
|
||||
const offset = getOffset();
|
||||
let found: string | null = null;
|
||||
|
||||
// Go through headings and find which one we're at
|
||||
for (let i = 0; i < headings.length; i++) {
|
||||
const el = document.getElementById(headings[i].id);
|
||||
if (!el) continue;
|
||||
|
||||
const rect = el.getBoundingClientRect();
|
||||
// If this heading is at or above our threshold, it's the current one
|
||||
if (rect.top <= offset + 10) {
|
||||
found = headings[i].id;
|
||||
} else {
|
||||
visibleHeadings.delete(entry.target.id);
|
||||
// First heading that's below threshold - stop here
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// Find the topmost visible heading
|
||||
if (visibleHeadings.size > 0) {
|
||||
const sortedHeadings = Array.from(visibleHeadings.entries())
|
||||
.sort((a, b) => a[1] - b[1]);
|
||||
setActiveId(sortedHeadings[0][0]);
|
||||
} else {
|
||||
setActiveId(null);
|
||||
}
|
||||
|
||||
// If at very top of page, select first heading
|
||||
if (!found && window.scrollY < 100 && headings.length > 0) {
|
||||
found = headings[0].id;
|
||||
}
|
||||
|
||||
setActiveId(found);
|
||||
};
|
||||
|
||||
// Run on mount
|
||||
updateActiveHeading();
|
||||
|
||||
// Throttle scroll events
|
||||
let ticking = false;
|
||||
const onScroll = () => {
|
||||
if (!ticking) {
|
||||
requestAnimationFrame(() => {
|
||||
updateActiveHeading();
|
||||
ticking = false;
|
||||
});
|
||||
ticking = true;
|
||||
}
|
||||
};
|
||||
|
||||
const observer = new IntersectionObserver(callback, {
|
||||
rootMargin: "-80px 0px -55% 0px",
|
||||
threshold: 0.1,
|
||||
});
|
||||
observerRef.current = observer;
|
||||
|
||||
headings.forEach((h) => {
|
||||
const el = document.getElementById(h.id);
|
||||
if (el) observer.observe(el);
|
||||
});
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, [headings]);
|
||||
|
||||
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>, headingId: string) => {
|
||||
e.preventDefault();
|
||||
const element = document.getElementById(headingId);
|
||||
if (element) {
|
||||
// Calculate the scroll offset to account for fixed header
|
||||
// Mobile: 80px, Desktop: 120px
|
||||
const isMobile = window.innerWidth < 768;
|
||||
const scrollOffset = isMobile ? 80 : 120;
|
||||
|
||||
const elementPosition = element.getBoundingClientRect().top + window.scrollY;
|
||||
const targetPosition = elementPosition - scrollOffset;
|
||||
|
||||
window.scrollTo({
|
||||
top: targetPosition,
|
||||
behavior: "smooth",
|
||||
});
|
||||
|
||||
// Update URL without causing a page reload
|
||||
window.history.pushState(null, "", `#${headingId}`);
|
||||
// Close mobile menu after click
|
||||
setIsOpen(false);
|
||||
window.addEventListener("scroll", onScroll, { passive: true });
|
||||
return () => window.removeEventListener("scroll", onScroll);
|
||||
}, [headings, getOffset]);
|
||||
|
||||
// Scroll to a heading when clicked
|
||||
const scrollToHeading = useCallback((id: string) => {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
|
||||
// Mark as manual scroll to prevent scroll spy interference
|
||||
isManualScrollRef.current = true;
|
||||
if (manualScrollTimeoutRef.current) {
|
||||
clearTimeout(manualScrollTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
|
||||
// Set active immediately for responsive feel
|
||||
setActiveId(id);
|
||||
setIsOpen(false);
|
||||
|
||||
// Calculate position
|
||||
const offset = getOffset();
|
||||
const top = el.getBoundingClientRect().top + window.scrollY - offset;
|
||||
|
||||
// Scroll
|
||||
window.scrollTo({ top: Math.max(0, top), behavior: "smooth" });
|
||||
|
||||
// Update URL
|
||||
history.replaceState(null, "", `#${id}`);
|
||||
|
||||
// Re-enable scroll spy after scroll completes
|
||||
manualScrollTimeoutRef.current = setTimeout(() => {
|
||||
isManualScrollRef.current = false;
|
||||
}, 1000);
|
||||
}, [getOffset]);
|
||||
|
||||
const handleClick = useCallback((e: React.MouseEvent, id: string) => {
|
||||
e.preventDefault();
|
||||
scrollToHeading(id);
|
||||
}, [scrollToHeading]);
|
||||
|
||||
// Handle hash on page load
|
||||
useEffect(() => {
|
||||
const hash = window.location.hash.slice(1);
|
||||
if (hash && headings.some(h => h.id === hash)) {
|
||||
setTimeout(() => scrollToHeading(hash), 200);
|
||||
}
|
||||
}, [headings, scrollToHeading]);
|
||||
|
||||
// Cleanup
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (manualScrollTimeoutRef.current) {
|
||||
clearTimeout(manualScrollTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (headings.length === 0) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Mobile Toggle Button */}
|
||||
<div className="toc-container">
|
||||
<button
|
||||
className="md:hidden w-full mb-4 px-4 py-2 bg-blue-50 text-blue-600 border border-blue-200 rounded-lg font-medium text-sm transition hover:bg-blue-100"
|
||||
className="toc-toggle md:hidden"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
aria-expanded={isOpen}
|
||||
aria-label="Alternar tabela de conteúdos"
|
||||
aria-controls="toc-nav"
|
||||
>
|
||||
{isOpen ? "✕ Fechar conteúdos" : "☰ Mostrar conteúdos"}
|
||||
<span className="toc-toggle-icon">{isOpen ? "✕" : "☰"}</span>
|
||||
<span>{isOpen ? "Fechar conteúdos" : "Mostrar conteúdos"}</span>
|
||||
</button>
|
||||
|
||||
{/* Desktop Sidebar + Mobile Collapsible */}
|
||||
<nav
|
||||
id="toc-nav"
|
||||
aria-label="Tabela de conteúdos"
|
||||
className={`
|
||||
md:sticky md:max-h-[calc(100vh-140px)] md:overflow-auto md:block
|
||||
${isOpen ? "block" : "hidden md:block"}
|
||||
mb-6 md:mb-0 p-4 md:p-0
|
||||
bg-white md:bg-transparent
|
||||
border md:border-none border-gray-200 md:border-0
|
||||
rounded-lg md:rounded-none
|
||||
`}
|
||||
style={{ top: "120px" }}
|
||||
className={`toc-nav ${isOpen ? "toc-nav--open" : ""}`}
|
||||
>
|
||||
<ul className="space-y-1">
|
||||
<h2 className="toc-title">Conteúdos</h2>
|
||||
<ul className="toc-list">
|
||||
{headings.map((h) => (
|
||||
<li key={h.id} style={{ paddingLeft: (h.level - 1) * 12 }}>
|
||||
<li
|
||||
key={h.id}
|
||||
className="toc-item"
|
||||
style={{ paddingLeft: `${(h.level - 1) * 0.75}rem` }}
|
||||
>
|
||||
<a
|
||||
href={`#${h.id}`}
|
||||
onClick={(e) => handleClick(e, h.id)}
|
||||
className={`
|
||||
block py-1 px-2 rounded transition
|
||||
${
|
||||
activeId === h.id
|
||||
? "font-semibold text-blue-600 bg-blue-50"
|
||||
: "text-gray-700 hover:bg-gray-100"
|
||||
}
|
||||
`}
|
||||
className={`toc-link ${activeId === h.id ? "toc-link--active" : ""}`}
|
||||
>
|
||||
{h.text}
|
||||
</a>
|
||||
|
|
@ -125,6 +162,6 @@ export function TableOfContents({ headings }: { headings: Heading[] }) {
|
|||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue