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