/* ============== Shared components & utilities ============== */ const { useState, useEffect, useRef, useMemo, useCallback } = React; /* ---- Smooth scroll to id ---- */ function scrollToId(id) { const el = document.getElementById(id); if (!el) return; const y = el.getBoundingClientRect().top + window.scrollY - 72; window.scrollTo({ top: y, behavior: 'smooth' }); } /* ---- Icon helpers (inline SVGs, simple) ---- */ const Icon = { ArrowRight: (p) => ( ), ArrowUpRight: (p) => ( ), Check: (p) => ( ), X: (p) => ( ), Phone: (p) => ( ), Menu: (p) => ( ), Star: (p) => ( ), Sparkles: (p) => ( ), Shield: (p) => ( ), Chart: (p) => ( ), Users: (p) => ( ), Bot: (p) => ( ), Doc: (p) => ( ), Zap: (p) => ( ), }; /* ---- Button component ---- */ function Button({ variant = 'primary', size, children, onClick, type = 'button', arrow = false, className = '', ...rest }) { const v = `btn-${variant}`; return ( ); } /* ---- Input ---- */ function Input({ label, value, onChange, error, type = 'text', placeholder, required, dark }) { return (
{label && } {error && {error}}
); } /* ---- Reveal-on-scroll wrapper ---- */ function Reveal({ children, delay = 0, as: As = 'div', className = '', style = {}, ...rest }) { const ref = useRef(null); const [shown, setShown] = useState(false); useEffect(() => { const el = ref.current; if (!el) return; const io = new IntersectionObserver(([e]) => { if (e.isIntersecting) { setShown(true); io.disconnect(); } }, { threshold: 0.12 }); io.observe(el); return () => io.disconnect(); }, []); return ( {children} ); } /* ---- Validation helpers ---- */ function validatePhone(v) { if (!v) return 'Введите телефон'; const digits = v.replace(/\D/g, ''); if (digits.length < 10) return 'Минимум 10 цифр'; return null; } function validateName(v) { if (!v || v.trim().length < 2) return 'Введите имя'; return null; } function formatPhone(input) { const digits = input.replace(/\D/g, '').slice(0, 11); if (!digits) return ''; let out = '+7'; const d = digits.startsWith('7') || digits.startsWith('8') ? digits.slice(1) : digits; if (d.length > 0) out += ' (' + d.slice(0, 3); if (d.length >= 3) out += ') ' + d.slice(3, 6); if (d.length >= 6) out += '-' + d.slice(6, 8); if (d.length >= 8) out += '-' + d.slice(8, 10); return out; } /* ---- Form (reusable, with validation + success state) ---- */ function LeadForm({ id, fields, button, onSubmit, dark, columns = 4, small }) { const init = useMemo(() => Object.fromEntries(fields.map(f => [f.key, ''])), [fields]); const [values, setValues] = useState(init); const [errors, setErrors] = useState({}); const [done, setDone] = useState(false); const setField = (key) => (e) => { let v = e.target.value; if (key === 'phone') v = formatPhone(v); setValues(s => ({ ...s, [key]: v })); if (errors[key]) setErrors(s => ({ ...s, [key]: null })); }; const submit = (e) => { e.preventDefault(); const errs = {}; fields.forEach(f => { const v = values[f.key]; if (f.validate === 'phone') { const err = validatePhone(v); if (err) errs[f.key] = err; } else if (f.validate === 'name') { const err = validateName(v); if (err) errs[f.key] = err; } else if (f.required && !v) { errs[f.key] = 'Заполните поле'; } }); setErrors(errs); if (Object.keys(errs).length === 0) { // отправка заявки в Google-таблицу (в фоне, не блокирует UX) fetch('https://script.google.com/macros/s/AKfycbzlfBbOI8lzvn2xctYuejSwbnZredO5saC0v0rqdQ9PTqZNLkyOg8HQBRndM1Pr2CDm/exec', { method: 'POST', mode: 'no-cors', headers: { 'Content-Type': 'text/plain;charset=utf-8' }, body: JSON.stringify({ ...values, page: location.pathname }) }).catch(function(){}); // отправка заявки в Битрикс24 (воронка Avito) через серверный эндпоинт fetch('/wp-json/formula/v1/lead', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...values, page: location.pathname, calc: window.FormulaCalc || '' }) }).catch(function(){}); setDone(true); onSubmit && onSubmit(values); } }; if (done) { return (
Заявка принята
Перезвоним в течение 30 минут в рабочее время
); } return (
{fields.map(f => ( ))}

Отправляя данные, вы соглашаетесь на обработку персональных данных согласно{' '} Политике конфиденциальности .

); } /* ---- Animated number ---- */ function AnimNumber({ value, format = (n) => n.toLocaleString('ru-RU'), duration = 1400 }) { const ref = useRef(null); const [display, setDisplay] = useState(0); const startedRef = useRef(false); useEffect(() => { const el = ref.current; if (!el) return; const io = new IntersectionObserver(([e]) => { if (e.isIntersecting && !startedRef.current) { startedRef.current = true; const startT = performance.now(); const tick = (now) => { const t = Math.min(1, (now - startT) / duration); const eased = 1 - Math.pow(1 - t, 3); setDisplay(Math.round(value * eased)); if (t < 1) requestAnimationFrame(tick); }; requestAnimationFrame(tick); } }, { threshold: 0.3 }); io.observe(el); return () => io.disconnect(); }, [value, duration]); return {format(display)}; } /* ---- export to window ---- */ Object.assign(window, { scrollToId, Icon, Button, Input, Reveal, LeadForm, AnimNumber, });