multy-stub/server/routers/questioneer/public/static/js/poll.js

1069 lines
38 KiB
JavaScript
Raw Normal View History

2025-03-11 23:50:50 +03:00
/* 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();
});