/** * ============================================================================= * Job·Ka — Groupe KA * Auteur : Simon-Pierre Boucher * Contact : contact@spboucher.ai * Fichier : frontend/src/App.tsx * Rôle : Gabarit global (en-tête + badge Groupe KA + connexion KA ID, * navigation, routes, pied de page commun Groupe KA) * Créé : 2026-08-17 Modifié : 2026-08-25 * ============================================================================= */ import { lazy, Suspense, useEffect, useState } from "react"; import { NavLink, Route, Routes, useLocation } from "react-router-dom"; import Home from "./pages/Home"; import JobPage from "./pages/Job"; import Sources from "./pages/Sources"; import Stats from "./pages/Stats"; import Contact from "./pages/Contact"; import Employeurs from "./pages/Employeurs"; import VillePage, { VillesPage } from "./pages/Ville"; import FavorisPage from "./pages/Favoris"; import { FavProvider, jobFavItem, useFav } from "./favorites"; import { Ico, IcoHeart } from "./components/Icons"; import { useCurrentListing } from "./fiche/current"; import GroupeKaBadge from "./ka/GroupeKaBadge"; import KaFooter from "./ka/KaFooter"; // MapLibre pèse ~700 kB : chargé uniquement quand on visite /carte const MapPage = lazy(() => import("./pages/MapPage")); interface Me { name: string; email: string; picture: string; ka_id: string } /** Connexion KA ID (hub groupe-ka.com) — bouton ou profil connecté. */ function KaAuth() { const [enabled, setEnabled] = useState(false); const [me, setMe] = useState(null); useEffect(() => { fetch("/api/auth/config") .then((r) => r.json()) .then((c) => setEnabled(Boolean(c.ka))) .catch(() => {}); fetch("/api/me") .then((r) => (r.ok ? r.json() : null)) .then(setMe) .catch(() => {}); }, []); const logout = () => { fetch("/api/auth/logout", { method: "POST" }) .then(() => setMe(null)) .catch(() => {}); }; if (!enabled) return null; if (me) { return ( {me.picture ? : null} {me.name || me.email} ); } return ( Se connecter avec KA ); } /** Actions du header sur une fiche d'offre : favoris + partage (compactes). L'offre affichée est publiée par la page fiche (fiche/current.ts). */ function FicheHeaderActions() { const job = useCurrentListing(); const { ids, toggle } = useFav(); if (!job) return null; const fav = ids.has(job.uid); const share = async () => { const url = `https://www.job-ka.com/emploi/${encodeURIComponent(job.uid)}`; try { if (navigator.share) await navigator.share({ title: document.title, url }); else await navigator.clipboard.writeText(url); } catch { /* annulé */ } }; return (
); } /** Liens de navigation principaux (header desktop + menu mobile). */ const NAV_LINKS: Array<{ to: string; label: string; end?: boolean }> = [ { to: "/", label: "Offres", end: true }, { to: "/carte", label: "Carte" }, { to: "/favoris", label: "Favoris ♥" }, { to: "/sources", label: "Employeurs" }, { to: "/stats", label: "Stats" }, { to: "/employeurs", label: "Publier" }, { to: "/contact", label: "Contact" }, ]; export default function App() { const [menuOpen, setMenuOpen] = useState(false); const location = useLocation(); // fiche d'offre : header compact (56 px), sans badge ; favoris + partage const isFiche = location.pathname.startsWith("/emploi/"); // Hauteur réelle du header → --header-h sur (la carte s'y cale : // le 58 px codé en dur était faux quand le header wrappe sur 2 lignes). useEffect(() => { const el = document.querySelector("header.site"); if (!el) return; const set = () => document.documentElement.style.setProperty("--header-h", `${el.offsetHeight}px`); set(); const ro = new ResizeObserver(set); ro.observe(el); return () => ro.disconnect(); }, []); // Menu mobile : fermeture au changement de route. useEffect(() => { setMenuOpen(false); }, [location.pathname]); // Menu ouvert : verrou du scroll d'arrière-plan, bulle KA Agent masquée // (z-index maximal chez elle) + fermeture à Escape (avec cleanup). useEffect(() => { if (!menuOpen) return; const prev = document.body.style.overflow; document.body.style.overflow = "hidden"; document.body.classList.add("ka-hide-agent"); const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") setMenuOpen(false); }; window.addEventListener("keydown", onKey); return () => { document.body.style.overflow = prev; document.body.classList.remove("ka-hide-agent"); window.removeEventListener("keydown", onKey); }; }, [menuOpen]); return (
Job·Ka {!isFiche && } {isFiche && }
{/* ✕ fixe du panneau : visible peu importe le scroll, le burger du header est masqué à l'ouverture */} {NAV_LINKS.map((l) => ( (isActive ? "active" : "")} onClick={() => setMenuOpen(false)} tabIndex={menuOpen ? undefined : -1} > {l.label} ))}
{menuOpen &&
setMenuOpen(false)} />} } /> Chargement de la carte…
}> } /> } /> } /> } /> } /> } /> } /> } /> } />

Page introuvable

} />
Job-Ka — Tous les emplois des employeurs québécois, directement à la source. Chaque offre est traçable à sa source originale, zéro boîte noire. Offres Mes favoris Villes Carte Employeurs Stats Publier une offre Contact
); }