/* Maitha Tech — Vagas: listagem (vagas.html). Busca a lista uma única vez (GET /opportunities) e filtra/pagina no cliente: OR dentro de cada grupo de filtro, AND entre grupos, busca sem acentos. */ const VAGAS_PAGE_SIZE = 6; function vagasReadPageParam() { const p = parseInt(vagasQueryParam("page"), 10); return Number.isFinite(p) && p > 0 ? p : 1; } function vagasWritePageParam(page) { const url = new URL(window.location.href); if (page > 1) url.searchParams.set("page", String(page)); else url.searchParams.delete("page"); history.replaceState(null, "", url.pathname + url.search + url.hash); } function useVagasList() { const [status, setStatus] = React.useState("loading"); const [all, setAll] = React.useState([]); const [query, setQuery] = React.useState(""); const [debounced, setDebounced] = React.useState(""); const [sel, setSel] = React.useState({ loc: new Set(), level: new Set(), skill: new Set() }); const [page, setPageState] = React.useState(vagasReadPageParam); const [reloadKey, setReloadKey] = React.useState(0); React.useEffect(() => { let alive = true; setStatus("loading"); vagasGetOpportunities() .then((res) => { if (!alive) return; setAll(((res && res.data && res.data.opportunitiesList) || []).filter(Boolean)); setStatus("ready"); }) .catch(() => { if (alive) setStatus("error"); }); return () => { alive = false; }; }, [reloadKey]); React.useEffect(() => { const t = setTimeout(() => setDebounced(query), 300); return () => clearTimeout(t); }, [query]); const facets = React.useMemo(() => { const count = (map, key, label) => { if (!key) return; const cur = map.get(key) || { key, label, count: 0 }; cur.count += 1; map.set(key, cur); }; const locs = new Map(), levels = new Map(), skills = new Map(); all.forEach((j) => { const l = vagasNormalizeLocation(j.location); count(locs, l.key, l.label); if (j.profile) count(levels, j.profile, j.profile); (j.skills || []).forEach((s) => count(skills, String(s), String(s))); }); const sortByLabel = (m) => Array.from(m.values()).sort((a, b) => a.label.localeCompare(b.label, "pt-BR")); return { locations: sortByLabel(locs), levels: sortByLabel(levels), skills: sortByLabel(skills) }; }, [all]); const q = vagasFold(debounced); const filtered = React.useMemo(() => all.filter((j) => { if (sel.loc.size && !sel.loc.has(vagasNormalizeLocation(j.location).key)) return false; if (sel.level.size && !sel.level.has(j.profile)) return false; if (sel.skill.size && !(j.skills || []).some((s) => sel.skill.has(String(s)))) return false; if (q) { const hay = vagasFold([j.name, (j.skills || []).join(" "), j.location, j.profile].join(" ")); if (!hay.includes(q)) return false; } return true; }), [all, sel, q]); const pages = Math.max(1, Math.ceil(filtered.length / VAGAS_PAGE_SIZE)); const safePage = Math.min(page, pages); const pageItems = filtered.slice((safePage - 1) * VAGAS_PAGE_SIZE, safePage * VAGAS_PAGE_SIZE); const firstRender = React.useRef(true); React.useEffect(() => { if (firstRender.current) { firstRender.current = false; return; } setPageState(1); }, [sel, q]); React.useEffect(() => { vagasWritePageParam(safePage); }, [safePage]); const setPage = (p) => setPageState(Math.max(1, Math.min(p, pages))); const toggle = (group, key) => setSel((s) => { const next = new Set(s[group]); if (next.has(key)) next.delete(key); else next.add(key); return { ...s, [group]: next }; }); const clearGroup = (group) => setSel((s) => ({ ...s, [group]: new Set() })); const clearAll = () => { setSel({ loc: new Set(), level: new Set(), skill: new Set() }); setQuery(""); }; const retry = () => setReloadKey((k) => k + 1); const hasFilters = Boolean(q) || sel.loc.size > 0 || sel.level.size > 0 || sel.skill.size > 0; return { status, all, facets, query, setQuery, sel, toggle, clearGroup, clearAll, hasFilters, filtered, page: safePage, pages, pageItems, setPage, retry }; } function VagasListPage() { const v = useVagasList(); const gridRef = React.useRef(null); const onPage = (p) => { v.setPage(p); const el = gridRef.current; if (!el) return; const reduce = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches; const nav = document.querySelector("nav"); const off = nav ? nav.getBoundingClientRect().height + 24 : 24; window.scrollTo({ top: el.getBoundingClientRect().top + window.pageYOffset - off, behavior: reduce ? "auto" : "smooth" }); }; return ( Vagas disponíveis>} lead="Veja as posições abertas nas nossas Units e dê o próximo passo na sua carreira tech." /> O que você gostaria de achar? v.toggle("loc", k)} onClear={() => v.clearGroup("loc")} /> v.toggle("level", k)} onClear={() => v.clearGroup("level")} /> v.toggle("skill", k)} onClear={() => v.clearGroup("skill")} /> {v.hasFilters ? ( Limpar filtros ) : null} {v.status === "loading" ? : null} {v.status === "error" ? : null} {v.status === "ready" ? ( v.filtered.length ? ( {v.pageItems.map((j, i) => )} ) : ( ) ) : null} ); } Object.assign(window, { VAGAS_PAGE_SIZE, useVagasList, VagasListPage });