Квізи та опитування на сайті: від структури БД до 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) | «Яка ймовірність, що порекомендуєте?» |
Отримайте консультацію інженера з вибору оптимальної архітектури для вашого квізу.
Як реалізувати розгалуження на стороні сервера?
Розгалуження — це фільтрація питань на основі попередніх відповідей. Реалізується у три кроки:
- У таблицю
questionsдодаємо JSONB-полеconditions. Приклад:{"depends_on": 3, "required_value": 5}— питання показується тільки якщо на питання 3 відповіли «5». - При завантаженні квізу в контролері проходимо по питаннях та фільтруємо ті, чиї
conditionsпорожні або відповідають поточним відповідям. - На фронтенд передаємо лише актуальні питання. Анонімний
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-інтеграцією.







