SPB Git

spb/job-ka

Public
7.7 KB · 219 lines tsx
1/**2 * =============================================================================3 * Job·Ka — Groupe KA4 * Auteur  : Simon-Pierre Boucher5 * Contact : contact@spboucher.ai6 * Fichier : frontend/src/App.tsx7 * Rôle    : Gabarit global (en-tête + badge Groupe KA + connexion KA ID,8 *           navigation, routes, pied de page commun Groupe KA)9 * Créé    : 2026-08-17   Modifié : 2026-08-2510 * =============================================================================11 */12import { lazy, Suspense, useEffect, useState } from "react";13import { NavLink, Route, Routes, useLocation } from "react-router-dom";14import Home from "./pages/Home";15import JobPage from "./pages/Job";16import Sources from "./pages/Sources";17import Stats from "./pages/Stats";18import Contact from "./pages/Contact";19import Employeurs from "./pages/Employeurs";20import VillePage, { VillesPage } from "./pages/Ville";21import FavorisPage from "./pages/Favoris";22import { FavProvider } from "./favorites";23import GroupeKaBadge from "./ka/GroupeKaBadge";24import KaFooter from "./ka/KaFooter";2526// MapLibre pèse ~700 kB : chargé uniquement quand on visite /carte27const MapPage = lazy(() => import("./pages/MapPage"));2829interface Me { name: string; email: string; picture: string; ka_id: string }3031/** Connexion KA ID (hub groupe-ka.com) — bouton ou profil connecté. */32function KaAuth() {33  const [enabled, setEnabled] = useState(false);34  const [me, setMe] = useState<Me | null>(null);3536  useEffect(() => {37    fetch("/api/auth/config")38      .then((r) => r.json())39      .then((c) => setEnabled(Boolean(c.ka)))40      .catch(() => {});41    fetch("/api/me")42      .then((r) => (r.ok ? r.json() : null))43      .then(setMe)44      .catch(() => {});45  }, []);4647  const logout = () => {48    fetch("/api/auth/logout", { method: "POST" })49      .then(() => setMe(null))50      .catch(() => {});51  };5253  if (!enabled) return null;54  if (me) {55    return (56      <span className="auth-user">57        {me.picture ? <img src={me.picture} alt="" referrerPolicy="no-referrer" /> : null}58        <span className="name">{me.name || me.email}</span>59        <button type="button" onClick={logout}>Déconnexion</button>60      </span>61    );62  }63  return (64    <a className="btn-ka" href="/api/auth/ka/login">65      Se connecter avec <b>KA</b>66    </a>67  );68}6970/** Liens de navigation principaux (header desktop + menu mobile). */71const NAV_LINKS: Array<{ to: string; label: string; end?: boolean }> = [72  { to: "/", label: "Offres", end: true },73  { to: "/carte", label: "Carte" },74  { to: "/favoris", label: "Favoris ♥" },75  { to: "/sources", label: "Employeurs" },76  { to: "/stats", label: "Stats" },77  { to: "/employeurs", label: "Publier" },78  { to: "/contact", label: "Contact" },79];8081export default function App() {82  const [menuOpen, setMenuOpen] = useState(false);83  const location = useLocation();8485  // Hauteur réelle du header → --header-h sur <html> (la carte s'y cale :86  // le 58 px codé en dur était faux quand le header wrappe sur 2 lignes).87  useEffect(() => {88    const el = document.querySelector<HTMLElement>("header.site");89    if (!el) return;90    const set = () =>91      document.documentElement.style.setProperty("--header-h", `${el.offsetHeight}px`);92    set();93    const ro = new ResizeObserver(set);94    ro.observe(el);95    return () => ro.disconnect();96  }, []);9798  // Menu mobile : fermeture au changement de route.99  useEffect(() => {100    setMenuOpen(false);101  }, [location.pathname]);102103  // Menu ouvert : verrou du scroll d'arrière-plan, bulle KA Agent masquée104  // (z-index maximal chez elle) + fermeture à Escape (avec cleanup).105  useEffect(() => {106    if (!menuOpen) return;107    const prev = document.body.style.overflow;108    document.body.style.overflow = "hidden";109    document.body.classList.add("ka-hide-agent");110    const onKey = (e: KeyboardEvent) => {111      if (e.key === "Escape") setMenuOpen(false);112    };113    window.addEventListener("keydown", onKey);114    return () => {115      document.body.style.overflow = prev;116      document.body.classList.remove("ka-hide-agent");117      window.removeEventListener("keydown", onKey);118    };119  }, [menuOpen]);120121  return (122    <FavProvider>123      <header className={menuOpen ? "site menu-open" : "site"}>124        <div className="container">125          <NavLink to="/" className="logo">Job<em>·</em>Ka</NavLink>126          <GroupeKaBadge dark />127          <nav className="main">128            {NAV_LINKS.map((l) => (129              <NavLink130                key={l.to}131                to={l.to}132                end={l.end}133                className={({ isActive }) => (isActive ? "active" : "")}134              >135                {l.label}136              </NavLink>137            ))}138          </nav>139          <div className="auth-box">140            <KaAuth />141          </div>142          <button143            type="button"144            className="menu-btn"145            aria-label={menuOpen ? "Fermer le menu" : "Ouvrir le menu"}146            aria-expanded={menuOpen}147            onClick={() => setMenuOpen((o) => !o)}148          >149            <span />150            <span />151            <span />152          </button>153        </div>154        <div className={menuOpen ? "mobile-menu open" : "mobile-menu"}>155          {/* ✕ fixe du panneau : visible peu importe le scroll, le burger du156              header est masqué à l'ouverture */}157          <button158            type="button"159            className="mm-close"160            aria-label="Fermer le menu"161            onClick={() => setMenuOpen(false)}162            tabIndex={menuOpen ? undefined : -1}163          >164            ✕165          </button>166          {NAV_LINKS.map((l) => (167            <NavLink168              key={l.to}169              to={l.to}170              end={l.end}171              className={({ isActive }) => (isActive ? "active" : "")}172              onClick={() => setMenuOpen(false)}173              tabIndex={menuOpen ? undefined : -1}174            >175              {l.label}176            </NavLink>177          ))}178        </div>179      </header>180      {menuOpen && <div className="mm-backdrop" onClick={() => setMenuOpen(false)} />}181      <Routes>182        <Route path="/" element={<Home />} />183        <Route path="/carte" element={184          <Suspense fallback={<div className="container empty">Chargement de la carte…</div>}>185            <MapPage />186          </Suspense>187        } />188        <Route path="/emploi/:uid" element={<JobPage />} />189        <Route path="/favoris" element={<FavorisPage />} />190        <Route path="/villes" element={<VillesPage />} />191        <Route path="/ville/:ville" element={<VillePage />} />192        <Route path="/sources" element={<Sources />} />193        <Route path="/stats" element={<Stats />} />194        <Route path="/contact" element={<Contact />} />195        <Route path="/employeurs" element={<Employeurs />} />196        <Route path="*" element={<div className="container empty"><h2>Page introuvable</h2></div>} />197      </Routes>198      <div className="prefooter">199        <div className="container">200          <span className="baseline">201            <b>Job-Ka</b> — Tous les emplois des employeurs québécois, directement202            à la source. Chaque offre est traçable à sa source originale, zéro203            boîte noire.204          </span>205          <NavLink to="/">Offres</NavLink>206          <NavLink to="/favoris">Mes favoris</NavLink>207          <NavLink to="/villes">Villes</NavLink>208          <NavLink to="/carte">Carte</NavLink>209          <NavLink to="/sources">Employeurs</NavLink>210          <NavLink to="/stats">Stats</NavLink>211          <NavLink to="/employeurs">Publier une offre</NavLink>212          <NavLink to="/contact">Contact</NavLink>213        </div>214      </div>215      <KaFooter siteId="job-ka" localLegal={[{ label: "Documentation", href: "/doc/" }]} />216    </FavProvider>217  );218}219
Files · 46f36ee4361b1e14acc76f02a08a736e59fb4ede
Menu

Theme