"use client"; import { useEffect, useState } from "react"; import { int, pct, prob, statusLabel, timeFr } from "@/lib/format"; import { partyVar, partyLabel } from "@/lib/parties"; import { PartyDot } from "./party"; import { Badge, Card, LiveBadge, SectionHeader } from "./ui"; interface Live { status: string; updatedAt: string | null; bureaux: [number, number]; votesValid: number; turnout: number | null; leader: string | null; marginVotes: number | null; marginPct: number | null; candidates: { name: string; party_id: string; votes: number; share: number }[] | null; final: boolean } export function RidingLive({ code, initial, forecast }: { code: number; initial: Live; forecast: { favorite: string; p: number } | null }) { const [live, setLive] = useState(initial); useEffect(() => { const load = () => fetch(`/api/ridings/${code}`, { cache: "no-store" }).then((r) => r.json()).then((d) => d.live && setLive(d.live)).catch(() => {}); const t = setInterval(load, 20000); let es: EventSource | null = null; try { es = new EventSource("/api/live/stream"); es.addEventListener("snapshot", load); } catch { /* polling */ } return () => { clearInterval(t); es?.close(); }; }, [code]); const total = live.candidates?.reduce((a, c) => a + c.votes, 0) ?? 0; return (
} />
{statusLabel(live.status)}{live.bureaux[0]} / {live.bureaux[1]} bureaux ({live.bureaux[1] ? Math.round((100 * live.bureaux[0]) / live.bureaux[1]) : 0} %){int(live.votesValid)} votes valides{live.turnout && participation {pct(live.turnout)}}màj {timeFr(live.updatedAt)}
{(live.candidates ?? []).map((c, i) => (
{c.name} {partyLabel(c.party_id)}{i === 0 && live.leader && en tête}
{int(c.votes)}{pct(total ? (100 * c.votes) / total : 0)}
))}
{live.marginVotes !== null &&
Marge : {int(live.marginVotes)} votes ({pct(live.marginPct)}){forecast && <> · Forecast QC26 avant 20 h : {partyLabel(forecast.favorite)} favori à {prob(forecast.p)}}
}
); }