SPB Git

spb/qc26

Public
4.2 KB · 69 lines tsx
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
Files · 08f5606551574077c8600a5faf68d3daef5dbc10
Menu

Theme