Python 51.6%
TypeScript 46.7%
CSS 1.7%
1"use client";23import type { FeatureCollection, Geometry } from "geojson";4import Link from "next/link";5import { useEffect, useMemo, useState } from "react";6import { useMeasure } from "@/lib/use-measure";7import { cls } from "@/lib/format";8import { PARTY_IDS, partyVar, partyLabel } from "@/lib/parties";9import { QuebecMap, type MapMode, type MapRiding } from "./map";1011const MODES: { id: MapMode; label: string; hint: string }[] = [12 { id: "projection", label: "Projection", hint: "Favori par circonscription ; intensité = certitude" },13 { id: "probabilite", label: "Probabilité", hint: "Opacité proportionnelle à la probabilité de victoire" },14 { id: "marge", label: "Marge", hint: "Écart projeté entre le favori et le 2e" },15 { id: "variation", label: "Variation", hint: "Circonscriptions qui changeraient de parti vs 2022" },16 { id: "2022", label: "2022", hint: "Résultat 2022 transposé sur la carte 2026" },17];1819interface RidingsPayload { ridings: (MapRiding & { incumbent?: { party: string | null } })[]; regions: { id: string; name: string }[] }2021export function MapSection({ height = 560, compact = false, initialMode = "projection", showModes = true, live = false, liveState }: {22 height?: number; compact?: boolean; initialMode?: MapMode; showModes?: boolean; live?: boolean;23 liveState?: Record<string, { status: string; leader: string | null; bureaux: [number, number]; marginPct: number | null }> | null;24}) {25 const [geo, setGeo] = useState<FeatureCollection<Geometry, { code: number; name: string; slug: string }> | null>(null);26 const [data, setData] = useState<RidingsPayload | null>(null);27 const [mode, setMode] = useState<MapMode>(live ? "live" : initialMode);28 const { ref, width } = useMeasure<HTMLDivElement>(1000);29 const mobile = width < 640;30 useEffect(() => {31 fetch("/api/geo").then((r) => r.json()).then(setGeo).catch(() => {});32 fetch("/api/ridings").then((r) => r.json()).then(setData).catch(() => {});33 }, []);34 // « 2022 » = parti du député sortant de la circonscription contributrice principale (résultat 2022 transposé)35 const ridings = useMemo(() => {36 const out: Record<number, MapRiding> = {};37 data?.ridings.forEach((r) => {38 out[r.code] = { ...r, baseline2022: r.incumbent?.party ?? null, live: liveState?.[String(r.code)] ?? r.live ?? null };39 });40 return out;41 }, [data, liveState]);42 const counts = useMemo(() => {43 const c: Record<string, number> = {};44 Object.values(ridings).forEach((r) => {45 const id = mode === "live" ? r.live?.leader : mode === "2022" ? r.baseline2022 : r.forecast?.favorite;46 if (id) c[id] = (c[id] ?? 0) + 1;47 });48 return c;49 }, [ridings, mode]);50 if (!geo || !data) return <div ref={ref} className="rounded-[10px] bg-surface-2 animate-pulse" style={{ height: mobile ? 380 : height }} aria-busy />;51 return (52 <div ref={ref}>53 {showModes && (54 <div className="flex flex-nowrap sm:flex-wrap overflow-x-auto scrollbar-none items-center gap-1.5 mb-3 -mx-1 px-1">55 {(live ? [{ id: "live" as MapMode, label: "2026 Live", hint: "Résultats officiels en cours" }, ...MODES.filter((m) => m.id === "projection" || m.id === "2022")] : MODES).map((m) => (56 <button key={m.id} onClick={() => setMode(m.id)} title={m.hint} className={cls("chip !py-1.5 !px-3 !text-[12.5px] shrink-0", mode === m.id && "chip-active")}>{m.label}</button>57 ))}58 <span className="text-[12px] text-ink-3 ml-1 hidden md:inline">{(live ? [{ id: "live", hint: "Résultats officiels en cours" }, ...MODES] : MODES).find((m) => m.id === mode)?.hint}</span>59 </div>60 )}61 <QuebecMap geo={geo} ridings={ridings} mode={mode} height={mobile ? 820 : height} compact={compact || mobile} interactive insets={mobile ? "below" : "overlay"} />62 <div className="flex flex-wrap items-center gap-x-4 gap-y-1 mt-3 text-[12.5px]">63 {PARTY_IDS.filter((id) => counts[id]).sort((a, b) => (counts[b] ?? 0) - (counts[a] ?? 0)).map((id) => (64 <span key={id} className="inline-flex items-center gap-1.5 num"><span className="w-2.5 h-2.5 rounded-[3px]" style={{ background: partyVar(id) }} /><b>{partyLabel(id)}</b> {counts[id]}</span>65 ))}66 <span className="text-ink-3">{mode === "live" ? "En tête / élu" : mode === "2022" ? "Sièges 2022 transposés" : "Favoris (probabilité, pas certitude)"}</span>67 <Link href="/carte" className="link ml-auto text-[12.5px]">Carte complète →</Link>68 </div>69 </div>70 );71}72