TSX386 linhas16.131 bytes
apps/web/src/app/premiacao/page.tsx
SHA-256
835f63bbacc878efd5abd41a29630d1acc25db175466835f2710277def7eab84
Somente leiturafonte-e58fe007b6ee
'use client'
import { useEffect, useState } from 'react'
import Link from 'next/link'
import { getPremiacao } from '@/lib/api'
import type { CategoriaPremio, PremiacaoResponse } from '@/lib/types'
import CategoriaCard from '@/components/premiacao/CategoriaCard'
import ShareButtons from '@/components/ShareButtons'
/** ISO → 20/09/2026. */
function dataLonga(iso: string): string {
const d = new Date(iso)
if (Number.isNaN(d.getTime())) return iso
return d.toLocaleDateString('pt-BR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
})
}
function Etapa({
valor,
rotulo,
nota,
}: {
valor: number
rotulo: string
nota: string
}) {
return (
<div className="bg-surface px-4 py-4">
<div className="font-mono text-2xl font-bold text-ink">{valor}</div>
<div className="mt-0.5 text-xs font-medium text-ink">{rotulo}</div>
<div className="mt-1 text-[11px] leading-snug text-muted">{nota}</div>
</div>
)
}
function Secao({
eyebrow,
titulo,
children,
nota,
}: {
eyebrow: string
titulo: string
nota?: React.ReactNode
children: React.ReactNode
}) {
return (
<section className="space-y-5">
<header>
<p className="eyebrow">{eyebrow}</p>
<h2 className="mt-1 font-serif text-2xl font-semibold text-ink">{titulo}</h2>
{nota && <p className="mt-2 max-w-3xl text-sm leading-relaxed text-muted">{nota}</p>}
</header>
{children}
</section>
)
}
export default function PremiacaoPage() {
const [dados, setDados] = useState<PremiacaoResponse | null>(null)
const [erro, setErro] = useState<string | null>(null)
const [carregando, setCarregando] = useState(true)
useEffect(() => {
getPremiacao()
.then(setDados)
.catch((e) => setErro(String(e)))
.finally(() => setCarregando(false))
}, [])
const por = (eixo: CategoriaPremio['eixo']) =>
dados?.categorias.filter((c) => c.eixo === eixo) ?? []
const grandePremio = por('grande_premio')[0]
const regionais = por('regional')
const tematicas = por('tematico')
// Acúmulo e distribuição partidária são fatos do resultado: exibidos, não escondidos.
const vitoriasPorSenador = new Map<string, { nome: string; titulos: string[] }>()
for (const c of dados?.categorias ?? []) {
const v = c.vencedor
const atual = vitoriasPorSenador.get(v.senadorCod) ?? { nome: v.nome, titulos: [] }
atual.titulos.push(c.titulo)
vitoriasPorSenador.set(v.senadorCod, atual)
}
const multiplos = [...vitoriasPorSenador.values()]
.filter((x) => x.titulos.length > 1)
.sort((a, b) => b.titulos.length - a.titulos.length)
const partidos = new Map<string, number>()
for (const c of dados?.categorias ?? []) {
partidos.set(c.vencedor.partido, (partidos.get(c.vencedor.partido) ?? 0) + 1)
}
const partidosOrdenados = [...partidos.entries()].sort(
(a, b) => b[1] - a[1] || a[0].localeCompare(b[0], 'pt-BR'),
)
return (
<div className="space-y-14 fade-in">
{/* ============= ABERTURA ============= */}
<section className="border-b border-border pb-10">
<p className="eyebrow">Prêmio da 57ª Legislatura · 2023 — 2027</p>
<h1 className="mt-3 font-serif text-4xl sm:text-5xl font-semibold leading-tight tracking-tight text-ink">
Prêmio Observatório do Senado
</h1>
<p className="mt-5 max-w-3xl text-base sm:text-lg leading-relaxed text-muted">
Doze reconhecimentos atribuídos <strong className="text-ink">por cálculo,
não por escolha</strong>. Não há júri, indicação, voto popular nem
curadoria editorial: cada categoria premia quem lidera um critério
numérico definido antes da apuração, calculado sobre as bases públicas
oficiais do Senado Federal.
</p>
<p className="mt-4 max-w-3xl text-sm leading-relaxed text-muted">
Todo resultado desta página pode ser refeito de forma independente. O
código que apura está publicado em{' '}
<Link href="/codigo" className="text-primary underline-offset-2 hover:underline">
código auditável
</Link>
, a fórmula está na{' '}
<Link href="/metodologia" className="text-primary underline-offset-2 hover:underline">
metodologia
</Link>{' '}
e os dados de origem vêm dos portais de dados abertos do Senado.
</p>
</section>
{carregando && (
<p className="py-16 text-center text-sm text-muted">Carregando apuração…</p>
)}
{erro && !carregando && (
<section className="rounded-sm border-l-4 border-danger bg-panel px-5 py-4">
<p className="eyebrow text-danger">Apuração indisponível</p>
<p className="mt-2 text-sm leading-relaxed text-muted">
Não foi possível exibir o resultado agora — ou a fonte de dados está
fora do ar, ou a apuração recusou publicar sobre dados que não
passaram nas verificações de integridade. A segunda hipótese é
proposital: a página prefere não exibir resultado a exibir um
resultado calculado sobre uma coorte incompleta. O detalhe técnico
abaixo distingue os dois casos.
</p>
<p className="mt-2 font-mono text-xs text-subtle">{erro}</p>
</section>
)}
{dados && (
<>
{/* ============= PROCEDÊNCIA ============= */}
<Secao
eyebrow="Procedência"
titulo="De onde vem este resultado"
nota={
<>
A apuração roda sobre um retrato datado do ranking, e não sobre
dados que mudam a cada visita. Assim o resultado publicado é
sempre o mesmo para todos os leitores e pode ser reconferido
depois.
</>
}
>
<div className="grid grid-cols-2 gap-px overflow-hidden rounded-sm border border-border bg-border sm:grid-cols-4">
<Etapa
valor={dados.coorte.avaliados}
rotulo="Avaliados"
nota="Exerceram mandato na 57ª Legislatura"
/>
<Etapa
valor={dados.coorte.elegiveis}
rotulo="Pontuados"
nota={`≥ ${dados.coorte.mesesMinimos} meses de exercício — recebem nota IDS`}
/>
<Etapa
valor={dados.coorte.concorrentes}
rotulo="Concorrem"
nota={`≥ ${dados.coorte.mesesMinimosVitoria} meses — metade da legislatura`}
/>
<Etapa
valor={dados.categorias.length}
rotulo="Categorias"
nota="1 geral, 5 regionais e 6 temáticas"
/>
</div>
<div className="rounded-sm border border-border bg-panel px-5 py-4 text-sm leading-relaxed text-muted">
<p>
<strong className="text-ink">Dados apurados em {dataLonga(dados.snapshot)}.</strong>{' '}
{dados.congelado ? (
<>
O resultado está <strong className="text-ink">fixado</strong> nesse
retrato: os recálculos semanais seguintes não alteram os
vencedores publicados.
</>
) : (
<>
O resultado ainda <strong className="text-ink">não está fixado</strong> e
acompanha o recálculo semanal — pode mudar até ser congelado.
</>
)}
</p>
<p className="mt-3">
<strong className="text-ink">Duas réguas de tempo, propositalmente distintas.</strong>{' '}
Recebe nota quem exerceu ao menos {dados.coorte.mesesMinimos} meses;
concorre aos prêmios quem exerceu ao menos{' '}
{dados.coorte.mesesMinimosVitoria} — metade dos 48 meses da
legislatura. Quem fica entre as duas segue com nota publicada no
ranking, apenas fora da disputa. Quem encerrou o mandato antes do
fim da legislatura continua concorrendo: o trabalho feito não é
apagado pela saída.
</p>
</div>
</Secao>
{/* ============= GRANDE PRÊMIO ============= */}
{grandePremio && (
<Secao
eyebrow="Eixo geral"
titulo="Grande Prêmio"
nota="Maior Índice de Desempenho Senatorial entre todos os concorrentes, somadas as seis dimensões com seus pesos."
>
<div className="max-w-2xl">
<CategoriaCard categoria={grandePremio} />
</div>
</Secao>
)}
{/* ============= REGIONAIS ============= */}
<Secao
eyebrow="Eixo regional"
titulo="Destaques por região"
nota={
<>
Um reconhecimento por região do IBGE. As notas{' '}
<strong className="text-ink">não são renormalizadas dentro da região</strong>:
todos são medidos na mesma régua nacional, então as diferenças de
nota entre regiões aparecem como são, sem nivelamento. As regiões
também têm números diferentes de concorrentes — o Nordeste tem
quase o triplo do Sul —, o que é informado em cada card.
</>
}
>
<div className="grid gap-5 md:grid-cols-2">
{regionais.map((c) => (
<CategoriaCard key={c.id} categoria={c} />
))}
</div>
</Secao>
{/* ============= TEMÁTICAS ============= */}
<Secao
eyebrow="Eixo temático"
titulo="Categorias por dimensão"
nota={
<>
Uma categoria para cada dimensão que compõe o IDS, com o peso que
ela tem no índice indicado em cada card. Os pesos são os mesmos
publicados na metodologia e não foram ajustados para esta
premiação.
</>
}
>
<div className="grid gap-5 md:grid-cols-2">
{tematicas.map((c) => (
<CategoriaCard key={c.id} categoria={c} />
))}
</div>
</Secao>
{/* ============= LEITURA DO CONJUNTO ============= */}
<Secao
eyebrow="Leitura do conjunto"
titulo="O que o resultado mostra"
nota="Fatos da apuração que o formato de cards não deixa ver de imediato."
>
<div className="grid gap-5 md:grid-cols-2">
<div className="card">
<h3 className="font-serif text-base font-semibold text-ink">
Acúmulo de prêmios
</h3>
<p className="mt-2 text-sm leading-relaxed text-muted">
Um senador pode vencer em mais de um eixo. Nenhuma categoria
remove alguém da disputa nas outras — suprimir isso distorceria
os dados para distribuir prêmios.
</p>
{multiplos.length > 0 ? (
<ul className="mt-3 space-y-2">
{multiplos.map((m) => (
<li key={m.nome} className="text-sm">
<span className="font-medium text-ink">{m.nome}</span>
<span className="text-muted"> — {m.titulos.length} categorias: </span>
<span className="text-muted">{m.titulos.join(', ')}</span>
</li>
))}
</ul>
) : (
<p className="mt-3 text-sm text-muted">
Nesta apuração, nenhum senador venceu mais de uma categoria.
</p>
)}
</div>
<div className="card">
<h3 className="font-serif text-base font-semibold text-ink">
Partidos dos premiados
</h3>
<p className="mt-2 text-sm leading-relaxed text-muted">
O partido não entra em nenhum critério de cálculo. A
distribuição abaixo é resultado da apuração, publicada para que
qualquer viés eventual fique visível em vez de oculto.
</p>
<ul className="mt-3 space-y-1.5">
{partidosOrdenados.map(([sigla, n]) => (
<li key={sigla} className="flex items-center gap-3 text-sm">
<span className="w-28 shrink-0 font-medium text-ink">{sigla}</span>
<span
className="h-2 rounded-sm bg-primary/70"
style={{ width: `${(n / dados.categorias.length) * 100}%` }}
/>
<span className="font-mono text-xs text-muted">{n}</span>
</li>
))}
</ul>
</div>
</div>
</Secao>
{/* ============= LIMITES ============= */}
<section className="rounded-sm border-l-4 border-accent bg-panel px-5 py-5">
<p className="eyebrow text-accent">O que este prêmio não mede</p>
<div className="mt-3 space-y-3 text-sm leading-relaxed text-muted">
<p>
O IDS é um indicador{' '}
<strong className="text-ink">quantitativo de atividade parlamentar</strong>.
Ele conta atos registrados nas bases oficiais — proposições,
votações, relatorias, discursos, despesas de cota. Não avalia o{' '}
<strong className="text-ink">mérito, o conteúdo ou as consequências</strong>{' '}
do que foi proposto ou votado, nem posições ideológicas.
</p>
<p>
Um número alto significa atividade intensa e regular, não
necessariamente boa representação; um número baixo indica pouca
atividade registrada, não necessariamente mau desempenho. Trabalho
que não deixa registro formal nas bases do Senado — articulação,
atuação em base eleitoral, negociação — não é capturado por
nenhuma das dimensões.
</p>
<p>
Os critérios e pesos foram definidos antes da apuração e valem
igualmente para todos. Divergências sobre a escolha dos critérios
são legítimas e devem ser dirigidas à{' '}
<Link href="/metodologia" className="text-primary underline-offset-2 hover:underline">
metodologia
</Link>
; erro de dado ou de cálculo pode ser verificado no{' '}
<Link href="/codigo" className="text-primary underline-offset-2 hover:underline">
código auditável
</Link>{' '}
e reportado pelo{' '}
<Link href="/contato" className="text-primary underline-offset-2 hover:underline">
contato
</Link>
.
</p>
</div>
</section>
<section className="border-t border-border pt-8">
<ShareButtons
variante="card"
titulo="Divulgue a apuração"
url="https://observasenado.org/premiacao"
texto={
`Prêmio Observatório do Senado — 57ª Legislatura: ${dados.categorias.length} ` +
`categorias apuradas por cálculo sobre os dados abertos do Senado, ` +
`entre ${dados.coorte.concorrentes} senadores concorrentes.`
}
/>
</section>
</>
)}
</div>
)
}