TSX193 linhas6.612 bytes
apps/web/src/components/premiacao/CategoriaCard.tsx
SHA-256
a6aa2128fd87d2ed6d1263c99d6ab6b8b4372e9f9a58b113eb4281dcd1871aaa
Somente leiturafonte-e58fe007b6ee
'use client'
import Link from 'next/link'
import type { CategoriaPremio, Premiado } from '@/lib/types'
/** ISO (2026-04-01) → 04/2026. */
function formatarData(iso: string): string {
const [ano, mes] = iso.split('-')
return mes ? `${mes}/${ano}` : iso
}
function numero(v: number): string {
return v.toLocaleString('pt-BR', { maximumFractionDigits: 1 })
}
/**
* Evidência por trás da nota.
*
* Nas categorias temáticas é o dado contado (relatorias, proposições…). Nas
* medidas pelo IDS o número bruto seria a própria nota, então mostra-se a
* decomposição por dimensão — é o que deixa ver o que produziu o total.
*/
function Indicador({ p }: { p: Premiado }) {
const { composicao } = p.indicador
if (composicao) {
return (
<span className="flex flex-wrap gap-x-3 gap-y-0.5 text-[11px] leading-relaxed text-muted">
{composicao.map((d) => (
<span key={d.rotulo}>
{d.rotulo}{' '}
<span className="font-mono font-medium text-ink">{numero(d.valor)}</span>
</span>
))}
</span>
)
}
return (
<span className="text-xs leading-relaxed text-muted">
<span className="font-medium text-ink">
{p.indicador.rotulo}: {numero(p.indicador.valor)} {p.indicador.unidade}
</span>
<span className="text-subtle"> · {p.indicador.detalhe}</span>
</span>
)
}
function Foto({ p, size }: { p: Premiado; size: 'lg' | 'sm' }) {
const cls = size === 'lg' ? 'w-16 h-16' : 'w-9 h-9'
if (!p.fotoUrl) {
return <div className={`${cls} rounded-full bg-border flex-shrink-0`} />
}
return (
<img
src={p.fotoUrl}
alt={p.nome}
className={`${cls} rounded-full object-cover flex-shrink-0 border border-border`}
/>
)
}
function Identificacao({ p }: { p: Premiado }) {
return (
<>
<span className="text-muted">
{p.partido}-{p.uf}
</span>
<span className="text-subtle font-mono"> · {p.mesesAtivos}m</span>
{!p.emExercicio && p.dataFimExercicio && (
<span className="text-subtle" title="Fim do exercício na 57ª Legislatura">
{' '}
· mandato até {formatarData(p.dataFimExercicio)}
</span>
)}
</>
)
}
export default function CategoriaCard({ categoria }: { categoria: CategoriaPremio }) {
const [primeiro, ...demais] = categoria.podio
const margem =
demais.length > 0
? Math.round((primeiro.valor - demais[0].valor) * 10) / 10
: null
return (
<article className="card flex flex-col">
<header className="border-b border-border pb-4">
<div className="flex items-start justify-between gap-3">
<h3 className="font-serif text-lg font-semibold leading-snug text-ink">
{categoria.titulo}
</h3>
{categoria.peso != null && (
<span
className="shrink-0 font-mono text-[11px] text-muted"
title="Peso desta dimensão no cálculo do IDS"
>
peso {Math.round(categoria.peso * 100)}%
</span>
)}
</div>
<p className="mt-2 text-sm leading-relaxed text-muted">{categoria.descricao}</p>
<p className="mt-2 text-[11px] text-subtle">
{categoria.elegiveis} concorrentes
{categoria.ufs && ` · ${categoria.ufs.join(', ')}`}
{' · critério: '}
<code className="font-mono">{categoria.criterio}</code>
</p>
</header>
{/* 1º lugar */}
<div className="flex items-start gap-4 py-5">
<Foto p={primeiro} size="lg" />
<div className="min-w-0 flex-1">
<div className="flex items-baseline gap-2">
<span className="seal !w-6 !h-6 !text-xs" aria-label="Primeiro lugar">
1
</span>
<Link
href={`/senador?codigo=${primeiro.senadorCod}`}
className="font-serif text-xl font-semibold text-ink hover:text-primary hover:underline underline-offset-2 transition-colors"
>
{primeiro.nome}
</Link>
</div>
<p className="mt-1 text-sm">
<Identificacao p={primeiro} />
</p>
<p className="mt-2">
<Indicador p={primeiro} />
</p>
</div>
<div className="shrink-0 text-right">
<div className="font-mono text-2xl font-bold text-primary">
{numero(primeiro.valor)}
</div>
<div className="text-[10px] uppercase tracking-wider text-subtle">nota</div>
</div>
</div>
{/* 2º e 3º */}
{demais.length > 0 && (
<ol className="border-t border-border">
{demais.map((p) => (
<li
key={p.senadorCod}
className="flex items-center gap-3 border-b border-border/60 py-3 last:border-b-0"
>
<span className="w-4 shrink-0 text-center font-mono text-xs text-subtle">
{p.posicao}
</span>
<Foto p={p} size="sm" />
<div className="min-w-0 flex-1">
<Link
href={`/senador?codigo=${p.senadorCod}`}
className="text-sm font-medium text-ink hover:text-primary hover:underline underline-offset-2 transition-colors"
>
{p.nome}
</Link>
<p className="text-xs">
<Identificacao p={p} />
</p>
<p className="mt-0.5">
<Indicador p={p} />
</p>
</div>
<span className="shrink-0 font-mono text-sm font-semibold text-muted">
{numero(p.valor)}
</span>
</li>
))}
</ol>
)}
{/* Procedência do resultado — margem e desempate ficam à vista */}
<footer className="mt-auto border-t border-border pt-3 text-[11px] leading-relaxed text-subtle">
{categoria.houveDesempate ? (
<span>
<strong className="text-warning">Empate na nota.</strong> As notas são
percentis arredondados e saturam no topo. Desempate pelo indicador
bruto desta mesma dimensão — não pelo IDS geral, para que a categoria
seja decidida pelo que ela mede —, depois IDS total, tempo de
exercício e, por fim, ordem alfabética.
</span>
) : margem != null ? (
<span>Margem sobre o 2º lugar: {numero(margem)} ponto(s).</span>
) : null}
</footer>
</article>
)
}