multy-stub/server/routers/questioneer/public/static/js/poll.js
Primakov Alexandr Alexandrovich 1fcc5ed70d init Questionnaire
2025-03-11 23:50:50 +03:00

1069 lines
38 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* global $, window, document, showAlert */
$(document).ready(function() {
const publicLink = window.location.pathname.split('/').pop();
let questionnaireData = null;
// Элементы DOM
const loadingEl = $('#loading');
const containerEl = $('#questionnaire-container');
const titleEl = $('#questionnaire-title');
const descriptionEl = $('#questionnaire-description');
const questionsContainerEl = $('#questions-container');
const formEl = $('#poll-form');
const resultsContainerEl = $('#results-container');
const pollResultsContainerEl = $('#poll-results-container');
// Элементы навигации для пошаговых опросов
const navigationControlsEl = $('#navigation-controls');
const prevButtonEl = $('#prev-question');
const nextButtonEl = $('#next-question');
const questionCounterEl = $('#question-counter');
const submitButtonEl = $('#submit-button');
// Для пошаговых опросов
let currentQuestionIndex = 0;
// Проверка доступности localStorage
const isLocalStorageAvailable = () => {
try {
const testKey = 'test';
window.localStorage.setItem(testKey, testKey);
window.localStorage.removeItem(testKey);
return true;
} catch (e) {
return false;
}
};
// Ключ для localStorage
const getLocalStorageKey = () => `questionnaire_${publicLink}_completed`;
// Проверка на повторное прохождение опроса
const checkIfAlreadyCompleted = () => {
if (!isLocalStorageAvailable()) return false;
return window.localStorage.getItem(getLocalStorageKey()) === 'true';
};
// Сохранение информации о прохождении опроса
const markAsCompleted = () => {
if (!isLocalStorageAvailable()) return;
window.localStorage.setItem(getLocalStorageKey(), 'true');
};
// Получаем базовый путь API
const getApiPath = () => {
const pathParts = window.location.pathname.split('/');
// Убираем последние две части пути (poll/:publicLink)
pathParts.pop();
pathParts.pop();
return pathParts.join('/') + '/api';
};
// Загрузка данных опроса
const loadQuestionnaire = () => {
$.ajax({
url: `${getApiPath()}/questionnaires/public/${publicLink}`,
method: 'GET',
success: function(result) {
if (result.success) {
questionnaireData = result.data;
// Проверяем, проходил ли пользователь уже этот опрос
if (checkIfAlreadyCompleted()) {
showAlreadyCompletedMessage();
} else {
showWelcomeAnimation();
}
} else {
loadingEl.text(`Ошибка: ${result.error}`);
}
},
error: function(error) {
console.error('Error loading questionnaire:', error);
loadingEl.text('Не удалось загрузить опрос. Пожалуйста, попробуйте позже.');
}
});
};
// Показываем анимацию приветствия
const showWelcomeAnimation = () => {
// Скрываем индикатор загрузки
loadingEl.hide();
// Создаем элемент приветствия
const $welcomeEl = $('<div>', {
id: 'welcome-animation',
class: 'welcome-animation',
css: {
opacity: 0,
transform: 'translateY(20px)'
}
});
const $welcomeIcon = $('<div>', {
class: 'welcome-icon',
html: '<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" fill="currentColor" viewBox="0 0 16 16"><path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/><path d="M4.285 9.567a.5.5 0 0 1 .683.183A3.498 3.498 0 0 0 8 11.5a3.498 3.498 0 0 0 3.032-1.75.5.5 0 1 1 .866.5A4.498 4.498 0 0 1 8 12.5a4.498 4.498 0 0 1-3.898-2.25.5.5 0 0 1 .183-.683zM7 6.5C7 7.328 6.552 8 6 8s-1-.672-1-1.5S5.448 5 6 5s1 .672 1 1.5zm4 0c0 .828-.448 1.5-1 1.5s-1-.672-1-1.5S9.448 5 10 5s1 .672 1 1.5z"/></svg>'
});
const $welcomeTitle = $('<h2>', {
text: `Добро пожаловать в опрос "${questionnaireData.title}"`,
class: 'welcome-title'
});
const $welcomeDescription = $('<p>', {
text: questionnaireData.description || 'Пожалуйста, ответьте на следующие вопросы:',
class: 'welcome-description'
});
const $startButton = $('<button>', {
type: 'button',
text: 'Начать опрос',
class: 'btn btn-primary welcome-start-btn'
});
$startButton.on('click', function() {
$welcomeEl.css({
opacity: 0,
transform: 'translateY(-20px)'
});
setTimeout(() => {
$welcomeEl.remove();
renderQuestionnaire();
}, 500);
});
$welcomeEl.append($welcomeIcon, $welcomeTitle, $welcomeDescription, $startButton);
// Добавляем приветствие перед контейнером опроса
containerEl.before($welcomeEl);
// Анимируем появление
setTimeout(() => {
$welcomeEl.css({
opacity: 1,
transform: 'translateY(0)'
});
}, 100);
};
// Показываем сообщение о том, что опрос уже пройден
const showAlreadyCompletedMessage = () => {
loadingEl.hide();
const $completedEl = $('<div>', {
class: 'already-completed',
css: {
opacity: 0,
transform: 'translateY(20px)'
}
});
const $completedIcon = $('<div>', {
class: 'completed-icon',
html: '<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" fill="currentColor" viewBox="0 0 16 16"><path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/><path d="M10.97 4.97a.235.235 0 0 0-.02.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-1.071-1.05z"/></svg>'
});
const $completedTitle = $('<h2>', {
text: 'Вы уже прошли этот опрос',
class: 'completed-title'
});
const $completedDescription = $('<p>', {
text: 'Спасибо за ваше участие. Вы уже отправили свои ответы на этот опрос.',
class: 'completed-description'
});
const $viewResultsButton = $('<button>', {
type: 'button',
text: 'Посмотреть результаты',
class: 'btn btn-primary view-results-btn'
});
const $startAgainButton = $('<button>', {
type: 'button',
text: 'Пройти снова',
class: 'btn btn-secondary start-again-btn'
});
$viewResultsButton.on('click', function() {
$completedEl.remove();
containerEl.show();
formEl.hide();
resultsContainerEl.show();
loadPollResults();
});
$startAgainButton.on('click', function() {
if (isLocalStorageAvailable()) {
window.localStorage.removeItem(getLocalStorageKey());
}
$completedEl.remove();
showWelcomeAnimation();
});
$completedEl.append($completedIcon, $completedTitle, $completedDescription, $viewResultsButton, $startAgainButton);
containerEl.before($completedEl);
setTimeout(() => {
$completedEl.css({
opacity: 1,
transform: 'translateY(0)'
});
}, 100);
};
// Отображение опроса
const renderQuestionnaire = () => {
titleEl.text(questionnaireData.title);
descriptionEl.text(questionnaireData.description || '');
// Удаляем кнопку "Отправить ответы", так как есть кнопка "Отправить" в навигации
formEl.find('.form-actions').remove();
// Добавляем навигацию для пошагового опроса
formEl.append(`
<div class="step-navigation">
<button type="button" id="prev-question" class="btn btn-secondary nav-btn">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z"/>
</svg>
Назад
</button>
<span id="question-counter">Вопрос 1 из ${questionnaireData.questions.length}</span>
<button type="button" id="next-question" class="btn nav-btn">
Далее
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z"/>
</svg>
</button>
</div>
`);
// Обработчики для навигации
$('#prev-question').on('click', prevQuestion);
$('#next-question').on('click', nextQuestion);
// Показываем только первый вопрос
currentQuestionIndex = 0;
renderCurrentQuestion();
// Показываем контейнер с данными
containerEl.show();
};
// Отображение текущего вопроса в пошаговом режиме
const renderCurrentQuestion = () => {
// Очищаем контейнер вопросов
questionsContainerEl.empty();
// Получаем текущий вопрос
const currentQuestion = questionnaireData.questions[currentQuestionIndex];
// Отображаем вопрос
renderQuestion(questionsContainerEl, currentQuestion, currentQuestionIndex);
// Добавляем классы для анимации
const $currentQuestionEl = questionsContainerEl.find(`.question-item[data-index="${currentQuestionIndex}"]`);
$currentQuestionEl.addClass('enter');
// Запускаем анимацию появления после небольшой задержки
setTimeout(() => {
$currentQuestionEl.removeClass('enter').addClass('active');
}, 50);
// Обновляем счетчик вопросов
questionCounterEl.addClass('update');
questionCounterEl.text(`Вопрос ${currentQuestionIndex + 1} из ${questionnaireData.questions.length}`);
// Снимаем класс анимации счетчика
setTimeout(() => {
questionCounterEl.removeClass('update');
}, 500);
// Управляем состоянием кнопок навигации
prevButtonEl.prop('disabled', currentQuestionIndex === 0);
nextButtonEl.text(
currentQuestionIndex === questionnaireData.questions.length - 1
? 'Отправить'
: 'Далее'
);
};
// Переход к предыдущему вопросу
const prevQuestion = function() {
if (currentQuestionIndex > 0) {
currentQuestionIndex--;
renderCurrentQuestion();
}
};
// Переход к следующему вопросу или завершение опроса
const nextQuestion = function() {
const currentQuestion = questionnaireData.questions[currentQuestionIndex];
// Проверяем, что на текущий вопрос есть ответ, если он обязательный
if (currentQuestion.required && !checkQuestionAnswer(currentQuestion, currentQuestionIndex)) {
// Добавляем класс ошибки к текущему вопросу
const $currentQuestionEl = questionsContainerEl.find(`.question-item[data-index="${currentQuestionIndex}"]`);
$currentQuestionEl.addClass('error shake');
// Удаляем класс ошибки после окончания анимации
setTimeout(() => {
$currentQuestionEl.removeClass('shake');
}, 500);
return;
}
// Если есть еще вопросы, переходим к следующему
if (currentQuestionIndex < questionnaireData.questions.length - 1) {
// Анимируем текущий вопрос перед переходом к следующему
questionsContainerEl.find(`.question-item[data-index="${currentQuestionIndex}"]`).addClass('exit');
// Увеличиваем индекс текущего вопроса
currentQuestionIndex++;
// Рендерим новый вопрос после небольшой задержки
setTimeout(() => {
renderCurrentQuestion();
}, 200);
} else {
// Анимируем контейнер перед отправкой формы
questionsContainerEl.find('.question-item').addClass('exit');
// Последний вопрос, отправляем форму
setTimeout(() => {
// Отключаем атрибут required у невидимых полей перед отправкой
$('input[required]:not(:visible), textarea[required]:not(:visible)').prop('required', false);
submitForm();
}, 300);
}
};
// Проверка наличия ответа на вопрос
const checkQuestionAnswer = (question, questionIndex) => {
switch (question.type) {
case 'single_choice':
return $(`.question-item[data-index="${questionIndex}"] input[name="question_${questionIndex}"]:checked`).length > 0;
case 'multiple_choice':
return $(`.question-item[data-index="${questionIndex}"] input[type="checkbox"]:checked:not(.multiple-choice-validation)`).length > 0;
case 'text':
return $(`.question-item[data-index="${questionIndex}"] textarea`).val().trim() !== '';
case 'scale':
return $(`.question-item[data-index="${questionIndex}"] input[name="question_${questionIndex}"]:checked`).length > 0;
case 'tag_cloud':
return $(`.question-item[data-index="${questionIndex}"] .tag-item.selected`).length > 0;
default:
return true;
}
};
// Отображение всех вопросов (стандартный режим)
const renderQuestions = () => {
questionsContainerEl.empty();
$.each(questionnaireData.questions, function(questionIndex, question) {
renderQuestion(questionsContainerEl, question, questionIndex);
});
};
// Отображение одного вопроса
const renderQuestion = (container, question, questionIndex) => {
const $questionEl = $('<div>', {
class: 'question-item',
'data-index': questionIndex
});
const $questionTitle = $('<h3>', {
text: question.text,
class: 'question-title'
});
if (question.required) {
const $requiredMark = $('<span>', {
class: 'required-mark',
text: ' *'
});
$questionTitle.append($requiredMark);
}
$questionEl.append($questionTitle);
// Отображение вариантов ответа в зависимости от типа вопроса
switch (question.type) {
case 'single_choice':
renderSingleChoice($questionEl, question, questionIndex);
break;
case 'multiple_choice':
renderMultipleChoice($questionEl, question, questionIndex);
break;
case 'text':
renderTextAnswer($questionEl, question, questionIndex);
break;
case 'scale':
renderScale($questionEl, question, questionIndex);
break;
case 'rating': // Обратная совместимость для рейтинга
renderScale($questionEl, { ...question, scaleMax: 5 }, questionIndex);
break;
case 'tag_cloud':
renderTagCloud($questionEl, question, questionIndex);
break;
}
container.append($questionEl);
};
// Отображение вопроса с одиночным выбором
const renderSingleChoice = ($container, question, questionIndex) => {
const $optionsContainer = $('<div>', {
class: 'radio-options-container'
});
$.each(question.options, function(optionIndex, option) {
const $optionContainer = $('<div>', {
class: 'radio-option'
});
const $optionInput = $('<input>', {
type: 'radio',
id: `question_${questionIndex}_option_${optionIndex}`,
name: `question_${questionIndex}`,
value: optionIndex,
required: question.required
});
const $optionLabel = $('<label>', {
for: `question_${questionIndex}_option_${optionIndex}`,
text: option.text
});
$optionContainer.append($optionInput, $optionLabel);
$optionsContainer.append($optionContainer);
});
$container.append($optionsContainer);
};
// Отображение вопроса с множественным выбором
const renderMultipleChoice = ($container, question, questionIndex) => {
const $optionsContainer = $('<div>', {
class: 'checkbox-options-container'
});
// Скрытое поле для проверки заполнения обязательного вопроса с чекбоксами
if (question.required) {
const $validationCheckbox = $('<input>', {
type: 'checkbox',
class: 'multiple-choice-validation',
required: true,
name: `question_${questionIndex}_validation`,
style: 'display: none;',
id: `question_${questionIndex}_validation`
});
$optionsContainer.append($validationCheckbox);
}
$.each(question.options, function(optionIndex, option) {
const $optionContainer = $('<div>', {
class: 'checkbox-option'
});
const $optionInput = $('<input>', {
type: 'checkbox',
id: `question_${questionIndex}_option_${optionIndex}`,
name: `question_${questionIndex}_option_${optionIndex}`,
value: 'true'
});
const $optionLabel = $('<label>', {
for: `question_${questionIndex}_option_${optionIndex}`,
text: option.text
});
// Обработчик изменения значения чекбокса
if (question.required) {
$optionInput.on('change', function() {
const anyChecked = $optionsContainer.find('input[type="checkbox"]:checked:not(.multiple-choice-validation)').length > 0;
$(`#question_${questionIndex}_validation`).prop('checked', anyChecked);
});
}
$optionContainer.append($optionInput, $optionLabel);
$optionsContainer.append($optionContainer);
});
$container.append($optionsContainer);
};
// Отображение текстового вопроса
const renderTextAnswer = ($container, question, questionIndex) => {
const $textareaContainer = $('<div>', {
class: 'textarea-container'
});
const $textarea = $('<textarea>', {
id: `question_${questionIndex}_text`,
name: `question_${questionIndex}_text`,
rows: 4,
placeholder: 'Введите ваш ответ здесь...',
required: question.required
});
$textareaContainer.append($textarea);
$container.append($textareaContainer);
};
// Отображение вопроса с шкалой оценки
const renderScale = ($container, question, questionIndex) => {
const minValue = question.scaleMin || 0;
const maxValue = question.scaleMax || 10;
const minLabel = question.scaleMinLabel || 'Минимум';
const maxLabel = question.scaleMaxLabel || 'Максимум';
const $scaleContainer = $('<div>', {
class: 'scale-container'
});
const $scaleLabels = $('<div>', {
class: 'scale-labels'
});
$scaleLabels.append(
$('<span>', { class: 'scale-label-min', text: minLabel }),
$('<span>', { class: 'scale-label-max', text: maxLabel })
);
const $scaleValues = $('<div>', {
class: 'scale-values'
});
// Создаем кнопки для шкалы от min до max
for (let i = minValue; i <= maxValue; i++) {
const $scaleItem = $('<div>', {
class: 'scale-item'
});
const $scaleInput = $('<input>', {
type: 'radio',
id: `question_${questionIndex}_scale_${i}`,
name: `question_${questionIndex}`,
value: i,
required: question.required
});
const $scaleLabel = $('<label>', {
for: `question_${questionIndex}_scale_${i}`,
text: i
});
$scaleItem.append($scaleInput, $scaleLabel);
$scaleValues.append($scaleItem);
}
$scaleContainer.append($scaleLabels, $scaleValues);
$container.append($scaleContainer);
};
// Отображение вопроса с облаком тегов
const renderTagCloud = ($container, question, questionIndex) => {
const $tagCloudContainer = $('<div>', {
class: 'tag-cloud-container'
});
if (question.options && question.options.length > 0) {
// Предопределенное облако тегов
$.each(question.options, function(optionIndex, option) {
if (!option || !option.text) return; // Пропускаем пустые или невалидные опции
const $tagItem = $('<div>', {
class: 'tag-item',
text: option.text,
'data-index': optionIndex,
click: function() {
$(this).toggleClass('selected');
updateTagValidation();
}
});
$tagCloudContainer.append($tagItem);
});
} else {
// Пользовательское облако тегов
const $tagInputContainer = $('<div>', {
class: 'tag-input-container'
});
const $tagInput = $('<input>', {
type: 'text',
placeholder: 'Введите теги, разделенные пробелом, и нажмите Enter',
class: 'tag-input'
});
const $tagItems = $('<div>', {
class: 'tag-items'
});
// Обработчик ввода тегов
$tagInput.on('keypress', function(e) {
if (e.which === 13) { // Enter
e.preventDefault();
const inputText = $(this).val().trim();
if (inputText) {
// Разбиваем ввод на отдельные теги
const tags = inputText.split(/\s+/);
// Добавляем каждый тег как отдельный элемент
tags.forEach(tagText => {
if (tagText) {
const $tagItem = $('<div>', {
class: 'tag-item selected',
text: tagText
});
const $tagRemove = $('<span>', {
class: 'tag-remove',
html: '&times;',
click: function(e) {
e.stopPropagation();
$(this).parent().remove();
updateTagValidation();
}
});
$tagItem.append($tagRemove);
$tagItems.append($tagItem);
}
});
$(this).val('');
updateTagValidation();
}
}
});
$tagInputContainer.append($tagInput, $tagItems);
$tagCloudContainer.append($tagInputContainer);
}
// Скрытое поле для валидации
if (question.required) {
const $validationInput = $('<input>', {
type: 'text',
required: true,
style: 'display: none;',
id: `question_${questionIndex}_tag_validation`
});
$tagCloudContainer.append($validationInput);
}
$container.append($tagCloudContainer);
// Функция для обновления валидации тегов
function updateTagValidation() {
if (question.required) {
const hasSelectedTags = $tagCloudContainer.find('.tag-item.selected').length > 0;
$(`#question_${questionIndex}_tag_validation`).val(hasSelectedTags ? 'valid' : '');
}
}
};
// Показываем анимацию благодарности
const showThankYouAnimation = () => {
// Скрываем форму
formEl.hide();
// Создаем анимацию благодарности
const $thankYouEl = $('<div>', {
class: 'thank-you-animation',
css: {
opacity: 0,
transform: 'scale(0.9)'
}
});
const $thankYouIcon = $('<div>', {
class: 'thank-you-icon',
html: '<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" fill="currentColor" viewBox="0 0 16 16"><path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/><path d="M10.97 4.97a.235.235 0 0 0-.02.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-1.071-1.05z"/></svg>'
});
const $thankYouTitle = $('<h2>', {
text: 'Спасибо за участие!',
class: 'thank-you-title'
});
const $thankYouDescription = $('<p>', {
text: 'Ваши ответы были успешно записаны. Мы ценим ваше мнение!',
class: 'thank-you-description'
});
$thankYouEl.append($thankYouIcon, $thankYouTitle, $thankYouDescription);
// Добавляем анимацию перед результатами
resultsContainerEl.prepend($thankYouEl);
// Показываем контейнер с результатами
resultsContainerEl.show();
// Анимируем появление
setTimeout(() => {
$thankYouEl.css({
opacity: 1,
transform: 'scale(1)'
});
// Загружаем результаты через некоторое время
setTimeout(() => {
loadPollResults();
}, 1000);
}, 100);
};
// Отправка формы
const submitForm = function() {
// Отключаем атрибут required у невидимых полей перед отправкой
$('input[required]:not(:visible), textarea[required]:not(:visible)').prop('required', false);
// Собираем ответы
const answers = [];
$.each(questionnaireData.questions, function(questionIndex, question) {
const $questionEl = $(`.question-item[data-index="${questionIndex}"]`);
let optionIndices;
let selectedOption;
let selectedOptions;
let selectedTags;
let textAnswer;
let scaleValue;
switch (question.type) {
case 'single_choice':
selectedOption = $questionEl.find(`input[name="question_${questionIndex}"]:checked`);
if (selectedOption.length) {
answers.push({
questionIndex,
optionIndices: [parseInt(selectedOption.val())]
});
}
break;
case 'multiple_choice':
selectedOptions = $questionEl.find('input[type="checkbox"]:checked:not(.multiple-choice-validation)');
if (selectedOptions.length) {
optionIndices = $.map(selectedOptions, function(option) {
const nameParts = $(option).attr('name').split('_');
return parseInt(nameParts[nameParts.length - 1]);
});
answers.push({
questionIndex,
optionIndices
});
}
break;
case 'text':
textAnswer = $questionEl.find('textarea').val().trim();
if (textAnswer) {
answers.push({
questionIndex,
textAnswer
});
}
break;
case 'scale':
case 'rating': // Обратная совместимость
selectedOption = $questionEl.find(`input[name="question_${questionIndex}"]:checked`);
if (selectedOption.length) {
answers.push({
questionIndex,
scaleValue: parseInt(selectedOption.val())
});
}
break;
case 'tag_cloud':
selectedTags = $questionEl.find('.tag-item.selected');
if (selectedTags.length) {
const tagTexts = $.map(selectedTags, function(tag) {
return $(tag).text().replace('×', '').trim();
});
answers.push({
questionIndex,
tagTexts
});
}
break;
}
});
// Отправляем ответы на сервер
$.ajax({
url: `${getApiPath()}/vote/${publicLink}`,
method: 'POST',
contentType: 'application/json',
data: JSON.stringify({ answers }),
success: function(result) {
if (result.success) {
// Сохраняем информацию о прохождении опроса
markAsCompleted();
// Показываем анимацию благодарности вместо сразу скрытия формы
showThankYouAnimation();
} else {
showAlert(`Ошибка: ${result.error}`, 'Ошибка');
}
},
error: function(error) {
console.error('Error submitting poll:', error);
showAlert('Не удалось отправить ответы. Пожалуйста, попробуйте позже.', 'Ошибка');
}
});
};
// Загрузка результатов опроса
const loadPollResults = () => {
$.ajax({
url: `${getApiPath()}/results/${publicLink}`,
method: 'GET',
success: function(result) {
if (result.success) {
renderPollResults(result.data);
} else {
pollResultsContainerEl.html(`<p class="error">Ошибка: ${result.error}</p>`);
}
},
error: function(error) {
console.error('Error loading poll results:', error);
pollResultsContainerEl.html('<p class="error">Не удалось загрузить результаты. Пожалуйста, попробуйте позже.</p>');
}
});
};
// Отображение результатов опроса
const renderPollResults = (data) => {
pollResultsContainerEl.empty();
// Добавляем заголовок с анимацией
const $resultsTitle = $('<h2>', {
text: 'Результаты опроса',
class: 'results-title'
});
pollResultsContainerEl.append($resultsTitle);
// Отображаем результаты с отложенной анимацией
data.questions.forEach((question, index) => {
setTimeout(() => {
const $questionResult = $('<div>', {
class: 'question-result',
css: { opacity: 0, transform: 'translateY(20px)' }
});
const $questionTitle = $('<h3>', {
text: question.text,
class: 'question-title'
});
$questionResult.append($questionTitle);
// Отображаем результаты в зависимости от типа вопроса
switch (question.type) {
case 'single_choice':
case 'multiple_choice':
if (question.options && question.options.length > 0) {
// Находим общее количество голосов
const totalVotes = question.options.reduce((sum, option) => sum + (option.count || 0), 0);
if (totalVotes > 0) {
question.options.forEach((option) => {
const percent = totalVotes > 0 ? Math.round((option.count || 0) * 100 / totalVotes) : 0;
const $optionResult = $('<div>', {
class: 'result-bar-container'
});
const $optionLabel = $('<div>', {
class: 'result-label',
text: option.text
});
const $resultBar = $('<div>', {
class: 'result-bar'
});
const $resultBarFill = $('<div>', {
class: 'result-bar-fill',
css: { width: '0%' }
});
const $resultPercent = $('<div>', {
class: 'result-percent',
text: `${percent}% (${option.count || 0} голосов)`
});
$resultBar.append($resultBarFill);
$optionResult.append($optionLabel, $resultBar, $resultPercent);
$questionResult.append($optionResult);
// Анимируем заполнение полосы после добавления в DOM
setTimeout(() => {
$resultBarFill.css('width', `${percent}%`);
}, 100);
});
} else {
$questionResult.append($('<p>', {
text: 'Пока нет голосов для этого вопроса',
class: 'no-results'
}));
}
}
break;
case 'tag_cloud':
if (question.tags && question.tags.length > 0) {
const $tagCloud = $('<div>', {
class: 'results-tag-cloud'
});
// Находим максимальное количество для масштабирования
const maxCount = Math.max(...question.tags.map(tag => tag.count || 1));
question.tags.forEach((tag, tagIndex) => {
// Масштабируем размер тега от 1 до 3 в зависимости от частоты
const scale = maxCount > 1 ? 1 + (tag.count / maxCount) * 2 : 1;
const $tag = $('<div>', {
class: 'result-tag',
text: tag.text,
css: {
opacity: 0,
transform: 'scale(0.5)',
fontSize: `${scale}em`
}
});
$tagCloud.append($tag);
// Анимируем появление тега с задержкой
setTimeout(() => {
$tag.css({
opacity: 1,
transform: 'scale(1)'
});
}, tagIndex * 100);
});
$questionResult.append($tagCloud);
} else {
$questionResult.append($('<p>', {
text: 'Пока нет тегов для этого вопроса',
class: 'no-results'
}));
}
break;
case 'scale':
case 'rating':
if (question.scaleValues && question.scaleValues.length > 0) {
const average = question.scaleAverage ||
(question.scaleValues.reduce((sum, val) => sum + val, 0) / question.scaleValues.length);
const min = Math.min(...question.scaleValues);
const max = Math.max(...question.scaleValues);
const $scaleAverage = $('<div>', {
class: 'scale-average',
html: `Средняя оценка: <span class="highlight">${average.toFixed(1)}</span> (мин: ${min}, макс: ${max})`
});
$questionResult.append($scaleAverage);
} else {
$questionResult.append($('<p>', {
text: 'Пока нет оценок для этого вопроса',
class: 'no-results'
}));
}
break;
case 'text':
if (question.answers && question.answers.length > 0) {
const $answersContainer = $('<div>', {
class: 'text-answers-container'
});
question.answers.forEach((answer, answerIndex) => {
const $textAnswer = $('<div>', {
class: 'text-answer',
text: answer,
css: {
opacity: 0,
transform: 'translateX(20px)'
}
});
$answersContainer.append($textAnswer);
// Анимируем появление ответа с задержкой
setTimeout(() => {
$textAnswer.css({
opacity: 1,
transform: 'translateX(0)'
});
}, answerIndex * 200);
});
$questionResult.append($answersContainer);
} else {
$questionResult.append($('<p>', {
text: 'Пока нет текстовых ответов для этого вопроса',
class: 'no-results'
}));
}
break;
}
pollResultsContainerEl.append($questionResult);
// Анимируем появление блока результатов
setTimeout(() => {
$questionResult.css({
opacity: 1,
transform: 'translateY(0)'
});
}, 100);
}, index * 300); // Задержка между вопросами
});
// Если нет данных для отображения
if (!data.questions || data.questions.length === 0) {
pollResultsContainerEl.append($('<p>', {
text: 'Нет данных для отображения',
class: 'no-results'
}));
}
};
// Обработка отправки формы
formEl.on('submit', function(e) {
e.preventDefault();
// Всегда используем навигацию через кнопки в пошаговом режиме
return false;
});
// Инициализация
loadQuestionnaire();
});