TSX190 linhas8.699 bytes
apps/web/src/app/premiacao/verificar/page.tsx
SHA-256
0f708ee32d25c8b5160ddcff84b0afb8993fb655ea8ebc78ecf2f79ddf7edf85
Somente leiturafonte-0531042d9f3e
'use client'
import { Suspense, useEffect, useState } from 'react'
import { useRouter, useSearchParams } from 'next/navigation'
import Link from 'next/link'
const API = process.env.NEXT_PUBLIC_API_URL ?? 'https://api.observasenado.org'
interface Verificacao {
codigo: string
situacao: 'valido' | 'revogado' | 'adulterado'
motivoRevogacao?: string
revogadoEm?: string
emitidoEm: string
edicao: { metodologiaVersao?: string; titulo: string; subtitulo: string; periodo: { inicio: string; fim: string }; apuracaoSha256: string; divulgada: boolean }
categoria: { titulo: string; descricao: string; ambito: string; concorrentes: number }
senador: { nome: string; tratamento: string; partido: string; uf: string }
idsTotal: number
indicador: { rotulo: string; valor: number; unidade: string; detalhe: string }
pdfSha256: string | null
}
function data(iso: string): string {
return new Date(`${iso.slice(0, 10)}T12:00:00`).toLocaleDateString('pt-BR', { day: 'numeric', month: 'long', year: 'numeric' })
}
const SITUACAO = {
valido: { rotulo: 'Certificado autêntico', cor: 'border-success bg-green-50 text-success' },
revogado: { rotulo: 'Certificado revogado', cor: 'border-danger bg-red-50 text-danger' },
adulterado: { rotulo: 'Registro com integridade comprometida', cor: 'border-danger bg-red-50 text-danger' },
} as const
function Verificador() {
const params = useSearchParams()
const router = useRouter()
const codigoUrl = params.get('codigo') ?? ''
const [codigo, setCodigo] = useState(codigoUrl)
const [estado, setEstado] = useState<'ocioso' | 'buscando' | 'achou' | 'nao_achou' | 'erro'>('ocioso')
const [v, setV] = useState<Verificacao | null>(null)
const [erro, setErro] = useState('')
useEffect(() => {
setCodigo(codigoUrl)
if (!codigoUrl) return
setEstado('buscando')
fetch(`${API}/api/premiacao/verificar/${encodeURIComponent(codigoUrl.trim())}`)
.then(async (r) => {
if (r.status === 404) return setEstado('nao_achou')
const body = await r.json()
if (!r.ok) {
setErro(body.error ?? `HTTP ${r.status}`)
return setEstado('erro')
}
setV(body)
setEstado('achou')
})
.catch(() => {
setErro('não foi possível consultar agora')
setEstado('erro')
})
}, [codigoUrl])
return (
<div className="max-w-2xl mx-auto py-10">
<p className="eyebrow">Prêmio Observatório do Senado</p>
<h1 className="font-serif text-3xl text-ink mt-2">Verificação de certificado</h1>
<p className="mt-3 text-sm text-muted leading-relaxed">
Todo certificado do prêmio traz um código no formato <span className="font-mono">OS-L57-XXXX-XXXX</span> e um QR code.
Digite o código para conferir se o documento foi de fato emitido pelo Observatório.
</p>
<form
className="mt-6 flex gap-2"
onSubmit={(e) => {
e.preventDefault()
if (codigo.trim()) router.replace(`/premiacao/verificar/?codigo=${encodeURIComponent(codigo.trim().toUpperCase())}`)
}}
>
<input
value={codigo}
onChange={(e) => setCodigo(e.target.value)}
placeholder="OS-L57-XXXX-XXXX"
className="flex-1 rounded-sm border border-border bg-surface px-3 py-2 font-mono text-sm uppercase tracking-wider focus:border-primary focus:outline-none"
aria-label="Código do certificado"
/>
<button type="submit" className="rounded-sm bg-primary px-4 py-2 text-xs font-medium uppercase tracking-wider text-white hover:bg-primary-hover">
Verificar
</button>
</form>
<div className="mt-8" aria-live="polite">
{estado === 'buscando' && <p className="text-sm text-muted">Consultando…</p>}
{estado === 'nao_achou' && (
<div className="rounded-sm border border-danger bg-red-50 p-4 text-sm text-danger">
Nenhum certificado com o código <span className="font-mono">{codigoUrl.toUpperCase()}</span>. Confira se foi digitado corretamente.
Um documento com código inexistente não foi emitido pelo Observatório do Senado.
</div>
)}
{estado === 'erro' && <p className="text-sm text-danger">Erro na consulta: {erro}.</p>}
{estado === 'achou' && v && (
<div className="space-y-5">
<div className={`rounded-sm border-l-4 p-4 ${SITUACAO[v.situacao].cor}`}>
<p className="font-semibold">{SITUACAO[v.situacao].rotulo}</p>
{v.situacao === 'revogado' && (
<p className="mt-1 text-sm">
Revogado em {data(v.revogadoEm!)}. Motivo: {v.motivoRevogacao}
</p>
)}
{v.situacao === 'adulterado' && (
<p className="mt-1 text-sm">O registro deste código não confere com o selo de integridade. Não considere este certificado.</p>
)}
</div>
<div className="card p-6">
<p className="text-xs uppercase tracking-institutional text-muted">Conferido a</p>
<p className="font-serif text-2xl text-ink mt-1">{v.senador.tratamento} {v.senador.nome}</p>
<p className="text-sm text-muted">{v.senador.partido} · {v.senador.uf}</p>
<p className="mt-5 text-xs uppercase tracking-institutional text-muted">Primeiro lugar na categoria</p>
<p className="font-serif text-xl text-accent-hover mt-1">{v.categoria.titulo}</p>
<p className="text-sm text-muted mt-1">{v.categoria.descricao}</p>
<dl className="mt-5 grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm">
<div>
<dt className="text-xs text-subtle">Edição</dt>
<dd className="text-ink">{v.edicao.titulo}</dd>
<dd className="text-xs text-muted">{v.edicao.subtitulo}</dd>
</div>
<div>
<dt className="text-xs text-subtle">Emitido em</dt>
<dd className="text-ink">{data(v.emitidoEm)}</dd>
</div>
<div>
<dt className="text-xs text-subtle">Concorrentes na categoria</dt>
<dd className="text-ink">{v.categoria.concorrentes}</dd>
</div>
<div>
<dt className="text-xs text-subtle">{v.indicador.rotulo}</dt>
<dd className="text-ink">
{v.indicador.valor.toLocaleString('pt-BR')} {v.indicador.unidade}
</dd>
<dd className="text-xs text-muted">{v.indicador.detalhe}</dd>
</div>
</dl>
<div className="mt-6 border-t border-border pt-4 space-y-2 text-xs text-muted">
<p className="font-mono break-all">
<span className="font-sans text-subtle">Código: </span>{v.codigo}
</p>
{v.pdfSha256 && (
<p className="font-mono break-all">
<span className="font-sans text-subtle">SHA-256 do PDF original: </span>{v.pdfSha256}
</p>
)}
<p className="font-mono break-all">
<span className="font-sans text-subtle">SHA-256 do resultado da edição: </span>{v.edicao.apuracaoSha256}
</p>
{v.pdfSha256 && (
<p>
<a href={`${API}/api/premiacao/certificado/${v.codigo}/pdf`} className="text-primary underline">
Baixar o PDF original
</a>{' '}
para comparar com a cópia recebida.
</p>
)}
</div>
</div>
<p className="text-xs text-muted leading-relaxed">
O prêmio não tem inscrição, indicação nem votação: o resultado decorre dos dados abertos do Senado Federal aplicados ao
Índice de Desempenho Senatorial{v.edicao.metodologiaVersao ? ` (${v.edicao.metodologiaVersao})` : ''}. Veja a <Link href="/metodologia/" className="text-primary underline">metodologia</Link> e
as <Link href="/premiacao/" className="text-primary underline">categorias do prêmio</Link>.
{!v.edicao.divulgada && ' O resultado completo desta edição ainda não foi divulgado.'}
</p>
</div>
)}
</div>
</div>
)
}
export default function VerificarPage() {
return (
<Suspense fallback={<div className="max-w-2xl mx-auto py-10 text-sm text-muted">Carregando…</div>}>
<Verificador />
</Suspense>
)
}