Python 51.6%
TypeScript 46.7%
CSS 1.7%
1"use client";23import Link from "next/link";4import { useMemo, useState } from "react";5import type { Poll } from "@/lib/api";6import { cls, dateShort, int, modeLabel } from "@/lib/format";7import { PARTY_IDS, partyLabel, partyVar } from "@/lib/parties";8import { PollRowMobile } from "./forecast-blocks";910export function PollsTable({ polls }: { polls: Poll[] }) {11 const [firm, setFirm] = useState("all");12 const [year, setYear] = useState("all");13 const [onlyPrimary, setOnlyPrimary] = useState(false);14 const [shown, setShown] = useState(25);15 const firms = useMemo(() => Array.from(new Map(polls.map((p) => [p.pollster.id, p.pollster.name])).entries()), [polls]);16 const years = useMemo(() => Array.from(new Set(polls.map((p) => p.fieldworkEnd.slice(0, 4)))).sort().reverse(), [polls]);17 const rows = polls.filter((p) => (firm === "all" || p.pollster.id === firm) && (year === "all" || p.fieldworkEnd.startsWith(year)) && (!onlyPrimary || p.sourceTier === "primary"));18 return (19 <div>20 <div className="flex flex-wrap items-center gap-2 p-3 border-b border-border text-[13px]">21 <select value={firm} onChange={(e) => setFirm(e.target.value)} className="card px-2.5 py-1.5 max-w-[48%]" aria-label="Firme"><option value="all">Toutes les firmes</option>{firms.map(([id, n]) => <option key={id} value={id}>{n}</option>)}</select>22 <select value={year} onChange={(e) => setYear(e.target.value)} className="card px-2.5 py-1.5" aria-label="Année"><option value="all">Toutes les années</option>{years.map((y) => <option key={y} value={y}>{y}</option>)}</select>23 <label className="inline-flex items-center gap-2 ml-1"><input type="checkbox" checked={onlyPrimary} onChange={(e) => setOnlyPrimary(e.target.checked)} /> <span className="hidden sm:inline">Documents originaux seulement</span><span className="sm:hidden">Originaux</span></label>24 <span className="ml-auto num text-ink-3">{rows.length} sondages</span>25 </div>26 <div className="md:hidden">27 {rows.slice(0, shown).map((p) => <PollRowMobile key={p.id} p={p} />)}28 {rows.length > shown && <button onClick={() => setShown((n) => n + 25)} className="w-full py-3 text-[13px] font-semibold text-accent border-t border-border">Afficher 25 sondages de plus ({rows.length - shown} restants)</button>}29 </div>30 <div className="hidden md:block overflow-x-auto max-h-[70vh]">31 <table className="data-table min-w-[820px]">32 <thead><tr><th>Firme</th><th>Terrain</th><th className="r">n</th>{PARTY_IDS.map((id) => <th key={id} className="r"><span className="inline-flex items-center gap-1"><span className="w-1.5 h-1.5 rounded-full" style={{ background: partyVar(id) }} />{partyLabel(id)}</span></th>)}<th className="r">Autres</th><th className="r">Poids</th><th>Source</th></tr></thead>33 <tbody>34 {rows.map((p) => {35 const lead = Object.entries(p.results).filter(([k]) => k !== "aut").sort((a, b) => b[1] - a[1])[0]?.[0];36 return (37 <tr key={p.id} className={cls(p.excluded && "opacity-50")} title={[p.sponsor && `Commanditaire : ${p.sponsor}`, p.mode && `Mode : ${modeLabel(p.mode)}`, p.population && `Population : ${p.population}`, p.publicationDate && `Publié le ${dateShort(p.publicationDate)}`].filter(Boolean).join("\n")}>38 <td><Link href={`/sondages/${p.id}`} className="font-semibold hover:underline">{p.pollster.name}</Link>{p.sponsor && <div className="text-[11.5px] text-ink-3">{p.sponsor}</div>}</td>39 <td className="num whitespace-nowrap"><Link href={`/sondages/${p.id}`}>{p.fieldworkStart ? `${dateShort(p.fieldworkStart)} – ` : ""}{dateShort(p.fieldworkEnd)}</Link></td>40 <td className="r num">{int(p.sampleSize)}</td>41 {PARTY_IDS.map((id) => <td key={id} className={cls("r num mono", id === lead && "font-bold")} style={id === lead ? { color: partyVar(id) } : undefined}>{p.results[id] !== undefined ? p.results[id].toFixed(p.results[id] % 1 ? 1 : 0).replace(".", ",") : "—"}</td>)}42 <td className="r num text-ink-2 mono">{p.results["aut"] !== undefined ? p.results["aut"].toFixed(0) : "—"}</td>43 <td className="r num text-ink-2 mono">{p.weight ? p.weight.weight.toFixed(2) : "—"}</td>44 <td className="whitespace-nowrap">{p.sourceTier === "primary" ? <span className="text-[10.5px] font-semibold uppercase tracking-wide text-ok mono">PDF archivé</span> : <span className="text-[10.5px] font-semibold uppercase tracking-wide text-warn mono" title="Valeur relevée par un index tiers ; document original en cours de récupération">index</span>}{p.excluded && <span className="ml-1 text-[10.5px] uppercase text-live mono">exclu</span>}</td>45 </tr>46 );47 })}48 </tbody>49 </table>50 </div>51 </div>52 );53}54