"use client"; import Link from "next/link"; import { usePathname, useRouter } from "next/navigation"; import { useCallback, useEffect, useRef, useState } from "react"; import { cls, daysLabel } from "@/lib/format"; import { partyLabel } from "@/lib/parties"; import { ThemeToggle } from "./theme"; const NAV = [ { href: "/prevision", label: "Prévision" }, { href: "/signaux", label: "Signaux" }, { href: "/sondages", label: "Sondages" }, { href: "/carte", label: "Carte" }, { href: "/circonscriptions", label: "Circonscriptions" }, { href: "/partis", label: "Partis" }, { href: "/boussole", label: "Boussole" }, { href: "/methodologie", label: "Méthode" }, ]; export function Wordmark({ className, size = 22 }: { className?: string; size?: number }) { return ( QC 26 ); } export function TopNav({ daysToElection, mode, modelVersion }: { daysToElection: number; mode: "forecast" | "live" | "archive"; modelVersion?: string }) { const path = usePathname(); const [paletteOpen, setPaletteOpen] = useState(false); const [scrolled, setScrolled] = useState(false); useEffect(() => { const onKey = (e: KeyboardEvent) => { if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") { e.preventDefault(); setPaletteOpen((v) => !v); } if (e.key === "Escape") setPaletteOpen(false); }; const onScroll = () => setScrolled(window.scrollY > 8); window.addEventListener("keydown", onKey); window.addEventListener("scroll", onScroll, { passive: true }); return () => { window.removeEventListener("keydown", onKey); window.removeEventListener("scroll", onScroll); }; }, []); const liveNav = mode !== "forecast" ? [{ href: "/live", label: mode === "live" ? "Direct" : "Résultats" }] : []; return ( <>
Québec 2026
{paletteOpen && setPaletteOpen(false)} />} ); } function ElectionChip({ days, mode, modelVersion }: { days: number; mode: string; modelVersion?: string }) { if (mode === "live") return DIRECT; if (mode === "archive") return Archive · 5 oct. 2026; return ( 5 oct. 2026 {daysLabel(days)} ); } export function MobileNav({ mode }: { mode: "forecast" | "live" | "archive" }) { const path = usePathname(); const items = [ { href: "/", label: "Accueil", icon: }, mode === "forecast" ? { href: "/prevision", label: "Prévision", icon: } : { href: "/live", label: mode === "live" ? "Direct" : "Résultats", icon: }, { href: "/signaux", label: "Signaux", icon: }, { href: "/carte", label: "Carte", icon: }, { href: "/plus", label: "Plus", icon: }, ]; return ( ); } interface SearchResult { ridings: { code: number; name: string; slug: string; region: string }[]; candidates: { name: string; party: string; riding: number }[]; parties: { id: string; name: string; leader: string }[]; polls: { id: string; pollster: string; date: string }[]; documents: { id: number; title: string; party: string }[]; proposals: { id: number; party: string; topic: string; proposal: string; documentId: number }[]; topics: { id: string; label: string }[] } export function CommandPalette({ onClose }: { onClose: () => void }) { const [q, setQ] = useState(""); const [res, setRes] = useState(null); const [sel, setSel] = useState(0); const router = useRouter(); const ref = useRef(null); useEffect(() => { ref.current?.focus(); }, []); useEffect(() => { if (q.trim().length < 2) { setRes(null); return; } const ctrl = new AbortController(); const t = setTimeout(() => { fetch(`/api/search?q=${encodeURIComponent(q.trim())}`, { signal: ctrl.signal }).then((r) => r.json()).then(setRes).catch(() => {}); }, 120); return () => { clearTimeout(t); ctrl.abort(); }; }, [q]); const items: { label: string; sub?: string; href: string }[] = []; const shortcuts = [ { label: "Prévision", href: "/prevision" }, { label: "Au-delà des sondages (signaux)", href: "/signaux" }, { label: "Sondages", href: "/sondages" }, { label: "Carte électorale", href: "/carte" }, { label: "Boussole électorale", href: "/boussole" }, { label: "Comparateur des partis", href: "/partis/comparateur" }, { label: "Méthodologie", href: "/methodologie" }, { label: "Statut des données", href: "/statut" }, ]; if (!res) items.push(...shortcuts.filter((s) => !q || s.label.toLowerCase().includes(q.toLowerCase()))); else { res.ridings.forEach((r) => items.push({ label: r.name, sub: "Circonscription", href: `/circonscription/${r.slug}` })); res.parties.forEach((p) => items.push({ label: p.name, sub: `Parti · ${p.leader ?? ""}`, href: `/partis/${p.id}` })); res.candidates.forEach((c) => items.push({ label: c.name, sub: `Candidat·e · ${partyLabel(c.party)}`, href: `/circonscription/${c.riding}` })); res.polls.forEach((p) => items.push({ label: `${p.pollster} · ${p.date}`, sub: "Sondage", href: `/sondages/${p.id}` })); res.topics.forEach((t) => items.push({ label: t.label, sub: "Enjeu", href: `/enjeux/${t.id}` })); res.documents.forEach((d) => items.push({ label: d.title, sub: `Document · ${partyLabel(d.party)}`, href: `/documents/${d.id}` })); res.proposals.forEach((p) => items.push({ label: p.proposal, sub: `Proposition · ${partyLabel(p.party)}`, href: `/documents/${p.documentId}#p${p.id}` })); } const go = useCallback((href: string) => { onClose(); router.push(href); }, [onClose, router]); return (
e.stopPropagation()}> { setQ(e.target.value); setSel(0); }} placeholder="Circonscription, candidat·e, firme, enjeu, parti…" className="w-full px-5 py-4 text-[16px] outline-none bg-transparent border-b border-border" onKeyDown={(e) => { if (e.key === "ArrowDown") { e.preventDefault(); setSel((s) => Math.min(items.length - 1, s + 1)); } if (e.key === "ArrowUp") { e.preventDefault(); setSel((s) => Math.max(0, s - 1)); } if (e.key === "Enter" && items[sel]) go(items[sel].href); }} />
    {items.length === 0 &&
  • Aucun résultat.
  • } {items.slice(0, 30).map((it, i) => (
  • ))}
↑↓ naviguer↵ ouvriresc fermer
); }