Python 51.6%
TypeScript 46.7%
CSS 1.7%
1import Link from "next/link";2import { apiSafe, type Forecast, type Meta, type Party, type Poll, type RidingListItem, type SignalsPayload } from "@/lib/api";3import { Battlegrounds, ComponentsCompare, HeroForecast, LatestPolls, MajorityPanel, PopularVoteTable, RegionalGrid, SeatProjectionTable, WhatChanged } from "@/components/forecast-blocks";4import { MapSection } from "@/components/map-section";5import { Legend, SeatMeter } from "@/components/charts";6import { TrendChartInteractive } from "@/components/trend-chart";7import { MarketsPanel, MediaPanel, SignalsOverview } from "@/components/signals";8import { PartyLogo } from "@/components/party";9import { Card, Empty, SectionHeader } from "@/components/ui";10import { PARTY_IDS, partyVar } from "@/lib/parties";11import { int, prob } from "@/lib/format";12import { LiveHome } from "@/components/live";1314export const revalidate = 60;1516export default async function Home() {17 const [meta, f, polls, ridings, regions, parties, sig] = await Promise.all([18 apiSafe<Meta>("/api/meta"),19 apiSafe<Forecast>("/api/forecast/latest"),20 apiSafe<{ polls: Poll[] }>("/api/polls?limit=8"),21 apiSafe<{ ridings: RidingListItem[] }>("/api/ridings"),22 apiSafe<{ regions: { id: string; name: string; ridings: number; favorites: Record<string, number>; voteNow: Record<string, number>; vote2022: Record<string, number> }[] }>("/api/regions"),23 apiSafe<{ parties: Party[] }>("/api/parties"),24 apiSafe<SignalsPayload>("/api/signals"),25 ]);26 if (!meta || !f) return <Empty title="Le moteur QC26 est temporairement indisponible">Les projections seront de retour dans quelques instants.</Empty>;27 if (meta.mode !== "forecast") return <LiveHome meta={meta} forecast={f} />;2829 const ridingNames = Object.fromEntries((ridings?.ridings ?? []).map((r) => [r.code, { name: r.name, slug: r.slug }]));30 const seats = Object.fromEntries(Object.entries(f.parties).map(([k, v]) => [k, v.seatsMedian]));31 const from = new Date(); from.setDate(from.getDate() - 182);32 const pollPts = (polls?.polls ?? []).map((p) => ({ date: p.fieldworkEnd, values: p.results, pollster: p.pollster.name }));33 const comp = f.summary.components;3435 return (36 <div className="space-y-12 md:space-y-16">37 <HeroForecast f={f} meta={meta} />3839 {comp && (40 <section>41 <SectionHeader num="02" eyebrow="Au-delà des sondages" title="Marchés, presse, attention, terrain" description="Depuis le modèle 2.0, quatre familles de signaux indépendants corrigent la moyenne des sondages, chacune avec un poids borné et publié. La vue « sondages seuls » reste toujours visible." action={<Link href="/signaux" className="link text-[13.5px]">Tous les signaux →</Link>} />42 <SignalsOverview comp={comp} />43 <Card className="mt-4"><ComponentsCompare f={f} /></Card>44 </section>45 )}4647 <section>48 <SectionHeader num="03" eyebrow="Sièges" title="Prévision des sièges" description={`Médiane, intervalles de crédibilité et probabilités issus des simulations pondérées. Le seuil de ${meta.majority} sièges est dérivé des ${meta.seatsTotal} circonscriptions officielles.`} action={<Link href="/prevision" className="link text-[13.5px]">Prévision complète →</Link>} />49 <Card><SeatMeter seats={seats} total={meta.seatsTotal} majority={meta.majority} className="mb-6" title="Sièges médians projetés" /><SeatProjectionTable f={f} /></Card>50 </section>5152 <section className="grid lg:grid-cols-2 gap-6">53 <div>54 <SectionHeader num="04" eyebrow="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)." />55 <Card><PopularVoteTable f={f} /></Card>56 </div>57 <div>58 <SectionHeader num="05" eyebrow="Majorité" title="Probabilité de majorité" description={`Part des simulations où le parti obtient au moins ${meta.majority} sièges.`} />59 <Card><MajorityPanel f={f} /></Card>60 </div>61 </section>6263 <section>64 <SectionHeader num="06" eyebrow="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>} />65 <Card pad={false} className="p-3 md:p-4"><MapSection height={520} compact /></Card>66 </section>6768 <section>69 <SectionHeader num="07" eyebrow="Sondages" title="Tendance et 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>} />70 <Card pad={false}>71 <div className="p-4 md:p-5 border-b border-border">72 {f.trend && <TrendChartInteractive trend={f.trend} polls={pollPts} from={from.toISOString().slice(0, 10)} height={260} markers={[{ date: meta.campaignStart, label: "Déclenchement" }]} />}73 <Legend ids={PARTY_IDS} className="mt-2" />74 </div>75 {polls && <LatestPolls polls={polls.polls} limit={6} compact />}76 </Card>77 </section>7879 {sig && sig.markets.asOf && (80 <section className="grid lg:grid-cols-2 gap-6">81 <div>82 <SectionHeader num="08" eyebrow="Marchés de prédiction" title="Ce que parient les marchés" description="Polymarket, prix « Oui » renormalisés. Comparés à la vue « sondages seuls » et à l'ensemble ci-dessus." action={<Link href="/signaux#marches" className="link text-[13.5px]">Détail →</Link>} />83 <Card><MarketsPanel m={sig.markets} comp={comp} compact /></Card>84 </div>85 <div>86 <SectionHeader num="09" eyebrow="Presse" title="Le ton de la couverture" description={`${sig.media.articles} titres des ${sig.media.window} derniers jours, classés par parti et par ton.`} action={<Link href="/signaux#presse" className="link text-[13.5px]">Revue de presse →</Link>} />87 <Card><MediaPanel m={sig.media} compact /></Card>88 </div>89 </section>90 )}9192 <section>93 <SectionHeader num="10" eyebrow="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." />94 <Card><WhatChanged f={f} ridingNames={ridingNames} /></Card>95 </section>9697 {ridings && (98 <section>99 <SectionHeader num="11" eyebrow="À 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>} />100 <Battlegrounds ridings={ridings.ridings} codes={f.summary.battlegrounds} limit={10} />101 </section>102 )}103104 {regions && (105 <section>106 <SectionHeader num="12" eyebrow="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." />107 <RegionalGrid regions={regions.regions} />108 </section>109 )}110111 <section className="grid lg:grid-cols-2 gap-6">112 <div className="card card-lg p-6 md:p-8 ink-panel flex flex-col">113 <div className="eyebrow" style={{ color: "color-mix(in srgb, var(--text-inverse) 60%, transparent)" }}>13 · Boussole électorale</div>114 <h2 className="display text-[30px] mt-3 leading-tight">Quel parti correspond le mieux à vos positions ?</h2>115 <p className="text-[14px] opacity-75 mt-3 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>116 <Link href="/boussole" className="mt-auto pt-6 btn self-start" style={{ background: "var(--text-inverse)", color: "var(--text)" }}>Commencer la boussole →</Link>117 </div>118 <div>119 <SectionHeader num="14" eyebrow="Partis" title="Comparaison des partis" action={<Link href="/partis/comparateur" className="link text-[13.5px]">Comparateur →</Link>} />120 <div className="grid grid-cols-1 gap-2">121 {(parties?.parties ?? []).sort((a, b) => (b.forecast?.seatsMedian ?? 0) - (a.forecast?.seatsMedian ?? 0)).map((p) => (122 <Link key={p.id} href={`/partis/${p.id}`} className="card card-hover p-3 flex items-center gap-4">123 <PartyLogo id={p.id} height={22} className="w-[92px] justify-start" />124 <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>125 <div className="text-right num shrink-0"><div className="figure text-[22px]" style={{ color: partyVar(p.id) }}>{p.forecast?.seatsMedian ?? "—"}</div><div className="text-[11px] text-ink-3">P(maj) {prob(p.forecast?.pMajority)}</div></div>126 </Link>127 ))}128 </div>129 </div>130 </section>131132 <section className="card card-lg p-6 md:p-8 grid md:grid-cols-[1fr_auto] gap-6 items-center">133 <div>134 <div className="eyebrow">15 · Méthodologie</div>135 <h2 className="display text-[26px] mt-2">Comment QC26 calcule</h2>136 <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) → prior fondamentaux et momentum bornés → swing proportionnel sur les résultats 2022 transposés → {int(f.nSimulations)} simulations à erreurs corrélées, repondérées vers les marchés. Chaque run est versionné (modèle {f.modelVersion}, run #{f.id}{f.codeCommit ? `, commit ${f.codeCommit}` : ""}).</p>137 </div>138 <div className="flex gap-3"><Link href="/methodologie" className="btn btn-primary">Méthodologie</Link><Link href="/methodologie/changelog" className="btn btn-ghost">Changelog</Link></div>139 </section>140 </div>141 );142}143