// results-widget.jsx — живой виджет результатов опроса const MIN_RESULTS_TO_SHOW = 5; function ResultsWidget({ visible }) { if (!visible) return null; const [data, setData] = React.useState(null); const [error, setError] = React.useState(false); const [refreshAt, setRefreshAt] = React.useState(Date.now()); React.useEffect(() => { let alive = true; fetch("/api/results.php") .then(r => r.json()) .then(d => { if (alive && d && d.ok) setData(d); else if (alive) setError(true); }) .catch(() => { if (alive) setError(true); }); return () => { alive = false; }; }, [refreshAt]); // Авто-обновление раз в 60 секунд (полезно во время защиты) React.useEffect(() => { const id = setInterval(() => setRefreshAt(Date.now()), 60_000); return () => clearInterval(id); }, []); // Время последнего обновления — относительное const updatedHuman = data?.updated ? humanizeTime(data.updated) : null; const total = data?.count || 0; const words = data?.top_words || []; const choices = data?.choices || {}; const maxCount = words.length ? words[0].count : 1; const choiceCards = [ { id: "q1", title: "Покупали удобрения" }, { id: "q2", title: "Траты за сезон" }, { id: "q3", title: "Куда уходят очистки" }, { id: "q4", title: "Где живут" } ].filter((card) => choices[card.id] && choices[card.id].length); if (!error && data === null) return null; if (!error && total < MIN_RESULTS_TO_SHOW) return null; return (
живые результаты

Что отвечают
прямо сейчас

Опрос идёт в открытом доступе. Здесь — счётчик ответов и самые частые слова из открытых полей, агрегированные по всему массиву. Виджет обновляется автоматически — раз в минуту.

{error && (

Не удалось загрузить результаты. Это значит, что бэкенд недоступен — или вы открыли локальную версию сайта. На ekokrug.ru виджет работает.

)} {!error && ( <>
{/* Большой счётчик */}
всего ответов
{data === null ? "—" : total.toLocaleString("ru-RU")}
{data === null ? ( загружаем… ) : total === 0 ? ( Ждём первых ответов ) : ( обновлено {updatedHuman || "только что"} )}
{/* Облако тегов из открытых полей */}
слова, которые повторялись
из открытых ответов
{data === null && (
{[34,52,28,46,40,22,48,30,36,26].map((w, i) => ( ))}
)} {data && words.length === 0 && (

Пока недостаточно ответов с открытым текстом, чтобы выделить частые темы. Виджет автоматически наполнится по мере поступления.

Пройти опрос
)} {data && words.length > 0 && (
{words.map((w, i) => { const t = w.count / maxCount; // 0..1 const size = 14 + Math.round(t * 30); // 14..44px const weight = t > 0.6 ? 600 : t > 0.3 ? 500 : 400; const opacity = 0.55 + t * 0.45; return ( {w.word} {w.count} ); })}
)}
{choiceCards.length > 0 && (
{choiceCards.map((card) => ( ))}
)} )}
анонимные данные обновление каждые 60 сек · итоги — через 2 недели → присоединиться к опросу
); } // Возвращает «12 минут назад», «вчера в 14:30» и т.п. function humanizeTime(iso) { try { const t = new Date(iso).getTime(); if (isNaN(t)) return null; const diff = (Date.now() - t) / 1000; if (diff < 60) return "только что"; if (diff < 3600) return `${Math.floor(diff / 60)} мин назад`; if (diff < 86400) return `${Math.floor(diff / 3600)} ч назад`; const d = new Date(iso); return d.toLocaleDateString("ru-RU", { day: "2-digit", month: "short" }); } catch (e) { return null; } } window.ResultsWidget = ResultsWidget; function ChoiceCard({ title, rows, total }) { const max = rows.length ? rows[0].count : 1; return (
{title}
{rows.slice(0, 5).map((row) => { const pct = total > 0 ? Math.round((row.count / total) * 100) : 0; const width = (row.count / max) * 100; return (
{row.label} {pct}%
); })}
); }