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%
3.2 KB · 43 lines tsx
Raw Blame History
1import { ImageResponse } from "next/og";2import { ENGINE_URL, type Forecast } from "@/lib/api";3import { PARTIES } from "@/lib/parties";45export const runtime = "nodejs";6export const alt = "QC26 — Projection de l'élection québécoise 2026";7export const size = { width: 1200, height: 630 };8export const contentType = "image/png";910export default async function Image() {11  let f: Forecast | null = null;12  try { f = await fetch(`${ENGINE_URL}/api/forecast/latest`, { next: { revalidate: 300 } }).then((r) => r.json()); } catch { f = null; }13  const rows = f ? Object.entries(f.parties).filter(([id]) => id !== "aut").sort((a, b) => b[1].seatsMean - a[1].seatsMean) : [];14  const color = (id: string) => PARTIES.find((p) => p.id === id)?.color ?? "#999";15  const date = f?.completedAt ? new Date(f.completedAt).toLocaleDateString("fr-CA", { day: "numeric", month: "long", timeZone: "America/Toronto" }) : "";16  return new ImageResponse(17    (18      <div style={{ display: "flex", width: "100%", height: "100%", flexDirection: "column", background: "#F7F8FA", color: "#111318", padding: 56, fontFamily: "Inter, sans-serif" }}>19        <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>20          <div style={{ display: "flex", alignItems: "baseline", fontSize: 44, fontWeight: 800, letterSpacing: -2 }}><span>QC</span><span style={{ width: 4, height: 36, background: "#2563EB", margin: "0 6px", borderRadius: 2 }} /><span style={{ color: "#2563EB" }}>26</span></div>21          <div style={{ display: "flex", fontSize: 20, color: "#656B76", letterSpacing: 3, textTransform: "uppercase" }}>Projection · {date}</div>22        </div>23        <div style={{ display: "flex", flex: 1, marginTop: 30, gap: 40 }}>24          <div style={{ display: "flex", flexDirection: "column", justifyContent: "center", flex: 1 }}>25            {rows.map(([id, v]) => (26              <div key={id} style={{ display: "flex", alignItems: "center", gap: 18, marginBottom: 14 }}>27                <div style={{ display: "flex", width: 90, fontSize: 30, fontWeight: 700, color: color(id) }}>{id.toUpperCase()}</div>28                <div style={{ display: "flex", flex: 1, height: 26, background: "#E9ECF0", borderRadius: 6, position: "relative" }}>29                  <div style={{ display: "flex", width: `${(v.seatsMedian / (f?.summary.seats_total ?? 127)) * 100}%`, height: 26, background: color(id), borderRadius: 6 }} />30                  <div style={{ display: "flex", position: "absolute", left: `${((f?.summary.majority ?? 64) / (f?.summary.seats_total ?? 127)) * 100}%`, top: -6, width: 3, height: 38, background: "#111318" }} />31                </div>32                <div style={{ display: "flex", width: 150, fontSize: 34, fontWeight: 800, textAlign: "right" }}>{v.seatsMedian}<span style={{ fontSize: 18, color: "#656B76", fontWeight: 500 }}> {v.seats80[0]}–{v.seats80[1]}</span></div>33              </div>34            ))}35          </div>36        </div>37        <div style={{ display: "flex", justifyContent: "space-between", fontSize: 20, color: "#656B76" }}><span>Québec 2026 · 5 octobre · {f?.summary.seats_total ?? 127} sièges · majorité {f?.summary.majority ?? 64}</span><span>qc26.ca</span></div>38      </div>39    ),40    { ...size },41  );42}43