// survey.jsx — встроенный интерактивный опрос function Survey({ visible }) { if (!visible) return null; const schema = window.SURVEY_SCHEMA; const isMiniApp = Boolean(window.__TG_INIT_DATA); const questions = isMiniApp ? schema.questions.filter((q) => q.kind !== "contact") : schema.questions; const sections = schema.sections.reduce((acc, section) => ({ ...acc, [section.id]: section }), {}); const draftKey = `ekokrug-survey-draft-${schema.schema_version}`; const params = new URLSearchParams(window.location.search); const startParam = params.get("tgWebAppStartParam") || window.__TG_WEB_APP?.initDataUnsafe?.start_param || ""; const source = params.get("source") || (isMiniApp ? "tg" : ""); const [step, setStep] = React.useState(0); const [answers, setAnswers] = React.useState(() => { try { return JSON.parse(localStorage.getItem(draftKey) || "{}"); } catch (e) { return {}; } }); const [website, setWebsite] = React.useState(""); // honeypot const [sending, setSending] = React.useState(false); const [totalResponses, setTotalResponses] = React.useState(null); const total = questions.length; React.useEffect(() => { localStorage.setItem(draftKey, JSON.stringify(publicAnswers())); }, [answers]); React.useEffect(() => { if (step !== 0) return; fetch("/api/results.php") .then((r) => r.json()) .then((d) => { if (d && d.ok) setTotalResponses(d.count); }) .catch(() => {}); }, [step]); const setAns = (id, v) => setAnswers((a) => ({ ...a, [id]: v })); const getContact = () => { const raw = answers.contact || {}; if (raw.intent === "Пока без контакта") return null; if (!raw.value || !raw.value.trim()) return null; return { intent: raw.intent || "Хочу получить итоги", value: raw.value.trim() }; }; const publicAnswers = () => { const copy = { ...answers }; delete copy.contact; return copy; }; const submit = async () => { setSending(true); try { const headers = { "Content-Type": "application/json" }; if (window.__TG_INIT_DATA) { headers["X-Telegram-Init-Data"] = window.__TG_INIT_DATA; } await fetch("/api/survey.php", { method: "POST", headers, body: JSON.stringify({ schema_version: schema.schema_version, source, start_param: startParam, answers: publicAnswers(), contact: getContact(), website }) }); localStorage.removeItem(draftKey); } catch (e) { // Всё равно показываем спасибо: не оставляем человека в тупике. } setSending(false); setStep(total + 1); }; const next = () => { if (step === total) { submit(); return; } setStep((s) => Math.min(s + 1, total + 1)); }; const prev = () => setStep((s) => Math.max(s - 1, 0)); const progress = step === 0 ? 0 : step > total ? 100 : Math.round(step / total * 100); if (step === 0) { return (
{schema.intro.eyebrow}

{schema.intro.heading}

{schema.intro.lead}

{schema.title}

{total} вопросов. Можно отвечать коротко, но честно. Если вопрос не про вас — так и напишите.

{isMiniApp &&

Контакт возьмём из Telegram автоматически, вручную вводить его не нужно.

}
{schema.intro.promises.map((item) =>
{item.title}{item.text}
)}
{totalResponses !== null && totalResponses > 0 &&
{totalResponses} {ruPersonCount(totalResponses)} уже {ruSharedVerb(totalResponses)} опытом
}
); } if (step > total) { return (

Спасибо!

Ваши ответы помогут сделать ЭкоКруг ближе к реальной жизни. Мы используем результаты только в обобщённом виде, а контакты не публикуем.

{isMiniApp ? : Посмотреть результаты }
); } const idx = step - 1; const q = questions[idx]; const a = answers[q.id]; const section = sections[q.section]; const canNext = canAnswer(q, a); return (
Вопрос {step} из {total}
{section &&
{section.title} {section.subtitle}
}
{String(step).padStart(2, "0")}

{q.q}

{q.hint &&

{q.hint}

} {q.kind === "single" && } {q.kind === "multi" && } {q.kind === "text" && } {q.kind === "contact" && }
setWebsite(e.target.value)} style={{ position: "absolute", left: "-9999px", width: 1, height: 1, opacity: 0 }} aria-hidden="true" />
); } function SingleQuestion({ q, value, setAns, next }) { return (
{q.opts.map((opt) => )}
); } function MultiQuestion({ q, value, otherValue, setAns }) { const selected = Array.isArray(value) ? value : []; const otherKey = `${q.id}_other`; return ( <>
{q.opts.map((opt) => { const sel = selected.includes(opt); return ( ); })}
{q.other && selected.includes("Другое") && setAns(otherKey, e.target.value)} /> } ); } function TextQuestion({ q, value, setAns }) { return (