SPB Git forge

spb/qc26

Public
10commits 1branches 0releases
2.8 MBsize
maindefault branch
17 days agolast push
Python 51.6% TypeScript 46.7% CSS 1.7%
3.5 KB · 37 lines tsx
Raw Blame History
1import type { Metadata } from "next";2import Link from "next/link";3import { apiSafe } from "@/lib/api";4import { PartyBadge } from "@/components/party";5import { Badge, Card, Empty, Pill } from "@/components/ui";6import { dateTimeFr } from "@/lib/format";7import { PARTY_IDS, partyLabel } from "@/lib/parties";89export const metadata: Metadata = { title: "Bibliothèque documentaire — documents officiels des partis", description: "Plateformes, programmes, cadres financiers et communiqués officiels, versionnés (SHA-256) et analysés par QC26." };10export const revalidate = 300;1112interface Doc { id: number; party: string; title: string; url: string; type: string; date: string | null; discoveredAt: string; lastCheckedAt: string | null; proposals: number; pages: number | null; isPdf: boolean; versions: number; analyzedAt: string | null; status: string }1314export default async function Page({ searchParams }: { searchParams: Promise<{ party?: string; type?: string }> }) {15  const sp = await searchParams;16  const qs = new URLSearchParams();17  if (sp.party) qs.set("party", sp.party);18  if (sp.type) qs.set("doc_type", sp.type);19  const data = await apiSafe<{ documents: Doc[] }>(`/api/documents?${qs}`);20  if (!data) return <Empty title="Bibliothèque indisponible" />;21  const types = ["plateforme", "cadre_financier", "communique", "document", "page"];22  return (23    <div className="space-y-6">24      <div><div className="eyebrow eyebrow-accent">Document Library · sources officielles uniquement</div><h1 className="display text-[40px] md:text-[56px] mt-3">Bibliothèque documentaire</h1><p className="text-ink-2 mt-3 max-w-2xl text-[15px] leading-relaxed">Chaque document est téléchargé depuis le site officiel du parti, haché (SHA-256), versionné et analysé. Quand un parti modifie un document, QC26 conserve les deux versions et affiche les changements.</p></div>25      <div className="flex flex-wrap gap-1.5 items-center text-[13px]">26        <Pill href="/documents" active={!sp.party}>Tous</Pill>{PARTY_IDS.map((id) => <Pill key={id} href={`/documents?party=${id}${sp.type ? `&type=${sp.type}` : ""}`} active={sp.party === id}>{partyLabel(id)}</Pill>)}27        <span className="mx-2 text-ink-3">·</span>{types.map((t) => <Pill key={t} href={`/documents?type=${t}${sp.party ? `&party=${sp.party}` : ""}`} active={sp.type === t}>{t.replace("_", " ")}</Pill>)}28      </div>29      <Card pad={false} className="overflow-x-auto">30        <table className="data-table min-w-[820px]"><thead><tr><th>Document</th><th>Parti</th><th>Type</th><th className="r">Pages</th><th className="r">Propositions</th><th className="r">Versions</th><th>Dernière analyse</th></tr></thead>31          <tbody>{data.documents.map((d) => (<tr key={d.id}><td><Link href={`/documents/${d.id}`} className="font-medium hover:underline">{d.title}</Link><div className="text-[11.5px] text-ink-3 truncate max-w-[60vw] md:max-w-[420px]">{d.url.replace(/^https?:\/\//, "")}</div></td><td><PartyBadge id={d.party} size="sm" /></td><td><Badge>{d.type.replace("_", " ")}</Badge>{d.isPdf && <span className="ml-1 text-[10.5px] text-ink-3">PDF</span>}</td><td className="r num">{d.pages ?? "—"}</td><td className="r num font-semibold">{d.proposals}</td><td className="r num">{d.versions}</td><td className="num text-ink-2 whitespace-nowrap">{d.analyzedAt ? dateTimeFr(d.analyzedAt) : <span className="text-ink-3">en file</span>}</td></tr>))}32            {data.documents.length === 0 && <tr><td colSpan={7} className="text-center text-ink-3 py-8">Aucun document pour ce filtre.</td></tr>}</tbody></table>33      </Card>34    </div>35  );36}37