Реализация квиза/опроса на сайте

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.
Разработка и обслуживание любых видов сайтов:
Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Реализация квиза/опроса на сайте
Средняя
~5 рабочих дней
Часто задаваемые вопросы
Наши компетенции:
Этапы разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1214
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    852
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    823
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    815

Реализация опросов и квизов

Квизы и опросы собирают обратную связь, проводят тестирования, помогают пользователям выбрать продукт. Ключевые требования: гибкая структура вопросов, поддержка разных типов ответов, отображение результатов.

Структура базы данных

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
);

Laravel 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: компонент квиза

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;
  }
}

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

// Агрегация ответов для отчёта
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, условными вопросами: 5–7 дней.