Python 51.6%
TypeScript 46.7%
CSS 1.7%
1import type { Metadata } from "next";2import Link from "next/link";3import { apiSafe, type Party } from "@/lib/api";4import { PartyLogo } from "@/components/party";5import { Empty } from "@/components/ui";6import { dateFr, int, pct, prob } from "@/lib/format";7import { partyVar } from "@/lib/parties";89export const metadata: Metadata = { title: "Partis — chefs, candidatures, projections, documents", description: "Les cinq principaux partis de l'élection québécoise 2026 : chef·fe (registre DGEQ), candidatures officielles, sièges projetés, documents et propositions." };10export const revalidate = 120;1112export default async function Page() {13 const data = await apiSafe<{ parties: Party[] }>("/api/parties");14 if (!data) return <Empty title="Partis indisponibles" />;15 return (16 <div className="space-y-6">17 <div className="flex items-end justify-between flex-wrap gap-3">18 <div><div className="eyebrow eyebrow-accent">Partis · registre officiel des partis autorisés (REPAQ)</div><h1 className="display text-[40px] md:text-[56px] mt-3">Party Explorer</h1></div>19 <Link href="/partis/comparateur" className="inline-flex items-center rounded-[8px] bg-brand text-white px-4 py-2.5 text-[14px] font-semibold">Comparateur des positions →</Link>20 </div>21 <div className="grid md:grid-cols-2 xl:grid-cols-3 gap-4">22 {data.parties.sort((a, b) => (b.forecast?.seatsMedian ?? 0) - (a.forecast?.seatsMedian ?? 0)).map((p) => (23 <Link key={p.id} href={`/partis/${p.id}`} className="card card-lg p-5 block hover:shadow-md transition-shadow" style={{ borderTop: `4px solid ${p.color}` }}>24 <div className="flex items-center justify-between gap-3"><PartyLogo id={p.id} height={30} /><span className="text-[11px] uppercase tracking-wide text-ink-3">{p.abbreviation}</span></div>25 <div className="font-bold text-[18px] mt-3 leading-tight">{p.officialName}</div>26 <div className="text-[13px] text-ink-2 mt-1">{p.leader ? <>Chef·fe : <b className="text-ink">{p.leader}</b>{p.leaderSince ? <span className="text-ink-3"> · depuis {dateFr(p.leaderSince, { month: "long", year: "numeric" })}</span> : null}</> : "—"}</div>27 <div className="grid grid-cols-3 gap-3 mt-4 text-center">28 <div className="surface-2 rounded-[8px] p-2.5"><div className="text-[11px] text-ink-3">Sièges projetés</div><div className="num font-bold text-[22px]" style={{ color: partyVar(p.id) }}>{p.forecast?.seatsMedian ?? "—"}</div><div className="text-[11px] text-ink-3 num">{p.forecast ? `${p.forecast.seats80[0]}–${p.forecast.seats80[1]}` : ""}</div></div>29 <div className="surface-2 rounded-[8px] p-2.5"><div className="text-[11px] text-ink-3">Vote projeté</div><div className="num font-bold text-[22px]">{p.forecast ? pct(p.forecast.voteMean, 1, "") : "—"}<span className="text-[13px]"> %</span></div><div className="text-[11px] text-ink-3 num">2022 : {pct(p.vote2022, 1)}</div></div>30 <div className="surface-2 rounded-[8px] p-2.5"><div className="text-[11px] text-ink-3">P(majorité)</div><div className="num font-bold text-[22px]">{prob(p.forecast?.pMajority)}</div><div className="text-[11px] text-ink-3 num">2022 : {p.seats2022} sièges</div></div>31 </div>32 <div className="flex justify-between text-[12.5px] text-ink-2 mt-4 num"><span>{int(p.candidatesCount)} candidatures</span><span>{p.documents} documents · {p.proposals} propositions</span></div>33 </Link>34 ))}35 </div>36 <p className="text-[12px] text-ink-3">Chef·fe et nom officiel tels qu'inscrits au registre des partis autorisés d'Élections Québec (pp_autorises.json). Logos officiels tirés des sites des partis, non modifiés.</p>37 </div>38 );39}40