Iniciativa Cidadã Independente · Transparência Parlamentar
TSX399 linhas14.532 bytes

apps/web/src/app/page.tsx

SHA-256

ca08d26b0fd202e05fe53bd824661d1d4d452d0972dd7ff35f63e39c7cf97ccb

Somente leiturafonte-acf635076da2
'use client'

import { useEffect, useState } from 'react'
import { getRanking, getRankingMeta, getAfastados } from '@/lib/api'
import type {
  FilterPreset,
  IdsScore,
  MetaResponse,
  SenadorAfastado,
  SortKey,
  SortDir,
} from '@/lib/types'
import RankingTable from '@/components/RankingTable'
import FilterBar from '@/components/FilterBar'
import FilterTabs from '@/components/FilterTabs'
import AfastadosTable from '@/components/AfastadosTable'
import NewsletterForm from '@/components/NewsletterForm'
import ShareButtons from '@/components/ShareButtons'

export default function HomePage() {
  // Abre no ranking do prêmio: a coorte que efetivamente concorre
  const [filter, setFilter] = useState<FilterPreset>('premio')
  const [scores, setScores] = useState<IdsScore[]>([])
  const [afastados, setAfastados] = useState<SenadorAfastado[]>([])
  const [meta, setMeta] = useState<MetaResponse | null>(null)
  const [counts, setCounts] = useState<Partial<Record<FilterPreset, number>>>({})
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState<string | null>(null)
  const [sortKey, setSortKey] = useState<SortKey>('idsTotal')
  const [sortDir, setSortDir] = useState<SortDir>('desc')
  const [partido, setPartido] = useState('')
  const [uf, setUf] = useState('')
  const [bloco, setBloco] = useState('')

  useEffect(() => {
    getRankingMeta().then(setMeta).catch(console.error)
    // Pré-carrega contagens das tabs (uma vez no mount)
    Promise.all([
      getRanking({ filter: 'premio' }),
      getRanking({ filter: 'geral' }),
      getRanking({ filter: 'ativos' }),
      getRanking({ filter: 'titulares' }),
      getRanking({ filter: 'suplentes' }),
      getRanking({ filter: 'recentes' }),
      getRanking({ filter: 'encerrados' }),
      getAfastados(),
    ])
      .then(([pr, g, at, t, sup, rec, enc, af]) =>
        setCounts({
          premio: pr.total,
          geral: g.total,
          ativos: at.total,
          titulares: t.total,
          suplentes: sup.total,
          recentes: rec.total,
          encerrados: enc.total,
          afastados: af.total,
        }),
      )
      .catch(console.error)
  }, [])

  useEffect(() => {
    setLoading(true)
    setError(null)
    if (filter === 'afastados') {
      getAfastados()
        .then((r) => {
          setAfastados(r.data)
          setScores([])
        })
        .catch((e) => setError(String(e)))
        .finally(() => setLoading(false))
      return
    }
    getRanking({
      filter,
      partido: partido || undefined,
      uf: uf || undefined,
      bloco: bloco || undefined,
    })
      .then((r) => {
        setScores(r.data)
        setAfastados([])
      })
      .catch((e) => setError(String(e)))
      .finally(() => setLoading(false))
  }, [filter, partido, uf, bloco])

  const sorted = [...scores].sort((a, b) => {
    const av = a[sortKey]
    const bv = b[sortKey]
    if (typeof av === 'string' && typeof bv === 'string') {
      return sortDir === 'asc' ? av.localeCompare(bv) : bv.localeCompare(av)
    }
    return sortDir === 'asc'
      ? (av as number) - (bv as number)
      : (bv as number) - (av as number)
  })

  function handleSort(key: SortKey) {
    if (sortKey === key) {
      setSortDir((d) => (d === 'asc' ? 'desc' : 'asc'))
    } else {
      setSortKey(key)
      setSortDir('desc')
    }
  }

  return (
    <div className="space-y-12 fade-in">
      {/* ============= ABERTURA INSTITUCIONAL ============= */}
      <section className="border-b border-border pb-10">
        <p className="eyebrow">Avaliação Parlamentar · 57ª Legislatura</p>
        <h1 className="mt-3 font-serif text-4xl sm:text-5xl font-semibold leading-tight tracking-tight text-ink">
          Ranking dos Senadores da República
        </h1>
        <p className="mt-5 max-w-3xl text-base sm:text-lg leading-relaxed text-muted">
          O <strong className="text-ink">Índice de Desempenho Senatorial (IDS)</strong>{' '}
          é um indicador composto que sintetiza, em uma única nota de 0 a 100,
          a atividade parlamentar mensurável de cada Senador que exerceu o
          mandato na 57ª Legislatura. A metodologia agrega seis dimensões —
          produtividade, efetividade, participação em votações, fiscalização,
          eficiência no uso da cota parlamentar e transparência — todas
          extraídas das bases públicas oficiais do Senado Federal.
        </p>
        <p className="mt-4 max-w-3xl text-sm leading-relaxed text-muted">
          Os dados são reprocessados diariamente. A janela analítica cobre toda
          a 57ª Legislatura, iniciada em 1º de fevereiro de 2023, com
          normalização por meses ativos no mandato para garantir comparabilidade
          entre titulares e suplentes que assumiram em momentos distintos.{' '}
          <strong className="text-ink">
            Quem deixou o exercício continua avaliado
          </strong>{' '}
          — o mandato encerrado não apaga o trabalho feito na legislatura.
          Concorre ao prêmio quem acumulou ao menos 12 meses de exercício.
        </p>
      </section>

      {/* ============= INDICADORES SÍNTESE ============= */}
      {!loading && scores.length > 0 && (
        <section>
          <p className="eyebrow mb-4">Panorama Geral</p>
          <div className="grid grid-cols-2 sm:grid-cols-4 gap-px bg-border border border-border rounded-sm overflow-hidden">
            <StatCard label="Senadores avaliados" value={scores.length} suffix="" />
            <StatCard
              label="IDS médio"
              value={Math.round(
                scores.reduce((s, x) => s + x.idsTotal, 0) / scores.length,
              )}
            />
            <StatCard
              label="Maior IDS"
              value={Math.max(...scores.map((s) => s.idsTotal))}
              highlight
            />
            <StatCard
              label="Menor IDS"
              value={Math.min(...scores.map((s) => s.idsTotal))}
            />
          </div>
        </section>
      )}

      {/* ============= NOTA DE LEITURA ============= */}
      <section className="rounded-sm border-l-4 border-accent bg-panel px-5 py-4">
        <p className="eyebrow text-accent">Nota de leitura</p>
        <p className="mt-2 text-sm leading-relaxed text-muted">
          O IDS é um indicador <strong className="text-ink">quantitativo</strong>{' '}
          de atividade parlamentar e não constitui julgamento sobre o mérito ou
          a qualidade do trabalho legislativo. A partir da v3, o índice aplica{' '}
          <strong className="text-ink">regularização Bayesiana (shrinkage)</strong>{' '}
          que ajusta o score em função do tempo efetivo de mandato — assim
          titulares plenos, suplentes consolidados e recém-empossados podem ser
          comparados sem distorção por amostra pequena. Veja a{' '}
          <a
            href="/metodologia"
            className="text-primary underline-offset-2 hover:underline"
          >
            metodologia
          </a>{' '}
          para a fórmula completa.
        </p>
      </section>

      {/* ============= RANKING ============= */}
      <section className="space-y-6">
        <header className="flex flex-col sm:flex-row sm:items-end sm:justify-between gap-3">
          <div>
            <p className="eyebrow">Ranking Completo</p>
            <h2 className="mt-1 font-serif text-2xl font-semibold text-ink">
              Classificação por Índice de Desempenho Senatorial
            </h2>
            <p className="mt-1 text-sm text-muted">
              Use as abas para alternar entre os recortes. Clique nas colunas
              para reordenar e nos seletores para segmentar por partido, UF ou
              bloco parlamentar.
            </p>
          </div>
        </header>

        <FilterTabs active={filter} onChange={setFilter} counts={counts} />

        {filter !== 'afastados' && meta && (
          <FilterBar
            meta={meta}
            partido={partido}
            uf={uf}
            bloco={bloco}
            onPartido={setPartido}
            onUf={setUf}
            onBloco={setBloco}
            onClear={() => {
              setPartido('')
              setUf('')
              setBloco('')
            }}
          />
        )}

        {error ? (
          <EmptyState
            title="Ranking em preparação"
            description="Os dados estão sendo processados pelo sistema. O cálculo completo é realizado uma vez por semana. Por favor, retorne em alguns minutos."
            detail={error}
          />
        ) : loading ? (
          <LoadingState />
        ) : filter === 'afastados' ? (
          <AfastadosTable afastados={afastados} />
        ) : scores.length === 0 ? (
          <EmptyState
            title="Nenhum senador encontrado"
            description="Os filtros aplicados não retornaram resultados. Tente ampliar os critérios de busca."
          />
        ) : (
          <RankingTable
            scores={sorted}
            sortKey={sortKey}
            sortDir={sortDir}
            onSort={handleSort}
          />
        )}
      </section>

      {/* ============= COMO INTERPRETAR ============= */}
      <section className="border-t border-border pt-10">
        <p className="eyebrow">Como interpretar o IDS</p>
        <h2 className="mt-2 font-serif text-2xl font-semibold text-ink">
          Faixas de classificação
        </h2>
        <p className="mt-2 max-w-2xl text-sm text-muted">
          A pontuação final, normalizada de 0 a 100, segue uma distribuição
          normal padronizada (z-score → CDF). Um score de 50 representa o
          desempenho mediano do conjunto avaliado.
        </p>

        <div className="mt-6 grid grid-cols-1 sm:grid-cols-5 gap-px bg-border border border-border rounded-sm overflow-hidden">
          <Faixa nome="Excelente" range="≥ 75" cor="#14532d" desc="Acima de um desvio-padrão" />
          <Faixa nome="Bom" range="55 – 74" cor="#1e7d4a" desc="Acima da média" />
          <Faixa nome="Médio" range="40 – 54" cor="#92742a" desc="Em torno da média" />
          <Faixa nome="Abaixo" range="25 – 39" cor="#9b6608" desc="Abaixo da média" />
          <Faixa nome="Crítico" range="< 25" cor="#9b2c2c" desc="Mais de um desvio-padrão abaixo" />
        </div>
      </section>

      {/* ============= APOIE / INDEPENDÊNCIA ============= */}
      <section className="border-t border-border pt-10">
        <div className="rounded-sm border border-border bg-panel px-6 py-6 sm:flex sm:items-center sm:justify-between sm:gap-6">
          <div className="max-w-2xl">
            <p className="eyebrow">Iniciativa cidadã independente</p>
            <h3 className="mt-2 font-serif text-xl font-semibold text-ink">
              Ajude a manter o Observatório do Senado
            </h3>
            <p className="mt-2 text-sm text-muted leading-relaxed">
              O IDS é mantido com dados públicos, código auditável e metodologia
              documentada. Sua contribuição ajuda a custear infraestrutura,
              processamento de dados e melhorias da plataforma. Apoiadores
              <strong className="text-ink"> não influenciam</strong> a metodologia,
              o ranking ou as decisões editoriais.
            </p>
          </div>
          <a
            href="/apoie"
            className="mt-4 sm:mt-0 inline-flex shrink-0 items-center justify-center rounded-sm bg-primary px-5 py-3 text-xs font-medium uppercase tracking-wider text-white hover:bg-primary-hover transition-colors"
          >
            Apoiar o projeto →
          </a>
        </div>
      </section>

      {/* ============= COMPARTILHAR ============= */}
      <section className="border-t border-border pt-10">
        <ShareButtons
          variante="card"
          titulo="Ajude a divulgar o Observatório do Senado"
          url="https://observasenado.org"
          texto={`Observatório do Senado — Índice de Desempenho Senatorial (IDS).\n\nAvaliação multidimensional dos Senadores da 57ª Legislatura com base em dados oficiais do Senado Federal.\n\n• Produtividade legislativa\n• Efetividade das proposições\n• Participação em votações\n• Fiscalização\n• Eficiência da cota parlamentar\n• Transparência`}
        />
      </section>

      {/* ============= NEWSLETTER ============= */}
      <section className="border-t border-border pt-10">
        <NewsletterForm />
      </section>
    </div>
  )
}

function StatCard({
  label,
  value,
  suffix,
  highlight,
}: {
  label: string
  value: number
  suffix?: string
  highlight?: boolean
}) {
  return (
    <div className="bg-surface px-5 py-5">
      <p className="text-[11px] uppercase tracking-widest text-muted font-medium">
        {label}
      </p>
      <p
        className={`mt-2 font-serif text-3xl font-semibold ${
          highlight ? 'text-accent' : 'text-ink'
        }`}
      >
        {value}
        {suffix}
      </p>
    </div>
  )
}

function Faixa({
  nome,
  range,
  cor,
  desc,
}: {
  nome: string
  range: string
  cor: string
  desc: string
}) {
  return (
    <div className="bg-surface px-4 py-4">
      <div className="flex items-center gap-2">
        <span
          className="inline-block w-2 h-2 rounded-full"
          style={{ background: cor }}
        />
        <span className="font-semibold text-sm text-ink">{nome}</span>
      </div>
      <p className="mt-1 font-mono text-xs text-muted">{range}</p>
      <p className="mt-1 text-xs text-muted leading-snug">{desc}</p>
    </div>
  )
}

function LoadingState() {
  return (
    <div className="space-y-2">
      {Array.from({ length: 10 }).map((_, i) => (
        <div
          key={i}
          className="h-12 rounded-sm bg-surface border border-border animate-pulse"
          style={{ opacity: 1 - i * 0.06 }}
        />
      ))}
    </div>
  )
}

function EmptyState({
  title,
  description,
  detail,
}: {
  title: string
  description: string
  detail?: string
}) {
  return (
    <div className="rounded-sm border border-border bg-surface p-10 text-center">
      <p className="font-serif text-lg font-semibold text-ink">{title}</p>
      <p className="mt-2 text-sm text-muted max-w-md mx-auto leading-relaxed">
        {description}
      </p>
      {detail && (
        <p className="mt-4 font-mono text-[11px] text-subtle">{detail}</p>
      )}
    </div>
  )
}