function smoothScrollToElement(element, offset = 150, duration = 500) { const rect = element.getBoundingClientRect(); const startY = window.pageYOffset || window.scrollY || 0; // ✅ offset inteligente: se o elemento está abaixo mas o offset é grande demais, // reduz o offset pra garantir que o targetY fique realmente abaixo do startY. let effectiveOffset = offset; const minGap = 20; // distância mínima que você quer manter do topo if (rect.top > 0 && rect.top < offset + minGap) { effectiveOffset = Math.max(0, rect.top - minGap); } let targetY = rect.top + startY - effectiveOffset; if (targetY < 0) targetY = 0; const distance = targetY - startY; // ✅ se ainda ficou 0, mas o elemento está abaixo, força um scroll mínimo if (distance === 0 && rect.top > 0) { targetY = startY + rect.top - minGap; } const finalDistance = targetY - startY; if (finalDistance === 0) return; const startTime = performance.now(); const easeOutQuad = (t) => t * (2 - t); function step(currentTime) { const elapsed = currentTime - startTime; const t = Math.min(1, elapsed / duration); const easedT = easeOutQuad(t); window.scrollTo(0, startY + finalDistance * easedT); if (t < 1) requestAnimationFrame(step); } requestAnimationFrame(step); } function isVisible(el) { if (!el) return false; const style = window.getComputedStyle(el); const rect = el.getBoundingClientRect(); return ( style.display !== "none" && style.visibility !== "hidden" && rect.height > 0 && rect.width > 0 ); } function scrollToAssinar({ offset = 250, duration = 500, tries = 40 } = {}) { // ✅ seu wrapper real é .form-element-field8 // Tentamos pegar o botão dentro dele; se não achar, rola até o wrapper mesmo const selector = '.form-element-field8 button, .form-element-field8 input[type="button"], .form-element-field8 input[type="submit"], .form-element-field8'; let attempt = 0; function tick() { const el = document.querySelector(selector); if (isVisible(el)) { smoothScrollToElement(el, offset, duration); return; } attempt++; if (attempt < tries) requestAnimationFrame(tick); else console.warn( "[scroll] Botão de assinar não encontrado/visível. Seletor:", selector, ); } requestAnimationFrame(tick); } function scrollToPlanosById(id, offset = 220, duration = 500) { const el = document.getElementById(id); if (!el) { console.warn('[scroll] nao encontrei id:', id); return; } requestAnimationFrame(() => { requestAnimationFrame(() => { smoothScrollToElement(el, offset, duration); setTimeout(() => smoothScrollToElement(el, offset, duration), 120); setTimeout(() => smoothScrollToElement(el, offset, duration), 260); }); }); } const cepInput = document.querySelector('[name="field6[]"]'); const numeroInput = document.querySelector('[name="field7[]"]'); const nomeInput = document.querySelector('[name="field3[]"]'); const emailInput = document.querySelector('[name="field4"]'); const telefone = document.querySelector('[name="field5[]"]'); const logradouro = document.querySelector('[name="field20[]"]'); const temosBandaLargaTitle = document.querySelector(".form-element-field23"); const temosBandaLargaBox = document.querySelector(".form-element-field24"); const naoTemosBandaLargaTitle = document.querySelector(".form-element-field33"); const errorBox = document.querySelector(".form-element-field32"); let temosBandaLarga = false; let error = false; const dataPayload = {}; const digitsOnly = (s) => (s || "").replace(/\D/g, ""); let debounceT, inflight, lastKey, reqSeq = 0; let isProcessing = false; let originalBtnText = ""; // limpa campo de endereço function limpar() { logradouro.value = ""; // esconder blocos de resultado caso estejam visíveis if (temosBandaLargaTitle) temosBandaLargaTitle.style.display = "none"; if (temosBandaLargaBox) temosBandaLargaBox.style.display = "none"; if (naoTemosBandaLargaTitle) naoTemosBandaLargaTitle.style.display = "none"; if (errorBox) errorBox.style.display = "none"; } function pronto() { // preciso que todos os campos estejam preenchidos (Nome, Email, CEP e Número) const cep = digitsOnly(cepInput?.value); const n = (numeroInput?.value || "").trim(); const nome = (nomeInput?.value || "").trim(); const email = (emailInput?.value || "").trim(); return cep.length === 8 && n && nome && email; } // JSON da consulta da API: // { // bairro: address.bairro, // cidade: address.localidade, // estado: address.uf, // logradouro: address.logradouro, // naoDedicado: naoDedicado, // dedicado: dedicado, // } async function consultarCep() { if (!pronto()) { limpar(); lastKey = null; error = true; return; } const cep = digitsOnly(cepInput.value); const numero = numeroInput.value.trim(); const key = `${cep}|${numero}`; if (key === lastKey) return; // dedupe lastKey = key; // cancela requisição anterior if (inflight) inflight.abort(); inflight = new AbortController(); const mySeq = ++reqSeq; // marca ordem da requisição try { const res = await fetch("https://api.sothis.com.br/api/viabilidade", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ nome: nomeInput.value, email: emailInput.value, telefone: telefone.value, cep, numero, }), signal: inflight.signal, }); const data = await res.json().catch(() => ({})); if (!res.ok) throw new Error(data?.error || `HTTP ${res.status}`); // evita aplicar resposta atrasada if (mySeq !== reqSeq) return; error = false; // sucesso // preenche no formulário dados de endereço logradouro.value = data.logradouro || ""; // armazena resultados em uma variavel global para uso posterior temosBandaLarga = data.naoDedicado; dataPayload.bairro = data.bairro; dataPayload.cidade = data.cidade; dataPayload.estado = data.estado; dataPayload.logradouro = data.logradouro; dataPayload.naoDedicado = data.naoDedicado; dataPayload.dedicado = data.dedicado; } catch (e) { if (e.name === "AbortError") return; if (mySeq !== reqSeq) return; limpar(); lastKey = null; // permite tentar de novo // TODO: mostrar bloco de erro pro usuário console.error("Erro ao consultar viabilidade:", e); error = true; } finally { if (mySeq === reqSeq) { inflight = null; } } } // CACHE (function () { function initFormCache() { const form = document.querySelector(".fc-form-23"); // ajuste se tiver mais de um formulário console.log("FormCraft cache: init chamado. Form encontrado?", !!form); if (!form) return; form.addEventListener("submit", function () { console.log("FormCraft cache: submit disparou"); const data = {}; const fields = form.querySelectorAll( "input[name], select[name], textarea[name]", ); // Além dos campos do formulário, preciso salvar o payload da consulta de viabilidade data["viabilidade"] = dataPayload; fields.forEach(function (field) { const name = field.name; if (!name) return; if (field.type === "checkbox") { if (!data[name]) data[name] = []; if (field.checked) data[name].push(field.value); } else if (field.type === "radio") { if (field.checked) { data[name] = field.value; } } else { data[name] = field.value; } }); sessionStorage.setItem("formcraft_step1", JSON.stringify(data)); }); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", initFormCache); } else { initFormCache(); } })(); // Função para consultar viabilidade ao clicar no botão document.addEventListener("click", function (e) { const btn = e.target.closest("#consulta-viabilidade"); const boxResultado = document.querySelector(".form-element-field23"); if (!btn) return; e.preventDefault(); if (isProcessing) return; isProcessing = true; originalBtnText = btn.textContent; btn.textContent = "Processando..."; btn.disabled = true; consultarCep().finally(() => { showResultField(); smoothScrollToElement(boxResultado, 150, 500); setTimeout(() => { isProcessing = false; btn.textContent = originalBtnText; btn.disabled = false; }, 2000); }); }); // FUNÇÃO BOTÃO CONSULTAR VIABILIDADE function showResultField() { console.log("Mostrando resultado de viabilidade.", { temosBandaLarga, error, }); const temosBandaLargaTitle = document.querySelector(".form-element-field23"); const temosBandaLargaBox = document.querySelector(".form-element-field24"); const naoTemosBandaLargaTitle = document.querySelector( ".form-element-field33", ); const errorBox = document.querySelector(".form-element-field32"); const camposNaoPreenchidos = document.querySelector(".form-element-field36"); // 1) some com todos os blocos if (temosBandaLargaTitle) temosBandaLargaTitle.style.display = "none"; if (temosBandaLargaBox) temosBandaLargaBox.style.display = "none"; if (naoTemosBandaLargaTitle) naoTemosBandaLargaTitle.style.display = "none"; if (errorBox) errorBox.style.display = "none"; if (camposNaoPreenchidos) camposNaoPreenchidos.style.display = "none"; // 2) se campos não estiverem preenchidos, trata como erro if (!pronto()) { if (camposNaoPreenchidos) camposNaoPreenchidos.style.display = "block"; return; } // 3) erro de API / outro erro if (error) { if (errorBox) errorBox.style.display = "block"; return; } // 4) temos / não temos banda if (temosBandaLarga) { // TEMOS banda larga if (temosBandaLargaTitle) temosBandaLargaTitle.style.display = "block"; if (temosBandaLargaBox) temosBandaLargaBox.style.display = "block"; } else { // NÃO TEMOS banda larga if (naoTemosBandaLargaTitle) naoTemosBandaLargaTitle.style.display = "block"; } } function setPlanoSelecionadoTexto(planoLabel) { const el = document.getElementById('plano-selecionado-msg'); if (!el) { console.warn('[plano] #plano-selecionado-msg não encontrado'); return; } el.textContent = `Você selecionou o plano de: ${planoLabel}`; } function planoValueToLabel(value) { const map = { "100-mega": "100 Mega", "200-mega": "200 Mega", "500-mega": "500 Mega", "700-mega": "700 Mega", "1-gb": "1 Giga", }; return map[value] || value; } // função genérica para selecionar o tipo de plano no FIELD30 function selecionarTipoPlano(tipo) { // procura o input do FormCraft pelo name e pelo value const input = document.querySelector( 'input[name="field30[]"][value="' + tipo + '"]', ); if (!input) { console.warn('Campo field30 com value="' + tipo + '" não encontrado'); return; } // marca o radio/checkbox input.checked = true; // dispara o evento "change" para o FormCraft reagir (lógica condicional, etc.) const evt = new Event("change", { bubbles: true }); input.dispatchEvent(evt); } function selecionarPlano(plano) { const input = document.querySelector( 'input[name="field31[]"][value="' + plano + '"]', ); console.log("[plano] procurando field31", plano, "→", input); if (!input) { console.warn( '[plano] Campo field31 com value="' + plano + '" não encontrado', ); return; } input.checked = true; const evt = new Event("change", { bubbles: true }); input.dispatchEvent(evt); setPlanoSelecionadoTexto(planoValueToLabel(plano)); console.log("[plano] marcado e disparado change"); } // listeners dos botões dos cards document.addEventListener("click", function (e) { // botão plano residência const btnResidencia = e.target.closest("#btn-plano-residencia"); if (btnResidencia) { e.preventDefault(); selecionarTipoPlano("plano-residencia"); document.querySelectorAll(".seller-card__container").forEach((card) => { card.classList.remove("is-selected"); }); const card = btnResidencia.closest(".seller-card__container"); if (card) card.classList.add("is-selected"); scrollToPlanosById("planos-residencial", 220, 500); return; } // botão plano empresa const btnEmpresa = e.target.closest("#btn-plano-empresa"); if (btnEmpresa) { e.preventDefault(); selecionarTipoPlano("plano-empresa"); document.querySelectorAll(".seller-card__container").forEach((card) => { card.classList.remove("is-selected"); }); const card = btnEmpresa.closest(".seller-card__container"); if (card) card.classList.add("is-selected"); scrollToPlanosById("planos-empresa", 220, 500); return; } const btnPlano100Mega = e.target.closest("#btn-plano-100-mega"); if (btnPlano100Mega) { e.preventDefault(); selecionarPlano("100-mega"); document .querySelectorAll(".seller-card__container, .out-card__container") .forEach((el) => { el.classList.remove("is-selected"); }); const card = btnPlano100Mega.closest(".seller-card__container"); if (card) card.classList.add("is-selected"); scrollToAssinar({ offset: 500, duration: 500 }); return; } const btnPlano200Mega = e.target.closest("#btn-plano-200-mega"); if (btnPlano200Mega) { e.preventDefault(); selecionarPlano("200-mega"); document .querySelectorAll(".seller-card__container, .out-card__container") .forEach((el) => { el.classList.remove("is-selected"); }); const card = btnPlano200Mega.closest(".seller-card__container"); if (card) card.classList.add("is-selected"); scrollToAssinar({ offset: 500, duration: 500 }); return; } const btnPlano500Mega = e.target.closest("#btn-plano-500-mega"); if (btnPlano500Mega) { e.preventDefault(); selecionarPlano("500-mega"); document .querySelectorAll(".seller-card__container, .out-card__container") .forEach((el) => { el.classList.remove("is-selected"); }); const card = btnPlano500Mega.closest(".out-card__container"); if (card) card.classList.add("is-selected"); scrollToAssinar({ offset: 500, duration: 500 }); return; } const btnPlano700Mega = e.target.closest("#btn-plano-700-mega"); if (btnPlano700Mega) { e.preventDefault(); selecionarPlano("700-mega"); document .querySelectorAll(".seller-card__container, .out-card__container") .forEach((el) => { el.classList.remove("is-selected"); }); const card = btnPlano700Mega.closest(".seller-card__container"); if (card) card.classList.add("is-selected"); scrollToAssinar({ offset: 500, duration: 500 }); return; } const btnPlano1Gb = e.target.closest("#btn-plano-1-gb"); if (btnPlano1Gb) { e.preventDefault(); selecionarPlano("1-gb"); document .querySelectorAll(".seller-card__container, .out-card__container") .forEach((el) => { el.classList.remove("is-selected"); }); const card = btnPlano1Gb.closest(".seller-card__container"); if (card) card.classList.add("is-selected"); scrollToAssinar({ offset: 500, duration: 500 }); return; } }); class FormCraftCEP { constructor() { // Inicializa a classe e define os conjuntos de campos this.fieldSets = [ { cepField: document.querySelector('[name="field6[]"]'), ruaField: document.querySelector('[name="field20[]"]'), }, ]; this.fieldSets.forEach((set, index) => { this.initializeFields(set, index); }); } initializeFields(set, index) { const missingFields = []; if (!set.cepField) missingFields.push("CEP"); if (!set.ruaField) missingFields.push("Rua"); if (missingFields.length > 0) { console.error( `Erro: Não foi possível localizar os seguintes campos do conjunto ${index + 1}: ${missingFields.join(", ")}`, ); return; } // Adicionar evento ao campo de CEP set.cepField.addEventListener("input", () => { const valor = this.getCEPValue(set.cepField); if (valor.length === 8) { this.verificaCEP(valor, set); } }); } getCEPValue(cepField) { const rawValue = cepField.value.trim(); return rawValue.replace(/\D/g, ""); } limpaFormularioCEP(set) { set.ruaField.value = ""; } meuCallback(conteudo, set) { if (!("erro" in conteudo)) { set.ruaField.value = conteudo.logradouro || ""; } else { this.limpaFormularioCEP(set); alert("CEP não encontrado."); } } verificaCEP(valor, set) { if (/^[0-9]{8}$/.test(valor)) { this.pesquisaCEP(valor, set); } else { console.warn("CEP inválido:", valor); } } pesquisaCEP(cep, set) { set.ruaField.value = "..."; let script = document.createElement("script"); script.src = `https://viacep.com.br/ws/${cep}/json/?callback=formCraftCEPInstance.createCallback("${cep}", "${set.cepField.name}")`; document.body.appendChild(script); } createCallback(cep, cepFieldName) { const set = this.fieldSets.find( (set) => set.cepField.name === cepFieldName, ); return function (conteudo) { formCraftCEPInstance.meuCallback(conteudo, set); }; } } // Criar instância da classe e torná-la global window.formCraftCEPInstance = new FormCraftCEP(); // Garantir que tela scrolle em caso de campo obrigatório sem preenchimento (function scrollToFirstInvalidOnSubmit() { const form = document.querySelector(".fc-form-23"); if (!form) return; const OFFSET = 180; // ajuste conforme seu header/spacing function scrollToInvalid() { // pega o primeiro inválido nativo do HTML5 const invalid = form.querySelector(":invalid") || form.querySelector("[aria-invalid='true']") || form.querySelector(".fc-error, .error, .has-error input, .has-error textarea, .has-error select"); if (!invalid) return; // se for label/div, tenta ir no input dentro const target = invalid.matches("input, select, textarea") ? invalid : invalid.querySelector?.("input, select, textarea") || invalid; // garante foco (ajuda o FormCraft e o usuário) if (target.focus) target.focus({ preventScroll: true }); // dá um “respiro” pro layout e então rola certinho requestAnimationFrame(() => { requestAnimationFrame(() => { smoothScrollToElement(target, OFFSET, 450); }); }); } // captura: roda antes dos handlers internos finalizarem form.addEventListener( "submit", () => { // espera o FormCraft marcar erros setTimeout(scrollToInvalid, 50); setTimeout(scrollToInvalid, 200); }, true ); // fallback: se o submit nem dispara, mas o clique foi no submit form.addEventListener( "click", (e) => { const btn = e.target.closest('button[type="submit"], input[type="submit"]'); if (!btn) return; setTimeout(scrollToInvalid, 50); setTimeout(scrollToInvalid, 200); }, true ); })();