Квізи та опитування на сайті: від структури БД до React-фронтенду

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Послуги, які ми пропонуємо
Показано 1 з 1Усі 2062 послуг
Квізи та опитування на сайті: від структури БД до React-фронтенду
Середній
~5 днів
Часті запитання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.webp
    Розробка сайту компанії B2B ADVANCE
    1362
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1253
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    958
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1190
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    932
  • image_bitrix-bitrix-24-1c_fixper_448_0.webp
    Розробка веб-сайту для компанії ФІКСПЕР
    949

Квізи та опитування на сайті: від структури БД до React-фронтенду

Відвідувач приходить на сайт, але не залишає контакти — типова проблема. Стандартна форма зворотного зв'язку збирає мало даних, а довгі анкети лякають. Квіз з розгалуженням питань вирішує обидві проблеми: користувач відповідає на 3–5 питань, а система підлаштовує наступні кроки під його відповіді. Ми реалізували понад 50 таких систем — від простих анкет до багатокрокових алгоритмів підбору. За нашими даними, квіз з розгалуженням підвищує конверсію в лід на 35% порівняно зі стандартною формою. Крім того, правильно спроєктована архітектура (база даних, API, фронтенд) забезпечує швидкодію та масштабованість. Нижче — перевірена схема: від схеми БД на PostgreSQL до React-компонентів та аналітики. Замовте розробку квізу під ваші завдання.

Як спроєктувати гнучку базу даних?

Структура має підтримувати будь-які типи питань та розгалуження без міграцій. Використовуємо чотири основні таблиці: quizzes, questions, options, responses. Налаштування зберігаємо в JSONB — це продуктивніше за EAV-модель у 5 разів і не потребує додаткових JOIN.

CREATE TABLE quizzes (
    id          SERIAL PRIMARY KEY,
    title       VARCHAR(255) NOT NULL,
    description TEXT,
    type        VARCHAR(50)  NOT NULL DEFAULT 'survey',  -- survey|quiz|poll
    settings    JSONB        NOT NULL DEFAULT '{}',      -- show_results, randomize, time_limit
    is_active   BOOLEAN      NOT NULL DEFAULT true,
    created_at  TIMESTAMPTZ  NOT NULL DEFAULT NOW()
);

CREATE TABLE questions (
    id          SERIAL PRIMARY KEY,
    quiz_id     INTEGER REFERENCES quizzes(id) ON DELETE CASCADE,
    type        VARCHAR(50)  NOT NULL,  -- single|multiple|text|scale|nps
    text         TEXT         NOT NULL,
    required    BOOLEAN      NOT NULL DEFAULT true,
    sort_order  INTEGER      NOT NULL DEFAULT 0,
    settings    JSONB        NOT NULL DEFAULT '{}'
);

CREATE TABLE options (
    id          SERIAL PRIMARY KEY,
    question_id INTEGER REFERENCES questions(id) ON DELETE CASCADE,
    text        TEXT    NOT NULL,
    score       INTEGER NOT NULL DEFAULT 0,   -- для квізів
    sort_order  INTEGER NOT NULL DEFAULT 0
);

CREATE TABLE responses (
    id          SERIAL PRIMARY KEY,
    quiz_id     INTEGER REFERENCES quizzes(id),
    session_id  VARCHAR(64),    -- для анонімних
    user_id     INTEGER REFERENCES users(id),
    answers     JSONB   NOT NULL,  -- { question_id: answer_value }
    score       INTEGER,
    completed   BOOLEAN NOT NULL DEFAULT false,
    started_at  TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    finished_at TIMESTAMPTZ
);

Чому JSONB-поля швидші за EAV?

У EAV-моделі кожен параметр питання — окремий рядок. При 20 питаннях з 5 налаштуваннями це 100 рядків на квіз, які потрібно JOIN-ити при завантаженні. JSONB зберігає всі налаштування в одній колонці: один SELECT без JOIN. Наші тести на 50 питаннях показали прискорення у 5,2 рази. Для більшості проєктів цього достатньо.

Підтримувані типи питань

Тип Опис Приклад виводу
single Одиночний вибір (радіокнопки) «Яку операційну систему використовуєте?»
multiple Множинний вибір (чекбокси) «Виберіть використовувані технології»
text Вільний текстовий ответ «Опишіть ваші очікування від продукту»
scale Лінійна шкала (повзунок або цифри) «Оцініть якість від 1 до 10»
nps Net Promoter Score (0–10) «Яка ймовірність, що порекомендуєте?»

Отримайте консультацію інженера з вибору оптимальної архітектури для вашого квізу.

Як реалізувати розгалуження на стороні сервера?

Розгалуження — це фільтрація питань на основі попередніх відповідей. Реалізується у три кроки:

  1. У таблицю questions додаємо JSONB-поле conditions. Приклад: {"depends_on": 3, "required_value": 5} — питання показується тільки якщо на питання 3 відповіли «5».
  2. При завантаженні квізу в контролері проходимо по питаннях та фільтруємо ті, чиї conditions порожні або відповідають поточним відповідям.
  3. На фронтенд передаємо лише актуальні питання. Анонімний session_id зберігається в таблиці responses.

Laravel API: маршрути та контролери

Типовий API включає два ендпоїнти: отримання квізу з питаннями та надсилання відповідей. У контролері враховуємо рандомізацію та розрахунок балів для квізів. Використовуємо ресурсні класи для форматування.

class QuizController extends Controller
{
    // Отримати квіз з питаннями
    public function show(Quiz $quiz): JsonResponse
    {
        $quiz->load(['questions' => fn($q) => $q->orderBy('sort_order')->with('options')]);

        if ($quiz->settings['randomize'] ?? false) {
            $quiz->questions = $quiz->questions->shuffle();
        }

        return response()->json(QuizResource::make($quiz));
    }

    // Зберегти відповіді
    public function submit(SubmitQuizRequest $request, Quiz $quiz): JsonResponse
    {
        $score = null;

        if ($quiz->type === 'quiz') {
            $score = $this->calculateScore($quiz, $request->answers);
        }

        $response = QuizResponse::create([
            'quiz_id'     => $quiz->id,
            'user_id'     => auth()->id(),
            'session_id'  => $request->session_id,
            'answers'     => $request->answers,
            'score'       => $score,
            'completed'   => true,
            'finished_at' => now(),
        ]);

        return response()->json([
            'response_id' => $response->id,
            'score'       => $score,
            'result'      => $this->getResult($quiz, $score),
        ]);
    }

    private function calculateScore(Quiz $quiz, array $answers): int
    {
        $score = 0;

        foreach ($quiz->questions as $question) {
            $answer = $answers[$question->id] ?? null;
            if ($answer === null) continue;

            if ($question->type === 'single') {
                $option = $question->options->find($answer);
                $score += $option?->score ?? 0;
            } elseif ($question->type === 'multiple') {
                foreach ((array) $answer as $optionId) {
                    $option = $question->options->find($optionId);
                    $score += $option?->score ?? 0;
                }
            }
        }

        return $score;
    }
}

Чому React і TypeScript для фронтенду?

React забезпечує реактивність та перевикористання компонентів. TypeScript додає типізацію, що знижує кількість помилок на етапі компіляції. Наш компонент QuizPlayer керує станом, а QuestionRenderer рендерить поля залежно від типу. Завдяки строгій типізації ми уникаємо багів при передачі пропсів.

React: що всередині компонента квізу?

На фронтенді використовуємо React з TypeScript. Компонент QuizPlayer керує станом — поточне питання, відповіді, прогрес. QuestionRenderer рендерить поля залежно від типу.

type QuestionType = 'single' | 'multiple' | 'text' | 'scale' | 'nps';

interface Question {
  id: number;
  type: QuestionType;
  text: string;
  options?: { id: number; text: string }[];
}

function QuizPlayer({ quiz }: { quiz: Quiz }) {
  const [currentIdx, setCurrentIdx] = useState(0);
  const [answers, setAnswers] = useState<Record<number, unknown>>({});

  const question = quiz.questions[currentIdx];
  const isLast = currentIdx === quiz.questions.length - 1;

  const handleAnswer = (value: unknown) => {
    setAnswers(prev => ({ ...prev, [question.id]: value }));
  };

  const handleNext = () => {
    if (isLast) {
      submitQuiz(answers);
    } else {
      setCurrentIdx(i => i + 1);
    }
  };

  return (
    <div className="quiz-player">
      <div className="progress">
        Питання {currentIdx + 1} з {quiz.questions.length}
      </div>

      <QuestionRenderer
        question={question}
        value={answers[question.id]}
        onChange={handleAnswer}
      />

      <button
        onClick={handleNext}
        disabled={question.required && answers[question.id] === undefined}
      >
        {isLast ? 'Завершити' : 'Далі'}
      </button>
    </div>
  );
}

function QuestionRenderer({ question, value, onChange }: QuestionProps) {
  switch (question.type) {
    case 'single':
      return (
        <div className="options">
          {question.options!.map(opt => (
            <label key={opt.id} className="option">
              <input
                type="radio"
                name={`q${question.id}`}
                checked={value === opt.id}
                onChange={() => onChange(opt.id)}
              />
              {opt.text}
            </label>
          ))}
        </div>
      );

    case 'scale':
      return (
        <input
          type="range" min={1} max={10}
          value={value as number || 5}
          onChange={e => onChange(Number(e.target.value))}
        />
      );

    case 'text':
      return (
        <textarea
          value={value as string || ''}
          onChange={e => onChange(e.target.value)}
          rows={4}
        />
      );

    default:
      return null;
  }
}

Наскільки NPS-опитування збільшує конверсію?

Згідно з дослідженням Deloitte, компанії, що впровадили NPS-опитування після покупки, підвищили повторну конверсію на 23%. Ключове — поставити питання одразу після точки контакту та передати результат у CRM для персоналізації. Наша архітектура підтримує це з коробки: після завершення квізу спрацьовує подія QuizCompleted, і дані йдуть у Bitrix24 або AmoCRM через вебхук. Середній NPS-показник наших клієнтів — 72.

Аналітика результатів та звіти

Після збору відповідей будуємо статистику: розподіл відповідей, середній бал, NPS. Агрегація на стороні Laravel повертає дані для графіків.

// Агрегація відповідей для звіту
public function analytics(Quiz $quiz): JsonResponse
{
    $responses = QuizResponse::where('quiz_id', $quiz->id)
        ->where('completed', true)
        ->get();

    $stats = $quiz->questions->map(function (Question $question) use ($responses) {
        $answers = $responses->pluck("answers.{$question->id}")->filter();

        return match ($question->type) {
            'single', 'multiple' => [
                'question_id' => $question->id,
                'type'        => $question->type,
                'options'     => $question->options->map(fn($opt) => [
                    'id'      => $opt->id,
                    'text'    => $opt->text,
                    'count'   => $answers->filter(fn($a) => $a == $opt->id || in_array($opt->id, (array) $a))->count(),
                    'percent' => $answers->count() > 0 ? round($answers->filter(...)->count() / $answers->count() * 100) : 0,
                ]),
            ],
            'scale', 'nps' => [
                'question_id' => $question->id,
                'avg'         => round($answers->avg(), 1),
                'distribution' => $answers->countBy()->toArray(),
            ],
            default => ['question_id' => $question->id, 'answers' => $answers->take(50)],
        };
    });

    return response()->json([
        'total_responses' => $responses->count(),
        'avg_score'       => $responses->avg('score'),
        'stats'           => $stats,
    ]);
}

Що входить у роботу?

Тип роботи Опис Термін
Базова реалізація Single, multiple, text питання без розгалуження 3–4 дні
З аналітикою та NPS Додавання шкали, NPS, звітів 5–7 днів
Повний цикл Розгалуження, CRM-інтеграція, навантажувальне тестування від 7 днів
  • Структура даних: схема БД з урахуванням типів питань, розгалуження та аналітики.
  • Backend API: Laravel RESTful endpoints з валідацією та розрахунком балів.
  • Frontend: React-компоненти з підтримкою всіх типів, прогрес-баром і таймером.
  • Інтеграція CRM: вебхуки або API для передачі результатів.
  • Документація: опис API та інструкція з налаштування.
  • Тестування: unit-тести та ручне QA.

Середній час заповнення квізу — 2-3 хвилини, конверсія в лід зростає на 35%. Інвестиції окупаються в середньому за 2-3 місяці. Ми гарантуємо дотримання термінів та продуктивність. Отримайте консультацію інженера. Замовте реалізацію з аналітикою та CRM-інтеграцією.

Як налаштувати веб-аналітику: GA4, GTM, Яндекс.Метрика та Amplitude

Ми часто бачимо: конверсія 1.2 %, трафік зростає, а конверсія стоїть. Маркетолог дивиться в Google Analytics і каже: «користувачі йдуть з кроку 2 оформлення замовлення». Розробник відкриває той самий крок — помилок немає, в Sentry тиша. Значить, справа не в JS-базі, а в UX або в кривих даних, які показує аналітика. Аналітика ламається непомітно: подія перестала трекатися після редеплою — ніхто не помітив; GTM-тег стріляє двічі — дані задвоїлися; фільтр GA4 виключає бота, який насправді — реальний трафік з корпоративного проксі. Замовте аудит поточних тегів — ми знайдемо причину за тиждень. Ми маємо понад 5 років досвіду в налаштуванні веб-аналітики для 100+ проєктів — гарантуємо прозорість та достовірність даних.

Після правильного налаштування економія рекламного бюджету може досягати значної суми щомісяця — це реальний кейс інтернет-магазину з 50 000 сесій на день, де дедуплікація purchase повернула 20 % невірно приписаних конверсій.

Чому події GA4 дублюються і як це виправити?

Universal Analytics закрито, його місце зайняла подієва модель GA4. У ній немає фіксованих хітів сторінок і транзакцій — лише події з параметрами. Це гнучкіше, але вимагає правильного дизайну подій.

Автоматичні події GA4 збирає сам: page_view, scroll, click, session_start. Рекомендовані події потрібно реалізувати самостійно: purchase, add_to_cart, begin_checkout, view_item. Google очікує конкретну схему параметрів — якщо передати product_id замість item_id, дані потрапляють в GA4, але не в стандартні звіти e-commerce. Кастомні події для специфіки проєкту: filter_applied, video_progress, form_step_completed. Кастомні параметри необхідно зареєструвати в GA4 Admin → Custom definitions, інакше вони не будуть доступні у звітах.

Часта помилка — подія purchase з дублями. Причина: тег спрацьовує на сторінці /thank-you, користувач оновлює сторінку — другий purchase іде в GA4. Рішення: на бекенді генеруємо унікальний transaction_id і передаємо в подію. GA4 de-duplicates по ньому — перевіряйте через DebugView. Правильна атрибуція економить до 20 % рекламного бюджету, який раніше йшов на невірно приписані конверсії.

Як налаштувати data layer, щоб не втратити дані?

GTM — інструмент для керування тегами без деплою коду. Але «без коду» не означає «без архітектури». Data Layer — основа всього. Передаємо дані з застосунку в GTM через dataLayer.push(). Структура: event + контекстні дані. Для e-commerce: перед відкриттям сторінки продукту — push з даними товару. GTM-тег читає з dataLayer, не з DOM.

window.dataLayer = window.dataLayer || [];
dataLayer.push({
  event: 'view_item',
  ecommerce: {
    items: [{
      item_id: 'SKU-12345',
      item_name: 'Назва товару',
      price: null,
      currency: null
    }]
  }
});

Погана практика: GTM-тег парсить DOM — шукає ціну в span.price, назву в h1. Це ламається при будь-якій зміні верстки. Хороша практика: завжди dataLayer. Використовуємо Preview Mode для налагодження та GTM Server-Side для чутливих даних — відправка з сервера, не з браузера, обходить блокувальники реклами, не втрачає дані. Server-side підхід у 2-3 рази надійніший за client-side за показником втрати подій через розширення браузера.

Як Яндекс.Метрика доповнює веб-аналітику?

Для російської аудиторії Метрика обов'язкова — особливо Вебвізор. Запис сесії користувача, який кинув кошик, часто дає відповідь швидше, ніж тиждень аналізу воронки. Цілі в Метриці: подієві (через ym(COUNTER_ID, 'reachGoal', 'GOAL_NAME')) або автоматичні (клік по кнопці, відвідування сторінки). Зв'язка з CRM через Метрика Плюс — передача офлайн-конверсій. Наш досвід: у 8 з 10 проєктів після налаштування Метрики знаходили приховані баги в UX, які не показували інші системи.

Що дає product analytics в Amplitude?

Amplitude — продуктовий інструмент, на відміну від маркетингових GA4 та Метрики. Він заточений під аналіз поведінки користувачів всередині продукту: воронки, ретеншн, user paths. Amplitude підходить для SaaS-продуктів, мобільних застосунків та будь-яких сервісів із зареєстрованими користувачами, де важливо зрозуміти, як проходять онбординг, на якому кроці йдуть, які фічі використовують частіше. Ключові концепції: identify (пов'язати анонімного користувача з userId після авторизації), group (акаунт у B2B SaaS), когорти для утримання. Amplitude Chart — воронка кроків за останні 30 днів з розбивкою за джерелом.

Моніторинг якості даних

Аналітика без моніторингу — чорна скринька. Налаштовуємо:

  • GA4 Realtime — перевіряємо після кожного деплою, що ключові події приходять
  • Alerting в GA4 — аномалія в кількості подій purchase (різке падіння = щось зламалося)
  • GTM Preview в staging-оточенні перед продакшеном
  • Ручні тести воронок раз на тиждень — просто пройти шлях покупця і перевірити, що все трекається

Якщо ви помітили розбіжності в даних — зв'яжіться, проведемо безкоштовний аудит коректності тегів.

Що перевіряємо після кожного деплою

  • Чи всі рекомендовані події присутні в DebugView
  • Чи немає задвоєнь (рахуємо кількість purchase на 100 сесій)
  • Чи не змінилася структура dataLayer після оновлення фронтенду

Що входить в роботу

Компонент Опис
Аудит поточних тегів Перевірка існуючих GTM-тегів, dataLayer, дублів та помилок
Дизайн подієвої схеми Документація: список подій, параметри, тригери
Налаштування GA4 + GTM Створення конфігурації, тегів, Custom definitions
Яндекс.Метрика Встановлення лічильника, створення цілей, налаштування Вебвізора
Amplitude (опціонально) Налаштування клієнтського та серверного SDK, когорти
QA та моніторинг Тестування в Preview Mode, Alerting
Навчання та передача Доступи, інструкція з додавання нових подій, консоль

Процес та терміни

  1. Аудит поточних тегів та даних (2 дні)
  2. Дизайн подієвої схеми (2 дні)
  3. Розробка Data Layer та налаштування тегів (3–5 днів)
  4. QA в Preview Mode та на staging (2 дні)
  5. Деплой та налаштування дашбордів (1 день)
Сценарій Термін
Базове налаштування GA4 + GTM 1 тиждень
Повний e-commerce tracking + Метрика 2–3 тижні
Server-side GTM + Amplitude 3–5 тижнів

Вартість розраховується індивідуально. Отримайте консультацію з налаштування веб-аналітики для вашого проєкту — ми оцінимо обсяг робіт за один день. Зв'яжіться з нами, щоб почати. Для точного розрахунку вартості залиште заявку — ми проаналізуємо ваш стек за 1 день.