SPB Git forge

spb/qc26

Public
10commits 1branches 0releases
2.8 MBsize
maindefault branch
17 days agolast push
Python 51.6% TypeScript 46.7% CSS 1.7%
4.4 KB · 72 lines tsx
Raw Blame History
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