/** * ============================================================================= * Job·Ka — Groupe KA * Auteur : Simon-Pierre Boucher * Contact : contact@spboucher.ai * Fichier : frontend/src/pages/Home.tsx * Rôle : Accueil — hero éditorial avec recherche centrale et chiffres du * marché en direct, filtres en rail discret (desktop) + panneau bas * (mobile), puces de filtres actifs, liste d'offres paginée * Créé : 2026-08-17 Modifié : 2026-08-25 * ============================================================================= */ import { FormEvent, useEffect, useMemo, useState } from "react"; import { useSearchParams } from "react-router-dom"; import { Facets, fetchFacets, fetchJobs, fetchStats, Job, LANG_FR, MODE_FR, SENIORITY_FR, Stats, TYPE_FR, } from "../api"; import JobCard from "../components/JobCard"; const PAGE = 30; /** Paramètres d'URL des filtres (hors recherche q et tri). */ const FILTER_KEYS = ["ville", "region", "categorie", "mode", "type", "seniorite", "langue", "salaire"] as const; interface FilterValues { q: string; city: string; region: string; category: string; work_mode: string; employment_type: string; seniority: string; language: string; with_salary: 1 | undefined; sort: string; } /** Les 7 sélecteurs de filtres — mêmes contrôles dans le rail (desktop) * et le panneau bas (mobile), seul le style CSS change selon le contexte. */ function FilterSelects({ facets, filters, setParam }: { facets: Facets | null; filters: FilterValues; setParam: (key: string, value: string) => void; }) { return ( <> ); } export default function Home() { const [params, setParams] = useSearchParams(); const [facets, setFacets] = useState(null); const [stats, setStats] = useState(null); const [jobs, setJobs] = useState([]); const [total, setTotal] = useState(0); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); const [sheetOpen, setSheetOpen] = useState(false); const filters = useMemo(() => ({ q: params.get("q") ?? "", city: params.get("ville") ?? "", region: params.get("region") ?? "", category: params.get("categorie") ?? "", work_mode: params.get("mode") ?? "", employment_type: params.get("type") ?? "", seniority: params.get("seniorite") ?? "", language: params.get("langue") ?? "", with_salary: params.get("salaire") === "1" ? 1 : undefined, sort: params.get("tri") ?? "recent", }), [params]); const page = Math.max(1, parseInt(params.get("page") ?? "1", 10)); useEffect(() => { fetchFacets().then(setFacets).catch(() => {}); }, []); useEffect(() => { fetchStats().then(setStats).catch(() => {}); }, []); useEffect(() => { setLoading(true); setError(""); fetchJobs(filters, PAGE, (page - 1) * PAGE) .then((r) => { setJobs(r.jobs); setTotal(r.total); }) .catch((e) => setError(String(e))) .finally(() => setLoading(false)); }, [filters, page]); // Panneau de filtres ouvert : verrou du scroll, bulle KA Agent masquée // (z-index maximal chez elle — on la cache plutôt que de la couvrir), // fermeture à Escape. useEffect(() => { if (!sheetOpen) 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") setSheetOpen(false); }; window.addEventListener("keydown", onKey); return () => { document.body.style.overflow = prev; document.body.classList.remove("ka-hide-agent"); window.removeEventListener("keydown", onKey); }; }, [sheetOpen]); const setParam = (key: string, value: string) => { const next = new URLSearchParams(params); if (value) next.set(key, value); else next.delete(key); next.delete("page"); setParams(next, { replace: true }); }; const resetFilters = () => { const next = new URLSearchParams(params); for (const k of FILTER_KEYS) next.delete(k); next.delete("q"); next.delete("page"); setParams(next, { replace: true }); }; const submitSearch = (e: FormEvent) => { e.preventDefault(); const input = e.currentTarget.elements.namedItem("q") as HTMLInputElement | null; setParam("q", input?.value.trim() ?? ""); }; const activeCount = FILTER_KEYS.filter((k) => params.get(k)).length; // puces des filtres actifs (retrait en un geste) const chips: Array<{ key: string; label: string }> = []; if (filters.q) chips.push({ key: "q", label: `« ${filters.q} »` }); if (filters.city) chips.push({ key: "ville", label: filters.city }); if (filters.region) chips.push({ key: "region", label: filters.region }); if (filters.category) chips.push({ key: "categorie", label: filters.category }); if (filters.work_mode) chips.push({ key: "mode", label: MODE_FR[filters.work_mode] ?? filters.work_mode }); if (filters.employment_type) chips.push({ key: "type", label: TYPE_FR[filters.employment_type] ?? filters.employment_type }); if (filters.seniority) chips.push({ key: "seniorite", label: SENIORITY_FR[filters.seniority] ?? filters.seniority }); if (filters.language) chips.push({ key: "langue", label: LANG_FR[filters.language] ?? filters.language }); if (filters.with_salary === 1) chips.push({ key: "salaire", label: "Salaire affiché" }); const pages = Math.max(1, Math.ceil(total / PAGE)); const pctSalary = stats && stats.total > 0 ? Math.round((stats.with_salary / stats.total) * 100) : null; return ( <>