Python 51.6%
TypeScript 46.7%
CSS 1.7%
1"use client";23import { useEffect, useMemo, useState } from "react";4import { prob, timeFr } from "@/lib/format";5import { PARTY_IDS, partyVar, partyLabel } from "@/lib/parties";6import { SeatMeter } from "./charts";7import { MapSection } from "./map-section";8import { Card, Empty } from "./ui";910interface Snap { id: number; fetchedAt: string; reporting: number; final: boolean; simulated: boolean }11interface State { snapshot: Snap; ridings: Record<string, { bureaux: [number, number]; leader: string | null; votes: number[]; final: boolean }>; nowcast: { parties: Record<string, { p_majority: number; total_now: number; seats_median: number }>; p_minority: number; majority: number } | null }1213export function Replay() {14 const [snaps, setSnaps] = useState<Snap[]>([]);15 const [i, setI] = useState(0);16 const [state, setState] = useState<State | null>(null);17 const [playing, setPlaying] = useState(false);18 useEffect(() => { fetch("/api/live/replay").then((r) => r.json()).then((d) => { setSnaps(d.snapshots ?? []); setI(Math.max(0, (d.snapshots?.length ?? 1) - 1)); }).catch(() => {}); }, []);19 useEffect(() => { if (!snaps[i]) return; fetch(`/api/live/replay?t=${encodeURIComponent(snaps[i].fetchedAt)}`).then((r) => r.json()).then(setState).catch(() => {}); }, [snaps, i]);20 useEffect(() => { if (!playing) return; const t = setInterval(() => setI((k) => { if (k >= snaps.length - 1) { setPlaying(false); return k; } return k + 1; }), 700); return () => clearInterval(t); }, [playing, snaps.length]);21 const liveState = useMemo(() => { const o: Record<string, { status: string; leader: string | null; bureaux: [number, number]; marginPct: number | null }> = {}; Object.entries(state?.ridings ?? {}).forEach(([c, r]) => { o[c] = { status: r.final ? "FINAL" : r.bureaux[0] > 0 ? "LEADING" : "NOT_STARTED", leader: r.leader, bureaux: r.bureaux, marginPct: null }; }); return o; }, [state]);22 const seats = useMemo(() => { const s: Record<string, number> = {}; Object.values(state?.ridings ?? {}).forEach((r) => { if (r.leader && r.bureaux[0] > 0) s[r.leader] = (s[r.leader] ?? 0) + 1; }); return s; }, [state]);23 if (snaps.length === 0) return <Empty title="Aucun snapshot à rejouer">Le replay se remplit le soir du vote (ou en mode simulation).</Empty>;24 const cur = snaps[i];25 return (26 <div className="space-y-4">27 <Card>28 <div className="flex items-center gap-3 flex-wrap"><button onClick={() => setPlaying((p) => !p)} className="rounded-[8px] bg-brand text-white px-3 py-1.5 text-[13px] font-semibold">{playing ? "Pause" : "▶ Lecture"}</button><input type="range" min={0} max={snaps.length - 1} value={i} onChange={(e) => { setPlaying(false); setI(Number(e.target.value)); }} className="flex-1 min-w-[200px]" aria-label="Moment" /><span className="num text-[13.5px]"><b>{timeFr(cur.fetchedAt)}</b> · {cur.reporting} circ. rapportent{cur.simulated ? " · simulation" : ""}{cur.final ? " · FINAL" : ""}</span></div>29 <div className="flex justify-between text-[11px] text-ink-3 num mt-1"><span>{timeFr(snaps[0].fetchedAt)}</span><span>{timeFr(snaps[snaps.length - 1].fetchedAt)}</span></div>30 <div className="mt-4"><SeatMeter seats={seats} total={127} majority={state?.nowcast?.majority ?? 64} /></div>31 {state?.nowcast && <div className="grid grid-cols-2 sm:grid-cols-5 gap-2 mt-4">{PARTY_IDS.map((id) => (<div key={id} className="surface-2 rounded-[8px] p-2.5 text-[12.5px]"><b style={{ color: partyVar(id) }}>{partyLabel(id)}</b><div className="num mt-0.5">en tête <b>{state.nowcast!.parties[id]?.total_now ?? 0}</b> · nowcast {state.nowcast!.parties[id]?.seats_median}</div><div className="num text-ink-2">P(maj) {prob(state.nowcast!.parties[id]?.p_majority)}</div></div>))}</div>}32 </Card>33 <Card pad={false} className="p-3 md:p-4"><MapSection height={560} live liveState={liveState} showModes={false} /></Card>34 </div>35 );36}37