HTML 82.1%
Python 14.6%
TypeScript 1.9%
CSS 1%
JavaScript 0.5%
1// -----------------------------------------------------------------------------2// Job·Ka — Agrégateur d'offres d'emploi (province de Québec)3// Auteur : Simon-Pierre Boucher — contact@spboucher.ai4// fiche/SectionNav.tsx : navigation sticky par sections — pastilles5// défilables horizontalement, section active suivie au scroll6// (IntersectionObserver), défilement doux au clic sans modifier l'URL7// (pas de #hash : la page s'ouvre toujours en haut).8// -----------------------------------------------------------------------------9import { useEffect, useRef, useState } from "react";1011export interface NavItem { id: string; label: string; }1213export default function SectionNav({ items }: { items: NavItem[] }) {14 const [active, setActive] = useState(items[0]?.id);15 const track = useRef<HTMLDivElement>(null);1617 useEffect(() => {18 const els = items.map((i) => document.getElementById(i.id)).filter((e): e is HTMLElement => !!e);19 if (els.length === 0) return;20 const visible = new Map<string, number>();21 const io = new IntersectionObserver((entries) => {22 for (const e of entries) visible.set((e.target as HTMLElement).id, e.isIntersecting ? e.intersectionRatio : 0);23 // section active = la première (ordre DOM) visible sous le header24 const first = items.find((i) => (visible.get(i.id) ?? 0) > 0);25 if (first) setActive(first.id);26 }, { rootMargin: "-120px 0px -55% 0px", threshold: [0, 0.1, 0.5] });27 els.forEach((e) => io.observe(e));28 return () => io.disconnect();29 }, [items]);3031 // garder la pastille active visible dans la barre — défilement HORIZONTAL de32 // la piste seulement (jamais scrollIntoView : il ferait défiler la page33 // verticalement, y compris à l'ouverture)34 useEffect(() => {35 const t = track.current, a = t?.querySelector<HTMLElement>(".on");36 if (!t || !a) return;37 const left = a.offsetLeft, right = left + a.offsetWidth;38 if (left < t.scrollLeft) t.scrollTo({ left: Math.max(0, left - 12), behavior: "smooth" });39 else if (right > t.scrollLeft + t.clientWidth) t.scrollTo({ left: right - t.clientWidth + 12, behavior: "smooth" });40 }, [active]);4142 const go = (id: string) => (e: React.MouseEvent) => {43 e.preventDefault();44 document.getElementById(id)?.scrollIntoView({ behavior: "smooth", block: "start" });45 setActive(id);46 };4748 return (49 <nav className="jk-nav" aria-label="Sections de la fiche">50 <div className="jk-nav-track" ref={track}>51 {items.map((i) => (52 <a key={i.id} href={`#${i.id}`} className={active === i.id ? "on" : ""}53 aria-current={active === i.id ? "true" : undefined} onClick={go(i.id)}>54 {i.label}55 </a>56 ))}57 </div>58 </nav>59 );60}61