// ============================================================ // landing-form.jsx — Formulário de captação (lista de espera) // // Campos: e-mail (obrigatório) · WhatsApp opcional c/ seletor de país (bandeiras) // · país (dropdown) · consentimento obrigatório · honeypot anti-spam // · campos UTM ocultos (capturados da URL). // // Depende de: intl-tel-input (CDN, carregado no via index.php/server.js). // Expõe: window.LandingForm // ============================================================ // Lista de países (código ISO-2 + nome PT/EN/ES). Usada no dropdown "País". // Ordem: Brasil e principais hispanos/EUA no topo, depois alfabética. const COUNTRIES = [ { code: 'BR', pt: 'Brasil', en: 'Brazil', es: 'Brasil' }, { code: 'PT', pt: 'Portugal', en: 'Portugal', es: 'Portugal' }, { code: 'US', pt: 'Estados Unidos', en: 'United States', es: 'Estados Unidos' }, { code: 'AR', pt: 'Argentina', en: 'Argentina', es: 'Argentina' }, { code: 'MX', pt: 'México', en: 'Mexico', es: 'México' }, { code: 'CL', pt: 'Chile', en: 'Chile', es: 'Chile' }, { code: 'CO', pt: 'Colômbia', en: 'Colombia', es: 'Colombia' }, { code: 'PE', pt: 'Peru', en: 'Peru', es: 'Perú' }, { code: 'VE', pt: 'Venezuela', en: 'Venezuela', es: 'Venezuela' }, { code: 'UY', pt: 'Uruguai', en: 'Uruguay', es: 'Uruguay' }, { code: 'PY', pt: 'Paraguai', en: 'Paraguay', es: 'Paraguay' }, { code: 'BO', pt: 'Bolívia', en: 'Bolivia', es: 'Bolivia' }, { code: 'EC', pt: 'Equador', en: 'Ecuador', es: 'Ecuador' }, { code: 'ES', pt: 'Espanha', en: 'Spain', es: 'España' }, { code: 'CA', pt: 'Canadá', en: 'Canada', es: 'Canadá' }, { code: 'GB', pt: 'Reino Unido', en: 'United Kingdom', es: 'Reino Unido' }, { code: 'DE', pt: 'Alemanha', en: 'Germany', es: 'Alemania' }, { code: 'FR', pt: 'França', en: 'France', es: 'Francia' }, { code: 'IT', pt: 'Itália', en: 'Italy', es: 'Italia' }, { code: 'OT', pt: 'Outro', en: 'Other', es: 'Otro' }, ]; // Lê um parâmetro da query string (?utm_source=...). function getQueryParam(name) { try { return new URLSearchParams(window.location.search).get(name) || ''; } catch (_) { return ''; } } // ── Marco ornamental nos cantos (mesmo visual dos outros cards) ── const CornerMarks = () => ( <> ); const LandingForm = () => { const { t, lang } = useT(); const [values, setValues] = React.useState({ email: '', country: '', consent: false, sugestao: '' }); const [errors, setErrors] = React.useState({}); const [status, setStatus] = React.useState('idle'); // idle | sending | success | error const [serverError, setServerError] = React.useState(''); const phoneInputRef = React.useRef(null); // do telefone const itiRef = React.useRef(null); // instância do intl-tel-input const honeypotRef = React.useRef(null); // campo isca (deve ficar vazio) // UTM capturados uma única vez no carregamento. const utm = React.useRef({ utm_source: getQueryParam('utm_source'), utm_medium: getQueryParam('utm_medium'), utm_campaign: getQueryParam('utm_campaign'), }); // Inicializa o intl-tel-input no de telefone (bandeiras + código de país). React.useEffect(() => { if (!phoneInputRef.current || !window.intlTelInput) return; const iti = window.intlTelInput(phoneInputRef.current, { initialCountry: 'br', preferredCountries: ['br', 'pt', 'us', 'ar', 'mx', 'cl', 'co'], separateDialCode: true, // utilsScript não é estritamente necessário; validação leve é feita abaixo. }); itiRef.current = iti; return () => { try { iti.destroy(); } catch (_) {} }; }, []); const countryName = (c) => c[lang] || c.en; const validate = (v) => { const e = {}; // E-mail obrigatório if (!v.email.trim()) e.email = t('lp_err_email_required'); else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v.email.trim())) e.email = t('lp_err_email_invalid'); // País obrigatório if (!v.country) e.country = t('lp_err_country_required'); // Consentimento obrigatório if (!v.consent) e.consent = t('lp_err_consent_required'); // Telefone é OPCIONAL — só validamos se algo foi digitado. // Validação leve por contagem de dígitos (NÃO depende do utils.js do // intl-tel-input, que não é carregado). 6–15 dígitos = faixa válida E.164. // O servidor (inscrever.php) revalida — esta é só uma checagem de sanidade. const iti = itiRef.current; if (iti) { const raw = phoneInputRef.current.value.trim(); if (raw) { const digits = raw.replace(/\D/g, ''); if (digits.length < 6 || digits.length > 15) { e.phone = t('lp_err_phone_invalid'); } } } return e; }; const onChange = (k) => (e) => { const val = k === 'consent' ? e.target.checked : e.target.value; const next = { ...values, [k]: val }; setValues(next); if (Object.keys(errors).length) setErrors(validate(next)); }; const onSubmit = async (e) => { e.preventDefault(); // Honeypot: se preenchido, é bot. Fingimos sucesso e não enviamos nada. if (honeypotRef.current && honeypotRef.current.value) { setStatus('success'); return; } const errs = validate(values); setErrors(errs); if (Object.keys(errs).length > 0) return; // Número internacional completo (ex: +5511999998888) — vazio se não preenchido. // Montado manualmente a partir do DDI do país selecionado (getSelectedCountryData // NÃO depende do utils.js) + dígitos digitados. Necessário porque separateDialCode:true // mantém o DDI fora do , e getNumber() retornaria vazio sem o utils.js. let phone = ''; const iti = itiRef.current; if (iti && phoneInputRef.current.value.trim()) { const data = iti.getSelectedCountryData(); const dial = (data && data.dialCode) || ''; const digits = phoneInputRef.current.value.replace(/\D/g, ''); phone = digits ? '+' + dial + digits : ''; } const payload = { email: values.email.trim(), telefone: phone, pais: values.country, idioma: lang, sugestao: values.sugestao.trim().slice(0, 500), consentimento: values.consent ? 1 : 0, utm_source: utm.current.utm_source, utm_medium: utm.current.utm_medium, utm_campaign: utm.current.utm_campaign, website: '', // honeypot (sempre vazio aqui) }; setStatus('sending'); setServerError(''); try { const resp = await fetch('/api/inscrever.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); const data = await resp.json().catch(() => ({})); if (resp.ok && data.ok) { // ===== RASTREAMENTO — disparar evento "Lead" no sucesso ===== // Descomente conforme for ativando cada pixel (IDs configurados no ). if (window.fbq) fbq('track', 'Lead'); // if (window.gtag) gtag('event', 'generate_lead'); // if (window.ttq) ttq.track('SubmitForm'); setStatus('success'); } else if (resp.status === 409) { // E-mail duplicado: tratamos como sucesso (a pessoa já está na lista). setStatus('success'); } else { setServerError(data.erro || t('lp_err_generic')); setStatus('error'); } } catch (_) { setServerError(t('lp_err_generic')); setStatus('error'); } }; // ── Estado de SUCESSO: "Você está na lista!" + comunidade ── if (status === 'success') { return (

{t('lp_success_title_l1')}
{t('lp_success_title_l2')}

{t('lp_success_body')}

INSTAGRAM FACEBOOK {/* DISCORD e WHATSAPP — ocultos até termos os links reais (evita botão -> "#"). Para reativar: descomente abaixo e troque o href="#" pelo link real, mantendo target/rel. {t('lp_cta_discord')} {t('lp_cta_whatsapp_group')} */}

{t('lp_success_share')}

); } const sending = status === 'sending'; return (
{/* E-MAIL (obrigatório) */} {/* WHATSAPP (opcional) — intl-tel-input adiciona o seletor de país c/ bandeira */} {/* PAÍS (obrigatório) */} {/* CONSENTIMENTO (obrigatório) */} {errors.consent && {errors.consent}} {/* SUGESTÃO (opcional) — feedback pré-lançamento; máx. 500 caracteres */}