Repository Public
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'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