/* Maitha Tech — Vagas: camada de API + helpers compartilhados. O contrato da API é idêntico ao das páginas legadas (scripts/vagas.js, vaga.js, formulario.js, formulario-indicacao.js, confirmacao-email.js). Carregado depois de mt-contact.jsx e antes de mt-vagas-ui.jsx. */ const VAGAS_API_URLS = { production: "https://vagas-api.maitha-tech.com", hml: "https://vagas-api-hml.maitha-tech.com", }; /* Mesma regra do legado: só o host maitha.com.br fala com produção. */ function vagasApiBase() { return window.location.hostname === "maitha.com.br" ? VAGAS_API_URLS.production : VAGAS_API_URLS.hml; } async function vagasFetchJson(path, init) { const res = await fetch(`${vagasApiBase()}${path}`, init); let body = null; try { body = await res.json(); } catch (e) { body = null; } if (!res.ok) { const err = new Error((body && body.error) || (body && body.message) || `HTTP ${res.status}`); err.status = res.status; err.body = body; throw err; } return body; } /* GET /opportunities → { data: { opportunitiesList, locationList, profiles, skillsList } } */ function vagasGetOpportunities() { return vagasFetchJson("/opportunities"); } /* GET /opportunities/{id} → { data: { name, description, profile, opportunityLocation:{address}, hiringType[] } } */ function vagasGetOpportunity(id) { return vagasFetchJson(`/opportunities/${encodeURIComponent(id)}`); } /* GET /positions → { data: { positions:[{name}] } } */ function vagasGetPositions() { return vagasFetchJson("/positions"); } /* GET /states → { data:[{sigla, nome}] } */ function vagasGetStates() { return vagasFetchJson("/states"); } /* GET /cities/{UF} → { city:[{nome}] } (sem wrapper data) */ function vagasGetCities(uf) { return vagasFetchJson(`/cities/${encodeURIComponent(uf)}`); } /* GET /technologies → { data:[{label}] } (inclui "Outros") */ function vagasGetTechnologies() { return vagasFetchJson("/technologies"); } /* POST multipart /candidates/withcv — devolve {status, body} sem lançar, para o formulário decidir a mensagem (200 ok / 400 body.error / outro). */ async function vagasSubmitCandidate(formData) { const res = await fetch(`${vagasApiBase()}/candidates/withcv`, { method: "POST", body: formData }); let body = null; try { body = await res.json(); } catch (e) { body = null; } return { status: res.status, body }; } /* POST JSON /candidates/email-confirmation */ async function vagasConfirmEmail(token) { const res = await fetch(`${vagasApiBase()}/candidates/email-confirmation`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ token }), }); let body = null; try { body = await res.json(); } catch (e) { body = null; } return { ok: res.ok, status: res.status, body }; } function vagasQueryParam(name) { return new URLSearchParams(window.location.search).get(name); } function vagasHexToRgba(hex, a) { const h = hex.replace("#", ""); return `rgba(${parseInt(h.substring(0, 2), 16)}, ${parseInt(h.substring(2, 4), 16)}, ${parseInt(h.substring(4, 6), 16)}, ${a})`; } /* Texto sem acentos, minúsculo, espaços normalizados — para buscas e chaves. */ function vagasFold(s) { return String(s || "").normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/\s+/g, " ").trim(); } function vagasTitleCase(s) { return String(s || "").toLowerCase().replace(/(^|[\s\-])(\p{L})/gu, (m, p, c) => p + c.toUpperCase()); } /* "São Paulo, SP, …" → { key:"sao paulo", label:"São Paulo" }. A lista de endereços da API é inconsistente ("REMOTO, REMOTO", "Remoto, SP"), então a cidade (1º segmento) vira a chave, sem acento e sem caixa. */ function vagasNormalizeLocation(address) { const city = String(address || "").split(",")[0].trim(); if (!city) return { key: "", label: "" }; const key = vagasFold(city); const label = city === city.toUpperCase() || city === city.toLowerCase() ? vagasTitleCase(city) : city; return { key, label }; } /* Primeiro valor de hiringType (string na lista, array no detalhe). */ function vagasHiringLabel(h) { const v = Array.isArray(h) ? h[0] : h; return v ? String(v) : ""; } /* —— Parser da descrição ———————————————————————————————— A API devolve texto corrido com "\n". Cabeçalhos conhecidos (com ou sem ":") abrem seções; bullets usam marcadores Unicode (✔ ➕ 🚀) ou -/•. Saída: [{ title: string|null, blocks: [{type:"p", text} | {type:"ul", kind, items}] }] */ const VAGAS_KNOWN_HEADINGS = [ "sobre a vaga", "responsabilidades", "requisitos", "diferenciais", "modelo de trabalho", "o que voce encontrara na maitha", "beneficios", "atividades", "requisitos desejaveis", "informacoes do projeto", "sobre a empresa", "o que esperamos de voce", "diferenciais competitivos", ]; const VAGAS_BULLET_MARKERS = [ { re: /^[✔✓✅]️?\s*/u, kind: "check" }, { re: /^[➕+]️?\s+/u, kind: "plus" }, { re: /^(?:🚀|[•·▪●\-–—*])️?\s*/u, kind: "dot" }, ]; function vagasStripEmoji(s) { return String(s || "").replace(/[\p{Extended_Pictographic}️]/gu, "").replace(/\s{2,}/g, " ").trim(); } function vagasClassifyBullet(line) { for (const m of VAGAS_BULLET_MARKERS) { if (m.re.test(line)) return { kind: m.kind, text: vagasStripEmoji(line.replace(m.re, "")) }; } return null; } function vagasIsHeading(line, nextLine) { const t = line.trim(); if (!t) return false; const bare = t.replace(/:$/, "").trim(); const folded = vagasFold(vagasStripEmoji(bare)); if (/:$/.test(t) && t.length <= 60) return true; if (VAGAS_KNOWN_HEADINGS.includes(folded)) return true; if (t.length <= 48 && !/[.!?;,]$/.test(t) && nextLine && vagasClassifyBullet(nextLine.trim())) return true; return false; } function vagasParseDescription(text) { const raw = String(text || ""); const lines = raw.split("\n").map((l) => l.trim()); const fallback = () => [{ title: null, blocks: lines.filter(Boolean).map((l) => ({ type: "p", text: vagasStripEmoji(l) })) }]; try { const sections = []; let cur = { title: null, blocks: [] }; let para = []; const flushPara = () => { if (para.length) { cur.blocks.push({ type: "p", text: para.join(" ") }); para = []; } }; const pushSection = () => { flushPara(); if (cur.title !== null || cur.blocks.length) sections.push(cur); }; for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (!line) { flushPara(); continue; } const next = lines.slice(i + 1).find((l) => l) || ""; const bullet = vagasClassifyBullet(line); if (bullet) { flushPara(); const last = cur.blocks[cur.blocks.length - 1]; if (last && last.type === "ul" && last.kind === bullet.kind) last.items.push(bullet.text); else cur.blocks.push({ type: "ul", kind: bullet.kind, items: [bullet.text] }); continue; } if (vagasIsHeading(line, next)) { pushSection(); cur = { title: vagasStripEmoji(line.replace(/:$/, "")), blocks: [] }; continue; } /* Linha curta sem marcador logo após uma lista, seguida por outro bullet do mesmo tipo (ou fim da seção): o autor esqueceu o marcador. */ const last = cur.blocks[cur.blocks.length - 1]; const nextBullet = next ? vagasClassifyBullet(next) : null; const nextIsBoundary = !next || vagasIsHeading(next, lines.slice(i + 2).find((l) => l) || ""); if (last && last.type === "ul" && !para.length && line.length <= 200 && ((nextBullet && nextBullet.kind === last.kind) || nextIsBoundary)) { last.items.push(vagasStripEmoji(line)); continue; } para.push(vagasStripEmoji(line)); } pushSection(); const hasContent = sections.some((s) => s.blocks.length); if (!sections.length || !hasContent) return fallback(); return sections; } catch (e) { return fallback(); } } /* Primeiro parágrafo de uma seção pelo título (sem acento). */ function vagasFindSection(sections, title) { const want = vagasFold(title); const s = (sections || []).find((x) => x.title && vagasFold(x.title) === want); if (!s) return ""; const p = s.blocks.find((b) => b.type === "p"); if (p) return p.text; const ul = s.blocks.find((b) => b.type === "ul"); return ul && ul.items.length === 1 ? ul.items[0] : ""; } Object.assign(window, { VAGAS_API_URLS, vagasApiBase, vagasFetchJson, vagasGetOpportunities, vagasGetOpportunity, vagasGetPositions, vagasGetStates, vagasGetCities, vagasGetTechnologies, vagasSubmitCandidate, vagasConfirmEmail, vagasQueryParam, vagasHexToRgba, vagasFold, vagasTitleCase, vagasNormalizeLocation, vagasHiringLabel, vagasStripEmoji, vagasParseDescription, vagasFindSection, });