SPB Git

spb/qc26

Public
8.2 KB · 118 lines tsx
1import Link from "next/link";2import { api, apiSafe, type Forecast, type Meta, type Party, type Poll, type RidingListItem } from "@/lib/api";3import { Battlegrounds, HeroForecast, LatestPolls, MajorityPanel, PopularVoteTable, RegionalGrid, SeatProjectionTable, WhatChanged } from "@/components/forecast-blocks";4import { MapSection } from "@/components/map-section";5import { Legend, SeatMeter, TrendChart } from "@/components/charts";6import { PartyLogo } from "@/components/party";7import { Card, Empty, SectionHeader } from "@/components/ui";8import { PARTY_IDS, partyColor } from "@/lib/parties";9import { int, prob } from "@/lib/format";10import { LiveHome } from "@/components/live";1112export const revalidate = 60;1314export default async function Home() {15  const [meta, f, polls, ridings, regions, parties] = await Promise.all([16    apiSafe<Meta>("/api/meta"),17    apiSafe<Forecast>("/api/forecast/latest"),18    apiSafe<{ polls: Poll[] }>("/api/polls?limit=8"),19    apiSafe<{ ridings: RidingListItem[] }>("/api/ridings"),20    apiSafe<{ regions: { id: string; name: string; ridings: number; favorites: Record<string, number>; voteNow: Record<string, number>; vote2022: Record<string, number> }[] }>("/api/regions"),21    apiSafe<{ parties: Party[] }>("/api/parties"),22  ]);23  if (!meta || !f) return <Empty title="Le moteur QC26 est temporairement indisponible">Les projections seront de retour dans quelques instants.</Empty>;24  if (meta.mode !== "forecast") return <LiveHome meta={meta} forecast={f} />;2526  const ridingNames = Object.fromEntries((ridings?.ridings ?? []).map((r) => [r.code, { name: r.name, slug: r.slug }]));27  const seats = Object.fromEntries(Object.entries(f.parties).map(([k, v]) => [k, v.seatsMedian]));28  const from = new Date(); from.setDate(from.getDate() - 180);29  const pollPts = (polls?.polls ?? []).map((p) => ({ date: p.fieldworkEnd, values: p.results }));3031  return (32    <div className="space-y-14">33      <HeroForecast f={f} meta={meta} />3435      <section>36        <SectionHeader eyebrow="02 · Sièges" title="Prévision des sièges" description="Médiane, intervalles de crédibilité et probabilités issus des simulations Monte Carlo. Le seuil de 64 sièges est dérivé des 127 circonscriptions officielles." action={<Link href="/prevision" className="link text-[13.5px]">Prévision complète →</Link>} />37        <Card><SeatMeter seats={seats} total={meta.seatsTotal} majority={meta.majority} className="mb-6" title="Sièges médians projetés" /><SeatProjectionTable f={f} /></Card>38      </section>3940      <section className="grid lg:grid-cols-2 gap-6">41        <div>42          <SectionHeader eyebrow="03 · Vote populaire" title="Intentions de vote projetées" description="Vote au jour du scrutin (moyenne QC26 corrigée des effets maison, projetée au 5 octobre)." />43          <Card><PopularVoteTable f={f} /></Card>44        </div>45        <div>46          <SectionHeader eyebrow="04 · Majorité" title="Probabilité de majorité" description="Part des simulations où le parti obtient au moins 64 sièges." />47          <Card><MajorityPanel f={f} /></Card>48        </div>49      </section>5051      <section>52        <SectionHeader eyebrow="05 · Carte" title="Carte électorale" description="Favori par circonscription ; l'intensité de la couleur traduit la certitude, jamais un résultat acquis." action={<Link href="/carte" className="link text-[13.5px]">Explorer la carte →</Link>} />53        <Card pad={false} className="p-3 md:p-4"><MapSection height={520} compact /></Card>54      </section>5556      <section>57        <SectionHeader eyebrow="06 · Sondages" title="Derniers sondages" description={`${f.pollCount} sondages depuis novembre 2022 alimentent la moyenne QC26.`} action={<Link href="/sondages" className="link text-[13.5px]">Tous les sondages →</Link>} />58        <Card pad={false}>59          <div className="p-4 md:p-5 border-b border-border">60            {f.trend && <TrendChart trend={f.trend} polls={pollPts} from={from.toISOString().slice(0, 10)} height={240} />}61            <Legend ids={PARTY_IDS} className="mt-2" />62          </div>63          {polls && <LatestPolls polls={polls.polls} limit={6} compact />}64        </Card>65      </section>6667      <section>68        <SectionHeader eyebrow="07 · Qu'est-ce qui a changé ?" title="Depuis le dernier recalcul" description="Différences entre les deux derniers runs publiés et les entrées qui les expliquent." />69        <Card><WhatChanged f={f} ridingNames={ridingNames} /></Card>70      </section>7172      {ridings && (73        <section>74          <SectionHeader eyebrow="08 · À surveiller" title="Circonscriptions les plus disputées" description="Classées par incertitude (1 − probabilité du favori)." action={<Link href="/circonscriptions?tri=serre" className="link text-[13.5px]">Toutes les circonscriptions →</Link>} />75          <Battlegrounds ridings={ridings.ridings} codes={f.summary.battlegrounds} limit={10} />76        </section>77      )}7879      {regions && (80        <section>81          <SectionHeader eyebrow="09 · Régions" title="Prévision régionale" description="Favoris et vote projeté par région, comparés au résultat 2022 transposé sur la carte 2026." />82          <RegionalGrid regions={regions.regions} />83        </section>84      )}8586      <section className="grid lg:grid-cols-2 gap-6">87        <div className="card card-lg p-6 md:p-8 bg-brand text-white flex flex-col">88          <div className="eyebrow !text-[rgba(255,255,255,.6)]">10 · Boussole électorale</div>89          <h2 className="text-[26px] font-bold tracking-tight mt-2 leading-tight">Quel parti correspond le mieux à vos positions ?</h2>90          <p className="text-[14px] text-[rgba(255,255,255,.75)] mt-2 leading-relaxed">48 énoncés sur 6 axes. Chaque position de parti est tirée d&apos;un document officiel, avec page et citation : vous pourrez voir pourquoi.</p>91          <Link href="/boussole" className="mt-auto pt-6 inline-flex items-center gap-2 self-start bg-white text-brand font-semibold rounded-[8px] px-4 py-2.5 text-[14px]">Commencer la boussole →</Link>92        </div>93        <div>94          <SectionHeader eyebrow="11 · Partis" title="Comparaison des partis" action={<Link href="/partis/comparateur" className="link text-[13.5px]">Comparateur →</Link>} />95          <div className="grid grid-cols-1 gap-2">96            {(parties?.parties ?? []).sort((a, b) => (b.forecast?.seatsMedian ?? 0) - (a.forecast?.seatsMedian ?? 0)).map((p) => (97              <Link key={p.id} href={`/partis/${p.id}`} className="card p-3 flex items-center gap-4 hover:shadow-md transition-shadow">98                <PartyLogo id={p.id} height={22} className="w-[92px] justify-start" />99                <div className="min-w-0 flex-1"><div className="font-semibold text-[14px] truncate">{p.officialName}</div><div className="text-[12px] text-ink-3 truncate">{p.leader} · {p.candidatesCount} candidatures · {p.proposals} propositions documentées</div></div>100                <div className="text-right num shrink-0"><div className="font-bold text-[18px]" style={{ color: partyColor(p.id) }}>{p.forecast?.seatsMedian ?? "—"}</div><div className="text-[11px] text-ink-3">P(maj) {prob(p.forecast?.pMajority)}</div></div>101              </Link>102            ))}103          </div>104        </div>105      </section>106107      <section className="card card-lg p-6 md:p-8 grid md:grid-cols-[1fr_auto] gap-6 items-center">108        <div>109          <div className="eyebrow">12 · Méthodologie</div>110          <h2 className="text-[22px] font-bold tracking-tight mt-1">Comment QC26 calcule</h2>111          <p className="text-[14px] text-ink-2 mt-2 leading-relaxed max-w-2xl">Sondages pondérés et corrigés des effets maison → état latent du vote (filtre de Kalman) → swing proportionnel sur les résultats 2022 transposés → {int(f.nSimulations)} simulations à erreurs corrélées. Chaque run est versionné (modèle {f.modelVersion}, run #{f.id}{f.codeCommit ? `, commit ${f.codeCommit}` : ""}).</p>112        </div>113        <div className="flex gap-3"><Link href="/methodologie" className="inline-flex items-center rounded-[8px] bg-brand text-white px-4 py-2.5 text-[14px] font-semibold">Méthodologie</Link><Link href="/methodologie/changelog" className="inline-flex items-center rounded-[8px] border border-border px-4 py-2.5 text-[14px] font-semibold">Changelog</Link></div>114      </section>115    </div>116  );117}118
Files · f3bb07025804f73080c3fe80ab3c20f6aeea6682
Menu

Theme